Compare commits

..

280 Commits

Author SHA1 Message Date
Harshavardhana b7c5e45fff heal: isObjectDangling should return false when it cannot decide (#14053)
In a multi-pool setup when disks are coming up, or in a single pool
setup let's say with 100's of erasure sets with a slow network.

It's possible when healing is attempted on `.minio.sys/config`
folder, it can lead to healing unexpectedly deleting some policy
files as dangling due to a mistake in understanding when `isObjectDangling`
is considered to be 'true'.

This issue happened in commit 30135eed86
when we assumed the validMeta with empty ErasureInfo is considered
to be fully dangling. This implementation issue gets exposed when
the server is starting up.

This is most easily seen with multiple-pool setups because of the
disconnected fashion pools that come up. The decision to purge the
object as dangling is taken incorrectly prior to the correct state
being achieved on each pool, when the corresponding drive let's say
returns 'errDiskNotFound', a 'delete' is triggered. At this point,
the 'drive' comes online because this is part of the startup sequence
as drives can come online lazily.

This kind of situation exists because we allow (totalDisks/2) number
of drives to be online when the server is being restarted.

Implementation made an incorrect assumption here leading to policies
getting deleted.

Added tests to capture the implementation requirements.
2022-01-07 19:11:54 -08:00
Aditya Manthramurthy 0a224654c2 fix: progagation of service accounts for site replication (#14054)
- Only non-root-owned service accounts are replicated for now.
- Add integration tests for OIDC with site replication
2022-01-07 17:41:43 -08:00
dharmendra kariya 47e4a36d7e update NOTES.txt in helm help (#14049) 2022-01-07 10:42:16 -08:00
Minio Trusted e420a1de4d Update yaml files to latest version RELEASE.2022-01-07T01-53-23Z 2022-01-07 05:57:36 +00:00
Aditya Manthramurthy 62dc0f7698 Update site replication docs (#14044)
- Now OpenID external IDP is also supported for site replication.

- IAM users/groups are also replicated
2022-01-06 17:53:23 -08:00
Aditya Manthramurthy 2d31d92271 Fix redigo dep to latest unretracted version (#14043)
To avoid error message like:

```
go: warning: github.com/gomodule/redigo@v2.0.0+incompatible: retracted by module author: Old development version not maintained or published.
go: to switch to the latest unretracted version, run:
	go get github.com/gomodule/redigo@latest
```
2022-01-06 16:00:31 -08:00
Aditya Manthramurthy 1981fe2072 Add internal IDP and OIDC users support for site-replication (#14041)
- This allows site-replication to be configured when using OpenID or the
  internal IDentity Provider.

- Internal IDP IAM users and groups will now be replicated to all members of the
  set of replicated sites.

- When using OpenID as the external identity provider, STS and service accounts
  are replicated.

- Currently this change dis-allows root service accounts from being
  replicated (TODO: discuss security implications).
2022-01-06 15:52:43 -08:00
Aditya Manthramurthy f68bd37acf Do not clean golangci-lint's cache (#14042)
- This speeds up running the linters during local development. With a fully
cached run, linter completes in 8 seconds.

- Any caching issues if present would be local and would not impact CI anyway
which always starts with a clean state.
2022-01-06 14:19:51 -08:00
Minio Trusted 76877eb6fa move gofumpt to golang-ci 2022-01-06 13:08:21 -08:00
Klaus Post 3d66d053c7 Add small client TLS PSK cache (#14039) 2022-01-06 11:34:02 -08:00
Harshavardhana 0d3ae3810f make sure to comply with MQTT spec (#14037)
- keep-alive cannot be 0 by default anymore
- client_id cannot be empty

fixes #13993
2022-01-06 11:25:39 -08:00
Klaus Post 0e31cff762 fix: DeleteMultipleObjects to finish even if cancelled + concurrent sets (#14038)
* Process sets concurrently.
* Disconnect context from request.
* Insert context cancellation checks.
* errFileNotFound and errFileVersionNotFound are ok, unless creating delete markers.
2022-01-06 10:47:49 -08:00
Shireesh Anjal c27110e37d Add timeinfo to health data (#14013)
Capture RoundtripDuration to figure out 
NTP issues in subnet health analyzer.
2022-01-06 01:51:10 -08:00
Harshavardhana 89441a22aa enforceRetentionForDeletion should return false early for delete-marker (#14033) 2022-01-05 17:05:28 -08:00
Minio Trusted 557135185c update helm to v3.4.5 2022-01-05 11:32:55 -08:00
Poorna 4d39fd4165 Add API for cluster replication status visibility (#13885) 2022-01-05 02:44:08 -08:00
Minio Trusted f4c03e56b8 Update yaml files to latest version RELEASE.2022-01-04T07-41-07Z 2022-01-04 23:01:29 +00:00
Minio Trusted d2b6aa9033 update console to v0.13.2 2022-01-03 23:41:07 -08:00
Minio Trusted 5dd40b9377 Update yaml files to latest version RELEASE.2022-01-03T18-22-58Z 2022-01-04 00:12:07 +00:00
Harshavardhana 001b77e7e1 use readConfig/saveConfig to simplify I/O on usage/tracker info (#14019) 2022-01-03 10:22:58 -08:00
Anis Elleuch 9d91d32d82 typo: Low capital in some JSON field names in log/audit output (#14020)
Use a low capital in some fields in JSON log/audit output to follow
other fields names.
2022-01-03 09:26:26 -08:00
Harshavardhana a60ac7ca17 fix: audit log to support object names in multipleObjectNames() handler (#14017) 2022-01-03 01:28:52 -08:00
Harshavardhana 42ba0da6b0 fix: initialize new drwMutex for each attempt in 'for {' loop. (#14009)
It is possible that GetLock() call remembers a previously
failed releaseAll() when there are networking issues, now
this state can have potential side effects.

This PR tries to avoid this side affect by making sure
to initialize NewNSLock() for each GetLock() attempts
made to avoid any prior state in the memory that can
interfere with the new lock grants.
2022-01-02 09:15:34 -08:00
Harshavardhana f527c708f2 run gofumpt cleanup across code-base (#14015) 2022-01-02 09:15:06 -08:00
Classmate Zhou 6f474982ed docs: rename 'baremetal' to 'bare metal' (#14014) 2022-01-01 08:39:02 -08:00
Minio Trusted fd6cd52728 update helm to v3.4.4 2021-12-30 20:21:24 -08:00
Minio Trusted c9e49f4366 Update yaml files to latest version RELEASE.2021-12-29T06-49-06Z 2021-12-29 07:38:12 +00:00
Harshavardhana 46fd9f4a53 fix: update storage-class properly
fixes #14005
2021-12-28 22:49:06 -08:00
Harshavardhana 4da641533d add compliance markdown 2021-12-28 15:59:11 -08:00
Harshavardhana 79df2c7ce7 correctly calculate read quorum based on the available fileInfo (#14000)
The current usage of assuming `default` parity of `4` is not correct
for all objects stored on MinIO, objects in .minio.sys have maximum
parity, healing won't trigger on these objects due to incorrect
verification of quorum.
2021-12-28 15:33:03 -08:00
Yoann Guillerme 3e28af1723 docs: update TLS doc use -keyout instead of -key (#14001) 2021-12-28 12:51:38 -08:00
Harshavardhana 866a95de38 fix: choose appropriate quorum for a given erasure set (#13998)
multiObject delete should honor expected quorum
2021-12-28 12:41:52 -08:00
Minio Trusted 6aa0574a53 Update yaml files to latest version RELEASE.2021-12-27T07-23-18Z 2021-12-27 19:52:13 +00:00
Minio Trusted bb97eafa82 madmin-go v1.1.23 and pkg v1.1.11 2021-12-26 23:23:18 -08:00
Minio Trusted 51d5efee1b update go v0.13.1 2021-12-26 22:36:49 -08:00
Harshavardhana c980804514 trim values from envrionment files (#13991)
trim values to remove any spaces, newlines
from the files while importing credentials
and other values.
2021-12-25 22:02:54 -08:00
Harshavardhana b883803b21 fix: healing across pools removing dangling objects (#13990)
adds other simplifications to the code when running
namespace heals across pools.
2021-12-25 09:01:44 -08:00
Harshavardhana 7e3a7d7044 add healing for invalid shards by skipping the blocks (#13978)
Built on top of #13945, now we need to simply skip the
shards and its automated.
2021-12-23 23:01:46 -08:00
Harshavardhana 9ad6012782 simplify logger time and avoid possible crashes (#13986)
time.Format() is not necessary prematurely for JSON
marshalling, since JSON marshalling indeed defaults
to RFC3339Nano.

This also ensures the 'time' is remembered until its
logged and it is the same time when the 'caller'
invoked 'log' functions.
2021-12-23 15:33:54 -08:00
Aditya Manthramurthy 5a96cbbeaa Fix user privilege escalation bug (#13976)
The AddUser() API endpoint was accepting a policy field. 
This API is used to update a user's secret key and account 
status, and allows a regular user to update their own secret key. 

The policy update is also applied though does not appear to 
be used by any existing client-side functionality.

This fix changes the accepted request body type and removes 
the ability to apply policy changes as that is possible via the 
policy set API.

NOTE: Changing passwords can be disabled as a workaround
for this issue by adding an explicit "Deny" rule to disable the API
for users.
2021-12-23 09:21:21 -08:00
Harshavardhana 416977436e rename MINIO_CACHE_.._MASTER_KEY to MINIO_CACHE_.._SECRET_KEY
fixes #13975
2021-12-22 12:11:07 -08:00
Harshavardhana 54ec0a1308 add configurable delta for skipping shards (#13967)
This PR is an attempt to make this configurable
as not all situations have same level of tolerable
delta, i.e disks are replaced days apart or even
hours.

There is also a possibility that nodes have drifted
in time, when NTP is not configured on the system.
2021-12-22 11:43:01 -08:00
Klaus Post ebd78e983f Limit key size to 3K (#13974)
User is reporting `Error 1071 :Specified key was too long,max key 
length is 3072 bytes`.

Regression caused by #13414
2021-12-22 11:41:51 -08:00
Harshavardhana 1cf726348f return meaningful error for disabled users (#13968)
fixes #13958
2021-12-22 11:40:21 -08:00
Harshavardhana 41f75e6d1b update helm to v3.4.3 2021-12-21 12:55:50 -08:00
Harshavardhana 0e3037631f skip inconsistent shards if possible (#13945)
data shards were wrong due to a healing bug
reported in #13803 mainly with unaligned object
sizes.

This PR is an attempt to automatically avoid
these shards, with available information about
the `xl.meta` and actually disk mtime.
2021-12-21 10:08:26 -08:00
Aditya Manthramurthy 6fbf4f96b6 Move last remaining IAM notification calls into IAMSys methods (#13941) 2021-12-21 02:16:50 -08:00
Minio Trusted e35709a99e update helm v3.4.2 2021-12-20 18:58:25 -08:00
Minio Trusted f3602d7d08 Update yaml files to latest version RELEASE.2021-12-20T22-07-16Z 2021-12-20 23:13:48 +00:00
Aditya Manthramurthy 526e10a2e0 Fix regression in STS permissions via group in internal IDP (#13955)
- When using MinIO's internal IDP, STS credential permissions did not check the
groups of a user.

- Also fix bug in policy checking in AccountInfo call
2021-12-20 14:07:16 -08:00
Harshavardhana 0b21734571 update helm chart link 2021-12-20 13:20:59 -08:00
Harshavardhana 499872f31d Add configurable channel queue_size for audit/logger webhook targets (#13819)
Also log all the missed events and logs instead of silently
swallowing the events.

Bonus: Extend the logger webhook to support mTLS
similar to audit webhook target.
2021-12-20 13:16:53 -08:00
Anis Elleuch 5cc16e098c env: Remove quotes when parsing a config env file (#13953)
The code parsing the config environment file does not remove 
quotes of environment variables values. This commit adds this 
capability.
2021-12-20 13:13:06 -08:00
Minio Trusted 364e27d5f2 add support for etcd secrets and relabel configs
helm release v3.4.1
2021-12-20 13:11:50 -08:00
Aditya Manthramurthy 1f4e0bd17c fix: access for root user's STS credential (#13947)
add a test to cover this case
2021-12-19 23:05:20 -08:00
Minio Trusted 0557e18472 update helm v3.4.0 2021-12-19 14:32:49 -08:00
Minio Trusted cfd66ab8c3 Update yaml files to latest version RELEASE.2021-12-18T04-42-33Z 2021-12-19 22:19:30 +00:00
Harshavardhana 691b763613 update console v0.12.9 2021-12-17 20:42:33 -08:00
Krishnan Parthasarathi 3ddb501190 Add docs for NewerNoncurrentVersions (#13944) 2021-12-17 19:32:29 -08:00
Aditya Manthramurthy 997e808088 fix; race in bucket replication stats (#13942)
- r.ulock was not locked when r.UsageCache was being modified

Bonus:

- simplify code by removing some unnecessary clone methods - we can 
do this because go arrays are values (not pointers/references) that are 
automatically copied on assignment.

- remove some unnecessary map allocation calls
2021-12-17 15:33:13 -08:00
Shireesh Anjal 13441ad0f8 Add IsKubernetes and IsDocker to health data (#13936) 2021-12-17 14:46:54 -08:00
Harshavardhana aa508591c1 cache only metrics served from the disks (#13940)
do not need to cache in-memory instant metrics
2021-12-17 11:40:09 -08:00
Harshavardhana 818f0201fc re-implement prometheus metrics endpoint to be simpler (#13922)
data-structures were repeatedly initialized
this causes GC pressure, instead re-use the
collectors.

Initialize collectors in `init()`, also make
sure to honor the cache semantics for performance
requirements.

Avoid a global map and a global lock for metrics
lookup instead let them all be lock-free unless
the cache is being invalidated.
2021-12-17 10:11:04 -08:00
Aditya Manthramurthy 890f43ffa5 Map policy to parent for STS (#13884)
When STS credentials are created for a user, a unique (hopefully stable) parent
user value exists for the credential, which corresponds to the user for whom the
credentials are created. The access policy is mapped to this parent-user and is
persisted. This helps ensure that all STS credentials of a user have the same
policy assignment at all times.

Before this change, for an OIDC STS credential, when the policy claim changes in
the provider (when not using RoleARNs), the change would not take effect on
existing credentials, but only on new ones.

To support existing STS credentials without parent-user policy mappings, we
lookup the policy in the policy claim value. This behavior should be deprecated
when such support is no longer required, as it can still lead to stale
policy mappings.

Additionally this change also simplifies the implementation for all non-RoleARN
STS credentials. Specifically, for AssumeRole (internal IDP) STS credentials,
policies are picked up from the parent user's policies; for
AssumeRoleWithCertificate STS credentials, policies are picked up from the
parent user mapping created when the STS credential is generated.
AssumeRoleWithLDAP already picks up policies mapped to the virtual parent user.
2021-12-17 00:46:30 -08:00
Poorna K e270ab65b3 fix: healing of replication delete markers (#13933)
A corner case can occur where the delete-marker was propagated 
but the metadata could not be updated on the primary. Sending 
a RemoveObject call with the Delete marker version would end 
up permanently deleting the version on target. Instead, perform 
a Stat on the delete-marker version on target and redo replication 
only if the delete-marker is missing on target.
2021-12-16 15:34:55 -08:00
Anis Elleuch 926373f9c1 Run the data scanner routine in a loop (#13928)
After the introduction of Refresh logic in locks, the data scanner can
quit when the data scanner lock is not able to get refreshed. In that
case, the context of the data scanner will get canceled and
runDataScanner() will quit. Another server would pick the scanning
routine but after some time, all nodes can just have all scanning
routine aborted, as described above.

This fix will just run the data scanner in a loop.
2021-12-16 08:32:15 -08:00
Poorna K 111c6177d2 Deprecate caching for erasure/distributed mode (#13909)
Fixes: #13907

Also removing default value of `writethrough` for cache commit
which was interfering with cache_after setting
2021-12-15 16:48:34 -08:00
Klaus Post 91f72f25ab select: Return early from bool AND, OR (#13914)
Return as soon as an AND fails and whenever an OR succeeds. Faster and more flexible.

For example makes `select * from S3object where _2 != '' AND _2 > 1` able to operate on empty fields.

Followup to #13900
2021-12-15 16:47:21 -08:00
Harshavardhana da540ccf8c update selfupdate dependency to fix signature verification (#13919)
minisign v0.10.0 tool broke compatibility, that leads
to our library failing to parse the newer signatures.

This PR
fixes - https://github.com/minio/operator/issues/913
fixes - https://github.com/minio/minio/issues/13824

A workaround for users facing this problem is to unset

```
MINIO_UPDATE_MINISIGN_PUBKEY
```

or set it to `empty` string then signature verification
is skipped automatically.
2021-12-15 15:05:23 -08:00
Daryl Hughes d6396f82fe support GCS gateway on vanilla helm chart (#13810)
These changes have been migrated from the previous chart: https://github.com/helm/charts/tree/master/stable/minio

Added `GCS` support for gateway mode in the helm chart.

Added a new GCS block under the gateway key to the house 
the GCS-specific variables.

The gateway-deployment template now sets the env var: GOOGLE_APPLICATION_CREDENTIALS as a path to the 
service-account-file.json

The service-account-file.json can be added to the MinIO 
the secret if an existingSecret is not specified.
2021-12-15 12:30:13 -08:00
Harshavardhana 4fa250a6a1 add hash-set debugging currently supports SIPMOD (#13911)
fixes a bug in s3-check-md5 for single part
uploaded multipart objects.
2021-12-15 12:07:15 -08:00
Poorna K b42cfcea60 Disallow versioning/replication change in cluster replication setup (#13910) 2021-12-15 10:37:08 -08:00
Klaus Post aca6dfbd60 Check for nil RPC in listing (#13917)
Fixes #13915
2021-12-15 09:19:11 -08:00
Harshavardhana 5f7e6d03ff copy bucket slice to avoid skipping .minio.sys/buckets (#13912)
healing was skipping `.minio.sys/buckets` path so
essentially not healing `.usage.json` - fix this
by making a copy of `buckets` slice.
2021-12-15 09:18:09 -08:00
Harshavardhana 88ad742da0 fix: error handling cases in site-replication (#13901)
- Allow proper SRError to be propagated to
  handlers and converted appropriately.

- Make sure to enable object locking on buckets
  when requested in MakeBucketHook.

- When DNSConfig is enabled attempt to delete it
  first before deleting buckets locally.
2021-12-14 14:09:57 -08:00
Klaus Post a8d4042853 select: Add IS (NOT) operators (#13906)
Add `IS` and `IS NOT` as comparison operators.

This may be a bit wider than the S3 spec, but we can rather 
easily remove the forwarding.
2021-12-14 09:54:50 -08:00
Krishnan Parthasarathi 44a9339c0a Newer noncurrent versions (#13815)
- Rename MaxNoncurrentVersions tag to NewerNoncurrentVersions

Note: We apply overlapping NewerNoncurrentVersions rules such that 
we honor the highest among applicable limits. e.g if 2 overlapping rules 
are configured with 2 and 3 noncurrent versions to be retained, we 
will retain 3.

- Expire newer noncurrent versions after noncurrent days
- MinIO extension: allow noncurrent days to be zero, allowing expiry 
  of noncurrent version as soon as more than configured 
  NewerNoncurrentVersions are present.
- Allow NewerNoncurrentVersions rules on object-locked buckets
- No x-amz-expiration when NewerNoncurrentVersions configured
- ComputeAction should skip rules with NewerNoncurrentVersions > 0
- Add unit tests for lifecycle.ComputeAction
- Support lifecycle rules with MaxNoncurrentVersions
- Extend ExpectedExpiryTime to work with zero days
- Fix all-time comparisons to be relative to UTC
2021-12-14 09:41:44 -08:00
Harshavardhana 113c7ff49a add code to parse secrets natively instead of shell scripts (#13883) 2021-12-13 18:23:31 -08:00
fpaupier 40dbe243d9 update: keycloak binding documentation (#13894) 2021-12-13 18:23:16 -08:00
Poorna K d422d24278 replication: warn if insufficient workers (#13899)
This should give an early warning if configured replication 
workers are insufficient to meet application workload.
2021-12-13 18:22:56 -08:00
Harshavardhana 109c927dad docs/debug: log any corruption and continue debug tool 2021-12-13 17:45:34 -08:00
Aditya Manthramurthy de400f3473 Allow setting non-existent policy on a user/group (#13898) 2021-12-13 15:55:52 -08:00
Harshavardhana 8144a125ce check for update in background (#13889) 2021-12-13 09:43:03 -08:00
Minio Trusted 3e34e41a5a update helm to 3.3.4 2021-12-11 09:59:34 -08:00
Minio Trusted 2270887d43 update helm to 3.3.3 2021-12-11 09:28:02 -08:00
Minio Trusted c6431f9a04 update helm to 3.3.2
resolves #13887
2021-12-11 09:26:01 -08:00
jiangfucheng 88c0d0120c update heal object unit test (#13886) 2021-12-11 09:04:07 -08:00
Aditya Manthramurthy 44fefe5b9f Add option to policy info API to return create/mod timestamps (#13796)
- This introduces a new admin API with a query parameter (v=2) to return a
response with the timestamps

- Older API still works for compatibility/smooth transition in console
2021-12-11 09:03:39 -08:00
Minio Trusted 878d368cea Update yaml files to latest version RELEASE.2021-12-10T23-03-39Z 2021-12-11 00:02:59 -08:00
Aditya Manthramurthy f2bd026d0e Allow OIDC user to query user info if policies permit (#13882) 2021-12-10 15:03:39 -08:00
Klaus Post 518612492c xl-meta: Add header titles (#13880)
Add type for headers and create custom marshal to make 
it easier to read. Group headers and metadata.

Restore functionality that will read `xl.meta` in the current dir with no params.

Before:
```
{
  "Headers": [
    [
      "8M04bTiYRDmEMQGeAsk1yg==",
      1639150471630100400,
      "rLD1Rw==",
      1,
      6
    ],
  ],
    "Versions": [
    {
      "Type": 1,
      "V2Obj": {
        "CSumAlgo": 1,
        "DDir": "oC1Xpg4tRfW03g8o8w7Bzg==",
        "EcAlgo": 1,
        "EcBSize": 1048576,
        "EcDist": [
          7,
          8,
          1,
          2,
          3,
          4,
          5,
          6
        ],
        "EcIndex": 1,
        "EcM": 4,
        "EcN": 4,
        "ID": "8M04bTiYRDmEMQGeAsk1yg==",
        "MTime": 1639150471630100400,
        "MetaSys": {
          "x-minio-internal-inline-data": "dHJ1ZQ=="
        },
        "MetaUsr": {
          "content-type": "application/octet-stream",
          "etag": "b8252c86fad2d8937300aa92b467a3aa"
        },
        "PartASizes": [
          1000
        ],
        "PartETags": null,
        "PartNums": [
          1
        ],
        "PartSizes": [
          1000
        ],
        "Size": 1000
      }
    }
  ]
}
```

After:
```
{
  "Versions": [
    {
      "Header": {
        "Flags": 6,
        "ModTime": "2021-12-10T16:34:31.6301004+01:00",
        "Signature": "acb0f547",
        "Type": 1,
        "VersionID": "f0cd386d389844398431019e02c935ca"
      },
      "Idx": 0,
      "Metadata": {
        "Type": 1,
        "V2Obj": {
          "CSumAlgo": 1,
          "DDir": "oC1Xpg4tRfW03g8o8w7Bzg==",
          "EcAlgo": 1,
          "EcBSize": 1048576,
          "EcDist": [
            7,
            8,
            1,
            2,
            3,
            4,
            5,
            6
          ],
          "EcIndex": 1,
          "EcM": 4,
          "EcN": 4,
          "ID": "8M04bTiYRDmEMQGeAsk1yg==",
          "MTime": 1639150471630100400,
          "MetaSys": {
            "x-minio-internal-inline-data": "dHJ1ZQ=="
          },
          "MetaUsr": {
            "content-type": "application/octet-stream",
            "etag": "b8252c86fad2d8937300aa92b467a3aa"
          },
          "PartASizes": [
            1000
          ],
          "PartETags": null,
          "PartNums": [
            1
          ],
          "PartSizes": [
            1000
          ],
          "Size": 1000
        }
      }
    }
  ]
}
```
2021-12-10 15:03:25 -08:00
Klaus Post 81e43b87c2 Don't zero buffer if big enough (#13877)
Only append zeroed bytes when we don't have enough space anyway.
2021-12-10 13:08:10 -08:00
Aditya Manthramurthy a02e17f15c Add tests to ensure that OIDC user can create IAM users (#13881) 2021-12-10 13:04:21 -08:00
Ravind Kumar c76f86fdbd Clarify example for Standalone Docker instructions (#13879)
Closes #13868 

Also points users to the web documentation for docker installation.
2021-12-10 09:43:02 -08:00
Harshavardhana 5b7c00ff52 add more tests to cover areas for weird object names (#13873)
continuation of #13858 to add more tests and also validate the 
written object data.
2021-12-09 17:52:53 -08:00
Aditya Manthramurthy b9f0046ee7 Allow STS credentials to create users (#13874)
- allow any regular user to change their own password
- allow STS credentials to create users if permissions allow

Bonus: do not allow changes to sts/service account credentials (via add user API)
2021-12-09 17:48:51 -08:00
Harshavardhana 3b79f7e4ae ignore if volume exists in MakeVolBulk, return other errors (#13866) 2021-12-09 15:55:42 -08:00
Aditya Manthramurthy 85d2df02b9 fix: user listing with LDAP (#13872)
Users listing was showing just a weird policy 
mapping output which does not make sense here.
2021-12-09 15:55:28 -08:00
Harshavardhana 2f1e8ba612 add more directory marker tests and fix a bug (#13871)
ListObjects() should never list a delete-marked folder
if latest is delete marker and delimiter is not provided.

ListObjectVersions() should list a delete-marked folder
even if latest is delete marker and delimiter is not
provided.

Enhance further versioning listing on the buckets
2021-12-09 14:59:23 -08:00
Anis Elleuch 84c690cb07 storage: Use request.Form and avoid mux matching (#13858)
request.Form uses less memory allocation and avoids gorilla mux matching
with weird characters in parameters such as '\n'

- Remove Queries() to avoid matching
- Ensure r.ParseForm is called to populate fields
- Add a unit test for object names with '\n'
2021-12-09 08:38:46 -08:00
Harshavardhana 239bbad7ab add test to expect prefix without a directory object (#13865)
Motivation is to cover more areas
2021-12-09 08:36:54 -08:00
Minio Trusted 4be8023408 Update yaml files to latest version RELEASE.2021-12-09T06-19-41Z 2021-12-09 08:40:46 +00:00
Harshavardhana 83e8da57b8 update console to release v0.12.8 2021-12-08 22:19:41 -08:00
Harshavardhana dcff6c996d fix: do not list delete-marked objects (#13864)
delete marked objects should not be considered
for listing when listing is delimited, this issue
as introduced in PR #13804 which was mainly to
address listing of directories in listing when
delimited.

This PR fixes this properly and adds tests to
ensure that we behave in accordance with how
an S3 API behaves for ListObjects() without
versions.
2021-12-08 17:34:52 -08:00
Poorna K 0a66a6f1e5 Avoid cache GC of writebacks before commit syncs (#13860)
Save part.1 for writebacks in a separate folder
and move it to cache dir atomically while saving
the cache metadata. This is to avoid GC mistaking
part.1 as orphaned cache entries and purging them.

This PR also fixes object size being overwritten during
retries for write-back mode.
2021-12-08 14:52:31 -08:00
Harshavardhana e82a5c5c54 fix: site replication issues and add tests (#13861)
- deleting policies was deleting all LDAP
  user mapping, this was a regression introduced
  in #13567

- deleting of policies is properly sent across
  all sites.

- remove unexpected errors instead embed the real
  errors as part of the 500 error response.
2021-12-08 11:50:15 -08:00
Harshavardhana 92fdcafb66 add verification tests for ETag on replicated content (#13857) 2021-12-07 10:08:26 -08:00
Harshavardhana b9aae1aaae fix: speedtest should exit upon errors cleanly (#13851)
- deleteBucket() should be called for cleanup
  if client abruptly disconnects

- out of disk errors should be sent to client
  properly and also cancel the calls

- limit concurrency to available MAXPROCS not
  32 for auto-tuned setup, if procs are beyond
  32 then continue normally. this is to handle
  smaller setups.

fixes #13834
2021-12-06 16:36:14 -08:00
Harshavardhana 7d70afc937 fix: potential crash in diskCache when fileScorer is empty (#13850)
```
goroutine 115 [running]:
github.com/minio/minio/cmd.(*diskCache).purge.func3({0xc007a10a40, 0x40}, 0x40)
   github.com/minio/minio/cmd/disk-cache-backend.go:430 +0x90d
```
2021-12-06 15:55:29 -08:00
Aditya Manthramurthy 12b63061c2 Fix LDAP service account creation (#13849)
- when a user has only group permissions
- fixes regression from ac74237f0 (#13657)
- fixes https://github.com/minio/console/issues/1291
2021-12-06 15:55:11 -08:00
Klaus Post 038fdeea83 snowball: return errors on failures (#13836)
Return errors when untar fails at once.

Current error handling was quite a mess. Errors are written 
to the stream, but processing continues.

Instead, return errors when they occur and transform 
internal errors to bad request errors, since it is likely a 
problem with the input.

Fixes #13832
2021-12-06 09:45:23 -08:00
Anis Elleuch 0b6225bcc3 Better error msg when version mismatch of internode API (#13845)
Sometimes, we see an error message like "Server expects 'storage' API
version 'v41', instead found 'v41'" shows a more generic error message
with the path of the REST call.
2021-12-06 09:44:48 -08:00
Anis Elleuch f286ef8e17 isMultipart to test on parts sizes only if object is encrypted (#13839)
ObjectInfo.isMultipart() is testing if parts sizes are compatible with
encrypted parts but this only can be done if the object is encrypted.
2021-12-06 09:43:43 -08:00
Harshavardhana b120bcb60a validate if cached value is empty before use (#13830)
fixes a crash reproduced while running hadoop tests

```
goroutine 201564 [running]:
github.com/minio/minio/cmd.metaCacheEntries.resolve({0xc0206ab7a0, 0x4, 0xc0015b1908}, 0xc0212a7040)
	github.com/minio/minio/cmd/metacache-entries.go:352 +0x58a
```

Bonus: HeadBucket() should always provide content-type
2021-12-06 02:59:51 -08:00
Harshavardhana be34fc9134 fix: kms-id header should have arn:aws:kms: prefix (#13833)
arn:aws:kms: is a must for KMS keyID.
2021-12-06 00:39:32 -08:00
Harshavardhana 8591d17d82 return appropriate errors upon parseErrors (#13831) 2021-12-05 11:36:26 -08:00
Harshavardhana f6190d6751 Add single drive support for directory prefixes in Listing (#13829)
This fixes the compatibility issue with Hadoop 3.3.1

fixes #13710
2021-12-03 18:08:40 -08:00
Harshavardhana f0fc77fded update CREDITS file with new deps 2021-12-03 13:24:49 -08:00
Klaus Post f56cac6381 jwt: Parse standard claims faster (#13821)
* Use structless/allocationless decoding for header (note "typ" isn't used)
* Create custom unmarshal code using jsonparser for StandardClaims.

Before/After:

```
BenchmarkParseJWTStandardClaims-32    	 4270724	       294.0 ns/op	     706 B/op	      16 allocs/op
BenchmarkParseJWTStandardClaims-32    	 5634847	       214.7 ns/op	     544 B/op	       9 allocs/op

BenchmarkParseJWTMapClaims-32    	 2763045	       428.6 ns/op	    1251 B/op	      29 allocs/op
BenchmarkParseJWTMapClaims-32    	 2839455	       410.9 ns/op	    1219 B/op	      26 allocs/op
```
2021-12-03 13:19:38 -08:00
Aditya Manthramurthy 4f35054d29 Ensure that role ARNs don't collide (#13817)
This is to prepare for multiple providers enhancement.
2021-12-03 13:15:56 -08:00
Shireesh Anjal d29df6714a Introduce new config subnet api_key (#13793)
The earlier approach of using a license token for 
communicating with SUBNET is being replaced 
with a simpler mechanism of API keys. Unlike the 
license which is a JWT token, these API keys will 
be simple UUID tokens and don't have any embedded 
information in them. SUBNET would generate the 
API key on cluster registration, and then it would 
be saved in this config, to be used for subsequent 
communication with SUBNET.
2021-12-03 09:32:11 -08:00
jiangfucheng 7460fb8349 fix padding error and compatible with uploaded objects (#13803) 2021-12-03 09:26:30 -08:00
Harshavardhana a7c430355a fix: throw appropriate errors when all disks fail (#13820)
when all disks fail with same error, fail server
startup anyways - we cannot proceed.

fixes #13818
2021-12-03 09:25:17 -08:00
Harshavardhana 1df1517449 Add missing Dockerfile.dev 2021-12-03 00:56:40 -08:00
Harshavardhana f7c357ebad update console to v0.12.6 2021-12-02 17:54:29 -08:00
Harshavardhana 20c60aae68 Update hotfix documentation and container building 2021-12-02 17:52:46 -08:00
Aditya Manthramurthy b14527b7af If role policy is configured, require that role ARN be set in STS (#13814) 2021-12-02 15:43:39 -08:00
Harshavardhana f840080e5b cleanup site-replication docs (#13812) 2021-12-02 13:27:01 -08:00
Harshavardhana 2c6983a2f1 fix: use consistent ports in verify-healing (#13813)
also use unique directories in setup testing.
2021-12-02 12:40:48 -08:00
Harshavardhana acfb83ec5e update to v3.3.1 - helm chart docs Minio -> MinIO
fixes #13775
2021-12-02 12:09:18 -08:00
Klaus Post 3db931dc0e Improve listing consistency with version merging (#13723) 2021-12-02 11:29:16 -08:00
Klaus Post 8309ddd486 Fix panic (not fatal) on connection drops (#13811)
Fix more regressions from #13597 with double closed channels.

```
panic: "POST /minio/storage/data/distxl-plain/s1/d2/v42/createfile?disk-id=c789f7e1-2b52-442a-b518-aa2dac03f3a1&file-path=f6161668-b939-4543-9873-91b9da4cdff6%2F5eafa986-a3bf-4b1c-8bc0-03a37de390a3%2Fpart.1&length=2621760&volume=.minio.sys%2Ftmp": send on closed channel
goroutine 1977 [running]:
runtime/debug.Stack()
        c:/go/src/runtime/debug/stack.go:24 +0x65
github.com/minio/minio/cmd.setCriticalErrorHandler.func1.1()
        d:/minio/minio/cmd/generic-handlers.go:468 +0x8e
panic({0x2928860, 0x4fb17e0})
        c:/go/src/runtime/panic.go:1038 +0x215
github.com/minio/minio/cmd.keepHTTPReqResponseAlive.func2({0x4fe4ea0, 0xc02737d8a0})
        d:/minio/minio/cmd/storage-rest-server.go:818 +0x48
github.com/minio/minio/cmd.(*storageRESTServer).CreateFileHandler(0xc0015a8510, {0x50073e0, 0xc0273ec460}, 0xc029b9a400)
        d:/minio/minio/cmd/storage-rest-server.go:334 +0x1d2
net/http.HandlerFunc.ServeHTTP(...)
        c:/go/src/net/http/server.go:2046
github.com/minio/minio/cmd.httpTraceHdrs.func1({0x50073e0, 0xc0273ec460}, 0x0)
        d:/minio/minio/cmd/handler-utils.go:372 +0x53
net/http.HandlerFunc.ServeHTTP(0x5007380, {0x50073e0, 0xc0273ec460}, 0x10)
        c:/go/src/net/http/server.go:2046 +0x2f
github.com/minio/minio/cmd.addCustomHeaders.func1({0x5007380, 0xc0273dcf00}, 0xc0273f7340)
```

Reverts but adds write checks.
2021-12-02 11:22:32 -08:00
Harshavardhana 21c868a646 fix: do not ignore delete-marker directories in ListObjects() (#13804)
Following scenario such as objects that exist inside a
prefix say `folder/` must be included in the listObjects()
response.

```
2aa16073-387e-492c-9d59-b4b0b7b6997a v2 DEL folder/
a5b9ce68-7239-4921-90ab-20aed402c7a2 v1 PUT folder/
f2211798-0eeb-4d9e-9184-fcfeae27d069 v1 PUT folder/1.txt
```

Current master does not handle this scenario, because it
ignores the top level delete-marker on folders. This is
however unexpected. It is expected that list-objects returns
the top level prefix in this situation.

```
aws s3api list-objects --bucket harshavardhana --prefix unique/ \
     --delimiter / --profile minio --endpoint-url http://localhost:9000
{
    "CommonPrefixes": [
        {
            "Prefix": "unique/folder/"
        }
    ]
}
```

There are applications in the wild such as Hadoop s3a connector
that exploit this behavior and expect the folder to be present
in the response.

This also makes the behavior consistent with AWS S3.
2021-12-02 08:46:33 -08:00
Harshavardhana ffe9acfe4a docs: Add a markdown documentation on hotfix branches and process 2021-12-01 01:00:18 -08:00
Harshavardhana 24d904d194 reload certs from disk upon SIGHUP (#13792) 2021-12-01 00:38:32 -08:00
Harshavardhana b280a37c4d add delete-marker proactively in DeleteObject() (#13795)
single object delete was not working properly
on a bucket when versioning was suspended,
current version 'null' object was never removed.

added unit tests to cover the behavior

fixes #13783
2021-11-30 18:30:06 -08:00
vinzenzs 906548d0ba Added the service account to deloyments and statefulset (#13790) 2021-11-30 15:25:30 -08:00
Poorna K 1485a5bf3b fix: dockerfile.dev to create /opt/bin first (#13794) 2021-11-30 15:24:39 -08:00
Poorna K 9ec197f2e8 Add support for adding new site(s) to site replication (#13696)
Currently, the new site is expected to be empty
2021-11-30 13:16:37 -08:00
Poorna K d21466f595 cache: in writeback mode skip etag verification (#13781)
if the commit is still in pending or failed status

This PR also does some minor code cleanup
2021-11-30 10:22:42 -08:00
Harshavardhana 4f3290309e Revert "disable CI/CD for draft PRs (#13784)"
This reverts commit 5a22f2cf0b.
2021-11-30 09:22:17 -08:00
Klaus Post d6fe0f61a9 do not panic when input cannot be parsed (#13791)
Fix cases where `s3Select.Open` fails and doesn't set the recordReader.

Fixes #13786
2021-11-30 08:42:42 -08:00
Krishnan Parthasarathi 5a22f2cf0b disable CI/CD for draft PRs (#13784) 2021-11-29 23:35:07 -08:00
Aditya Manthramurthy 42d11d9e7d Move IAM notifications into IAM system functions (#13780) 2021-11-29 14:38:57 -08:00
Harshavardhana e49c184595 add configurable 'shutdown-timeout' for HTTP server (#13771)
fixes #12317
2021-11-29 09:06:56 -08:00
Harshavardhana 99d87c5ca2 fix: totalDrives reported in speedTest for multiple-pools (#13770)
totalDrives reported in speedTest result were wrong
for multiple pools, this PR fixes this.

Bonus: add support for configurable storage-class, this
allows us to test REDUCED_REDUNDANCY to see further
maximum throughputs across the cluster.
2021-11-29 09:05:46 -08:00
Aditya Manthramurthy 4c0f48c548 Add role ARN support for OIDC identity provider (#13651)
- Allows setting a role policy parameter when configuring OIDC provider

- When role policy is set, the server prints a role ARN usable in STS API requests

- The given role policy is applied to STS API requests when the roleARN parameter is provided.

- Service accounts for role policy are also possible and work as expected.
2021-11-26 19:22:40 -08:00
Aditya Manthramurthy 4ce6d35e30 Add new site config sub-system intended to replace region (#13672)
- New sub-system has "region" and "name" fields.

- `region` subsystem is marked as deprecated, however still works, unless the
new region parameter under `site` is set - in this case, the region subsystem is
ignored. `region` subsystem is hidden from top-level help (i.e. from `mc admin
config set myminio`), but appears when specifically requested (i.e. with `mc
admin config set myminio region`).

- MINIO_REGION, MINIO_REGION_NAME are supported as legacy environment variables for server region.

- Adds MINIO_SITE_REGION as the current environment variable to configure the
server region and MINIO_SITE_NAME for the site name.
2021-11-25 13:06:25 -08:00
Harshavardhana 81bf0c66c6 update helm chart to 3.3.0 2021-11-25 09:33:26 -08:00
Klaus Post 34dc725d26 fix: s3zip in fs mode (#13758)
The index was converted directly from bytes to binary. This would fail a roundtrip through json.

This would result in `Error: invalid input: magic number mismatch` when reading back.

On non-erasure backends store index as base64.
2021-11-25 09:11:25 -08:00
Minio Trusted a5db4ca092 Update yaml files to latest version RELEASE.2021-11-24T23-19-33Z 2021-11-25 07:39:00 +00:00
Aditya Manthramurthy 61029fe20b fix: returning invalid account-not-exists error for LDAP svc acc (#13756) 2021-11-24 15:19:33 -08:00
Harshavardhana fee3f88cb5 use acceptedResponseStatusCode everywhere in HTTP logger (#13755) 2021-11-24 13:53:11 -08:00
Harshavardhana 932500e43d update console to v0.12.5 2021-11-24 12:46:53 -08:00
Anis Elleuch 55d4cdd464 multi-delete: Avoid empty Delete tag in the response (#13725)
When removing an object fails, such as when it is WORM protected, a
wrong <Delete> will still be in the response. This commit fixes it.
2021-11-24 10:01:07 -08:00
Klaus Post fe3e47b1e8 Fix "send on closed channel" panic (#13745)
The httpStreamResponse should not return until CloseWithError has been called.

Instead keep track of write state and skip writing/flushing if an error has occurred.

Fixes #13743

Regression from #13597 (not released)
2021-11-24 09:42:42 -08:00
Harshavardhana 9ca25bd48f fix: atomic.Value should be a concrete type to avoid panics (#13740)
Go's atomic.Value does not support `nil` type,
concrete type is necessary to avoid any panics with
the current implementation.

Also remove boolean to turn-off tracking of freezeCount.
2021-11-23 16:09:28 -08:00
Harshavardhana 91e0823ff0 allow service freeze/unfreeze on a setup (#13707)
an active running speedTest will reject all
new S3 requests to the server, until speedTest
is complete.

this is to ensure that speedTest results are
accurate and trusted.

Co-authored-by: Klaus Post <klauspost@gmail.com>
2021-11-23 12:02:16 -08:00
Klaus Post 142c6b11b3 Reduce JWT overhead for internode tokens (#13738)
Since JWT tokens remain valid for up to 15 minutes, we 
don't have to regenerate tokens for every call.

Cache tokens for matching access+secret+audience 
for up to 15 seconds.

```
BenchmarkAuthenticateNode/uncached-32         	  270567	      4179 ns/op	    2961 B/op	      33 allocs/op
BenchmarkAuthenticateNode/cached-32           	 7684824	       157.5 ns/op	      48 B/op	       1 allocs/op
```

Reduces internode call allocations a great deal.
2021-11-23 09:51:53 -08:00
chrisbecke ef0b8367b5 Update minio-overview.json data source panel (#13730)
Add missing datasource in `Healing` panel.
2021-11-23 09:01:07 -08:00
Anis Elleuch d1bfb4d2c0 policy: Fix a typo when validating the list of policies (#13735)
When assigning two policies to a user using mc command, the server code
wrongly validates due to a typo in the code, the commit fixes it.
2021-11-23 08:57:29 -08:00
Harshavardhana 3b5d6f003f update dockerfile with proper PATHs 2021-11-22 16:48:05 -08:00
Harshavardhana 26c457860b remove "expires" header from presign v2 as metadata (#13718)
fixes #13704
2021-11-22 16:07:23 -08:00
3nprob d0515031c7 disable IPv6 globally on docker build (#13724) 2021-11-22 13:56:06 -08:00
Harshavardhana 08f4a0a816 fix: make sure esClient is allocated before use (#13727) 2021-11-22 12:46:46 -08:00
Harshavardhana 28f95f1fbe quorum calculation getLatestFileInfo should be itself (#13717)
FileInfo quorum shouldn't be passed down, instead
inferred after obtaining a maximally occurring FileInfo.

This PR also changes other functions that rely on
wrong quorum calculation.

Update tests as well to handle the proper requirement. All
these changes are needed when migrating from older deployments
where we used to set N/2 quorum for reads to EC:4 parity in
newer releases.
2021-11-22 09:36:29 -08:00
Harshavardhana c791de0e1e re-implement pickValidInfo dataDir, move to quorum calculation (#13681)
dataDir loosely based on maxima is incorrect and does not
work in all situations such as disks in the following order

- xl.json migration to xl.meta there may be partial xl.json's
  leftover if some disks are not yet connected when the disk
  is yet to come up, since xl.json mtime and xl.meta is
  same the dataDir maxima doesn't work properly leading to
  quorum issues.

- its also possible that XLV1 might be true among the disks
  available, make sure to keep FileInfo based on common quorum
  and skip unexpected disks with the older data format.

Also, this PR tests upgrade from older to a newer release if the 
data is readable and matches the checksum.

NOTE: this is just initial work we can build on top of this to do further tests.
2021-11-21 10:41:30 -08:00
Harshavardhana 36b5426f6e dataDir needs maxima calculation to be correct (#13715)
there is a corner case where the new check
doesn't work where dataDir has changed, especially
when xl.json -> xl.meta healing happens, if some
healing is partial this can make certain backend
files unreadable.

This PR fixes and updates unit-tests
2021-11-20 11:26:30 -08:00
Harshavardhana 1e72e9b1cd update console to v0.12.4 2021-11-20 09:49:14 -08:00
Aditya Manthramurthy 9739e55d0f tests: add OpenID service accounts creation and update (#13708)
- service account creation for STS accounts
- service account session policy update for STS accounts
- refactor svc acc tests and add them for OpenID
2021-11-20 02:07:16 -08:00
Klaus Post 1cddbc80cf fix: entries not cleared on resolve (#13705)
This can cause old entries to be included (albeit unlikely) in resolution.
2021-11-20 02:02:57 -08:00
Krishnan Parthasarathi 3da9ee15d3 Add MaxNoncurrentVersions to NoncurrentExpiration action (#13580)
This unit allows users to limit the maximum number of noncurrent 
versions of an object.

To enable this rule you need the following *ilm.json*
```
cat >> ilm.json <<EOF
{
    "Rules": [
        {
            "ID": "test-max-noncurrent",
            "Status": "Enabled",
            "Filter": {
                "Prefix": "user-uploads/"
            },
            "NoncurrentVersionExpiration": {
                "MaxNoncurrentVersions": 5
            }
        }
    ]
}
EOF
mc ilm import myminio/mybucket < ilm.json
```
2021-11-19 17:54:10 -08:00
Aditya Manthramurthy 1e2fac054c Add caching to CI jobs (#13712)
- Seems to be improving times for shorter jobs at least.

- Remove Go 1.16.x tests for IAM and replication
2021-11-19 16:18:23 -08:00
Harshavardhana 914bfb2d9c fix: allow compaction on replicated buckets (#13711)
currently getReplicationConfig() failure incorrectly
returns error on unexpected buckets upon upgrade, we
should always calculate usage as much as possible.
2021-11-19 14:46:14 -08:00
Aditya Manthramurthy 40244994ad Allow users to list their own service accounts (#13706)
Bonus: add extensive tests for svc acc actions by users
2021-11-19 12:35:35 -08:00
Harshavardhana 556ae07857 simplify the reader for speedtest (#13682)
additionally count only success operations,
truncated incomplete calls don't need to be
counted.
2021-11-19 10:41:37 -08:00
Harshavardhana 17fd71164c retry disk replacement healing if listing fails (#13689)
listing can fail and it is allowed to be retried,
instead of returning right away return an error at
the end - heal the rest of the buckets and objects,
and when we are retrying skip the buckets that
are already marked done by using the tracked buckets.

fixes #12972
2021-11-19 08:46:47 -08:00
Harshavardhana 81d19156e9 allow in-memory persistence for gateway (#13694)
NAS gateway would persist however with or without etcd as before.
2021-11-18 23:47:02 -08:00
Mani 7b82411e6f change the unit of measurement from TB to TiB (#13686) 2021-11-18 20:06:37 -08:00
Harshavardhana fb268add7a do not flush if Write() failed (#13597)
- Go might reset the internal http.ResponseWriter() to `nil`
  after Write() failure if the go-routine has returned, do not
  flush() such scenarios and avoid spurious flushes() as
  returning handlers always flush.
- fix some racy tests with the console 
- avoid ticker leaks in certain situations
2021-11-18 17:19:58 -08:00
Harshavardhana 7700973538 add missing copyright on testfile (#13691)
remove fsSimpleRenameFile implementation for Rename()
2021-11-18 16:09:12 -08:00
Aditya Manthramurthy 54e25a0251 Fix: Use policies from claims for service accounts (#13690)
Fixes #13676
2021-11-18 15:38:54 -08:00
Harshavardhana 79b3a1fe4e remove object torrent, AWS S3 removed support for torrent API 2021-11-18 12:21:48 -08:00
Klaus Post faf013ec84 Improve performance on multiple versions (#13573)
Existing:

```go
type xlMetaV2 struct {
    Versions []xlMetaV2Version `json:"Versions" msg:"Versions"`
}
```

Serialized as regular MessagePack.

```go
//msgp:tuple xlMetaV2VersionHeader
type xlMetaV2VersionHeader struct {
	VersionID [16]byte
	ModTime   int64
	Type      VersionType
	Flags     xlFlags
}
```

Serialize as streaming MessagePack, format:

```
int(headerVersion)
int(xlmetaVersion)
int(nVersions)
for each version {
    binary blob, xlMetaV2VersionHeader, serialized
    binary blob, xlMetaV2Version, serialized.
}
```

xlMetaV2VersionHeader is <= 30 bytes serialized. Deserialized struct 
can easily be reused and does not contain pointers, so efficient as a 
slice (single allocation)

This allows quickly parsing everything as slices of bytes (no copy).

Versions are always *saved* sorted by modTime, newest *first*. 
No more need to sort on load.

* Allows checking if a version exists.
* Allows reading single version without unmarshal all.
* Allows reading latest version of type without unmarshal all.
* Allows reading latest version without unmarshal of all.
* Allows checking if the latest is deleteMarker by reading first entry.
* Allows adding/updating/deleting a version with only header deserialization.
* Reduces allocations on conversion to FileInfo(s).
2021-11-18 12:15:22 -08:00
Shireesh Anjal 7152915318 Use pointer based TLS field (#13659)
This will help other projects like `health-analyzer` to verify that the
struct was indeed populated by the minio server, and is not
default-populated during unmarshalling of the JSON.

Signed-off-by: Shireesh Anjal <shireesh@minio.io>
2021-11-18 09:02:33 -08:00
Harshavardhana 886262e58a heal legacy objects when versioning is enabled after upgrade (#13671)
legacy objects in 'xl.json' after upgrade, should have
following sequence of events - bucket should have versioning
enabled and the object should have been overwritten with
another version of an object.

this situation was not handled, which would lead to older
objects to stay perpetually with "legacy" dataDir, however
these objects were readable by all means - there weren't
converted to newer format.

This PR fixes this situation properly.
2021-11-17 15:49:12 -08:00
Harshavardhana 9c5d9ae376 fallback O_DIRECT if not supported, do regular reads() (#13680) 2021-11-17 15:48:47 -08:00
Ashish Kumar Sinha 3d2bc15e9a Add grafana json file for replication metrics (#13678) 2021-11-17 14:49:46 -08:00
Harshavardhana 20c43c447d de-couple bucket metadata loading with lock context (#13679)
avoid passing lock context while loading bucket
metadata, refactor such that we can de-couple things
for subsystem loading.
2021-11-17 13:42:08 -08:00
Anis Elleuch 4caed7cc0d metrics: Add replication latency metrics (#13515)
Add a new Prometheus metric for bucket replication latency

e.g.:
minio_bucket_replication_latency_ns{
    bucket="testbucket",
    operation="upload",
    range="LESS_THAN_1_MiB",
    server="127.0.0.1:9001",
    targetArn="arn:minio:replication::45da043c-14f5-4da4-9316-aba5f77bf730:testbucket"} 2.2015663e+07

Co-authored-by: Klaus Post <klauspost@gmail.com>
2021-11-17 12:10:57 -08:00
Harshavardhana 5b68f8ea6a honor requests_max based on cgroup_limits if configured (#13673)
container limits would not be properly honored in
our current implementation, mem.VirtualMemory()
function only reads /proc/meminfo which points to
the host system information inside the container.
2021-11-17 09:55:45 -08:00
Harshavardhana 8378bc9958 support dynamic redirect_uri based on incoming 'host' header (#13666)
This feature is useful in situations when console is exposed
over multiple intranent or internet entities when users are
connecting over local IP v/s going through load balancer.

Related console work was merged here

https://github.com/minio/console/commit/373bfbfe3f16e6f7d376fb9765a4610d76cf15a2
2021-11-16 18:40:39 -08:00
Krishnan Parthasarathi 367cb48096 reduceErrs to handle context.Canceled errors (#13670)
With this change, reduceErrs will group all errors due to 
context cancelation as the same.

e.g, Following are errors due to context cancelation seen 
from 3 remote disks. Their error values are different but 
they are all caused due to the same context cancelation.

['Post
"http://minio2:9000/minio/storage/data1/v37/statvol?disk-id=101cbc99-f5d2-4a9d-b18b-97e872b3e4a7&volume=mybucket":
context canceled',
 'Post
 "http://minio3:9000/minio/storage/data1/v37/statvol?disk-id=7a84474b-a4fd-4b80-8aab-d308a587c280&volume=mybucket":
 context canceled',
 'Post
 "http://minio4:9000/minio/storage/data1/v37/statvol?disk-id=d60d571a-83c8-487d-9e14-beebc94682d2&volume=mybucket":
 context canceled']
2021-11-16 15:26:48 -08:00
Harshavardhana 661b263e77 add gocritic/ruleguard checks back again, cleanup code. (#13665)
- remove some duplicated code
- reported a bug, separately fixed in #13664
- using strings.ReplaceAll() when needed
- using filepath.ToSlash() use when needed
- remove all non-Go style comments from the codebase

Co-authored-by: Aditya Manthramurthy <donatello@users.noreply.github.com>
2021-11-16 09:28:29 -08:00
Aditya Manthramurthy 07c5e72cdb add thread context in surrounding function into IAM functions (#13658) 2021-11-15 14:14:22 -08:00
Harshavardhana 7752cdbfaf fix: restored object to preserve x-amz-meta properly (#13664)
with SelectRestoreRequest OutputLocation provides
additional metadata for the object, this is not
preserved due to argument order change.
2021-11-15 13:25:55 -08:00
Harshavardhana 4545ecad58 ignore swapped drives instead of throwing errors (#13655)
- add checks such that swapped disks are detected
  and ignored - never used for normal operations.

- implement `unrecognizedDisk` to be ignored with
  all operations returning `errDiskNotFound`.

- also add checks such that we do not load unexpected
  disks while connecting automatically.

- additionally humanize the values when printing the errors.

Bonus: fixes handling of non-quorum situations in
getLatestFileInfo(), that does not work when 2 drives
are down, currently this function would return errors
incorrectly.
2021-11-15 09:46:55 -08:00
Harshavardhana ac74237f01 add explicit deny support for service accounts (#13657)
creating service accounts is implicitly enabled
for all users, this PR however adds support to
reject creating service accounts, with an explicit
"Deny" policy.
2021-11-15 06:57:52 -08:00
Harshavardhana 5a36179c19 update go.mod to v0.12.3 2021-11-13 09:05:28 -08:00
Harshavardhana 82d73f387d add tool to read healing.bin for debugging (#13650) 2021-11-12 16:18:53 -08:00
Aditya Manthramurthy e8c6314770 IAM: init IAM with Init() rather than InitStore() in tests (#13643)
- rename InitStore() to initStore() and fix tests

- Use IAMSys.Lock() only when IAMSys struct is being mutated
2021-11-11 21:03:02 -08:00
Aditya Manthramurthy 087c1b98dc Add tests for OpenID STS creds and add to CI (#13638) 2021-11-11 11:23:30 -08:00
Harshavardhana 68c5ad83fb fix: backend not reachable should be more descriptive (#13634) 2021-11-10 22:33:17 -08:00
Harshavardhana 5acc8c0134 add multi-site replication tests (#13631) 2021-11-10 18:18:09 -08:00
Klaus Post c897b6a82d fix: missing entries on first list resume (#13627)
On first list resume or when specifying a custom markers entries could be missed in rare cases.

Do conservative truncation of entries when forwarding.

Replaces #13619
2021-11-10 10:41:21 -08:00
Shireesh Anjal d008e90d50 Support dynamic reset of minio config (#13626)
If a given MinIO config is dynamic (can be changed without restart),
ensure that it can be reset also without restart.

Signed-off-by: Shireesh Anjal <shireesh@minio.io>
2021-11-10 10:01:32 -08:00
Harshavardhana ea820b30bf fix: use equalFold() instead of lower and compare (#13624) 2021-11-10 08:12:50 -08:00
Poorna K 03725dc015 Default multipart caching to writethrough (#13613)
when `MINIO_CACHE_COMMIT` is set.

- `writeback` caching applies only to single 
uploads. When cache commit mode is 
`writeback`, default multipart caching to be
synchronous.

- Add writethrough caching for single uploads
2021-11-10 08:12:03 -08:00
Harshavardhana 0a6f9bc1eb allocate new highwayhash for each string hash (#13623)
fixes #13622
2021-11-09 15:28:08 -08:00
Aditya Manthramurthy 1946922de3 Add CI for etcd IAM backend (#13614)
Runs when ETCD_SERVER env var is set
2021-11-09 09:25:13 -08:00
Minio Trusted edf1f4233b Update yaml files to latest version RELEASE.2021-11-09T03-21-45Z 2021-11-09 04:51:05 +00:00
Harshavardhana f4b55ea7a7 update console to v0.12.2 2021-11-08 19:21:45 -08:00
Aditya Manthramurthy 8dfd1f03e9 fix: IAM initialization crash with etcd store (#13612) 2021-11-08 12:55:27 -08:00
Harshavardhana acf26c5ab7 re-arrange metacache struct to be optimal (#13609) 2021-11-08 10:26:08 -08:00
Klaus Post d9800c8135 fix: make sure to log panic in handlers (#13611) 2021-11-08 09:28:13 -08:00
Harshavardhana 02bef7560f add missing Copyright header 2021-11-08 09:13:15 -08:00
Daniel A. Ochoa 07dd0692b6 Fix hdfs gateway concurrent map writes (#13596)
Co-authored-by: Harshavardhana <harsha@minio.io>
2021-11-08 09:07:58 -08:00
Klaus Post 4f3317effe Close stream on panic (#13605)
Always close streamHTTPResponse on panic on main thread to avoid 
write/flush after response handler has returned.
2021-11-08 08:41:27 -08:00
Klaus Post 9afdbe3648 fix: RLock UID memory leak (#13607)
UID were misnamed in RLock, leading to memory buildup.

Regression in #13430
2021-11-08 07:35:50 -08:00
Aditya Manthramurthy fe0df01448 fix: locking in some situations for IAM store (#13595)
- Fix a bug where read locks were taken instead of write locks in some situations
- Remove an unnecessary lock when updating based on notifications.
2021-11-07 17:42:32 -08:00
Harshavardhana 12e6907512 apply spelling checks for US locale (#13599) 2021-11-07 01:22:59 -08:00
Harshavardhana 5aef492b4c update disk-caching design guide 2021-11-07 01:21:34 -08:00
Harshavardhana 5d7ed8ff7d update S3 gateway limitation docs 2021-11-06 23:24:48 -07:00
Harshavardhana b1754fc5ff update go.mod and CREDITS 2021-11-06 11:39:17 -07:00
Harshavardhana 19bbf3e142 update CREDITS 2021-11-05 13:53:21 -07:00
jiangfucheng e1755275a0 resume heal from previous object instead of bucket after server restart (#13581) 2021-11-05 13:10:41 -07:00
Harshavardhana 520037e721 move to jwt-go v4 with correct releases (#13586) 2021-11-05 12:20:08 -07:00
Minio Trusted cbb0828ab8 Update yaml files to latest version RELEASE.2021-11-05T09-16-26Z 2021-11-05 10:03:56 +00:00
Andreas Auernhammer 8774d10bdf sts: always verify the key usage of client certificates (#13583)
This commit makes the MinIO server behavior more consistent
w.r.t. key usage verification.

When MinIO verifies the client certificates it also checks
that the client certificate is valid of client authentication
(or any (i.e. wildcard) usage).

However, the MinIO server used to not verify the client key usage
when client certificate verification was disabled.
Now, the MinIO server verifies the client key usage even when
client certificate verification has been disabled. This makes
the MinIO behavior more consistent from a client's perspective.

Now, a client certificate has to be valid for client authentication
in all cases.

Signed-off-by: Andreas Auernhammer <hi@aead.dev>
2021-11-05 02:16:26 -07:00
Harshavardhana df9f479d58 update console to v0.12.1 2021-11-04 16:43:59 -07:00
Harshavardhana 8bb52c9c2a fix: ignore disks that are available but not writable (#13585)
This is to allow replacing drives while some drives
while available are not writable.
2021-11-04 16:42:49 -07:00
Aditya Manthramurthy 947c423824 fix: user DN filtering that causes some unnecessary logs (#13584)
Additionally, remove the unnecessary `isUsingLookupBind` field in the LDAP struct
2021-11-04 13:11:20 -07:00
Harshavardhana c3d24fb26d use single encoder for sending speedtest results (#13579)
Bonus: if runs have PUT higher then capture it anyways
to display an unexpected result, which provides a way
to understand what might be slowing things down on the
system.

For example on a Data24 WDC setup it is clearly visible
there is a bug in the hardware.

```
./mc admin speedtest wdc/
⠧ Running speedtest (With 64 MiB object size, 32 concurrency) PUT: 31 GiB/s GET: 24 GiB/s
⠹ Running speedtest (With 64 MiB object size, 48 concurrency) PUT: 38 GiB/s GET: 24 GiB/s

MinIO 2021-11-04T06:08:33Z, 6 servers, 48 drives
PUT: 38 GiB/s, 605 objs/s
GET: 24 GiB/s, 383 objs/s
```

Reads are almost 14GiB/sec slower than Writes which
is practically not possible.
2021-11-04 12:11:52 -07:00
Pavel M 112f9ae087 claim exp should be integer (#13582)
claim exp can be 

- float64
- json.Number

As per OIDC spec https://openid.net/specs/openid-connect-core-1_0.html#IDToken

Avoid using strings since the upstream library only supports these two types now.
2021-11-04 12:03:43 -07:00
Aditya Manthramurthy 01b9ff54d9 Add LDAP STS tests and workflow for CI (#13576)
Runs LDAP tests with openldap container on GH Actions
2021-11-04 08:16:30 -07:00
Aditya Manthramurthy 64a1904136 Remove unused GlobalServiceDoneCh (#13578) 2021-11-04 08:15:10 -07:00
Aditya Manthramurthy bce6864785 Add tests to verify default server policies (#13575)
Check that they are present and that they can be modified by user
2021-11-03 19:49:05 -07:00
Aditya Manthramurthy ecd54b4cba Move all IAM storage functionality into iam store type (#13567)
This reverts commit 091a7ae359.

- Ensure all actions accessing storage lock properly.

- Behavior change: policies can be deleted only when they
  are not associated with any active credentials.

Also adds fix for accidental canned policy removal that was present in the
reverted version of the change.
2021-11-03 19:47:49 -07:00
Harshavardhana ca2b288a4b update console to v0.12.0 2021-11-03 16:23:45 -07:00
Harshavardhana 1016fbb8f9 feat: detect starting from windows explorer (#13570)
Windows users often click on the binary without
knowing MinIO is a command-line tool and should be
run from a terminal. Throw a message to guide them
on what to do.

Co-authored-by: Klaus Post <klauspost@gmail.com>
2021-11-03 14:22:13 -07:00
Harshavardhana be3f81c7ec remove unused activeIOCount in single drive mode (#13574) 2021-11-03 12:29:45 -07:00
Minio Trusted 9f3c151c3c Update yaml files to latest version RELEASE.2021-11-03T03-36-36Z 2021-11-03 06:48:34 +00:00
Harshavardhana 9735f3d8f2 fix missing go.sum changes 2021-11-02 20:36:36 -07:00
Harshavardhana 34680c5ccf fix: SQL select to honor limits properly for array queries (#13568)
added tests to cover the scenarios as well.
2021-11-02 19:14:46 -07:00
Krishna Srinivas 58934e5881 Support live updates for clients during speedtest (#13566) 2021-11-02 15:27:03 -07:00
Harshavardhana ad3f98b8e7 add util-linux RPM for setpriv command 2021-11-02 14:25:01 -07:00
Poorna K 7c33a33ef3 cache: fix commit value lookup in config (#13551) 2021-11-02 14:20:52 -07:00
Poorna K 3dfcca68e6 fix race in TestComputeActions test (#13564) 2021-11-02 14:20:15 -07:00
Harshavardhana 73b74c94a1 remove unnecessary RPMs to reduce security reports (#13565) 2021-11-02 14:15:46 -07:00
Harshavardhana 18338d60d5 treat all 2xx, 3xx as good status-codes
fixes #13560
2021-11-02 14:12:43 -07:00
Harshavardhana e106070640 update docs to mention the expected behavior for requests_max
fixes #13561
2021-11-02 14:10:21 -07:00
Harshavardhana 091a7ae359 Revert "Move all IAM storage functionality into iam store type (#13541)"
This reverts commit caadcc3ed8.
2021-11-02 13:51:42 -07:00
Krishna Srinivas 70160aeab3 Remove IOPS autotuning and simplify autotune code (#13554) 2021-11-02 13:03:00 -07:00
jandres - moscardo 1aa08f594d Update README.md prometheus (#13514)
Modify the doc to warn users about Prometheus sending `domain:port`
2021-11-02 12:27:30 -07:00
Harshavardhana 14d8a931fe re-use io.Copy buffers with 32k pools (#13553)
Borrowed idea from Go's usage of this
optimization for ReadFrom() on client
side, we should re-use the 32k buffers
io.Copy() allocates for generic copy
from a reader to writer.

the performance increase for reads for
really tiny objects is at this range
after this change.

> * Fastest: +7.89% (+1.3 MiB/s) throughput, +7.89% (+1308.1) obj/s
2021-11-02 08:11:50 -07:00
Harshavardhana 30ba85bc67 no need to write storageClass globally (#13555)
fixes #13548
2021-11-02 08:11:20 -07:00
Aditya Manthramurthy caadcc3ed8 Move all IAM storage functionality into iam store type (#13541)
- Ensure all actions accessing storage lock properly.

- Behavior change: policies can be deleted only when they 
  are not associated with any active credentials.
2021-11-01 21:58:07 -07:00
Poorna K 26f55472c6 fix: clean up dangling buckets during bucket delete (#13523) 2021-11-01 21:52:45 -07:00
Aditya Manthramurthy 79a58e275c fix: race in delete user functionality (#13547)
- The race happens with a goroutine that refreshes IAM cache data from storage.
- It could lead to deleted users re-appearing as valid live credentials.
- This change also causes CI to run tests without a race flag (in addition to
running it with).
2021-11-01 15:03:07 -07:00
Aditya Manthramurthy 900e584514 CI: Cancel in-progress jobs when a PR is updated (#13552)
- This should lead to faster results as jobs will be queued for shorter periods
when PRs are updated.

- Current behavior is that previously running CI jobs for an updated PR run to
completion needlessly, and cause new CI jobs to be queued.

Ref: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#concurrency
2021-11-01 13:42:48 -07:00
Harshavardhana bb639d9f29 remove double reads delete versions (#13544)
deleting collection of versions belonging
to same object, we can avoid re-reading
the xl.meta from the disk instead purge
all the requested versions in-memory,

the tradeoff is to allocate a map to de-dup
the versions, allow disks to be read only
once per object.

additionally reduce the data transfer between
nodes by shortening msgp data values.
2021-11-01 10:50:07 -07:00
Poorna K 15dcacc1fc Add support for caching multipart in writethrough mode (#13507) 2021-11-01 08:11:58 -07:00
Harshavardhana 6d53e3c2d7 reduce number of middleware handlers (#13546)
- combine similar looking functionalities into single
  handlers, and remove unnecessary proxying of the
  requests at handler layer.

- remove bucket forwarding handler as part of default setup
  add it only if bucket federation is enabled.

Improvements observed for 1kiB object reads.
```
-------------------
Operation: GET
Operations: 4538555 -> 4595804
* Average: +1.26% (+0.2 MiB/s) throughput, +1.26% (+190.2) obj/s
* Fastest: +4.67% (+0.7 MiB/s) throughput, +4.67% (+739.8) obj/s
* 50% Median: +1.15% (+0.2 MiB/s) throughput, +1.15% (+173.9) obj/s
```
2021-11-01 08:04:03 -07:00
Klaus Post 8ed7346273 Disable AVX512 on Darwin (#13550)
Preemptively disable AVX512 until https://github.com/golang/go/issues/49233 has been resolved.

This potentially affects reedsolomon, simdjson, sha256-simd, md5-simd packages.

Init order requires a separate package since main itself is initialized last, but imports are initialized in the order they are imported from main (confirmed).
2021-11-01 08:03:16 -07:00
Harshavardhana 3c1220adca add tests for default governance replication 2021-10-30 08:57:59 -07:00
Harshavardhana 4ed0eb7012 remove double reads updating object metadata (#13542)
Removes RLock/RUnlock for updating metadata,
since we already take a write lock to update
metadata, this change removes reading of xl.meta
as well as an additional lock, the performance gain
should increase 3x theoretically for

- PutObjectRetention
- PutObjectLegalHold

This optimization is mainly for Veeam like
workloads that require a certain level of iops
from these API calls, we were losing iops.
2021-10-30 08:22:04 -07:00
Harshavardhana 2af5445309 update 3-site replication tests 2021-10-29 22:09:55 -07:00
Harshavardhana abb1916bda update list objects limit to match S3 spec 2021-10-28 18:21:51 -07:00
Klaus Post 9424dca9e4 jwt: Improve allocations (#13532)
Avoid string -> byte allocations.

```
BenchmarkParseJWTStandardClaims-32       3527152           343.2 ns/op      1489 B/op         21 allocs/op
BenchmarkParseJWTStandardClaims-32       4713199           259.2 ns/op       706 B/op         16 allocs/op

BenchmarkParseJWTMapClaims-32        2666668           448.7 ns/op      1883 B/op         32 allocs/op
BenchmarkParseJWTMapClaims-32        3120709           377.1 ns/op      1227 B/op         28 allocs/op
```
2021-10-28 17:04:48 -07:00
Harshavardhana db84bb9bd3 avoid atomics for self contained reader/writers (#13531)
read/writers are not concurrent in handlers
and self contained - no need to use atomics on
them.

avoids unnecessary contentions where it's not
required.
2021-10-28 17:03:00 -07:00
Klaus Post c603f85488 readAllData: Reuse small file buffers (#13530)
(Re)use small buffers for small readAllData operations.
2021-10-28 17:02:22 -07:00
Aditya Manthramurthy 2f1ee25f50 Add test for AssumeRole with internal IDP (#13527) 2021-10-28 09:05:51 -07:00
Klaus Post 7bdf9005e5 Remove HTTP flushes for returning handlers (#13528)
When handlers return they are automatically flushed. Manual flushing can force responsewriters to use suboptimal paths and generally just wastes CPU.
2021-10-28 07:36:34 -07:00
Klaus Post d9c1d79e30 Protect logger targets (#13529)
Logger targets were not race protected against concurrent updates from for example `HTTPConsoleLoggerSys`.

Restrict direct access to targets and make slices immutable so a returned slice can be processed safely without locks.
2021-10-28 07:35:28 -07:00
Harshavardhana bd88b86919 update console to latest to fix CVE-2021-42836 2021-10-27 21:14:02 -07:00
Minio Trusted 8e29ae8c44 Update yaml files to latest version RELEASE.2021-10-27T16-29-42Z 2021-10-28 02:45:22 +00:00
moon d158607f8e fix(AuditLog): panic while st is nil (#13510) 2021-10-27 09:29:42 -07:00
Krishnan Parthasarathi 939fbb3c38 ilm: Make per-tier stats available via admin-tier-info (#13381) 2021-10-23 18:38:33 -07:00
Aditya Manthramurthy 3b9dfa9d29 Add IAM service account tests (#13502) 2021-10-23 09:36:57 -07:00
Minio Trusted 0c76fb57f2 Update yaml files to latest version RELEASE.2021-10-23T03-28-24Z 2021-10-23 07:46:29 +00:00
483 changed files with 32440 additions and 11779 deletions
+9 -6
View File
@@ -1,23 +1,26 @@
name: Go
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
jobs:
build:
name: MinIO crosscompile tests on ${{ matrix.go-version }} and ${{ matrix.os }}
name: Build Tests with Go ${{ matrix.go-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
go-version: [1.16.x, 1.17.x]
go-version: [1.17.x]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '12'
- uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
+48
View File
@@ -0,0 +1,48 @@
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
jobs:
build:
name: Go ${{ matrix.go-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
go-version: [1.17.x]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- uses: actions/cache@v2
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-${{ matrix.go-version }}-go-
- name: Build on ${{ matrix.os }}
if: matrix.os == 'ubuntu-latest'
env:
CGO_ENABLED: 0
GO111MODULE: on
MINIO_KMS_KES_CERT_FILE: /home/runner/work/minio/minio/.github/workflows/root.cert
MINIO_KMS_KES_KEY_FILE: /home/runner/work/minio/minio/.github/workflows/root.key
MINIO_KMS_KES_ENDPOINT: "https://play.min.io:7373"
MINIO_KMS_KES_KEY_NAME: "my-minio-key"
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
+28 -3
View File
@@ -1,23 +1,47 @@
name: Go
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
jobs:
build:
name: MinIO tests on ${{ matrix.go-version }} and ${{ matrix.os }}
name: Go ${{ matrix.go-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
go-version: [1.16.x, 1.17.x]
go-version: [1.17.x]
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- uses: actions/cache@v2
if: matrix.os == 'ubuntu-latest'
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-${{ matrix.go-version }}-go-
- uses: actions/cache@v2
if: matrix.os == 'windows-latest'
with:
path: |
%LocalAppData%\go-build
~/go/pkg/mod
key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-${{ matrix.go-version }}-go-
- name: Build on ${{ matrix.os }}
if: matrix.os == 'windows-latest'
env:
@@ -39,4 +63,5 @@ jobs:
curl -L -o nancy https://github.com/sonatype-nexus-community/nancy/releases/download/${nancy_version}/nancy-${nancy_version}-linux-amd64 && chmod +x nancy
go list -deps -json ./... | jq -s 'unique_by(.Module.Path)|.[]|select(has("Module"))|.Module' | ./nancy sleuth
make
make test
make test-race
+17 -4
View File
@@ -1,23 +1,37 @@
name: Go
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
jobs:
build:
name: MinIO Setup on ${{ matrix.go-version }} and ${{ matrix.os }}
name: Go ${{ matrix.go-version }} on ${{ matrix.os }} - healing
runs-on: ${{ matrix.os }}
strategy:
matrix:
go-version: [1.16.x, 1.17.x]
go-version: [1.17.x]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- uses: actions/cache@v2
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-${{ matrix.go-version }}-go-
- name: Build on ${{ matrix.os }}
if: matrix.os == 'ubuntu-latest'
env:
@@ -32,4 +46,3 @@ jobs:
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
make verify
make verify-healing
+95
View File
@@ -0,0 +1,95 @@
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
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"
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.17.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@v2
- uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- uses: actions/cache@v2
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-${{ matrix.go-version }}-go-
- name: Test LDAP/OpenID/Etcd combo
env:
LDAP_TEST_SERVER: ${{ matrix.ldap }}
ETCD_SERVER: ${{ matrix.etcd }}
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 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
+46
View File
@@ -0,0 +1,46 @@
name: Multi-site replication 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
jobs:
replication-test:
name: Replication Tests with Go ${{ matrix.go-version }}
runs-on: ubuntu-latest
strategy:
matrix:
go-version: [1.17.x]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- uses: actions/cache@v2
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-${{ matrix.go-version }}-go-
- 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
+31
View File
@@ -0,0 +1,31 @@
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
jobs:
build:
name: Go ${{ matrix.go-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
go-version: [1.17.x]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v1
- uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Start upgrade tests
run: |
make test-upgrade
+12 -1
View File
@@ -21,4 +21,15 @@ prime/
stage/
.sia_temp/
config.json
node_modules/
node_modules/
mc.*
s3-check-md5*
xl-meta*
healing-*
inspect*
200M*
hash-set
minio.RELEASE*
mc
nancy
inspects/*
+17 -1
View File
@@ -23,12 +23,28 @@ linters:
- structcheck
- unconvert
- varcheck
- gocritic
- gofumpt
linters-settings:
gofumpt:
lang-version: "1.17"
# Choose whether or not to use the extra rules that are disabled
# by default
extra-rules: false
issues:
exclude-use-default: false
exclude:
- should have a package comment
- error strings should not be capitalized or end with punctuation or a newline
# todo fix these when we get enough time.
- "singleCaseSwitch: should rewrite switch statement to if statement"
- "unlambda: replace"
- "captLocal:"
- "ifElseChain:"
- "elseif:"
service:
golangci-lint-version: 1.20.0 # use the fixed version to not introduce new linters unexpectedly
golangci-lint-version: 1.43.0 # use the fixed version to not introduce new linters unexpectedly
+6
View File
@@ -0,0 +1,6 @@
# 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.
+1263 -529
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -1,5 +1,8 @@
FROM minio/minio:latest
COPY ./minio /opt/bin/minio
COPY dockerscripts/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
VOLUME ["/data"]
+3 -10
View File
@@ -1,14 +1,7 @@
FROM minio/minio:edge
FROM minio/minio:latest
LABEL maintainer="MinIO Inc <dev@min.io>"
COPY minio /usr/bin/
COPY dockerscripts/docker-entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/minio && \
chmod +x /usr/bin/docker-entrypoint.sh
EXPOSE 9000
COPY ./minio /opt/bin/minio
COPY dockerscripts/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
+50
View File
@@ -0,0 +1,50 @@
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4
ARG RELEASE
LABEL name="MinIO" \
vendor="MinIO Inc <dev@min.io>" \
maintainer="MinIO Inc <dev@min.io>" \
version="${RELEASE}" \
release="${RELEASE}" \
summary="MinIO is a High Performance Object Storage, API compatible with Amazon S3 cloud storage service." \
description="MinIO object storage is fundamentally different. Designed for performance and the S3 API, it is 100% open-source. MinIO is ideal for large, private cloud environments with stringent security requirements and delivers mission-critical availability across a diverse range of workloads."
ENV MINIO_ACCESS_KEY_FILE=access_key \
MINIO_SECRET_KEY_FILE=secret_key \
MINIO_ROOT_USER_FILE=access_key \
MINIO_ROOT_PASSWORD_FILE=secret_key \
MINIO_KMS_SECRET_KEY_FILE=kms_master_key \
MINIO_UPDATE_MINISIGN_PUBKEY="RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav" \
MINIO_CONFIG_ENV_FILE=config.env \
PATH=/opt/bin:$PATH
COPY dockerscripts/verify-minio.sh /usr/bin/verify-minio.sh
COPY dockerscripts/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
COPY CREDITS /licenses/CREDITS
COPY LICENSE /licenses/LICENSE
RUN \
microdnf clean all && \
microdnf update --nodocs && \
microdnf install curl ca-certificates shadow-utils util-linux --nodocs && \
rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm && \
microdnf install minisign --nodocs && \
mkdir -p /opt/bin && chmod -R 777 /opt/bin && \
curl -s -q https://dl.min.io/server/minio/hotfixes/linux-amd64/archive/minio.${RELEASE} -o /opt/bin/minio && \
curl -s -q https://dl.min.io/server/minio/hotfixes/linux-amd64/archive/minio.${RELEASE}.sha256sum -o /opt/bin/minio.sha256sum && \
curl -s -q https://dl.min.io/server/minio/hotfixes/linux-amd64/archive/minio.${RELEASE}.minisig -o /opt/bin/minio.minisig && \
microdnf clean all && \
chmod +x /opt/bin/minio && \
chmod +x /usr/bin/docker-entrypoint.sh && \
chmod +x /usr/bin/verify-minio.sh && \
/usr/bin/verify-minio.sh && \
microdnf clean all
EXPOSE 9000
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
VOLUME ["/data"]
CMD ["minio"]
+5 -3
View File
@@ -19,7 +19,7 @@ ENV MINIO_ACCESS_KEY_FILE=access_key \
MINIO_KMS_SECRET_KEY_FILE=kms_master_key \
MINIO_UPDATE_MINISIGN_PUBKEY="RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav" \
MINIO_CONFIG_ENV_FILE=config.env \
PATH=$PATH:/opt/bin
PATH=/opt/bin:$PATH
COPY dockerscripts/verify-minio.sh /usr/bin/verify-minio.sh
COPY dockerscripts/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
@@ -27,8 +27,9 @@ COPY CREDITS /licenses/CREDITS
COPY LICENSE /licenses/LICENSE
RUN \
microdnf clean all && \
microdnf update --nodocs && \
microdnf install curl ca-certificates shadow-utils util-linux iproute iputils --nodocs && \
microdnf install curl ca-certificates shadow-utils util-linux --nodocs && \
rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm && \
microdnf install minisign --nodocs && \
mkdir -p /opt/bin && chmod -R 777 /opt/bin && \
@@ -39,7 +40,8 @@ RUN \
chmod +x /opt/bin/minio && \
chmod +x /usr/bin/docker-entrypoint.sh && \
chmod +x /usr/bin/verify-minio.sh && \
/usr/bin/verify-minio.sh
/usr/bin/verify-minio.sh && \
microdnf clean all
EXPOSE 9000
+5 -3
View File
@@ -19,7 +19,7 @@ ENV MINIO_ACCESS_KEY_FILE=access_key \
MINIO_KMS_SECRET_KEY_FILE=kms_master_key \
MINIO_UPDATE_MINISIGN_PUBKEY="RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav" \
MINIO_CONFIG_ENV_FILE=config.env \
PATH=$PATH:/opt/bin
PATH=/opt/bin:$PATH
COPY dockerscripts/verify-minio.sh /usr/bin/verify-minio.sh
COPY dockerscripts/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
@@ -27,8 +27,9 @@ COPY CREDITS /licenses/CREDITS
COPY LICENSE /licenses/LICENSE
RUN \
microdnf clean all && \
microdnf update --nodocs && \
microdnf install curl ca-certificates shadow-utils util-linux iproute iputils --nodocs && \
microdnf install curl ca-certificates shadow-utils util-linux --nodocs && \
rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm && \
microdnf install minisign --nodocs && \
mkdir -p /opt/bin && chmod -R 777 /opt/bin && \
@@ -39,7 +40,8 @@ RUN \
chmod +x /opt/bin/minio && \
chmod +x /usr/bin/docker-entrypoint.sh && \
chmod +x /usr/bin/verify-minio.sh && \
/usr/bin/verify-minio.sh
/usr/bin/verify-minio.sh && \
microdnf clean all
EXPOSE 9000
+53 -15
View File
@@ -19,9 +19,9 @@ help: ## print this help
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 $(GOPATH)/bin v1.40.1
@which msgp 1>/dev/null || (echo "Installing msgp" && go install -v github.com/tinylib/msgp@v1.1.3)
@which stringer 1>/dev/null || (echo "Installing stringer" && go install -v golang.org/x/tools/cmd/stringer)
@echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin v1.43.0
@echo "Installing msgp" && go install -v github.com/tinylib/msgp@v1.1.7-0.20211026165309-e818a1881b0e
@echo "Installing stringer" && go install -v golang.org/x/tools/cmd/stringer@latest
crosscompile: ## cross compile minio
@(env bash $(PWD)/buildscripts/cross-compile.sh)
@@ -34,45 +34,83 @@ check-gen: ## check for updated autogenerated files
lint: ## runs golangci-lint suite of linters
@echo "Running $@ check"
@GO111MODULE=on ${GOPATH}/bin/golangci-lint cache clean
@GO111MODULE=on ${GOPATH}/bin/golangci-lint run --build-tags kqueue --timeout=10m --config ./.golangci.yml
@${GOPATH}/bin/golangci-lint run --build-tags kqueue --timeout=10m --config ./.golangci.yml
check: test
test: verifiers build ## builds minio, runs linters, tests
@echo "Running unit tests"
@GOGC=25 GO111MODULE=on CGO_ENABLED=0 go test -tags kqueue ./... 1>/dev/null
@CGO_ENABLED=0 go test -tags kqueue ./...
test-race: verifiers build
test-upgrade: build
@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: build ## verify IAM (external IDP, etcd backends)
@echo "Running tests for IAM (external IDP, etcd backends)"
@CGO_ENABLED=0 go test -tags kqueue -v -run TestIAM* ./cmd
@echo "Running tests for IAM (external IDP, etcd backends) with -race"
@CGO_ENABLED=1 go test -race -tags kqueue -v -run TestIAM* ./cmd
test-replication: install ## verify multi site replication
@echo "Running tests for replicating three sites"
@(env bash $(PWD)/docs/bucket/replication/setup_3site_replication.sh)
test-site-replication-ldap: install ## 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 ## 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 ## 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)
verify: ## verify minio various setups
@echo "Verifying build with race"
@GO111MODULE=on CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
@CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
@(env bash $(PWD)/buildscripts/verify-build.sh)
verify-healing: ## verify healing and replacing disks with minio binary
@echo "Verify healing build with race"
@GO111MODULE=on CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
@CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
@(env bash $(PWD)/buildscripts/verify-healing.sh)
@(env bash $(PWD)/buildscripts/unaligned-healing.sh)
build: checks ## builds minio to $(PWD)
@echo "Building minio binary to './minio'"
@GO111MODULE=on CGO_ENABLED=0 go build -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
@CGO_ENABLED=0 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 TAG := "minio/minio:$(shell git describe --tags --abbrev=0).hotfix.$(shell git rev-parse --short HEAD)")
hotfix: hotfix-vars install ## builds minio binary with hotfix tags
$(eval VERSION := $(shell git describe --tags --abbrev=0).hotfix.$(shell git rev-parse --short HEAD))
$(eval TAG := "minio/minio:$(VERSION)")
docker-hotfix: hotfix checks ## builds minio docker container with hotfix tags
hotfix: hotfix-vars install ## builds minio binary with hotfix tags
@mv -f ./minio ./minio.$(VERSION)
@minisign -qQSm ./minio.$(VERSION) -s "${CRED_DIR}/minisign.key" < "${CRED_DIR}/minisign-passphrase"
@sha256sum < ./minio.$(VERSION) | sed 's, -,minio.$(VERSION),g' > minio.$(VERSION).sha256sum
hotfix-push: hotfix
@scp -r minio.$(VERSION)* minio@dl-0.minio.io:~/releases/server/minio/hotfixes/linux-amd64/archive/
@scp -r minio.$(VERSION)* minio@dl-1.minio.io:~/releases/server/minio/hotfixes/linux-amd64/archive/
docker-hotfix-push: docker-hotfix
@docker push $(TAG)
docker-hotfix: hotfix-push checks ## builds minio docker container with hotfix tags
@echo "Building minio docker image '$(TAG)'"
@docker build -t $(TAG) . -f Dockerfile.dev
@docker build -t $(TAG) --build-arg RELEASE=$(VERSION) . -f Dockerfile.hotfix
docker: build checks ## builds minio docker container
@echo "Building minio docker image '$(TAG)'"
@docker build -t $(TAG) . -f Dockerfile.dev
@docker build -t $(TAG) . -f Dockerfile
install: build ## builds minio and installs it to $GOPATH/bin.
@echo "Installing minio binary to '$(GOPATH)/bin/minio'"
+7 -8
View File
@@ -5,7 +5,7 @@
MinIO is a High Performance Object Storage released under GNU Affero General Public License v3.0. It is API compatible with Amazon S3 cloud storage service. Use MinIO to build high performance infrastructure for machine learning, analytics and application data workloads.
This README provides quickstart instructions on running MinIO on baremetal hardware, including container-based installations. For Kubernetes environments, use the [MinIO Kubernetes Operator](https://github.com/minio/operator/blob/master/README.md).
This README provides quickstart instructions on running MinIO on bare metal hardware, including container-based installations. For Kubernetes environments, use the [MinIO Kubernetes Operator](https://github.com/minio/operator/blob/master/README.md).
# Container Installation
@@ -124,7 +124,7 @@ You can also connect using any S3-compatible tool, such as the MinIO Client `mc`
# Install from Source
Use the following commands to compile and run a standalone MinIO server from source. Source installation is only intended for developers and advanced users. 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.16](https://golang.org/dl/#stable)
Use the following commands to compile and run a standalone MinIO server from source. Source installation is only intended for developers and advanced users. 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.17](https://golang.org/dl/#stable)
```sh
GO111MODULE=on go install github.com/minio/minio@latest
@@ -237,13 +237,11 @@ mc admin update <minio alias, e.g., myminio>
## Important things to remember during MinIO upgrades
- Container environments are advised to update the container images instead of updating binaries inside the container.
- `mc admin update` is disabled in kubernetes/container environments, container environments provide their own mechanisms to rollout of updates.
- `mc admin update` will only work if the user running MinIO has write access to the parent directory where the binary is located, for example if the current binary is at `/usr/local/bin/minio`, you would need write access to `/usr/local/bin`.
- `mc admin update` updates and restarts all servers simultaneously, applications would retry and continue their respective operations upon upgrade.
- `mc admin update` is disabled in kubernetes/container environments, container environments provide their own mechanisms to rollout of updates.
- In the case of federated setups `mc admin update` should be run against each cluster individually. Avoid updating `mc` to any new releases until all clusters have been successfully updated.
- If using `kes` as KMS with MinIO, just replace the binary and restart `kes` more information about `kes` can be found [here](https://github.com/minio/kes/wiki)
- If using Vault as KMS with MinIO, ensure you have followed the Vault upgrade procedure outlined here: https://www.vaultproject.io/docs/upgrading/index.html
- If using etcd with MinIO for the federation, ensure you have followed the etcd upgrade procedure outlined here: https://github.com/etcd-io/etcd/blob/master/Documentation/upgrades/upgrading-etcd.md
# Explore Further
- [MinIO Erasure Code QuickStart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide)
@@ -257,5 +255,6 @@ mc admin update <minio alias, e.g., myminio>
Please follow MinIO [Contributor's Guide](https://github.com/minio/minio/blob/master/CONTRIBUTING.md)
# License
MinIO source is licensed under the GNU AGPLv3 license that can be found in the [LICENSE](https://github.com/minio/minio/blob/master/LICENSE) file.
MinIO [Documentation](https://github.com/minio/minio/tree/master/docs) © 2021 by MinIO, Inc is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
- MinIO source is licensed under the GNU AGPLv3 license that can be found in the [LICENSE](https://github.com/minio/minio/blob/master/LICENSE) file.
- MinIO [Documentation](https://github.com/minio/minio/tree/master/docs) © 2021 by MinIO, Inc 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)
+92
View File
@@ -0,0 +1,92 @@
#!/bin/bash
trap 'cleanup $LINENO' ERR
# shellcheck disable=SC2120
cleanup() {
MINIO_VERSION=dev docker-compose \
-f "buildscripts/upgrade-tests/compose.yml" \
rm -s -f
docker volume prune -f
}
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
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 policy 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 docker-compose -f "buildscripts/upgrade-tests/compose.yml" stop
}
main() {
MINIO_VERSION=dev 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 "$@" )
+171
View File
@@ -0,0 +1,171 @@
#!/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="$PWD/.verify-$RANDOM"
MINIO_CONFIG_DIR="$WORK_DIR/.minio"
MINIO_OLD=( "$PWD/minio.RELEASE.2021-11-24T23-19-33Z" --config-dir "$MINIO_CONFIG_DIR" server )
MINIO=( "$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" server )
function download_old_release() {
if [ ! -f minio.RELEASE.2021-11-24T23-19-33Z ]; then
curl --silent -O https://dl.minio.io/server/minio/release/linux-amd64/archive/minio.RELEASE.2021-11-24T23-19-33Z
chmod a+x minio.RELEASE.2021-11-24T23-19-33Z
fi
}
function start_minio_16drive() {
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_SHARD_DISKTIME_DELTA="5s" # do not change this as its needed for tests
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
sleep 30
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
shred --iterations=1 --size=5241856 - 1>"${WORK_DIR}/unaligned" 2>/dev/null
"${WORK_DIR}/mc" mb minio/healing-shard-bucket --quiet
"${WORK_DIR}/mc" cp \
"${WORK_DIR}/unaligned" \
minio/healing-shard-bucket/unaligned \
--disable-multipart --quiet
## "unaligned" object name gets consistently distributed
## to disks in following distribution order
##
## NOTE: if you change the name make sure to change the
## distribution order present here
##
## [15, 16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
## make sure to remove the "last" data shard
rm -rf "${WORK_DIR}/xl14/healing-shard-bucket/unaligned"
sleep 10
## Heal the shard
"${WORK_DIR}/mc" admin heal --quiet --recursive minio/healing-shard-bucket
## then remove any other data shard let's pick first disk
## - 1st data shard.
rm -rf "${WORK_DIR}/xl3/healing-shard-bucket/unaligned"
sleep 10
go build ./docs/debugging/s3-check-md5/
if ! ./s3-check-md5 \
-debug \
-access-key minio \
-secret-key minio123 \
-endpoint http://127.0.0.1:${start_port}/ 2>&1 | grep CORRUPTED; then
echo "server1 log:"
cat "${WORK_DIR}/server1.log"
echo "FAILED"
purge "$WORK_DIR"
exit 1
fi
pkill minio
sleep 3
"${MINIO[@]}" --address ":$start_port" "${WORK_DIR}/xl{1...16}" > "${WORK_DIR}/server1.log" 2>&1 &
pid=$!
disown $pid
sleep 30
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 \
-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-shard-bucket/unaligned/**)
"${WORK_DIR}/mc" mb play/inspects
"${WORK_DIR}/mc" mirror inspects play/inspects
purge "$WORK_DIR"
exit 1
fi
"${WORK_DIR}/mc" admin heal --quiet --recursive minio/healing-shard-bucket
if ! ./s3-check-md5 \
-debug \
-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-shard-bucket/unaligned/**)
"${WORK_DIR}/mc" mb play/inspects
"${WORK_DIR}/mc" mirror inspects play/inspects
purge "$WORK_DIR"
exit 1
fi
pkill minio
sleep 3
}
function main() {
download_old_release
start_port=$(shuf -i 10000-65000 -n 1)
start_minio_16drive ${start_port}
}
function purge()
{
rm -rf "$1"
}
( main "$@" )
rv=$?
purge "$WORK_DIR"
exit "$rv"
+81
View File
@@ -0,0 +1,81 @@
version: '3.7'
# 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"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
# 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:
+3
View File
@@ -0,0 +1,3 @@
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin
MINIO_BROWSER=off
+68
View File
@@ -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;
}
}
}
+1 -1
View File
@@ -66,7 +66,7 @@ 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{1...4} http://[::1]:9001${WORK_DIR}/pool-disk-sets{5...8}"
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 &
+60 -38
View File
@@ -18,54 +18,73 @@ function start_minio_3_node() {
export MINIO_ROOT_PASSWORD=minio123
export MINIO_ERASURE_SET_DRIVE_COUNT=6
start_port=$(shuf -i 10000-65000 -n 1)
start_port=$2
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/"
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 &
disown $!
pid1=$!
disown ${pid1}
"${MINIO[@]}" --address ":$[$start_port+2]" $args > "${WORK_DIR}/dist-minio-server2.log" 2>&1 &
disown $!
pid2=$!
disown $pid2
"${MINIO[@]}" --address ":$[$start_port+3]" $args > "${WORK_DIR}/dist-minio-server3.log" 2>&1 &
disown $!
pid3=$!
disown $pid3
sleep "$1"
if [ "$(pgrep -c minio)" -ne 3 ]; then
for i in $(seq 1 3); do
echo "server$i log:"
cat "${WORK_DIR}/dist-minio-server$i.log"
done
echo "FAILED"
purge "$WORK_DIR"
exit 1
if ! ps -p $pid1 1>&2 > /dev/null; then
echo "server1 log:"
cat "${WORK_DIR}/dist-minio-server1.log"
echo "FAILED"
purge "$WORK_DIR"
exit 1
fi
if ! ps -p $pid2 1>&2 > /dev/null; then
echo "server2 log:"
cat "${WORK_DIR}/dist-minio-server2.log"
echo "FAILED"
purge "$WORK_DIR"
exit 1
fi
if ! ps -p $pid3 1>&2 > /dev/null; then
echo "server3 log:"
cat "${WORK_DIR}/dist-minio-server3.log"
echo "FAILED"
purge "$WORK_DIR"
exit 1
fi
if ! pkill minio; then
for i in $(seq 1 3); do
echo "server$i log:"
cat "${WORK_DIR}/dist-minio-server$i.log"
done
echo "FAILED"
purge "$WORK_DIR"
exit 1
for i in $(seq 1 3); do
echo "server$i log:"
cat "${WORK_DIR}/dist-minio-server$i.log"
done
echo "FAILED"
purge "$WORK_DIR"
exit 1
fi
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
# forcibly killing, to proceed further properly.
if ! pkill -9 minio; then
echo "no minio process running anymore, proceed."
fi
fi
}
function check_online() {
if grep -q 'Unable to initialize sub-systems' ${WORK_DIR}/dist-minio-*.log; then
echo "1"
echo "1"
fi
}
@@ -85,33 +104,36 @@ function __init__()
}
function perform_test() {
start_minio_3_node 120
start_minio_3_node 120 $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}/*/
start_minio_3_node 120
start_minio_3_node 120 $2
rv=$(check_online)
if [ "$rv" == "1" ]; then
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
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
fi
}
function main()
{
perform_test "2"
perform_test "1"
perform_test "3"
# 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 "$@" )
-8
View File
@@ -114,8 +114,6 @@ func (api objectAPIHandlers) PutBucketACLHandler(w http.ResponseWriter, r *http.
writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL)
return
}
w.(http.Flusher).Flush()
}
// GetBucketACLHandler - GET Bucket ACL
@@ -164,8 +162,6 @@ func (api objectAPIHandlers) GetBucketACLHandler(w http.ResponseWriter, r *http.
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
w.(http.Flusher).Flush()
}
// PutObjectACLHandler - PUT Object ACL
@@ -229,8 +225,6 @@ func (api objectAPIHandlers) PutObjectACLHandler(w http.ResponseWriter, r *http.
writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL)
return
}
w.(http.Flusher).Flush()
}
// GetObjectACLHandler - GET Object ACL
@@ -283,6 +277,4 @@ func (api objectAPIHandlers) GetObjectACLHandler(w http.ResponseWriter, r *http.
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
w.(http.Flusher).Flush()
}
+1 -1
View File
@@ -155,7 +155,7 @@ func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http.
return
}
var target madmin.BucketTarget
var json = jsoniter.ConfigCompatibleWithStandardLibrary
json := jsoniter.ConfigCompatibleWithStandardLibrary
if err = json.Unmarshal(reqBytes, &target); err != nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
return
+19 -8
View File
@@ -29,7 +29,7 @@ import (
iampolicy "github.com/minio/pkg/iam/policy"
)
func validateAdminReq(ctx context.Context, w http.ResponseWriter, r *http.Request, action iampolicy.AdminAction) (ObjectLayer, auth.Credentials) {
func validateAdminReq(ctx context.Context, w http.ResponseWriter, r *http.Request, actions ...iampolicy.AdminAction) (ObjectLayer, auth.Credentials) {
// Get current object layer instance.
objectAPI := newObjectLayerFn()
if objectAPI == nil || globalNotificationSys == nil {
@@ -37,14 +37,17 @@ func validateAdminReq(ctx context.Context, w http.ResponseWriter, r *http.Reques
return nil, auth.Credentials{}
}
// Validate request signature.
cred, adminAPIErr := checkAdminRequestAuth(ctx, r, action, "")
if adminAPIErr != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
return nil, cred
for _, action := range actions {
// Validate request signature.
cred, adminAPIErr := checkAdminRequestAuth(ctx, r, action, "")
if adminAPIErr != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
return nil, cred
}
return objectAPI, cred
}
return objectAPI, cred
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return nil, auth.Credentials{}
}
// AdminError - is a generic error for all admin APIs.
@@ -83,6 +86,8 @@ func toAdminAPIErr(ctx context.Context, err error) APIError {
Description: e.Message,
HTTPStatusCode: e.StatusCode,
}
case SRError:
apiErr = errorCodes.ToAPIErrWithErr(e.Code, e.Cause)
default:
switch {
case errors.Is(err, errConfigNotFound):
@@ -103,6 +108,12 @@ func toAdminAPIErr(ctx context.Context, err error) APIError {
Description: err.Error(),
HTTPStatusCode: http.StatusServiceUnavailable,
}
case errors.Is(err, errPolicyInUse):
apiErr = APIError{
Code: "XMinioAdminPolicyInUse",
Description: "The policy cannot be removed, as it is in use",
HTTPStatusCode: http.StatusBadRequest,
}
case errors.Is(err, kes.ErrKeyExists):
apiErr = APIError{
Code: "XMinioKMSKeyExists",
+22 -15
View File
@@ -19,6 +19,7 @@ package cmd
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
@@ -78,6 +79,22 @@ func (a adminAPIHandlers) DelConfigKVHandler(w http.ResponseWriter, r *http.Requ
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
dynamic := config.SubSystemsDynamic.Contains(string(kvBytes))
if dynamic {
applyDynamic(ctx, objectAPI, cfg, r, w)
}
}
func applyDynamic(ctx context.Context, objectAPI ObjectLayer, cfg config.Config, r *http.Request, w http.ResponseWriter) {
// Apply dynamic values.
if err := applyDynamicConfig(GlobalContext, objectAPI, cfg); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
globalNotificationSys.SignalService(serviceReloadDynamic)
// Tell the client that dynamic config was applied.
w.Header().Set(madmin.ConfigAppliedHeader, madmin.ConfigAppliedTrue)
}
// SetConfigKVHandler - PUT /minio/admin/v3/set-config-kv
@@ -135,14 +152,7 @@ func (a adminAPIHandlers) SetConfigKVHandler(w http.ResponseWriter, r *http.Requ
}
if dynamic {
// Apply dynamic values.
if err := applyDynamicConfig(GlobalContext, objectAPI, cfg); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
globalNotificationSys.SignalService(serviceReloadDynamic)
// If all values were dynamic, tell the client.
w.Header().Set(madmin.ConfigAppliedHeader, madmin.ConfigAppliedTrue)
applyDynamic(ctx, objectAPI, cfg, r, w)
}
writeSuccessResponseHeadersOnly(w)
}
@@ -160,7 +170,7 @@ func (a adminAPIHandlers) GetConfigKVHandler(w http.ResponseWriter, r *http.Requ
cfg := globalServerConfig.Clone()
vars := mux.Vars(r)
var buf = &bytes.Buffer{}
buf := &bytes.Buffer{}
cw := config.NewConfigWriteTo(cfg, vars["key"])
if _, err := cw.WriteTo(buf); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
@@ -205,11 +215,9 @@ func (a adminAPIHandlers) ClearConfigHistoryKVHandler(w http.ResponseWriter, r *
return
}
}
} else {
if err := delServerConfigHistory(ctx, objectAPI, 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
}
}
@@ -326,7 +334,6 @@ func (a adminAPIHandlers) HelpConfigKVHandler(w http.ResponseWriter, r *http.Req
}
json.NewEncoder(w).Encode(rd)
w.(http.Flusher).Flush()
}
// SetConfigHandler - PUT /minio/admin/v3/config
+122 -75
View File
@@ -45,16 +45,16 @@ func (a adminAPIHandlers) SiteReplicationAdd(w http.ResponseWriter, r *http.Requ
}
var sites []madmin.PeerSite
errCode := readJSONBody(ctx, r.Body, &sites, cred.SecretKey)
if errCode != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(errCode), r.URL)
err := parseJSONBody(ctx, r.Body, &sites, cred.SecretKey)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
status, errInfo := globalSiteReplicationSys.AddPeerClusters(ctx, sites)
if errInfo.Code != ErrNone {
logger.LogIf(ctx, errInfo)
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(errInfo.Code, errInfo.Cause), r.URL)
status, err := globalSiteReplicationSys.AddPeerClusters(ctx, sites)
if err != nil {
logger.LogIf(ctx, err)
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
@@ -67,12 +67,12 @@ func (a adminAPIHandlers) SiteReplicationAdd(w http.ResponseWriter, r *http.Requ
writeSuccessResponseJSON(w, body)
}
// SRInternalJoin - PUT /minio/admin/v3/site-replication/join
// 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) SRInternalJoin(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "SRInternalJoin")
func (a adminAPIHandlers) SRPeerJoin(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "SRPeerJoin")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
@@ -81,24 +81,22 @@ func (a adminAPIHandlers) SRInternalJoin(w http.ResponseWriter, r *http.Request)
return
}
var joinArg madmin.SRInternalJoinReq
errCode := readJSONBody(ctx, r.Body, &joinArg, cred.SecretKey)
if errCode != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(errCode), r.URL)
var joinArg madmin.SRPeerJoinReq
if err := parseJSONBody(ctx, r.Body, &joinArg, cred.SecretKey); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
errInfo := globalSiteReplicationSys.InternalJoinReq(ctx, joinArg)
if errInfo.Code != ErrNone {
logger.LogIf(ctx, errInfo)
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(errInfo.Code, errInfo.Cause), r.URL)
if err := globalSiteReplicationSys.PeerJoinReq(ctx, joinArg); err != nil {
logger.LogIf(ctx, err)
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
}
// SRInternalBucketOps - PUT /minio/admin/v3/site-replication/bucket-ops?bucket=x&operation=y
func (a adminAPIHandlers) SRInternalBucketOps(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "SRInternalBucketOps")
// SRPeerBucketOps - PUT /minio/admin/v3/site-replication/bucket-ops?bucket=x&operation=y
func (a adminAPIHandlers) SRPeerBucketOps(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "SRPeerBucketOps")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
@@ -113,6 +111,8 @@ func (a adminAPIHandlers) SRInternalBucketOps(w http.ResponseWriter, r *http.Req
var err error
switch operation {
default:
err = errSRInvalidRequest(errInvalidArgument)
case madmin.MakeWithVersioningBktOp:
_, isLockEnabled := r.Form["lockEnabled"]
_, isVersioningEnabled := r.Form["versioningEnabled"]
@@ -128,21 +128,17 @@ func (a adminAPIHandlers) SRInternalBucketOps(w http.ResponseWriter, r *http.Req
err = globalSiteReplicationSys.PeerBucketDeleteHandler(ctx, bucket, false)
case madmin.ForceDeleteBucketBktOp:
err = globalSiteReplicationSys.PeerBucketDeleteHandler(ctx, bucket, true)
default:
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminInvalidArgument), r.URL)
return
}
if err != nil {
logger.LogIf(ctx, err)
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL)
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
}
// SRInternalReplicateIAMItem - PUT /minio/admin/v3/site-replication/iam-item
func (a adminAPIHandlers) SRInternalReplicateIAMItem(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "SRInternalReplicateIAMItem")
// SRPeerReplicateIAMItem - PUT /minio/admin/v3/site-replication/iam-item
func (a adminAPIHandlers) SRPeerReplicateIAMItem(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "SRPeerReplicateIAMItem")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
@@ -152,45 +148,51 @@ func (a adminAPIHandlers) SRInternalReplicateIAMItem(w http.ResponseWriter, r *h
}
var item madmin.SRIAMItem
errCode := readJSONBody(ctx, r.Body, &item, "")
if errCode != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(errCode), r.URL)
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:
var policy *iampolicy.Policy
if len(item.Policy) > 0 {
policy, err = iampolicy.ParseConfig(bytes.NewReader(item.Policy))
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
if item.Policy == nil {
err = globalSiteReplicationSys.PeerAddPolicyHandler(ctx, item.Name, nil)
} else {
policy, perr := iampolicy.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)
} else {
err = globalSiteReplicationSys.PeerAddPolicyHandler(ctx, item.Name, policy)
}
}
err = globalSiteReplicationSys.PeerAddPolicyHandler(ctx, item.Name, policy)
case madmin.SRIAMItemSvcAcc:
err = globalSiteReplicationSys.PeerSvcAccChangeHandler(ctx, *item.SvcAccChange)
err = globalSiteReplicationSys.PeerSvcAccChangeHandler(ctx, item.SvcAccChange)
case madmin.SRIAMItemPolicyMapping:
err = globalSiteReplicationSys.PeerPolicyMappingHandler(ctx, *item.PolicyMapping)
err = globalSiteReplicationSys.PeerPolicyMappingHandler(ctx, item.PolicyMapping)
case madmin.SRIAMItemSTSAcc:
err = globalSiteReplicationSys.PeerSTSAccHandler(ctx, *item.STSCredential)
default:
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminInvalidArgument), r.URL)
return
err = globalSiteReplicationSys.PeerSTSAccHandler(ctx, item.STSCredential)
case madmin.SRIAMItemIAMUser:
err = globalSiteReplicationSys.PeerIAMUserChangeHandler(ctx, item.IAMUser)
case madmin.SRIAMItemGroupInfo:
err = globalSiteReplicationSys.PeerGroupInfoChangeHandler(ctx, item.GroupInfo)
}
if err != nil {
logger.LogIf(ctx, err)
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL)
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
}
// SRInternalReplicateBucketItem - PUT /minio/admin/v3/site-replication/bucket-meta
func (a adminAPIHandlers) SRInternalReplicateBucketItem(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "SRInternalReplicateIAMItem")
// SRPeerReplicateBucketItem - PUT /minio/admin/v3/site-replication/bucket-meta
func (a adminAPIHandlers) SRPeerReplicateBucketItem(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "SRPeerReplicateBucketItem")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
@@ -200,38 +202,40 @@ func (a adminAPIHandlers) SRInternalReplicateBucketItem(w http.ResponseWriter, r
}
var item madmin.SRBucketMeta
errCode := readJSONBody(ctx, r.Body, &item, "")
if errCode != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(errCode), r.URL)
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.SRBucketMetaTypePolicy:
var bktPolicy *policy.Policy
if len(item.Policy) > 0 {
bktPolicy, err = policy.ParseConfig(bytes.NewReader(item.Policy), item.Bucket)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
if item.Policy == nil {
err = globalSiteReplicationSys.PeerBucketPolicyHandler(ctx, item.Bucket, nil)
} else {
bktPolicy, berr := policy.ParseConfig(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)
} else {
err = globalSiteReplicationSys.PeerBucketPolicyHandler(ctx, item.Bucket, bktPolicy)
}
}
err = globalSiteReplicationSys.PeerBucketPolicyHandler(ctx, item.Bucket, bktPolicy)
case madmin.SRBucketMetaTypeTags:
err = globalSiteReplicationSys.PeerBucketTaggingHandler(ctx, item.Bucket, item.Tags)
case madmin.SRBucketMetaTypeObjectLockConfig:
err = globalSiteReplicationSys.PeerBucketObjectLockConfigHandler(ctx, item.Bucket, item.ObjectLockConfig)
case madmin.SRBucketMetaTypeSSEConfig:
err = globalSiteReplicationSys.PeerBucketSSEConfigHandler(ctx, item.Bucket, item.SSEConfig)
default:
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminInvalidArgument), r.URL)
return
}
if err != nil {
logger.LogIf(ctx, err)
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL)
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
}
@@ -269,11 +273,9 @@ func (a adminAPIHandlers) SiteReplicationInfo(w http.ResponseWriter, r *http.Req
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
w.(http.Flusher).Flush()
}
func (a adminAPIHandlers) SRInternalGetIDPSettings(w http.ResponseWriter, r *http.Request) {
func (a adminAPIHandlers) SRPeerGetIDPSettings(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "SiteReplicationGetIDPSettings")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
@@ -288,28 +290,73 @@ func (a adminAPIHandlers) SRInternalGetIDPSettings(w http.ResponseWriter, r *htt
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
w.(http.Flusher).Flush()
}
func readJSONBody(ctx context.Context, body io.Reader, v interface{}, encryptionKey string) APIErrorCode {
func parseJSONBody(ctx context.Context, body io.Reader, v interface{}, encryptionKey string) error {
data, err := ioutil.ReadAll(body)
if err != nil {
return ErrInvalidRequest
return SRError{
Cause: err,
Code: ErrSiteReplicationInvalidRequest,
}
}
if encryptionKey != "" {
data, err = madmin.DecryptData(encryptionKey, bytes.NewReader(data))
if err != nil {
logger.LogIf(ctx, err)
return ErrInvalidRequest
return SRError{
Cause: err,
Code: ErrSiteReplicationInvalidRequest,
}
}
}
err = json.Unmarshal(data, v)
if err != nil {
return ErrAdminConfigBadJSON
return json.Unmarshal(data, v)
}
// SiteReplicationStatus - GET /minio/admin/v3/site-replication/status
func (a adminAPIHandlers) SiteReplicationStatus(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "SiteReplicationStatus")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SiteReplicationInfoAction)
if objectAPI == nil {
return
}
return ErrNone
info, err := globalSiteReplicationSys.SiteReplicationStatus(ctx, objectAPI)
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
}
}
// SiteReplicationMetaInfo - GET /minio/admin/v3/site-replication/metainfo
func (a adminAPIHandlers) SiteReplicationMetaInfo(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "SiteReplicationMetaInfo")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SiteReplicationInfoAction)
if objectAPI == nil {
return
}
info, err := globalSiteReplicationSys.SiteReplicationMetaInfo(ctx, objectAPI)
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
}
}
+141
View File
@@ -0,0 +1,141 @@
// 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 <http://www.gnu.org/licenses/>.
//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"
"sync"
"testing"
"time"
"github.com/minio/madmin-go"
minio "github.com/minio/minio-go/v7"
)
func runAllIAMConcurrencyTests(suite *TestSuiteIAM, c *check) {
suite.SetUpSuite(c)
suite.TestDeleteUserRace(c)
suite.TearDownSuite(c)
}
func TestIAMInternalIDPConcurrencyServerSuite(t *testing.T) {
baseTestCases := []TestSuiteCommon{
// Init and run test on FS backend with signature v4.
{serverType: "FS", signer: signerV4},
// Init and run test on FS backend, with tls enabled.
{serverType: "FS", signer: signerV4, secure: true},
// Init and run test on Erasure backend.
{serverType: "Erasure", signer: signerV4},
// Init and run test on ErasureSet backend.
{serverType: "ErasureSet", signer: signerV4},
}
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(), 30*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 := []byte(fmt.Sprintf(`{
"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 := 0; i < userCount; i++ {
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.SetPolicy(ctx, policy, accessKey, false)
if err != nil {
c.Fatalf("Unable to set policy: %v", err)
}
accessKeys[i] = accessKey
secretKeys[i] = secretKey
}
wg := sync.WaitGroup{}
for i := 0; i < userCount; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
uClient := s.getUserClient(c, accessKeys[i], secretKeys[i], "")
err := s.adm.RemoveUser(ctx, accessKeys[i])
if err != nil {
c.Fatalf("unable to remove user: %v", err)
}
c.mustNotListObjects(ctx, uClient, bucket)
}(i)
}
wg.Wait()
}
+155 -217
View File
@@ -58,17 +58,20 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
return
}
if err := globalIAMSys.DeleteUser(accessKey); err != nil {
if err := globalIAMSys.DeleteUser(ctx, accessKey, true); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
// Notify all other MinIO peers to delete user.
for _, nerr := range globalNotificationSys.DeleteUser(accessKey) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
Type: madmin.SRIAMItemIAMUser,
IAMUser: &madmin.SRIAMUser{
AccessKey: accessKey,
IsDeleteReq: true,
},
}); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
}
@@ -164,34 +167,27 @@ func (a adminAPIHandlers) GetUserInfo(w http.ResponseWriter, r *http.Request) {
return
}
accessKey := cred.ParentUser
if accessKey == "" {
accessKey = cred.AccessKey
checkDenyOnly := false
if name == cred.AccessKey {
// Check that there is no explicit deny - otherwise it's allowed
// to view one's own info.
checkDenyOnly = true
}
// For temporary credentials always
// the temporary credentials to check
// policy without implicit permissions.
if cred.IsTemp() && cred.ParentUser == globalActiveCred.AccessKey {
accessKey = cred.AccessKey
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey,
Groups: cred.Groups,
Action: iampolicy.GetUserAdminAction,
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
IsOwner: owner,
Claims: claims,
DenyOnly: checkDenyOnly,
}) {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return
}
implicitPerm := name == accessKey
if !implicitPerm {
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: accessKey,
Groups: cred.Groups,
Action: iampolicy.GetUserAdminAction,
ConditionValues: getConditionValues(r, "", accessKey, claims),
IsOwner: owner,
Claims: claims,
}) {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return
}
}
userInfo, err := globalIAMSys.GetUserInfo(name)
userInfo, err := globalIAMSys.GetUserInfo(ctx, name)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
@@ -231,24 +227,23 @@ func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Requ
}
if updReq.IsRemove {
err = globalIAMSys.RemoveUsersFromGroup(updReq.Group, updReq.Members)
err = globalIAMSys.RemoveUsersFromGroup(ctx, updReq.Group, updReq.Members)
} else {
err = globalIAMSys.AddUsersToGroup(updReq.Group, updReq.Members)
err = globalIAMSys.AddUsersToGroup(ctx, updReq.Group, updReq.Members)
}
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
// Notify all other MinIO peers to load group.
if !globalIAMSys.HasWatcher() {
for _, nerr := range globalNotificationSys.LoadGroup(updReq.Group) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
Type: madmin.SRIAMItemGroupInfo,
GroupInfo: &madmin.SRGroupInfo{
UpdateReq: updReq,
},
}); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
}
@@ -292,7 +287,7 @@ func (a adminAPIHandlers) ListGroups(w http.ResponseWriter, r *http.Request) {
return
}
groups, err := globalIAMSys.ListGroups()
groups, err := globalIAMSys.ListGroups(ctx)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
@@ -323,27 +318,18 @@ func (a adminAPIHandlers) SetGroupStatus(w http.ResponseWriter, r *http.Request)
status := vars["status"]
var err error
if status == statusEnabled {
err = globalIAMSys.SetGroupStatus(group, true)
} else if status == statusDisabled {
err = globalIAMSys.SetGroupStatus(group, false)
} else {
switch status {
case statusEnabled:
err = globalIAMSys.SetGroupStatus(ctx, group, true)
case statusDisabled:
err = globalIAMSys.SetGroupStatus(ctx, group, false)
default:
err = errInvalidArgument
}
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
// Notify all other MinIO peers to reload user.
if !globalIAMSys.HasWatcher() {
for _, nerr := range globalNotificationSys.LoadGroup(group) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
}
}
// SetUserStatus - PUT /minio/admin/v3/set-user-status?accessKey=<access_key>&status=[enabled|disabled]
@@ -367,20 +353,10 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request)
return
}
if err := globalIAMSys.SetUserStatus(accessKey, madmin.AccountStatus(status)); err != nil {
if err := globalIAMSys.SetUserStatus(ctx, accessKey, madmin.AccountStatus(status)); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
// Notify all other MinIO peers to reload user.
if !globalIAMSys.HasWatcher() {
for _, nerr := range globalNotificationSys.LoadUser(accessKey, false) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
}
}
// AddUser - PUT /minio/admin/v3/add-user?accessKey=<access_key>
@@ -411,6 +387,14 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
return
}
userCred, exists := globalIAMSys.GetUser(ctx, accessKey)
if exists && (userCred.IsTemp() || userCred.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.
@@ -418,39 +402,21 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
return
}
implicitPerm := accessKey == cred.AccessKey
if !implicitPerm {
parentUser := cred.ParentUser
if parentUser == "" {
parentUser = cred.AccessKey
}
// For temporary credentials always
// the temporary credentials to check
// policy without implicit permissions.
if cred.IsTemp() && cred.ParentUser == globalActiveCred.AccessKey {
parentUser = cred.AccessKey
}
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: parentUser,
Groups: cred.Groups,
Action: iampolicy.CreateUserAdminAction,
ConditionValues: getConditionValues(r, "", parentUser, claims),
IsOwner: owner,
Claims: claims,
}) {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return
}
checkDenyOnly := false
if accessKey == cred.AccessKey {
// Check that there is no explicit deny - otherwise it's allowed
// to change one's own password.
checkDenyOnly = true
}
if implicitPerm && !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: accessKey,
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey,
Groups: cred.Groups,
Action: iampolicy.CreateUserAdminAction,
ConditionValues: getConditionValues(r, "", accessKey, claims),
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
IsOwner: owner,
Claims: claims,
DenyOnly: true, // check if changing password is explicitly denied.
DenyOnly: checkDenyOnly,
}) {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return
@@ -470,26 +436,28 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
return
}
var uinfo madmin.UserInfo
if err = json.Unmarshal(configBytes, &uinfo); err != nil {
var ureq madmin.AddOrUpdateUserReq
if err = json.Unmarshal(configBytes, &ureq); err != nil {
logger.LogIf(ctx, err)
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
return
}
if err = globalIAMSys.CreateUser(accessKey, uinfo); err != nil {
if err = globalIAMSys.CreateUser(ctx, accessKey, ureq); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
// Notify all other Minio peers to reload user
if !globalIAMSys.HasWatcher() {
for _, nerr := range globalNotificationSys.LoadUser(accessKey, false) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
Type: madmin.SRIAMItemIAMUser,
IAMUser: &madmin.SRIAMUser{
AccessKey: accessKey,
IsDeleteReq: false,
UserReq: &ureq,
},
}); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
}
@@ -564,6 +532,23 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
// that targetUser is a real user (i.e. not derived
// credentials).
if isSvcAccForRequestor {
// Check if adding service account is explicitly denied.
//
// This allows turning off service accounts for request sender,
// if there is no deny statement this call is implicitly enabled.
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: requestorUser,
Groups: requestorGroups,
Action: iampolicy.CreateServiceAccountAdminAction,
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
IsOwner: owner,
Claims: claims,
DenyOnly: true,
}) {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return
}
if requestorIsDerivedCredential {
if requestorParentUser == "" {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx,
@@ -587,10 +572,12 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
// user <> to the request sender
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: requestorUser,
Groups: requestorGroups,
Action: iampolicy.CreateServiceAccountAdminAction,
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
IsOwner: owner,
Claims: claims,
DenyOnly: true,
}) {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return
@@ -630,24 +617,9 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
return
}
// Notify all other Minio peers to reload user the service account
if !globalIAMSys.HasWatcher() {
for _, nerr := range globalNotificationSys.LoadServiceAccount(newCred.AccessKey) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
}
// Call hook for cluster-replication.
//
// FIXME: This wont work in an OpenID situation as the parent credential
// may not be present on peer clusters to provide inherited policies.
// Also, we should not be replicating root user's service account - as
// they are not authenticated by a common external IDP, so we skip when
// opts.ldapUser == "".
if _, isLDAPAccount := opts.claims[ldapUserN]; isLDAPAccount {
// Call hook for cluster-replication if the service account is not for a
// root user.
if newCred.ParentUser != globalActiveCred.AccessKey {
err = globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
Type: madmin.SRIAMItemSvcAcc,
SvcAccChange: &madmin.SRSvcAccChange{
@@ -668,7 +640,7 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
}
}
var createResp = madmin.AddServiceAccountResp{
createResp := madmin.AddServiceAccountResp{
Credentials: madmin.Credentials{
AccessKey: newCred.AccessKey,
SecretKey: newCred.SecretKey,
@@ -771,24 +743,8 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re
return
}
// Notify all other Minio peers to reload user the service account
if !globalIAMSys.HasWatcher() {
for _, nerr := range globalNotificationSys.LoadServiceAccount(accessKey) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
}
// Call site replication hook. Only LDAP accounts are supported for
// replication operations.
svcAccClaims, err := globalIAMSys.GetClaimsForSvcAcc(ctx, accessKey)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
if _, isLDAPAccount := svcAccClaims[ldapUserN]; isLDAPAccount {
// Call site replication hook - non-root user accounts are replicated.
if svcAccount.ParentUser != globalActiveCred.AccessKey {
err = globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
Type: madmin.SRIAMItemSvcAcc,
SvcAccChange: &madmin.SRSvcAccChange{
@@ -805,6 +761,7 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re
return
}
}
writeSuccessNoContent(w)
}
@@ -879,7 +836,7 @@ func (a adminAPIHandlers) InfoServiceAccount(w http.ResponseWriter, r *http.Requ
return
}
var infoResp = madmin.InfoServiceAccountResp{
infoResp := madmin.InfoServiceAccountResp{
ParentUser: svcAccount.ParentUser,
AccountStatus: svcAccount.Status,
ImpliedPolicy: impliedPolicy,
@@ -922,8 +879,10 @@ func (a adminAPIHandlers) ListServiceAccounts(w http.ResponseWriter, r *http.Req
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 != "" {
if user != "" && user != cred.AccessKey {
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey,
Action: iampolicy.ListServiceAccountsAdminAction,
@@ -954,7 +913,7 @@ func (a adminAPIHandlers) ListServiceAccounts(w http.ResponseWriter, r *http.Req
serviceAccountsNames = append(serviceAccountsNames, svc.AccessKey)
}
var listResp = madmin.ListServiceAccountsResp{
listResp := madmin.ListServiceAccountsResp{
Accounts: serviceAccountsNames,
}
@@ -1026,27 +985,14 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re
}
}
err = globalIAMSys.DeleteServiceAccount(ctx, serviceAccount)
err = globalIAMSys.DeleteServiceAccount(ctx, serviceAccount, true)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
for _, nerr := range globalNotificationSys.DeleteServiceAccount(serviceAccount) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
// Call site replication hook. Only LDAP accounts are supported for
// replication operations.
svcAccClaims, err := globalIAMSys.GetClaimsForSvcAcc(ctx, serviceAccount)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
if _, isLDAPAccount := svcAccClaims[ldapUserN]; isLDAPAccount {
// Call site replication hook - non-root user accounts are replicated.
if svcAccount.ParentUser != globalActiveCred.AccessKey {
err = globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
Type: madmin.SRIAMItemSvcAcc,
SvcAccChange: &madmin.SRSvcAccChange{
@@ -1161,19 +1107,11 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
}
accountName := cred.AccessKey
var policies []string
switch globalIAMSys.usersSysType {
case MinIOUsersSysType:
policies, err = globalIAMSys.PolicyDBGet(accountName, false)
case LDAPUsersSysType:
parentUser := accountName
if cred.ParentUser != "" {
parentUser = cred.ParentUser
}
policies, err = globalIAMSys.PolicyDBGet(parentUser, false, cred.Groups...)
default:
err = errors.New("should never happen")
if cred.IsTemp() || cred.IsServiceAccount() {
// For derived credentials, check the parent user's permissions.
accountName = cred.ParentUser
}
policies, err := globalIAMSys.PolicyDBGet(accountName, false, cred.Groups...)
if err != nil {
logger.LogIf(ctx, err)
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
@@ -1252,6 +1190,16 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
}
// 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 := newContext(r, w, "InfoCannedPolicy")
@@ -1262,13 +1210,36 @@ func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Reques
return
}
policy, err := globalIAMSys.InfoPolicy(mux.Vars(r)["name"])
name := mux.Vars(r)["name"]
policies := newMappedPolicy(name).toSlice()
if len(policies) != 1 {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errTooManyPolicies), r.URL)
return
}
policyDoc, err := globalIAMSys.InfoPolicy(name)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
buf, err := json.MarshalIndent(policy, "", " ")
// 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
@@ -1288,13 +1259,13 @@ func (a adminAPIHandlers) ListBucketPolicies(w http.ResponseWriter, r *http.Requ
}
bucket := mux.Vars(r)["bucket"]
policies, err := globalIAMSys.ListPolicies(bucket)
policies, err := globalIAMSys.ListPolicies(ctx, bucket)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
var newPolicies = make(map[string]iampolicy.Policy)
newPolicies := make(map[string]iampolicy.Policy)
for name, p := range policies {
_, err = json.Marshal(p)
if err != nil {
@@ -1307,9 +1278,6 @@ func (a adminAPIHandlers) ListBucketPolicies(w http.ResponseWriter, r *http.Requ
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
w.(http.Flusher).Flush()
}
// ListCannedPolicies - GET /minio/admin/v3/list-canned-policies
@@ -1323,13 +1291,13 @@ func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Requ
return
}
policies, err := globalIAMSys.ListPolicies("")
policies, err := globalIAMSys.ListPolicies(ctx, "")
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
var newPolicies = make(map[string]iampolicy.Policy)
newPolicies := make(map[string]iampolicy.Policy)
for name, p := range policies {
_, err = json.Marshal(p)
if err != nil {
@@ -1342,8 +1310,6 @@ func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Requ
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
w.(http.Flusher).Flush()
}
// RemoveCannedPolicy - DELETE /minio/admin/v3/remove-canned-policy?name=<policy_name>
@@ -1360,19 +1326,11 @@ func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Requ
vars := mux.Vars(r)
policyName := vars["name"]
if err := globalIAMSys.DeletePolicy(policyName); err != nil {
if err := globalIAMSys.DeletePolicy(ctx, policyName, true); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
// Notify all other MinIO peers to delete policy
for _, nerr := range globalNotificationSys.DeletePolicy(policyName) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
// Call cluster-replication policy creation hook to replicate policy deletion to
// other minio clusters.
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
@@ -1428,21 +1386,11 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request
return
}
if err = globalIAMSys.SetPolicy(policyName, *iamPolicy); err != nil {
if err = globalIAMSys.SetPolicy(ctx, policyName, *iamPolicy); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
// Notify all other MinIO peers to reload policy
if !globalIAMSys.HasWatcher() {
for _, nerr := range globalNotificationSys.LoadPolicy(policyName) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
}
// Call cluster-replication policy creation hook to replicate policy to
// other minio clusters.
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
@@ -1483,21 +1431,11 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http
}
}
if err := globalIAMSys.PolicyDBSet(entityName, policyName, isGroup); err != nil {
if err := globalIAMSys.PolicyDBSet(ctx, entityName, policyName, isGroup); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
// Notify all other MinIO peers to reload policy
if !globalIAMSys.HasWatcher() {
for _, nerr := range globalNotificationSys.LoadPolicyMapping(entityName, isGroup) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
}
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
Type: madmin.SRIAMItemPolicyMapping,
PolicyMapping: &madmin.SRPolicyMapping{
+779 -72
View File
@@ -18,8 +18,16 @@
package cmd
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"testing"
"time"
@@ -27,30 +35,34 @@ import (
"github.com/minio/madmin-go"
minio "github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
cr "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"
)
const (
testDefaultTimeout = 10 * time.Second
testDefaultTimeout = 30 * time.Second
)
// API suite container for IAM
type TestSuiteIAM struct {
TestSuiteCommon
// Flag to turn on tests for etcd backend IAM
withEtcdBackend bool
endpoint string
adm *madmin.AdminClient
client *minio.Client
}
func newTestSuiteIAM(c TestSuiteCommon) *TestSuiteIAM {
return &TestSuiteIAM{TestSuiteCommon: c}
func newTestSuiteIAM(c TestSuiteCommon, withEtcdBackend bool) *TestSuiteIAM {
return &TestSuiteIAM{TestSuiteCommon: c, withEtcdBackend: withEtcdBackend}
}
func (s *TestSuiteIAM) SetUpSuite(c *check) {
s.TestSuiteCommon.SetUpSuite(c)
func (s *TestSuiteIAM) iamSetup(c *check) {
var err error
// strip url scheme from endpoint
s.endpoint = strings.TrimPrefix(s.endPoint, "http://")
@@ -75,6 +87,62 @@ func (s *TestSuiteIAM) SetUpSuite(c *check) {
}
}
const (
EnvTestEtcdBackend = "ETCD_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 := os.Getenv(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.TestSuiteCommon.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),
@@ -87,29 +155,47 @@ func (s *TestSuiteIAM) getUserClient(c *check, accessKey, secretKey, sessionToke
return client
}
func runAllIAMTests(suite *TestSuiteIAM, c *check) {
suite.SetUpSuite(c)
suite.TestUserCreate(c)
suite.TestPolicyCreate(c)
suite.TestGroupAddRemove(c)
suite.TearDownSuite(c)
}
func TestIAMInternalIDPServerSuite(t *testing.T) {
testCases := []*TestSuiteIAM{
baseTestCases := []TestSuiteCommon{
// Init and run test on FS backend with signature v4.
newTestSuiteIAM(TestSuiteCommon{serverType: "FS", signer: signerV4}),
{serverType: "FS", signer: signerV4},
// Init and run test on FS backend, with tls enabled.
newTestSuiteIAM(TestSuiteCommon{serverType: "FS", signer: signerV4, secure: true}),
{serverType: "FS", signer: signerV4, secure: true},
// Init and run test on Erasure backend.
newTestSuiteIAM(TestSuiteCommon{serverType: "Erasure", signer: signerV4}),
{serverType: "Erasure", signer: signerV4},
// Init and run test on ErasureSet backend.
newTestSuiteIAM(TestSuiteCommon{serverType: "ErasureSet", signer: signerV4}),
{serverType: "ErasureSet", signer: signerV4},
}
testCases := []*TestSuiteIAM{}
for _, bt := range baseTestCases {
testCases = append(testCases,
newTestSuiteIAM(bt, false),
newTestSuiteIAM(bt, true),
)
}
for i, testCase := range testCases {
t.Run(fmt.Sprintf("Test: %d, ServerType: %s", i+1, testCase.serverType), func(t *testing.T) {
runAllIAMTests(testCase, &check{t, testCase.serverType})
})
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) {
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.TestServiceAccountOpsByUser(c)
suite.TestAddServiceAccountPerms(c)
suite.TearDownSuite(c)
},
)
}
}
@@ -146,6 +232,24 @@ func (s *TestSuiteIAM) TestUserCreate(c *check) {
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 {
@@ -184,6 +288,220 @@ func (s *TestSuiteIAM) TestUserCreate(c *check) {
}
}
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 := []byte(fmt.Sprintf(`{
"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)
}
err = s.adm.SetPolicy(ctx, policy, accessKey, false)
if err != nil {
c.Fatalf("Unable to set 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 = ioutil.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.SetPolicy(ctx, policy1, accessKey, false)
if err != nil {
c.Fatalf("Unable to set 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!")
}
// 3.2 associate policy to user
err = s.adm.SetPolicy(ctx, policy2, accessKey, false)
if err != nil {
c.Fatalf("Unable to set 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()
@@ -232,25 +550,15 @@ func (s *TestSuiteIAM) TestPolicyCreate(c *check) {
}
// 3.1 check that user does not have any access to the bucket
uClient := s.getUserClient(c, accessKey, secretKey, "")
res := uClient.ListObjects(ctx, bucket, minio.ListObjectsOptions{})
v, ok := <-res
if !ok {
c.Fatalf("list channel was closed!")
}
if v.Err == nil {
c.Fatalf("User appears to be able to list!")
}
c.mustNotListObjects(ctx, uClient, bucket)
// 3.2 associate policy to user
err = s.adm.SetPolicy(ctx, policy, accessKey, false)
if err != nil {
c.Fatalf("Unable to set policy: %v", err)
}
// 3.3 check user has access to bucket
res = uClient.ListObjects(ctx, bucket, minio.ListObjectsOptions{})
v, ok = <-res
if ok {
c.Fatalf("list channel was not closed - unexpected error or objects in listing: %v", v)
}
c.mustListObjects(ctx, uClient, bucket)
// 3.4 Check that user cannot exceed their permissions
err = uClient.RemoveBucket(ctx, bucket)
if err == nil {
@@ -262,15 +570,88 @@ func (s *TestSuiteIAM) TestPolicyCreate(c *check) {
if err != nil {
c.Fatalf("policy list err: %v", err)
}
_, ok = ps[policy]
_, ok := ps[policy]
if !ok {
c.Fatalf("policy was missing!")
}
// 5. Check that policy can be deleted.
// 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 delete err: %v", err)
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 := []byte(fmt.Sprintf(`{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::%s/*"
]
}
]
}`, 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)
}
infoStr := string(info)
if !strings.Contains(infoStr, `"s3:PutObject"`) || !strings.Contains(infoStr, ":"+bucket+"/") {
c.Fatalf("policy contains unexpected content!")
}
}
@@ -324,14 +705,7 @@ func (s *TestSuiteIAM) TestGroupAddRemove(c *check) {
// 2. Check that user has no access
uClient := s.getUserClient(c, accessKey, secretKey, "")
res := uClient.ListObjects(ctx, bucket, minio.ListObjectsOptions{})
v, ok := <-res
if !ok {
c.Fatalf("list channel was closed!")
}
if v.Err == nil {
c.Fatalf("User appears to be able to list!")
}
c.mustNotListObjects(ctx, uClient, bucket)
// 3. Associate policy to group and check user got access.
err = s.adm.SetPolicy(ctx, policy, group, true)
@@ -339,11 +713,7 @@ func (s *TestSuiteIAM) TestGroupAddRemove(c *check) {
c.Fatalf("Unable to set policy: %v", err)
}
// 3.1 check user has access to bucket
res = uClient.ListObjects(ctx, bucket, minio.ListObjectsOptions{})
v, ok = <-res
if ok {
c.Fatalf("list channel was not closed - unexpected error or objects in listing: %v", v)
}
c.mustListObjects(ctx, uClient, bucket)
// 3.2 Check that user cannot exceed their permissions
err = uClient.RemoveBucket(ctx, bucket)
if err == nil {
@@ -377,14 +747,8 @@ func (s *TestSuiteIAM) TestGroupAddRemove(c *check) {
c.Fatalf("group desc err: %v", err)
}
c.Assert(groupInfo.Status, string(madmin.GroupDisabled))
res = uClient.ListObjects(ctx, bucket, minio.ListObjectsOptions{})
v, ok = <-res
if !ok {
c.Fatalf("list channel was closed!")
}
if v.Err == nil {
c.Fatalf("User appears to be able to list!")
}
c.mustNotListObjects(ctx, uClient, bucket)
err = s.adm.SetGroupStatus(ctx, group, madmin.GroupEnabled)
if err != nil {
c.Fatalf("group set status err: %v", err)
@@ -394,11 +758,7 @@ func (s *TestSuiteIAM) TestGroupAddRemove(c *check) {
c.Fatalf("group desc err: %v", err)
}
c.Assert(groupInfo.Status, string(madmin.GroupEnabled))
res = uClient.ListObjects(ctx, bucket, minio.ListObjectsOptions{})
v, ok = <-res
if ok {
c.Fatalf("list channel was not closed - unexpected error or objects in listing: %v", v)
}
c.mustListObjects(ctx, uClient, bucket)
// 6. Verify that group cannot be deleted with users.
err = s.adm.UpdateGroupMembers(ctx, madmin.GroupAddRemove{
@@ -423,14 +783,8 @@ func (s *TestSuiteIAM) TestGroupAddRemove(c *check) {
if err != nil {
c.Fatalf("group update err: %v", err)
}
res = uClient.ListObjects(ctx, bucket, minio.ListObjectsOptions{})
v, ok = <-res
if !ok {
c.Fatalf("list channel was closed!")
}
if v.Err == nil {
c.Fatalf("User appears to be able to list!")
}
c.mustNotListObjects(ctx, uClient, bucket)
// 7.1 verify group still exists
groupInfo, err = s.adm.GetGroupDescription(ctx, group)
if err != nil {
@@ -460,6 +814,359 @@ func (s *TestSuiteIAM) TestGroupAddRemove(c *check) {
}
}
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 := []byte(fmt.Sprintf(`{
"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)
}
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.SetPolicy(ctx, policy, accessKey, false)
if err != nil {
c.Fatalf("Unable to set policy: %v", err)
}
// Create an madmin client with user creds
userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{
Creds: cr.NewStaticV4(accessKey, secretKey, ""),
Secure: s.secure,
})
if err != nil {
c.Fatalf("Err creating user admin client: %v", err)
}
userAdmClient.SetCustomTransport(s.TestSuiteCommon.client.Transport)
// Create svc acc
cr := c.mustCreateSvcAccount(ctx, accessKey, userAdmClient)
// 1. Check that svc account appears in listing
c.assertSvcAccAppearsInListing(ctx, userAdmClient, accessKey, cr.AccessKey)
// 2. Check that svc account info can be queried
c.assertSvcAccInfoQueryable(ctx, userAdmClient, accessKey, cr.AccessKey, false)
// 3. Check S3 access
c.assertSvcAccS3Access(ctx, s, cr, bucket)
// 4. Check that svc account can restrict the policy, and that the
// session policy can be updated.
c.assertSvcAccSessionPolicyUpdate(ctx, s, userAdmClient, accessKey, 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)
}
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 := []byte(fmt.Sprintf(`{
"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)
}
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.SetPolicy(ctx, policy, accessKey, false)
if err != nil {
c.Fatalf("Unable to set 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 (c *check) mustCreateIAMUser(ctx context.Context, admClnt *madmin.AdminClient) madmin.Credentials {
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 {
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) {
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 {
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) {
_, 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) {
res := client.ListObjects(ctx, bucket, minio.ListObjectsOptions{})
v, ok := <-res
if !ok || v.Err == nil {
c.Fatalf("user was able to list unexpectedly!")
}
}
func (c *check) mustListObjects(ctx context.Context, client *minio.Client, bucket string) {
res := client.ListObjects(ctx, bucket, minio.ListObjectsOptions{})
v, ok := <-res
if ok && v.Err != nil {
msg := fmt.Sprintf("user was unable to list: %v", v.Err)
c.Fatalf(msg)
}
}
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) {
listResp, err := madmClient.ListServiceAccounts(ctx, parentAK)
if err != nil {
c.Fatalf("unable to list svc accounts: %v", err)
}
if !set.CreateStringSet(listResp.Accounts...).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) {
svcAK, svcSK := mustGenerateCredentials(c)
// This policy does not allow listing objects.
policyBytes := []byte(fmt.Sprintf(`{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::%s/*"
]
}
]
}`, bucket))
cr, err := 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 := []byte(fmt.Sprintf(`{
"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) {
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) {
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) {
ak, sk, err := auth.GenerateCredentials()
if err != nil {
+102 -73
View File
@@ -190,7 +190,11 @@ func (a adminAPIHandlers) ServerUpdateHandler(w http.ResponseWriter, r *http.Req
// ServiceHandler - POST /minio/admin/v3/service?action={action}
// ----------
// restarts/stops minio server gracefully. In a distributed setup,
// 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 := newContext(r, w, "Service")
@@ -205,6 +209,10 @@ func (a adminAPIHandlers) ServiceHandler(w http.ResponseWriter, r *http.Request)
serviceSig = serviceRestart
case madmin.ServiceActionStop:
serviceSig = serviceStop
case madmin.ServiceActionFreeze:
serviceSig = serviceFreeze
case madmin.ServiceActionUnfreeze:
serviceSig = serviceUnFreeze
default:
logger.LogIf(ctx, fmt.Errorf("Unrecognized service action %s requested", action), logger.Application)
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMalformedPOSTRequest), r.URL)
@@ -217,6 +225,8 @@ func (a adminAPIHandlers) ServiceHandler(w http.ResponseWriter, r *http.Request)
objectAPI, _ = validateAdminReq(ctx, w, r, iampolicy.ServiceRestartAdminAction)
case serviceStop:
objectAPI, _ = validateAdminReq(ctx, w, r, iampolicy.ServiceStopAdminAction)
case serviceFreeze, serviceUnFreeze:
objectAPI, _ = validateAdminReq(ctx, w, r, iampolicy.ServiceFreezeAdminAction)
}
if objectAPI == nil {
return
@@ -233,7 +243,14 @@ func (a adminAPIHandlers) ServiceHandler(w http.ResponseWriter, r *http.Request)
// Reply to the client before restarting, stopping MinIO server.
writeSuccessResponseHeadersOnly(w)
globalServiceSignalCh <- serviceSig
switch serviceSig {
case serviceFreeze:
freezeServices()
case serviceUnFreeze:
unfreezeServices()
case serviceRestart, serviceStop:
globalServiceSignalCh <- serviceSig
}
}
// ServerProperties holds some server information such as, version, region
@@ -316,7 +333,6 @@ func (a adminAPIHandlers) StorageInfoHandler(w http.ResponseWriter, r *http.Requ
// Reply with storage information (across nodes in a
// distributed setup) as json.
writeSuccessResponseJSON(w, jsonBytes)
}
// DataUsageInfoHandler - GET /minio/admin/v3/datausage
@@ -756,13 +772,17 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}
// Send whitespace and keep connection open
w.Write([]byte(" "))
if _, err := w.Write([]byte(" ")); err != nil {
return
}
w.(http.Flusher).Flush()
case hr := <-respCh:
switch hr.apiErr {
case noError:
if started {
w.Write(hr.respBytes)
if _, err := w.Write(hr.respBytes); err != nil {
return
}
w.(http.Flusher).Flush()
} else {
writeSuccessResponseJSON(w, hr.respBytes)
@@ -787,7 +807,9 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set(xhttp.ContentType, string(mimeJSON))
w.WriteHeader(hr.apiErr.HTTPStatusCode)
}
w.Write(errorRespJSON)
if _, err := w.Write(errorRespJSON); err != nil {
return
}
w.(http.Flusher).Flush()
}
break forLoop
@@ -911,12 +933,16 @@ func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r *
}
}
// SpeedtestHandler - 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) SpeedtestHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "SpeedtestHandler")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealAdminAction)
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealthInfoAdminAction)
if objectAPI == nil {
return
}
@@ -929,12 +955,8 @@ func (a adminAPIHandlers) SpeedtestHandler(w http.ResponseWriter, r *http.Reques
sizeStr := r.Form.Get(peerRESTSize)
durationStr := r.Form.Get(peerRESTDuration)
concurrentStr := r.Form.Get(peerRESTConcurrent)
autotuneStr := r.Form.Get("autotune")
var autotune bool
if autotuneStr != "" {
autotune = true
}
autotune := r.Form.Get("autotune") == "true"
storageClass := r.Form.Get("storage-class")
size, err := strconv.Atoi(sizeStr)
if err != nil {
@@ -946,59 +968,55 @@ func (a adminAPIHandlers) SpeedtestHandler(w http.ResponseWriter, r *http.Reques
concurrent = 32
}
if runtime.GOMAXPROCS(0) < concurrent {
concurrent = runtime.GOMAXPROCS(0)
}
duration, err := time.ParseDuration(durationStr)
if err != nil {
duration = time.Second * 10
}
throughputSize := size
iopsSize := size
if autotune {
iopsSize = 4 * humanize.KiByte
deleteBucket := func() {
loc := pathJoin(minioMetaSpeedTestBucket, minioMetaSpeedTestBucketPrefix)
objectAPI.DeleteBucket(context.Background(), loc, DeleteBucketOptions{
Force: true,
NoRecreate: true,
})
}
defer deleteBucket()
// Freeze all incoming S3 API calls before running speedtest.
globalNotificationSys.ServiceFreeze(ctx, true)
// unfreeze all incoming S3 API calls after speedtest.
defer globalNotificationSys.ServiceFreeze(ctx, false)
keepAliveTicker := time.NewTicker(500 * time.Millisecond)
defer keepAliveTicker.Stop()
endBlankRepliesCh := make(chan error)
go func() {
for {
select {
case <-ctx.Done():
endBlankRepliesCh <- nil
return
case <-keepAliveTicker.C:
// Write a blank entry to prevent client from disconnecting
if err := json.NewEncoder(w).Encode(madmin.SpeedTestResult{}); err != nil {
endBlankRepliesCh <- err
return
}
w.(http.Flusher).Flush()
case endBlankRepliesCh <- nil:
enc := json.NewEncoder(w)
ch := speedTest(ctx, speedTestOpts{size, concurrent, duration, autotune, storageClass})
for {
select {
case <-ctx.Done():
return
case <-keepAliveTicker.C:
// Write a blank entry to prevent client from disconnecting
if err := enc.Encode(madmin.SpeedTestResult{}); err != nil {
return
}
w.(http.Flusher).Flush()
case result, ok := <-ch:
if !ok {
return
}
if err := enc.Encode(result); err != nil {
return
}
w.(http.Flusher).Flush()
}
}()
result, err := speedTest(ctx, throughputSize, iopsSize, concurrent, duration, autotune)
if <-endBlankRepliesCh != nil {
return
}
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
if err := json.NewEncoder(w).Encode(result); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
objectAPI.DeleteBucket(ctx, pathJoin(minioMetaSpeedTestBucket, minioMetaSpeedTestBucketPrefix), DeleteBucketOptions{Force: true, NoRecreate: true})
w.(http.Flusher).Flush()
}
// Admin API errors
@@ -1208,10 +1226,10 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque
if err := enc.Encode(log); err != nil {
return
}
}
if len(logCh) == 0 {
// Flush if nothing is queued
w.(http.Flusher).Flush()
if len(logCh) == 0 {
// Flush if nothing is queued
w.(http.Flusher).Flush()
}
}
case <-keepAliveTicker.C:
if len(logCh) > 0 {
@@ -1313,7 +1331,7 @@ func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Req
if keyID == "" {
keyID = stat.DefaultKey
}
var response = madmin.KMSKeyStatus{
response := madmin.KMSKeyStatus{
KeyID: keyID,
}
@@ -1370,6 +1388,7 @@ func getServerInfo(ctx context.Context, r *http.Request) madmin.InfoMessage {
ldap := madmin.LDAP{}
if globalLDAPConfig.Enabled {
ldapConn, err := globalLDAPConfig.Connect()
//nolint:gocritic
if err != nil {
ldap.Status = string(madmin.ItemOffline)
} else if ldapConn == nil {
@@ -1451,7 +1470,7 @@ func getServerInfo(ctx context.Context, r *http.Request) madmin.InfoMessage {
return madmin.InfoMessage{
Mode: string(mode),
Domain: domain,
Region: globalServerRegion,
Region: globalSite.Region,
SQSARN: globalNotificationSys.GetARNList(false),
DeploymentID: globalDeploymentID,
Buckets: buckets,
@@ -1650,8 +1669,8 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
anonymizeCmdLine := func(cmdLine string) string {
if !globalIsDistErasure {
// FS mode - single server - hard code to `server1`
anonCmdLine := strings.Replace(cmdLine, globalLocalNodeName, "server1", -1)
return strings.Replace(anonCmdLine, globalMinioConsoleHost, "server1", -1)
anonCmdLine := strings.ReplaceAll(cmdLine, globalLocalNodeName, "server1")
return strings.ReplaceAll(anonCmdLine, globalMinioConsoleHost, "server1")
}
// Server start command regex groups:
@@ -1796,7 +1815,6 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
anonNetwork[anonEndpoint] = status
}
return anonNetwork
}
anonymizeDrives := func(drives []madmin.Disk) []madmin.Disk {
@@ -1848,6 +1866,9 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
})
}
tls := getTLSInfo()
isK8s := IsKubernetes()
isDocker := IsDocker()
healthInfo.Minio.Info = madmin.MinioInfo{
Mode: infoMessage.Mode,
Domain: infoMessage.Domain,
@@ -1860,7 +1881,9 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
Services: infoMessage.Services,
Backend: infoMessage.Backend,
Servers: servers,
TLS: getTLSInfo(),
TLS: &tls,
IsKubernetes: &isK8s,
IsDocker: &isDocker,
}
partialWrite(healthInfo)
}
@@ -1875,19 +1898,22 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
if !ok {
return
}
logger.LogIf(ctx, enc.Encode(oinfo))
w.(http.Flusher).Flush()
if err := enc.Encode(oinfo); err != nil {
return
}
if len(healthInfoCh) == 0 {
// Flush if nothing is queued
w.(http.Flusher).Flush()
}
case <-ticker.C:
if _, err := w.Write([]byte(" ")); err != nil {
return
}
w.(http.Flusher).Flush()
case <-deadlinedCtx.Done():
w.(http.Flusher).Flush()
return
}
}
}
func getTLSInfo() madmin.TLSInfo {
@@ -1943,17 +1969,21 @@ func (a adminAPIHandlers) BandwidthMonitorHandler(w http.ResponseWriter, r *http
}
}
}()
enc := json.NewEncoder(w)
for {
select {
case report, ok := <-reportCh:
if !ok {
return
}
if err := json.NewEncoder(w).Encode(report); err != nil {
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
if err := enc.Encode(report); err != nil {
return
}
w.(http.Flusher).Flush()
if len(reportCh) == 0 {
// Flush if nothing is queued
w.(http.Flusher).Flush()
}
case <-keepAliveTicker.C:
if _, err := w.Write([]byte(" ")); err != nil {
return
@@ -2009,7 +2039,6 @@ func assignPoolNumbers(servers []madmin.ServerProperties) {
}
func fetchLambdaInfo() []map[string][]madmin.TargetIDStatus {
lambdaMap := make(map[string][]madmin.TargetIDStatus)
for _, tgt := range globalConfigTargetList.Targets() {
@@ -2096,7 +2125,7 @@ func fetchKMSStatus() madmin.KMS {
func fetchLoggerInfo() ([]madmin.Logger, []madmin.Audit) {
var loggerInfo []madmin.Logger
var auditloggerInfo []madmin.Audit
for _, target := range logger.Targets {
for _, target := range logger.Targets() {
if target.Endpoint() != "" {
tgt := target.String()
err := checkConnection(target.Endpoint(), 15*time.Second)
@@ -2112,7 +2141,7 @@ func fetchLoggerInfo() ([]madmin.Logger, []madmin.Audit) {
}
}
for _, target := range logger.AuditTargets {
for _, target := range logger.AuditTargets() {
if target.Endpoint() != "" {
tgt := target.String()
err := checkConnection(target.Endpoint(), 15*time.Second)
@@ -2251,7 +2280,7 @@ func (a adminAPIHandlers) InspectDataHandler(w http.ResponseWriter, r *http.Requ
}
if si.Mode == 0 {
// Not, set it to default.
si.Mode = 0600
si.Mode = 0o600
}
header, zerr := zip.FileInfoHeader(dummyFileInfo{
name: filename,
+11 -5
View File
@@ -28,6 +28,7 @@ import (
"net/url"
"sync"
"testing"
"time"
"github.com/gorilla/mux"
"github.com/minio/madmin-go"
@@ -40,11 +41,13 @@ 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()
@@ -56,11 +59,13 @@ func prepareAdminErasureTestBed(ctx context.Context) (*adminErasureTestBed, erro
// 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
}
@@ -71,9 +76,9 @@ func prepareAdminErasureTestBed(ctx context.Context) (*adminErasureTestBed, erro
newAllSubsystems()
initAllSubsystems(ctx, objLayer)
initConfigSubsystem(ctx, objLayer)
globalIAMSys.InitStore(objLayer, globalEtcdClient)
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, globalNotificationSys, 2*time.Second)
// Setup admin mgmt REST API handlers.
adminRouter := mux.NewRouter()
@@ -83,12 +88,14 @@ func prepareAdminErasureTestBed(ctx context.Context) (*adminErasureTestBed, erro
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()
}
@@ -229,8 +236,8 @@ func TestServiceRestartHandler(t *testing.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) {
contentLength int64, bodySeeker io.ReadSeeker) (*http.Request, error,
) {
req, err := newTestRequest(method,
adminPathPrefix+adminAPIVersionPrefix+path+"?"+queryVal.Encode(),
contentLength, bodySeeker)
@@ -373,5 +380,4 @@ func TestExtractHealInitParams(t *testing.T) {
}
}
}
}
+11 -7
View File
@@ -278,8 +278,8 @@ func (ahs *allHealState) stopHealSequence(path string) ([]byte, APIError) {
// 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) {
respBytes []byte, apiErr APIError, errMsg string,
) {
if h.forceStarted {
_, apiErr = ahs.stopHealSequence(pathJoin(h.bucket, h.object))
if apiErr.Code != "" {
@@ -338,8 +338,8 @@ func (ahs *allHealState) LaunchNewHealSequence(h *healSequence, objAPI ObjectLay
// representation. The clientToken helps ensure there aren't
// conflicting clients fetching status.
func (ahs *allHealState) PopHealStatusJSON(hpath string,
clientToken string) ([]byte, APIErrorCode) {
clientToken string) ([]byte, APIErrorCode,
) {
// fetch heal state for given path
h, exists := ahs.getHealSequence(hpath)
if !exists {
@@ -453,8 +453,8 @@ type healSequence struct {
// 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 {
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))
@@ -491,7 +491,7 @@ func (h *healSequence) getScannedItemsCount() int64 {
defer h.mutex.RUnlock()
for _, v := range h.scannedItemsMap {
count = count + v
count += v
}
return count
}
@@ -710,6 +710,9 @@ func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItem
select {
case globalBackgroundHealRoutine.tasks <- task:
if serverDebugLog {
logger.Info("Task in the queue: %#v", task)
}
case <-h.ctx.Done():
return nil
}
@@ -885,6 +888,7 @@ func (h *healSequence) healObject(bucket, object, versionID string) error {
bucket: bucket,
object: object,
versionID: versionID,
opts: &h.settings,
}, madmin.HealItemObject)
// Wait and proceed if there are active requests
+13 -13
View File
@@ -38,13 +38,10 @@ 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()
/// Service operations
adminVersions := []string{
adminAPIVersionPrefix,
}
@@ -71,7 +68,7 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/datausageinfo").HandlerFunc(gz(httpTraceAll(adminAPI.DataUsageInfoHandler)))
if globalIsDistErasure || globalIsErasure {
/// Heal operations
// Heal operations
// Heal processing endpoint.
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/").HandlerFunc(gz(httpTraceAll(adminAPI.HealHandler)))
@@ -79,9 +76,6 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/{bucket}/{prefix:.*}").HandlerFunc(gz(httpTraceAll(adminAPI.HealHandler)))
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/background-heal/status").HandlerFunc(gz(httpTraceAll(adminAPI.BackgroundHealStatusHandler)))
/// Health operations
}
// Profiling operations
@@ -106,7 +100,7 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/restore-config-history-kv").HandlerFunc(gz(httpTraceHdrs(adminAPI.RestoreConfigHistoryKVHandler))).Queries("restoreId", "{restoreId:.*}")
}
/// Config import/export bulk operations
// Config import/export bulk operations
if enableConfigOps {
// Get config
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/config").HandlerFunc(gz(httpTraceHdrs(adminAPI.GetConfigHandler)))
@@ -192,15 +186,21 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/tier/{tier}").HandlerFunc(gz(httpTraceHdrs(adminAPI.EditTierHandler)))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListTierHandler)))
// Tier stats
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier-stats").HandlerFunc(gz(httpTraceHdrs(adminAPI.TierStatsHandler)))
// Cluster Replication APIs
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/add").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationAdd)))
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/disable").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationDisable)))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/info").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationInfo)))
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/join").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRInternalJoin)))
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/site-replication/peer/bucket-ops").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRInternalBucketOps))).Queries("bucket", "{bucket:.*}").Queries("operation", "{operation:.*}")
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/iam-item").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRInternalReplicateIAMItem)))
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/bucket-meta").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRInternalReplicateBucketItem)))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/peer/idp-settings").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRInternalGetIDPSettings)))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/metainfo").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationMetaInfo)))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/status").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationStatus)))
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/join").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerJoin)))
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/site-replication/peer/bucket-ops").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerBucketOps))).Queries("bucket", "{bucket:.*}").Queries("operation", "{operation:.*}")
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/iam-item").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerReplicateIAMItem)))
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/bucket-meta").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerReplicateBucketItem)))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/peer/idp-settings").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerGetIDPSettings)))
}
if globalIsDistErasure {
+7 -2
View File
@@ -48,10 +48,15 @@ func (t DeleteMarkerMTime) MarshalXML(e *xml.Encoder, startElement xml.StartElem
return e.EncodeElement(t.Time.Format(time.RFC3339), startElement)
}
// ObjectToDelete carries key name for the object to delete.
type ObjectToDelete struct {
// 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)
+38 -12
View File
@@ -82,6 +82,7 @@ const (
ErrIncompleteBody
ErrInternalError
ErrInvalidAccessKeyID
ErrAccessKeyDisabled
ErrInvalidBucketName
ErrInvalidDigest
ErrInvalidRange
@@ -109,6 +110,7 @@ const (
ErrNoSuchBucketPolicy
ErrNoSuchBucketLifecycle
ErrNoSuchLifecycleConfiguration
ErrInvalidLifecycleWithObjectLock
ErrNoSuchBucketSSEConfig
ErrNoSuchCORSConfiguration
ErrNoSuchWebsiteConfiguration
@@ -128,6 +130,7 @@ const (
ErrReplicationSourceNotVersionedError
ErrReplicationNeedsVersioningError
ErrReplicationBucketNeedsVersioningError
ErrReplicationDenyEditError
ErrReplicationNoMatchingRuleError
ErrObjectRestoreAlreadyInProgress
ErrNoSuchKey
@@ -395,10 +398,10 @@ func (e errorCodeMap) ToAPIErrWithErr(errCode APIErrorCode, err error) APIError
if err != nil {
apiErr.Description = fmt.Sprintf("%s (%s)", apiErr.Description, err)
}
if globalServerRegion != "" {
if globalSite.Region != "" {
switch errCode {
case ErrAuthorizationHeaderMalformed:
apiErr.Description = fmt.Sprintf("The authorization header is malformed; the region is wrong; expecting '%s'.", globalServerRegion)
apiErr.Description = fmt.Sprintf("The authorization header is malformed; the region is wrong; expecting '%s'.", globalSite.Region)
return apiErr
}
}
@@ -512,6 +515,11 @@ var errorCodes = errorCodeMap{
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,
},
ErrInvalidBucketName: {
Code: "InvalidBucketName",
Description: "The specified bucket is not valid.",
@@ -577,6 +585,11 @@ var errorCodes = errorCodeMap{
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",
@@ -674,7 +687,7 @@ var errorCodes = errorCodeMap{
},
ErrAllAccessDisabled: {
Code: "AllAccessDisabled",
Description: "All access to this bucket has been disabled.",
Description: "All access to this resource has been disabled.",
HTTPStatusCode: http.StatusForbidden,
},
ErrMalformedPolicy: {
@@ -882,6 +895,11 @@ var errorCodes = errorCodeMap{
Description: "No matching replication rule found for this object prefix",
HTTPStatusCode: http.StatusBadRequest,
},
ErrReplicationDenyEditError: {
Code: "XMinioReplicationDenyEdit",
Description: "Cannot alter local replication config since this server is in a cluster replication setup",
HTTPStatusCode: http.StatusConflict,
},
ErrBucketRemoteIdenticalToSource: {
Code: "XMinioAdminRemoteIdenticalToSource",
Description: "The remote target cannot be identical to source",
@@ -973,7 +991,7 @@ var errorCodes = errorCodeMap{
HTTPStatusCode: http.StatusNotFound,
},
/// Bucket notification related errors.
// Bucket notification related errors.
ErrEventNotification: {
Code: "InvalidArgument",
Description: "A specified event is not supported for notifications.",
@@ -1120,14 +1138,14 @@ var errorCodes = errorCodeMap{
HTTPStatusCode: http.StatusForbidden,
},
/// S3 extensions.
// S3 extensions.
ErrContentSHA256Mismatch: {
Code: "XAmzContentSHA256Mismatch",
Description: "The provided 'x-amz-content-sha256' header does not match what was computed.",
HTTPStatusCode: http.StatusBadRequest,
},
/// MinIO extensions.
// MinIO extensions.
ErrStorageFull: {
Code: "XMinioStorageFull",
Description: "Storage backend has reached its minimum free disk threshold. Please delete a few objects to proceed.",
@@ -1362,15 +1380,15 @@ var errorCodes = errorCodeMap{
},
ErrBackendDown: {
Code: "XMinioBackendDown",
Description: "Object storage backend is unreachable",
HTTPStatusCode: http.StatusServiceUnavailable,
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
// S3 Select API Errors
ErrEmptyRequestBody: {
Code: "EmptyRequestBody",
Description: "Request body cannot be empty.",
@@ -1956,6 +1974,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
apiErr = ErrNoSuchKey
case MethodNotAllowed:
apiErr = ErrMethodNotAllowed
case ObjectLocked:
apiErr = ErrObjectLocked
case InvalidVersionID:
apiErr = ErrInvalidVersionID
case VersionNotFound:
@@ -2072,6 +2092,7 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
default:
var ie, iw int
// This work-around is to handle the issue golang/go#30648
//nolint:gocritic
if _, ferr := fmt.Fscanf(strings.NewReader(err.Error()),
"request declared a Content-Length of %d but only wrote %d bytes",
&ie, &iw); ferr != nil {
@@ -2104,7 +2125,7 @@ func toAPIError(ctx context.Context, err error) APIError {
return noError
}
var apiErr = errorCodes.ToAPIErr(toAPIErrorCode(ctx, err))
apiErr := errorCodes.ToAPIErr(toAPIErrorCode(ctx, err))
e, ok := err.(dns.ErrInvalidBucketName)
if ok {
code := toAPIErrorCode(ctx, e)
@@ -2127,6 +2148,11 @@ func toAPIError(ctx context.Context, err error) APIError {
}
}
if apiErr.Code == "XMinioBackendDown" {
apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, err)
return apiErr
}
if apiErr.Code == "InternalError" {
// If we see an internal error try to interpret
// any underlying errors if possible depending on
@@ -2212,7 +2238,6 @@ func toAPIError(ctx context.Context, err error) APIError {
// since S3 only sends one Error XML response.
if len(e.Errors) >= 1 {
apiErr.Code = e.Errors[0].Reason
}
case azblob.StorageError:
apiErr = APIError{
@@ -2222,6 +2247,7 @@ func toAPIError(ctx context.Context, err error) APIError {
}
// Add more Gateway SDKs here if any in future.
default:
//nolint:gocritic
if errors.Is(err, errMalformedEncoding) {
apiErr = APIError{
Code: "BadRequest",
@@ -2271,7 +2297,7 @@ func getAPIErrorResponse(ctx context.Context, err APIError, resource, requestID,
BucketName: reqInfo.BucketName,
Key: reqInfo.ObjectName,
Resource: resource,
Region: globalServerRegion,
Region: globalSite.Region,
RequestID: requestID,
HostID: hostID,
}
+1 -1
View File
@@ -52,7 +52,7 @@ func setCommonHeaders(w http.ResponseWriter) {
// Set `x-amz-bucket-region` only if region is set on the server
// by default minio uses an empty region.
if region := globalServerRegion; region != "" {
if region := globalSite.Region; region != "" {
w.Header().Set(xhttp.AmzBucketRegion, region)
}
w.Header().Set(xhttp.AcceptRanges, "bytes")
+1 -1
View File
@@ -23,7 +23,7 @@ import (
func TestNewRequestID(t *testing.T) {
// Ensure that it returns an alphanumeric result of length 16.
var id = mustGetRequestID(UTCNow())
id := mustGetRequestID(UTCNow())
if len(id) != 16 {
t.Fail()
+18 -23
View File
@@ -268,7 +268,6 @@ type StringMap map[string]string
// MarshalXML - StringMap marshals into XML.
func (s StringMap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
tokens := []xml.Token{start}
for key, value := range s {
@@ -417,8 +416,8 @@ func getObjectLocation(r *http.Request, domains []string, bucket, object string)
// serialized to match XML and JSON API spec output.
func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
listbuckets := make([]Bucket, 0, len(buckets))
var data = ListBucketsResponse{}
var owner = Owner{
data := ListBucketsResponse{}
owner := Owner{
ID: globalMinioDefaultOwnerID,
DisplayName: "minio",
}
@@ -439,14 +438,14 @@ func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
// generates an ListBucketVersions response for the said bucket with other enumerated options.
func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delimiter, encodingType string, maxKeys int, resp ListObjectVersionsInfo) ListVersionsResponse {
versions := make([]ObjectVersion, 0, len(resp.Objects))
var owner = Owner{
owner := Owner{
ID: globalMinioDefaultOwnerID,
DisplayName: "minio",
}
var data = ListVersionsResponse{}
data := ListVersionsResponse{}
for _, object := range resp.Objects {
var content = ObjectVersion{}
content := ObjectVersion{}
if object.Name == "" {
continue
}
@@ -486,7 +485,7 @@ func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delim
prefixes := make([]CommonPrefix, 0, len(resp.Prefixes))
for _, prefix := range resp.Prefixes {
var prefixItem = CommonPrefix{}
prefixItem := CommonPrefix{}
prefixItem.Prefix = s3EncodeName(prefix, encodingType)
prefixes = append(prefixes, prefixItem)
}
@@ -497,14 +496,14 @@ func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delim
// generates an ListObjectsV1 response for the said bucket with other enumerated options.
func generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingType string, maxKeys int, resp ListObjectsInfo) ListObjectsResponse {
contents := make([]Object, 0, len(resp.Objects))
var owner = Owner{
owner := Owner{
ID: globalMinioDefaultOwnerID,
DisplayName: "minio",
}
var data = ListObjectsResponse{}
data := ListObjectsResponse{}
for _, object := range resp.Objects {
var content = Object{}
content := Object{}
if object.Name == "" {
continue
}
@@ -535,7 +534,7 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingTy
prefixes := make([]CommonPrefix, 0, len(resp.Prefixes))
for _, prefix := range resp.Prefixes {
var prefixItem = CommonPrefix{}
prefixItem := CommonPrefix{}
prefixItem.Prefix = s3EncodeName(prefix, encodingType)
prefixes = append(prefixes, prefixItem)
}
@@ -546,14 +545,14 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingTy
// generates an ListObjectsV2 response for the said bucket with other enumerated options.
func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter, delimiter, encodingType string, fetchOwner, isTruncated bool, maxKeys int, objects []ObjectInfo, prefixes []string, metadata bool) ListObjectsV2Response {
contents := make([]Object, 0, len(objects))
var owner = Owner{
owner := Owner{
ID: globalMinioDefaultOwnerID,
DisplayName: "minio",
}
var data = ListObjectsV2Response{}
data := ListObjectsV2Response{}
for _, object := range objects {
var content = Object{}
content := Object{}
if object.Name == "" {
continue
}
@@ -608,7 +607,7 @@ func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter,
commonPrefixes := make([]CommonPrefix, 0, len(prefixes))
for _, prefix := range prefixes {
var prefixItem = CommonPrefix{}
prefixItem := CommonPrefix{}
prefixItem.Prefix = s3EncodeName(prefix, encodingType)
commonPrefixes = append(commonPrefixes, prefixItem)
}
@@ -724,9 +723,6 @@ func generateMultiDeleteResponse(quiet bool, deletedObjects []DeletedObject, err
if !quiet {
deleteResp.DeletedObjects = deletedObjects
}
if len(errs) == len(deletedObjects) {
deleteResp.DeletedObjects = nil
}
deleteResp.Errors = errs
return deleteResp
}
@@ -740,7 +736,6 @@ func writeResponse(w http.ResponseWriter, statusCode int, response []byte, mType
w.WriteHeader(statusCode)
if response != nil {
w.Write(response)
w.(http.Flusher).Flush()
}
}
@@ -791,9 +786,9 @@ func writeErrorResponse(ctx context.Context, w http.ResponseWriter, err APIError
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
w.Header().Set(xhttp.RetryAfter, "120")
case "InvalidRegion":
err.Description = fmt.Sprintf("Region does not match; expecting '%s'.", globalServerRegion)
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'.", globalServerRegion)
err.Description = fmt.Sprintf("The authorization header is malformed; the region is wrong; expecting '%s'.", globalSite.Region)
}
// Generate error response.
@@ -825,8 +820,8 @@ func writeErrorResponseJSON(ctx context.Context, w http.ResponseWriter, err APIE
// 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) {
errBody string, reqURL *url.URL,
) {
reqInfo := logger.GetReqInfo(ctx)
errorResponse := APIErrorResponse{
Code: err.Code,
+17 -4
View File
@@ -24,6 +24,7 @@ import (
"github.com/gorilla/mux"
"github.com/klauspost/compress/gzhttp"
"github.com/minio/console/restapi"
xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/logger"
"github.com/minio/pkg/wildcard"
@@ -42,6 +43,18 @@ func setHTTPServer(h *xhttp.Server) {
globalObjLayerMutex.Unlock()
}
func newConsoleServerFn() *restapi.Server {
globalObjLayerMutex.RLock()
defer globalObjLayerMutex.RUnlock()
return globalConsoleSrv
}
func setConsoleSrv(srv *restapi.Server) {
globalObjLayerMutex.Lock()
globalConsoleSrv = srv
globalObjLayerMutex.Unlock()
}
func newObjectLayerFn() ObjectLayer {
globalObjLayerMutex.RLock()
defer globalObjLayerMutex.RUnlock()
@@ -301,7 +314,8 @@ func registerAPIRouter(router *mux.Router) {
router.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(
collectAPIStats("restoreobject", maxClients(gz(httpTraceAll(api.PostRestoreObjectHandler))))).Queries("restore", "")
/// Bucket operations
// Bucket operations
// GetBucketLocation
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("getbucketlocation", maxClients(gz(httpTraceAll(api.GetBucketLocationHandler))))).Queries("location", "")
@@ -355,7 +369,7 @@ func registerAPIRouter(router *mux.Router) {
// GetBucketTaggingHandler
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("getbuckettagging", maxClients(gz(httpTraceAll(api.GetBucketTaggingHandler))))).Queries("tagging", "")
//DeleteBucketWebsiteHandler
// DeleteBucketWebsiteHandler
router.Methods(http.MethodDelete).HandlerFunc(
collectAPIStats("deletebucketwebsite", maxClients(gz(httpTraceAll(api.DeleteBucketWebsiteHandler))))).Queries("website", "")
// DeleteBucketTaggingHandler
@@ -452,7 +466,7 @@ func registerAPIRouter(router *mux.Router) {
collectAPIStats("listobjectsv1", maxClients(gz(httpTraceAll(api.ListObjectsV1Handler)))))
}
/// Root operation
// Root operation
// ListenNotification
apiRouter.Methods(http.MethodGet).Path(SlashSeparator).HandlerFunc(
@@ -470,7 +484,6 @@ func registerAPIRouter(router *mux.Router) {
// 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)
-1
View File
@@ -44,7 +44,6 @@ func TestS3EncodeName(t *testing.T) {
if testCase.expectedOutput != outputText {
t.Errorf("Expected `%s`, got `%s`", testCase.expectedOutput, outputText)
}
})
}
}
+279 -276
View File
File diff suppressed because one or more lines are too long
+23 -16
View File
@@ -238,19 +238,6 @@ func getClaimsFromToken(token string) (map[string]interface{}, error) {
claims.MapClaims[iampolicy.SessionPolicyName] = string(spBytes)
}
// If LDAP claim key is set, return here.
if _, ok := claims.MapClaims[ldapUser]; ok {
return claims.Map(), nil
}
// Session token must have a policy, reject requests without policy
// claim.
_, pokOpenID := claims.MapClaims[iamPolicyClaimNameOpenID()]
_, pokSA := claims.MapClaims[iamPolicyClaimNameSA()]
if !pokOpenID && !pokSA {
return nil, errAuthentication
}
return claims.Map(), nil
}
@@ -299,7 +286,7 @@ func checkRequestAuthTypeCredential(ctx context.Context, r *http.Request, action
}
cred, owner, s3Err = getReqAccessKeyV2(r)
case authTypeSigned, authTypePresigned:
region := globalServerRegion
region := globalSite.Region
switch action {
case policy.GetBucketLocationAction, policy.ListAllMyBucketsAction:
region = ""
@@ -487,6 +474,26 @@ func setAuthHandler(h http.Handler) http.Handler {
// handler for validating incoming authorization headers.
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
aType := getRequestAuthType(r)
if aType == authTypeSigned || aType == authTypeSignedV2 || aType == authTypeStreamingSigned {
// Verify if date headers are set, if not reject the request
amzDate, errCode := parseAmzDateHeader(r)
if errCode != ErrNone {
// All our internal APIs are sensitive towards Date
// header, for all requests where Date header is not
// present we will reject such clients.
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 {
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrRequestTimeTooSkewed), r.URL)
atomic.AddUint64(&globalHTTPStats.rejectedRequestsTime, 1)
return
}
}
if isSupportedS3AuthType(aType) || aType == authTypeJWT || aType == authTypeSTS {
h.ServeHTTP(w, r)
return
@@ -509,7 +516,7 @@ func validateSignature(atype authType, r *http.Request) (auth.Credentials, bool,
}
cred, owner, s3Err = getReqAccessKeyV2(r)
case authTypePresigned, authTypeSigned:
region := globalServerRegion
region := globalSite.Region
if s3Err = isReqAuthenticated(GlobalContext, r, region, serviceS3); s3Err != ErrNone {
return cred, owner, s3Err
}
@@ -576,7 +583,7 @@ func isPutActionAllowed(ctx context.Context, atype authType, bucketName, objectN
case authTypeSignedV2, authTypePresignedV2:
cred, owner, s3Err = getReqAccessKeyV2(r)
case authTypeStreamingSigned, authTypePresigned, authTypeSigned:
region := globalServerRegion
region := globalSite.Region
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
}
if s3Err != ErrNone {
+11 -9
View File
@@ -364,9 +364,12 @@ func TestIsReqAuthenticated(t *testing.T) {
newAllSubsystems()
initAllSubsystems(context.Background(), objLayer)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
globalIAMSys.InitStore(objLayer, globalEtcdClient)
initConfigSubsystem(ctx, objLayer)
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, globalNotificationSys, 2*time.Second)
creds, err := auth.CreateCredentials("myuser", "mypassword")
if err != nil {
@@ -392,10 +395,9 @@ func TestIsReqAuthenticated(t *testing.T) {
{mustNewSignedRequest(http.MethodGet, "http://127.0.0.1:9000", 0, nil, t), ErrNone},
}
ctx := context.Background()
// Validates all testcases.
for i, testCase := range testCases {
s3Error := isReqAuthenticated(ctx, testCase.req, globalServerRegion, serviceS3)
s3Error := isReqAuthenticated(ctx, testCase.req, globalSite.Region, serviceS3)
if s3Error != testCase.s3Error {
if _, err := ioutil.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)
@@ -433,15 +435,15 @@ func TestCheckAdminRequestAuthType(t *testing.T) {
}
ctx := context.Background()
for i, testCase := range testCases {
if _, s3Error := checkAdminRequestAuth(ctx, testCase.Request, iampolicy.AllAdminActions, globalServerRegion); s3Error != testCase.ErrCode {
if _, s3Error := checkAdminRequestAuth(ctx, testCase.Request, iampolicy.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 := context.Background()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
objLayer, fsDir, err := prepareFS()
if err != nil {
@@ -455,9 +457,9 @@ func TestValidateAdminSignature(t *testing.T) {
newAllSubsystems()
initAllSubsystems(context.Background(), objLayer)
initConfigSubsystem(ctx, objLayer)
globalIAMSys.InitStore(objLayer, globalEtcdClient)
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, globalNotificationSys, 2*time.Second)
creds, err := auth.CreateCredentials("admin", "mypassword")
if err != nil {
-1
View File
@@ -115,7 +115,6 @@ func newHealRoutine() *healRoutine {
tasks: make(chan healTask),
workers: workers,
}
}
// healDiskFormat - heals format.json, return value indicates if a
+14 -15
View File
@@ -18,12 +18,12 @@
package cmd
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"sort"
"strings"
"sync"
@@ -305,7 +305,6 @@ func getLocalDisksToHeal() (disksToHeal Endpoints) {
}
}
return disksToHeal
}
// monitorLocalDisksAndHeal - ensures that detected new disks are healed
@@ -375,15 +374,13 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools, bgSeq
buckets, _ := z.ListBuckets(ctx)
buckets = append(buckets, BucketInfo{
Name: pathJoin(minioMetaBucket, minioConfigPrefix),
})
// Buckets data are dispersed in multiple zones/sets, make
// sure to heal all bucket metadata configuration.
buckets = append(buckets, []BucketInfo{
{Name: pathJoin(minioMetaBucket, bucketMetaPrefix)},
}...)
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 {
@@ -412,7 +409,8 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools, bgSeq
// So someone changed the drives underneath, healing tracker missing.
tracker, err := loadHealingTracker(ctx, disk)
if err != nil {
logger.Info("Healing tracker missing on '%s', disk was swapped again on %s pool", disk, humanize.Ordinal(i+1))
logger.Info("Healing tracker missing on '%s', disk was swapped again on %s pool",
disk, humanize.Ordinal(i+1))
tracker = newHealingTracker(disk)
}
@@ -434,17 +432,18 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools, bgSeq
return
}
err = z.serverPools[i].sets[setIndex].healErasureSet(ctx, buckets, tracker)
err = z.serverPools[i].sets[setIndex].healErasureSet(ctx, tracker.QueuedBuckets, tracker)
if err != nil {
logger.LogIf(ctx, err)
continue
}
logger.Info("Healing disk '%s' on %s pool complete", disk, humanize.Ordinal(i+1))
var buf bytes.Buffer
tracker.printTo(&buf)
logger.Info("Summary:\n%s", buf.String())
logger.Info("Healing disk '%s' on %s pool, %s set complete", disk,
humanize.Ordinal(i+1), humanize.Ordinal(setIndex+1))
logger.Info("Summary:\n")
tracker.printTo(os.Stdout)
logger.LogIf(ctx, tracker.delete(ctx))
logger.Info("\n")
// Only upon success pop the healed disk.
globalBackgroundHealState.popHealLocalDisks(disk.Endpoint())
+1 -1
View File
@@ -212,7 +212,7 @@ func bitrotVerify(r io.Reader, wantSize, partSize int64, algo BitrotAlgorithm, w
// bitrotSelfTest tries to catch any issue in the bitrot implementation
// early instead of silently corrupting data.
func bitrotSelfTest() {
var checksums = map[BitrotAlgorithm]string{
checksums := map[BitrotAlgorithm]string{
SHA256: "a7677ff19e0182e4d52e3a3db727804abc82a5818749336369552e54b838b004",
BLAKE2b512: "e519b7d84b1c3c917985f544773a35cf265dcab10948be3550320d156bab612124a5ae2ae5a8c73c0eea360f68b0e28136f26e858756dbfe7375a7389f26c669",
HighwayHash256: "39c0407ed3f01b18d22c85db4aeff11e060ca5f43131b0126731ca197cd42313",
+18 -5
View File
@@ -86,14 +86,28 @@ func (s1 ServerSystemConfig) Diff(s2 ServerSystemConfig) error {
}
}
if !reflect.DeepEqual(s1.MinioEnv, s2.MinioEnv) {
return fmt.Errorf("Expected same MINIO_ environment variables and values")
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)
}
}
if len(mismatching) > 0 {
return fmt.Errorf(`Expected same MINIO_ environment variables and values across all servers: Missing environment values: %s / Mismatch environment values: %s`, missing, mismatching)
}
return fmt.Errorf(`Expected same MINIO_ environment variables and values across all servers: Missing environment values: %s`, missing)
}
return nil
}
var skipEnvs = map[string]struct{}{
"MINIO_OPTS": {},
"MINIO_CERT_PASSWD": {},
"MINIO_OPTS": {},
"MINIO_CERT_PASSWD": {},
"MINIO_SERVER_DEBUG": {},
}
func getServerSystemCfg() ServerSystemConfig {
@@ -123,7 +137,6 @@ func (b *bootstrapRESTServer) VerifyHandler(w http.ResponseWriter, r *http.Reque
ctx := newContext(r, w, "VerifyHandler")
cfg := getServerSystemCfg()
logger.LogIf(ctx, json.NewEncoder(w).Encode(&cfg))
w.(http.Flusher).Flush()
}
// registerBootstrapRESTHandlers - register bootstrap rest router.
@@ -248,7 +261,7 @@ func newBootstrapRESTClient(endpoint Endpoint) *bootstrapRESTClient {
Path: bootstrapRESTPath,
}
restClient := rest.NewClient(serverURL, globalInternodeTransport, newAuthToken)
restClient := rest.NewClient(serverURL, globalInternodeTransport, newCachedAuthToken())
restClient.HealthCheckFn = nil
return &bootstrapRESTClient{endpoint: endpoint, restClient: restClient}
+72 -41
View File
@@ -133,8 +133,6 @@ func initFederatorBackend(buckets []BucketInfo, objLayer ObjectLayer) {
// Add/update buckets that are not registered with the DNS
bucketsToBeUpdatedSlice := bucketsToBeUpdated.ToSlice()
g := errgroup.WithNErrs(len(bucketsToBeUpdatedSlice)).WithConcurrency(50)
ctx, cancel := g.WithCancelOnError(GlobalContext)
defer cancel()
for index := range bucketsToBeUpdatedSlice {
index := index
@@ -143,9 +141,12 @@ func initFederatorBackend(buckets []BucketInfo, objLayer ObjectLayer) {
}, index)
}
if err := g.WaitErr(); err != nil {
logger.LogIf(ctx, err)
return
ctx := GlobalContext
for _, err := range g.Wait() {
if err != nil {
logger.LogIf(ctx, err)
return
}
}
for _, bucket := range bucketsInConflict.ToSlice() {
@@ -210,7 +211,7 @@ func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *
// Generate response.
encodedSuccessResponse := encodeResponse(LocationResponse{})
// Get current region.
region := globalServerRegion
region := globalSite.Region
if region != globalMinioDefaultRegion {
encodedSuccessResponse = encodeResponse(LocationResponse{
Location: region,
@@ -414,18 +415,23 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
const maxBodySize = 2 * 100000 * 1024
// Unmarshal list of keys to be deleted.
deleteObjects := &DeleteObjectsRequest{}
if err := xmlDecoder(r.Body, deleteObjects, maxBodySize); err != nil {
deleteObjectsReq := &DeleteObjectsRequest{}
if err := xmlDecoder(r.Body, deleteObjectsReq, maxBodySize); err != nil {
logger.LogIf(ctx, err, logger.Application)
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 deleteObjects.Objects {
deleteObjects.Objects[i].ObjectName = trimLeadingSlash(deleteObjects.Objects[i].ObjectName)
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, "")
@@ -443,12 +449,12 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
}
// Return Malformed XML as S3 spec if the number of objects is empty
if len(deleteObjects.Objects) == 0 || len(deleteObjects.Objects) > maxDeleteList {
if len(deleteObjectsReq.Objects) == 0 || len(deleteObjectsReq.Objects) > maxDeleteList {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedXML), r.URL)
return
}
var objectsToDelete = map[ObjectToDelete]int{}
objectsToDelete := map[ObjectToDelete]int{}
getObjectInfoFn := objectAPI.GetObjectInfo
if api.CacheAPI() != nil {
getObjectInfoFn = api.CacheAPI().GetObjectInfo
@@ -460,7 +466,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
goi ObjectInfo
gerr error
)
replicateDeletes := hasReplicationRules(ctx, bucket, deleteObjects.Objects)
replicateDeletes := hasReplicationRules(ctx, bucket, deleteObjectsReq.Objects)
if rcfg, _ := globalBucketObjectLockSys.Get(bucket); rcfg.LockEnabled {
hasLockEnabled = true
}
@@ -468,16 +474,23 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
versioned := globalBucketVersioningSys.Enabled(bucket)
suspended := globalBucketVersioningSys.Suspended(bucket)
dErrs := make([]DeleteError, len(deleteObjects.Objects))
oss := make([]*objSweeper, len(deleteObjects.Objects))
for index, object := range deleteObjects.Objects {
type deleteResult struct {
delInfo DeletedObject
errInfo DeleteError
}
deleteResults := make([]deleteResult, len(deleteObjectsReq.Objects))
oss := make([]*objSweeper, len(deleteObjectsReq.Objects))
for index, object := range deleteObjectsReq.Objects {
if apiErrCode := checkRequestAuthType(ctx, r, policy.DeleteObjectAction, bucket, object.ObjectName); apiErrCode != ErrNone {
if apiErrCode == ErrSignatureDoesNotMatch || apiErrCode == ErrInvalidAccessKeyID {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(apiErrCode), r.URL)
return
}
apiErr := errorCodes.ToAPIErr(apiErrCode)
dErrs[index] = DeleteError{
deleteResults[index].errInfo = DeleteError{
Code: apiErr.Code,
Message: apiErr.Description,
Key: object.ObjectName,
@@ -489,7 +502,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
if _, err := uuid.Parse(object.VersionID); err != nil {
logger.LogIf(ctx, fmt.Errorf("invalid version-id specified %w", err))
apiErr := errorCodes.ToAPIErr(ErrNoSuchVersion)
dErrs[index] = DeleteError{
deleteResults[index].errInfo = DeleteError{
Code: apiErr.Code,
Message: apiErr.Description,
Key: object.ObjectName,
@@ -519,8 +532,10 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
if replicateDeletes {
dsc = checkReplicateDelete(ctx, bucket, ObjectToDelete{
ObjectName: object.ObjectName,
VersionID: object.VersionID,
ObjectV: ObjectV{
ObjectName: object.ObjectName,
VersionID: object.VersionID,
},
}, goi, opts, gerr)
if dsc.ReplicateAny() {
if object.VersionID != "" {
@@ -535,7 +550,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
if object.VersionID != "" && hasLockEnabled {
if apiErrCode := enforceRetentionBypassForDelete(ctx, r, bucket, object, goi, gerr); apiErrCode != ErrNone {
apiErr := errorCodes.ToAPIErr(apiErrCode)
dErrs[index] = DeleteError{
deleteResults[index].errInfo = DeleteError{
Code: apiErr.Code,
Message: apiErr.Description,
Key: object.ObjectName,
@@ -561,20 +576,25 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
return
}
// Disable timeouts and cancellation
ctx = bgContext(ctx)
deleteList := toNames(objectsToDelete)
dObjects, errs := deleteObjectsFn(ctx, bucket, deleteList, ObjectOptions{
Versioned: versioned,
VersionSuspended: suspended,
})
deletedObjects := make([]DeletedObject, len(deleteObjects.Objects))
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{
ObjectName: dObjects[i].ObjectName,
VersionID: dObjects[i].VersionID,
ObjectV: ObjectV{
ObjectName: dObjects[i].ObjectName,
VersionID: dObjects[i].VersionID,
},
VersionPurgeStatus: dObjects[i].VersionPurgeStatus(),
VersionPurgeStatuses: dObjects[i].ReplicationState.VersionPurgeStatusInternal,
DeleteMarkerReplicationStatus: dObjects[i].ReplicationState.ReplicationStatusInternal,
@@ -585,11 +605,11 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
if replicateDeletes {
dObjects[i].ReplicationState = deleteList[i].ReplicationState()
}
deletedObjects[dindex] = dObjects[i]
deleteResults[dindex].delInfo = dObjects[i]
continue
}
apiErr := toAPIError(ctx, errs[i])
dErrs[dindex] = DeleteError{
deleteResults[dindex].errInfo = DeleteError{
Code: apiErr.Code,
Message: apiErr.Description,
Key: deleteList[i].ObjectName,
@@ -597,15 +617,18 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
}
}
var deleteErrors []DeleteError
for _, dErr := range dErrs {
if dErr.Code != "" {
deleteErrors = append(deleteErrors, dErr)
// 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 {
deletedObjects = append(deletedObjects, deleteResult.delInfo)
}
}
// Generate response
response := generateMultiDeleteResponse(deleteObjects.Quiet, deletedObjects, deleteErrors)
response := generateMultiDeleteResponse(deleteObjectsReq.Quiet, deletedObjects, deleteErrors)
encodedSuccessResponse := encodeResponse(response)
// Write success response.
@@ -902,7 +925,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
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.Replace(formValues.Get("Key"), "${filename}", fileName, -1))
formValues.Set("Key", strings.ReplaceAll(formValues.Get("Key"), "${filename}", fileName))
}
object := trimLeadingSlash(formValues.Get("Key"))
@@ -1199,7 +1222,7 @@ func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Re
return
}
writeSuccessResponseHeadersOnly(w)
writeResponse(w, http.StatusOK, nil, mimeXML)
}
// DeleteBucketHandler - Delete bucket
@@ -1269,7 +1292,7 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
}
if globalDNSConfig != nil {
if err2 := globalDNSConfig.Put(bucket); err2 != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to restore bucket DNS entry %w, pl1ease fix it manually", err2))
logger.LogIf(ctx, fmt.Errorf("Unable to restore bucket DNS entry %w, please fix it manually", err2))
}
}
writeErrorResponse(ctx, w, apiErr, r.URL)
@@ -1575,7 +1598,10 @@ func (api objectAPIHandlers) PutBucketReplicationConfigHandler(w http.ResponseWr
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
if globalSiteReplicationSys.isEnabled() {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationDenyEditError), r.URL)
return
}
if versioned := globalBucketVersioningSys.Enabled(bucket); !versioned {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationNeedsVersioningError), r.URL)
return
@@ -1677,6 +1703,10 @@ func (api objectAPIHandlers) DeleteBucketReplicationConfigHandler(w http.Respons
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
if globalSiteReplicationSys.isEnabled() {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationDenyEditError), r.URL)
return
}
if err := globalBucketMetadataSys.Update(bucket, bucketReplicationConfig, nil); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
@@ -1720,13 +1750,13 @@ func (api objectAPIHandlers) GetBucketReplicationMetricsHandler(w http.ResponseW
usageInfo = dataUsageInfo.BucketsUsage[bucket]
}
bucketReplStats := getLatestReplicationStats(bucket, usageInfo)
jsonData, err := json.Marshal(bucketReplStats)
if err != nil {
w.Header().Set(xhttp.ContentType, string(mimeJSON))
enc := json.NewEncoder(w)
if err = enc.Encode(getLatestReplicationStats(bucket, usageInfo)); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
writeSuccessResponseJSON(w, jsonData)
}
// ResetBucketReplicationStateHandler - starts a replication reset for all objects in a bucket which
@@ -1788,7 +1818,8 @@ func (api objectAPIHandlers) ResetBucketReplicationStateHandler(w http.ResponseW
tgtArns := config.FilterTargetArns(
replication.ObjectOpts{
OpType: replication.ResyncReplicationType,
TargetArn: arn})
TargetArn: arn,
})
if len(tgtArns) == 0 {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
+75 -21
View File
@@ -20,6 +20,7 @@ package cmd
import (
"bytes"
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
@@ -80,8 +81,8 @@ func TestGetBucketLocationHandler(t *testing.T) {
}
func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
credentials auth.Credentials, t *testing.T) {
credentials auth.Credentials, t *testing.T,
) {
// test cases with sample input and expected output.
testCases := []struct {
bucketName string
@@ -162,7 +163,6 @@ func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName stri
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: <ERROR> %v", i+1, instanceType, err)
}
@@ -209,7 +209,6 @@ func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName stri
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)
}
@@ -224,8 +223,8 @@ func TestHeadBucketHandler(t *testing.T) {
}
func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
credentials auth.Credentials, t *testing.T) {
credentials auth.Credentials, t *testing.T,
) {
// test cases with sample input and expected output.
testCases := []struct {
bucketName string
@@ -281,7 +280,6 @@ func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, api
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: <ERROR> %v", i+1, instanceType, err)
}
@@ -296,7 +294,6 @@ func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, api
// 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\": <ERROR> %v",
instanceType, bucketName, err)
@@ -314,7 +311,6 @@ func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, api
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)
}
@@ -330,8 +326,8 @@ func TestListMultipartUploadsHandler(t *testing.T) {
// testListMultipartUploadsHandler - Tests validate listing of multipart uploads.
func testListMultipartUploadsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
credentials auth.Credentials, t *testing.T) {
credentials auth.Credentials, t *testing.T,
) {
// Collection of non-exhaustive ListMultipartUploads test cases, valid errors
// and success responses.
testCases := []struct {
@@ -551,7 +547,6 @@ func testListMultipartUploadsHandler(obj ObjectLayer, instanceType, bucketName s
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)
}
@@ -567,8 +562,8 @@ func TestListBucketsHandler(t *testing.T) {
// testListBucketsHandler - Tests validate listing of buckets.
func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
credentials auth.Credentials, t *testing.T) {
credentials auth.Credentials, t *testing.T,
) {
testCases := []struct {
bucketName string
accessKey string
@@ -614,7 +609,6 @@ func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, ap
// 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: <ERROR> %v", i+1, instanceType, err)
}
@@ -629,7 +623,6 @@ func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, ap
// 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)
}
@@ -645,7 +638,6 @@ func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, ap
// 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)
}
@@ -656,12 +648,12 @@ func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, ap
// Wrapper for calling DeleteMultipleObjects HTTP handler tests for both Erasure multiple disks and single node setup.
func TestAPIDeleteMultipleObjectsHandler(t *testing.T) {
ExecObjectLayerAPITest(t, testAPIDeleteMultipleObjectsHandler, []string{"DeleteMultipleObjects"})
ExecObjectLayerAPITest(t, testAPIDeleteMultipleObjectsHandler, []string{"DeleteMultipleObjects", "PutBucketPolicy"})
}
func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
credentials auth.Credentials, t *testing.T) {
credentials auth.Credentials, t *testing.T,
) {
var err error
contentBytes := []byte("hello")
@@ -680,10 +672,35 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
objectNames = append(objectNames, objectName)
}
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: <ERROR> %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 := []byte(fmt.Sprintf(`{"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: <ERROR> %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{
ObjectName: objectName,
ObjectV: ObjectV{
ObjectName: objectName,
},
})
}
@@ -702,9 +719,21 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
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.
@@ -741,6 +770,20 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
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
@@ -800,6 +843,17 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
expectedContent: encodedAnonResponse,
expectedRespStatus: http.StatusOK,
},
// Test case - 6.
// Anonymous user has access to some public folder, issue removing with
// another private object as well
{
bucket: bucketName,
objects: anonRequestWithPartialPublicAccess,
accessKey: "",
secretKey: "",
expectedContent: encodedAnonResponseWithPartialPublicAccess,
expectedRespStatus: http.StatusOK,
},
}
for i, testCase := range testCases {
+4 -4
View File
@@ -150,8 +150,8 @@ func TestBucketLifecycle(t *testing.T) {
// 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) {
creds auth.Credentials, t *testing.T,
) {
// test cases with sample input and expected output.
testCases := []struct {
method string
@@ -266,8 +266,8 @@ func testBucketLifecycle(obj ObjectLayer, instanceType, bucketName string, apiRo
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()
+46 -20
View File
@@ -81,40 +81,58 @@ type expiryTask struct {
}
type expiryState struct {
once sync.Once
expiryCh chan expiryTask
once sync.Once
byDaysCh chan expiryTask
byNewerNoncurrentCh chan newerNoncurrentTask
}
// PendingTasks returns the number of pending ILM expiry tasks.
func (es *expiryState) PendingTasks() int {
return len(es.expiryCh)
return len(es.byDaysCh) + len(es.byNewerNoncurrentCh)
}
func (es *expiryState) queueExpiryTask(oi ObjectInfo, restoredObject bool, rmVersion bool) {
// close closes work channels exactly once.
func (es *expiryState) close() {
es.once.Do(func() {
close(es.byDaysCh)
close(es.byNewerNoncurrentCh)
})
}
// enqueueByDays enqueues object versions expired by days for expiry.
func (es *expiryState) enqueueByDays(oi ObjectInfo, restoredObject bool, rmVersion bool) {
select {
case <-GlobalContext.Done():
es.once.Do(func() {
close(es.expiryCh)
})
case es.expiryCh <- expiryTask{objInfo: oi, versionExpiry: rmVersion, restoredObject: restoredObject}:
es.close()
case es.byDaysCh <- expiryTask{objInfo: oi, versionExpiry: rmVersion, restoredObject: restoredObject}:
default:
}
}
var (
globalExpiryState *expiryState
)
// enqueueByNewerNoncurrent enqueues object versions expired by
// NewerNoncurrentVersions limit for expiry.
func (es *expiryState) enqueueByNewerNoncurrent(bucket string, versions []ObjectToDelete) {
select {
case <-GlobalContext.Done():
es.close()
case es.byNewerNoncurrentCh <- newerNoncurrentTask{bucket: bucket, versions: versions}:
default:
}
}
var globalExpiryState *expiryState
func newExpiryState() *expiryState {
return &expiryState{
expiryCh: make(chan expiryTask, 10000),
byDaysCh: make(chan expiryTask, 10000),
byNewerNoncurrentCh: make(chan newerNoncurrentTask, 10000),
}
}
func initBackgroundExpiry(ctx context.Context, objectAPI ObjectLayer) {
globalExpiryState = newExpiryState()
go func() {
for t := range globalExpiryState.expiryCh {
for t := range globalExpiryState.byDaysCh {
if t.objInfo.TransitionedObject.Status != "" {
applyExpiryOnTransitionedObject(ctx, objectAPI, t.objInfo, t.restoredObject)
} else {
@@ -122,6 +140,18 @@ func initBackgroundExpiry(ctx context.Context, objectAPI ObjectLayer) {
}
}
}()
go func() {
for t := range globalExpiryState.byNewerNoncurrentCh {
deleteObjectVersions(ctx, objectAPI, t.bucket, t.versions)
}
}()
}
// newerNoncurrentTask encapsulates arguments required by worker to expire objects
// by NewerNoncurrentVersions
type newerNoncurrentTask struct {
bucket string
versions []ObjectToDelete
}
type transitionState struct {
@@ -148,9 +178,7 @@ func (t *transitionState) queueTransitionTask(oi ObjectInfo) {
}
}
var (
globalTransitionState *transitionState
)
var globalTransitionState *transitionState
func newTransitionState(ctx context.Context, objAPI ObjectLayer) *transitionState {
return &transitionState{
@@ -436,9 +464,7 @@ func (sp *SelectParameters) IsEmpty() bool {
return sp == nil
}
var (
selectParamsXMLName = "SelectParameters"
)
var selectParamsXMLName = "SelectParameters"
// UnmarshalXML - decodes XML data.
func (sp *SelectParameters) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
@@ -550,7 +576,7 @@ func putRestoreOpts(bucket, object string, rreq *RestoreObjectRequest, objInfo O
if rreq.Type == SelectRestoreRequest {
for _, v := range rreq.OutputLocation.S3.UserMetadata {
if !strings.HasPrefix("x-amz-meta", strings.ToLower(v.Name)) {
if !strings.HasPrefix(strings.ToLower(v.Name), "x-amz-meta") {
meta["x-amz-meta-"+v.Name] = v.Value
continue
}
+3 -5
View File
@@ -32,8 +32,6 @@ import (
func concurrentDecryptETag(ctx context.Context, objects []ObjectInfo) {
g := errgroup.WithNErrs(len(objects)).WithConcurrency(500)
_, cancel := g.WithCancelOnError(ctx)
defer cancel()
for index := range objects {
index := index
g.Go(func() error {
@@ -45,7 +43,7 @@ func concurrentDecryptETag(ctx context.Context, objects []ObjectInfo) {
return nil
}, index)
}
g.WaitErr()
g.Wait()
}
// Validate all the ListObjects query arguments, returns an APIErrorCode
@@ -61,8 +59,8 @@ func validateListObjectsArgs(marker, delimiter, encodingType string, maxKeys int
}
if encodingType != "" {
// Only url encoding type is supported
if strings.ToLower(encodingType) != "url" {
// AWS S3 spec only supports 'url' encoding type
if !strings.EqualFold(encodingType, "url") {
return ErrInvalidEncodingMethod
}
}
+5 -5
View File
@@ -120,7 +120,7 @@ func (b *BucketMetadata) Load(ctx context.Context, api ObjectLayer, name string)
logger.LogIf(ctx, errors.New("bucket name cannot be empty"))
return errors.New("bucket name cannot be empty")
}
configFile := path.Join(bucketConfigPrefix, name, bucketMetadataFile)
configFile := path.Join(bucketMetaPrefix, name, bucketMetadataFile)
data, err := readConfig(ctx, api, configFile)
if err != nil {
return err
@@ -277,7 +277,7 @@ func (b *BucketMetadata) convertLegacyConfigs(ctx context.Context, objectAPI Obj
}
for _, legacyFile := range legacyConfigs {
configFile := path.Join(bucketConfigPrefix, b.Name, legacyFile)
configFile := path.Join(bucketMetaPrefix, b.Name, legacyFile)
configData, err := readConfig(ctx, objectAPI, configFile)
if err != nil {
@@ -338,7 +338,7 @@ func (b *BucketMetadata) convertLegacyConfigs(ctx context.Context, objectAPI Obj
}
for legacyFile := range configs {
configFile := path.Join(bucketConfigPrefix, b.Name, legacyFile)
configFile := path.Join(bucketMetaPrefix, b.Name, legacyFile)
if err := deleteConfig(ctx, objectAPI, configFile); err != nil && !errors.Is(err, errConfigNotFound) {
logger.LogIf(ctx, err)
}
@@ -365,7 +365,7 @@ func (b *BucketMetadata) Save(ctx context.Context, api ObjectLayer) error {
return err
}
configFile := path.Join(bucketConfigPrefix, b.Name, bucketMetadataFile)
configFile := path.Join(bucketMetaPrefix, b.Name, bucketMetadataFile)
return saveConfig(ctx, api, configFile, data)
}
@@ -377,7 +377,7 @@ func deleteBucketMetadata(ctx context.Context, obj objectDeleter, bucket string)
bucketMetadataFile,
}
for _, metaFile := range metadataFiles {
configFile := path.Join(bucketConfigPrefix, bucket, metaFile)
configFile := path.Join(bucketMetaPrefix, bucket, metaFile)
if err := deleteConfig(ctx, obj, configFile); err != nil && err != errConfigNotFound {
return err
}
+3 -4
View File
@@ -30,7 +30,6 @@ import (
)
const (
bucketConfigPrefix = "buckets"
bucketNotificationConfig = "notification.xml"
)
@@ -72,8 +71,8 @@ func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter,
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
config.SetRegion(globalServerRegion)
if err = config.Validate(globalServerRegion, globalNotificationSys.targetList); err != nil {
config.SetRegion(globalSite.Region)
if err = config.Validate(globalSite.Region, globalNotificationSys.targetList); err != nil {
arnErr, ok := err.(*event.ErrARNNotFound)
if ok {
for i, queue := range config.QueueList {
@@ -145,7 +144,7 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
return
}
config, err := event.ParseConfig(io.LimitReader(r.Body, r.ContentLength), globalServerRegion, globalNotificationSys.targetList)
config, err := event.ParseConfig(io.LimitReader(r.Body, r.ContentLength), globalSite.Region, globalNotificationSys.targetList)
if err != nil {
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
if event.IsEventError(err) {
+35 -23
View File
@@ -58,6 +58,10 @@ func (sys *BucketObjectLockSys) Get(bucketName string) (r objectlock.Retention,
// 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
@@ -175,68 +179,76 @@ func enforceRetentionBypassForDelete(ctx context.Context, r *http.Request, bucke
// 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, bucket, object string, getObjectInfoFn GetObjectInfoFn, objRetention *objectlock.ObjectRetention, cred auth.Credentials, owner bool) (ObjectInfo, APIErrorCode) {
func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, oi ObjectInfo, objRetention *objectlock.ObjectRetention, cred auth.Credentials, owner bool) error {
byPassSet := objectlock.IsObjectLockGovernanceBypassSet(r.Header)
opts, err := getOpts(ctx, r, bucket, object)
if err != nil {
return ObjectInfo{}, toAPIErrorCode(ctx, err)
}
oi, err := getObjectInfoFn(ctx, bucket, object, opts)
if err != nil {
return oi, toAPIErrorCode(ctx, err)
}
t, err := objectlock.UTCNowNTP()
if err != nil {
logger.LogIf(ctx, err)
return oi, ErrObjectLocked
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.
// 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) {
perm := isPutRetentionAllowed(bucket, object,
apiErr := isPutRetentionAllowed(oi.Bucket, oi.Name,
days, objRetention.RetainUntilDate.Time,
objRetention.Mode, byPassSet, r, cred,
owner)
return oi, perm
switch apiErr {
case ErrAccessDenied:
return errAuthentication
}
return nil
}
switch ret.Mode {
case objectlock.RetGovernance:
govPerm := isPutRetentionAllowed(bucket, object, days,
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 oi, ErrObjectLocked
return ObjectLocked{Bucket: oi.Bucket, Object: oi.Name, VersionID: oi.VersionID}
}
}
return oi, govPerm
switch govPerm {
case 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 oi, ErrObjectLocked
return ObjectLocked{Bucket: oi.Bucket, Object: oi.Name, VersionID: oi.VersionID}
}
compliancePerm := isPutRetentionAllowed(bucket, object,
apiErr := isPutRetentionAllowed(oi.Bucket, oi.Name,
days, objRetention.RetainUntilDate.Time, objRetention.Mode,
false, r, cred, owner)
return oi, compliancePerm
switch apiErr {
case ErrAccessDenied:
return errAuthentication
}
return nil
}
return oi, ErrNone
return nil
} // No pre-existing retention metadata present.
perm := isPutRetentionAllowed(bucket, object,
apiErr := isPutRetentionAllowed(oi.Bucket, oi.Name,
days, objRetention.RetainUntilDate.Time,
objRetention.Mode, byPassSet, r, cred, owner)
return oi, perm
switch apiErr {
case ErrAccessDenied:
return errAuthentication
}
return nil
}
// checkPutObjectLockAllowed enforces object retention policy and legal hold policy
+5 -13
View File
@@ -105,7 +105,7 @@ func testCreateBucket(obj ObjectLayer, instanceType, bucketName string, apiRoute
bucketName1 := fmt.Sprintf("%s-1", bucketName)
const n = 100
var start = make(chan struct{})
start := make(chan struct{})
var ok, errs int
var wg sync.WaitGroup
var mu sync.Mutex
@@ -147,8 +147,8 @@ func TestPutBucketPolicyHandler(t *testing.T) {
// testPutBucketPolicyHandler - Test for Bucket policy end point.
func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
credentials auth.Credentials, t *testing.T) {
credentials auth.Credentials, t *testing.T,
) {
bucketName1 := fmt.Sprintf("%s-1", bucketName)
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName1, BucketOptions{}); err != nil {
t.Fatal(err)
@@ -333,7 +333,6 @@ func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
// 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\": <ERROR> %v",
instanceType, bucketName, err)
@@ -352,14 +351,12 @@ func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
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.
@@ -465,7 +462,6 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
// 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: <ERROR> %v", i+1, err)
}
@@ -540,7 +536,6 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
// 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\": <ERROR> %v",
instanceType, bucketName, err)
@@ -559,7 +554,6 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
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)
}
@@ -575,8 +569,8 @@ func TestDeleteBucketPolicyHandler(t *testing.T) {
// testDeleteBucketPolicyHandler - Test for Delete bucket policy end point.
func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
credentials auth.Credentials, t *testing.T) {
credentials auth.Credentials, t *testing.T,
) {
// template for constructing HTTP request body for PUT bucket policy.
bucketPolicyTemplate := `{
"Version": "2012-10-17",
@@ -743,7 +737,6 @@ func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName str
// 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\": <ERROR> %v",
instanceType, bucketName, err)
@@ -762,7 +755,6 @@ func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName str
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)
}
+6 -5
View File
@@ -172,11 +172,12 @@ func getConditionValues(r *http.Request, lc string, username string, claims map[
vStr, ok := v.(string)
if ok {
// Special case for AD/LDAP STS users
if k == ldapUser {
switch k {
case ldapUser:
args["user"] = []string{vStr}
} else if k == ldapUserN {
case ldapUserN:
args["username"] = []string{vStr}
} else {
default:
args[k] = []string{vStr}
}
}
@@ -199,7 +200,7 @@ func PolicyToBucketAccessPolicy(bucketPolicy *policy.Policy) (*miniogopolicy.Buc
}
var policyInfo miniogopolicy.BucketAccessPolicy
var json = jsoniter.ConfigCompatibleWithStandardLibrary
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
@@ -217,7 +218,7 @@ func BucketAccessPolicyToPolicy(policyInfo *miniogopolicy.BucketAccessPolicy) (*
}
var bucketPolicy policy.Policy
var json = jsoniter.ConfigCompatibleWithStandardLibrary
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
+9 -11
View File
@@ -154,7 +154,7 @@ func enforceFIFOQuotaBucket(ctx context.Context, objectAPI ObjectLayer, bucket s
// ModTime to find the oldest objects in bucket to delete. In
// the context of bucket quota enforcement - number of hits are
// irrelevant.
scorer, err := newFileScorer(toFree, time.Now().Unix(), 1)
scorer, err := newFileScorer(toFree, UTCNow().Unix(), 1)
if err != nil {
logger.LogIf(ctx, err)
return
@@ -162,11 +162,6 @@ func enforceFIFOQuotaBucket(ctx context.Context, objectAPI ObjectLayer, bucket s
rcfg, _ := globalBucketObjectLockSys.Get(bucket)
for obj := range objInfoCh {
if obj.DeleteMarker {
// Delete markers are automatically added for FIFO purge.
scorer.addFileWithObjInfo(obj, 1)
continue
}
// skip objects currently under retention
if rcfg.LockEnabled && enforceRetentionForDeletion(ctx, obj) {
continue
@@ -178,6 +173,7 @@ func enforceFIFOQuotaBucket(ctx context.Context, objectAPI ObjectLayer, bucket s
if scorer.seenBytes <= cfg.Quota {
return
}
// Calculate how much we want to delete now.
toFreeNow := scorer.seenBytes - cfg.Quota
// We were less over quota than we thought. Adjust so we delete less.
@@ -193,8 +189,10 @@ func enforceFIFOQuotaBucket(ctx context.Context, objectAPI ObjectLayer, bucket s
numKeys := len(scorer.fileObjInfos())
for i, obj := range scorer.fileObjInfos() {
objects = append(objects, ObjectToDelete{
ObjectName: obj.Name,
VersionID: obj.VersionID,
ObjectV: ObjectV{
ObjectName: obj.Name,
VersionID: obj.VersionID,
},
})
if len(objects) < maxDeleteList && (i < numKeys-1) {
// skip deletion until maxDeleteList or end of slice
@@ -209,9 +207,9 @@ func enforceFIFOQuotaBucket(ctx context.Context, objectAPI ObjectLayer, bucket s
_, deleteErrs := objectAPI.DeleteObjects(ctx, bucket, objects, ObjectOptions{
Versioned: versioned,
})
for i := range deleteErrs {
if deleteErrs[i] != nil {
logger.LogIf(ctx, deleteErrs[i])
for j := range deleteErrs {
if deleteErrs[j] != nil {
logger.LogIf(ctx, deleteErrs[j])
continue
}
+54 -46
View File
@@ -20,7 +20,6 @@ package cmd
import (
"context"
"sync"
"sync/atomic"
"time"
"github.com/minio/minio/internal/bucket/replication"
@@ -52,8 +51,10 @@ func (r *ReplicationStats) Delete(bucket string) {
r.Lock()
defer r.Unlock()
delete(r.Cache, bucket)
delete(r.UsageCache, bucket)
r.ulock.Lock()
defer r.ulock.Unlock()
delete(r.UsageCache, bucket)
}
// UpdateReplicaStat updates in-memory replica statistics with new values.
@@ -68,54 +69,56 @@ func (r *ReplicationStats) UpdateReplicaStat(bucket string, n int64) {
if !ok {
bs = &BucketReplicationStats{Stats: make(map[string]*BucketReplicationStat)}
}
atomic.AddInt64(&bs.ReplicaSize, n)
bs.ReplicaSize += n
r.Cache[bucket] = bs
}
// Update updates in-memory replication statistics with new values.
func (r *ReplicationStats) Update(bucket string, arn string, n int64, status, prevStatus replication.StatusType, opType replication.Type) {
func (r *ReplicationStats) Update(bucket string, arn string, n int64, duration time.Duration, status, prevStatus replication.StatusType, opType replication.Type) {
if r == nil {
return
}
r.RLock()
r.Lock()
defer r.Unlock()
bs, ok := r.Cache[bucket]
if !ok {
bs = &BucketReplicationStats{Stats: make(map[string]*BucketReplicationStat)}
r.Cache[bucket] = bs
}
b, ok := bs.Stats[arn]
if !ok {
b = &BucketReplicationStat{}
bs.Stats[arn] = b
}
r.RUnlock()
switch status {
case replication.Completed:
switch prevStatus { // adjust counters based on previous state
case replication.Failed:
atomic.AddInt64(&b.FailedCount, -1)
b.FailedCount--
}
if opType == replication.ObjectReplicationType {
atomic.AddInt64(&b.ReplicatedSize, n)
b.ReplicatedSize += n
switch prevStatus {
case replication.Failed:
atomic.AddInt64(&b.FailedSize, -1*n)
b.FailedSize -= n
}
if duration > 0 {
b.Latency.update(n, duration)
}
}
case replication.Failed:
if opType == replication.ObjectReplicationType {
if prevStatus == replication.Pending {
atomic.AddInt64(&b.FailedSize, n)
atomic.AddInt64(&b.FailedCount, 1)
b.FailedSize += n
b.FailedCount++
}
}
case replication.Replica:
if opType == replication.ObjectReplicationType {
atomic.AddInt64(&b.ReplicaSize, n)
b.ReplicaSize += n
}
}
r.Lock()
bs.Stats[arn] = b
r.Cache[bucket] = bs
r.Unlock()
}
// GetInitialUsage get replication metrics available at the time of cluster initialization
@@ -126,10 +129,10 @@ func (r *ReplicationStats) GetInitialUsage(bucket string) BucketReplicationStats
r.ulock.RLock()
defer r.ulock.RUnlock()
st, ok := r.UsageCache[bucket]
if ok {
return st.Clone()
if !ok {
return BucketReplicationStats{}
}
return BucketReplicationStats{Stats: make(map[string]*BucketReplicationStat)}
return st.Clone()
}
// Get replication metrics for a bucket from this node since this node came up.
@@ -143,7 +146,7 @@ func (r *ReplicationStats) Get(bucket string) BucketReplicationStats {
st, ok := r.Cache[bucket]
if !ok {
return BucketReplicationStats{Stats: make(map[string]*BucketReplicationStat)}
return BucketReplicationStats{}
}
return st.Clone()
}
@@ -160,41 +163,46 @@ func NewReplicationStats(ctx context.Context, objectAPI ObjectLayer) *Replicatio
func (r *ReplicationStats) loadInitialReplicationMetrics(ctx context.Context) {
rTimer := time.NewTimer(time.Minute * 1)
defer rTimer.Stop()
var (
dui DataUsageInfo
err error
)
outer:
for {
select {
case <-ctx.Done():
return
case <-rTimer.C:
dui, err := loadDataUsageFromBackend(GlobalContext, newObjectLayerFn())
dui, err = loadDataUsageFromBackend(GlobalContext, newObjectLayerFn())
if err != nil {
continue
}
// data usage has not captured any data yet.
if dui.LastUpdate.IsZero() {
continue
// If LastUpdate is set, data usage is available.
if !dui.LastUpdate.IsZero() {
break outer
}
m := make(map[string]*BucketReplicationStats)
for bucket, usage := range dui.BucketsUsage {
b := &BucketReplicationStats{
Stats: make(map[string]*BucketReplicationStat, len(usage.ReplicationInfo)),
}
for arn, uinfo := range usage.ReplicationInfo {
b.Stats[arn] = &BucketReplicationStat{
FailedSize: int64(uinfo.ReplicationFailedSize),
ReplicatedSize: int64(uinfo.ReplicatedSize),
ReplicaSize: int64(uinfo.ReplicaSize),
FailedCount: int64(uinfo.ReplicationFailedCount),
}
}
b.ReplicaSize += int64(usage.ReplicaSize)
if b.hasReplicationUsage() {
m[bucket] = b
}
}
r.ulock.Lock()
defer r.ulock.Unlock()
r.UsageCache = m
return
}
}
m := make(map[string]*BucketReplicationStats)
for bucket, usage := range dui.BucketsUsage {
b := &BucketReplicationStats{
Stats: make(map[string]*BucketReplicationStat, len(usage.ReplicationInfo)),
}
for arn, uinfo := range usage.ReplicationInfo {
b.Stats[arn] = &BucketReplicationStat{
FailedSize: int64(uinfo.ReplicationFailedSize),
ReplicatedSize: int64(uinfo.ReplicatedSize),
ReplicaSize: int64(uinfo.ReplicaSize),
FailedCount: int64(uinfo.ReplicationFailedCount),
}
}
b.ReplicaSize += int64(usage.ReplicaSize)
if b.hasReplicationUsage() {
m[bucket] = b
}
}
r.ulock.Lock()
defer r.ulock.Unlock()
r.UsageCache = m
}
+6 -3
View File
@@ -34,6 +34,7 @@ import (
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
@@ -172,7 +173,7 @@ func (o *ObjectInfo) TargetReplicationStatus(arn string) (status replication.Sta
type replicateTargetDecision struct {
Replicate bool // Replicate to this target
Synchronous bool // Synchronous replication configured.
Arn string //ARN of replication target
Arn string // ARN of replication target
ID string
}
@@ -501,8 +502,10 @@ func getHealReplicateObjectInfo(objInfo ObjectInfo, rcfg replicationConfig) Repl
var tgtStatuses map[string]replication.StatusType
if oi.DeleteMarker || !oi.VersionPurgeStatus.Empty() {
dsc = checkReplicateDelete(GlobalContext, oi.Bucket, ObjectToDelete{
ObjectName: oi.Name,
VersionID: oi.VersionID,
ObjectV: ObjectV{
ObjectName: oi.Name,
VersionID: oi.VersionID,
},
}, oi, ObjectOptions{}, nil)
} else {
dsc = mustReplicate(GlobalContext, oi.Bucket, oi.Name, getMustReplicateOptions(ObjectInfo{
+17 -17
View File
@@ -32,7 +32,7 @@ var replicatedInfosTests = []struct {
expectedOpType replication.Type
expectedAction replicationAction
}{
{ //1. empty tgtInfos slice
{ // 1. empty tgtInfos slice
name: "no replicated targets",
tgtInfos: []replicatedTargetInfo{},
expectedCompletedSize: 0,
@@ -41,7 +41,7 @@ var replicatedInfosTests = []struct {
expectedOpType: replication.UnsetReplicationType,
expectedAction: replicateNone,
},
{ //2. replication completed to single target
{ // 2. replication completed to single target
name: "replication completed to single target",
tgtInfos: []replicatedTargetInfo{
{
@@ -59,7 +59,7 @@ var replicatedInfosTests = []struct {
expectedOpType: replication.ObjectReplicationType,
expectedAction: replicateAll,
},
{ //3. replication completed to single target; failed to another
{ // 3. replication completed to single target; failed to another
name: "replication completed to single target",
tgtInfos: []replicatedTargetInfo{
{
@@ -77,14 +77,15 @@ var replicatedInfosTests = []struct {
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
{ // 4. replication pending on one target; failed to another
name: "replication completed to single target",
tgtInfos: []replicatedTargetInfo{
{
@@ -102,7 +103,8 @@ var replicatedInfosTests = []struct {
ReplicationStatus: replication.Failed,
OpType: replication.ObjectReplicationType,
ReplicationAction: replicateAll,
}},
},
},
expectedCompletedSize: 0,
expectedReplicationStatusInternal: "arn1=PENDING;arn2=FAILED;",
expectedReplicationStatus: replication.Failed,
@@ -137,7 +139,7 @@ var parseReplicationDecisionTest = []struct {
expDsc ReplicateDecision
expErr error
}{
{ //1.
{ // 1.
name: "empty string",
dsc: "",
expDsc: ReplicateDecision{
@@ -146,7 +148,7 @@ var parseReplicationDecisionTest = []struct {
expErr: nil,
},
{ //2.
{ // 2.
name: "replicate decision for one target",
dsc: "arn:minio:replication::id:bucket=true;false;arn:minio:replication::id:bucket;id",
expErr: nil,
@@ -156,7 +158,7 @@ var parseReplicationDecisionTest = []struct {
},
},
},
{ //3.
{ // 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,
@@ -167,7 +169,7 @@ var parseReplicationDecisionTest = []struct {
},
},
},
{ //4.
{ // 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,
@@ -181,9 +183,7 @@ var parseReplicationDecisionTest = []struct {
func TestParseReplicateDecision(t *testing.T) {
for i, test := range parseReplicationDecisionTest {
//dsc, err := parseReplicateDecision(test.dsc)
dsc, err := parseReplicateDecision(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)
@@ -208,22 +208,22 @@ var replicationStateTest = []struct {
arn string
expStatus replication.StatusType
}{
{ //1. no replication status header
{ // 1. no replication status header
name: "no replicated targets",
rs: ReplicationState{},
expStatus: replication.StatusType(""),
},
{ //2. replication status for one target
{ // 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
{ // 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
{ // 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;",
@@ -231,7 +231,7 @@ var replicationStateTest = []struct {
},
expStatus: replication.Failed,
},
{ //5. replication status for replica version
{ // 5. replication status for replica version
name: "replication status for replica version",
rs: ReplicationState{ReplicationStatusInternal: string(replication.Replica)},
expStatus: replication.Replica,
+129 -37
View File
@@ -21,6 +21,7 @@ import (
"context"
"fmt"
"io"
"math"
"net/http"
"reflect"
"strings"
@@ -61,6 +62,8 @@ const (
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"
// ReplicationWorkerMultiplier is suggested worker multiplier if traffic exceeds replication worker capacity
ReplicationWorkerMultiplier = 1.5
)
// gets replication config associated to a given bucket name.
@@ -136,6 +139,7 @@ func (o mustReplicateOptions) ReplicationStatus() (s replication.StatusType) {
}
return s
}
func (o mustReplicateOptions) isExistingObjectReplication() bool {
return o.opType == replication.ExistingObjectReplicationType
}
@@ -143,6 +147,7 @@ func (o mustReplicateOptions) isExistingObjectReplication() bool {
func (o mustReplicateOptions) isMetadataReplication() bool {
return o.opType == replication.MetadataReplicationType
}
func getMustReplicateOptions(o ObjectInfo, op replication.Type, opts ObjectOptions) mustReplicateOptions {
if !op.Valid() {
op = replication.ObjectReplicationType
@@ -433,12 +438,12 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj
// to decrement pending count later.
for _, rinfo := range rinfos.Targets {
if rinfo.ReplicationStatus != rinfo.PrevReplicationStatus {
globalReplicationStats.Update(dobj.Bucket, rinfo.Arn, 0, replicationStatus,
globalReplicationStats.Update(dobj.Bucket, rinfo.Arn, 0, 0, replicationStatus,
prevStatus, replication.DeleteReplicationType)
}
}
var eventName = event.ObjectReplicationComplete
eventName := event.ObjectReplicationComplete
if replicationStatus == replication.Failed {
eventName = event.ObjectReplicationFailed
}
@@ -514,19 +519,18 @@ func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationI
}
return
}
// early return if already replicated delete marker for existing object replication
if dobj.DeleteMarkerVersionID != "" && dobj.OpType == replication.ExistingObjectReplicationType {
// early return if already replicated delete marker for existing object replication/ healing delete markers
if dobj.DeleteMarkerVersionID != "" && (dobj.OpType == replication.ExistingObjectReplicationType || dobj.OpType == replication.HealReplicationType) {
if _, err := tgt.StatObject(ctx, tgt.Bucket, dobj.ObjectName, miniogo.StatObjectOptions{
VersionID: versionID,
Internal: miniogo.AdvancedGetOptions{
ReplicationProxyRequest: "false",
}}); isErrMethodNotAllowed(ErrorRespToObjectError(err, dobj.Bucket, dobj.ObjectName)) {
},
}); isErrMethodNotAllowed(ErrorRespToObjectError(err, dobj.Bucket, dobj.ObjectName)) {
if dobj.VersionID == "" {
rinfo.ReplicationStatus = replication.Completed
} else {
rinfo.VersionPurgeStatus = Complete
return
}
return
}
}
@@ -724,7 +728,7 @@ const (
// 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(strings.ToLower(k1), strings.ToLower(k2)) {
if strings.EqualFold(k1, k2) {
return true
}
}
@@ -901,7 +905,7 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
// FIXME: add support for missing replication events
// - event.ObjectReplicationMissedThreshold
// - event.ObjectReplicationReplicatedAfterThreshold
var eventName = event.ObjectReplicationComplete
eventName := event.ObjectReplicationComplete
if rinfos.ReplicationStatus() == replication.Failed {
eventName = event.ObjectReplicationFailed
}
@@ -910,24 +914,24 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
// metadata should be updated with last resync timestamp.
if objInfo.ReplicationStatusInternal != newReplStatusInternal || rinfos.ReplicationResynced() {
popts := ObjectOptions{
MTime: objInfo.ModTime,
VersionID: objInfo.VersionID,
UserDefined: make(map[string]string, len(objInfo.UserDefined)),
}
for k, v := range objInfo.UserDefined {
popts.UserDefined[k] = v
}
popts.UserDefined[ReservedMetadataPrefixLower+ReplicationStatus] = newReplStatusInternal
popts.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano)
popts.UserDefined[xhttp.AmzBucketReplicationStatus] = string(rinfos.ReplicationStatus())
for _, rinfo := range rinfos.Targets {
if rinfo.ResyncTimestamp != "" {
popts.UserDefined[targetResetHeader(rinfo.Arn)] = rinfo.ResyncTimestamp
}
}
if objInfo.UserTags != "" {
popts.UserDefined[xhttp.AmzObjectTagging] = objInfo.UserTags
MTime: objInfo.ModTime,
VersionID: objInfo.VersionID,
EvalMetadataFn: func(oi ObjectInfo) 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 objInfo.UserTags != "" {
oi.UserDefined[xhttp.AmzObjectTagging] = objInfo.UserTags
}
return nil
},
}
if _, err = objectAPI.PutObjectMetadata(ctx, bucket, object, popts); err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to update replication metadata for %s/%s(%s): %w",
bucket, objInfo.Name, objInfo.VersionID, err))
@@ -938,7 +942,7 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
}
for _, rinfo := range rinfos.Targets {
if rinfo.ReplicationStatus != rinfo.PrevReplicationStatus {
globalReplicationStats.Update(bucket, rinfo.Arn, rinfo.Size, rinfo.ReplicationStatus, rinfo.PrevReplicationStatus, opType)
globalReplicationStats.Update(bucket, rinfo.Arn, rinfo.Size, rinfo.Duration, rinfo.ReplicationStatus, rinfo.PrevReplicationStatus, opType)
}
}
}
@@ -963,6 +967,7 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
// replicateObjectToTarget replicates the specified version of the object to destination bucket
// The source object is then updated to reflect the replication status.
func replicateObjectToTarget(ctx context.Context, ri ReplicateObjectInfo, objectAPI ObjectLayer, tgt *TargetClient) (rinfo replicatedTargetInfo) {
startTime := time.Now()
objInfo := ri.ObjectInfo.Clone()
bucket := objInfo.Bucket
object := objInfo.Name
@@ -1056,7 +1061,8 @@ func replicateObjectToTarget(ctx context.Context, ri ReplicateObjectInfo, object
VersionID: objInfo.VersionID,
Internal: miniogo.AdvancedGetOptions{
ReplicationProxyRequest: "false",
}})
},
})
if cerr == nil {
rAction = getReplicationAction(objInfo, oi, ri.OpType)
rinfo.ReplicationStatus = replication.Completed
@@ -1100,6 +1106,7 @@ func replicateObjectToTarget(ctx context.Context, ri ReplicateObjectInfo, object
rinfo.ResyncTimestamp = fmt.Sprintf("%s;%s", UTCNow().Format(http.TimeFormat), tgt.ResetID)
rinfo.ReplicationResynced = true
}
rinfo.Duration = time.Since(startTime)
}()
// use core client to avoid doing multipart on PUT
c := &miniogo.Core{Client: tgt.Client}
@@ -1114,7 +1121,8 @@ func replicateObjectToTarget(ctx context.Context, ri ReplicateObjectInfo, object
Internal: miniogo.AdvancedPutOptions{
SourceVersionID: objInfo.VersionID,
ReplicationRequest: true, // always set this to distinguish between `mc mirror` replication and serverside
}}
},
}
if _, err = c.CopyObject(ctx, tgt.Bucket, object, tgt.Bucket, object, getCopyObjMetadata(objInfo, tgt.StorageClass), srcOpts, dstOpts); err != nil {
rinfo.ReplicationStatus = replication.Failed
logger.LogIf(ctx, fmt.Errorf("Unable to replicate metadata for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err))
@@ -1210,7 +1218,8 @@ func replicateObjectWithMultipart(ctx context.Context, c *miniogo.Core, bucket,
SourceMTime: objInfo.ModTime,
// always set this to distinguish between `mc mirror` replication and serverside
ReplicationRequest: true,
}})
},
})
return err
}
@@ -1354,7 +1363,6 @@ func (p *ReplicationPool) AddWorker() {
return
}
}
}
// AddExistingObjectReplicateWorker adds a worker to queue existing objects that need to be sync'd
@@ -1409,6 +1417,14 @@ func (p *ReplicationPool) ResizeFailedWorkers(n int) {
}
}
// suggestedWorkers recommends an increase in number of workers to meet replication load.
func (p *ReplicationPool) suggestedWorkers(failQueue bool) int {
if failQueue {
return int(float64(p.mrfWorkerSize) * ReplicationWorkerMultiplier)
}
return int(float64(p.workerSize) * ReplicationWorkerMultiplier)
}
func (p *ReplicationPool) queueReplicaFailedTask(ri ReplicateObjectInfo) {
if p == nil {
return
@@ -1422,6 +1438,7 @@ func (p *ReplicationPool) queueReplicaFailedTask(ri ReplicateObjectInfo) {
})
case p.mrfReplicaCh <- ri:
default:
logger.LogOnceIf(GlobalContext, fmt.Errorf("WARNING: Replication failed workers could not keep up with healing failures - consider increasing number of replication failed workers with `mc admin config set api replication_failed_workers=%d`", p.suggestedWorkers(true)), replicationSubsystem)
}
}
@@ -1447,6 +1464,7 @@ func (p *ReplicationPool) queueReplicaTask(ri ReplicateObjectInfo) {
})
case ch <- ri:
default:
logger.LogOnceIf(GlobalContext, fmt.Errorf("WARNING: Replication workers could not keep up with incoming traffic - consider increasing number of replication workers with `mc admin config set api replication_workers=%d`", p.suggestedWorkers(false)), replicationSubsystem)
}
}
@@ -1483,6 +1501,7 @@ func (p *ReplicationPool) queueReplicaDeleteTask(doi DeletedObjectReplicationInf
})
case ch <- doi:
default:
logger.LogOnceIf(GlobalContext, fmt.Errorf("WARNING: Replication workers could not keep up with incoming traffic - consider increasing number of replication workers with `mc admin config set api replication_workers=%d`", p.suggestedWorkers(false)), replicationSubsystem)
}
}
@@ -1634,7 +1653,7 @@ func scheduleReplication(ctx context.Context, objInfo ObjectInfo, o ObjectLayer,
}
if sz, err := objInfo.GetActualSize(); err == nil {
for arn := range dsc.targetsMap {
globalReplicationStats.Update(objInfo.Bucket, arn, sz, objInfo.ReplicationStatus, replication.StatusType(""), opType)
globalReplicationStats.Update(objInfo.Bucket, arn, sz, 0, objInfo.ReplicationStatus, replication.StatusType(""), opType)
}
}
}
@@ -1642,10 +1661,10 @@ func scheduleReplication(ctx context.Context, objInfo ObjectInfo, o ObjectLayer,
func scheduleReplicationDelete(ctx context.Context, dv DeletedObjectReplicationInfo, o ObjectLayer) {
globalReplicationPool.queueReplicaDeleteTask(dv)
for arn := range dv.ReplicationState.Targets {
globalReplicationStats.Update(dv.Bucket, arn, 0, replication.Pending, replication.StatusType(""), replication.DeleteReplicationType)
globalReplicationStats.Update(dv.Bucket, arn, 0, 0, replication.Pending, replication.StatusType(""), replication.DeleteReplicationType)
}
for arn := range dv.ReplicationState.PurgeTargets {
globalReplicationStats.Update(dv.Bucket, arn, 0, replication.Pending, replication.StatusType(""), replication.DeleteReplicationType)
globalReplicationStats.Update(dv.Bucket, arn, 0, 0, replication.Pending, replication.StatusType(""), replication.DeleteReplicationType)
}
}
@@ -1657,6 +1676,7 @@ type replicationConfig struct {
func (c replicationConfig) Empty() bool {
return c.Config == nil
}
func (c replicationConfig) Replicate(opts replication.ObjectOpts) bool {
return c.Config.Replicate(opts)
}
@@ -1680,7 +1700,8 @@ func (c replicationConfig) Resync(ctx context.Context, oi ObjectInfo, dsc *Repli
DeleteMarker: oi.DeleteMarker,
VersionID: oi.VersionID,
OpType: replication.DeleteReplicationType,
ExistingObject: true}
ExistingObject: true,
}
tgtArns := c.Config.FilterTargetArns(opts)
// indicates no matching target with Existing object replication enabled.
@@ -1740,7 +1761,7 @@ func resyncTarget(oi ObjectInfo, arn string, resetID string, resetBeforeDate tim
}
rs, ok := oi.UserDefined[targetResetHeader(arn)]
if !ok {
rs, ok = oi.UserDefined[xhttp.MinIOReplicationResetStatus] //for backward compatibility
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
@@ -1768,3 +1789,74 @@ func resyncTarget(oi ObjectInfo, arn string, resetID string, resetBeforeDate tim
rd.Replicate = newReset && oi.ModTime.Before(resetBeforeDate)
return
}
// get the most current of in-memory replication stats and data usage info from crawler.
func getLatestReplicationStats(bucket string, u BucketUsageInfo) (s BucketReplicationStats) {
bucketStats := globalNotificationSys.GetClusterBucketStats(GlobalContext, bucket)
// accumulate cluster bucket stats
stats := make(map[string]*BucketReplicationStat)
var totReplicaSize int64
for _, bucketStat := range bucketStats {
totReplicaSize += bucketStat.ReplicationStats.ReplicaSize
for arn, stat := range bucketStat.ReplicationStats.Stats {
oldst := stats[arn]
if oldst == nil {
oldst = &BucketReplicationStat{}
}
stats[arn] = &BucketReplicationStat{
FailedCount: stat.FailedCount + oldst.FailedCount,
FailedSize: stat.FailedSize + oldst.FailedSize,
ReplicatedSize: stat.ReplicatedSize + oldst.ReplicatedSize,
Latency: stat.Latency.merge(oldst.Latency),
}
}
}
// add initial usage stat to cluster stats
usageStat := globalReplicationStats.GetInitialUsage(bucket)
totReplicaSize += usageStat.ReplicaSize
for arn, stat := range usageStat.Stats {
st, ok := stats[arn]
if !ok {
st = &BucketReplicationStat{}
stats[arn] = st
}
st.ReplicatedSize += stat.ReplicatedSize
st.FailedSize += stat.FailedSize
st.FailedCount += stat.FailedCount
}
s = BucketReplicationStats{
Stats: make(map[string]*BucketReplicationStat, len(stats)),
}
var latestTotReplicatedSize int64
for _, st := range u.ReplicationInfo {
latestTotReplicatedSize += int64(st.ReplicatedSize)
}
// normalize computed real time stats with latest usage stat
for arn, tgtstat := range stats {
st := BucketReplicationStat{}
bu, ok := u.ReplicationInfo[arn]
if !ok {
bu = BucketTargetUsageInfo{}
}
// use in memory replication stats if it is ahead of usage info.
st.ReplicatedSize = int64(bu.ReplicatedSize)
if tgtstat.ReplicatedSize >= int64(bu.ReplicatedSize) {
st.ReplicatedSize = tgtstat.ReplicatedSize
}
s.ReplicatedSize += st.ReplicatedSize
// Reset FailedSize and FailedCount to 0 for negative overflows which can
// happen since data usage picture can lag behind actual usage state at the time of cluster start
st.FailedSize = int64(math.Max(float64(tgtstat.FailedSize), 0))
st.FailedCount = int64(math.Max(float64(tgtstat.FailedCount), 0))
st.Latency = tgtstat.Latency
s.Stats[arn] = &st
s.FailedSize += st.FailedSize
s.FailedCount += st.FailedCount
}
// normalize overall stats
s.ReplicaSize = int64(math.Max(float64(totReplicaSize), float64(u.ReplicaSize)))
s.ReplicatedSize = int64(math.Max(float64(s.ReplicatedSize), float64(latestTotReplicatedSize)))
return s
}
+184 -165
View File
@@ -55,27 +55,28 @@ var replicationConfigTests = []struct {
tgtStatuses map[string]replication.StatusType
expectedSync bool
}{
{ //1. no replication config
{ // 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
{ // 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
{ // 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
{ // 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,
info: ObjectInfo{
Size: 100,
ReplicationStatus: replication.Completed,
VersionID: "a3348c34-c352-4498-82f0-1098e8b34df9",
},
@@ -93,174 +94,192 @@ func TestReplicationResync(t *testing.T) {
}
}
var start = UTCNow().AddDate(0, 0, -1)
var 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(),
}}},
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,
},
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(),
}}},
},
{ //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,
},
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),
{ // 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(),
}}},
},
},
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")},
{ // 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,
}}},
},
},
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 {
+41 -21
View File
@@ -18,11 +18,39 @@
package cmd
import (
"sync/atomic"
"time"
)
//go:generate msgp -file $GOFILE
// ReplicationLatency holds information of bucket operations latency, such us uploads
type ReplicationLatency struct {
// Single & Multipart PUTs latency
UploadHistogram LastMinuteLatencies
}
// Merge two replication latency into a new one
func (rl ReplicationLatency) merge(other ReplicationLatency) (newReplLatency ReplicationLatency) {
newReplLatency.UploadHistogram = rl.UploadHistogram.Merge(other.UploadHistogram)
return
}
// Get upload latency of each object size range
func (rl ReplicationLatency) getUploadLatency() (ret map[string]uint64) {
ret = make(map[string]uint64)
avg := rl.UploadHistogram.GetAvg()
for k, v := range avg {
// Convert nanoseconds to milliseconds
ret[sizeTagToString(k)] = v.avg() / uint64(time.Millisecond)
}
return
}
// Update replication upload latency with a new value
func (rl *ReplicationLatency) update(size int64, duration time.Duration) {
rl.UploadHistogram.Add(size, duration)
}
// BucketStats bucket statistics
type BucketStats struct {
ReplicationStats BucketReplicationStats
@@ -52,28 +80,18 @@ func (brs *BucketReplicationStats) Empty() bool {
}
// Clone creates a new BucketReplicationStats copy
func (brs BucketReplicationStats) Clone() BucketReplicationStats {
c := BucketReplicationStats{
Stats: make(map[string]*BucketReplicationStat, len(brs.Stats)),
}
//this is called only by replicationStats cache and already holds a read lock before calling Clone()
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 {
c.Stats[arn] = &BucketReplicationStat{
FailedSize: atomic.LoadInt64(&st.FailedSize),
ReplicatedSize: atomic.LoadInt64(&st.ReplicatedSize),
ReplicaSize: atomic.LoadInt64(&st.ReplicaSize),
FailedCount: atomic.LoadInt64(&st.FailedCount),
PendingSize: atomic.LoadInt64(&st.PendingSize),
PendingCount: atomic.LoadInt64(&st.PendingCount),
}
// make a copy of `*st`
s := *st
c.Stats[arn] = &s
}
// update total counts across targets
c.FailedSize = atomic.LoadInt64(&brs.FailedSize)
c.FailedCount = atomic.LoadInt64(&brs.FailedCount)
c.PendingCount = atomic.LoadInt64(&brs.PendingCount)
c.PendingSize = atomic.LoadInt64(&brs.PendingSize)
c.ReplicaSize = atomic.LoadInt64(&brs.ReplicaSize)
c.ReplicatedSize = atomic.LoadInt64(&brs.ReplicatedSize)
return c
}
@@ -93,6 +111,8 @@ type BucketReplicationStat struct {
PendingCount int64 `json:"pendingReplicationCount"`
// Total number of failed operations including metadata updates
FailedCount int64 `json:"failedReplicationCount"`
// Replication latency information
Latency ReplicationLatency `json:"replicationLatency"`
}
func (bs *BucketReplicationStat) hasReplicationUsage() bool {
+196 -5
View File
@@ -60,6 +60,35 @@ func (z *BucketReplicationStat) DecodeMsg(dc *msgp.Reader) (err error) {
err = msgp.WrapError(err, "FailedCount")
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
}
}
}
default:
err = dc.Skip()
if err != nil {
@@ -73,9 +102,9 @@ func (z *BucketReplicationStat) DecodeMsg(dc *msgp.Reader) (err error) {
// EncodeMsg implements msgp.Encodable
func (z *BucketReplicationStat) EncodeMsg(en *msgp.Writer) (err error) {
// map header, size 6
// map header, size 7
// write "PendingSize"
err = en.Append(0x86, 0xab, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65)
err = en.Append(0x87, 0xab, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65)
if err != nil {
return
}
@@ -134,15 +163,31 @@ func (z *BucketReplicationStat) EncodeMsg(en *msgp.Writer) (err error) {
err = msgp.WrapError(err, "FailedCount")
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
}
return
}
// MarshalMsg implements msgp.Marshaler
func (z *BucketReplicationStat) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize())
// map header, size 6
// map header, size 7
// string "PendingSize"
o = append(o, 0x86, 0xab, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65)
o = append(o, 0x87, 0xab, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65)
o = msgp.AppendInt64(o, z.PendingSize)
// string "ReplicatedSize"
o = append(o, 0xae, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65)
@@ -159,6 +204,16 @@ func (z *BucketReplicationStat) MarshalMsg(b []byte) (o []byte, err error) {
// string "FailedCount"
o = append(o, 0xab, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74)
o = msgp.AppendInt64(o, z.FailedCount)
// 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
}
return
}
@@ -216,6 +271,35 @@ func (z *BucketReplicationStat) UnmarshalMsg(bts []byte) (o []byte, err error) {
err = msgp.WrapError(err, "FailedCount")
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
}
}
}
default:
bts, err = msgp.Skip(bts)
if err != nil {
@@ -230,7 +314,7 @@ func (z *BucketReplicationStat) UnmarshalMsg(bts []byte) (o []byte, err error) {
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z *BucketReplicationStat) Msgsize() (s int) {
s = 1 + 12 + msgp.Int64Size + 15 + msgp.Int64Size + 12 + msgp.Int64Size + 11 + msgp.Int64Size + 13 + msgp.Int64Size + 12 + msgp.Int64Size
s = 1 + 12 + msgp.Int64Size + 15 + msgp.Int64Size + 12 + msgp.Int64Size + 11 + msgp.Int64Size + 13 + msgp.Int64Size + 12 + msgp.Int64Size + 8 + 1 + 16 + z.Latency.UploadHistogram.Msgsize()
return
}
@@ -707,3 +791,110 @@ func (z *BucketStats) Msgsize() (s int) {
s = 1 + 17 + z.ReplicationStats.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
}
+113
View File
@@ -347,3 +347,116 @@ func BenchmarkDecodeBucketStats(b *testing.B) {
}
}
}
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)
}
}
}
+6 -4
View File
@@ -332,8 +332,10 @@ func (sys *BucketTargetSys) set(bucket BucketInfo, meta BucketMetadata) {
}
// getRemoteTargetInstanceTransport contains a singleton roundtripper.
var getRemoteTargetInstanceTransport http.RoundTripper
var getRemoteTargetInstanceTransportOnce sync.Once
var (
getRemoteTargetInstanceTransport http.RoundTripper
getRemoteTargetInstanceTransportOnce sync.Once
)
// Returns a minio-go Client configured to access remote host described in replication target config.
func (sys *BucketTargetSys) getRemoteTargetClient(tcfg *madmin.BucketTarget) (*TargetClient, error) {
@@ -414,7 +416,7 @@ func parseBucketTargetConfig(bucket string, cdata, cmetadata []byte) (*madmin.Bu
return nil, nil
}
data = cdata
var json = jsoniter.ConfigCompatibleWithStandardLibrary
json := jsoniter.ConfigCompatibleWithStandardLibrary
if len(cmetadata) != 0 {
if err := json.Unmarshal(cmetadata, &meta); err != nil {
return nil, err
@@ -444,6 +446,6 @@ type TargetClient struct {
StorageClass string // storage class on remote
disableProxy bool
healthCancelFn context.CancelFunc // cancellation function for client healthcheck
ARN string //ARN to uniquely identify remote target
ARN string // ARN to uniquely identify remote target
ResetID string
}
+9 -1
View File
@@ -63,6 +63,15 @@ func (api objectAPIHandlers) PutBucketVersioningHandler(w http.ResponseWriter, r
return
}
if globalSiteReplicationSys.isEnabled() {
writeErrorResponse(ctx, w, APIError{
Code: "InvalidBucketState",
Description: "Cluster replication is enabled for this site, so the versioning state cannot be changed.",
HTTPStatusCode: http.StatusConflict,
}, r.URL)
return
}
if rcfg, _ := globalBucketObjectLockSys.Get(bucket); rcfg.LockEnabled && v.Suspended() {
writeErrorResponse(ctx, w, APIError{
Code: "InvalidBucketState",
@@ -135,5 +144,4 @@ func (api objectAPIHandlers) GetBucketVersioningHandler(w http.ResponseWriter, r
// Write bucket versioning configuration to client
writeSuccessResponseXML(w, configData)
}
+244 -15
View File
@@ -18,12 +18,15 @@
package cmd
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/gob"
"errors"
"fmt"
"io/ioutil"
"math/rand"
"net"
"net/http"
@@ -34,16 +37,19 @@ import (
"sort"
"strconv"
"strings"
"syscall"
"time"
fcolor "github.com/fatih/color"
"github.com/go-openapi/loads"
"github.com/inconshreveable/mousetrap"
dns2 "github.com/miekg/dns"
"github.com/minio/cli"
consoleCerts "github.com/minio/console/pkg/certs"
"github.com/minio/console/restapi"
"github.com/minio/console/restapi/operations"
"github.com/minio/kes"
"github.com/minio/madmin-go"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/minio/minio-go/v7/pkg/set"
@@ -63,9 +69,24 @@ import (
// serverDebugLog will enable debug printing
var serverDebugLog = env.Get("_MINIO_SERVER_DEBUG", config.EnableOff) == config.EnableOn
var defaultAWSCredProvider []credentials.Provider
var (
shardDiskTimeDelta time.Duration
defaultAWSCredProvider []credentials.Provider
)
func init() {
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)
}
}
rand.Seed(time.Now().UTC().UnixNano())
logger.Init(GOPATH, GOROOT)
@@ -112,6 +133,8 @@ func init() {
console.SetColor("Debug", fcolor.New())
gob.Register(StorageErr(""))
gob.Register(madmin.TimeInfo{})
gob.Register(map[string]interface{}{})
defaultAWSCredProvider = []credentials.Provider{
&credentials.IAM{
@@ -121,6 +144,12 @@ func init() {
},
}
var err error
shardDiskTimeDelta, err = time.ParseDuration(env.Get("_MINIO_SHARD_DISKTIME_DELTA", "1m"))
if err != nil {
shardDiskTimeDelta = 1 * time.Minute
}
// All minio-go API operations shall be performed only once,
// another way to look at this is we are turning off retries.
minio.MaxRetry = 1
@@ -162,7 +191,12 @@ func minioConfigToConsoleFeatures() {
os.Setenv("CONSOLE_IDP_HMAC_PASSPHRASE", globalOpenIDConfig.ClientID)
os.Setenv("CONSOLE_IDP_SCOPES", strings.Join(globalOpenIDConfig.DiscoveryDoc.ScopesSupported, ","))
if globalOpenIDConfig.ClaimUserinfo {
os.Setenv("CONSOLE_IDP_USERINFO", "on")
os.Setenv("CONSOLE_IDP_USERINFO", config.EnableOn)
}
if globalOpenIDConfig.RedirectURIDynamic {
// Enable dynamic redirect-uri's based on incoming 'host' header,
// Overrides any other callback URL.
os.Setenv("CONSOLE_IDP_CALLBACK_DYNAMIC", config.EnableOn)
}
if globalOpenIDConfig.RedirectURI != "" {
os.Setenv("CONSOLE_IDP_CALLBACK", globalOpenIDConfig.RedirectURI)
@@ -170,11 +204,14 @@ func minioConfigToConsoleFeatures() {
os.Setenv("CONSOLE_IDP_CALLBACK", getConsoleEndpoints()[0]+"/oauth_callback")
}
}
os.Setenv("CONSOLE_MINIO_REGION", globalServerRegion)
os.Setenv("CONSOLE_MINIO_REGION", globalSite.Region)
os.Setenv("CONSOLE_CERT_PASSWD", env.Get("MINIO_CERT_PASSWD", ""))
if globalSubnetConfig.License != "" {
os.Setenv("CONSOLE_SUBNET_LICENSE", globalSubnetConfig.License)
}
if globalSubnetConfig.APIKey != "" {
os.Setenv("CONSOLE_SUBNET_API_KEY", globalSubnetConfig.APIKey)
}
}
func initConsoleServer() (*restapi.Server, error) {
@@ -286,7 +323,7 @@ func checkUpdate(mode string) {
return
}
logStartupMessage(prepareUpdateMessage("Run `mc admin update`", lrTime.Sub(crTime)))
logStartupMessage(prepareUpdateMessage("\nRun `mc admin update`", lrTime.Sub(crTime)))
}
func newConfigDirFromCtx(ctx *cli.Context, option string, getDefaultDir func() string) (*ConfigDir, bool) {
@@ -331,7 +368,6 @@ func newConfigDirFromCtx(ctx *cli.Context, option string, getDefaultDir func() s
}
func handleCommonCmdArgs(ctx *cli.Context) {
// Get "json" flag from command line argument and
// enable json and quite modes if json flag is turned on.
globalCLIContext.JSON = ctx.IsSet("json") || ctx.GlobalIsSet("json")
@@ -412,7 +448,166 @@ func handleCommonCmdArgs(ctx *cli.Context) {
logger.FatalIf(mkdirAllIgnorePerm(globalCertsCADir.Get()), "Unable to create certs CA directory at %s", globalCertsCADir.Get())
}
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
}
const envSeparator = "="
envTokens := strings.SplitN(strings.TrimSpace(strings.TrimPrefix(envEntry, "export")), 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 := os.Open(envConfigFile)
if err != nil {
if os.IsNotExist(err) { // ignore if file doesn't exist.
return nil, 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 := ioutil.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.EnvKMSSecretKeyFile) {
kmsSecret, err := readFromSecret(env.Get(config.EnvKMSSecretKeyFile, ""))
if err != nil {
logger.Fatal(err, "Unable to read the KMS secret key inherited from secret file")
}
if kmsSecret != "" {
os.Setenv(config.EnvKMSSecretKey, kmsSecret)
}
}
if env.IsSet(config.EnvConfigEnvFile) {
ekvs, err := minioEnvironFromFile(env.Get(config.EnvConfigEnvFile, ""))
if err != nil {
logger.Fatal(err, "Unable to read the config environment file")
}
for _, ekv := range ekvs {
os.Setenv(ekv.Key, ekv.Value)
}
}
}
func handleCommonEnvVars() {
loadEnvVarsFromFiles()
var err error
globalBrowserEnabled, err = config.ParseBool(env.Get(config.EnvBrowser, config.EnableOn))
if err != nil {
@@ -479,7 +674,7 @@ func handleCommonEnvVars() {
publicIPs := env.Get(config.EnvPublicIPs, "")
if len(publicIPs) != 0 {
minioEndpoints := strings.Split(publicIPs, config.ValueSeparator)
var domainIPs = set.NewStringSet()
domainIPs := set.NewStringSet()
for _, endpoint := range minioEndpoints {
if net.ParseIP(endpoint) == nil {
// Checking if the IP is a DNS entry.
@@ -509,11 +704,12 @@ func handleCommonEnvVars() {
// 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
// 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) {
@@ -530,24 +726,24 @@ func handleCommonEnvVars() {
// are defined or both are not defined.
// Check both cases and authenticate them if correctly defined
var user, password string
haveRootCredentials := false
haveAccessCredentials := false
var hasCredentials bool
//nolint:gocritic
if env.IsSet(config.EnvRootUser) && env.IsSet(config.EnvRootPassword) {
user = env.Get(config.EnvRootUser, "")
password = env.Get(config.EnvRootPassword, "")
haveRootCredentials = true
hasCredentials = true
} else if env.IsSet(config.EnvAccessKey) && env.IsSet(config.EnvSecretKey) {
user = env.Get(config.EnvAccessKey, "")
password = env.Get(config.EnvSecretKey, "")
haveAccessCredentials = true
hasCredentials = true
}
if haveRootCredentials || haveAccessCredentials {
if hasCredentials {
cred, err := auth.CreateCredentials(user, password)
if err != nil {
logger.Fatal(config.ErrInvalidCredentials(err),
"Unable to validate credentials inherited from the shell environment")
}
if haveAccessCredentials {
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,
@@ -595,7 +791,7 @@ func handleCommonEnvVars() {
logger.Fatal(err, fmt.Sprintf("Unable to load X.509 root CAs for KES from %q", env.Get(config.EnvKESServerCA, globalCertsCADir.Get())))
}
var defaultKeyID = env.Get(config.EnvKESKeyName, "")
defaultKeyID := env.Get(config.EnvKESKeyName, "")
KMS, err := kms.NewWithConfig(kms.Config{
Endpoints: endpoints,
DefaultKeyID: defaultKeyID,
@@ -699,6 +895,10 @@ func getTLSConfig() (x509Certs []*x509.Certificate, manager *certs.Manager, secu
}
}
secureConn = true
// syscall.SIGHUP to reload the certs.
manager.ReloadOnSignal(syscall.SIGHUP)
return x509Certs, manager, secureConn, nil
}
@@ -711,3 +911,32 @@ func contextCanceled(ctx context.Context) bool {
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 interface{}) interface{} {
return a.parent.Value(key)
}
+161
View File
@@ -0,0 +1,161 @@
// 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 <http://www.gnu.org/licenses/>.
package cmd
import (
"errors"
"io/ioutil"
"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 {
testCase := testCase
t.Run("", func(t *testing.T) {
tmpfile, err := ioutil.TempFile("", "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,
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run("", func(t *testing.T) {
tmpfile, err := ioutil.TempFile("", "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 !reflect.DeepEqual(ekvs, testCase.expectedEkvs) {
t.Errorf("expected %v, got %v", testCase.expectedEkvs, ekvs)
}
})
}
}
+14 -10
View File
@@ -29,27 +29,31 @@ import (
var errConfigNotFound = errors.New("config file not found")
func readConfig(ctx context.Context, objAPI ObjectLayer, configFile string) ([]byte, error) {
// Read entire content by setting size to -1
r, err := objAPI.GetObjectNInfo(ctx, minioMetaBucket, configFile, nil, http.Header{}, readLock, ObjectOptions{})
func readConfigWithMetadata(ctx context.Context, store objectIO, configFile string) ([]byte, ObjectInfo, error) {
r, err := store.GetObjectNInfo(ctx, minioMetaBucket, configFile, nil, http.Header{}, readLock, ObjectOptions{})
if err != nil {
// Treat object not found as config not found.
if isErrObjectNotFound(err) {
return nil, errConfigNotFound
return nil, ObjectInfo{}, errConfigNotFound
}
return nil, err
return nil, ObjectInfo{}, err
}
defer r.Close()
buf, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
return nil, ObjectInfo{}, err
}
if len(buf) == 0 {
return nil, errConfigNotFound
return nil, ObjectInfo{}, errConfigNotFound
}
return buf, nil
return buf, r.ObjInfo, nil
}
func readConfig(ctx context.Context, store objectIO, configFile string) ([]byte, error) {
buf, _, err := readConfigWithMetadata(ctx, store, configFile)
return buf, err
}
type objectDeleter interface {
@@ -66,13 +70,13 @@ func deleteConfig(ctx context.Context, objAPI objectDeleter, configFile string)
return err
}
func saveConfig(ctx context.Context, objAPI ObjectLayer, configFile string, data []byte) error {
func saveConfig(ctx context.Context, store objectIO, configFile string, data []byte) error {
hashReader, err := hash.NewReader(bytes.NewReader(data), int64(len(data)), "", getSHA256Hash(data), int64(len(data)))
if err != nil {
return err
}
_, err = objAPI.PutObject(ctx, minioMetaBucket, configFile, NewPutObjReader(hashReader), ObjectOptions{MaxParity: true})
_, err = store.PutObject(ctx, minioMetaBucket, configFile, NewPutObjReader(hashReader), ObjectOptions{MaxParity: true})
return err
}
+35 -24
View File
@@ -50,7 +50,7 @@ import (
)
func initHelp() {
var kvs = map[string]config.KVS{
kvs := map[string]config.KVS{
config.EtcdSubSys: etcd.DefaultKVS,
config.CacheSubSys: cache.DefaultKVS,
config.CompressionSubSys: compress.DefaultKVS,
@@ -58,10 +58,11 @@ func initHelp() {
config.IdentityOpenIDSubSys: openid.DefaultKVS,
config.IdentityTLSSubSys: xtls.DefaultKVS,
config.PolicyOPASubSys: opa.DefaultKVS,
config.SiteSubSys: config.DefaultSiteKVS,
config.RegionSubSys: config.DefaultRegionKVS,
config.APISubSys: api.DefaultKVS,
config.CredentialsSubSys: config.DefaultCredentialKVS,
config.LoggerWebhookSubSys: logger.DefaultKVS,
config.LoggerWebhookSubSys: logger.DefaultLoggerWebhookKVS,
config.AuditWebhookSubSys: logger.DefaultAuditWebhookKVS,
config.AuditKafkaSubSys: logger.DefaultAuditKafkaKVS,
config.HealSubSys: heal.DefaultKVS,
@@ -77,10 +78,10 @@ func initHelp() {
config.RegisterDefaultKVS(kvs)
// Captures help for each sub-system
var helpSubSys = config.HelpKVS{
helpSubSys := config.HelpKVS{
config.HelpKV{
Key: config.RegionSubSys,
Description: "label the location of the server",
Key: config.SiteSubSys,
Description: "label the server and its location",
},
config.HelpKV{
Key: config.CacheSubSys,
@@ -190,7 +191,7 @@ func initHelp() {
config.HelpKV{
Key: config.SubnetSubSys,
Type: "string",
Description: "set subnet config for the cluster e.g. license token",
Description: "set subnet config for the cluster e.g. api key",
Optional: true,
},
}
@@ -204,8 +205,9 @@ func initHelp() {
}
}
var helpMap = map[string]config.HelpKVS{
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,
@@ -231,10 +233,20 @@ func initHelp() {
config.NotifyRedisSubSys: notify.HelpRedis,
config.NotifyWebhookSubSys: notify.HelpWebhook,
config.NotifyESSubSys: notify.HelpES,
config.SubnetSubSys: subnet.HelpLicense,
config.SubnetSubSys: subnet.HelpSubnet,
}
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.RegisterHelpDeprecatedSubSys(deprecatedHelpKVMap)
}
var (
@@ -259,7 +271,7 @@ func validateConfig(s config.Config) error {
return err
}
if _, err := config.LookupRegion(s[config.RegionSubSys][config.Default]); err != nil {
if _, err := config.LookupSite(s[config.SiteSubSys][config.Default], s[config.RegionSubSys][config.Default]); err != nil {
return err
}
@@ -315,7 +327,7 @@ func validateConfig(s config.Config) error {
}
}
if _, err := openid.LookupConfig(s[config.IdentityOpenIDSubSys][config.Default],
NewGatewayHTTPTransport(), xhttp.DrainBody); err != nil {
NewGatewayHTTPTransport(), xhttp.DrainBody, globalSite.Region); err != nil {
return err
}
@@ -437,9 +449,9 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
// but not federation.
globalBucketFederation = etcdCfg.PathPrefix == "" && etcdCfg.Enabled
globalServerRegion, err = config.LookupRegion(s[config.RegionSubSys][config.Default])
globalSite, err = config.LookupSite(s[config.SiteSubSys][config.Default], s[config.RegionSubSys][config.Default])
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Invalid region configuration: %w", err))
logger.LogIf(ctx, fmt.Errorf("Invalid site configuration: %w", err))
}
apiConfig, err := api.LookupConfig(s[config.APISubSys][config.Default])
@@ -501,7 +513,7 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
}
globalOpenIDConfig, err = openid.LookupConfig(s[config.IdentityOpenIDSubSys][config.Default],
NewGatewayHTTPTransport(), xhttp.DrainBody)
NewGatewayHTTPTransport(), xhttp.DrainBody, globalSite.Region)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize OpenID: %w", err))
}
@@ -531,7 +543,7 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
loggerCfg, err := logger.LookupConfig(s)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize logger: %w", err))
logger.LogIf(ctx, fmt.Errorf("Unable to initialize logger/audit targets: %w", err))
}
for _, l := range loggerCfg.HTTP {
@@ -541,7 +553,7 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
l.Transport = NewGatewayHTTPTransportWithClientCerts(l.ClientCert, l.ClientKey)
// Enable http logging
if err = logger.AddTarget(http.New(l)); err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize console HTTP target: %w", err))
logger.LogIf(ctx, fmt.Errorf("Unable to initialize server logger HTTP target: %w", err))
}
}
}
@@ -553,7 +565,7 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
l.Transport = NewGatewayHTTPTransportWithClientCerts(l.ClientCert, l.ClientKey)
// Enable http audit logging
if err = logger.AddAuditTarget(http.New(l)); err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize audit HTTP target: %w", err))
logger.LogIf(ctx, fmt.Errorf("Unable to initialize server audit HTTP target: %w", err))
}
}
}
@@ -563,7 +575,7 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
l.LogOnce = logger.LogOnceIf
// Enable Kafka audit logging
if err = logger.AddAuditTarget(kafka.New(l)); err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize audit Kafka target: %w", err))
logger.LogIf(ctx, fmt.Errorf("Unable to initialize server audit Kafka target: %w", err))
}
}
}
@@ -674,7 +686,10 @@ func GetHelp(subSys, key string, envOnly bool) (Help, error) {
subSysHelp, ok := config.HelpSubSysMap[""].Lookup(subSys)
if !ok {
return Help{}, config.Errorf("unknown sub-system %s", subSys)
subSysHelp, ok = config.HelpDeprecatedSubSysMap[subSys]
if !ok {
return Help{}, config.Errorf("unknown sub-system %s", subSys)
}
}
h, ok := config.HelpSubSysMap[subSys]
@@ -696,9 +711,7 @@ func GetHelp(subSys, key string, envOnly bool) (Help, error) {
// to list the ENV, for regular k/v EnableKey is
// implicit, for ENVs we cannot make it implicit.
if subSysHelp.MultipleTargets {
envK := config.EnvPrefix + strings.Join([]string{
strings.ToTitle(subSys), strings.ToTitle(madmin.EnableKey),
}, config.EnvWordDelimiter)
envK := config.EnvPrefix + strings.ToTitle(subSys) + config.EnvWordDelimiter + strings.ToTitle(madmin.EnableKey)
envHelp = append(envHelp, config.HelpKV{
Key: envK,
Description: fmt.Sprintf("enable %s target, default is 'off'", subSys),
@@ -707,9 +720,7 @@ func GetHelp(subSys, key string, envOnly bool) (Help, error) {
})
}
for _, hkv := range h {
envK := config.EnvPrefix + strings.Join([]string{
strings.ToTitle(subSys), strings.ToTitle(hkv.Key),
}, config.EnvWordDelimiter)
envK := config.EnvPrefix + strings.ToTitle(subSys) + config.EnvWordDelimiter + strings.ToTitle(hkv.Key)
envHelp = append(envHelp, config.HelpKV{
Key: envK,
Description: hkv.Description,
+8 -5
View File
@@ -36,18 +36,21 @@ func TestServerConfig(t *testing.T) {
t.Fatalf("Init Test config failed")
}
if globalServerRegion != globalMinioDefaultRegion {
t.Errorf("Expecting region `us-east-1` found %s", globalServerRegion)
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")
region, err := config.LookupRegion(globalServerConfig[config.RegionSubSys][config.Default])
site, err := config.LookupSite(
globalServerConfig[config.SiteSubSys][config.Default],
globalServerConfig[config.RegionSubSys][config.Default],
)
if err != nil {
t.Fatal(err)
}
if region != "us-west-1" {
t.Errorf("Expecting region `us-west-1` found %s", globalServerRegion)
if site.Region != "us-west-1" {
t.Errorf("Expecting region `us-west-1` found %s", globalSite.Region)
}
if err := saveServerConfig(context.Background(), objLayer, globalServerConfig); err != nil {
+1 -1
View File
@@ -84,7 +84,7 @@ func (dir *ConfigDir) Get() string {
// Attempts to create all directories, ignores any permission denied errors.
func mkdirAllIgnorePerm(path string) error {
err := os.MkdirAll(path, 0700)
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.
+2 -2
View File
@@ -2445,12 +2445,12 @@ func migrateConfigToMinioSys(objAPI ObjectLayer) (err error) {
return err
} // if errConfigNotFound proceed to migrate..
var configFiles = []string{
configFiles := []string{
getConfigFile(),
getConfigFile() + ".deprecated",
configFile,
}
var config = &serverConfigV27{}
config := &serverConfigV27{}
for _, cfgFile := range configFiles {
if _, err = Load(cfgFile, config); err != nil {
if !osIsNotExist(err) && !osIsPermission(err) {
+6 -6
View File
@@ -51,7 +51,7 @@ func TestServerConfigMigrateV1(t *testing.T) {
// Create a V1 config json file and store it
configJSON := "{ \"version\":\"1\", \"accessKeyId\":\"abcde\", \"secretAccessKey\":\"abcdefgh\"}"
configPath := rootPath + "/fsUsers.json"
if err := ioutil.WriteFile(configPath, []byte(configJSON), 0644); err != nil {
if err := ioutil.WriteFile(configPath, []byte(configJSON), 0o644); err != nil {
t.Fatal("Unexpected error: ", err)
}
@@ -181,7 +181,7 @@ func TestServerConfigMigrateV2toV33(t *testing.T) {
configPath := rootPath + SlashSeparator + minioConfigFile
// Create a corrupted config file
if err := ioutil.WriteFile(configPath, []byte("{ \"version\":\"2\","), 0644); err != nil {
if err := ioutil.WriteFile(configPath, []byte("{ \"version\":\"2\","), 0o644); err != nil {
t.Fatal("Unexpected error: ", err)
}
// Fire a migrateConfig()
@@ -194,7 +194,7 @@ func TestServerConfigMigrateV2toV33(t *testing.T) {
// Create a V2 config json file and store it
configJSON := "{ \"version\":\"2\", \"credentials\": {\"accessKeyId\":\"" + accessKey + "\", \"secretAccessKey\":\"" + secretKey + "\", \"region\":\"us-east-1\"}, \"mongoLogger\":{\"addr\":\"127.0.0.1:3543\", \"db\":\"foodb\", \"collection\":\"foo\"}, \"syslogLogger\":{\"network\":\"127.0.0.1:543\", \"addr\":\"addr\"}, \"fileLogger\":{\"filename\":\"log.out\"}}"
if err := ioutil.WriteFile(configPath, []byte(configJSON), 0644); err != nil {
if err := ioutil.WriteFile(configPath, []byte(configJSON), 0o644); err != nil {
t.Fatal("Unexpected error: ", err)
}
@@ -244,7 +244,7 @@ func TestServerConfigMigrateFaultyConfig(t *testing.T) {
configPath := rootPath + SlashSeparator + minioConfigFile
// Create a corrupted config file
if err := ioutil.WriteFile(configPath, []byte("{ \"version\":\"2\", \"test\":"), 0644); err != nil {
if err := ioutil.WriteFile(configPath, []byte("{ \"version\":\"2\", \"test\":"), 0o644); err != nil {
t.Fatal("Unexpected error: ", err)
}
@@ -343,7 +343,7 @@ func TestServerConfigMigrateCorruptedConfig(t *testing.T) {
for i := 3; i <= 17; i++ {
// Create a corrupted config file
if err = ioutil.WriteFile(configPath, []byte(fmt.Sprintf("{ \"version\":\"%d\", \"credential\": { \"accessKey\": 1 } }", i)),
0644); err != nil {
0o644); err != nil {
t.Fatal("Unexpected error: ", err)
}
@@ -354,7 +354,7 @@ func TestServerConfigMigrateCorruptedConfig(t *testing.T) {
}
// Create a corrupted config file for version '2'.
if err = ioutil.WriteFile(configPath, []byte("{ \"version\":\"2\", \"credentials\": { \"accessKeyId\": 1 } }"), 0644); err != nil {
if err = ioutil.WriteFile(configPath, []byte("{ \"version\":\"2\", \"credentials\": { \"accessKeyId\": 1 } }"), 0o644); err != nil {
t.Fatal("Unexpected error: ", err)
}
-3
View File
@@ -34,14 +34,12 @@ import (
"github.com/minio/pkg/quick"
)
/////////////////// Config V1 ///////////////////
type configV1 struct {
Version string `json:"version"`
AccessKey string `json:"accessKeyId"`
SecretKey string `json:"secretAccessKey"`
}
/////////////////// Config V2 ///////////////////
type configV2 struct {
Version string `json:"version"`
Credentials struct {
@@ -63,7 +61,6 @@ type configV2 struct {
} `json:"fileLogger"`
}
/////////////////// Config V3 ///////////////////
// backendV3 type.
type backendV3 struct {
Type string `json:"type"`
+9 -13
View File
@@ -21,6 +21,7 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"path"
"sort"
"strings"
@@ -45,8 +46,8 @@ const (
)
func listServerConfigHistory(ctx context.Context, objAPI ObjectLayer, withData bool, count int) (
[]madmin.ConfigHistoryEntry, error) {
[]madmin.ConfigHistoryEntry, error,
) {
var configHistory []madmin.ConfigHistoryEntry
// List all kvs
@@ -139,7 +140,7 @@ func saveServerConfig(ctx context.Context, objAPI ObjectLayer, cfg interface{})
return err
}
var configFile = path.Join(minioConfigPrefix, minioConfigFile)
configFile := path.Join(minioConfigPrefix, minioConfigFile)
if GlobalKMS != nil {
data, err = config.EncryptBytes(GlobalKMS, data, kms.Context{
minioMetaBucket: path.Join(minioMetaBucket, configFile),
@@ -152,7 +153,7 @@ func saveServerConfig(ctx context.Context, objAPI ObjectLayer, cfg interface{})
}
func readServerConfig(ctx context.Context, objAPI ObjectLayer) (config.Config, error) {
var srvCfg = config.New()
srvCfg := config.New()
configFile := path.Join(minioConfigPrefix, minioConfigFile)
data, err := readConfig(ctx, objAPI, configFile)
if err != nil {
@@ -173,7 +174,7 @@ func readServerConfig(ctx context.Context, objAPI ObjectLayer) (config.Config, e
}
}
var json = jsoniter.ConfigCompatibleWithStandardLibrary
json := jsoniter.ConfigCompatibleWithStandardLibrary
if err = json.Unmarshal(data, &srvCfg); err != nil {
return nil, err
}
@@ -185,11 +186,6 @@ func readServerConfig(ctx context.Context, objAPI ObjectLayer) (config.Config, e
// ConfigSys - config system.
type ConfigSys struct{}
// Load - load config.json.
func (sys *ConfigSys) Load(objAPI ObjectLayer) error {
return sys.Init(objAPI)
}
// Init - initializes config system from config.json.
func (sys *ConfigSys) Init(objAPI ObjectLayer) error {
if objAPI == nil {
@@ -222,18 +218,18 @@ func initConfig(objAPI ObjectLayer) error {
// If etcd is set then migrates /config/config.json
// to '<export_path>/.minio.sys/config/config.json'
if err := migrateConfigToMinioSys(objAPI); err != nil {
return err
return fmt.Errorf("migrateConfigToMinioSys: %w", err)
}
// Migrates backend '<export_path>/.minio.sys/config/config.json' to latest version.
if err := migrateMinioSysConfig(objAPI); err != nil {
return err
return fmt.Errorf("migrateMinioSysConfig: %w", err)
}
// Migrates backend '<export_path>/.minio.sys/config/config.json' to
// latest config format.
if err := migrateMinioSysConfigToKV(objAPI); err != nil {
return err
return fmt.Errorf("migrateMinioSysConfigToKV: %w", err)
}
return loadConfig(objAPI)
+1 -1
View File
@@ -32,7 +32,7 @@ import (
// number of log messages to buffer
const defaultLogBufferCount = 10000
//HTTPConsoleLoggerSys holds global console logger state
// HTTPConsoleLoggerSys holds global console logger state
type HTTPConsoleLoggerSys struct {
sync.RWMutex
pubsub *pubsub.PubSub
+11 -19
View File
@@ -25,29 +25,21 @@ const crossDomainXML = `<?xml version="1.0"?><!DOCTYPE cross-domain-policy SYSTE
// Standard path where an app would find cross domain policy information.
const crossDomainXMLEntity = "/crossdomain.xml"
// Cross domain policy implements http.Handler interface, implementing a custom ServerHTTP.
type crossDomainPolicy struct {
handler http.Handler
}
// 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 setCrossDomainPolicy(h http.Handler) http.Handler {
return crossDomainPolicy{handler: h}
}
func (c crossDomainPolicy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Look for 'crossdomain.xml' in the incoming request.
switch r.URL.Path {
case crossDomainXMLEntity:
// Write the standard cross domain policy xml.
w.Write([]byte(crossDomainXML))
// Request completed, no need to serve to other handlers.
return
}
// Continue to serve the request further.
c.handler.ServeHTTP(w, r)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Look for 'crossdomain.xml' in the incoming request.
switch r.URL.Path {
case crossDomainXMLEntity:
// Write the standard cross domain policy xml.
w.Write([]byte(crossDomainXML))
// Request completed, no need to serve to other handlers.
return
}
h.ServeHTTP(w, r)
})
}
+100 -45
View File
@@ -22,9 +22,9 @@ import (
"context"
"encoding/binary"
"errors"
"io/fs"
"math"
"math/rand"
"net/http"
"os"
"path"
"strings"
@@ -39,7 +39,6 @@ import (
"github.com/minio/minio/internal/color"
"github.com/minio/minio/internal/config/heal"
"github.com/minio/minio/internal/event"
"github.com/minio/minio/internal/hash"
"github.com/minio/minio/internal/logger"
"github.com/minio/pkg/console"
)
@@ -70,7 +69,19 @@ var (
// initDataScanner will start the scanner in the background.
func initDataScanner(ctx context.Context, objAPI ObjectLayer) {
go runDataScanner(ctx, objAPI)
go func() {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
// Run the data scanner in a loop
for {
runDataScanner(ctx, objAPI)
duration := time.Duration(r.Float64() * float64(scannerCycle.Get()))
if duration < time.Second {
// Make sure to sleep atleast a second to avoid high CPU ticks.
duration = time.Second
}
time.Sleep(duration)
}
}()
}
type safeDuration struct {
@@ -95,36 +106,26 @@ func (s *safeDuration) Get() time.Duration {
// There should only ever be one scanner running per cluster.
func runDataScanner(pctx context.Context, objAPI ObjectLayer) {
// Make sure only 1 scanner is running on the cluster.
locker := objAPI.NewNSLock(minioMetaBucket, "runDataScanner.lock")
var ctx context.Context
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for {
lkctx, err := locker.GetLock(pctx, dataScannerLeaderLockTimeout)
if err != nil {
time.Sleep(time.Duration(r.Float64() * float64(scannerCycle.Get())))
continue
locker := objAPI.NewNSLock(minioMetaBucket, "scanner/runDataScanner.lock")
lkctx, err := locker.GetLock(pctx, dataScannerLeaderLockTimeout)
if err != nil {
if intDataUpdateTracker.debug {
logger.LogIf(pctx, err)
}
ctx = lkctx.Context()
defer lkctx.Cancel()
break
// No unlock for "leader" lock.
return
}
ctx := lkctx.Context()
defer lkctx.Cancel()
// No unlock for "leader" lock.
// Load current bloom cycle
nextBloomCycle := intDataUpdateTracker.current() + 1
br, err := objAPI.GetObjectNInfo(ctx, dataUsageBucket, dataUsageBloomName, nil, http.Header{}, readLock, ObjectOptions{})
if err != nil {
if !isErrObjectNotFound(err) && !isErrBucketNotFound(err) {
buf, _ := readConfig(ctx, objAPI, dataUsageBloomNamePath)
if len(buf) >= 8 {
if err = binary.Read(bytes.NewReader(buf), binary.LittleEndian, &nextBloomCycle); err != nil {
logger.LogIf(ctx, err)
}
} else {
if br.ObjInfo.Size == 8 {
if err = binary.Read(br, binary.LittleEndian, &nextBloomCycle); err != nil {
logger.LogIf(ctx, err)
}
}
br.Close()
}
scannerTimer := time.NewTimer(scannerCycle.Get())
@@ -154,14 +155,7 @@ func runDataScanner(pctx context.Context, objAPI ObjectLayer) {
nextBloomCycle++
var tmp [8]byte
binary.LittleEndian.PutUint64(tmp[:], nextBloomCycle)
r, err := hash.NewReader(bytes.NewReader(tmp[:]), int64(len(tmp)), "", "", int64(len(tmp)))
if err != nil {
logger.LogIf(ctx, err)
continue
}
_, err = objAPI.PutObject(ctx, dataUsageBucket, dataUsageBloomName, NewPutObjReader(r), ObjectOptions{})
if !isErrBucketNotFound(err) {
if err = saveConfig(ctx, objAPI, dataUsageBloomNamePath, tmp[:]); err != nil {
logger.LogIf(ctx, err)
}
}
@@ -266,7 +260,6 @@ func scanDataFolder(ctx context.Context, basePath string, cache dataUsageCache,
defer func() {
console.Debugf(logPrefix+" Scanner time: %v %s\n", time.Since(t), logSuffix)
}()
}
switch cache.Info.Name {
@@ -655,6 +648,7 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
dirQuorum: getReadQuorum(len(f.disks)),
objQuorum: getReadQuorum(len(f.disks)),
bucket: "",
strict: false,
}
healObjectsPrefix := color.Green("healObjects:")
@@ -774,7 +768,7 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
if flat.Objects < dataScannerCompactLeastObject {
if f.dataUsageScannerDebug && flat.Objects > 1 {
// Disabled, rather chatty:
//console.Debugf(scannerLogPrefix+" Only %d objects, compacting %s -> %+v\n", flat.Objects, folder.name, flat)
// console.Debugf(scannerLogPrefix+" Only %d objects, compacting %s -> %+v\n", flat.Objects, folder.name, flat)
}
compact = true
} else {
@@ -790,7 +784,7 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
}
if f.dataUsageScannerDebug && compact {
// Disabled, rather chatty:
//console.Debugf(scannerLogPrefix+" Only objects (%d), compacting %s -> %+v\n", flat.Objects, folder.name, flat)
// console.Debugf(scannerLogPrefix+" Only objects (%d), compacting %s -> %+v\n", flat.Objects, folder.name, flat)
}
}
if compact {
@@ -816,14 +810,13 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
// scannerItem represents each file while walking.
type scannerItem struct {
Path string
Typ os.FileMode
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.
lifeCycle *lifecycle.Lifecycle
replication replicationConfig
lifeCycle *lifecycle.Lifecycle
Typ fs.FileMode
heal bool // Has the object been selected for heal check?
debug bool
}
@@ -838,6 +831,7 @@ type sizeSummary struct {
pendingCount uint64
failedCount uint64
replTargetStats map[string]replTargetSizeSummary
tiers map[string]tierStats
}
// replTargetSizeSummary holds summary of replication stats by target
@@ -863,7 +857,10 @@ func (i *scannerItem) transformMetaDir() {
i.objectName = split[len(split)-1]
}
var applyActionsLogPrefix = color.Green("applyActions:")
var (
applyActionsLogPrefix = color.Green("applyActions:")
applyVersionActionsLogPrefix = color.Green("applyVersionActions:")
)
func (i *scannerItem) applyHealing(ctx context.Context, o ObjectLayer, oi ObjectInfo) (size int64) {
if i.debug {
@@ -966,7 +963,67 @@ func (i *scannerItem) applyTierObjSweep(ctx context.Context, o ObjectLayer, oi O
if ignoreNotFoundErr(err) != nil {
logger.LogIf(ctx, err)
}
}
// applyNewerNoncurrentVersionLimit removes noncurrent versions older than the most recent NewerNoncurrentVersions configured.
// Note: This function doesn't update sizeSummary since it always removes versions that it doesn't return.
func (i *scannerItem) applyNewerNoncurrentVersionLimit(ctx context.Context, _ ObjectLayer, fivs []FileInfo) ([]FileInfo, error) {
if i.lifeCycle == nil {
return fivs, nil
}
_, days, lim := i.lifeCycle.NoncurrentVersionsExpirationLimit(lifecycle.ObjectOpts{Name: i.objectPath()})
if lim == 0 || len(fivs) <= lim+1 { // fewer than lim _noncurrent_ versions
return fivs, nil
}
overflowVersions := fivs[lim+1:]
// current version + most recent lim noncurrent versions
fivs = fivs[:lim+1]
rcfg, _ := globalBucketObjectLockSys.Get(i.bucket)
toDel := make([]ObjectToDelete, 0, len(overflowVersions))
for _, fi := range overflowVersions {
obj := fi.ToObjectInfo(i.bucket, i.objectPath())
// skip versions with object locking enabled
if rcfg.LockEnabled && enforceRetentionForDeletion(ctx, obj) {
if i.debug {
if obj.VersionID != "" {
console.Debugf(applyVersionActionsLogPrefix+" lifecycle: %s v(%s) is locked, not deleting\n", obj.Name, obj.VersionID)
} else {
console.Debugf(applyVersionActionsLogPrefix+" lifecycle: %s is locked, not deleting\n", obj.Name)
}
}
// add this version back to remaining versions for
// subsequent lifecycle policy applications
fivs = append(fivs, fi)
continue
}
// NoncurrentDays not passed yet.
if time.Now().UTC().Before(lifecycle.ExpectedExpiryTime(obj.SuccessorModTime, days)) {
// add this version back to remaining versions for
// subsequent lifecycle policy applications
fivs = append(fivs, fi)
continue
}
toDel = append(toDel, ObjectToDelete{
ObjectV: ObjectV{
ObjectName: fi.Name,
VersionID: fi.VersionID,
},
})
}
globalExpiryState.enqueueByNewerNoncurrent(i.bucket, toDel)
return fivs, nil
}
// applyVersionActions will apply lifecycle checks on all versions of a scanned item. Returns versions that remain
// after applying lifecycle checks configured.
func (i *scannerItem) applyVersionActions(ctx context.Context, o ObjectLayer, fivs []FileInfo) ([]FileInfo, error) {
return i.applyNewerNoncurrentVersionLimit(ctx, o, fivs)
}
// applyActions will apply lifecycle checks on to a scanned item.
@@ -974,8 +1031,6 @@ func (i *scannerItem) applyTierObjSweep(ctx context.Context, o ObjectLayer, oi O
// 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, o ObjectLayer, oi ObjectInfo, sizeS *sizeSummary) int64 {
i.applyTierObjSweep(ctx, o, oi)
applied, size := i.applyLifecycle(ctx, o, oi)
// For instance, an applied lifecycle means we remove/transitioned an object
// from the current deployment, which means we don't have to call healing
@@ -1030,7 +1085,6 @@ func applyTransitionRule(obj ObjectInfo) bool {
}
globalTransitionState.queueTransitionTask(obj)
return true
}
func applyExpiryOnTransitionedObject(ctx context.Context, objLayer ObjectLayer, obj ObjectInfo, restoredObject bool) bool {
@@ -1092,7 +1146,7 @@ func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLay
// Apply object, object version, restored object or restored object version action on the given object
func applyExpiryRule(obj ObjectInfo, restoredObject, applyOnVersion bool) bool {
globalExpiryState.queueExpiryTask(obj, restoredObject, applyOnVersion)
globalExpiryState.enqueueByDays(obj, restoredObject, applyOnVersion)
return true
}
@@ -1199,6 +1253,7 @@ func (i *scannerItem) healReplicationDeletes(ctx context.Context, o ObjectLayer,
DeleteMarker: roi.DeleteMarker,
},
Bucket: roi.Bucket,
OpType: replication.HealReplicationType,
}
if roi.ExistingObjResync.mustResync() {
doi.OpType = replication.ExistingObjectReplicationType
+24 -19
View File
@@ -203,7 +203,7 @@ func (d *dataUpdateTracker) latestWithDir(dir string) uint64 {
// start a saver goroutine.
// All of these will exit when the context is canceled.
func (d *dataUpdateTracker) start(ctx context.Context, drives ...string) {
if len(drives) <= 0 {
if len(drives) == 0 {
logger.LogIf(ctx, errors.New("dataUpdateTracker.start: No drives specified"))
return
}
@@ -220,7 +220,7 @@ func (d *dataUpdateTracker) start(ctx context.Context, drives ...string) {
// If no valid data usage tracker can be found d will remain unchanged.
// If object is shared the caller should lock it.
func (d *dataUpdateTracker) load(ctx context.Context, drives ...string) {
if len(drives) <= 0 {
if len(drives) == 0 {
logger.LogIf(ctx, errors.New("dataUpdateTracker.load: No drives specified"))
return
}
@@ -473,27 +473,32 @@ func (d *dataUpdateTracker) deserialize(src io.Reader, newerThan time.Time) erro
// start a collector that picks up entries from objectUpdatedCh
// and adds them to the current bloom filter.
func (d *dataUpdateTracker) startCollector(ctx context.Context) {
for in := range d.input {
bucket, _ := path2BucketObjectWithBasePath("", in)
if bucket == "" {
if d.debug && len(in) > 0 {
console.Debugf(color.Green("dataUpdateTracker:")+" no bucket (%s)\n", in)
for {
select {
case <-ctx.Done():
return
case in := <-d.input:
bucket, _ := path2BucketObjectWithBasePath("", in)
if bucket == "" {
if d.debug && len(in) > 0 {
console.Debugf(color.Green("dataUpdateTracker:")+" no bucket (%s)\n", in)
}
continue
}
continue
}
if isReservedOrInvalidBucket(bucket, false) {
continue
}
split := splitPathDeterministic(in)
if isReservedOrInvalidBucket(bucket, false) {
continue
}
split := splitPathDeterministic(in)
// Add all paths until done.
d.mu.Lock()
for i := range split {
d.Current.bf.AddString(hashPath(path.Join(split[:i+1]...)).String())
// Add all paths until done.
d.mu.Lock()
for i := range split {
d.Current.bf.AddString(hashPath(path.Join(split[:i+1]...)).String())
}
d.dirty = d.dirty || len(split) > 0
d.mu.Unlock()
}
d.dirty = d.dirty || len(split) > 0
d.mu.Unlock()
}
}
+1 -1
View File
@@ -105,7 +105,7 @@ func TestDataUpdateTracker(t *testing.T) {
defer cancel()
dut.start(ctx, tmpDir)
var tests = []struct {
tests := []struct {
in string
check []string // if not empty, check against these instead.
exist bool
+201 -60
View File
@@ -31,6 +31,7 @@ import (
"github.com/cespare/xxhash/v2"
"github.com/klauspost/compress/zstd"
"github.com/minio/madmin-go"
"github.com/minio/minio/internal/bucket/lifecycle"
"github.com/minio/minio/internal/hash"
"github.com/minio/minio/internal/logger"
@@ -45,16 +46,70 @@ type dataUsageHash string
// sizeHistogram is a size histogram.
type sizeHistogram [dataUsageBucketLen]uint64
//msgp:tuple dataUsageEntry
type dataUsageEntry struct {
Children dataUsageHashMap
Children dataUsageHashMap `msg:"ch"`
// These fields do no include any children.
Size int64
Objects uint64
Versions uint64 // Versions that are not delete markers.
ObjSizes sizeHistogram
ReplicationStats *replicationAllStats
Compacted bool
Size int64 `msg:"sz"`
Objects uint64 `msg:"os"`
Versions uint64 `msg:"vs"` // Versions that are not delete markers.
ObjSizes sizeHistogram `msg:"szs"`
ReplicationStats *replicationAllStats `msg:"rs,omitempty"`
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(sz sizeSummary) {
for tier, st := range sz.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) adminStats(stats map[string]madmin.TierStats) map[string]madmin.TierStats {
if ats == nil {
return stats
}
// 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,
}
}
return stats
}
// 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 {
ts.TotalSize += u.TotalSize
ts.NumVersions += u.NumVersions
ts.NumObjects += u.NumObjects
return ts
}
//msgp:tuple replicationStatsV1
@@ -96,14 +151,19 @@ func (rs replicationStats) Empty() bool {
rs.FailedCount == 0
}
//msgp:tuple replicationAllStats
type replicationAllStats struct {
Targets map[string]replicationStats `msg:"t,omitempty"`
ReplicaSize uint64 `msg:"r,omitempty"`
}
//msgp:tuple replicationAllStatsV1
type replicationAllStatsV1 struct {
Targets map[string]replicationStats
ReplicaSize uint64 `msg:"ReplicaSize,omitempty"`
}
//msgp:encode ignore dataUsageEntryV2 dataUsageEntryV3 dataUsageEntryV4
//msgp:marshal ignore dataUsageEntryV2 dataUsageEntryV3 dataUsageEntryV4
//msgp:encode ignore dataUsageEntryV2 dataUsageEntryV3 dataUsageEntryV4 dataUsageEntryV5 dataUsageEntryV6
//msgp:marshal ignore dataUsageEntryV2 dataUsageEntryV3 dataUsageEntryV4 dataUsageEntryV5 dataUsageEntryV6
//msgp:tuple dataUsageEntryV2
type dataUsageEntryV2 struct {
@@ -149,6 +209,18 @@ type dataUsageEntryV5 struct {
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
ReplicationStats *replicationAllStatsV1
Compacted bool
}
// dataUsageCache contains a cache of data usage entries latest version.
type dataUsageCache struct {
Info dataUsageCacheInfo
@@ -156,8 +228,8 @@ type dataUsageCache struct {
Disks []string
}
//msgp:encode ignore dataUsageCacheV2 dataUsageCacheV3 dataUsageCacheV4 dataUsageCacheV5
//msgp:marshal ignore dataUsageCacheV2 dataUsageCacheV3 dataUsageCacheV4 dataUsageCacheV5
//msgp:encode ignore dataUsageCacheV2 dataUsageCacheV3 dataUsageCacheV4 dataUsageCacheV5 dataUsageCacheV6
//msgp:marshal ignore dataUsageCacheV2 dataUsageCacheV3 dataUsageCacheV4 dataUsageCacheV5 dataUsageCacheV6
// dataUsageCacheV2 contains a cache of data usage entries version 2.
type dataUsageCacheV2 struct {
@@ -166,27 +238,34 @@ type dataUsageCacheV2 struct {
Cache map[string]dataUsageEntryV2
}
// dataUsageCache contains a cache of data usage entries version 3.
// dataUsageCacheV3 contains a cache of data usage entries version 3.
type dataUsageCacheV3 struct {
Info dataUsageCacheInfo
Disks []string
Cache map[string]dataUsageEntryV3
}
// dataUsageCache contains a cache of data usage entries version 4.
// dataUsageCacheV4 contains a cache of data usage entries version 4.
type dataUsageCacheV4 struct {
Info dataUsageCacheInfo
Disks []string
Cache map[string]dataUsageEntryV4
}
// dataUsageCache contains a cache of data usage entries version 5.
// dataUsageCacheV5 contains a cache of data usage entries version 5.
type dataUsageCacheV5 struct {
Info dataUsageCacheInfo
Disks []string
Cache map[string]dataUsageEntryV5
}
// dataUsageCacheV6 contains a cache of data usage entries version 6.
type dataUsageCacheV6 struct {
Info dataUsageCacheInfo
Disks []string
Cache map[string]dataUsageEntryV6
}
//msgp:ignore dataUsageEntryInfo
type dataUsageEntryInfo struct {
Name string
@@ -242,6 +321,12 @@ func (e *dataUsageEntry) addSizes(summary sizeSummary) {
e.ReplicationStats.Targets[arn] = tgtStat
}
}
if summary.tiers != nil {
if e.AllTierStats == nil {
e.AllTierStats = newAllTierStats()
}
e.AllTierStats.addSizes(summary)
}
}
// merge other data usage entry into this, excluding children.
@@ -271,6 +356,13 @@ func (e *dataUsageEntry) merge(other dataUsageEntry) {
for i, v := range other.ObjSizes[:] {
e.ObjSizes[i] += v
}
if other.AllTierStats != nil {
if e.AllTierStats == nil {
e.AllTierStats = newAllTierStats()
}
e.AllTierStats.merge(other.AllTierStats)
}
}
// mod returns true if the hash mod cycles == cycle.
@@ -317,6 +409,11 @@ func (e dataUsageEntry) clone() dataUsageEntry {
r := *e.ReplicationStats
e.ReplicationStats = &r
}
if e.AllTierStats != nil {
ats := newAllTierStats()
ats.merge(e.AllTierStats)
e.AllTierStats = ats
}
return e
}
@@ -438,6 +535,7 @@ func (d *dataUsageCache) dui(path string, buckets []BucketInfo) DataUsageInfo {
ObjectsTotalSize: uint64(flat.Size),
BucketsCount: uint64(len(e.Children)),
BucketsUsage: d.bucketsUsageInfo(buckets),
TierStats: d.tiersUsageInfo(buckets),
}
return dui
}
@@ -526,7 +624,7 @@ func (d *dataUsageCache) reduceChildrenOf(path dataUsageHash, limit int, compact
// Appears to be printed with _MINIO_SERVER_DEBUG=off
// console.Debugf(" %d children found, compacting %v\n", total, path)
var leaves = make([]struct {
leaves := make([]struct {
objects uint64
path dataUsageHash
}, total)
@@ -654,10 +752,29 @@ func (h *sizeHistogram) toMap() map[string]uint64 {
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 {
var dst = make(map[string]BucketUsageInfo, len(buckets))
dst := make(map[string]BucketUsageInfo, len(buckets))
for _, bucket := range buckets {
e := d.find(bucket.Name)
if e == nil {
@@ -680,7 +797,6 @@ func (d *dataUsageCache) bucketsUsageInfo(buckets []BucketInfo) map[string]Bucke
ReplicationPendingCount: stat.PendingCount,
ReplicationFailedCount: stat.FailedCount,
}
}
}
dst[bucket.Name] = bui
@@ -805,6 +921,7 @@ func (d *dataUsageCache) load(ctx context.Context, store objectIO, name string)
// Abandon if more than 5 minutes, so we don't hold up scanner.
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()
r, err := store.GetObjectNInfo(ctx, dataUsageBucket, name, nil, http.Header{}, readLock, ObjectOptions{})
if err != nil {
switch err.(type) {
@@ -857,7 +974,8 @@ func (d *dataUsageCache) save(ctx context.Context, store objectIO, name string)
// Bumping the cache version will drop data from previous versions
// and write new data with the new version.
const (
dataUsageCacheVerCurrent = 6
dataUsageCacheVerCurrent = 7
dataUsageCacheVerV6 = 6
dataUsageCacheVerV5 = 5
dataUsageCacheVerV4 = 4
dataUsageCacheVerV3 = 3
@@ -953,19 +1071,17 @@ func (d *dataUsageCache) deserialize(r io.Reader) error {
Children: v.Children,
}
if v.ReplicatedSize > 0 || v.ReplicaSize > 0 || v.ReplicationFailedSize > 0 || v.ReplicationPendingSize > 0 {
due.ReplicationStats = &replicationAllStats{
Targets: make(map[string]replicationStats),
}
cfg, err := getReplicationConfig(GlobalContext, d.Info.Name)
if err != nil {
return err
}
due.ReplicationStats.ReplicaSize = v.ReplicaSize
due.ReplicationStats.Targets[cfg.RoleArn] = replicationStats{
ReplicatedSize: v.ReplicatedSize,
FailedSize: v.ReplicationFailedSize,
PendingSize: v.ReplicationPendingSize,
cfg, _ := getReplicationConfig(GlobalContext, d.Info.Name)
if cfg != nil && cfg.RoleArn != "" {
due.ReplicationStats = &replicationAllStats{
Targets: make(map[string]replicationStats),
}
due.ReplicationStats.ReplicaSize = v.ReplicaSize
due.ReplicationStats.Targets[cfg.RoleArn] = replicationStats{
ReplicatedSize: v.ReplicatedSize,
FailedSize: v.ReplicationFailedSize,
PendingSize: v.ReplicationPendingSize,
}
}
}
due.Compacted = len(due.Children) == 0 && k != d.Info.Name
@@ -997,21 +1113,20 @@ func (d *dataUsageCache) deserialize(r io.Reader) error {
empty := replicationStatsV1{}
if v.ReplicationStats != empty {
due.ReplicationStats = &replicationAllStats{
Targets: make(map[string]replicationStats),
cfg, _ := getReplicationConfig(GlobalContext, d.Info.Name)
if cfg != nil && cfg.RoleArn != "" {
due.ReplicationStats = &replicationAllStats{
Targets: make(map[string]replicationStats),
}
due.ReplicationStats.Targets[cfg.RoleArn] = replicationStats{
ReplicatedSize: v.ReplicationStats.ReplicatedSize,
FailedSize: v.ReplicationStats.FailedSize,
FailedCount: v.ReplicationStats.FailedCount,
PendingSize: v.ReplicationStats.PendingSize,
PendingCount: v.ReplicationStats.PendingCount,
}
due.ReplicationStats.ReplicaSize = v.ReplicationStats.ReplicaSize
}
cfg, err := getReplicationConfig(GlobalContext, d.Info.Name)
if err != nil {
return err
}
due.ReplicationStats.Targets[cfg.RoleArn] = replicationStats{
ReplicatedSize: v.ReplicationStats.ReplicatedSize,
FailedSize: v.ReplicationStats.FailedSize,
FailedCount: v.ReplicationStats.FailedCount,
PendingSize: v.ReplicationStats.PendingSize,
PendingCount: v.ReplicationStats.PendingCount,
}
due.ReplicationStats.ReplicaSize = v.ReplicationStats.ReplicaSize
}
due.Compacted = len(due.Children) == 0 && k != d.Info.Name
@@ -1040,7 +1155,6 @@ func (d *dataUsageCache) deserialize(r io.Reader) error {
d.Info = dold.Info
d.Disks = dold.Disks
d.Cache = make(map[string]dataUsageEntry, len(dold.Cache))
var arn string
for k, v := range dold.Cache {
due := dataUsageEntry{
Size: v.Size,
@@ -1049,21 +1163,14 @@ func (d *dataUsageCache) deserialize(r io.Reader) error {
Children: v.Children,
}
if v.ReplicationStats != nil && !v.ReplicationStats.Empty() {
if arn == "" {
cfg, err := getReplicationConfig(GlobalContext, d.Info.Name)
if err != nil {
return err
cfg, _ := getReplicationConfig(GlobalContext, d.Info.Name)
if cfg != nil && cfg.RoleArn != "" {
due.ReplicationStats = &replicationAllStats{
Targets: make(map[string]replicationStats),
}
d.Info.replication = replicationConfig{Config: cfg}
arn = d.Info.replication.Config.RoleArn
}
due.ReplicationStats = &replicationAllStats{
Targets: make(map[string]replicationStats),
}
if arn != "" {
due.ReplicationStats.Targets[arn] = replicationStats{
due.ReplicationStats.Targets[cfg.RoleArn] = replicationStats{
ReplicatedSize: v.ReplicationStats.ReplicatedSize,
FailedSize: v.ReplicationStats.FailedSize,
FailedCount: v.ReplicationStats.FailedCount,
@@ -1086,6 +1193,40 @@ func (d *dataUsageCache) deserialize(r io.Reader) error {
d.Cache[k] = e
}
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.Disks = dold.Disks
d.Cache = make(map[string]dataUsageEntry, len(dold.Cache))
for k, v := range dold.Cache {
var replicationStats *replicationAllStats
if v.ReplicationStats != nil {
replicationStats = &replicationAllStats{
Targets: v.ReplicationStats.Targets,
ReplicaSize: v.ReplicationStats.ReplicaSize,
}
}
due := dataUsageEntry{
Children: v.Children,
Size: v.Size,
Objects: v.Objects,
Versions: v.Versions,
ObjSizes: v.ObjSizes,
ReplicationStats: replicationStats,
Compacted: v.Compacted,
}
d.Cache[k] = due
}
return nil
case dataUsageCacheVerCurrent:
// Zstd compressed.
dec, err := zstd.NewReader(r, zstd.WithDecoderConcurrency(2))
+1353 -226
View File
File diff suppressed because it is too large Load Diff
+339 -113
View File
@@ -9,6 +9,119 @@ import (
"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 TestMarshalUnmarshaldataUsageCache(t *testing.T) {
v := dataUsageCache{}
bts, err := v.MarshalMsg(nil)
@@ -348,119 +461,6 @@ func BenchmarkDecodedataUsageEntry(b *testing.B) {
}
}
func TestMarshalUnmarshaldataUsageEntryV5(t *testing.T) {
v := dataUsageEntryV5{}
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 BenchmarkMarshalMsgdataUsageEntryV5(b *testing.B) {
v := dataUsageEntryV5{}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v.MarshalMsg(nil)
}
}
func BenchmarkAppendMsgdataUsageEntryV5(b *testing.B) {
v := dataUsageEntryV5{}
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 BenchmarkUnmarshaldataUsageEntryV5(b *testing.B) {
v := dataUsageEntryV5{}
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 TestEncodeDecodedataUsageEntryV5(t *testing.T) {
v := dataUsageEntryV5{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
m := v.Msgsize()
if buf.Len() > m {
t.Log("WARNING: TestEncodeDecodedataUsageEntryV5 Msgsize() is inaccurate")
}
vn := dataUsageEntryV5{}
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 BenchmarkEncodedataUsageEntryV5(b *testing.B) {
v := dataUsageEntryV5{}
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 BenchmarkDecodedataUsageEntryV5(b *testing.B) {
v := dataUsageEntryV5{}
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 TestMarshalUnmarshalreplicationAllStats(t *testing.T) {
v := replicationAllStats{}
bts, err := v.MarshalMsg(nil)
@@ -574,6 +574,119 @@ func BenchmarkDecodereplicationAllStats(b *testing.B) {
}
}
func TestMarshalUnmarshalreplicationAllStatsV1(t *testing.T) {
v := replicationAllStatsV1{}
bts, err := v.MarshalMsg(nil)
if err != nil {
t.Fatal(err)
}
left, err := v.UnmarshalMsg(bts)
if err != nil {
t.Fatal(err)
}
if len(left) > 0 {
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
}
left, err = msgp.Skip(bts)
if err != nil {
t.Fatal(err)
}
if len(left) > 0 {
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
}
}
func BenchmarkMarshalMsgreplicationAllStatsV1(b *testing.B) {
v := replicationAllStatsV1{}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v.MarshalMsg(nil)
}
}
func BenchmarkAppendMsgreplicationAllStatsV1(b *testing.B) {
v := replicationAllStatsV1{}
bts := make([]byte, 0, v.Msgsize())
bts, _ = v.MarshalMsg(bts[0:0])
b.SetBytes(int64(len(bts)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
bts, _ = v.MarshalMsg(bts[0:0])
}
}
func BenchmarkUnmarshalreplicationAllStatsV1(b *testing.B) {
v := replicationAllStatsV1{}
bts, _ := v.MarshalMsg(nil)
b.ReportAllocs()
b.SetBytes(int64(len(bts)))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := v.UnmarshalMsg(bts)
if err != nil {
b.Fatal(err)
}
}
}
func TestEncodeDecodereplicationAllStatsV1(t *testing.T) {
v := replicationAllStatsV1{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
m := v.Msgsize()
if buf.Len() > m {
t.Log("WARNING: TestEncodeDecodereplicationAllStatsV1 Msgsize() is inaccurate")
}
vn := replicationAllStatsV1{}
err := msgp.Decode(&buf, &vn)
if err != nil {
t.Error(err)
}
buf.Reset()
msgp.Encode(&buf, &v)
err = msgp.NewReader(&buf).Skip()
if err != nil {
t.Error(err)
}
}
func BenchmarkEncodereplicationAllStatsV1(b *testing.B) {
v := replicationAllStatsV1{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
b.SetBytes(int64(buf.Len()))
en := msgp.NewWriter(msgp.Nowhere)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v.EncodeMsg(en)
}
en.Flush()
}
func BenchmarkDecodereplicationAllStatsV1(b *testing.B) {
v := replicationAllStatsV1{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
b.SetBytes(int64(buf.Len()))
rd := msgp.NewEndlessReader(buf.Bytes(), b)
dc := msgp.NewReader(rd)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
err := v.DecodeMsg(dc)
if err != nil {
b.Fatal(err)
}
}
}
func TestMarshalUnmarshalreplicationStats(t *testing.T) {
v := replicationStats{}
bts, err := v.MarshalMsg(nil)
@@ -912,3 +1025,116 @@ func BenchmarkDecodesizeHistogram(b *testing.B) {
}
}
}
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)
}
}
}
+47 -1
View File
@@ -18,7 +18,10 @@
package cmd
import (
"sort"
"time"
"github.com/minio/madmin-go"
)
// BucketTargetUsageInfo - bucket target usage info provides
@@ -81,7 +84,50 @@ type DataUsageInfo struct {
// - 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 globalTierConfigMgr.Empty() {
return nil
}
ts := make(map[string]madmin.TierStats)
// Add configured remote tiers
for tier := range globalTierConfigMgr.Tiers {
ts[tier] = madmin.TierStats{}
}
// Add STANDARD (hot-tier)
ts[minioHotTier] = madmin.TierStats{}
ts = dui.TierStats.adminStats(ts)
infos := make([]madmin.TierInfo, 0, len(ts))
for tier, st := range ts {
var tierType string
if tier == minioHotTier {
tierType = "internal"
} else {
tierType = globalTierConfigMgr.Tiers[tier].Type.String()
}
infos = append(infos, madmin.TierInfo{
Name: tier,
Type: tierType,
Stats: st,
})
}
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
}
+21 -30
View File
@@ -18,13 +18,10 @@
package cmd
import (
"bytes"
"context"
"net/http"
"strings"
jsoniter "github.com/json-iterator/go"
"github.com/minio/minio/internal/hash"
"github.com/minio/minio/internal/logger"
)
@@ -32,28 +29,24 @@ const (
dataUsageRoot = SlashSeparator
dataUsageBucket = minioMetaBucket + SlashSeparator + bucketMetaPrefix
dataUsageObjName = ".usage.json"
dataUsageObjName = ".usage.json"
dataUsageObjNamePath = bucketMetaPrefix + SlashSeparator + dataUsageObjName
dataUsageBloomName = ".bloomcycle.bin"
dataUsageBloomNamePath = bucketMetaPrefix + SlashSeparator + dataUsageBloomName
dataUsageCacheName = ".usage-cache.bin"
dataUsageBloomName = ".bloomcycle.bin"
)
// storeDataUsageInBackend will store all objects sent on the gui channel until closed.
func storeDataUsageInBackend(ctx context.Context, objAPI ObjectLayer, dui <-chan DataUsageInfo) {
for dataUsageInfo := range dui {
var json = jsoniter.ConfigCompatibleWithStandardLibrary
json := jsoniter.ConfigCompatibleWithStandardLibrary
dataUsageJSON, err := json.Marshal(dataUsageInfo)
if err != nil {
logger.LogIf(ctx, err)
continue
}
size := int64(len(dataUsageJSON))
r, err := hash.NewReader(bytes.NewReader(dataUsageJSON), size, "", "", size)
if err != nil {
logger.LogIf(ctx, err)
continue
}
_, err = objAPI.PutObject(ctx, dataUsageBucket, dataUsageObjName, NewPutObjReader(r), ObjectOptions{})
if !isErrBucketNotFound(err) {
if err = saveConfig(ctx, objAPI, dataUsageObjNamePath, dataUsageJSON); err != nil {
logger.LogIf(ctx, err)
}
}
@@ -95,18 +88,17 @@ func loadPrefixUsageFromBackend(ctx context.Context, objAPI ObjectLayer, bucket
}
func loadDataUsageFromBackend(ctx context.Context, objAPI ObjectLayer) (DataUsageInfo, error) {
r, err := objAPI.GetObjectNInfo(ctx, dataUsageBucket, dataUsageObjName, nil, http.Header{}, readLock, ObjectOptions{})
buf, err := readConfig(ctx, objAPI, dataUsageObjNamePath)
if err != nil {
if isErrObjectNotFound(err) || isErrBucketNotFound(err) {
return DataUsageInfo{}, nil
}
return DataUsageInfo{}, toObjectErr(err, dataUsageBucket, dataUsageObjName)
return DataUsageInfo{}, toObjectErr(err, minioMetaBucket, dataUsageObjNamePath)
}
defer r.Close()
var dataUsageInfo DataUsageInfo
var json = jsoniter.ConfigCompatibleWithStandardLibrary
if err = json.NewDecoder(r).Decode(&dataUsageInfo); err != nil {
json := jsoniter.ConfigCompatibleWithStandardLibrary
if err = json.Unmarshal(buf, &dataUsageInfo); err != nil {
return DataUsageInfo{}, err
}
// For forward compatibility reasons, we need to add this code.
@@ -128,17 +120,16 @@ func loadDataUsageFromBackend(ctx context.Context, objAPI ObjectLayer) (DataUsag
for bucket, bui := range dataUsageInfo.BucketsUsage {
if bui.ReplicatedSizeV1 > 0 || bui.ReplicationFailedCountV1 > 0 ||
bui.ReplicationFailedSizeV1 > 0 || bui.ReplicationPendingCountV1 > 0 {
dataUsageInfo.ReplicationInfo = make(map[string]BucketTargetUsageInfo)
cfg, err := getReplicationConfig(GlobalContext, bucket)
if err != nil {
return DataUsageInfo{}, err
}
dataUsageInfo.ReplicationInfo[cfg.RoleArn] = BucketTargetUsageInfo{
ReplicationFailedSize: bui.ReplicationFailedSizeV1,
ReplicationFailedCount: bui.ReplicationFailedCountV1,
ReplicatedSize: bui.ReplicatedSizeV1,
ReplicationPendingCount: bui.ReplicationPendingCountV1,
ReplicationPendingSize: bui.ReplicationPendingSizeV1,
cfg, _ := getReplicationConfig(GlobalContext, bucket)
if cfg != nil && cfg.RoleArn != "" {
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,
}
}
}
}
+5 -5
View File
@@ -41,7 +41,7 @@ func TestDataUsageUpdate(t *testing.T) {
}
const bucket = "bucket"
defer os.RemoveAll(base)
var files = []usageTestFile{
files := []usageTestFile{
{name: "rootfile", size: 10000},
{name: "rootfile2", size: 10000},
{name: "dir1/d1file", size: 2000},
@@ -73,7 +73,7 @@ func TestDataUsageUpdate(t *testing.T) {
}
// Test dirs
var want = []struct {
want := []struct {
path string
isNil bool
size, objs int
@@ -257,7 +257,7 @@ func TestDataUsageUpdatePrefix(t *testing.T) {
}
scannerSleeper.Update(0, 0)
defer os.RemoveAll(base)
var files = []usageTestFile{
files := []usageTestFile{
{name: "bucket/rootfile", size: 10000},
{name: "bucket/rootfile2", size: 10000},
{name: "bucket/dir1/d1file", size: 2000},
@@ -302,7 +302,7 @@ func TestDataUsageUpdatePrefix(t *testing.T) {
}
// Test dirs
var want = []struct {
want := []struct {
path string
isNil bool
size, objs int
@@ -543,7 +543,7 @@ func TestDataUsageCacheSerialize(t *testing.T) {
}
const bucket = "abucket"
defer os.RemoveAll(base)
var files = []usageTestFile{
files := []usageTestFile{
{name: "rootfile", size: 10000},
{name: "rootfile2", size: 10000},
{name: "dir1/d1file", size: 2000},
+721 -100
View File
File diff suppressed because it is too large Load Diff
+91 -2
View File
@@ -110,10 +110,9 @@ func cacheControlOpts(o ObjectInfo) *cacheControl {
var headerVal string
for k, v := range m {
if strings.ToLower(k) == "cache-control" {
if strings.EqualFold(k, "cache-control") {
headerVal = v
}
}
if headerVal == "" {
return nil
@@ -246,6 +245,9 @@ func decryptCacheObjectETag(info *ObjectInfo) error {
if globalCacheKMS == nil {
return errKMSNotConfigured
}
if len(info.Parts) > 0 { // multipart ETag is not encrypted since it is not md5sum
return nil
}
keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(info.UserDefined)
if err != nil {
return err
@@ -271,6 +273,43 @@ func decryptCacheObjectETag(info *ObjectInfo) error {
return nil
}
// decryptCacheObjectETag tries to decrypt the ETag saved in encrypted format using the cache KMS
func decryptCachePartETags(c *cacheMeta) ([]string, error) {
var partETags []string
encrypted := crypto.S3.IsEncrypted(c.Meta) && isCacheEncrypted(c.Meta)
switch {
case encrypted:
if globalCacheKMS == nil {
return partETags, errKMSNotConfigured
}
keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(c.Meta)
if err != nil {
return partETags, err
}
extKey, err := globalCacheKMS.DecryptKey(keyID, kmsKey, kms.Context{c.Bucket: path.Join(c.Bucket, c.Object)})
if err != nil {
return partETags, err
}
var objectKey crypto.ObjectKey
if err = objectKey.Unseal(extKey, sealedKey, crypto.S3.String(), c.Bucket, c.Object); err != nil {
return partETags, err
}
for i := range c.PartETags {
etagStr := tryDecryptETag(objectKey[:], c.PartETags[i], false)
// backend ETag was hex encoded before encrypting, so hex decode to get actual ETag
etag, err := hex.DecodeString(etagStr)
if err != nil {
return []string{}, err
}
partETags = append(partETags, string(etag))
}
return partETags, nil
default:
return c.PartETags, nil
}
}
func isMetadataSame(m1, m2 map[string]string) bool {
if m1 == nil && m2 == nil {
return true
@@ -379,6 +418,9 @@ func (f *fileScorer) addFileWithObjInfo(objInfo ObjectInfo, hits int) {
// Returns true if there still is a need to delete files (n+saveBytes >0),
// false if no more bytes needs to be saved.
func (f *fileScorer) adjustSaveBytes(n int64) bool {
if f == nil {
return false
}
if int64(f.saveBytes)+n <= 0 {
f.saveBytes = 0
f.trimQueue()
@@ -506,3 +548,50 @@ func bytesToClear(total, free int64, quotaPct, lowWatermark, highWatermark uint6
lowWMUsage := total * (int64)(lowWatermark*quotaPct) / (100 * 100)
return (uint64)(math.Min(float64(quotaAllowed), math.Max(0.0, float64(used-lowWMUsage))))
}
type multiWriter struct {
backendWriter io.Writer
cacheWriter *io.PipeWriter
pipeClosed bool
}
// multiWriter writes to backend and cache - if cache write
// fails close the pipe, but continue writing to the backend
func (t *multiWriter) Write(p []byte) (n int, err error) {
n, err = t.backendWriter.Write(p)
if err == nil && n != len(p) {
err = io.ErrShortWrite
return
}
if err != nil {
if !t.pipeClosed {
t.cacheWriter.CloseWithError(err)
}
return
}
// ignore errors writing to cache
if !t.pipeClosed {
_, cerr := t.cacheWriter.Write(p)
if cerr != nil {
t.pipeClosed = true
t.cacheWriter.CloseWithError(cerr)
}
}
return len(p), nil
}
func cacheMultiWriter(w1 io.Writer, w2 *io.PipeWriter) io.Writer {
return &multiWriter{backendWriter: w1, cacheWriter: w2}
}
// writebackInProgress returns true if writeback commit is not complete
func writebackInProgress(m map[string]string) bool {
if v, ok := m[writeBackStatusHeader]; ok {
switch cacheCommitStatus(v) {
case CommitPending, CommitFailed:
return true
}
}
return false
}
+2 -2
View File
@@ -38,7 +38,7 @@ func TestGetCacheControlOpts(t *testing.T) {
{"max-age=2592000, no-store", timeSentinel, &cacheControl{maxAge: 2592000, sMaxAge: 0, noStore: true, minFresh: 0, expiry: time.Time{}}, false},
{"must-revalidate, max-age=600", timeSentinel, &cacheControl{maxAge: 600, sMaxAge: 0, minFresh: 0, expiry: time.Time{}}, false},
{"s-maxAge=2500, max-age=600", timeSentinel, &cacheControl{maxAge: 600, sMaxAge: 2500, minFresh: 0, expiry: time.Time{}}, false},
{"s-maxAge=2500, max-age=600", expiry, &cacheControl{maxAge: 600, sMaxAge: 2500, minFresh: 0, expiry: time.Date(2015, time.October, 21, 07, 28, 00, 00, time.UTC)}, false},
{"s-maxAge=2500, max-age=600", expiry, &cacheControl{maxAge: 600, sMaxAge: 2500, minFresh: 0, expiry: time.Date(2015, time.October, 21, 0o7, 28, 0o0, 0o0, time.UTC)}, false},
{"s-maxAge=2500, max-age=600s", timeSentinel, &cacheControl{maxAge: 600, sMaxAge: 2500, minFresh: 0, expiry: time.Time{}}, true},
}
@@ -61,7 +61,6 @@ func TestGetCacheControlOpts(t *testing.T) {
}
func TestIsMetadataSame(t *testing.T) {
testCases := []struct {
m1 map[string]string
m2 map[string]string
@@ -148,6 +147,7 @@ func TestNewFileScorer(t *testing.T) {
t.Fatal("unexpected file list", fs.queueString())
}
}
func TestBytesToClear(t *testing.T) {
testCases := []struct {
total int64
+380 -43
View File
@@ -59,6 +59,13 @@ const (
CommitFailed cacheCommitStatus = "failed"
)
const (
// CommitWriteBack allows staging and write back of cached content for single object uploads
CommitWriteBack string = "writeback"
// CommitWriteThrough allows caching multipart uploads to disk synchronously
CommitWriteThrough string = "writethrough"
)
// String returns string representation of status
func (s cacheCommitStatus) String() string {
return string(s)
@@ -80,6 +87,13 @@ type CacheObjectLayer interface {
DeleteObjects(ctx context.Context, bucket string, objects []ObjectToDelete, opts ObjectOptions) ([]DeletedObject, []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)
// Multipart operations.
NewMultipartUpload(ctx context.Context, bucket, object string, opts ObjectOptions) (uploadID string, err error)
PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, data *PutObjReader, opts ObjectOptions) (info PartInfo, 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)
CopyObjectPart(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject, uploadID string, partID int, startOffset int64, length int64, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (pi PartInfo, e error)
// Storage operations.
StorageInfo(ctx context.Context) CacheStorageInfo
CacheStats() *CacheStats
@@ -94,31 +108,35 @@ type cacheObjects struct {
// number of accesses after which to cache an object
after int
// commit objects in async manner
commitWriteback bool
commitWriteback bool
commitWritethrough bool
// if true migration is in progress from v1 to v2
migrating bool
// mutex to protect migration bool
migMutex sync.Mutex
// retry queue for writeback cache mode to reattempt upload to backend
wbRetryCh chan ObjectInfo
// Cache stats
cacheStats *CacheStats
InnerGetObjectNInfoFn func(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error)
InnerGetObjectInfoFn func(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
InnerDeleteObjectFn func(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
InnerPutObjectFn func(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error)
InnerCopyObjectFn func(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error)
InnerGetObjectNInfoFn func(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error)
InnerGetObjectInfoFn func(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
InnerDeleteObjectFn func(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
InnerPutObjectFn func(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error)
InnerCopyObjectFn func(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error)
InnerNewMultipartUploadFn func(ctx context.Context, bucket, object string, opts ObjectOptions) (uploadID string, err error)
InnerPutObjectPartFn func(ctx context.Context, bucket, object, uploadID string, partID int, data *PutObjReader, opts ObjectOptions) (info PartInfo, err error)
InnerAbortMultipartUploadFn func(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) error
InnerCompleteMultipartUploadFn func(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart, opts ObjectOptions) (objInfo ObjectInfo, err error)
InnerCopyObjectPartFn func(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject, uploadID string, partID int, startOffset int64, length int64, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (pi PartInfo, e error)
}
func (c *cacheObjects) incHitsToMeta(ctx context.Context, dcache *diskCache, bucket, object string, size int64, eTag string, rs *HTTPRangeSpec) error {
metadata := map[string]string{"etag": eTag}
return dcache.SaveMetadata(ctx, bucket, object, metadata, size, rs, "", true)
return dcache.SaveMetadata(ctx, bucket, object, metadata, size, rs, "", true, false)
}
// Backend metadata could have changed through server side copy - reset cache metadata if that is the case
func (c *cacheObjects) updateMetadataIfChanged(ctx context.Context, dcache *diskCache, bucket, object string, bkObjectInfo, cacheObjInfo ObjectInfo, rs *HTTPRangeSpec) error {
bkMeta := make(map[string]string, len(bkObjectInfo.UserDefined))
cacheMeta := make(map[string]string, len(cacheObjInfo.UserDefined))
for k, v := range bkObjectInfo.UserDefined {
@@ -140,7 +158,7 @@ func (c *cacheObjects) updateMetadataIfChanged(ctx context.Context, dcache *disk
bkObjectInfo.ETag != cacheObjInfo.ETag ||
bkObjectInfo.ContentType != cacheObjInfo.ContentType ||
!bkObjectInfo.Expires.Equal(cacheObjInfo.Expires) {
return dcache.SaveMetadata(ctx, bucket, object, getMetadata(bkObjectInfo), bkObjectInfo.Size, nil, "", false)
return dcache.SaveMetadata(ctx, bucket, object, getMetadata(bkObjectInfo), bkObjectInfo.Size, nil, "", false, false)
}
return c.incHitsToMeta(ctx, dcache, bucket, object, cacheObjInfo.Size, cacheObjInfo.ETag, rs)
}
@@ -257,6 +275,10 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
bReader.ObjInfo.CacheStatus = CacheMiss
return bReader, err
}
// serve cached content without ETag verification if writeback commit is not yet complete
if writebackInProgress(cacheReader.ObjInfo.UserDefined) {
return cacheReader, nil
}
}
objInfo, err := c.InnerGetObjectInfoFn(ctx, bucket, object, opts)
@@ -349,7 +371,7 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
// use a new context to avoid locker prematurely timing out operation when the GetObjectNInfo returns.
dcache.Put(GlobalContext, bucket, object, bReader, bReader.ObjInfo.Size, rs, ObjectOptions{
UserDefined: getMetadata(bReader.ObjInfo),
}, false)
}, false, false)
return
}
}()
@@ -367,7 +389,7 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
io.LimitReader(pr, bkReader.ObjInfo.Size),
bkReader.ObjInfo.Size, rs, ObjectOptions{
UserDefined: userDefined,
}, false)
}, false, false)
// close the read end of the pipe, so the error gets
// propagated to teeReader
pr.CloseWithError(putErr)
@@ -403,6 +425,10 @@ func (c *cacheObjects) GetObjectInfo(ctx context.Context, bucket, object string,
c.cacheStats.incHit()
return cachedObjInfo, nil
}
// serve cache metadata without ETag verification if writeback commit is not yet complete
if writebackInProgress(cachedObjInfo.UserDefined) {
return cachedObjInfo, nil
}
}
objInfo, err := getObjectInfoFn(ctx, bucket, object, opts)
@@ -488,8 +514,6 @@ func (c *cacheObjects) CacheStats() (cs *CacheStats) {
// skipCache() returns true if cache migration is in progress
func (c *cacheObjects) skipCache() bool {
c.migMutex.Lock()
defer c.migMutex.Unlock()
return c.migrating
}
@@ -619,8 +643,6 @@ func (c *cacheObjects) migrateCacheFromV1toV2(ctx context.Context) {
}
// update migration status
c.migMutex.Lock()
defer c.migMutex.Unlock()
c.migrating = false
logStartupMessage(color.Blue("Cache migration completed successfully."))
}
@@ -663,31 +685,82 @@ func (c *cacheObjects) PutObject(ctx context.Context, bucket, object string, r *
return putObjectFn(ctx, bucket, object, r, opts)
}
if c.commitWriteback {
oi, err := dcache.Put(ctx, bucket, object, r, r.Size(), nil, opts, false)
oi, err := dcache.Put(ctx, bucket, object, r, r.Size(), nil, opts, false, true)
if err != nil {
return ObjectInfo{}, err
}
go c.uploadObject(GlobalContext, oi)
return oi, nil
}
objInfo, err = putObjectFn(ctx, bucket, object, r, opts)
if err == nil {
go func() {
// fill cache in the background
bReader, bErr := c.InnerGetObjectNInfoFn(GlobalContext, bucket, object, nil, http.Header{}, readLock, ObjectOptions{})
if bErr != nil {
return
}
defer bReader.Close()
oi, _, err := dcache.Stat(GlobalContext, bucket, object)
// avoid cache overwrite if another background routine filled cache
if err != nil || oi.ETag != bReader.ObjInfo.ETag {
dcache.Put(GlobalContext, bucket, object, bReader, bReader.ObjInfo.Size, nil, ObjectOptions{UserDefined: getMetadata(bReader.ObjInfo)}, false)
}
}()
if !c.commitWritethrough {
objInfo, err = putObjectFn(ctx, bucket, object, r, opts)
if err == nil {
go func() {
// fill cache in the background
bReader, bErr := c.InnerGetObjectNInfoFn(GlobalContext, bucket, object, nil, http.Header{}, readLock, ObjectOptions{})
if bErr != nil {
return
}
defer bReader.Close()
oi, _, err := dcache.Stat(GlobalContext, bucket, object)
// avoid cache overwrite if another background routine filled cache
if err != nil || oi.ETag != bReader.ObjInfo.ETag {
dcache.Put(GlobalContext, bucket, object, bReader, bReader.ObjInfo.Size, nil, ObjectOptions{UserDefined: getMetadata(bReader.ObjInfo)}, false, false)
}
}()
}
return objInfo, err
}
return objInfo, err
cLock, lkctx, cerr := dcache.GetLockContext(GlobalContext, bucket, object)
if cerr != nil {
return putObjectFn(ctx, bucket, object, r, opts)
}
defer cLock.Unlock(lkctx.Cancel)
// Initialize pipe to stream data to backend
pipeReader, pipeWriter := io.Pipe()
hashReader, err := hash.NewReader(pipeReader, size, "", "", r.ActualSize())
if err != nil {
return
}
// Initialize pipe to stream data to cache
rPipe, wPipe := io.Pipe()
infoCh := make(chan ObjectInfo)
errorCh := make(chan error)
go func() {
info, err := putObjectFn(ctx, bucket, object, NewPutObjReader(hashReader), opts)
if err != nil {
close(infoCh)
pipeReader.CloseWithError(err)
rPipe.CloseWithError(err)
errorCh <- err
return
}
close(errorCh)
infoCh <- info
}()
go func() {
_, err := dcache.put(lkctx.Context(), bucket, object, rPipe, r.Size(), nil, opts, false, false)
if err != nil {
rPipe.CloseWithError(err)
return
}
}()
mwriter := cacheMultiWriter(pipeWriter, wPipe)
_, err = io.Copy(mwriter, r)
pipeWriter.Close()
wPipe.Close()
if err != nil {
err = <-errorCh
return ObjectInfo{}, err
}
info := <-infoCh
if cerr = dcache.updateMetadata(lkctx.Context(), bucket, object, info.ETag, info.ModTime, info.Size); cerr != nil {
dcache.delete(bucket, object)
}
return info, err
}
// upload cached object to backend in async commit mode.
@@ -720,6 +793,7 @@ func (c *cacheObjects) uploadObject(ctx context.Context, oi ObjectInfo) {
opts.UserDefined[xhttp.ContentMD5] = oi.UserDefined["content-md5"]
objInfo, err := c.InnerPutObjectFn(ctx, oi.Bucket, oi.Name, NewPutObjReader(hashReader), opts)
wbCommitStatus := CommitComplete
size := objInfo.Size
if err != nil {
wbCommitStatus = CommitFailed
}
@@ -730,12 +804,13 @@ func (c *cacheObjects) uploadObject(ctx context.Context, oi ObjectInfo) {
retryCnt, _ = strconv.Atoi(meta[writeBackRetryHeader])
retryCnt++
meta[writeBackRetryHeader] = strconv.Itoa(retryCnt)
size = cReader.ObjInfo.Size
} else {
delete(meta, writeBackRetryHeader)
}
meta[writeBackStatusHeader] = wbCommitStatus.String()
meta["etag"] = oi.ETag
dcache.SaveMetadata(ctx, oi.Bucket, oi.Name, meta, objInfo.Size, nil, "", false)
dcache.SaveMetadata(ctx, oi.Bucket, oi.Name, meta, size, nil, "", false, wbCommitStatus == CommitComplete)
if retryCnt > 0 {
// slow down retries
time.Sleep(time.Second * time.Duration(retryCnt%10+1))
@@ -745,6 +820,8 @@ func (c *cacheObjects) uploadObject(ctx context.Context, oi ObjectInfo) {
func (c *cacheObjects) queueWritebackRetry(oi ObjectInfo) {
select {
case <-GlobalContext.Done():
return
case c.wbRetryCh <- oi:
c.uploadObject(GlobalContext, oi)
default:
@@ -759,13 +836,14 @@ func newServerCacheObjects(ctx context.Context, config cache.Config) (CacheObjec
return nil, err
}
c := &cacheObjects{
cache: cache,
exclude: config.Exclude,
after: config.After,
migrating: migrateSw,
migMutex: sync.Mutex{},
commitWriteback: config.CommitWriteback,
cacheStats: newCacheStats(),
cache: cache,
exclude: config.Exclude,
after: config.After,
migrating: migrateSw,
commitWriteback: config.CacheCommitMode == CommitWriteBack,
commitWritethrough: config.CacheCommitMode == CommitWriteThrough,
cacheStats: newCacheStats(),
InnerGetObjectInfoFn: func(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
return newObjectLayerFn().GetObjectInfo(ctx, bucket, object, opts)
},
@@ -781,6 +859,21 @@ func newServerCacheObjects(ctx context.Context, config cache.Config) (CacheObjec
InnerCopyObjectFn: func(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error) {
return newObjectLayerFn().CopyObject(ctx, srcBucket, srcObject, destBucket, destObject, srcInfo, srcOpts, dstOpts)
},
InnerNewMultipartUploadFn: func(ctx context.Context, bucket, object string, opts ObjectOptions) (uploadID string, err error) {
return newObjectLayerFn().NewMultipartUpload(ctx, bucket, object, opts)
},
InnerPutObjectPartFn: func(ctx context.Context, bucket, object, uploadID string, partID int, data *PutObjReader, opts ObjectOptions) (info PartInfo, err error) {
return newObjectLayerFn().PutObjectPart(ctx, bucket, object, uploadID, partID, data, opts)
},
InnerAbortMultipartUploadFn: func(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) error {
return newObjectLayerFn().AbortMultipartUpload(ctx, bucket, object, uploadID, opts)
},
InnerCompleteMultipartUploadFn: func(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart, opts ObjectOptions) (objInfo ObjectInfo, err error) {
return newObjectLayerFn().CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts)
},
InnerCopyObjectPartFn: func(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject, uploadID string, partID int, startOffset int64, length int64, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (pi PartInfo, e error) {
return newObjectLayerFn().CopyObjectPart(ctx, srcBucket, srcObject, dstBucket, dstObject, uploadID, partID, startOffset, length, srcInfo, srcOpts, dstOpts)
},
}
c.cacheStats.GetDiskStats = func() []CacheDiskStats {
cacheDiskStats := make([]CacheDiskStats, len(c.cache))
@@ -859,3 +952,247 @@ func (c *cacheObjects) queuePendingWriteback(ctx context.Context) {
}
}
}
// NewMultipartUpload - Starts a new multipart upload operation to backend - if writethrough mode is enabled, starts caching the multipart.
func (c *cacheObjects) NewMultipartUpload(ctx context.Context, bucket, object string, opts ObjectOptions) (uploadID string, err error) {
newMultipartUploadFn := c.InnerNewMultipartUploadFn
dcache, err := c.getCacheToLoc(ctx, bucket, object)
if err != nil {
// disk cache could not be located,execute backend call.
return newMultipartUploadFn(ctx, bucket, object, opts)
}
if c.skipCache() {
return newMultipartUploadFn(ctx, bucket, object, opts)
}
if opts.ServerSideEncryption != nil { // avoid caching encrypted objects
dcache.Delete(ctx, bucket, object)
return newMultipartUploadFn(ctx, bucket, object, opts)
}
// skip cache for objects with locks
objRetention := objectlock.GetObjectRetentionMeta(opts.UserDefined)
legalHold := objectlock.GetObjectLegalHoldMeta(opts.UserDefined)
if objRetention.Mode.Valid() || legalHold.Status.Valid() {
dcache.Delete(ctx, bucket, object)
return newMultipartUploadFn(ctx, bucket, object, opts)
}
// fetch from backend if cache exclude pattern or cache-control
// directive set to exclude
if c.isCacheExclude(bucket, object) {
dcache.Delete(ctx, bucket, object)
return newMultipartUploadFn(ctx, bucket, object, opts)
}
if !c.commitWritethrough && !c.commitWriteback {
return newMultipartUploadFn(ctx, bucket, object, opts)
}
// perform multipart upload on backend and cache simultaneously
uploadID, err = newMultipartUploadFn(ctx, bucket, object, opts)
dcache.NewMultipartUpload(GlobalContext, bucket, object, uploadID, opts)
return uploadID, err
}
// PutObjectPart streams part to cache concurrently if writethrough mode is enabled. Otherwise redirects the call to remote
func (c *cacheObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, data *PutObjReader, opts ObjectOptions) (info PartInfo, err error) {
putObjectPartFn := c.InnerPutObjectPartFn
dcache, err := c.getCacheToLoc(ctx, bucket, object)
if err != nil {
// disk cache could not be located,execute backend call.
return putObjectPartFn(ctx, bucket, object, uploadID, partID, data, opts)
}
if !c.commitWritethrough && !c.commitWriteback {
return putObjectPartFn(ctx, bucket, object, uploadID, partID, data, opts)
}
if c.skipCache() {
return putObjectPartFn(ctx, bucket, object, uploadID, partID, data, opts)
}
size := data.Size()
// avoid caching part if space unavailable
if !dcache.diskSpaceAvailable(size) {
return putObjectPartFn(ctx, bucket, object, uploadID, partID, data, opts)
}
if opts.ServerSideEncryption != nil {
dcache.Delete(ctx, bucket, object)
return putObjectPartFn(ctx, bucket, object, uploadID, partID, data, opts)
}
// skip cache for objects with locks
objRetention := objectlock.GetObjectRetentionMeta(opts.UserDefined)
legalHold := objectlock.GetObjectLegalHoldMeta(opts.UserDefined)
if objRetention.Mode.Valid() || legalHold.Status.Valid() {
dcache.Delete(ctx, bucket, object)
return putObjectPartFn(ctx, bucket, object, uploadID, partID, data, opts)
}
// fetch from backend if cache exclude pattern or cache-control
// directive set to exclude
if c.isCacheExclude(bucket, object) {
dcache.Delete(ctx, bucket, object)
return putObjectPartFn(ctx, bucket, object, uploadID, partID, data, opts)
}
info = PartInfo{}
// Initialize pipe to stream data to backend
pipeReader, pipeWriter := io.Pipe()
hashReader, err := hash.NewReader(pipeReader, size, "", "", data.ActualSize())
if err != nil {
return
}
// Initialize pipe to stream data to cache
rPipe, wPipe := io.Pipe()
pinfoCh := make(chan PartInfo)
cinfoCh := make(chan PartInfo)
errorCh := make(chan error)
go func() {
info, err = putObjectPartFn(ctx, bucket, object, uploadID, partID, NewPutObjReader(hashReader), opts)
if err != nil {
close(pinfoCh)
pipeReader.CloseWithError(err)
rPipe.CloseWithError(err)
errorCh <- err
return
}
close(errorCh)
pinfoCh <- info
}()
go func() {
pinfo, perr := dcache.PutObjectPart(GlobalContext, bucket, object, uploadID, partID, rPipe, data.Size(), opts)
if perr != nil {
rPipe.CloseWithError(perr)
close(cinfoCh)
// clean up upload
dcache.AbortUpload(bucket, object, uploadID)
return
}
cinfoCh <- pinfo
}()
mwriter := cacheMultiWriter(pipeWriter, wPipe)
_, err = io.Copy(mwriter, data)
pipeWriter.Close()
wPipe.Close()
if err != nil {
err = <-errorCh
return PartInfo{}, err
}
info = <-pinfoCh
cachedInfo := <-cinfoCh
if info.PartNumber == cachedInfo.PartNumber {
cachedInfo.ETag = info.ETag
cachedInfo.LastModified = info.LastModified
dcache.SavePartMetadata(GlobalContext, bucket, object, uploadID, partID, cachedInfo)
}
return info, err
}
// CopyObjectPart behaves similar to PutObjectPart - caches part to upload dir if writethrough mode is enabled.
func (c *cacheObjects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject, uploadID string, partID int, startOffset int64, length int64, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (pi PartInfo, e error) {
copyObjectPartFn := c.InnerCopyObjectPartFn
dcache, err := c.getCacheToLoc(ctx, dstBucket, dstObject)
if err != nil {
// disk cache could not be located,execute backend call.
return copyObjectPartFn(ctx, srcBucket, srcObject, dstBucket, dstObject, uploadID, partID, startOffset, length, srcInfo, srcOpts, dstOpts)
}
if !c.commitWritethrough && !c.commitWriteback {
return copyObjectPartFn(ctx, srcBucket, srcObject, dstBucket, dstObject, uploadID, partID, startOffset, length, srcInfo, srcOpts, dstOpts)
}
if err := dcache.uploadIDExists(dstBucket, dstObject, uploadID); err != nil {
return copyObjectPartFn(ctx, srcBucket, srcObject, dstBucket, dstObject, uploadID, partID, startOffset, length, srcInfo, srcOpts, dstOpts)
}
partInfo, err := copyObjectPartFn(ctx, srcBucket, srcObject, dstBucket, dstObject, uploadID, partID, startOffset, length, srcInfo, srcOpts, dstOpts)
if err != nil {
return pi, toObjectErr(err, dstBucket, dstObject)
}
go func() {
isSuffixLength := false
if startOffset < 0 {
isSuffixLength = true
}
rs := &HTTPRangeSpec{
IsSuffixLength: isSuffixLength,
Start: startOffset,
End: startOffset + length,
}
// fill cache in the background
bReader, bErr := c.InnerGetObjectNInfoFn(GlobalContext, srcBucket, srcObject, rs, http.Header{}, readLock, ObjectOptions{})
if bErr != nil {
return
}
defer bReader.Close()
// avoid cache overwrite if another background routine filled cache
dcache.PutObjectPart(GlobalContext, dstBucket, dstObject, uploadID, partID, bReader, length, ObjectOptions{UserDefined: getMetadata(bReader.ObjInfo)})
}()
// Success.
return partInfo, nil
}
// CompleteMultipartUpload - completes multipart upload operation on the backend. If writethrough mode is enabled, this also
// finalizes the upload saved in cache multipart dir.
func (c *cacheObjects) CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart, opts ObjectOptions) (oi ObjectInfo, err error) {
completeMultipartUploadFn := c.InnerCompleteMultipartUploadFn
if !c.commitWritethrough && !c.commitWriteback {
return completeMultipartUploadFn(ctx, bucket, object, uploadID, uploadedParts, opts)
}
dcache, err := c.getCacheToLoc(ctx, bucket, object)
if err != nil {
// disk cache could not be located,execute backend call.
return completeMultipartUploadFn(ctx, bucket, object, uploadID, uploadedParts, opts)
}
// perform multipart upload on backend and cache simultaneously
oi, err = completeMultipartUploadFn(ctx, bucket, object, uploadID, uploadedParts, opts)
if err == nil {
// fill cache in the background
go func() {
_, err := dcache.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, oi, opts)
if err != nil {
// fill cache in the background
bReader, bErr := c.InnerGetObjectNInfoFn(GlobalContext, bucket, object, nil, http.Header{}, readLock, ObjectOptions{})
if bErr != nil {
return
}
defer bReader.Close()
oi, _, err := dcache.Stat(GlobalContext, bucket, object)
// avoid cache overwrite if another background routine filled cache
if err != nil || oi.ETag != bReader.ObjInfo.ETag {
dcache.Put(GlobalContext, bucket, object, bReader, bReader.ObjInfo.Size, nil, ObjectOptions{UserDefined: getMetadata(bReader.ObjInfo)}, false, false)
}
}
}()
}
return
}
// AbortMultipartUpload - aborts multipart upload on backend and cache.
func (c *cacheObjects) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) error {
abortMultipartUploadFn := c.InnerAbortMultipartUploadFn
if !c.commitWritethrough && !c.commitWriteback {
return abortMultipartUploadFn(ctx, bucket, object, uploadID, opts)
}
dcache, err := c.getCacheToLoc(ctx, bucket, object)
if err != nil {
// disk cache could not be located,execute backend call.
return abortMultipartUploadFn(ctx, bucket, object, uploadID, opts)
}
if err = dcache.uploadIDExists(bucket, object, uploadID); err != nil {
return toObjectErr(err, bucket, object, uploadID)
}
// execute backend operation
err = abortMultipartUploadFn(ctx, bucket, object, uploadID, opts)
if err != nil {
return err
}
// abort multipart upload on cache
go dcache.AbortUpload(bucket, object, uploadID)
return nil
}
+1 -1
View File
@@ -25,7 +25,7 @@ import (
// Tests ToObjectInfo function.
func TestCacheMetadataObjInfo(t *testing.T) {
m := cacheMeta{Meta: nil}
objInfo := m.ToObjectInfo("testbucket", "testobject")
objInfo := m.ToObjectInfo()
if objInfo.Size != 0 {
t.Fatal("Unexpected object info value for Size", objInfo.Size)
}
+1 -1
View File
@@ -61,7 +61,7 @@ func NewDummyDataGen(totalLength, skipOffset int64) io.ReadSeeker {
panic("Negative rotations are not allowed")
}
skipOffset = skipOffset % int64(len(alphabets))
skipOffset %= int64(len(alphabets))
as := make([]byte, 2*len(alphabets))
copy(as, alphabets)
copy(as[len(alphabets):], alphabets)
-1
View File
@@ -164,7 +164,6 @@ func (api objectAPIHandlers) GetBucketLoggingHandler(w http.ResponseWriter, r *h
// DeleteBucketWebsiteHandler - DELETE bucket website, a dummy api
func (api objectAPIHandlers) DeleteBucketWebsiteHandler(w http.ResponseWriter, r *http.Request) {
writeSuccessResponseHeadersOnly(w)
w.(http.Flusher).Flush()
}
// GetBucketCorsHandler - GET bucket cors, a dummy api

Some files were not shown because too many files have changed in this diff Show More