Compare commits

..

185 Commits

Author SHA1 Message Date
Poorna Krishnamoorthy 878bc6c72b Redact LDAP password if any in request trace (#11750)
Fixes: #11742
2021-03-09 14:43:16 -08:00
Klaus Post fdc2f69218 truncate xl.meta files upon rewrites #11749)
If the destination files exist and is larger - junk data will be left at the end of the file.
2021-03-09 14:42:24 -08:00
Anis Elleuch 0d124095ea lc: Return expiration header only when version id is unspecified (#11718)
Follow S3 specification to return Expiration header in HEAD/GET call
only when version-id is not passed in the request.
2021-03-09 13:19:08 -08:00
Harshavardhana 691035832a fix: normalize object layer inputs (#11534)
Cases where we have applications making request
for `//` in object names make sure that all
are normalized to `/` and all such requests that
are prefixed '/' are removed. To ensure a
consistent view from all operations.
2021-03-09 12:58:22 -08:00
Anis Elleuch eac66e67ec Use maximum parity for config files (#11740)
Some deployments have low parity (EC:2), but we really do not need to
save our config data with the same parity configuration.

N/2 would be better to keep MinIO configurations intact when unexpected
a number of drives fail.
2021-03-09 10:19:47 -08:00
Anis Elleuch 57f3ed22d4 erasure: Reduce the interval of cleaning up .trash folder (#11741)
Reduce from 30 to 10 minutes.
2021-03-09 09:45:38 -08:00
Poorna Krishnamoorthy 2f29719e6b resize replication worker pool dynamically after config update (#11737) 2021-03-09 02:56:42 -08:00
Andreas Auernhammer 209fe61dcc vault: disable Hashicorp Vault with opt-in (#11711)
This commit disables the Hashicorp Vault
support but provides a way to temp. enable
it via the `MINIO_KMS_VAULT_DEPRECATION=off`

Vault support has been deprecated long ago
and this commit just requires users to take
action if they maintain a Vault integration.
2021-03-09 00:02:35 -08:00
Harshavardhana 8ecffdb7a7 Revert "Revert "heal: Heal bucket metadata when a fresh disk is inserted (#11734)""
This reverts commit 806df164b2.
2021-03-08 16:12:17 -08:00
Harshavardhana 806df164b2 Revert "heal: Heal bucket metadata when a fresh disk is inserted (#11734)"
This reverts commit 64662a49ff.
2021-03-08 14:43:24 -08:00
Klaus Post 4ac9ed4248 CopyObject: Do not remove crypto info when compressed (#11702)
Removing crypto info makes it impossible to copy encrypted+compressed objects.

Disable destination compression when encrypted.
2021-03-08 12:57:54 -08:00
Harshavardhana 3d8c512bba update browser package.json 2021-03-08 11:35:37 -08:00
Klaus Post 3ff5f55dcb Fetch fileinfo concurrently (#11700)
For non-erasure setups fetch up to 10 fileinfos concurrently.

Fixes #11625
2021-03-08 11:30:43 -08:00
Max Xu 097e5eba9f feat: remove go-bindata-assetfs in favor of embed by upgrading to go1.16 (#11733) 2021-03-08 11:26:43 -08:00
Andreas Auernhammer ba6930bb13 fips: always enable AES in FIPS mode when using madmin (#11732)
This commit adds FIPS-specifc build tags to the madmin
package. When madmin is compiled with `--tags "fips"`
it will always use AES-GCM for encryption - not just
when an optimized AES implementation is available.
2021-03-08 10:58:02 -08:00
Anis Elleuch 64662a49ff heal: Heal bucket metadata when a fresh disk is inserted (#11734)
Replacing disk with a fresh one never heals bucket metadata (policy,
notification, etc..). This commit fixes the issue.
2021-03-08 10:54:13 -08:00
Harshavardhana 78e867e145 ignore healing .trash, .metacache amd .multipart paths (#11725) 2021-03-07 09:38:31 -08:00
Harshavardhana 9ccc483df6 [feat]: change erasure coding default block size from 10MiB to 1MiB (#11721)
major performance improvements in range GETs to avoid large
read amplification when ranges are tiny and random

```
-------------------
Operation: GET
Operations: 142014 -> 339421
Duration: 4m50s -> 4m56s
* Average: +139.41% (+1177.3 MiB/s) throughput, +139.11% (+658.4) obj/s
* Fastest: +125.24% (+1207.4 MiB/s) throughput, +132.32% (+612.9) obj/s
* 50% Median: +139.06% (+1175.7 MiB/s) throughput, +133.46% (+660.9) obj/s
* Slowest: +203.40% (+1267.9 MiB/s) throughput, +198.59% (+753.5) obj/s
```

TTFB from 10MiB BlockSize
```
* First Access TTFB: Avg: 81ms, Median: 61ms, Best: 20ms, Worst: 2.056s
```

TTFB from 1MiB BlockSize
```
* First Access TTFB: Avg: 22ms, Median: 21ms, Best: 8ms, Worst: 91ms
```

Full object reads however do see a slight change which won't be
noticeable in real world, so not doing any comparisons

TTFB still had improvements with full object reads with 1MiB

```
* First Access TTFB: Avg: 68ms, Median: 35ms, Best: 11ms, Worst: 1.16s
```

v/s

TTFB with 10MiB
```
* First Access TTFB: Avg: 388ms, Median: 98ms, Best: 20ms, Worst: 4.156s
```

This change should affect all new uploads, previous uploads should
continue to work with business as usual. But dramatic improvements can
be seen with these changes.
2021-03-06 14:09:34 -08:00
Anis Elleuch abce040088 fix: Remove repetitive IAM ready message (#11723)
"IAM initialization complete" is printed each 5 minutes, avoid this by
printing it only during the first initialization of IAM.
2021-03-06 09:27:46 -08:00
Anis Elleuch 558762bdf6 iam: Return a slice of policies for a group (#11722)
A group can have multiple policies, a user subscribed to readwrite &
diagnostics can perform S3 operations & admin operations as well.
However, the current code only returns one policy for one group.
2021-03-06 09:27:06 -08:00
Harshavardhana d971061305 use listPathRaw for HealObjects() instead of expensive WalkVersions() (#11675) 2021-03-06 09:25:48 -08:00
Andreas Auernhammer 509bcc01ad fips: do not use SHA-3 when building a FIPS-140 2 binary (#11710)
This commit disables SHA-3 for OpenID when building a
FIPS-140 2 compatible binary. While SHA-3 is a
crypto. hash function accepted by NIST there is no
FIPS-140 2 compliant implementation available when
using the boringcrypto Go branch.

Therefore, SHA-3 must not be used when building
a FIPS-140 2 binary.
2021-03-05 20:43:42 -08:00
Anis Elleuch 7ea95fcec8 Add mint versioning tests (#11500)
Co-authored-by: Harshavardhana <harsha@minio.io>
2021-03-05 19:15:42 -08:00
Krishnan Parthasarathi 79b0d056a2 lifecycle: don't transition delete markers (#11692) 2021-03-05 15:29:27 -08:00
Harshavardhana ec547c0fa8 enable race detector CI for macos-latest (#11715) 2021-03-05 14:16:23 -08:00
Krishnan Parthasarathi bcf9825082 Data usage should account for transitioned objects (#11717) 2021-03-05 14:15:53 -08:00
Harshavardhana 651487507a fix: Merge() should merge and return a copy (#11714)
fixes #11713
2021-03-05 09:42:46 -08:00
sgandon 124816f6a6 fix : IAM Intialization failing with a large number of users/policies (#11701) 2021-03-05 08:36:16 -08:00
Klaus Post fa9cf1251b Imporve healing and reporting (#11312)
* Provide information on *actively* healing, buckets healed/queued, objects healed/failed.
* Add concurrent healing of multiple sets (typically on startup).
* Add bucket level resume, so restarts will only heal non-healed buckets.
* Print summary after healing a disk is done.
2021-03-04 14:36:23 -08:00
Harshavardhana 97e7a902d0 fix: shellcheck mint shellscript 2021-03-04 14:28:13 -08:00
Harshavardhana d73d756a80 fix: incorrect errors thrown by lint (#11699)
fixes #11698
2021-03-04 14:27:38 -08:00
Aditya Manthramurthy 7488c77e7c Test LDAP connection configuration at startup (#11684) 2021-03-04 12:17:36 -08:00
Harshavardhana 786585009e fix: capture disks when entire peer is offline (#11697)
currently when one of the peer is down, the
drives from that peer are reported as '0/0'
offline instead we should capture/filter the
drives from the peer and populate it appropriately
such that `mc admin info` displays correct info.
2021-03-04 10:07:05 -08:00
Anis Elleuch 7be7109471 locking: Add Refresh for better locking cleanup (#11535)
Co-authored-by: Anis Elleuch <anis@min.io>
Co-authored-by: Harshavardhana <harsha@minio.io>
2021-03-03 18:36:43 -08:00
Minio Trusted 464fa08f2e Update yaml files to latest version RELEASE.2021-03-04T00-53-13Z 2021-03-04 01:15:49 +00:00
Klaus Post c3217bd6eb Use actual size for buffer selection (#11687)
For compressed inputs, this will be -1, but the object may be small.
2021-03-03 16:28:10 -08:00
Andreas Auernhammer f14cc6c943 etag: add FromContentMD5 to parse content-md5 as ETag (#11688)
This commit adds the `FromContentMD5` function to
parse a client-provided content-md5 as ETag.

Further, it also adds multipart ETag computation
for future needs.
2021-03-03 12:58:28 -08:00
Harshavardhana 2c198ae7b6 fix: prometheus metrics disks_online count when disks are down (#11689)
prometheus metrics was using total disks instead
of online disk count, when disks were down, this
PR fixes this and also adds a new metric for
total_disk_count
2021-03-03 11:18:41 -08:00
Poorna Krishnamoorthy 690434514d Avoid notification event for replicas (#11683)
Creating notification events for replica creation
is not particularly useful to send as the notification
event generated at source already includes replication
completion events.

For applications using replica cluster as failover, avoiding
duplicate notifications for replica event will allow seamless
failover.
2021-03-03 11:13:31 -08:00
Harshavardhana 039f59b552 fix: missing user policy enforcement in PostPolicyHandler (#11682) 2021-03-03 08:47:08 -08:00
Harshavardhana c6a120df0e fix: Prometheus metrics to re-use storage disks (#11647)
also re-use storage disks for all `mc admin server info`
calls as well, implement a new LocalStorageInfo() API
call at ObjectLayer to lookup local disks storageInfo

also fixes bugs where there were double calls to StorageInfo()
2021-03-02 17:28:04 -08:00
Klaus Post cd9e30c0f4 IAM: Block while loading users (#11671)
While starting up a request that needs all IAM data will start another load operation if the first on startup hasn't finished. This slows down both operations.

Block these requests until initial load has completed.

Blocking calls will be ListPolicies, ListUsers, ListServiceAccounts, ListGroups - and the calls that eventually trigger these. These will wait for the initial load to complete.

Fixes issue seen in #11305
2021-03-02 17:08:25 -08:00
Harshavardhana f96d4cf7d3 fix: do not deny admins to change other passwords
fixes a regression from #11680
2021-03-02 17:02:32 -08:00
Harshavardhana 879599b0cf fix: enforce deny if present for implicit permissions (#11680)
Implicit permissions for any user is to be allowed to
change their own password, we need to restrict this
further even if there is an implicit allow for this
scenario - we have to honor Deny statements if they
are specified.
2021-03-02 15:35:50 -08:00
Harshavardhana b1bb3f7016 [feat]: implement GetBucketPolicyStatus API (#11673)
additionally also add more APIs in notImplemented
list, adjust routing rules appropriately
2021-03-01 23:10:33 -08:00
Anis Elleuch e8d8dfa3ae Add metric for internode RPC calls errors (#11669) 2021-03-01 12:31:33 -08:00
Nitish Tiwari bbd1244a88 Add support for mTLS for Audit log target (#11645) 2021-03-01 09:19:13 -08:00
Klaus Post 10bdb78699 fix: listObjectVersions Include object in marker (#11562)
ListObjectVersions would skip past the object in the marker when version id is specified. 
Make `listPath` return the object with the marker and truncate it if not needed.

Avoid having to parse unintended objects to find a version marker.
2021-03-01 08:12:02 -08:00
Shireesh Anjal 289b22d911 fix: pool number not added for one server (#11670)
The previous code was iterating over replies from peers and assigning
pool numbers to them, thus missing to add it for the local server.

Fixed by iterating over the server properties of all the servers
including the local one.
2021-03-01 08:09:43 -08:00
Harshavardhana 0b9c17443e update gopsutil to use the v3 API (#11638) 2021-03-01 00:15:46 -08:00
Bala FA 23f7ab40b3 Add PoolNumber field to madmin.ServerProperties (#11327) 2021-02-28 21:26:28 -08:00
Minio Trusted e3f8830ab7 Update yaml files to latest version RELEASE.2021-03-01T04-20-55Z 2021-03-01 04:43:28 +00:00
Harshavardhana 2f4af09c01 fix: alow changes to readAllData to decrement activeCount() 2021-02-28 20:09:23 -08:00
Harshavardhana 37960cbc2f fix: avoid writing more content on network with O_DIRECT reads (#11659)
There was an io.LimitReader was missing for the 'length'
parameter for ranged requests, that would cause client to
get truncated responses and errors.

fixes #11651
2021-02-28 15:33:03 -08:00
cbows c67d1bf120 add unauthenticated lookup-bind mode to LDAP identity (#11655)
Closes #11646
2021-02-28 12:57:31 -08:00
Klaus Post c5b3a675fa Block profiling tweaks (#11612)
The base profiles contains no valuable data, don't record them.

Reduce block rate by 2 orders of magnitude, should still capture just as valuable data with less CPU strain.
2021-02-27 09:22:14 -08:00
Harshavardhana b690304eed use faster way for siphash (#11640) 2021-02-26 16:53:06 -08:00
Harshavardhana 9171d6ef65 rename all references from crawl -> scanner (#11621) 2021-02-26 15:11:42 -08:00
Harshavardhana 6386b45c08 [feat] use rename instead of recursive deletes (#11641)
most of the delete calls today spend time in
a blocking operation where multiple calls need
to be recursively sent to delete the objects,
instead we can use rename operation to atomically
move the objects from the namespace to `tmp/.trash`

we can schedule deletion of objects at this
location once in 15, 30mins and we can also add
wait times between each delete operation.

this allows us to make delete's faster as well
less chattier on the drives, each server runs locally
a groutine which would clean this up regularly.
2021-02-26 09:52:27 -08:00
Andreas Auernhammer 1f659204a2 remove GetObject from ObjectLayer interface (#11635)
This commit removes the `GetObject` method
from the `ObjectLayer` interface.

The `GetObject` method is not longer used by
the HTTP handlers implementing the high-level
S3 semantics. Instead, they use the `GetObjectNInfo`
method which returns both, an object handle as well
as the object metadata.

Therefore, it is no longer necessary that a concrete
`ObjectLayer` implements `GetObject`.
2021-02-26 09:52:02 -08:00
Harshavardhana f9f6fd0421 fix: service account permissions generated from LDAP user (#11637)
service accounts generated from LDAP parent user
did not inherit correct permissions, this PR fixes
this fully.
2021-02-25 13:49:59 -08:00
Klaus Post 85620dfe93 use bucket in path in distribution hash (#11634)
Use bucket in erasure distribution hash.

For the rare cases where objects with the same names are uploaded to many buckets.
2021-02-25 10:11:31 -08:00
Harshavardhana a8e4f64ff3 Revert "fix: remove persistence layer for metacache store in memory (#11538)"
This reverts commit b23659927c.
2021-02-24 22:24:51 -08:00
Krishnan Parthasarathi ca5c6e3160 fix: translate empty versionID string to null version where appropriate (#11629)
We store the null version as empty string. We should translate it to null
version for bucket with version suspended too.
2021-02-24 18:39:10 -08:00
Harshavardhana b23659927c fix: remove persistence layer for metacache store in memory (#11538)
store the cache in-memory instead of disks to avoid large
write amplifications for list heavy workloads, store in
memory instead and let it auto expire.
2021-02-24 15:51:41 -08:00
Minio Trusted b912e9ab41 Update yaml files to latest version RELEASE.2021-02-24T18-44-45Z 2021-02-24 19:08:36 +00:00
Andreas Auernhammer c1a49be639 use crypto/sha256 for FIPS 140-2 compliance (#11623)
This commit replaces the usage of
github.com/minio/sha256-simd with crypto/sha256
of the standard library in all non-performance
critical paths.

This is necessary for FIPS 140-2 compliance which
requires that all crypto. primitives are implemented
by a FIPS-validated module.

Go can use the Google FIPS module. The boringcrypto
branch of the Go standard library uses the BoringSSL
FIPS module to implement crypto. primitives like AES
or SHA256.

We only keep github.com/minio/sha256-simd when computing
the content-SHA256 of an object. Therefore, this commit
relies on a build tag `fips`.

When MinIO is compiled without the `fips` flag it will
use github.com/minio/sha256-simd. When MinIO is compiled
with the fips flag (go build --tags "fips") then MinIO
uses crypto/sha256 to compute the content-SHA256.
2021-02-24 09:00:15 -08:00
Klaus Post 03172b89e2 Ensure cache has finished deserializing (#11620)
Make sure that response has been fully deserialized before returning.
2021-02-24 02:59:49 -08:00
Harshavardhana b517c791e9 [feat]: use DSYNC for xl.meta writes and NOATIME for reads (#11615)
Instead of using O_SYNC, we are better off using O_DSYNC
instead since we are only ever interested in data to be
persisted to disk not the associated filesystem metadata.

For reads we ask customers to turn off noatime, but instead
we can proactively use O_NOATIME flag to avoid atime updates
upon reads.
2021-02-24 00:14:16 -08:00
Petr Tichý 14aef52004 remove Content-MD5 on Range requests (#11611)
This removes the Content-MD5 response header on Range requests in Azure
Gateway mode. The partial content MD5 doesn't match the full object MD5
in metadata.
2021-02-23 19:32:56 -08:00
Harshavardhana 67b20125e4 fix: do not ignore CREDITS file 2021-02-23 12:34:59 -08:00
Andreas Auernhammer d4b822d697 pkg/etag: add new package for S3 ETag handling (#11577)
This commit adds a new package `etag` for dealing
with S3 ETags.

Even though ETag is often viewed as MD5 checksum of
an object, handling S3 ETags correctly is a surprisingly
complex task. While it is true that the ETag corresponds
to the MD5 for the most basic S3 API operations, there are
many exceptions in case of multipart uploads or encryption.

In worse, some S3 clients expect very specific behavior when
it comes to ETags. For example, some clients expect that the
ETag is a double-quoted string and fail otherwise.
Non-AWS compliant ETag handling has been a source of many bugs
in the past.

Therefore, this commit adds a dedicated `etag` package that provides
functionality for parsing, generating and converting S3 ETags.
Further, this commit removes the ETag computation from the `hash`
package. Instead, the `hash` package (i.e. `hash.Reader`) should
focus only on computing and verifying the content-sha256.

One core feature of this commit is to provide a mechanism to
communicate a computed ETag from a low-level `io.Reader` to
a high-level `io.Reader`.

This problem occurs when an S3 server receives a request and
has to compute the ETag of the content. However, the server
may also wrap the initial body with several other `io.Reader`,
e.g. when encrypting or compressing the content:
```
   reader := Encrypt(Compress(ETag(content)))
```
In such a case, the ETag should be accessible by the high-level
`io.Reader`.

The `etag` provides a mechanism to wrap `io.Reader` implementations
such that the `ETag` can be accessed by a type-check.
This technique is applied to the PUT, COPY and Upload handlers.
2021-02-23 12:31:53 -08:00
Minio Trusted 1b63291ee2 Update yaml files to latest version RELEASE.2021-02-23T20-05-01Z 2021-02-23 20:28:30 +00:00
Harshavardhana aa7244a9a4 fix: make sure to convert the error properly in HealBucket() (#11610)
server startup code expects the object layer to properly
convert error into a proper type, so that in situations when
servers are coming up and quorum is not available servers
wait on each other.
2021-02-23 09:23:11 -08:00
Harshavardhana 2a79ea0332 isServerResolvable its sufficient to check server is reachable (#11609)
using isServerResolvable for expiration can lead to chicken
and egg problems, a lock might expire knowingly when server
is booting up causing perpetual locks getting expired.
2021-02-22 16:29:53 -08:00
Ritesh H Shukla 6e5c61d917 Skip printing error if empty for reporting bandwidth (#11606) 2021-02-22 13:41:40 -08:00
Aditya Manthramurthy 02e7de6367 LDAP config: fix substitution variables (#11586)
- In username search filter and username format variables we support %s for
replacing with the username.

- In group search filter we support %s for username and %d for the full DN of
the username.
2021-02-22 13:20:36 -08:00
Harshavardhana cec12f4c76 update sha256-simd to v1.0.0 (#11607) 2021-02-22 13:19:53 -08:00
Harshavardhana da676ac298 remove network calls for getLocalDisks (#11603) 2021-02-22 13:19:44 -08:00
Harshavardhana 18ec933085 fix: for containers use root-disk detection cleverly (#11593)
root-disk implemented currently had issues where root
disk partitions getting modified might race and provide
incorrect results, to avoid this lets rely again back on
DeviceID and match it instead.

In-case of containers `/data` is one such extra entity that
needs to be verified for root disk, due to how 'overlay'
filesystem works and the 'overlay' presents a completely
different 'device' id - using `/data` as another entity
for fallback helps because our containers describe 'VOLUME'
parameter that allows containers to automatically have a
virtual `/data` that points to the container root path this
can either be at `/` or `/var/lib/` (on different partition)
2021-02-22 10:32:21 -08:00
Harshavardhana c31d2c3fdc fix: CrawlAndGetDataUsage close pipe() before using a new one (#11600)
also additionally make sure errors during deserializer closes
the reader with right error type such that Write() end
actually see the final error, this avoids a waitGroup usage
and waiting.
2021-02-22 10:04:32 -08:00
Harshavardhana 8778828a03 fix: read metadata in O_DIRECT if configured and supported (#11594)
reduce the page-cache pressure completely by moving
the entire read-phase of our operations to O_DIRECT,
primarily this is going to be very useful for chatty
metadata operations such as listing, scanner, ilm, healing
like operations to avoid filling up the page-cache upon
repeated runs.
2021-02-22 01:36:17 -08:00
Sarasa Kisaragi 48b212dd8e Fix HDFS wrong filepath if subpath provided (#11574) 2021-02-20 15:32:18 -08:00
Harshavardhana be7de911c4 fix: update minio-go to fix an issue with S3 gateway (#11591)
since we have changed our default envs to MINIO_ROOT_USER,
MINIO_ROOT_PASSWORD this was not supported by minio-go
credentials package, update minio-go to v7.0.10 for this
support. This also addresses few bugs related to users
had to specify AWS_ACCESS_KEY_ID as well to authenticate
with their S3 backend if they only used MINIO_ROOT_USER.
2021-02-20 11:10:21 -08:00
Harshavardhana 8cad407e0b fix: Bring support for symlink on regular files on NAS (#11383)
fixes #11203
2021-02-20 00:30:12 -08:00
Poorna Krishnamoorthy 85d2187c20 fix: ETag mismatch for large upload in replica (#11587) 2021-02-20 00:22:17 -08:00
Anis Elleuch 98d3f94996 metrics: Add the number of requests in the waiting queue (#11580)
We can use this metric to check if there are too many S3 clients in the
queue and could explain why some of those S3 clients are timing out.

```
minio_s3_requests_waiting_total{server="127.0.0.1:9000"} 9981
```

If max_requests is 10000 then there is a strong possibility that clients
are timing out because of the queue deadline.
2021-02-20 00:21:55 -08:00
mailsmail 173284903b fix incorrect http range in SelectObjectContentHandler (#11585) 2021-02-19 17:55:28 -08:00
WangYuMu c70240b893 fix incorrect values in sizing guide (#11583) 2021-02-19 10:05:04 -08:00
Harshavardhana 8ba2136e06 Update yaml files to latest version RELEASE.2021-02-19T04-38-02Z 2021-02-18 21:02:25 -08:00
Poorna Krishnamoorthy 2dce5d9442 fix: delete marker permanent delete replication (#11581) 2021-02-18 16:35:37 -08:00
Anis Elleuch f28b063091 heal: Use healDeleteDangling global const in self healing (#11579)
A small fix, use healDeleteDangling constant instead of 'true' in the
self-healing code.
2021-02-18 15:16:20 -08:00
Klaus Post 90abea5b7a check if kafka producer is connected (#11578)
fixes #11576
2021-02-18 11:14:27 -08:00
Klaus Post c5b2a8441b fix: faster healing when disk is replaced. (#11520) 2021-02-18 11:06:54 -08:00
Harshavardhana 0f5ca83418 move CI to go1.15 and go1.16 (#11570) 2021-02-18 09:42:32 -08:00
Klaus Post 8a6b13c239 Avoid synchronizing usage writes (#11560)
If the periodic `case <-t.C:` save gets held up for a long time it will end up 
synchronize all disk writes for saving the caches.

We add jitter to per set writes so they don't sync up and don't hold a 
lock for the write, since it isn't needed anyway.

If an outage prevents writes for a long while we also add individual 
waits for each disk in case there was a queue.

Furthermore limit the number of buffers kept to 2GiB, since this could get 
huge in large clusters. This will not act as a hard limit but should be enough 
for normal operation.
2021-02-18 00:38:37 -08:00
Poorna Krishnamoorthy 8e8a792d9d Allow delete marker replication from replica (#11566)
in the case of active-active replication.

This PR also has the following changes:

- add docs on replication design
- fix corner case of completing versioned delete on a delete marker
  when the target is down and `mc rm --vid` is performed repeatedly. Instead
  the version should still be retained in the `PENDING|FAILED` state until
  replication sync completes.
- remove `s3:Replication:OperationCompletedReplication` and
   `s3:Replication:OperationFailedReplication` from ObjectCreated 
  events type
2021-02-18 00:33:51 -08:00
Harshavardhana 95e0acbb26 fix: allow accountInfo with creds with parentUsers (#11568) 2021-02-17 20:57:17 -08:00
Poorna Krishnamoorthy 55037e6e54 lifecycle:Fix args passed to determine expiry header (#11567) 2021-02-17 19:25:19 -08:00
Harshavardhana 289e1d8b2a fix: reduce crawler memory usage by orders of magnitude (#11556)
currently crawler waits for an entire readdir call to
return until it processes usage, lifecycle, replication
and healing - instead we should pass the applicator all
the way down to avoid building any special stack for all
the contents in a single directory.

This allows for

- no need to remember the entire list of entries per directory
  before applying the required functions
- no need to wait for entire readdir() call to finish before
  applying the required functions
2021-02-17 15:34:42 -08:00
Anis Elleuch e07918abe3 lifecycle: Fix expiration header in some cases (#11565)
Expiration header was not correctly computed in case of non current
versions.

The behavior is fixed in this commit.
2021-02-17 14:51:29 -08:00
Harshavardhana ffea6fcf09 fix: rename crawler as scanner in config (#11549) 2021-02-17 12:04:11 -08:00
Klaus Post 11b2220696 Don't autoheal if disks are healing (#11558)
Don't spawn automatic healing ops if a disk is healing.
2021-02-17 10:18:12 -08:00
Harshavardhana aa8450a2a1 fix: parallelize getPoolIdx() for object lookup (#11547) 2021-02-16 19:36:15 -08:00
Harshavardhana 87cce344f6 default to common conditions if conditions not present (#11546)
fixes #11544
2021-02-16 11:56:45 -08:00
Harshavardhana 7d4a2d2b68 fix: multiple pool reads parallelize when possible (#11537) 2021-02-16 02:43:47 -08:00
Minio Trusted cfc8b92dff Update yaml files to latest version RELEASE.2021-02-14T04-01-33Z 2021-02-14 04:25:52 +00:00
Anis Elleuch c4e12dc846 fix: in MultiDelete API return MalformedXML upon empty input (#11532)
To follow S3 spec
2021-02-13 09:48:25 -08:00
Harshavardhana a94a9c37fa fix: support IAM policy handling for wildcard actions (#11530)
This PR fixes

- allow 's3:versionid` as a valid conditional for
  Get,Put,Tags,Object locking APIs
- allow additional headers missing for object APIs
- allow wildcard based action matching
2021-02-12 23:05:09 -08:00
Harshavardhana 79b6a43467 fix: avoid timed value for network calls (#11531)
additionally simply timedValue to have RWMutex
to avoid concurrent calls to DiskInfo() getting
serialized, this has an effect on all calls that
use GetDiskInfo() on the same disks.

Such as getOnlineDisks, getOnlineDisksWithoutHealing
2021-02-12 18:17:52 -08:00
Shireesh Anjal 928de04f7a fix: osinfos incomplete in case of warnings (#11505)
The function used for getting host information
(host.SensorsTemperaturesWithContext) returns warnings in some cases.

Returning with error in such cases means we miss out on the other useful
information already fetched (os info).

If the OS info has been succesfully fetched, it should always be
included in the output irrespective of whether the other data (CPU
sensors, users) could be fetched or not.
2021-02-12 17:57:57 -08:00
Poorna Krishnamoorthy 93fd248b52 fix: save ModTime properly in disk cache (#11522)
fix #11414
2021-02-11 19:25:47 -08:00
Harshavardhana 2a7b123895 turn off http2 for TLS setups for now (#11523)
due to lots of issues with x/net/http2, as
well as the bundled h2_bundle.go in the go
runtime should be avoided for now.

https://github.com/golang/go/issues/23559
https://github.com/golang/go/issues/42534
https://github.com/golang/go/issues/43989
https://github.com/golang/go/issues/33425
https://github.com/golang/go/issues/29246

With collection of such issues present, it
make sense to remove HTTP2 support for now
2021-02-11 15:53:04 -08:00
Harshavardhana b3c56b53fb fix: metacache should only rename entries during cleanup (#11503)
To avoid large delays in metacache cleanup, use rename
instead of recursive delete calls, renames are cheaper
move the content to minioMetaTmpBucket and then cleanup
this folder once in 24hrs instead.

If the new cache can replace an existing one, we should
let it replace since that is currently being saved anyways,
this avoids pile up of 1000's of metacache entires for
same listing calls that are not necessary to be stored
on disk.
2021-02-11 10:22:03 -08:00
Minio Trusted 0ef3e359d8 Update yaml files to latest version RELEASE.2021-02-11T08-23-43Z 2021-02-11 08:47:10 +00:00
Poorna Krishnamoorthy f24d8127ab fix: DeleteMultipleObjectsHandler to process deleted objects correctly (#11515)
DeleteMarkerVersionID which is returned by the lower layer should 
not be used in the key to lookup ObjectToDelete map
2021-02-10 23:41:41 -08:00
Harshavardhana 7875d472bc avoid notification for non-existent delete objects (#11514)
Skip notifications on objects that might have had
an error during deletion, this also avoids unnecessary
replication attempt on such objects.

Refactor some places to make sure that we have notified
the client before we

- notify
- schedule for replication
- lifecycle etc.
2021-02-10 22:00:42 -08:00
Harshavardhana 711adb9652 remove ipv6 fallbackdelay leave it as default 2021-02-10 17:35:09 -08:00
Poorna Krishnamoorthy e6b4ea7618 More fixes for delete marker replication (#11504)
continuation of PR#11491 for multiple server pools and
bi-directional replication.

Moving proxying for GET/HEAD to handler level rather than
server pool layer as this was also causing incorrect proxying 
of HEAD.

Also fixing metadata update on CopyObject - minio-go was not passing
source version ID in X-Amz-Copy-Source header
2021-02-10 17:25:04 -08:00
Aditya Manthramurthy 466e95bb59 Return group DN instead of group name in LDAP STS (#11501)
- Additionally, check if the user or their groups has a policy attached during
the STS call.

- Remove the group name attribute configuration value.
2021-02-10 16:52:49 -08:00
Harshavardhana 881f98e511 fix: use getPoolIdx in DeleteObjects() (#11513)
filter out relevant objects for each pool to
avoid calling, further delete operations on
subsequent pools where some of these objects
might not exist.

This is mainly useful to avoid situations
during bi-directional bucket replication.
2021-02-10 14:25:43 -08:00
Harshavardhana cbf4bb62e0 fix: getPoolIdx decouple from top level options (#11512)
top-level options shouldn't be passed down for
GetObjectInfo() while verifying the objects in
different pools, this is to make sure that
we always get the value from the pool where
the object exists.
2021-02-10 11:45:02 -08:00
Anis Elleuch 682482459d Change the default object content-type to binary/octet-stream (#11508) 2021-02-10 08:56:37 -08:00
Krishnan Parthasarathi b87fae0049 Simplify PutObjReader for plain-text reader usage (#11470)
This change moves away from a unified constructor for plaintext and encrypted
usage. NewPutObjReader is simplified for the plain-text reader use. For
encrypted reader use, WithEncryption should be called on an initialized PutObjReader.

Plaintext:
func NewPutObjReader(rawReader *hash.Reader) *PutObjReader

The hash.Reader is used to provide payload size and md5sum to the downstream
consumers. This is different from the previous version in that there is no need
to pass nil values for unused parameters.

Encrypted:
func WithEncryption(encReader *hash.Reader,
key *crypto.ObjectKey) (*PutObjReader, error)

This method sets up encrypted reader along with the key to seal the md5sum
produced by the plain-text reader (already setup when NewPutObjReader was
called).

Usage:
```
  pReader := NewPutObjReader(rawReader)
  // ... other object handler code goes here

  // Prepare the encrypted hashed reader
  pReader, err = pReader.WithEncryption(encReader, objEncKey)

```
2021-02-10 08:52:50 -08:00
Anis Elleuch b8b44c879f lifecycle: Remove a single delete marker with noncurrent expiry rule (#11444)
NoncurrentVersionExpiry can remove single delete markers according to
S3 spec:

```
The NoncurrentVersionExpiration action in the same Lifecycle
configuration removes noncurrent objects 30 days after they become
noncurrent. Thus, in this example, all object versions are permanently
removed 90 days after object creation. You will have expired object
delete markers, but Amazon S3 detects and removes the expired object
delete markers for you.
```
2021-02-10 08:51:34 -08:00
Harshavardhana f53d1de87f fix: missing data on multiple columns reading parquet (#11499)
fixes #11413
2021-02-10 08:49:48 -08:00
Shireesh Anjal 5a18d437ce fix: drive hw info incomplete when smartinfo fails (#11509)
Collection of SMART information doesn't work in certain scenarios e.g.
in a container based setup. In such cases, instead of returning an error
(without any data), we should only set the error on the smartinfo
struct, so that other important drive hw info like device, mountpoint,
etc is retained in the output.
2021-02-10 08:48:14 -08:00
Poorna Krishnamoorthy 93eb549a83 fix: duplicate delete marker attempts in bi-directional replication (#11491) 2021-02-09 15:11:43 -08:00
Harshavardhana fe3c39b583 use the new errgroup API whereever applicable (#11466)
start using the new errgroup concurrency control
API introduced in #11457
2021-02-09 12:08:25 -08:00
Harshavardhana 84d400487f fix: accountInfo API to cater for federated setups (#11484)
when MinIO is deployed in a federated setup, use etcd 
based listing of buckets to provide appropriate filtering 
of buckets per user.
2021-02-09 09:53:07 -08:00
Shireesh Anjal 3afa499885 fix: empty buckets/objects nodes in new setup (#11493) 2021-02-09 09:52:38 -08:00
Shireesh Anjal 13d015cf93 Fix make target hotfix (#11492)
The code inside the `hotfix` taget is overriding the values set at the
beginning of the Makefile affecting other make targets as well.

For example, running `TAG=mytag make docker` also ends up tagging
the docker image as a hotfix instead of `mytag`.

Using the `eval` function inside the `hotfix` target fixes this.
2021-02-09 09:29:43 -08:00
Krishna Srinivas 876b79b8d8 read-health check endpoint returns success if cluster can serve read requests (#11310) 2021-02-09 01:00:44 -08:00
Ritesh H Shukla 3d74efa6b1 fux: copy object for encrypted objects (#11490) 2021-02-08 19:58:17 -08:00
Harshavardhana 68d299e719 fix: case-insensitive lookups for metadata (#11489)
continuation of #11487, with more changes
2021-02-08 18:12:28 -08:00
Poorna Krishnamoorthy f9c5636c2d fix: lookup metdata case insensitively (#11487)
while setting replication options
2021-02-08 16:19:05 -08:00
Klaus Post 9b10118d34 Metacache add abs entry limit (#11483)
Add an absolute limit to the number of metacaches for a bucket.

Delete excess caches if they haven't been handed out in an hour.
2021-02-08 11:36:16 -08:00
Harshavardhana 0e3211f4ad fix: server upgrades should have more descriptive error messages (#11476)
during rolling upgrade, provide a more descriptive error
message and discourage rolling upgrade in such situations,
allowing users to take action.

additionally also rename `slashpath -> pathutil` to avoid
a slighly mis-pronounced usage of `path` package.
2021-02-08 10:15:12 -08:00
Harshavardhana 2e4d9124ad honor region specified for remote targets (#11480)
fixes #11472
2021-02-08 08:54:27 -08:00
Harshavardhana 6fef4c21b9 fix: align atomic variables for 32bit arch (#11475)
fixes #11474
2021-02-08 08:51:12 -08:00
Poorna Krishnamoorthy 8e1bbd989a replication:alloc UserDefined map before use (#11478) 2021-02-07 22:01:10 -08:00
Sarasa Kisaragi 152d7cd95b HDFS support keytab (#11473) 2021-02-07 17:29:47 -08:00
Harshavardhana 74080bf108 update CREDITS file 2021-02-07 00:37:12 -08:00
Minio Trusted 647a209c73 Update yaml files to latest version RELEASE.2021-02-07T01-31-02Z 2021-02-07 01:53:27 +00:00
Harshavardhana 0d057c777a remove restriction for multi pool distribution algo 2021-02-06 16:19:05 -08:00
Anis Elleuch 275f7a63e8 lc: Apply DeleteAction correctly to objects (#11471)
When lifecycle decides to Delete an object and not a version in a
versioned bucket, the code should create a delete marker and not
removing the scanned version.

This commit fixes the issue.
2021-02-06 16:10:33 -08:00
Shireesh Anjal 97fe57bba9 Remove Connections from SysProcess struct (#11373)
The connections info of the processes takes up a huge amount of space,
and is not important for adding any useful health checks. Removing it
will significantly reduce the size of the subnet health report.
2021-02-05 21:32:28 -08:00
Harshavardhana 88c1bb0720 fix: improper ticker usage in goroutines (#11468)
- lock maintenance loop was incorrectly sleeping
  as well as using ticker badly, leading to
  extra expiration routines getting triggered
  that could flood the network.

- multipart upload cleanup should be based on
  timer instead of ticker, to ensure that long
  running jobs don't get triggered twice.

- make sure to get right lockers for object name
2021-02-05 19:23:48 -08:00
Harshavardhana 1fdafaf72f fix: listing for directory object when delimiter is present (#11463)
When you have heirarchy of prefixes with directory objects
our current master would list directory objects as prefixes
when delimiter is present, this is inconsistent with AWS S3

```
aws s3api list-objects --endpoint-url http://localhost:9000 \
    --profile minio --bucket testbucket-v --prefix new/ --delimiter /
{
    "CommonPrefixes": [
        {
            "Prefix": "new/"
        },
        {
            "Prefix": "new/new/"
        }
    ]
}
```

Instead this PR fixes this to behave like AWS S3

```
aws s3api list-objects --endpoint-url http://localhost:9000 \
      --profile minio --bucket testbucket-v --prefix new/ --delimiter /
{
    "Contents": [
        {
            "Key": "new/",
            "LastModified": "2021-02-05T06:27:42.660Z",
            "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
            "Size": 0,
            "StorageClass": "STANDARD",
            "Owner": {
                "DisplayName": "",
                "ID": "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4"
            }
        }
    ],
    "CommonPrefixes": [
        {
            "Prefix": "new/new/"
        }
    ]
}
```
2021-02-05 16:24:40 -08:00
Ritesh H Shukla 5fe4bb6b36 Reduce redundant crawler logging (#11448) 2021-02-05 15:51:11 -08:00
Harshavardhana 99b733d44c fix: deletion of delete marker regression (#11465)
fixes #11440
fixes #11451
fixes #11454
2021-02-05 15:06:23 -08:00
Klaus Post b4ac05523b Add parallel bucket healing during startup (#11457)
Replaces #11449

Does concurrent healing but limits concurrency to 50 buckets.

Aborts on first error.

`errgroup.Group` is extended to facilitate this in a generic way.
2021-02-05 13:04:26 -08:00
Anis Elleuch c7eacba41c health-info: Add tags to errors (#11412)
We use multiple libraries in health info, but the returned error does
not indicate exactly what library call is failing, hence adding named
tags to returned errors whenever applicable.
2021-02-05 12:37:15 -08:00
Anis Elleuch 1887c25279 xl: Fix feeding NumVersions & SuccessorModTime to lifecycle (#11462)
After recent refactor where lifecycle started to rely on ObjectInfo to
make decisions, it turned out there are some issues calculating
Successor Modtime and NumVersions, hence the lifecycle is not working as
expected in a versioning bucket in some cases.

This commit fixes the behavior.
2021-02-05 11:59:08 -08:00
Harshavardhana c9b0f595b9 support directory objects in listing in certain scenarios (#11452)
When a directory object is presented as a `prefix`
param our implementation tend to only list objects
present common to the `prefix` than the `prefix` itself,
to mimic AWS S3 like flat key behavior this PR ensures
that if `prefix` is directory object, it should be
automatically considered to be part of the eventual
listing result.

fixes #11370
2021-02-05 10:12:25 -08:00
Harshavardhana 8bb580abfc fix: use getObjectNInfo to avoid bytes.Buffer usage (#11428)
few places were still using legacy call GetObject()
which was mainly designed for client response writer,
use GetObjectNInfo() for internal calls instead.
2021-02-05 09:57:30 -08:00
Harshavardhana af9cb5f5f2 remove deprecated StandardSCData 2021-02-05 01:34:23 -08:00
Harshavardhana 9497dfd804 docs: add deprecation notice for federation 2021-02-04 17:18:37 -08:00
Harshavardhana da55a05587 fix aggressive expiration detection (#11446)
for some flaky networks this may be too fast of a value
choose a defensive value, and let this be addressed
properly in a new refactor of dsync with renewal logic.

Also enable faster fallback delay to cater for misconfigured
IPv6 servers

refer
 - https://golang.org/pkg/net/#Dialer
 - https://tools.ietf.org/html/rfc6555
2021-02-04 16:56:40 -08:00
Harshavardhana 3fc4d6f620 update dependenices for relevant projects (#11445)
- minio-go -> v7.0.8
- ldap/v3 -> v3.2.4
- reedsolomon -> v1.9.11
- sio-go -> v0.3.1
- msgp -> v1.1.5
- simdjson-go, md5-simd, highwayhash
2021-02-04 13:49:52 -08:00
Ritesh H Shukla 67a8f37df0 fix: disk usage capacity metric reporting (#11435) 2021-02-04 12:26:58 -08:00
Anis Elleuch 075c429021 lifcycle: Add more validation to the config (#11382) 2021-02-04 11:26:02 -08:00
ArthurMa df0c678167 fix: ldap config parsing issue for UserDNSearchFilter (#11437) 2021-02-04 11:07:29 -08:00
Harshavardhana f108873c48 fix: replication metadata comparsion and other fixes (#11410)
- using miniogo.ObjectInfo.UserMetadata is not correct
- using UserTags from Map->String() can change order
- ContentType comparison needs to be removed.
- Compare both lowercase and uppercase key names.
- do not silently error out constructing PutObjectOptions
  if tag parsing fails
- avoid notification for empty object info, failed operations
  should rely on valid objInfo for notification in all
  situations
- optimize copyObject implementation, also introduce a new 
  replication event
- clone ObjectInfo() before scheduling for replication
- add additional headers for comparison
- remove strings.EqualFold comparison avoid unexpected bugs
- fix pool based proxying with multiple pools
- compare only specific metadata

Co-authored-by: Poorna Krishnamoorthy <poornas@users.noreply.github.com>
2021-02-03 20:41:33 -08:00
Andreas Auernhammer 871b450dbd crypto: add support for decrypting SSE-KMS metadata (#11415)
This commit refactors the SSE implementation and add
S3-compatible SSE-KMS context handling.

SSE-KMS differs from SSE-S3 in two main aspects:
 1. The client can request a particular key and
    specify a KMS context as part of the request.
 2. The ETag of an SSE-KMS encrypted object is not
    the MD5 sum of the object content.

This commit only focuses on the 1st aspect.

A client can send an optional SSE context when using
SSE-KMS. This context is remembered by the S3 server
such that the client does not have to specify the
context again (during multipart PUT / GET / HEAD ...).
The crypto. context also includes the bucket/object
name to prevent renaming objects at the backend.

Now, AWS S3 behaves as following:
 - If the user does not provide a SSE-KMS context
   it does not store one - resp. does not include
   the SSE-KMS context header in the response (e.g. HEAD).
 - If the user specifies a SSE-KMS context without
   the bucket/object name then AWS stores the exact
   context the client provided but adds the bucket/object
   name internally. The response contains the KMS context
   without the bucket/object name.
 - If the user specifies a SSE-KMS context with
   the bucket/object name then AWS again stores the exact
   context provided by the client. The response contains
   the KMS context with the bucket/object name.

This commit implements this behavior w.r.t. SSE-KMS.
However, as of now, no such object can be created since
the server rejects SSE-KMS encryption requests.

This commit is one stepping stone for SSE-KMS support.

Co-authored-by: Harshavardhana <harsha@minio.io>
2021-02-03 15:19:08 -08:00
Harshavardhana f71e192343 avoid listing an empty dir without __XLDIR__ (#11427)
```
minio server /tmp/disk{1...4}
mc mb myminio/testbucket/
mkdir -p /tmp/disk{1..4}/testbucket/test-prefix/
```

This would end up being listed in the current
master, this PR fixes this situation.

If a directory is a leaf dir we should it
being listed, since it cannot be deleted anymore
with DeleteObject, DeleteObjects() API calls
because we natively support directories now.

Avoid listing it and let healing purge this folder
eventually in the background.
2021-02-03 14:06:54 -08:00
Anis Elleuch b3f81e75f6 xl: Make it clear when to create delete marker for a non existant object (#11423) 2021-02-03 10:33:43 -08:00
Klaus Post a71e0483c9 Fix nil disks in getOnlineDisksWithHealing (#11419)
If a disk is skipped when nil it is still returned.
2021-02-02 17:04:37 -08:00
Bahram Aghaei f2d49ec21a Update ldap.md: add a link to ldap.go (#11409) 2021-02-02 15:47:04 -08:00
Klaus Post 4a9d9c8585 Update colinmarc/hdfs (#11417)
Updates needed dependency as well.

Fixes #11416
2021-02-02 15:37:30 -08:00
Harshavardhana c885777ac6 Add support for TCP_QUICKACK (#11369)
TCP_QUICKACK is a setting that allows TCP endpoints
to acknowledge the receipt of data instantly in situations
where they would normally wait to see if more data
would be arriving.

https://assets.extrahop.com/whitepapers/TCP-Optimization-Guide-by-ExtraHop.pdf
2021-02-02 09:44:18 -08:00
Poorna Krishnamoorthy fe3aca70c3 Make number of replication workers configurable. (#11379)
MINIO_API_REPLICATION_WORKERS env.var and
`mc admin config set api` allow number of replication
workers to be configurable. Defaults to half the number
of cpus available.

Co-authored-by: Poorna Krishnamoorthy <poorna@minio.io>
2021-02-02 16:45:06 +05:30
Ritesh H Shukla c4848f9b4f Add process start time to cluster metrics. (#11405) 2021-02-01 23:02:18 -08:00
Andreas Auernhammer 838d4dafbd gateway: don't use encrypted ETags for If-Match (#11400)
This commit fixes a bug in the S3 gateway that causes
GET requests to fail when the object is encrypted by the
gateway itself.

The gateway was not able to GET the object since it always
specified a `If-Match` pre-condition checking that the object
ETag matches an expected ETag - even for encrypted ETags.

The problem is that an encrypted ETag will never match the ETag
computed by the backend causing the `If-Match` pre-condition
to fail.

This commit fixes this by not sending an `If-Match` header when
the ETag is encrypted. This is acceptable because:
  1. A gateway-encrypted object consists of two objects at the backend
     and there is no way to provide a concurrency-safe implementation
     of two consecutive S3 GETs in the deployment model of the S3
     gateway.
     Ref: S3 gateways are self-contained and isolated - and there may
          be multiple instances at the same time (no lock across
          instances).
  2. Even if the data object changes (concurrent PUT) while gateway
     A has download the metadata object (but not issued the GET to
     the data object => data race) then we don't return invalid data
     to the client since the decryption (of the currently uploaded data)
     will fail - given the metadata of the previous object.
2021-02-01 23:02:08 -08:00
swartz-k 8c663f93f7 fix: typo in chinese docs (#11401) 2021-02-01 18:42:58 -08:00
Minio Trusted b4cb7edf85 Update yaml files to latest version RELEASE.2021-02-01T22-56-52Z 2021-02-01 23:28:23 +00:00
Anis Elleuch e96fdcd5ec tagging: Add event notif for PUT object tagging (#11366)
An optimization to avoid double calling for during PutObject tagging
2021-02-01 13:52:51 -08:00
Anis Elleuch 6ef678663e xl: Create a delete-marker when no other version exists (#11362)
Currently, it is not possible to create a delete-marker when xl.meta
does not exist (no version is created for that object yet). This makes a
problem for replication and mc mirroring with versioning enabled.

This also follows S3 specification.
2021-02-01 13:23:50 -08:00
Harshavardhana f737a027cf fix: regression introduced in federated listing buckets
regression was introduced in
6cd255d516 fix it properly.
2021-02-01 12:06:58 -08:00
Ravind Kumar 547efecd82 Reverting TOC due to gluegun incompatibility. Revert this commit once we migrate to new docs site (#11402) 2021-02-01 11:10:07 -08:00
Anis Elleuch 65aa2bc614 ilm: Remove object in HEAD/GET if having an applicable ILM rule (#11296)
Remove an object on the fly if there is a lifecycle rule with delete
expiry action for the corresponding object.
2021-02-01 09:52:11 -08:00
Daniel Jakots de4421d6a3 fix: build on OpenBSD (#11384)
github.com/ncw/directio doesn't support OpenBSD, but OpenBSD has
syscall.Fsync. (It also has fdatasync:
https://man.openbsd.org/fdatasync
but apparently Golang can't call it).
2021-02-01 09:48:49 -08:00
swartz-k 6c3467e300 fix: docs typo in README_zh_CN (#11375) 2021-01-31 00:11:01 -08:00
Andreas Auernhammer 33554651e9 crypto: deprecate native Hashicorp Vault support (#11352)
This commit deprecates the native Hashicorp Vault
support and removes the legacy Vault documentation.

The native Hashicorp Vault documentation is marked as
outdated and deprecated for over a year now. We give
another 6 months before we start removing Hashicorp Vault
support and show a deprecation warning when a MinIO server
starts with a native Vault configuration.
2021-01-29 17:55:37 -08:00
Minio Trusted 451d9057f3 Update yaml files to latest version RELEASE.2021-01-30T00-20-58Z 2021-01-30 00:45:11 +00:00
317 changed files with 35809 additions and 6700 deletions
+7
View File
@@ -1,2 +1,9 @@
.git .git
.github .github
docs
default.etcd
browser
*.gz
*.tar.gz
*.bzip2
*.zip
+10 -4
View File
@@ -11,8 +11,8 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
go-version: [1.14.x, 1.15.x] go-version: [1.16.x]
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest, macos-latest]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v1 - uses: actions/setup-node@v1
@@ -21,6 +21,14 @@ jobs:
- uses: actions/setup-go@v2 - uses: actions/setup-go@v2
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
- name: Build on ${{ matrix.os }}
if: matrix.os == 'macos-latest'
env:
CGO_ENABLED: 0
GO111MODULE: on
run: |
make
make test-race
- name: Build on ${{ matrix.os }} - name: Build on ${{ matrix.os }}
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
env: env:
@@ -42,8 +50,6 @@ jobs:
curl -L -o nancy https://github.com/sonatype-nexus-community/nancy/releases/download/${nancy_version}/nancy-${nancy_version}-linux-amd64 && chmod +x nancy 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 -m all | ./nancy sleuth go list -m all | ./nancy sleuth
make make
diff -au <(gofmt -s -d cmd) <(printf "")
diff -au <(gofmt -s -d pkg) <(printf "")
make test-race make test-race
make crosscompile make crosscompile
make verify make verify
+2
View File
@@ -17,6 +17,8 @@ linters:
- gosimple - gosimple
- deadcode - deadcode
- structcheck - structcheck
- gomodguard
- gofmt
issues: issues:
exclude-use-default: false exclude-use-default: false
+2011 -228
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.15-alpine as builder FROM golang:1.16-alpine as builder
LABEL maintainer="MinIO Inc <dev@min.io>" LABEL maintainer="MinIO Inc <dev@min.io>"
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.15-alpine as builder FROM golang:1.16-alpine as builder
LABEL maintainer="MinIO Inc <dev@min.io>" LABEL maintainer="MinIO Inc <dev@min.io>"
+3 -6
View File
@@ -6,8 +6,6 @@ LABEL maintainer="MinIO Inc <dev@min.io>"
COPY dockerscripts/docker-entrypoint.sh /usr/bin/ COPY dockerscripts/docker-entrypoint.sh /usr/bin/
COPY minio /usr/bin/ COPY minio /usr/bin/
COPY CREDITS /licenses/CREDITS
COPY LICENSE /licenses/LICENSE
ENV MINIO_UPDATE=off \ ENV MINIO_UPDATE=off \
MINIO_ACCESS_KEY_FILE=access_key \ MINIO_ACCESS_KEY_FILE=access_key \
@@ -17,10 +15,9 @@ ENV MINIO_UPDATE=off \
MINIO_KMS_MASTER_KEY_FILE=kms_master_key \ MINIO_KMS_MASTER_KEY_FILE=kms_master_key \
MINIO_SSE_MASTER_KEY_FILE=sse_master_key MINIO_SSE_MASTER_KEY_FILE=sse_master_key
RUN \ RUN microdnf update --nodocs
microdnf update --nodocs && \ RUN microdnf install curl ca-certificates shadow-utils util-linux --nodocs
microdnf install curl ca-certificates shadow-utils util-linux --nodocs && \ RUN microdnf clean all && \
microdnf clean all && \
chmod +x /usr/bin/minio && \ chmod +x /usr/bin/minio && \
chmod +x /usr/bin/docker-entrypoint.sh chmod +x /usr/bin/docker-entrypoint.sh
+2 -2
View File
@@ -5,8 +5,8 @@ ARG TARGETARCH
LABEL name="MinIO" \ LABEL name="MinIO" \
vendor="MinIO Inc <dev@min.io>" \ vendor="MinIO Inc <dev@min.io>" \
maintainer="MinIO Inc <dev@min.io>" \ maintainer="MinIO Inc <dev@min.io>" \
version="RELEASE.2021-01-16T02-19-44Z" \ version="RELEASE.2021-03-04T00-53-13Z" \
release="RELEASE.2021-01-16T02-19-44Z" \ release="RELEASE.2021-03-04T00-53-13Z" \
summary="MinIO is a High Performance Object Storage, API compatible with Amazon S3 cloud storage service." \ 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." 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."
+8 -18
View File
@@ -17,32 +17,22 @@ checks:
getdeps: getdeps:
@mkdir -p ${GOPATH}/bin @mkdir -p ${GOPATH}/bin
@which golangci-lint 1>/dev/null || (echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin v1.27.0) @which golangci-lint 1>/dev/null || (echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin v1.27.0)
@which ruleguard 1>/dev/null || (echo "Installing ruleguard" && go get github.com/quasilyte/go-ruleguard/cmd/ruleguard@v0.2.1)
@which msgp 1>/dev/null || (echo "Installing msgp" && go get github.com/tinylib/msgp@v1.1.3) @which msgp 1>/dev/null || (echo "Installing msgp" && go get github.com/tinylib/msgp@v1.1.3)
@which stringer 1>/dev/null || (echo "Installing stringer" && go get golang.org/x/tools/cmd/stringer) @which stringer 1>/dev/null || (echo "Installing stringer" && go get golang.org/x/tools/cmd/stringer)
crosscompile: crosscompile:
@(env bash $(PWD)/buildscripts/cross-compile.sh) @(env bash $(PWD)/buildscripts/cross-compile.sh)
verifiers: getdeps fmt lint ruleguard check-gen verifiers: getdeps lint check-gen
check-gen: check-gen:
@go generate ./... >/dev/null @go generate ./... >/dev/null
@(! git diff --name-only | grep '_gen.go$$') || (echo "Non-committed changes in auto-generated code is detected, please commit them to proceed." && false) @(! git diff --name-only | grep '_gen.go$$') || (echo "Non-committed changes in auto-generated code is detected, please commit them to proceed." && false)
fmt:
@echo "Running $@ check"
@GO111MODULE=on gofmt -d cmd/
@GO111MODULE=on gofmt -d pkg/
lint: lint:
@echo "Running $@ check" @echo "Running $@ check"
@GO111MODULE=on ${GOPATH}/bin/golangci-lint cache clean @GO111MODULE=on ${GOPATH}/bin/golangci-lint cache clean
@GO111MODULE=on ${GOPATH}/bin/golangci-lint run --timeout=10m --config ./.golangci.yml @GO111MODULE=on ${GOPATH}/bin/golangci-lint run --build-tags kqueue --timeout=10m --config ./.golangci.yml
ruleguard:
@echo "Running $@ check"
@${GOPATH}/bin/ruleguard -rules ruleguard.rules.go github.com/minio/minio/...
# Builds minio, runs the verifiers then runs the tests. # Builds minio, runs the verifiers then runs the tests.
check: test check: test
@@ -71,18 +61,18 @@ build: checks
@echo "Building minio binary to './minio'" @echo "Building minio binary to './minio'"
@GO111MODULE=on CGO_ENABLED=0 go build -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null @GO111MODULE=on CGO_ENABLED=0 go build -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
hotfix: 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 | \ hotfix-vars:
sed 's#RELEASE\.\([0-9]\+\)-\([0-9]\+\)-\([0-9]\+\)T\([0-9]\+\)-\([0-9]\+\)-\([0-9]\+\)Z#\1-\2-\3T\4:\5:\6Z#')) $(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 | \
TAG := "minio/minio:$(shell git describe --tags --abbrev=0).hotfix.$(shell git rev-parse --short HEAD)" sed 's#RELEASE\.\([0-9]\+\)-\([0-9]\+\)-\([0-9]\+\)T\([0-9]\+\)-\([0-9]\+\)-\([0-9]\+\)Z#\1-\2-\3T\4:\5:\6Z#')))
hotfix: install $(eval TAG := "minio/minio:$(shell git describe --tags --abbrev=0).hotfix.$(shell git rev-parse --short HEAD)")
hotfix: hotfix-vars install
docker-hotfix: hotfix checks docker-hotfix: hotfix checks
@echo "Building minio docker image '$(TAG)'" @echo "Building minio docker image '$(TAG)'"
@docker build -t $(TAG) . -f Dockerfile.dev @docker build -t $(TAG) . -f Dockerfile.dev
docker: checks docker: build checks
@echo "Building minio docker image '$(TAG)'" @echo "Building minio docker image '$(TAG)'"
@GOOS=linux GO111MODULE=on CGO_ENABLED=0 go build -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
@docker build -t $(TAG) . -f Dockerfile.dev @docker build -t $(TAG) . -f Dockerfile.dev
# Builds minio and installs it to $GOPATH/bin. # Builds minio and installs it to $GOPATH/bin.
+50 -65
View File
@@ -5,47 +5,32 @@
MinIO is a High Performance Object Storage released under Apache License v2.0. It is API compatible with Amazon S3 cloud storage service. Use MinIO to build high performance infrastructure for machine learning, analytics and application data workloads. MinIO is a High Performance Object Storage released under Apache License v2.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 Docker-based installations. For Kubernetes environments, This README provides quickstart instructions on running MinIO on baremetal hardware, including Docker-based installations. For Kubernetes environments,
use the [MinIO Kubernetes Operator](https://github.com/minio/operator/blob/master/README.md). use the [MinIO Kubernetes Operator](https://github.com/minio/operator/blob/master/README.md).
**Table of Contents**
- [Docker Installation](#docker-installation)
- [macOS Installation](#macos)
- [GNU/Linux Installation](#gnulinux)
- [Windows Installation](#microsoft-windows)
- [FreeBSD Installation](#freebsd)
- [Source Installation](#install-from-source)
- [Deployment Recommendations](#deployment-recommendations)
- [Test Connectivity to MinIO](#test-minio-connectivity)
- [Upgrade MinIO](#upgrading-minio)
- [Explore Further](#explore-further)
- [Contribute to MinIO](#contribute-to-minio-project)
- [License](#license)
# Docker Installation # Docker Installation
Use the following commands to run a standalone MinIO server on a Docker container. Use the following commands to run a standalone MinIO server on a Docker container.
Standalone MinIO servers are best suited for early development and evaluation. Certain features such as versioning, object locking, and bucket replication Standalone MinIO servers are best suited for early development and evaluation. Certain features such as versioning, object locking, and bucket replication
require distributed deploying MinIO with Erasure Coding. For extended development and production, deploy MinIO with Erasure Coding enabled - specifically, require distributed deploying MinIO with Erasure Coding. For extended development and production, deploy MinIO with Erasure Coding enabled - specifically,
with a *minimum* of 4 drives per MinIO server. See [MinIO Erasure Code Quickstart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide.html) with a *minimum* of 4 drives per MinIO server. See [MinIO Erasure Code Quickstart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide.html)
for more complete documentation. for more complete documentation.
## Stable ## Stable
Run the following command to run the latest stable image of MinIO on a Docker container using an ephemeral data volume: Run the following command to run the latest stable image of MinIO on a Docker container using an ephemeral data volume:
```sh ```sh
docker run -p 9000:9000 minio/minio server /data docker run -p 9000:9000 minio/minio server /data
``` ```
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Browser, an embedded The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Browser, an embedded
web-based object browser built into MinIO Server. Point a web browser running on the host machine to http://127.0.0.1:9000 and log in with the web-based object browser built into MinIO Server. Point a web browser running on the host machine to http://127.0.0.1:9000 and log in with the
root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server. root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See
[Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers, [Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers,
see https://docs.min.io/docs/ and click **MINIO SDKS** in the navigation to view MinIO SDKs for supported languages. see https://docs.min.io/docs/ and click **MINIO SDKS** in the navigation to view MinIO SDKs for supported languages.
@@ -60,12 +45,12 @@ Run the following command to run the bleeding-edge image of MinIO on a Docker co
docker run -p 9000:9000 minio/minio:edge server /data docker run -p 9000:9000 minio/minio:edge server /data
``` ```
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Browser, an embedded The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Browser, an embedded
web-based object browser built into MinIO Server. Point a web browser running on the host machine to http://127.0.0.1:9000 and log in with the web-based object browser built into MinIO Server. Point a web browser running on the host machine to http://127.0.0.1:9000 and log in with the
root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server. root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See
[Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers, [Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers,
see https://docs.min.io/docs/ and click **MINIO SDKS** in the navigation to view MinIO SDKs for supported languages. see https://docs.min.io/docs/ and click **MINIO SDKS** in the navigation to view MinIO SDKs for supported languages.
@@ -76,9 +61,9 @@ see https://docs.min.io/docs/ and click **MINIO SDKS** in the navigation to view
Use the following commands to run a standalone MinIO server on macOS. Use the following commands to run a standalone MinIO server on macOS.
Standalone MinIO servers are best suited for early development and evaluation. Certain features such as versioning, object locking, and bucket replication Standalone MinIO servers are best suited for early development and evaluation. Certain features such as versioning, object locking, and bucket replication
require distributed deploying MinIO with Erasure Coding. For extended development and production, deploy MinIO with Erasure Coding enabled - specifically, require distributed deploying MinIO with Erasure Coding. For extended development and production, deploy MinIO with Erasure Coding enabled - specifically,
with a *minimum* of 4 drives per MinIO server. See [MinIO Erasure Code Quickstart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide.html) with a *minimum* of 4 drives per MinIO server. See [MinIO Erasure Code Quickstart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide.html)
for more complete documentation. for more complete documentation.
## Homebrew (recommended) ## Homebrew (recommended)
@@ -97,12 +82,12 @@ brew uninstall minio
brew install minio/stable/minio brew install minio/stable/minio
``` ```
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Browser, an embedded The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Browser, an embedded
web-based object browser built into MinIO Server. Point a web browser running on the host machine to http://127.0.0.1:9000 and log in with the web-based object browser built into MinIO Server. Point a web browser running on the host machine to http://127.0.0.1:9000 and log in with the
root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server. root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See
[Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers, [Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers,
see https://docs.min.io/docs/ and click **MINIO SDKS** in the navigation to view MinIO SDKs for supported languages. see https://docs.min.io/docs/ and click **MINIO SDKS** in the navigation to view MinIO SDKs for supported languages.
## Binary Download ## Binary Download
@@ -115,12 +100,12 @@ chmod +x minio
./minio server /data ./minio server /data
``` ```
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Browser, an embedded The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Browser, an embedded
web-based object browser built into MinIO Server. Point a web browser running on the host machine to http://127.0.0.1:9000 and log in with the web-based object browser built into MinIO Server. Point a web browser running on the host machine to http://127.0.0.1:9000 and log in with the
root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server. root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See
[Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers, [Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers,
see https://docs.min.io/docs/ and click **MINIO SDKS** in the navigation to view MinIO SDKs for supported languages. see https://docs.min.io/docs/ and click **MINIO SDKS** in the navigation to view MinIO SDKs for supported languages.
@@ -145,18 +130,18 @@ The following table lists supported architectures. Replace the `wget` URL with t
| 64-bit PowerPC LE (ppc64le) | https://dl.min.io/server/minio/release/linux-ppc64le/minio | | 64-bit PowerPC LE (ppc64le) | https://dl.min.io/server/minio/release/linux-ppc64le/minio |
| IBM Z-Series (S390X) | https://dl.min.io/server/minio/release/linux-s390x/minio | | IBM Z-Series (S390X) | https://dl.min.io/server/minio/release/linux-s390x/minio |
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Browser, an embedded The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Browser, an embedded
web-based object browser built into MinIO Server. Point a web browser running on the host machine to http://127.0.0.1:9000 and log in with the web-based object browser built into MinIO Server. Point a web browser running on the host machine to http://127.0.0.1:9000 and log in with the
root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server. root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See
[Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers, [Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers,
see https://docs.min.io/docs/ and click **MINIO SDKS** in the navigation to view MinIO SDKs for supported languages. see https://docs.min.io/docs/ and click **MINIO SDKS** in the navigation to view MinIO SDKs for supported languages.
> NOTE: Standalone MinIO servers are best suited for early development and evaluation. Certain features such as versioning, object locking, and bucket replication > NOTE: Standalone MinIO servers are best suited for early development and evaluation. Certain features such as versioning, object locking, and bucket replication
require distributed deploying MinIO with Erasure Coding. For extended development and production, deploy MinIO with Erasure Coding enabled - specifically, require distributed deploying MinIO with Erasure Coding. For extended development and production, deploy MinIO with Erasure Coding enabled - specifically,
with a *minimum* of 4 drives per MinIO server. See [MinIO Erasure Code Quickstart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide.html) with a *minimum* of 4 drives per MinIO server. See [MinIO Erasure Code Quickstart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide.html)
for more complete documentation. for more complete documentation.
# Microsoft Windows # Microsoft Windows
@@ -173,17 +158,17 @@ Use the following command to run a standalone MinIO server on the Windows host.
minio.exe server D:\ minio.exe server D:\
``` ```
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Browser, an embedded The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Browser, an embedded
web-based object browser built into MinIO Server. Point a web browser running on the host machine to http://127.0.0.1:9000 and log in with the web-based object browser built into MinIO Server. Point a web browser running on the host machine to http://127.0.0.1:9000 and log in with the
root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server. root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See
[Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers, [Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers,
see https://docs.min.io/docs/ and click **MINIO SDKS** in the navigation to view MinIO SDKs for supported languages. see https://docs.min.io/docs/ and click **MINIO SDKS** in the navigation to view MinIO SDKs for supported languages.
> NOTE: Standalone MinIO servers are best suited for early development and evaluation. Certain features such as versioning, object locking, and bucket replication > NOTE: Standalone MinIO servers are best suited for early development and evaluation. Certain features such as versioning, object locking, and bucket replication
require distributed deploying MinIO with Erasure Coding. For extended development and production, deploy MinIO with Erasure Coding enabled - specifically, require distributed deploying MinIO with Erasure Coding. For extended development and production, deploy MinIO with Erasure Coding enabled - specifically,
with a *minimum* of 4 drives per MinIO server. See [MinIO Erasure Code Quickstart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide.html) with a *minimum* of 4 drives per MinIO server. See [MinIO Erasure Code Quickstart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide.html)
for more complete documentation. for more complete documentation.
# FreeBSD # FreeBSD
@@ -199,27 +184,27 @@ service minio start
# Install from Source # 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.15](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.16](https://golang.org/dl/#stable)
```sh ```sh
GO111MODULE=on go get github.com/minio/minio GO111MODULE=on go get github.com/minio/minio
``` ```
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Browser, an embedded The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Browser, an embedded
web-based object browser built into MinIO Server. Point a web browser running on the host machine to http://127.0.0.1:9000 and log in with the web-based object browser built into MinIO Server. Point a web browser running on the host machine to http://127.0.0.1:9000 and log in with the
root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server. root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See
[Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers, [Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers,
see https://docs.min.io/docs/ and click **MINIO SDKS** in the navigation to view MinIO SDKs for supported languages. see https://docs.min.io/docs/ and click **MINIO SDKS** in the navigation to view MinIO SDKs for supported languages.
> NOTE: Standalone MinIO servers are best suited for early development and evaluation. Certain features such as versioning, object locking, and bucket replication > NOTE: Standalone MinIO servers are best suited for early development and evaluation. Certain features such as versioning, object locking, and bucket replication
require distributed deploying MinIO with Erasure Coding. For extended development and production, deploy MinIO with Erasure Coding enabled - specifically, require distributed deploying MinIO with Erasure Coding. For extended development and production, deploy MinIO with Erasure Coding enabled - specifically,
with a *minimum* of 4 drives per MinIO server. See [MinIO Erasure Code Quickstart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide.html) with a *minimum* of 4 drives per MinIO server. See [MinIO Erasure Code Quickstart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide.html)
for more complete documentation. for more complete documentation.
MinIO strongly recommends *against* using compiled-from-source MinIO servers for production environments. MinIO strongly recommends *against* using compiled-from-source MinIO servers for production environments.
# Deployment Recommendations # Deployment Recommendations
+4 -4
View File
@@ -78,7 +78,7 @@ minio.exe server D:\Photos
## FreeBSD ## FreeBSD
### Port ### Port
使用 [pkg](https://github.com/freebsd/pkg)进行安装,, MinIO官方并没有提供FreeBSD二进制文件, 它由FreeBSD上游维护,点击 [这里](https://www.freshports.org/www/minio)查看。 使用 [pkg](https://github.com/freebsd/pkg)进行安装,MinIO官方并没有提供FreeBSD二进制文件, 它由FreeBSD上游维护,点击 [这里](https://www.freshports.org/www/minio)查看。
```sh ```sh
pkg install minio pkg install minio
@@ -89,7 +89,7 @@ service minio start
## 使用源码安装 ## 使用源码安装
采用源码安装仅供开发人员和高级用户使用,如果你还没有Golang环境, 请参考 [How to install Golang](https://golang.org/doc/install)。最低需要Golang版本为 [go1.14](https://golang.org/dl/#stable) 采用源码安装仅供开发人员和高级用户使用,如果你还没有Golang环境, 请参考 [How to install Golang](https://golang.org/doc/install)。最低需要Golang版本为 [go1.16](https://golang.org/dl/#stable)
```sh ```sh
GO111MODULE=on go get github.com/minio/minio GO111MODULE=on go get github.com/minio/minio
@@ -179,7 +179,7 @@ mc admin update <minio alias, e.g., myminio>
- 对于联盟部署模式,应分别针对每个群集运行`mc admin update`。 在成功更新所有群集之前,不要将`mc`更新为任何新版本。 - 对于联盟部署模式,应分别针对每个群集运行`mc admin update`。 在成功更新所有群集之前,不要将`mc`更新为任何新版本。
- 如果将`kes`用作MinIO的KMS,只需替换二进制文件并重新启动`kes`,可以在 [这里](https://github.com/minio/kes/wiki) 找到有关`kes`的更多信息。 - 如果将`kes`用作MinIO的KMS,只需替换二进制文件并重新启动`kes`,可以在 [这里](https://github.com/minio/kes/wiki) 找到有关`kes`的更多信息。
- 如果将Vault作为MinIO的KMS,请确保已遵循如下Vault升级过程的概述:https://www.vaultproject.io/docs/upgrading/index.html - 如果将Vault作为MinIO的KMS,请确保已遵循如下Vault升级过程的概述:https://www.vaultproject.io/docs/upgrading/index.html
- 如果将MindIO与etcd配合使用, 请确保已遵循如下etcd升级过程的概述: https://github.com/etcd-io/etcd/blob/master/Documentation/upgrades/upgrading-etcd.md - 如果将MinIO与etcd配合使用, 请确保已遵循如下etcd升级过程的概述: https://github.com/etcd-io/etcd/blob/master/Documentation/upgrades/upgrading-etcd.md
## 了解更多 ## 了解更多
- [MinIO纠删码入门](https://docs.min.io/docs/minio-erasure-code-quickstart-guide) - [MinIO纠删码入门](https://docs.min.io/docs/minio-erasure-code-quickstart-guide)
@@ -190,7 +190,7 @@ mc admin update <minio alias, e.g., myminio>
- [MinIO文档](https://docs.min.io) - [MinIO文档](https://docs.min.io)
## 如何参与到MinIO项目 ## 如何参与到MinIO项目
请参考 [贡献者指南](https://github.com/minio/minio/blob/master/CONTRIBUTING.md)。欢迎各位中国程序员加到MinIO项目中。 请参考 [贡献者指南](https://github.com/minio/minio/blob/master/CONTRIBUTING.md)。欢迎各位中国程序员加到MinIO项目中。
## 授权许可 ## 授权许可
MinIO的使用受 Apache 2.0 License 约束,你可以在 [LICENSE](./LICENSE) 查看许可。 MinIO的使用受 Apache 2.0 License 约束,你可以在 [LICENSE](./LICENSE) 查看许可。
-1
View File
@@ -17,4 +17,3 @@ release
*.syso *.syso
coverage.txt coverage.txt
node_modules node_modules
production
+1 -12
View File
@@ -17,24 +17,13 @@ nvm install stable
npm install npm install
``` ```
### Install `go-bindata` and `go-bindata-assetfs`
If you do not have a working Golang environment, please follow [Install Golang](https://golang.org/doc/install)
```sh
go get github.com/go-bindata/go-bindata/go-bindata
go get github.com/elazarl/go-bindata-assetfs/go-bindata-assetfs
```
## Generating Assets ## Generating Assets
### Generate ui-assets.go
```sh ```sh
npm run release npm run release
``` ```
This generates ui-assets.go in the current directory. Now do `make` in the parent directory to build the minio binary with the newly generated ``ui-assets.go`` This generates `production` in the current directory.
## Run MinIO Browser with live reload ## Run MinIO Browser with live reload
@@ -24,9 +24,6 @@ jest.mock("jwt-decode")
jwtDecode.mockImplementation(() => ({ sub: "minio" })) jwtDecode.mockImplementation(() => ({ sub: "minio" }))
jest.mock("../../web", () => ({ jest.mock("../../web", () => ({
GenerateAuth: jest.fn(() => {
return Promise.resolve({ accessKey: "gen1", secretKey: "gen2" })
}),
SetAuth: jest.fn( SetAuth: jest.fn(
({ currentAccessKey, currentSecretKey, newAccessKey, newSecretKey }) => { ({ currentAccessKey, currentSecretKey, newAccessKey, newSecretKey }) => {
if ( if (
+11
View File
@@ -0,0 +1,11 @@
package browser
import "embed"
//go:embed production/*
var fs embed.FS
// GetStaticAssets returns assets
func GetStaticAssets() embed.FS {
return fs
}
-76
View File
@@ -14,19 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
var moment = require('moment')
var async = require('async') var async = require('async')
var exec = require('child_process').exec var exec = require('child_process').exec
var fs = require('fs') var fs = require('fs')
var isProduction = process.env.NODE_ENV == 'production' ? true : false var isProduction = process.env.NODE_ENV == 'production' ? true : false
var assetsFileName = ''
var commitId = ''
var date = moment.utc()
var version = date.format('YYYY-MM-DDTHH:mm:ss') + 'Z'
var releaseTag = date.format('YYYY-MM-DDTHH-mm-ss') + 'Z'
var buildType = 'DEVELOPMENT'
if (process.env.MINIO_UI_BUILD) buildType = process.env.MINIO_UI_BUILD
rmDir = function(dirPath) { rmDir = function(dirPath) {
try { var files = fs.readdirSync(dirPath); } try { var files = fs.readdirSync(dirPath); }
@@ -53,74 +45,6 @@ async.waterfall([
console.log('Running', cmd) console.log('Running', cmd)
exec(cmd, cb) exec(cmd, cb)
}, },
function(stdout, stderr, cb) {
if (isProduction) {
fs.renameSync('production/index_bundle.js',
'production/index_bundle-' + releaseTag + '.js')
} else {
fs.renameSync('dev/index_bundle.js',
'dev/index_bundle-' + releaseTag + '.js')
}
var cmd = 'git log --format="%H" -n1'
console.log('Running', cmd)
exec(cmd, cb)
},
function(stdout, stderr, cb) {
if (!stdout) throw new Error('commitId is empty')
commitId = stdout.replace('\n', '')
if (commitId.length !== 40) throw new Error('commitId invalid : ' + commitId)
assetsFileName = 'ui-assets.go';
var cmd = 'go-bindata-assetfs -o bindata_assetfs.go -pkg browser -nocompress=true production/...'
if (!isProduction) {
cmd = 'go-bindata-assetfs -o bindata_assetfs.go -pkg browser -nocompress=true dev/...'
}
console.log('Running', cmd)
exec(cmd, cb)
},
function(stdout, stderr, cb) {
var cmd = 'gofmt -s -w -l bindata_assetfs.go'
console.log('Running', cmd)
exec(cmd, cb)
},
function(stdout, stderr, cb) {
fs.renameSync('bindata_assetfs.go', assetsFileName)
fs.appendFileSync(assetsFileName, '\n')
fs.appendFileSync(assetsFileName, 'var UIReleaseTag = "' + buildType + '.' +
releaseTag + '"\n')
fs.appendFileSync(assetsFileName, 'var UICommitID = "' + commitId + '"\n')
fs.appendFileSync(assetsFileName, 'var UIVersion = "' + version + '"')
fs.appendFileSync(assetsFileName, '\n')
var contents;
if (isProduction) {
contents = fs.readFileSync(assetsFileName, 'utf8')
.replace(/_productionIndexHtml/g, '_productionIndexHTML')
.replace(/productionIndexHtmlBytes/g, 'productionIndexHTMLBytes')
.replace(/productionIndexHtml/g, 'productionIndexHTML')
.replace(/_productionIndex_bundleJs/g, '_productionIndexBundleJs')
.replace(/productionIndex_bundleJsBytes/g, 'productionIndexBundleJsBytes')
.replace(/productionIndex_bundleJs/g, 'productionIndexBundleJs')
.replace(/_productionJqueryUiMinJs/g, '_productionJqueryUIMinJs')
.replace(/productionJqueryUiMinJsBytes/g, 'productionJqueryUIMinJsBytes')
.replace(/productionJqueryUiMinJs/g, 'productionJqueryUIMinJs');
} else {
contents = fs.readFileSync(assetsFileName, 'utf8')
.replace(/_devIndexHtml/g, '_devIndexHTML')
.replace(/devIndexHtmlBytes/g, 'devIndexHTMLBytes')
.replace(/devIndexHtml/g, 'devIndexHTML')
.replace(/_devIndex_bundleJs/g, '_devIndexBundleJs')
.replace(/devIndex_bundleJsBytes/g, 'devIndexBundleJsBytes')
.replace(/devIndex_bundleJs/g, 'devIndexBundleJs')
.replace(/_devJqueryUiMinJs/g, '_devJqueryUIMinJs')
.replace(/devJqueryUiMinJsBytes/g, 'devJqueryUIMinJsBytes')
.replace(/devJqueryUiMinJs/g, 'devJqueryUIMinJs');
}
contents = contents.replace(/MINIO_UI_VERSION/g, version)
contents = contents.replace(/index_bundle.js/g, 'index_bundle-' + releaseTag + '.js')
fs.writeFileSync(assetsFileName, contents, 'utf8')
console.log('UI assets file :', assetsFileName)
cb()
}
], function(err) { ], function(err) {
if (err) return console.log(err) if (err) return console.log(err)
}) })
+21429 -24
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -6,7 +6,7 @@
"test": "jest", "test": "jest",
"dev": "NODE_ENV=dev webpack-dev-server --devtool cheap-module-eval-source-map --progress --colors --hot --content-base dev", "dev": "NODE_ENV=dev webpack-dev-server --devtool cheap-module-eval-source-map --progress --colors --hot --content-base dev",
"build": "NODE_ENV=dev node build.js", "build": "NODE_ENV=dev node build.js",
"release": "NODE_ENV=production MINIO_UI_BUILD=RELEASE node build.js", "release": "NODE_ENV=production node build.js",
"format": "esformatter -i 'app/**/*.js'" "format": "esformatter -i 'app/**/*.js'"
}, },
"jest": { "jest": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

+59
View File
@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MinIO Browser</title>
<link rel="icon" type="image/png" sizes="32x32" href="/minio/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/minio/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/minio/favicon-16x16.png">
<link rel="stylesheet" href="/minio/loader.css" type="text/css">
</head>
<body>
<div class="page-load">
<div class="pl-inner">
<img src="/minio/logo.svg" alt="">
</div>
</div>
<div id="root"></div>
<!--[if lt IE 11]>
<div class="ie-warning">
<div class="iw-inner">
<i class="iwi-icon fas fa-exclamation-triangle"></i>
You are using Internet Explorer version 12.0 or lower. Due to security issues and lack of support for Web Standards it is highly recommended that you upgrade to a modern browser
<ul>
<li>
<a href="http://www.google.com/chrome/">
<img src="chrome.png" alt="">
<div>Chrome</div>
</a>
</li>
<li>
<a href="https://www.mozilla.org/en-US/firefox/new/">
<img src="firefox.png" alt="">
<div>Firefox</div>
</a>
</li>
<li>
<a href="https://www.apple.com/safari/">
<img src="safari.png" alt="">
<div>Safari</div>
</a>
</li>
</ul>
<div class="iwi-skip">Skip & Continue</div>
</div>
</div>
<![endif]-->
<script>currentUiVersion = 'MINIO_UI_VERSION'</script>
<script src="/minio/index_bundle.js"></script>
</body>
</html>
File diff suppressed because one or more lines are too long
+98
View File
@@ -0,0 +1,98 @@
.page-load {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: #002a37;
z-index: 100;
transition: opacity 200ms;
-webkit-transition: opacity 200ms;
}
.pl-0{
opacity: 0;
}
.pl-1 {
display: none;
}
.pl-inner {
position: absolute;
width: 100px;
height: 100px;
left: 50%;
margin-left: -50px;
top: 50%;
margin-top: -50px;
text-align: center;
-webkit-animation: fade-in 500ms;
animation: fade-in 500ms;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
animation-delay: 350ms;
-webkit-animation-delay: 350ms;
-webkit-backface-visibility: visible;
backface-visibility: visible;
}
.pl-inner:before {
content: '';
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
display: block;
-webkit-animation: spin 1000ms infinite linear;
animation: spin 1000ms infinite linear;
border: 1px solid rgba(255, 255, 255, 0.2);;
border-left-color: #fff;
border-radius: 50%;
}
.pl-inner > img {
width: 30px;
margin-top: 21px;
}
@-webkit-keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="93px" height="187px" viewBox="0 0 93 187" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>logo</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="logo" transform="translate(0.187500, -0.683594)" fill="#FFFFFF" fill-rule="nonzero">
<path d="M91.49,46.551 C86.7827023,38.7699609 82.062696,30.9966172 77.33,23.231 C74.87,19.231 72.33,15.231 69.88,11.231 C69.57,10.731 69.18,10.291 68.88,9.831 C64.35,2.931 55.44,-1.679 46.73,2.701 C42.9729806,4.51194908 40.0995718,7.75449451 38.7536428,11.7020516 C37.4077139,15.6496086 37.701799,19.9721186 39.57,23.701 C41.08,26.641 43.57,29.121 45.91,31.581 C53.03,39.141 60.38,46.491 67.45,54.111 C72.4175495,59.4492221 74.4526451,66.8835066 72.8965704,74.0075359 C71.3404956,81.1315653 66.390952,87.0402215 59.65,89.821 C59.4938176,89.83842 59.3361824,89.83842 59.18,89.821 L59.18,54.591 C46.6388051,61.0478363 35.3944735,69.759905 26.01,80.291 C11.32,96.671 2.64,117.141 0.01,132.071 L23.96,119.821 C31.96,115.771 39.86,111.821 48.14,107.581 L48.14,175.921 L59.14,187.131 L59.14,101.831 C59.14,101.831 59.39,101.711 60.22,101.261 C63.5480598,99.6738911 66.7772674,97.8873078 69.89,95.911 C77.7130888,90.4306687 82.7479457,81.8029342 83.6709542,72.295947 C84.5939627,62.7889599 81.3127806,53.3538429 74.69,46.471 C66.49,37.891 58.24,29.351 50.05,20.761 C47.67,18.261 47.72,15.101 50.05,12.881 C52.38,10.661 55.56,10.881 57.96,13.331 L61.38,16.781 C64.1,19.681 66.79,22.611 69.53,25.481 C76.4547149,32.7389629 83.3947303,39.9823123 90.35,47.211 C90.7,47.571 91.12,47.871 91.5,48.211 L91.93,47.951 C91.8351945,47.4695902 91.6876376,47.0000911 91.49,46.551 Z M48.11,94.931 C47.9883217,95.5022568 47.6230065,95.9917791 47.11,96.271 C42.72,98.601 38.29,100.871 33.87,103.141 L17.76,111.401 C24.771203,96.7435071 35.1132853,83.9289138 47.96,73.981 C48.08,74.221 48.16,74.301 48.16,74.381 C48.15,81.231 48.17,88.081 48.11,94.931 Z" id="Shape"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -21,7 +21,7 @@ _init() {
## Minimum required versions for build dependencies ## Minimum required versions for build dependencies
GIT_VERSION="1.0" GIT_VERSION="1.0"
GO_VERSION="1.13" GO_VERSION="1.16"
OSX_VERSION="10.8" OSX_VERSION="10.8"
KNAME=$(uname -s) KNAME=$(uname -s)
ARCH=$(uname -m) ARCH=$(uname -m)
+1 -1
View File
@@ -9,7 +9,7 @@ function _init() {
export CGO_ENABLED=0 export CGO_ENABLED=0
## List of architectures and OS to test coss compilation. ## List of architectures and OS to test coss compilation.
SUPPORTED_OSARCH="linux/ppc64le linux/mips64 linux/arm64 linux/s390x darwin/amd64 freebsd/amd64 windows/amd64 linux/arm linux/386 netbsd/amd64" SUPPORTED_OSARCH="linux/ppc64le linux/mips64 linux/arm64 linux/s390x darwin/arm64 darwin/amd64 freebsd/amd64 windows/amd64 linux/arm linux/386 netbsd/amd64"
} }
function _build() { function _build() {
+1 -1
View File
@@ -3,5 +3,5 @@
set -e set -e
for d in $(go list ./... | grep -v browser); do for d in $(go list ./... | grep -v browser); do
CGO_ENABLED=1 go test -v -race --timeout 100m "$d" CGO_ENABLED=1 go test -v -tags kqueue -race --timeout 100m "$d"
done done
+2 -3
View File
@@ -20,7 +20,6 @@ import (
"encoding/xml" "encoding/xml"
"io" "io"
"net/http" "net/http"
"net/url"
"github.com/gorilla/mux" "github.com/gorilla/mux"
xhttp "github.com/minio/minio/cmd/http" xhttp "github.com/minio/minio/cmd/http"
@@ -180,7 +179,7 @@ func (api objectAPIHandlers) PutObjectACLHandler(w http.ResponseWriter, r *http.
vars := mux.Vars(r) vars := mux.Vars(r)
bucket := vars["bucket"] bucket := vars["bucket"]
object, err := url.PathUnescape(vars["object"]) object, err := unescapePath(vars["object"])
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return return
@@ -244,7 +243,7 @@ func (api objectAPIHandlers) GetObjectACLHandler(w http.ResponseWriter, r *http.
vars := mux.Vars(r) vars := mux.Vars(r)
bucket := vars["bucket"] bucket := vars["bucket"]
object, err := url.PathUnescape(vars["object"]) object, err := unescapePath(vars["object"])
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return return
+43 -12
View File
@@ -23,8 +23,10 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"path" "path"
"sort"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/minio/minio/cmd/config/dns"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
iampolicy "github.com/minio/minio/pkg/iam/policy" iampolicy "github.com/minio/minio/pkg/iam/policy"
@@ -66,7 +68,7 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
accessKey := vars["accessKey"] accessKey := vars["accessKey"]
ok, err := globalIAMSys.IsTempUser(accessKey) ok, _, err := globalIAMSys.IsTempUser(accessKey)
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return return
@@ -405,6 +407,18 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
} }
} }
if implicitPerm && !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: accessKey,
Action: iampolicy.CreateUserAdminAction,
ConditionValues: getConditionValues(r, "", accessKey, claims),
IsOwner: owner,
Claims: claims,
DenyOnly: true, // check if changing password is explicitly denied.
}) {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return
}
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 { if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
// More than maxConfigSize bytes were available // More than maxConfigSize bytes were available
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
@@ -483,7 +497,7 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
parentUser = cred.ParentUser parentUser = cred.ParentUser
} }
newCred, err := globalIAMSys.NewServiceAccount(ctx, parentUser, createReq.Policy) newCred, err := globalIAMSys.NewServiceAccount(ctx, parentUser, cred.Groups, createReq.Policy)
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return return
@@ -688,12 +702,6 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
return rd, wr return rd, wr
} }
buckets, err := objectAPI.ListBuckets(ctx)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
// Load the latest calculated data usage // Load the latest calculated data usage
dataUsageInfo, err := loadDataUsageFromBackend(ctx, objectAPI) dataUsageInfo, err := loadDataUsageFromBackend(ctx, objectAPI)
if err != nil { if err != nil {
@@ -701,11 +709,34 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
} }
accountName := cred.AccessKey // If etcd, dns federation configured list buckets from etcd.
if cred.ParentUser != "" { var buckets []BucketInfo
accountName = cred.ParentUser if globalDNSConfig != nil && globalBucketFederation {
dnsBuckets, err := globalDNSConfig.List()
if err != nil && !IsErrIgnored(err,
dns.ErrNoEntriesFound,
dns.ErrDomainMissing) {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
for _, dnsRecords := range dnsBuckets {
buckets = append(buckets, BucketInfo{
Name: dnsRecords[0].Key,
Created: dnsRecords[0].CreationDate,
})
}
sort.Slice(buckets, func(i, j int) bool {
return buckets[i].Name < buckets[j].Name
})
} else {
buckets, err = objectAPI.ListBuckets(ctx)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
} }
accountName := cred.AccessKey
policies, err := globalIAMSys.PolicyDBGet(accountName, false) policies, err := globalIAMSys.PolicyDBGet(accountName, false)
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
@@ -963,7 +994,7 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http
isGroup := vars["isGroup"] == "true" isGroup := vars["isGroup"] == "true"
if !isGroup { if !isGroup {
ok, err := globalIAMSys.IsTempUser(entityName) ok, _, err := globalIAMSys.IsTempUser(entityName)
if err != nil && err != errNoSuchUser { if err != nil && err != errNoSuchUser {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return return
+70 -71
View File
@@ -259,6 +259,7 @@ type ServerHTTPAPIStats struct {
// ServerHTTPStats holds all type of http operations performed to/from the server // ServerHTTPStats holds all type of http operations performed to/from the server
// including their average execution time. // including their average execution time.
type ServerHTTPStats struct { type ServerHTTPStats struct {
S3RequestsInQueue int32 `json:"s3RequestsInQueue"`
CurrentS3Requests ServerHTTPAPIStats `json:"currentS3Requests"` CurrentS3Requests ServerHTTPAPIStats `json:"currentS3Requests"`
TotalS3Requests ServerHTTPAPIStats `json:"totalS3Requests"` TotalS3Requests ServerHTTPAPIStats `json:"totalS3Requests"`
TotalS3Errors ServerHTTPAPIStats `json:"totalS3Errors"` TotalS3Errors ServerHTTPAPIStats `json:"totalS3Errors"`
@@ -296,7 +297,7 @@ func (a adminAPIHandlers) StorageInfoHandler(w http.ResponseWriter, r *http.Requ
storageInfo, _ := objectAPI.StorageInfo(ctx) storageInfo, _ := objectAPI.StorageInfo(ctx)
// Collect any disk healing. // Collect any disk healing.
healing, _ := getAggregatedBackgroundHealState(ctx) healing, _ := getAggregatedBackgroundHealState(ctx, nil)
healDisks := make(map[string]struct{}, len(healing.HealDisks)) healDisks := make(map[string]struct{}, len(healing.HealDisks))
for _, disk := range healing.HealDisks { for _, disk := range healing.HealDisks {
healDisks[disk] = struct{}{} healDisks[disk] = struct{}{}
@@ -860,16 +861,14 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
keepConnLive(w, r, respCh) keepConnLive(w, r, respCh)
} }
func getAggregatedBackgroundHealState(ctx context.Context) (madmin.BgHealState, error) { // getAggregatedBackgroundHealState returns the heal state of disks.
var bgHealStates []madmin.BgHealState // If no ObjectLayer is provided no set status is returned.
func getAggregatedBackgroundHealState(ctx context.Context, o ObjectLayer) (madmin.BgHealState, error) {
localHealState, ok := getLocalBackgroundHealStatus()
if !ok {
return madmin.BgHealState{}, errServerNotInitialized
}
// Get local heal status first // Get local heal status first
bgHealStates = append(bgHealStates, localHealState) bgHealStates, ok := getBackgroundHealStatus(ctx, o)
if !ok {
return bgHealStates, errServerNotInitialized
}
if globalIsDistErasure { if globalIsDistErasure {
// Get heal status from other peers // Get heal status from other peers
@@ -884,33 +883,10 @@ func getAggregatedBackgroundHealState(ctx context.Context) (madmin.BgHealState,
if errCount == len(nerrs) { if errCount == len(nerrs) {
return madmin.BgHealState{}, fmt.Errorf("all remote servers failed to report heal status, cluster is unhealthy") return madmin.BgHealState{}, fmt.Errorf("all remote servers failed to report heal status, cluster is unhealthy")
} }
bgHealStates = append(bgHealStates, peersHealStates...) bgHealStates.Merge(peersHealStates...)
} }
// Aggregate healing result return bgHealStates, nil
var aggregatedHealStateResult = madmin.BgHealState{
ScannedItemsCount: bgHealStates[0].ScannedItemsCount,
LastHealActivity: bgHealStates[0].LastHealActivity,
NextHealRound: bgHealStates[0].NextHealRound,
HealDisks: bgHealStates[0].HealDisks,
}
bgHealStates = bgHealStates[1:]
for _, state := range bgHealStates {
aggregatedHealStateResult.ScannedItemsCount += state.ScannedItemsCount
aggregatedHealStateResult.HealDisks = append(aggregatedHealStateResult.HealDisks, state.HealDisks...)
if !state.LastHealActivity.IsZero() && aggregatedHealStateResult.LastHealActivity.Before(state.LastHealActivity) {
aggregatedHealStateResult.LastHealActivity = state.LastHealActivity
// The node which has the last heal activity means its
// is the node that is orchestrating self healing operations,
// which also means it is the same node which decides when
// the next self healing operation will be done.
aggregatedHealStateResult.NextHealRound = state.NextHealRound
}
}
return aggregatedHealStateResult, nil
} }
func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r *http.Request) {
@@ -929,7 +905,7 @@ func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r *
return return
} }
aggregateHealStateResult, err := getAggregatedBackgroundHealState(r.Context()) aggregateHealStateResult, err := getAggregatedBackgroundHealState(r.Context(), objectAPI)
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return return
@@ -1347,8 +1323,10 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
deadlinedCtx, cancel := context.WithTimeout(ctx, deadline) deadlinedCtx, cancel := context.WithTimeout(ctx, deadline)
defer cancel() defer cancel()
var err error
nsLock := objectAPI.NewNSLock(minioMetaBucket, "health-check-in-progress") nsLock := objectAPI.NewNSLock(minioMetaBucket, "health-check-in-progress")
if err := nsLock.GetLock(ctx, newDynamicTimeout(deadline, deadline)); err != nil { // returns a locked lock ctx, err = nsLock.GetLock(ctx, newDynamicTimeout(deadline, deadline))
if err != nil { // returns a locked lock
errResp(err) errResp(err)
return return
} }
@@ -1544,19 +1522,19 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
return return
} }
vault := fetchVaultStatus() kmsStat := fetchKMSStatus()
ldap := madmin.LDAP{} ldap := madmin.LDAP{}
if globalLDAPConfig.Enabled { if globalLDAPConfig.Enabled {
ldapConn, err := globalLDAPConfig.Connect() ldapConn, err := globalLDAPConfig.Connect()
if err != nil { if err != nil {
ldap.Status = "offline" ldap.Status = string(madmin.ItemOffline)
} else if ldapConn == nil { } else if ldapConn == nil {
ldap.Status = "Not Configured" ldap.Status = "Not Configured"
} else { } else {
// Close ldap connection to avoid leaks. // Close ldap connection to avoid leaks.
ldapConn.Close() ldapConn.Close()
ldap.Status = "online" ldap.Status = string(madmin.ItemOnline)
} }
} }
@@ -1565,12 +1543,14 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
// Get the notification target info // Get the notification target info
notifyTarget := fetchLambdaInfo() notifyTarget := fetchLambdaInfo()
server := getLocalServerProperty(globalEndpoints, r) local := getLocalServerProperty(globalEndpoints, r)
servers := globalNotificationSys.ServerInfo() servers := globalNotificationSys.ServerInfo()
servers = append(servers, server) servers = append(servers, local)
assignPoolNumbers(servers)
var backend interface{} var backend interface{}
mode := madmin.ObjectLayerInitializing mode := madmin.ItemInitializing
buckets := madmin.Buckets{} buckets := madmin.Buckets{}
objects := madmin.Objects{} objects := madmin.Objects{}
@@ -1578,18 +1558,22 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
objectAPI := newObjectLayerFn() objectAPI := newObjectLayerFn()
if objectAPI != nil { if objectAPI != nil {
mode = madmin.ObjectLayerOnline mode = madmin.ItemOnline
// Load data usage // Load data usage
dataUsageInfo, err := loadDataUsageFromBackend(ctx, objectAPI) dataUsageInfo, err := loadDataUsageFromBackend(ctx, objectAPI)
if err == nil { if err == nil {
buckets = madmin.Buckets{Count: dataUsageInfo.BucketsCount} buckets = madmin.Buckets{Count: dataUsageInfo.BucketsCount}
objects = madmin.Objects{Count: dataUsageInfo.ObjectsTotalCount} objects = madmin.Objects{Count: dataUsageInfo.ObjectsTotalCount}
usage = madmin.Usage{Size: dataUsageInfo.ObjectsTotalSize} usage = madmin.Usage{Size: dataUsageInfo.ObjectsTotalSize}
} else {
buckets = madmin.Buckets{Error: err.Error()}
objects = madmin.Objects{Error: err.Error()}
usage = madmin.Usage{Error: err.Error()}
} }
// Fetching the backend information // Fetching the backend information
backendInfo := objectAPI.BackendInfo() backendInfo := objectAPI.BackendInfo()
if backendInfo.Type == BackendType(madmin.Erasure) { if backendInfo.Type == madmin.Erasure {
// Calculate the number of online/offline disks of all nodes // Calculate the number of online/offline disks of all nodes
var allDisks []madmin.Disk var allDisks []madmin.Disk
for _, s := range servers { for _, s := range servers {
@@ -1613,7 +1597,7 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
domain := globalDomainNames domain := globalDomainNames
services := madmin.Services{ services := madmin.Services{
Vault: vault, KMS: kmsStat,
LDAP: ldap, LDAP: ldap,
Logger: log, Logger: log,
Audit: audit, Audit: audit,
@@ -1621,7 +1605,7 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
} }
infoMsg := madmin.InfoMessage{ infoMsg := madmin.InfoMessage{
Mode: mode, Mode: string(mode),
Domain: domain, Domain: domain,
Region: globalServerRegion, Region: globalServerRegion,
SQSARN: globalNotificationSys.GetARNList(false), SQSARN: globalNotificationSys.GetARNList(false),
@@ -1646,6 +1630,22 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
writeSuccessResponseJSON(w, jsonBytes) writeSuccessResponseJSON(w, jsonBytes)
} }
func assignPoolNumbers(servers []madmin.ServerProperties) {
for i := range servers {
for idx, ge := range globalEndpoints {
for _, endpoint := range ge.Endpoints {
if servers[i].Endpoint == endpoint.Host {
servers[i].PoolNumber = idx + 1
} else if host, err := xnet.ParseHost(servers[i].Endpoint); err == nil {
if host.Name == endpoint.Hostname() {
servers[i].PoolNumber = idx + 1
}
}
}
}
}
}
func fetchLambdaInfo() []map[string][]madmin.TargetIDStatus { func fetchLambdaInfo() []map[string][]madmin.TargetIDStatus {
lambdaMap := make(map[string][]madmin.TargetIDStatus) lambdaMap := make(map[string][]madmin.TargetIDStatus)
@@ -1655,9 +1655,9 @@ func fetchLambdaInfo() []map[string][]madmin.TargetIDStatus {
active, _ := tgt.IsActive() active, _ := tgt.IsActive()
targetID := tgt.ID() targetID := tgt.ID()
if active { if active {
targetIDStatus[targetID.ID] = madmin.Status{Status: "Online"} targetIDStatus[targetID.ID] = madmin.Status{Status: string(madmin.ItemOnline)}
} else { } else {
targetIDStatus[targetID.ID] = madmin.Status{Status: "Offline"} targetIDStatus[targetID.ID] = madmin.Status{Status: string(madmin.ItemOffline)}
} }
list := lambdaMap[targetID.Name] list := lambdaMap[targetID.Name]
list = append(list, targetIDStatus) list = append(list, targetIDStatus)
@@ -1669,9 +1669,9 @@ func fetchLambdaInfo() []map[string][]madmin.TargetIDStatus {
active, _ := tgt.IsActive() active, _ := tgt.IsActive()
targetID := tgt.ID() targetID := tgt.ID()
if active { if active {
targetIDStatus[targetID.ID] = madmin.Status{Status: "Online"} targetIDStatus[targetID.ID] = madmin.Status{Status: string(madmin.ItemOnline)}
} else { } else {
targetIDStatus[targetID.ID] = madmin.Status{Status: "Offline"} targetIDStatus[targetID.ID] = madmin.Status{Status: string(madmin.ItemOffline)}
} }
list := lambdaMap[targetID.Name] list := lambdaMap[targetID.Name]
list = append(list, targetIDStatus) list = append(list, targetIDStatus)
@@ -1689,47 +1689,46 @@ func fetchLambdaInfo() []map[string][]madmin.TargetIDStatus {
return notify return notify
} }
// fetchVaultStatus fetches Vault Info // fetchKMSStatus fetches KMS-related status information.
func fetchVaultStatus() madmin.Vault { func fetchKMSStatus() madmin.KMS {
vault := madmin.Vault{} kmsStat := madmin.KMS{}
if GlobalKMS == nil { if GlobalKMS == nil {
vault.Status = "disabled" kmsStat.Status = "disabled"
return vault return kmsStat
} }
keyID := GlobalKMS.DefaultKeyID() keyID := GlobalKMS.DefaultKeyID()
kmsInfo := GlobalKMS.Info() kmsInfo := GlobalKMS.Info()
if len(kmsInfo.Endpoints) == 0 { if len(kmsInfo.Endpoints) == 0 {
vault.Status = "KMS configured using master key" kmsStat.Status = "KMS configured using master key"
return vault return kmsStat
} }
if err := checkConnection(kmsInfo.Endpoints[0], 15*time.Second); err != nil { if err := checkConnection(kmsInfo.Endpoints[0], 15*time.Second); err != nil {
vault.Status = "offline" kmsStat.Status = string(madmin.ItemOffline)
} else { } else {
vault.Status = "online" kmsStat.Status = string(madmin.ItemOnline)
kmsContext := crypto.Context{"MinIO admin API": "ServerInfoHandler"} // Context for a test key operation kmsContext := crypto.Context{"MinIO admin API": "ServerInfoHandler"} // Context for a test key operation
// 1. Generate a new key using the KMS. // 1. Generate a new key using the KMS.
key, sealedKey, err := GlobalKMS.GenerateKey(keyID, kmsContext) key, sealedKey, err := GlobalKMS.GenerateKey(keyID, kmsContext)
if err != nil { if err != nil {
vault.Encrypt = fmt.Sprintf("Encryption failed: %v", err) kmsStat.Encrypt = fmt.Sprintf("Encryption failed: %v", err)
} else { } else {
vault.Encrypt = "Ok" kmsStat.Encrypt = "success"
} }
// 2. Verify that we can indeed decrypt the (encrypted) key // 2. Verify that we can indeed decrypt the (encrypted) key
decryptedKey, err := GlobalKMS.UnsealKey(keyID, sealedKey, kmsContext) decryptedKey, err := GlobalKMS.UnsealKey(keyID, sealedKey, kmsContext)
switch { switch {
case err != nil: case err != nil:
vault.Decrypt = fmt.Sprintf("Decryption failed: %v", err) kmsStat.Decrypt = fmt.Sprintf("Decryption failed: %v", err)
case subtle.ConstantTimeCompare(key[:], decryptedKey[:]) != 1: case subtle.ConstantTimeCompare(key[:], decryptedKey[:]) != 1:
vault.Decrypt = "Decryption failed: decrypted key does not match generated key" kmsStat.Decrypt = "Decryption failed: decrypted key does not match generated key"
default: default:
vault.Decrypt = "Ok" kmsStat.Decrypt = "success"
} }
} }
return vault return kmsStat
} }
// fetchLoggerDetails return log info // fetchLoggerDetails return log info
@@ -1742,11 +1741,11 @@ func fetchLoggerInfo() ([]madmin.Logger, []madmin.Audit) {
err := checkConnection(target.Endpoint(), 15*time.Second) err := checkConnection(target.Endpoint(), 15*time.Second)
if err == nil { if err == nil {
mapLog := make(map[string]madmin.Status) mapLog := make(map[string]madmin.Status)
mapLog[tgt] = madmin.Status{Status: "Online"} mapLog[tgt] = madmin.Status{Status: string(madmin.ItemOnline)}
loggerInfo = append(loggerInfo, mapLog) loggerInfo = append(loggerInfo, mapLog)
} else { } else {
mapLog := make(map[string]madmin.Status) mapLog := make(map[string]madmin.Status)
mapLog[tgt] = madmin.Status{Status: "offline"} mapLog[tgt] = madmin.Status{Status: string(madmin.ItemOffline)}
loggerInfo = append(loggerInfo, mapLog) loggerInfo = append(loggerInfo, mapLog)
} }
} }
@@ -1758,11 +1757,11 @@ func fetchLoggerInfo() ([]madmin.Logger, []madmin.Audit) {
err := checkConnection(target.Endpoint(), 15*time.Second) err := checkConnection(target.Endpoint(), 15*time.Second)
if err == nil { if err == nil {
mapAudit := make(map[string]madmin.Status) mapAudit := make(map[string]madmin.Status)
mapAudit[tgt] = madmin.Status{Status: "Online"} mapAudit[tgt] = madmin.Status{Status: string(madmin.ItemOnline)}
auditloggerInfo = append(auditloggerInfo, mapAudit) auditloggerInfo = append(auditloggerInfo, mapAudit)
} else { } else {
mapAudit := make(map[string]madmin.Status) mapAudit := make(map[string]madmin.Status)
mapAudit[tgt] = madmin.Status{Status: "Offline"} mapAudit[tgt] = madmin.Status{Status: string(madmin.ItemOffline)}
auditloggerInfo = append(auditloggerInfo, mapAudit) auditloggerInfo = append(auditloggerInfo, mapAudit)
} }
} }
+60 -17
View File
@@ -22,7 +22,6 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"sort" "sort"
"strings"
"sync" "sync"
"time" "time"
@@ -90,8 +89,9 @@ type allHealState struct {
sync.RWMutex sync.RWMutex
// map of heal path to heal sequence // map of heal path to heal sequence
healSeqMap map[string]*healSequence healSeqMap map[string]*healSequence // Indexed by endpoint
healLocalDisks map[Endpoint]struct{} healLocalDisks map[Endpoint]struct{}
healStatus map[string]healingTracker // Indexed by disk ID
} }
// newHealState - initialize global heal state management // newHealState - initialize global heal state management
@@ -99,6 +99,7 @@ func newHealState(cleanup bool) *allHealState {
hstate := &allHealState{ hstate := &allHealState{
healSeqMap: make(map[string]*healSequence), healSeqMap: make(map[string]*healSequence),
healLocalDisks: map[Endpoint]struct{}{}, healLocalDisks: map[Endpoint]struct{}{},
healStatus: make(map[string]healingTracker),
} }
if cleanup { if cleanup {
go hstate.periodicHealSeqsClean(GlobalContext) go hstate.periodicHealSeqsClean(GlobalContext)
@@ -113,7 +114,56 @@ func (ahs *allHealState) healDriveCount() int {
return len(ahs.healLocalDisks) return len(ahs.healLocalDisks)
} }
func (ahs *allHealState) getHealLocalDisks() Endpoints { func (ahs *allHealState) popHealLocalDisks(healLocalDisks ...Endpoint) {
ahs.Lock()
defer ahs.Unlock()
for _, ep := range healLocalDisks {
delete(ahs.healLocalDisks, ep)
}
for id, disk := range ahs.healStatus {
for _, ep := range healLocalDisks {
if disk.Endpoint == ep.String() {
delete(ahs.healStatus, id)
}
}
}
}
// updateHealStatus will update the heal status.
func (ahs *allHealState) updateHealStatus(tracker *healingTracker) {
ahs.Lock()
defer ahs.Unlock()
ahs.healStatus[tracker.ID] = *tracker
}
// Sort by zone, set and disk index
func sortDisks(disks []madmin.Disk) {
sort.Slice(disks, func(i, j int) bool {
a, b := &disks[i], &disks[j]
if a.PoolIndex != b.PoolIndex {
return a.PoolIndex < b.PoolIndex
}
if a.SetIndex != b.SetIndex {
return a.SetIndex < b.SetIndex
}
return a.DiskIndex < b.DiskIndex
})
}
// getLocalHealingDisks returns local healing disks indexed by endpoint.
func (ahs *allHealState) getLocalHealingDisks() map[string]madmin.HealingDisk {
ahs.RLock()
defer ahs.RUnlock()
dst := make(map[string]madmin.HealingDisk, len(ahs.healStatus))
for _, v := range ahs.healStatus {
dst[v.Endpoint] = v.toHealingDisk()
}
return dst
}
func (ahs *allHealState) getHealLocalDiskEndpoints() Endpoints {
ahs.RLock() ahs.RLock()
defer ahs.RUnlock() defer ahs.RUnlock()
@@ -124,15 +174,6 @@ func (ahs *allHealState) getHealLocalDisks() Endpoints {
return endpoints return endpoints
} }
func (ahs *allHealState) popHealLocalDisks(healLocalDisks ...Endpoint) {
ahs.Lock()
defer ahs.Unlock()
for _, ep := range healLocalDisks {
delete(ahs.healLocalDisks, ep)
}
}
func (ahs *allHealState) pushHealLocalDisks(healLocalDisks ...Endpoint) { func (ahs *allHealState) pushHealLocalDisks(healLocalDisks ...Endpoint) {
ahs.Lock() ahs.Lock()
defer ahs.Unlock() defer ahs.Unlock()
@@ -662,6 +703,13 @@ func (h *healSequence) healSequenceStart(objAPI ObjectLayer) {
} }
} }
func (h *healSequence) logHeal(healType madmin.HealItemType) {
h.mutex.Lock()
h.scannedItemsMap[healType]++
h.lastHealActivity = UTCNow()
h.mutex.Unlock()
}
func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItemType) error { func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItemType) error {
globalHealConfigMu.Lock() globalHealConfigMu.Lock()
opts := globalHealConfig opts := globalHealConfig
@@ -834,11 +882,6 @@ func (h *healSequence) healMinioSysMeta(objAPI ObjectLayer, metaPrefix string) f
return errHealStopSignalled return errHealStopSignalled
} }
// Skip metacache entries healing
if strings.HasPrefix(object, "buckets/.minio.sys/.metacache/") {
return nil
}
err := h.queueHealTask(healSource{ err := h.queueHealTask(healSource{
bucket: bucket, bucket: bucket,
object: object, object: object,
+1 -2
View File
@@ -110,10 +110,9 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
// -- IAM APIs -- // -- IAM APIs --
// Add policy IAM // Add policy IAM
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.AddCannedPolicy)).Queries("name", "{name:.*}") adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-canned-policy").HandlerFunc(httpTraceAll(adminAPI.AddCannedPolicy)).Queries("name", "{name:.*}")
// Add user IAM // Add user IAM
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/accountinfo").HandlerFunc(httpTraceAll(adminAPI.AccountInfoHandler)) adminRouter.Methods(http.MethodGet).Path(adminVersion + "/accountinfo").HandlerFunc(httpTraceAll(adminAPI.AccountInfoHandler))
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-user").HandlerFunc(httpTraceHdrs(adminAPI.AddUser)).Queries("accessKey", "{accessKey:.*}") adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-user").HandlerFunc(httpTraceHdrs(adminAPI.AddUser)).Queries("accessKey", "{accessKey:.*}")
+17 -42
View File
@@ -17,9 +17,11 @@
package cmd package cmd
import ( import (
"context"
"net/http" "net/http"
"time" "time"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/madmin" "github.com/minio/minio/pkg/madmin"
) )
@@ -40,65 +42,38 @@ func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Req
} }
if endpoint.IsLocal { if endpoint.IsLocal {
// Only proceed for local endpoints // Only proceed for local endpoints
network[nodeName] = "online" network[nodeName] = string(madmin.ItemOnline)
localEndpoints = append(localEndpoints, endpoint) localEndpoints = append(localEndpoints, endpoint)
continue continue
} }
_, present := network[nodeName] _, present := network[nodeName]
if !present { if !present {
if err := isServerResolvable(endpoint, time.Second); err == nil { if err := isServerResolvable(endpoint, 2*time.Second); err == nil {
network[nodeName] = "online" network[nodeName] = string(madmin.ItemOnline)
} else { } else {
network[nodeName] = "offline" network[nodeName] = string(madmin.ItemOffline)
// log once the error
logger.LogOnceIf(context.Background(), err, nodeName)
} }
} }
} }
} }
localDisks, _ := initStorageDisksWithErrors(localEndpoints) props := madmin.ServerProperties{
defer closeStorageDisks(localDisks) State: string(madmin.ItemInitializing),
storageInfo, _ := getStorageInfo(localDisks, localEndpoints.GetAllStrings())
return madmin.ServerProperties{
State: "ok",
Endpoint: addr, Endpoint: addr,
Uptime: UTCNow().Unix() - globalBootTime.Unix(), Uptime: UTCNow().Unix() - globalBootTime.Unix(),
Version: Version, Version: Version,
CommitID: CommitID, CommitID: CommitID,
Network: network, Network: network,
Disks: storageInfo.Disks,
} }
}
func getLocalDisks(endpointServerPools EndpointServerPools) []madmin.Disk { objLayer := newObjectLayerFn()
var localEndpoints Endpoints if objLayer != nil {
network := make(map[string]string) storageInfo, _ := objLayer.LocalStorageInfo(GlobalContext)
props.State = string(madmin.ItemOnline)
for _, ep := range endpointServerPools { props.Disks = storageInfo.Disks
for _, endpoint := range ep.Endpoints {
nodeName := endpoint.Host
if nodeName == "" {
nodeName = "localhost"
}
if endpoint.IsLocal {
// Only proceed for local endpoints
network[nodeName] = "online"
localEndpoints = append(localEndpoints, endpoint)
continue
}
_, present := network[nodeName]
if !present {
if err := isServerResolvable(endpoint, time.Second); err == nil {
network[nodeName] = "online"
} else {
network[nodeName] = "offline"
}
}
}
} }
localDisks, _ := initStorageDisksWithErrors(localEndpoints)
defer closeStorageDisks(localDisks) return props
storageInfo, _ := getStorageInfo(localDisks, localEndpoints.GetAllStrings())
return storageInfo.Disks
} }
+19 -13
View File
@@ -133,18 +133,20 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSp
} }
// https://github.com/google/security-research/security/advisories/GHSA-76wf-9vgp-pj7w // https://github.com/google/security-research/security/advisories/GHSA-76wf-9vgp-pj7w
if strings.EqualFold(k, xhttp.AmzMetaUnencryptedContentLength) || strings.EqualFold(k, xhttp.AmzMetaUnencryptedContentMD5) { if equals(k, xhttp.AmzMetaUnencryptedContentLength, xhttp.AmzMetaUnencryptedContentMD5) {
continue continue
} }
var isSet bool var isSet bool
for _, userMetadataPrefix := range userMetadataKeyPrefixes { for _, userMetadataPrefix := range userMetadataKeyPrefixes {
if !strings.HasPrefix(k, userMetadataPrefix) { if !strings.HasPrefix(strings.ToLower(k), strings.ToLower(userMetadataPrefix)) {
continue continue
} }
w.Header()[strings.ToLower(k)] = []string{v} w.Header()[strings.ToLower(k)] = []string{v}
isSet = true isSet = true
break break
} }
if !isSet { if !isSet {
w.Header().Set(k, v) w.Header().Set(k, v)
} }
@@ -177,21 +179,25 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSp
if objInfo.VersionID != "" { if objInfo.VersionID != "" {
w.Header()[xhttp.AmzVersionID] = []string{objInfo.VersionID} w.Header()[xhttp.AmzVersionID] = []string{objInfo.VersionID}
} }
if objInfo.ReplicationStatus.String() != "" { if objInfo.ReplicationStatus.String() != "" {
w.Header()[xhttp.AmzBucketReplicationStatus] = []string{objInfo.ReplicationStatus.String()} w.Header()[xhttp.AmzBucketReplicationStatus] = []string{objInfo.ReplicationStatus.String()}
} }
if lc, err := globalLifecycleSys.Get(objInfo.Bucket); err == nil { if lc, err := globalLifecycleSys.Get(objInfo.Bucket); err == nil {
ruleID, expiryTime := lc.PredictExpiryTime(lifecycle.ObjectOpts{ if opts.VersionID == "" {
Name: objInfo.Name, if ruleID, expiryTime := lc.PredictExpiryTime(lifecycle.ObjectOpts{
UserTags: objInfo.UserTags, Name: objInfo.Name,
VersionID: objInfo.VersionID, UserTags: objInfo.UserTags,
ModTime: objInfo.ModTime, VersionID: objInfo.VersionID,
IsLatest: objInfo.IsLatest, ModTime: objInfo.ModTime,
DeleteMarker: objInfo.DeleteMarker, IsLatest: objInfo.IsLatest,
}) DeleteMarker: objInfo.DeleteMarker,
if !expiryTime.IsZero() { SuccessorModTime: objInfo.SuccessorModTime,
w.Header()[xhttp.AmzExpiration] = []string{ }); !expiryTime.IsZero() {
fmt.Sprintf(`expiry-date="%s", rule-id="%s"`, expiryTime.Format(http.TimeFormat), ruleID), w.Header()[xhttp.AmzExpiration] = []string{
fmt.Sprintf(`expiry-date="%s", rule-id="%s"`, expiryTime.Format(http.TimeFormat), ruleID),
}
} }
} }
if objInfo.TransitionStatus == lifecycle.TransitionComplete { if objInfo.TransitionStatus == lifecycle.TransitionComplete {
+52 -8
View File
@@ -36,8 +36,19 @@ func getListObjectsV1Args(values url.Values) (prefix, marker, delimiter string,
maxkeys = maxObjectList maxkeys = maxObjectList
} }
prefix = values.Get("prefix") var err error
marker = values.Get("marker") prefix, err = unescapePath(values.Get("prefix"))
if err != nil {
errCode = ErrInvalidRequest
return
}
marker, err = unescapePath(values.Get("marker"))
if err != nil {
errCode = ErrInvalidRequest
return
}
delimiter = values.Get("delimiter") delimiter = values.Get("delimiter")
encodingType = values.Get("encoding-type") encodingType = values.Get("encoding-type")
return return
@@ -56,8 +67,19 @@ func getListBucketObjectVersionsArgs(values url.Values) (prefix, marker, delimit
maxkeys = maxObjectList maxkeys = maxObjectList
} }
prefix = values.Get("prefix") var err error
marker = values.Get("key-marker") prefix, err = unescapePath(values.Get("prefix"))
if err != nil {
errCode = ErrInvalidRequest
return
}
marker, err = unescapePath(values.Get("key-marker"))
if err != nil {
errCode = ErrInvalidRequest
return
}
delimiter = values.Get("delimiter") delimiter = values.Get("delimiter")
encodingType = values.Get("encoding-type") encodingType = values.Get("encoding-type")
versionIDMarker = values.Get("version-id-marker") versionIDMarker = values.Get("version-id-marker")
@@ -86,8 +108,19 @@ func getListObjectsV2Args(values url.Values) (prefix, token, startAfter, delimit
maxkeys = maxObjectList maxkeys = maxObjectList
} }
prefix = values.Get("prefix") var err error
startAfter = values.Get("start-after") prefix, err = unescapePath(values.Get("prefix"))
if err != nil {
errCode = ErrInvalidRequest
return
}
startAfter, err = unescapePath(values.Get("start-after"))
if err != nil {
errCode = ErrInvalidRequest
return
}
delimiter = values.Get("delimiter") delimiter = values.Get("delimiter")
fetchOwner = values.Get("fetch-owner") == "true" fetchOwner = values.Get("fetch-owner") == "true"
encodingType = values.Get("encoding-type") encodingType = values.Get("encoding-type")
@@ -117,8 +150,19 @@ func getBucketMultipartResources(values url.Values) (prefix, keyMarker, uploadID
maxUploads = maxUploadsList maxUploads = maxUploadsList
} }
prefix = values.Get("prefix") var err error
keyMarker = values.Get("key-marker") prefix, err = unescapePath(values.Get("prefix"))
if err != nil {
errCode = ErrInvalidRequest
return
}
keyMarker, err = unescapePath(values.Get("key-marker"))
if err != nil {
errCode = ErrInvalidRequest
return
}
uploadIDMarker = values.Get("upload-id-marker") uploadIDMarker = values.Get("upload-id-marker")
delimiter = values.Get("delimiter") delimiter = values.Get("delimiter")
encodingType = values.Get("encoding-type") encodingType = values.Get("encoding-type")
+33 -14
View File
@@ -48,6 +48,12 @@ type LocationResponse struct {
Location string `xml:",chardata"` Location string `xml:",chardata"`
} }
// PolicyStatus captures information returned by GetBucketPolicyStatusHandler
type PolicyStatus struct {
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ PolicyStatus" json:"-"`
IsPublic string
}
// ListVersionsResponse - format for list bucket versions response. // ListVersionsResponse - format for list bucket versions response.
type ListVersionsResponse struct { type ListVersionsResponse struct {
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListVersionsResult" json:"-"` XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListVersionsResult" json:"-"`
@@ -410,9 +416,11 @@ func getObjectLocation(r *http.Request, domains []string, bucket, object string)
func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse { func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
listbuckets := make([]Bucket, 0, len(buckets)) listbuckets := make([]Bucket, 0, len(buckets))
var data = ListBucketsResponse{} var data = ListBucketsResponse{}
var owner = Owner{} var owner = Owner{
ID: globalMinioDefaultOwnerID,
DisplayName: "minio",
}
owner.ID = globalMinioDefaultOwnerID
for _, bucket := range buckets { for _, bucket := range buckets {
var listbucket = Bucket{} var listbucket = Bucket{}
listbucket.Name = bucket.Name listbucket.Name = bucket.Name
@@ -429,10 +437,12 @@ func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
// generates an ListBucketVersions response for the said bucket with other enumerated options. // 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 { func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delimiter, encodingType string, maxKeys int, resp ListObjectVersionsInfo) ListVersionsResponse {
versions := make([]ObjectVersion, 0, len(resp.Objects)) versions := make([]ObjectVersion, 0, len(resp.Objects))
var owner = Owner{} var owner = Owner{
ID: globalMinioDefaultOwnerID,
DisplayName: "minio",
}
var data = ListVersionsResponse{} var data = ListVersionsResponse{}
owner.ID = globalMinioDefaultOwnerID
for _, object := range resp.Objects { for _, object := range resp.Objects {
var content = ObjectVersion{} var content = ObjectVersion{}
if object.Name == "" { if object.Name == "" {
@@ -485,10 +495,12 @@ func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delim
// generates an ListObjectsV1 response for the said bucket with other enumerated options. // 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 { func generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingType string, maxKeys int, resp ListObjectsInfo) ListObjectsResponse {
contents := make([]Object, 0, len(resp.Objects)) contents := make([]Object, 0, len(resp.Objects))
var owner = Owner{} var owner = Owner{
ID: globalMinioDefaultOwnerID,
DisplayName: "minio",
}
var data = ListObjectsResponse{} var data = ListObjectsResponse{}
owner.ID = globalMinioDefaultOwnerID
for _, object := range resp.Objects { for _, object := range resp.Objects {
var content = Object{} var content = Object{}
if object.Name == "" { if object.Name == "" {
@@ -532,12 +544,11 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingTy
// generates an ListObjectsV2 response for the said bucket with other enumerated options. // 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 { 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)) contents := make([]Object, 0, len(objects))
var owner = Owner{} var owner = Owner{
var data = ListObjectsV2Response{} ID: globalMinioDefaultOwnerID,
DisplayName: "minio",
if fetchOwner {
owner.ID = globalMinioDefaultOwnerID
} }
var data = ListObjectsV2Response{}
for _, object := range objects { for _, object := range objects {
var content = Object{} var content = Object{}
@@ -565,7 +576,7 @@ func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter,
continue continue
} }
// https://github.com/google/security-research/security/advisories/GHSA-76wf-9vgp-pj7w // https://github.com/google/security-research/security/advisories/GHSA-76wf-9vgp-pj7w
if strings.EqualFold(k, xhttp.AmzMetaUnencryptedContentLength) || strings.EqualFold(k, xhttp.AmzMetaUnencryptedContentMD5) { if equals(k, xhttp.AmzMetaUnencryptedContentLength, xhttp.AmzMetaUnencryptedContentMD5) {
continue continue
} }
content.UserMetadata[k] = v content.UserMetadata[k] = v
@@ -639,8 +650,16 @@ func generateListPartsResponse(partsInfo ListPartsInfo, encodingType string) Lis
listPartsResponse.Key = s3EncodeName(partsInfo.Object, encodingType) listPartsResponse.Key = s3EncodeName(partsInfo.Object, encodingType)
listPartsResponse.UploadID = partsInfo.UploadID listPartsResponse.UploadID = partsInfo.UploadID
listPartsResponse.StorageClass = globalMinioDefaultStorageClass listPartsResponse.StorageClass = globalMinioDefaultStorageClass
listPartsResponse.Initiator.ID = globalMinioDefaultOwnerID
listPartsResponse.Owner.ID = globalMinioDefaultOwnerID // Dumb values not meaningful
listPartsResponse.Initiator = Initiator{
ID: globalMinioDefaultOwnerID,
DisplayName: globalMinioDefaultOwnerID,
}
listPartsResponse.Owner = Owner{
ID: globalMinioDefaultOwnerID,
DisplayName: globalMinioDefaultOwnerID,
}
listPartsResponse.MaxParts = partsInfo.MaxParts listPartsResponse.MaxParts = partsInfo.MaxParts
listPartsResponse.PartNumberMarker = partsInfo.PartNumberMarker listPartsResponse.PartNumberMarker = partsInfo.PartNumberMarker
+9 -5
View File
@@ -185,6 +185,10 @@ func registerAPIRouter(router *mux.Router) {
bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(
collectAPIStats("deleteobject", maxClients(httpTraceAll(api.DeleteObjectHandler)))) collectAPIStats("deleteobject", maxClients(httpTraceAll(api.DeleteObjectHandler))))
// PostRestoreObject
bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(
collectAPIStats("restoreobject", maxClients(httpTraceAll(api.PostRestoreObjectHandler)))).Queries("restore", "")
/// Bucket operations /// Bucket operations
// GetBucketLocation // GetBucketLocation
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
@@ -262,9 +266,9 @@ func registerAPIRouter(router *mux.Router) {
// ListObjectVersions // ListObjectVersions
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("listobjectversions", maxClients(httpTraceAll(api.ListObjectVersionsHandler)))).Queries("versions", "") collectAPIStats("listobjectversions", maxClients(httpTraceAll(api.ListObjectVersionsHandler)))).Queries("versions", "")
// ListObjectsV1 (Legacy) // GetBucketPolicyStatus
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("listobjectsv1", maxClients(httpTraceAll(api.ListObjectsV1Handler)))) collectAPIStats("getpolicystatus", maxClients(httpTraceAll(api.GetBucketPolicyStatusHandler)))).Queries("policyStatus", "")
// PutBucketLifecycle // PutBucketLifecycle
bucket.Methods(http.MethodPut).HandlerFunc( bucket.Methods(http.MethodPut).HandlerFunc(
collectAPIStats("putbucketlifecycle", maxClients(httpTraceAll(api.PutBucketLifecycleHandler)))).Queries("lifecycle", "") collectAPIStats("putbucketlifecycle", maxClients(httpTraceAll(api.PutBucketLifecycleHandler)))).Queries("lifecycle", "")
@@ -320,9 +324,9 @@ func registerAPIRouter(router *mux.Router) {
// DeleteBucket // DeleteBucket
bucket.Methods(http.MethodDelete).HandlerFunc( bucket.Methods(http.MethodDelete).HandlerFunc(
collectAPIStats("deletebucket", maxClients(httpTraceAll(api.DeleteBucketHandler)))) collectAPIStats("deletebucket", maxClients(httpTraceAll(api.DeleteBucketHandler))))
// PostRestoreObject // ListObjectsV1 (Legacy)
bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("restoreobject", maxClients(httpTraceAll(api.PostRestoreObjectHandler)))).Queries("restore", "") collectAPIStats("listobjectsv1", maxClients(httpTraceAll(api.ListObjectsV1Handler))))
} }
/// Root operation /// Root operation
+9 -13
View File
@@ -36,6 +36,7 @@ import (
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
objectlock "github.com/minio/minio/pkg/bucket/object/lock" objectlock "github.com/minio/minio/pkg/bucket/object/lock"
"github.com/minio/minio/pkg/bucket/policy" "github.com/minio/minio/pkg/bucket/policy"
"github.com/minio/minio/pkg/etag"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
iampolicy "github.com/minio/minio/pkg/iam/policy" iampolicy "github.com/minio/minio/pkg/iam/policy"
) )
@@ -185,12 +186,12 @@ func getSessionToken(r *http.Request) (token string) {
// Fetch claims in the security token returned by the client, doesn't return // Fetch claims in the security token returned by the client, doesn't return
// errors - upon errors the returned claims map will be empty. // errors - upon errors the returned claims map will be empty.
func mustGetClaimsFromToken(r *http.Request) map[string]interface{} { func mustGetClaimsFromToken(r *http.Request) map[string]interface{} {
claims, _ := getClaimsFromToken(r, getSessionToken(r)) claims, _ := getClaimsFromToken(getSessionToken(r))
return claims return claims
} }
// Fetch claims in the security token returned by the client. // Fetch claims in the security token returned by the client.
func getClaimsFromToken(r *http.Request, token string) (map[string]interface{}, error) { func getClaimsFromToken(token string) (map[string]interface{}, error) {
claims := xjwt.NewMapClaims() claims := xjwt.NewMapClaims()
if token == "" { if token == "" {
return claims.Map(), nil return claims.Map(), nil
@@ -237,7 +238,7 @@ func getClaimsFromToken(r *http.Request, token string) (map[string]interface{},
if err != nil { if err != nil {
// Base64 decoding fails, we should log to indicate // Base64 decoding fails, we should log to indicate
// something is malforming the request sent by client. // something is malforming the request sent by client.
logger.LogIf(r.Context(), err, logger.Application) logger.LogIf(GlobalContext, err, logger.Application)
return nil, errAuthentication return nil, errAuthentication
} }
claims.MapClaims[iampolicy.SessionPolicyName] = string(spBytes) claims.MapClaims[iampolicy.SessionPolicyName] = string(spBytes)
@@ -258,7 +259,7 @@ func checkClaimsFromToken(r *http.Request, cred auth.Credentials) (map[string]in
if subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 { if subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
return nil, ErrInvalidToken return nil, ErrInvalidToken
} }
claims, err := getClaimsFromToken(r, token) claims, err := getClaimsFromToken(token)
if err != nil { if err != nil {
return nil, toAPIErrorCode(r.Context(), err) return nil, toAPIErrorCode(r.Context(), err)
} }
@@ -430,19 +431,14 @@ func isReqAuthenticated(ctx context.Context, r *http.Request, region string, sty
return errCode return errCode
} }
var ( clientETag, err := etag.FromContentMD5(r.Header)
err error
contentMD5, contentSHA256 []byte
)
// Extract 'Content-Md5' if present.
contentMD5, err = checkValidMD5(r.Header)
if err != nil { if err != nil {
return ErrInvalidDigest return ErrInvalidDigest
} }
// Extract either 'X-Amz-Content-Sha256' header or 'X-Amz-Content-Sha256' query parameter (if V4 presigned) // Extract either 'X-Amz-Content-Sha256' header or 'X-Amz-Content-Sha256' query parameter (if V4 presigned)
// Do not verify 'X-Amz-Content-Sha256' if skipSHA256. // Do not verify 'X-Amz-Content-Sha256' if skipSHA256.
var contentSHA256 []byte
if skipSHA256 := skipContentSha256Cksum(r); !skipSHA256 && isRequestPresignedSignatureV4(r) { if skipSHA256 := skipContentSha256Cksum(r); !skipSHA256 && isRequestPresignedSignatureV4(r) {
if sha256Sum, ok := r.URL.Query()[xhttp.AmzContentSha256]; ok && len(sha256Sum) > 0 { if sha256Sum, ok := r.URL.Query()[xhttp.AmzContentSha256]; ok && len(sha256Sum) > 0 {
contentSHA256, err = hex.DecodeString(sha256Sum[0]) contentSHA256, err = hex.DecodeString(sha256Sum[0])
@@ -459,8 +455,7 @@ func isReqAuthenticated(ctx context.Context, r *http.Request, region string, sty
// Verify 'Content-Md5' and/or 'X-Amz-Content-Sha256' if present. // Verify 'Content-Md5' and/or 'X-Amz-Content-Sha256' if present.
// The verification happens implicit during reading. // The verification happens implicit during reading.
reader, err := hash.NewReader(r.Body, -1, hex.EncodeToString(contentMD5), reader, err := hash.NewReader(r.Body, -1, clientETag.String(), hex.EncodeToString(contentSHA256), -1)
hex.EncodeToString(contentSHA256), -1, globalCLIContext.StrictS3Compat)
if err != nil { if err != nil {
return toAPIErrorCode(ctx, err) return toAPIErrorCode(ctx, err)
} }
@@ -498,6 +493,7 @@ func setAuthHandler(h http.Handler) http.Handler {
// Validate Authorization header if its valid for JWT request. // Validate Authorization header if its valid for JWT request.
if _, _, authErr := webRequestAuthenticate(r); authErr != nil { if _, _, authErr := webRequestAuthenticate(r); authErr != nil {
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(authErr.Error()))
return return
} }
h.ServeHTTP(w, r) h.ServeHTTP(w, r)
+254 -36
View File
@@ -17,16 +17,23 @@
package cmd package cmd
import ( import (
"bytes"
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"sort" "sort"
"strings"
"sync"
"time" "time"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/color" "github.com/minio/minio/pkg/color"
"github.com/minio/minio/pkg/console" "github.com/minio/minio/pkg/console"
"github.com/minio/minio/pkg/madmin"
) )
const ( const (
@@ -35,10 +42,200 @@ const (
) )
//go:generate msgp -file $GOFILE -unexported //go:generate msgp -file $GOFILE -unexported
type healingTracker struct {
ID string
// future add more tracking capabilities // healingTracker is used to persist healing information during a heal.
type healingTracker struct {
disk StorageAPI `msg:"-"`
ID string
PoolIndex int
SetIndex int
DiskIndex int
Path string
Endpoint string
Started time.Time
LastUpdate time.Time
ObjectsHealed uint64
ObjectsFailed uint64
BytesDone uint64
BytesFailed uint64
// Last object scanned.
Bucket string `json:"-"`
Object string `json:"-"`
// Numbers when current bucket started healing,
// for resuming with correct numbers.
ResumeObjectsHealed uint64 `json:"-"`
ResumeObjectsFailed uint64 `json:"-"`
ResumeBytesDone uint64 `json:"-"`
ResumeBytesFailed uint64 `json:"-"`
// Filled on startup/restarts.
QueuedBuckets []string
// Filled during heal.
HealedBuckets []string
// Add future tracking capabilities
// Be sure that they are included in toHealingDisk
}
// loadHealingTracker will load the healing tracker from the supplied disk.
// The disk ID will be validated against the loaded one.
func loadHealingTracker(ctx context.Context, disk StorageAPI) (*healingTracker, error) {
if disk == nil {
return nil, errors.New("loadHealingTracker: nil disk given")
}
diskID, err := disk.GetDiskID()
if err != nil {
return nil, err
}
b, err := disk.ReadAll(ctx, minioMetaBucket,
pathJoin(bucketMetaPrefix, slashSeparator, healingTrackerFilename))
if err != nil {
return nil, err
}
var h healingTracker
_, err = h.UnmarshalMsg(b)
if err != nil {
return nil, err
}
if h.ID != diskID && h.ID != "" {
return nil, fmt.Errorf("loadHealingTracker: disk id mismatch expected %s, got %s", h.ID, diskID)
}
h.disk = disk
h.ID = diskID
return &h, nil
}
// newHealingTracker will create a new healing tracker for the disk.
func newHealingTracker(disk StorageAPI) *healingTracker {
diskID, _ := disk.GetDiskID()
h := healingTracker{
disk: disk,
ID: diskID,
Path: disk.String(),
Endpoint: disk.Endpoint().String(),
Started: time.Now().UTC(),
}
h.PoolIndex, h.SetIndex, h.DiskIndex = disk.GetDiskLoc()
return &h
}
// update will update the tracker on the disk.
// If the tracker has been deleted an error is returned.
func (h *healingTracker) update(ctx context.Context) error {
if h.disk.Healing() == nil {
return fmt.Errorf("healingTracker: disk %q is not marked as healing", h.ID)
}
if h.ID == "" || h.PoolIndex < 0 || h.SetIndex < 0 || h.DiskIndex < 0 {
h.ID, _ = h.disk.GetDiskID()
h.PoolIndex, h.SetIndex, h.DiskIndex = h.disk.GetDiskLoc()
}
return h.save(ctx)
}
// save will unconditionally save the tracker and will be created if not existing.
func (h *healingTracker) save(ctx context.Context) error {
if h.PoolIndex < 0 || h.SetIndex < 0 || h.DiskIndex < 0 {
// Attempt to get location.
if api := newObjectLayerFn(); api != nil {
if ep, ok := api.(*erasureServerPools); ok {
h.PoolIndex, h.SetIndex, h.DiskIndex, _ = ep.getPoolAndSet(h.ID)
}
}
}
h.LastUpdate = time.Now().UTC()
htrackerBytes, err := h.MarshalMsg(nil)
if err != nil {
return err
}
globalBackgroundHealState.updateHealStatus(h)
return h.disk.WriteAll(ctx, minioMetaBucket,
pathJoin(bucketMetaPrefix, slashSeparator, healingTrackerFilename),
htrackerBytes)
}
// delete the tracker on disk.
func (h *healingTracker) delete(ctx context.Context) error {
return h.disk.Delete(ctx, minioMetaBucket,
pathJoin(bucketMetaPrefix, slashSeparator, healingTrackerFilename),
false)
}
func (h *healingTracker) isHealed(bucket string) bool {
for _, v := range h.HealedBuckets {
if v == bucket {
return true
}
}
return false
}
// resume will reset progress to the numbers at the start of the bucket.
func (h *healingTracker) resume() {
h.ObjectsHealed = h.ResumeObjectsHealed
h.ObjectsFailed = h.ResumeObjectsFailed
h.BytesDone = h.ResumeBytesDone
h.BytesFailed = h.ResumeBytesFailed
}
// bucketDone should be called when a bucket is done healing.
// Adds the bucket to the list of healed buckets and updates resume numbers.
func (h *healingTracker) bucketDone(bucket string) {
h.ResumeObjectsHealed = h.ObjectsHealed
h.ResumeObjectsFailed = h.ObjectsFailed
h.ResumeBytesDone = h.BytesDone
h.ResumeBytesFailed = h.BytesFailed
h.HealedBuckets = append(h.HealedBuckets, bucket)
for i, b := range h.QueuedBuckets {
if b == bucket {
// Delete...
h.QueuedBuckets = append(h.QueuedBuckets[:i], h.QueuedBuckets[i+1:]...)
}
}
}
// setQueuedBuckets will add buckets, but exclude any that is already in h.HealedBuckets.
// Order is preserved.
func (h *healingTracker) setQueuedBuckets(buckets []BucketInfo) {
s := set.CreateStringSet(h.HealedBuckets...)
h.QueuedBuckets = make([]string, 0, len(buckets))
for _, b := range buckets {
if !s.Contains(b.Name) {
h.QueuedBuckets = append(h.QueuedBuckets, b.Name)
}
}
}
func (h *healingTracker) printTo(writer io.Writer) {
b, err := json.MarshalIndent(h, "", " ")
if err != nil {
writer.Write([]byte(err.Error()))
}
writer.Write(b)
}
// toHealingDisk converts the information to madmin.HealingDisk
func (h *healingTracker) toHealingDisk() madmin.HealingDisk {
return madmin.HealingDisk{
ID: h.ID,
Endpoint: h.Endpoint,
PoolIndex: h.PoolIndex,
SetIndex: h.SetIndex,
DiskIndex: h.DiskIndex,
Path: h.Path,
Started: h.Started.UTC(),
LastUpdate: h.LastUpdate.UTC(),
ObjectsHealed: h.ObjectsHealed,
ObjectsFailed: h.ObjectsFailed,
BytesDone: h.BytesDone,
BytesFailed: h.BytesFailed,
Bucket: h.Bucket,
Object: h.Object,
QueuedBuckets: h.QueuedBuckets,
HealedBuckets: h.HealedBuckets,
}
} }
func initAutoHeal(ctx context.Context, objAPI ObjectLayer) { func initAutoHeal(ctx context.Context, objAPI ObjectLayer) {
@@ -90,7 +287,7 @@ func getLocalDisksToHeal() (disksToHeal Endpoints) {
disk, _, err := connectEndpoint(endpoint) disk, _, err := connectEndpoint(endpoint)
if errors.Is(err, errUnformattedDisk) { if errors.Is(err, errUnformattedDisk) {
disksToHeal = append(disksToHeal, endpoint) disksToHeal = append(disksToHeal, endpoint)
} else if err == nil && disk != nil && disk.Healing() { } else if err == nil && disk != nil && disk.Healing() != nil {
disksToHeal = append(disksToHeal, disk.Endpoint()) disksToHeal = append(disksToHeal, disk.Endpoint())
} }
} }
@@ -114,7 +311,7 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools, bgSeq
// Perform automatic disk healing when a disk is replaced locally. // Perform automatic disk healing when a disk is replaced locally.
diskCheckTimer := time.NewTimer(defaultMonitorNewDiskInterval) diskCheckTimer := time.NewTimer(defaultMonitorNewDiskInterval)
defer diskCheckTimer.Stop() defer diskCheckTimer.Stop()
wait:
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
@@ -125,7 +322,7 @@ wait:
var erasureSetInPoolDisksToHeal []map[int][]StorageAPI var erasureSetInPoolDisksToHeal []map[int][]StorageAPI
healDisks := globalBackgroundHealState.getHealLocalDisks() healDisks := globalBackgroundHealState.getHealLocalDiskEndpoints()
if len(healDisks) > 0 { if len(healDisks) > 0 {
// Reformat disks // Reformat disks
bgSeq.sourceCh <- healSource{bucket: SlashSeparator} bgSeq.sourceCh <- healSource{bucket: SlashSeparator}
@@ -174,55 +371,76 @@ wait:
buckets, _ := z.ListBuckets(ctx) 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)},
}...)
// Heal latest buckets first. // Heal latest buckets first.
sort.Slice(buckets, func(i, j int) bool { sort.Slice(buckets, func(i, j int) bool {
a, b := strings.HasPrefix(buckets[i].Name, minioMetaBucket), strings.HasPrefix(buckets[j].Name, minioMetaBucket)
if a != b {
return a
}
return buckets[i].Created.After(buckets[j].Created) return buckets[i].Created.After(buckets[j].Created)
}) })
// TODO(klauspost): This will block until all heals are done,
// in the future this should be able to start healing other sets at once.
var wg sync.WaitGroup
for i, setMap := range erasureSetInPoolDisksToHeal { for i, setMap := range erasureSetInPoolDisksToHeal {
i := i
for setIndex, disks := range setMap { for setIndex, disks := range setMap {
for _, disk := range disks { if len(disks) == 0 {
logger.Info("Healing disk '%s' on %s pool", disk, humanize.Ordinal(i+1)) continue
}
wg.Add(1)
go func(setIndex int, disks []StorageAPI) {
defer wg.Done()
for _, disk := range disks {
logger.Info("Healing disk '%v' on %s pool", disk, humanize.Ordinal(i+1))
// So someone changed the drives underneath, healing tracker missing. // So someone changed the drives underneath, healing tracker missing.
if !disk.Healing() { tracker, err := loadHealingTracker(ctx, disk)
logger.Info("Healing tracker missing on '%s', disk was swapped again on %s pool", disk, humanize.Ordinal(i+1))
diskID, err := disk.GetDiskID()
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.Info("Healing tracker missing on '%s', disk was swapped again on %s pool", disk, humanize.Ordinal(i+1))
// reading format.json failed or not found, proceed to look tracker = newHealingTracker(disk)
// for new disks to be healed again, we cannot proceed further.
goto wait
} }
if err := saveHealingTracker(disk, diskID); err != nil { tracker.PoolIndex, tracker.SetIndex, tracker.DiskIndex = disk.GetDiskLoc()
tracker.setQueuedBuckets(buckets)
if err := tracker.save(ctx); err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
// Unable to write healing tracker, permission denied or some // Unable to write healing tracker, permission denied or some
// other unexpected error occurred. Proceed to look for new // other unexpected error occurred. Proceed to look for new
// disks to be healed again, we cannot proceed further. // disks to be healed again, we cannot proceed further.
goto wait return
} }
err = z.serverPools[i].sets[setIndex].healErasureSet(ctx, buckets, 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.LogIf(ctx, tracker.delete(ctx))
// Only upon success pop the healed disk.
globalBackgroundHealState.popHealLocalDisks(disk.Endpoint())
} }
}(setIndex, disks)
lbDisks := z.serverPools[i].sets[setIndex].getOnlineDisks()
if err := healErasureSet(ctx, setIndex, buckets, lbDisks); err != nil {
logger.LogIf(ctx, err)
continue
}
logger.Info("Healing disk '%s' on %s pool complete", disk, humanize.Ordinal(i+1))
if err := disk.Delete(ctx, pathJoin(minioMetaBucket, bucketMetaPrefix),
healingTrackerFilename, false); err != nil && !errors.Is(err, errFileNotFound) {
logger.LogIf(ctx, err)
continue
}
// Only upon success pop the healed disk.
globalBackgroundHealState.popHealLocalDisks(disk.Endpoint())
}
} }
} }
wg.Wait()
} }
} }
} }
+562 -8
View File
@@ -30,6 +30,146 @@ func (z *healingTracker) DecodeMsg(dc *msgp.Reader) (err error) {
err = msgp.WrapError(err, "ID") err = msgp.WrapError(err, "ID")
return return
} }
case "PoolIndex":
z.PoolIndex, err = dc.ReadInt()
if err != nil {
err = msgp.WrapError(err, "PoolIndex")
return
}
case "SetIndex":
z.SetIndex, err = dc.ReadInt()
if err != nil {
err = msgp.WrapError(err, "SetIndex")
return
}
case "DiskIndex":
z.DiskIndex, err = dc.ReadInt()
if err != nil {
err = msgp.WrapError(err, "DiskIndex")
return
}
case "Path":
z.Path, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Path")
return
}
case "Endpoint":
z.Endpoint, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Endpoint")
return
}
case "Started":
z.Started, err = dc.ReadTime()
if err != nil {
err = msgp.WrapError(err, "Started")
return
}
case "LastUpdate":
z.LastUpdate, err = dc.ReadTime()
if err != nil {
err = msgp.WrapError(err, "LastUpdate")
return
}
case "ObjectsHealed":
z.ObjectsHealed, err = dc.ReadUint64()
if err != nil {
err = msgp.WrapError(err, "ObjectsHealed")
return
}
case "ObjectsFailed":
z.ObjectsFailed, err = dc.ReadUint64()
if err != nil {
err = msgp.WrapError(err, "ObjectsFailed")
return
}
case "BytesDone":
z.BytesDone, err = dc.ReadUint64()
if err != nil {
err = msgp.WrapError(err, "BytesDone")
return
}
case "BytesFailed":
z.BytesFailed, err = dc.ReadUint64()
if err != nil {
err = msgp.WrapError(err, "BytesFailed")
return
}
case "Bucket":
z.Bucket, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Bucket")
return
}
case "Object":
z.Object, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Object")
return
}
case "ResumeObjectsHealed":
z.ResumeObjectsHealed, err = dc.ReadUint64()
if err != nil {
err = msgp.WrapError(err, "ResumeObjectsHealed")
return
}
case "ResumeObjectsFailed":
z.ResumeObjectsFailed, err = dc.ReadUint64()
if err != nil {
err = msgp.WrapError(err, "ResumeObjectsFailed")
return
}
case "ResumeBytesDone":
z.ResumeBytesDone, err = dc.ReadUint64()
if err != nil {
err = msgp.WrapError(err, "ResumeBytesDone")
return
}
case "ResumeBytesFailed":
z.ResumeBytesFailed, err = dc.ReadUint64()
if err != nil {
err = msgp.WrapError(err, "ResumeBytesFailed")
return
}
case "QueuedBuckets":
var zb0002 uint32
zb0002, err = dc.ReadArrayHeader()
if err != nil {
err = msgp.WrapError(err, "QueuedBuckets")
return
}
if cap(z.QueuedBuckets) >= int(zb0002) {
z.QueuedBuckets = (z.QueuedBuckets)[:zb0002]
} else {
z.QueuedBuckets = make([]string, zb0002)
}
for za0001 := range z.QueuedBuckets {
z.QueuedBuckets[za0001], err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "QueuedBuckets", za0001)
return
}
}
case "HealedBuckets":
var zb0003 uint32
zb0003, err = dc.ReadArrayHeader()
if err != nil {
err = msgp.WrapError(err, "HealedBuckets")
return
}
if cap(z.HealedBuckets) >= int(zb0003) {
z.HealedBuckets = (z.HealedBuckets)[:zb0003]
} else {
z.HealedBuckets = make([]string, zb0003)
}
for za0002 := range z.HealedBuckets {
z.HealedBuckets[za0002], err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "HealedBuckets", za0002)
return
}
}
default: default:
err = dc.Skip() err = dc.Skip()
if err != nil { if err != nil {
@@ -42,10 +182,10 @@ func (z *healingTracker) DecodeMsg(dc *msgp.Reader) (err error) {
} }
// EncodeMsg implements msgp.Encodable // EncodeMsg implements msgp.Encodable
func (z healingTracker) EncodeMsg(en *msgp.Writer) (err error) { func (z *healingTracker) EncodeMsg(en *msgp.Writer) (err error) {
// map header, size 1 // map header, size 20
// write "ID" // write "ID"
err = en.Append(0x81, 0xa2, 0x49, 0x44) err = en.Append(0xde, 0x0, 0x14, 0xa2, 0x49, 0x44)
if err != nil { if err != nil {
return return
} }
@@ -54,16 +194,283 @@ func (z healingTracker) EncodeMsg(en *msgp.Writer) (err error) {
err = msgp.WrapError(err, "ID") err = msgp.WrapError(err, "ID")
return return
} }
// write "PoolIndex"
err = en.Append(0xa9, 0x50, 0x6f, 0x6f, 0x6c, 0x49, 0x6e, 0x64, 0x65, 0x78)
if err != nil {
return
}
err = en.WriteInt(z.PoolIndex)
if err != nil {
err = msgp.WrapError(err, "PoolIndex")
return
}
// write "SetIndex"
err = en.Append(0xa8, 0x53, 0x65, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78)
if err != nil {
return
}
err = en.WriteInt(z.SetIndex)
if err != nil {
err = msgp.WrapError(err, "SetIndex")
return
}
// write "DiskIndex"
err = en.Append(0xa9, 0x44, 0x69, 0x73, 0x6b, 0x49, 0x6e, 0x64, 0x65, 0x78)
if err != nil {
return
}
err = en.WriteInt(z.DiskIndex)
if err != nil {
err = msgp.WrapError(err, "DiskIndex")
return
}
// write "Path"
err = en.Append(0xa4, 0x50, 0x61, 0x74, 0x68)
if err != nil {
return
}
err = en.WriteString(z.Path)
if err != nil {
err = msgp.WrapError(err, "Path")
return
}
// write "Endpoint"
err = en.Append(0xa8, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74)
if err != nil {
return
}
err = en.WriteString(z.Endpoint)
if err != nil {
err = msgp.WrapError(err, "Endpoint")
return
}
// write "Started"
err = en.Append(0xa7, 0x53, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64)
if err != nil {
return
}
err = en.WriteTime(z.Started)
if err != nil {
err = msgp.WrapError(err, "Started")
return
}
// write "LastUpdate"
err = en.Append(0xaa, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65)
if err != nil {
return
}
err = en.WriteTime(z.LastUpdate)
if err != nil {
err = msgp.WrapError(err, "LastUpdate")
return
}
// write "ObjectsHealed"
err = en.Append(0xad, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
if err != nil {
return
}
err = en.WriteUint64(z.ObjectsHealed)
if err != nil {
err = msgp.WrapError(err, "ObjectsHealed")
return
}
// write "ObjectsFailed"
err = en.Append(0xad, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
if err != nil {
return
}
err = en.WriteUint64(z.ObjectsFailed)
if err != nil {
err = msgp.WrapError(err, "ObjectsFailed")
return
}
// write "BytesDone"
err = en.Append(0xa9, 0x42, 0x79, 0x74, 0x65, 0x73, 0x44, 0x6f, 0x6e, 0x65)
if err != nil {
return
}
err = en.WriteUint64(z.BytesDone)
if err != nil {
err = msgp.WrapError(err, "BytesDone")
return
}
// write "BytesFailed"
err = en.Append(0xab, 0x42, 0x79, 0x74, 0x65, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
if err != nil {
return
}
err = en.WriteUint64(z.BytesFailed)
if err != nil {
err = msgp.WrapError(err, "BytesFailed")
return
}
// write "Bucket"
err = en.Append(0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
if err != nil {
return
}
err = en.WriteString(z.Bucket)
if err != nil {
err = msgp.WrapError(err, "Bucket")
return
}
// write "Object"
err = en.Append(0xa6, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74)
if err != nil {
return
}
err = en.WriteString(z.Object)
if err != nil {
err = msgp.WrapError(err, "Object")
return
}
// write "ResumeObjectsHealed"
err = en.Append(0xb3, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
if err != nil {
return
}
err = en.WriteUint64(z.ResumeObjectsHealed)
if err != nil {
err = msgp.WrapError(err, "ResumeObjectsHealed")
return
}
// write "ResumeObjectsFailed"
err = en.Append(0xb3, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
if err != nil {
return
}
err = en.WriteUint64(z.ResumeObjectsFailed)
if err != nil {
err = msgp.WrapError(err, "ResumeObjectsFailed")
return
}
// write "ResumeBytesDone"
err = en.Append(0xaf, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x44, 0x6f, 0x6e, 0x65)
if err != nil {
return
}
err = en.WriteUint64(z.ResumeBytesDone)
if err != nil {
err = msgp.WrapError(err, "ResumeBytesDone")
return
}
// write "ResumeBytesFailed"
err = en.Append(0xb1, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
if err != nil {
return
}
err = en.WriteUint64(z.ResumeBytesFailed)
if err != nil {
err = msgp.WrapError(err, "ResumeBytesFailed")
return
}
// write "QueuedBuckets"
err = en.Append(0xad, 0x51, 0x75, 0x65, 0x75, 0x65, 0x64, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x73)
if err != nil {
return
}
err = en.WriteArrayHeader(uint32(len(z.QueuedBuckets)))
if err != nil {
err = msgp.WrapError(err, "QueuedBuckets")
return
}
for za0001 := range z.QueuedBuckets {
err = en.WriteString(z.QueuedBuckets[za0001])
if err != nil {
err = msgp.WrapError(err, "QueuedBuckets", za0001)
return
}
}
// write "HealedBuckets"
err = en.Append(0xad, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x73)
if err != nil {
return
}
err = en.WriteArrayHeader(uint32(len(z.HealedBuckets)))
if err != nil {
err = msgp.WrapError(err, "HealedBuckets")
return
}
for za0002 := range z.HealedBuckets {
err = en.WriteString(z.HealedBuckets[za0002])
if err != nil {
err = msgp.WrapError(err, "HealedBuckets", za0002)
return
}
}
return return
} }
// MarshalMsg implements msgp.Marshaler // MarshalMsg implements msgp.Marshaler
func (z healingTracker) MarshalMsg(b []byte) (o []byte, err error) { func (z *healingTracker) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize()) o = msgp.Require(b, z.Msgsize())
// map header, size 1 // map header, size 20
// string "ID" // string "ID"
o = append(o, 0x81, 0xa2, 0x49, 0x44) o = append(o, 0xde, 0x0, 0x14, 0xa2, 0x49, 0x44)
o = msgp.AppendString(o, z.ID) o = msgp.AppendString(o, z.ID)
// string "PoolIndex"
o = append(o, 0xa9, 0x50, 0x6f, 0x6f, 0x6c, 0x49, 0x6e, 0x64, 0x65, 0x78)
o = msgp.AppendInt(o, z.PoolIndex)
// string "SetIndex"
o = append(o, 0xa8, 0x53, 0x65, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78)
o = msgp.AppendInt(o, z.SetIndex)
// string "DiskIndex"
o = append(o, 0xa9, 0x44, 0x69, 0x73, 0x6b, 0x49, 0x6e, 0x64, 0x65, 0x78)
o = msgp.AppendInt(o, z.DiskIndex)
// string "Path"
o = append(o, 0xa4, 0x50, 0x61, 0x74, 0x68)
o = msgp.AppendString(o, z.Path)
// string "Endpoint"
o = append(o, 0xa8, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74)
o = msgp.AppendString(o, z.Endpoint)
// string "Started"
o = append(o, 0xa7, 0x53, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64)
o = msgp.AppendTime(o, z.Started)
// string "LastUpdate"
o = append(o, 0xaa, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65)
o = msgp.AppendTime(o, z.LastUpdate)
// string "ObjectsHealed"
o = append(o, 0xad, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
o = msgp.AppendUint64(o, z.ObjectsHealed)
// string "ObjectsFailed"
o = append(o, 0xad, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
o = msgp.AppendUint64(o, z.ObjectsFailed)
// string "BytesDone"
o = append(o, 0xa9, 0x42, 0x79, 0x74, 0x65, 0x73, 0x44, 0x6f, 0x6e, 0x65)
o = msgp.AppendUint64(o, z.BytesDone)
// string "BytesFailed"
o = append(o, 0xab, 0x42, 0x79, 0x74, 0x65, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
o = msgp.AppendUint64(o, z.BytesFailed)
// string "Bucket"
o = append(o, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
o = msgp.AppendString(o, z.Bucket)
// string "Object"
o = append(o, 0xa6, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74)
o = msgp.AppendString(o, z.Object)
// string "ResumeObjectsHealed"
o = append(o, 0xb3, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
o = msgp.AppendUint64(o, z.ResumeObjectsHealed)
// string "ResumeObjectsFailed"
o = append(o, 0xb3, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
o = msgp.AppendUint64(o, z.ResumeObjectsFailed)
// string "ResumeBytesDone"
o = append(o, 0xaf, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x44, 0x6f, 0x6e, 0x65)
o = msgp.AppendUint64(o, z.ResumeBytesDone)
// string "ResumeBytesFailed"
o = append(o, 0xb1, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
o = msgp.AppendUint64(o, z.ResumeBytesFailed)
// string "QueuedBuckets"
o = append(o, 0xad, 0x51, 0x75, 0x65, 0x75, 0x65, 0x64, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x73)
o = msgp.AppendArrayHeader(o, uint32(len(z.QueuedBuckets)))
for za0001 := range z.QueuedBuckets {
o = msgp.AppendString(o, z.QueuedBuckets[za0001])
}
// string "HealedBuckets"
o = append(o, 0xad, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x73)
o = msgp.AppendArrayHeader(o, uint32(len(z.HealedBuckets)))
for za0002 := range z.HealedBuckets {
o = msgp.AppendString(o, z.HealedBuckets[za0002])
}
return return
} }
@@ -91,6 +498,146 @@ func (z *healingTracker) UnmarshalMsg(bts []byte) (o []byte, err error) {
err = msgp.WrapError(err, "ID") err = msgp.WrapError(err, "ID")
return return
} }
case "PoolIndex":
z.PoolIndex, bts, err = msgp.ReadIntBytes(bts)
if err != nil {
err = msgp.WrapError(err, "PoolIndex")
return
}
case "SetIndex":
z.SetIndex, bts, err = msgp.ReadIntBytes(bts)
if err != nil {
err = msgp.WrapError(err, "SetIndex")
return
}
case "DiskIndex":
z.DiskIndex, bts, err = msgp.ReadIntBytes(bts)
if err != nil {
err = msgp.WrapError(err, "DiskIndex")
return
}
case "Path":
z.Path, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Path")
return
}
case "Endpoint":
z.Endpoint, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Endpoint")
return
}
case "Started":
z.Started, bts, err = msgp.ReadTimeBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Started")
return
}
case "LastUpdate":
z.LastUpdate, bts, err = msgp.ReadTimeBytes(bts)
if err != nil {
err = msgp.WrapError(err, "LastUpdate")
return
}
case "ObjectsHealed":
z.ObjectsHealed, bts, err = msgp.ReadUint64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "ObjectsHealed")
return
}
case "ObjectsFailed":
z.ObjectsFailed, bts, err = msgp.ReadUint64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "ObjectsFailed")
return
}
case "BytesDone":
z.BytesDone, bts, err = msgp.ReadUint64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "BytesDone")
return
}
case "BytesFailed":
z.BytesFailed, bts, err = msgp.ReadUint64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "BytesFailed")
return
}
case "Bucket":
z.Bucket, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Bucket")
return
}
case "Object":
z.Object, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Object")
return
}
case "ResumeObjectsHealed":
z.ResumeObjectsHealed, bts, err = msgp.ReadUint64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "ResumeObjectsHealed")
return
}
case "ResumeObjectsFailed":
z.ResumeObjectsFailed, bts, err = msgp.ReadUint64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "ResumeObjectsFailed")
return
}
case "ResumeBytesDone":
z.ResumeBytesDone, bts, err = msgp.ReadUint64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "ResumeBytesDone")
return
}
case "ResumeBytesFailed":
z.ResumeBytesFailed, bts, err = msgp.ReadUint64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "ResumeBytesFailed")
return
}
case "QueuedBuckets":
var zb0002 uint32
zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err, "QueuedBuckets")
return
}
if cap(z.QueuedBuckets) >= int(zb0002) {
z.QueuedBuckets = (z.QueuedBuckets)[:zb0002]
} else {
z.QueuedBuckets = make([]string, zb0002)
}
for za0001 := range z.QueuedBuckets {
z.QueuedBuckets[za0001], bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "QueuedBuckets", za0001)
return
}
}
case "HealedBuckets":
var zb0003 uint32
zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err, "HealedBuckets")
return
}
if cap(z.HealedBuckets) >= int(zb0003) {
z.HealedBuckets = (z.HealedBuckets)[:zb0003]
} else {
z.HealedBuckets = make([]string, zb0003)
}
for za0002 := range z.HealedBuckets {
z.HealedBuckets[za0002], bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "HealedBuckets", za0002)
return
}
}
default: default:
bts, err = msgp.Skip(bts) bts, err = msgp.Skip(bts)
if err != nil { if err != nil {
@@ -104,7 +651,14 @@ func (z *healingTracker) UnmarshalMsg(bts []byte) (o []byte, err error) {
} }
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z healingTracker) Msgsize() (s int) { func (z *healingTracker) Msgsize() (s int) {
s = 1 + 3 + msgp.StringPrefixSize + len(z.ID) s = 3 + 3 + msgp.StringPrefixSize + len(z.ID) + 10 + msgp.IntSize + 9 + msgp.IntSize + 10 + msgp.IntSize + 5 + msgp.StringPrefixSize + len(z.Path) + 9 + msgp.StringPrefixSize + len(z.Endpoint) + 8 + msgp.TimeSize + 11 + msgp.TimeSize + 14 + msgp.Uint64Size + 14 + msgp.Uint64Size + 10 + msgp.Uint64Size + 12 + msgp.Uint64Size + 7 + msgp.StringPrefixSize + len(z.Bucket) + 7 + msgp.StringPrefixSize + len(z.Object) + 20 + msgp.Uint64Size + 20 + msgp.Uint64Size + 16 + msgp.Uint64Size + 18 + msgp.Uint64Size + 14 + msgp.ArrayHeaderSize
for za0001 := range z.QueuedBuckets {
s += msgp.StringPrefixSize + len(z.QueuedBuckets[za0001])
}
s += 14 + msgp.ArrayHeaderSize
for za0002 := range z.HealedBuckets {
s += msgp.StringPrefixSize + len(z.HealedBuckets[za0002])
}
return return
} }
-138
View File
@@ -19,7 +19,6 @@ package cmd
import ( import (
"bytes" "bytes"
"context" "context"
"io/ioutil"
"math" "math"
"math/rand" "math/rand"
"strconv" "strconv"
@@ -175,56 +174,6 @@ func benchmarkPutObjectParallel(b *testing.B, instanceType string, objSize int)
runPutObjectBenchmarkParallel(b, objLayer, objSize) runPutObjectBenchmarkParallel(b, objLayer, objSize)
} }
// Benchmark utility functions for ObjectLayer.GetObject().
// Creates Object layer setup ( MakeBucket, PutObject) and then runs the benchmark.
func runGetObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
// obtains random bucket name.
bucket := getRandomBucketName()
// create bucket.
err := obj.MakeBucketWithLocation(context.Background(), bucket, BucketOptions{})
if err != nil {
b.Fatal(err)
}
textData := generateBytesData(objSize)
// generate etag for the generated data.
// etag of the data to written is required as input for PutObject.
// PutObject is the functions which writes the data onto the FS/Erasure backend.
// get text data generated for number of bytes equal to object size.
md5hex := getMD5Hash(textData)
sha256hex := ""
for i := 0; i < 10; i++ {
// insert the object.
var objInfo ObjectInfo
objInfo, err = obj.PutObject(context.Background(), bucket, "object"+strconv.Itoa(i),
mustGetPutObjReader(b, bytes.NewReader(textData), int64(len(textData)), md5hex, sha256hex), ObjectOptions{})
if err != nil {
b.Fatal(err)
}
if objInfo.ETag != md5hex {
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, objInfo.ETag, md5hex)
}
}
// benchmark utility which helps obtain number of allocations and bytes allocated per ops.
b.ReportAllocs()
// the actual benchmark for GetObject starts here. Reset the benchmark timer.
b.ResetTimer()
for i := 0; i < b.N; i++ {
var buffer = new(bytes.Buffer)
err = obj.GetObject(context.Background(), bucket, "object"+strconv.Itoa(i%10), 0, int64(objSize), buffer, "", ObjectOptions{})
if err != nil {
b.Error(err)
}
}
// Benchmark ends here. Stop timer.
b.StopTimer()
}
// randomly picks a character and returns its equivalent byte array. // randomly picks a character and returns its equivalent byte array.
func getRandomByte() []byte { func getRandomByte() []byte {
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
@@ -240,38 +189,6 @@ func generateBytesData(size int) []byte {
return bytes.Repeat(getRandomByte(), size) return bytes.Repeat(getRandomByte(), size)
} }
// creates Erasure/FS backend setup, obtains the object layer and calls the runGetObjectBenchmark function.
func benchmarkGetObject(b *testing.B, instanceType string, objSize int) {
// create a temp Erasure/FS backend.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
objLayer, disks, err := prepareTestBackend(ctx, instanceType)
if err != nil {
b.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
}
// cleaning up the backend by removing all the directories and files created.
defer removeRoots(disks)
// uses *testing.B and the object Layer to run the benchmark.
runGetObjectBenchmark(b, objLayer, objSize)
}
// creates Erasure/FS backend setup, obtains the object layer and runs parallel benchmark for ObjectLayer.GetObject() .
func benchmarkGetObjectParallel(b *testing.B, instanceType string, objSize int) {
// create a temp Erasure/FS backend.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
objLayer, disks, err := prepareTestBackend(ctx, instanceType)
if err != nil {
b.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
}
// cleaning up the backend by removing all the directories and files created.
defer removeRoots(disks)
// uses *testing.B and the object Layer to run the benchmark.
runGetObjectBenchmarkParallel(b, objLayer, objSize)
}
// Parallel benchmark utility functions for ObjectLayer.PutObject(). // Parallel benchmark utility functions for ObjectLayer.PutObject().
// Creates Object layer setup ( MakeBucket ) and then runs the PutObject benchmark. // Creates Object layer setup ( MakeBucket ) and then runs the PutObject benchmark.
func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) { func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
@@ -315,58 +232,3 @@ func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
// Benchmark ends here. Stop timer. // Benchmark ends here. Stop timer.
b.StopTimer() b.StopTimer()
} }
// Parallel benchmark utility functions for ObjectLayer.GetObject().
// Creates Object layer setup ( MakeBucket, PutObject) and then runs the benchmark.
func runGetObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
// obtains random bucket name.
bucket := getRandomBucketName()
// create bucket.
err := obj.MakeBucketWithLocation(context.Background(), bucket, BucketOptions{})
if err != nil {
b.Fatal(err)
}
// get text data generated for number of bytes equal to object size.
textData := generateBytesData(objSize)
// generate md5sum for the generated data.
// md5sum of the data to written is required as input for PutObject.
// PutObject is the functions which writes the data onto the FS/Erasure backend.
md5hex := getMD5Hash([]byte(textData))
sha256hex := ""
for i := 0; i < 10; i++ {
// insert the object.
var objInfo ObjectInfo
objInfo, err = obj.PutObject(context.Background(), bucket, "object"+strconv.Itoa(i),
mustGetPutObjReader(b, bytes.NewReader(textData), int64(len(textData)), md5hex, sha256hex), ObjectOptions{})
if err != nil {
b.Fatal(err)
}
if objInfo.ETag != md5hex {
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, objInfo.ETag, md5hex)
}
}
// benchmark utility which helps obtain number of allocations and bytes allocated per ops.
b.ReportAllocs()
// the actual benchmark for GetObject starts here. Reset the benchmark timer.
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
err = obj.GetObject(context.Background(), bucket, "object"+strconv.Itoa(i), 0, int64(objSize), ioutil.Discard, "", ObjectOptions{})
if err != nil {
b.Error(err)
}
i++
if i == 10 {
i = 0
}
}
})
// Benchmark ends here. Stop timer.
b.StopTimer()
}
+1 -1
View File
@@ -17,13 +17,13 @@
package cmd package cmd
import ( import (
"crypto/sha256"
"errors" "errors"
"hash" "hash"
"io" "io"
"github.com/minio/highwayhash" "github.com/minio/highwayhash"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
sha256 "github.com/minio/sha256-simd"
"golang.org/x/crypto/blake2b" "golang.org/x/crypto/blake2b"
) )
+9 -10
View File
@@ -20,7 +20,6 @@ import (
"context" "context"
"io" "io"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"testing" "testing"
) )
@@ -28,7 +27,7 @@ import (
func testBitrotReaderWriterAlgo(t *testing.T, bitrotAlgo BitrotAlgorithm) { func testBitrotReaderWriterAlgo(t *testing.T, bitrotAlgo BitrotAlgorithm) {
tmpDir, err := ioutil.TempDir("", "") tmpDir, err := ioutil.TempDir("", "")
if err != nil { if err != nil {
log.Fatal(err) t.Fatal(err)
} }
defer os.RemoveAll(tmpDir) defer os.RemoveAll(tmpDir)
@@ -46,35 +45,35 @@ func testBitrotReaderWriterAlgo(t *testing.T, bitrotAlgo BitrotAlgorithm) {
_, err = writer.Write([]byte("aaaaaaaaaa")) _, err = writer.Write([]byte("aaaaaaaaaa"))
if err != nil { if err != nil {
log.Fatal(err) t.Fatal(err)
} }
_, err = writer.Write([]byte("aaaaaaaaaa")) _, err = writer.Write([]byte("aaaaaaaaaa"))
if err != nil { if err != nil {
log.Fatal(err) t.Fatal(err)
} }
_, err = writer.Write([]byte("aaaaaaaaaa")) _, err = writer.Write([]byte("aaaaaaaaaa"))
if err != nil { if err != nil {
log.Fatal(err) t.Fatal(err)
} }
_, err = writer.Write([]byte("aaaaa")) _, err = writer.Write([]byte("aaaaa"))
if err != nil { if err != nil {
log.Fatal(err) t.Fatal(err)
} }
writer.(io.Closer).Close() writer.(io.Closer).Close()
reader := newBitrotReader(disk, nil, volume, filePath, 35, bitrotAlgo, bitrotWriterSum(writer), 10) reader := newBitrotReader(disk, nil, volume, filePath, 35, bitrotAlgo, bitrotWriterSum(writer), 10)
b := make([]byte, 10) b := make([]byte, 10)
if _, err = reader.ReadAt(b, 0); err != nil { if _, err = reader.ReadAt(b, 0); err != nil {
log.Fatal(err) t.Fatal(err)
} }
if _, err = reader.ReadAt(b, 10); err != nil { if _, err = reader.ReadAt(b, 10); err != nil {
log.Fatal(err) t.Fatal(err)
} }
if _, err = reader.ReadAt(b, 20); err != nil { if _, err = reader.ReadAt(b, 20); err != nil {
log.Fatal(err) t.Fatal(err)
} }
if _, err = reader.ReadAt(b[:5], 30); err != nil { if _, err = reader.ReadAt(b[:5], 30); err != nil {
log.Fatal(err) t.Fatal(err)
} }
} }
+140 -28
View File
@@ -17,14 +17,15 @@
package cmd package cmd
import ( import (
"crypto/subtle"
"encoding/base64" "encoding/base64"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"net/textproto"
"net/url" "net/url"
"path" "path"
"path/filepath"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -125,7 +126,10 @@ func initFederatorBackend(buckets []BucketInfo, objLayer ObjectLayer) {
// Add/update buckets that are not registered with the DNS // Add/update buckets that are not registered with the DNS
bucketsToBeUpdatedSlice := bucketsToBeUpdated.ToSlice() bucketsToBeUpdatedSlice := bucketsToBeUpdated.ToSlice()
g := errgroup.WithNErrs(len(bucketsToBeUpdatedSlice)) g := errgroup.WithNErrs(len(bucketsToBeUpdatedSlice)).WithConcurrency(50)
ctx, cancel := g.WithCancelOnError(GlobalContext)
defer cancel()
for index := range bucketsToBeUpdatedSlice { for index := range bucketsToBeUpdatedSlice {
index := index index := index
g.Go(func() error { g.Go(func() error {
@@ -133,14 +137,13 @@ func initFederatorBackend(buckets []BucketInfo, objLayer ObjectLayer) {
}, index) }, index)
} }
for _, err := range g.Wait() { if err := g.WaitErr(); err != nil {
if err != nil { logger.LogIf(ctx, err)
logger.LogIf(GlobalContext, err) return
}
} }
for _, bucket := range bucketsInConflict.ToSlice() { for _, bucket := range bucketsInConflict.ToSlice() {
logger.LogIf(GlobalContext, fmt.Errorf("Unable to add bucket DNS entry for bucket %s, an entry exists for the same bucket by a different tenant. This local bucket will be ignored. Bucket names are globally unique in federated deployments. Use path style requests on following addresses '%v' to access this bucket.", bucket, globalDomainIPs.ToSlice())) logger.LogIf(ctx, fmt.Errorf("Unable to add bucket DNS entry for bucket %s, an entry exists for the same bucket by a different tenant. This local bucket will be ignored. Bucket names are globally unique in federated deployments. Use path style requests on following addresses '%v' to access this bucket", bucket, globalDomainIPs.ToSlice()))
} }
var wg sync.WaitGroup var wg sync.WaitGroup
@@ -334,7 +337,7 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
// err will be nil here as we already called this function // err will be nil here as we already called this function
// earlier in this request. // earlier in this request.
claims, _ := getClaimsFromToken(r, getSessionToken(r)) claims, _ := getClaimsFromToken(getSessionToken(r))
n := 0 n := 0
// Use the following trick to filter in place // Use the following trick to filter in place
// https://github.com/golang/go/wiki/SliceTricks#filter-in-place // https://github.com/golang/go/wiki/SliceTricks#filter-in-place
@@ -424,6 +427,12 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
deleteObjectsFn = api.CacheAPI().DeleteObjects deleteObjectsFn = api.CacheAPI().DeleteObjects
} }
// Return Malformed XML as S3 spec if the list of objects is empty
if len(deleteObjects.Objects) == 0 {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedXML), r.URL, guessIsBrowserReq(r))
return
}
var objectsToDelete = map[ObjectToDelete]int{} var objectsToDelete = map[ObjectToDelete]int{}
getObjectInfoFn := objectAPI.GetObjectInfo getObjectInfoFn := objectAPI.GetObjectInfo
if api.CacheAPI() != nil { if api.CacheAPI() != nil {
@@ -544,7 +553,6 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
dindex := objectsToDelete[ObjectToDelete{ dindex := objectsToDelete[ObjectToDelete{
ObjectName: dObjects[i].ObjectName, ObjectName: dObjects[i].ObjectName,
VersionID: dObjects[i].VersionID, VersionID: dObjects[i].VersionID,
DeleteMarkerVersionID: dObjects[i].DeleteMarkerVersionID,
VersionPurgeStatus: dObjects[i].VersionPurgeStatus, VersionPurgeStatus: dObjects[i].VersionPurgeStatus,
DeleteMarkerReplicationStatus: dObjects[i].DeleteMarkerReplicationStatus, DeleteMarkerReplicationStatus: dObjects[i].DeleteMarkerReplicationStatus,
PurgeTransitioned: dObjects[i].PurgeTransitioned, PurgeTransitioned: dObjects[i].PurgeTransitioned,
@@ -580,6 +588,10 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
// Write success response. // Write success response.
writeSuccessResponseXML(w, encodedSuccessResponse) writeSuccessResponseXML(w, encodedSuccessResponse)
for _, dobj := range deletedObjects { for _, dobj := range deletedObjects {
if dobj.ObjectName == "" {
continue
}
if replicateDeletes { if replicateDeletes {
if dobj.DeleteMarkerReplicationStatus == string(replication.Pending) || dobj.VersionPurgeStatus == Pending { if dobj.DeleteMarkerReplicationStatus == string(replication.Pending) || dobj.VersionPurgeStatus == Pending {
dv := DeletedObjectVersionInfo{ dv := DeletedObjectVersionInfo{
@@ -591,22 +603,14 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
} }
if hasLifecycleConfig && dobj.PurgeTransitioned == lifecycle.TransitionComplete { // clean up transitioned tier if hasLifecycleConfig && dobj.PurgeTransitioned == lifecycle.TransitionComplete { // clean up transitioned tier
action := lifecycle.DeleteAction deleteTransitionedObject(ctx, objectAPI, bucket, dobj.ObjectName, lifecycle.ObjectOpts{
if dobj.VersionID != "" {
action = lifecycle.DeleteVersionAction
}
deleteTransitionedObject(ctx, newObjectLayerFn(), bucket, dobj.ObjectName, lifecycle.ObjectOpts{
Name: dobj.ObjectName, Name: dobj.ObjectName,
VersionID: dobj.VersionID, VersionID: dobj.VersionID,
DeleteMarker: dobj.DeleteMarker, DeleteMarker: dobj.DeleteMarker,
}, action, true) }, false, true)
} }
}
// Notify deleted event for objects.
for _, dobj := range deletedObjects {
eventName := event.ObjectRemovedDelete eventName := event.ObjectRemovedDelete
objInfo := ObjectInfo{ objInfo := ObjectInfo{
Name: dobj.ObjectName, Name: dobj.ObjectName,
VersionID: dobj.VersionID, VersionID: dobj.VersionID,
@@ -793,13 +797,15 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL, guessIsBrowserReq(r))
return return
} }
resource, err := getResource(r.URL.Path, r.Host, globalDomainNames) resource, err := getResource(r.URL.Path, r.Host, globalDomainNames)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL, guessIsBrowserReq(r))
return return
} }
// Make sure that the URL does not contain object name.
if bucket != filepath.Clean(resource[1:]) { // Make sure that the URL does not contain object name.
if bucket != path.Clean(resource[1:]) {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r))
return return
} }
@@ -842,7 +848,6 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
defer fileBody.Close() defer fileBody.Close()
formValues.Set("Bucket", bucket) formValues.Set("Bucket", bucket)
if fileName != "" && strings.Contains(formValues.Get("Key"), "${filename}") { if fileName != "" && strings.Contains(formValues.Get("Key"), "${filename}") {
// S3 feature to replace ${filename} found in Key form field // S3 feature to replace ${filename} found in Key form field
// by the filename attribute passed in multipart // by the filename attribute passed in multipart
@@ -850,6 +855,12 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
} }
object := formValues.Get("Key") object := formValues.Get("Key")
object, err = unescapePath(object)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
successRedirect := formValues.Get("success_action_redirect") successRedirect := formValues.Get("success_action_redirect")
successStatus := formValues.Get("success_action_status") successStatus := formValues.Get("success_action_status")
var redirectURL *url.URL var redirectURL *url.URL
@@ -862,12 +873,51 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
} }
// Verify policy signature. // Verify policy signature.
errCode := doesPolicySignatureMatch(formValues) cred, errCode := doesPolicySignatureMatch(formValues)
if errCode != ErrNone { if errCode != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(errCode), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(errCode), r.URL, guessIsBrowserReq(r))
return return
} }
// Once signature is validated, check if the user has
// explicit permissions for the user.
{
token := formValues.Get(xhttp.AmzSecurityToken)
if token != "" && cred.AccessKey == "" {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNoAccessKey), r.URL, guessIsBrowserReq(r))
return
}
if cred.IsServiceAccount() && token == "" {
token = cred.SessionToken
}
if subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidToken), r.URL, guessIsBrowserReq(r))
return
}
// Extract claims if any.
claims, err := getClaimsFromToken(token)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey,
Action: iampolicy.PutObjectAction,
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
BucketName: bucket,
ObjectName: object,
IsOwner: globalActiveCred.AccessKey == cred.AccessKey,
Claims: claims,
}) {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL, guessIsBrowserReq(r))
return
}
}
policyBytes, err := base64.StdEncoding.DecodeString(formValues.Get("Policy")) policyBytes, err := base64.StdEncoding.DecodeString(formValues.Get("Policy"))
if err != nil { if err != nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedPOSTRequest), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedPOSTRequest), r.URL, guessIsBrowserReq(r))
@@ -907,20 +957,20 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
// Extract metadata to be saved from received Form. // Extract metadata to be saved from received Form.
metadata := make(map[string]string) metadata := make(map[string]string)
err = extractMetadataFromMap(ctx, formValues, metadata) err = extractMetadataFromMime(ctx, textproto.MIMEHeader(formValues), metadata)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return return
} }
hashReader, err := hash.NewReader(fileBody, fileSize, "", "", fileSize, globalCLIContext.StrictS3Compat) hashReader, err := hash.NewReader(fileBody, fileSize, "", "", fileSize)
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return return
} }
rawReader := hashReader rawReader := hashReader
pReader := NewPutObjReader(rawReader, nil, nil) pReader := NewPutObjReader(rawReader)
var objectEncryptionKey crypto.ObjectKey var objectEncryptionKey crypto.ObjectKey
// Check if bucket encryption is enabled // Check if bucket encryption is enabled
@@ -960,12 +1010,16 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
} }
info := ObjectInfo{Size: fileSize} info := ObjectInfo{Size: fileSize}
// do not try to verify encrypted content // do not try to verify encrypted content
hashReader, err = hash.NewReader(reader, info.EncryptedSize(), "", "", fileSize, globalCLIContext.StrictS3Compat) hashReader, err = hash.NewReader(reader, info.EncryptedSize(), "", "", fileSize)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
pReader, err = pReader.WithEncryption(hashReader, &objectEncryptionKey)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return return
} }
pReader = NewPutObjReader(rawReader, hashReader, &objectEncryptionKey)
} }
} }
@@ -1022,6 +1076,64 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
} }
} }
// GetBucketPolicyStatusHandler - Retrieves the policy status
// for an MinIO bucket, indicating whether the bucket is public.
func (api objectAPIHandlers) GetBucketPolicyStatusHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "GetBucketPolicyStatus")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
vars := mux.Vars(r)
bucket := vars["bucket"]
objectAPI := api.ObjectAPI()
if objectAPI == nil {
writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(ErrServerNotInitialized))
return
}
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketPolicyStatusAction, bucket, ""); s3Error != ErrNone {
writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(s3Error))
return
}
// Check if bucket exists.
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
// Check if anonymous (non-owner) has access to list objects.
readable := globalPolicySys.IsAllowed(policy.Args{
Action: policy.ListBucketAction,
BucketName: bucket,
ConditionValues: getConditionValues(r, "", "", nil),
IsOwner: false,
})
// Check if anonymous (non-owner) has access to upload objects.
writable := globalPolicySys.IsAllowed(policy.Args{
Action: policy.PutObjectAction,
BucketName: bucket,
ConditionValues: getConditionValues(r, "", "", nil),
IsOwner: false,
})
encodedSuccessResponse := encodeResponse(PolicyStatus{
IsPublic: func() string {
// Silly to have special 'boolean' values yes
// but complying with silly implementation
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketPolicyStatus.html
if readable && writable {
return "TRUE"
}
return "FALSE"
}(),
})
writeSuccessResponseXML(w, encodedSuccessResponse)
}
// HeadBucketHandler - HEAD Bucket // HeadBucketHandler - HEAD Bucket
// ---------- // ----------
// This operation is useful to determine if a bucket exists. // This operation is useful to determine if a bucket exists.
+101 -45
View File
@@ -65,6 +65,46 @@ func NewLifecycleSys() *LifecycleSys {
return &LifecycleSys{} return &LifecycleSys{}
} }
type expiryTask struct {
objInfo ObjectInfo
versionExpiry bool
}
type expiryState struct {
expiryCh chan expiryTask
}
func (es *expiryState) queueExpiryTask(oi ObjectInfo, rmVersion bool) {
select {
case es.expiryCh <- expiryTask{objInfo: oi, versionExpiry: rmVersion}:
default:
}
}
var (
globalExpiryState *expiryState
)
func newExpiryState() *expiryState {
es := &expiryState{
expiryCh: make(chan expiryTask, 10000),
}
go func() {
<-GlobalContext.Done()
close(es.expiryCh)
}()
return es
}
func initBackgroundExpiry(ctx context.Context, objectAPI ObjectLayer) {
globalExpiryState = newExpiryState()
go func() {
for t := range globalExpiryState.expiryCh {
applyExpiryRule(ctx, objectAPI, t.objInfo, false, t.versionExpiry)
}
}()
}
type transitionState struct { type transitionState struct {
// add future metrics here // add future metrics here
transitionCh chan ObjectInfo transitionCh chan ObjectInfo
@@ -205,16 +245,11 @@ func transitionSCInUse(ctx context.Context, lfc *lifecycle.Lifecycle, bucket, ar
} }
// set PutObjectOptions for PUT operation to transition data to target cluster // set PutObjectOptions for PUT operation to transition data to target cluster
func putTransitionOpts(objInfo ObjectInfo) (putOpts miniogo.PutObjectOptions) { func putTransitionOpts(objInfo ObjectInfo) (putOpts miniogo.PutObjectOptions, err error) {
meta := make(map[string]string) meta := make(map[string]string)
tag, err := tags.ParseObjectTags(objInfo.UserTags)
if err != nil {
return
}
putOpts = miniogo.PutObjectOptions{ putOpts = miniogo.PutObjectOptions{
UserMetadata: meta, UserMetadata: meta,
UserTags: tag.ToMap(),
ContentType: objInfo.ContentType, ContentType: objInfo.ContentType,
ContentEncoding: objInfo.ContentEncoding, ContentEncoding: objInfo.ContentEncoding,
StorageClass: objInfo.StorageClass, StorageClass: objInfo.StorageClass,
@@ -224,29 +259,47 @@ func putTransitionOpts(objInfo ObjectInfo) (putOpts miniogo.PutObjectOptions) {
SourceETag: objInfo.ETag, SourceETag: objInfo.ETag,
}, },
} }
if mode, ok := objInfo.UserDefined[xhttp.AmzObjectLockMode]; ok {
if objInfo.UserTags != "" {
tag, _ := tags.ParseObjectTags(objInfo.UserTags)
if tag != nil {
putOpts.UserTags = tag.ToMap()
}
}
lkMap := caseInsensitiveMap(objInfo.UserDefined)
if lang, ok := lkMap.Lookup(xhttp.ContentLanguage); ok {
putOpts.ContentLanguage = lang
}
if disp, ok := lkMap.Lookup(xhttp.ContentDisposition); ok {
putOpts.ContentDisposition = disp
}
if cc, ok := lkMap.Lookup(xhttp.CacheControl); ok {
putOpts.CacheControl = cc
}
if mode, ok := lkMap.Lookup(xhttp.AmzObjectLockMode); ok {
rmode := miniogo.RetentionMode(mode) rmode := miniogo.RetentionMode(mode)
putOpts.Mode = rmode putOpts.Mode = rmode
} }
if retainDateStr, ok := objInfo.UserDefined[xhttp.AmzObjectLockRetainUntilDate]; ok { if retainDateStr, ok := lkMap.Lookup(xhttp.AmzObjectLockRetainUntilDate); ok {
rdate, err := time.Parse(time.RFC3339, retainDateStr) rdate, err := time.Parse(time.RFC3339, retainDateStr)
if err != nil { if err != nil {
return return putOpts, err
} }
putOpts.RetainUntilDate = rdate putOpts.RetainUntilDate = rdate
} }
if lhold, ok := objInfo.UserDefined[xhttp.AmzObjectLockLegalHold]; ok { if lhold, ok := lkMap.Lookup(xhttp.AmzObjectLockLegalHold); ok {
putOpts.LegalHold = miniogo.LegalHoldStatus(lhold) putOpts.LegalHold = miniogo.LegalHoldStatus(lhold)
} }
return return putOpts, nil
} }
// handle deletes of transitioned objects or object versions when one of the following is true: // handle deletes of transitioned objects or object versions when one of the following is true:
// 1. temporarily restored copies of objects (restored with the PostRestoreObject API) expired. // 1. temporarily restored copies of objects (restored with the PostRestoreObject API) expired.
// 2. life cycle expiry date is met on the object. // 2. life cycle expiry date is met on the object.
// 3. Object is removed through DELETE api call // 3. Object is removed through DELETE api call
func deleteTransitionedObject(ctx context.Context, objectAPI ObjectLayer, bucket, object string, lcOpts lifecycle.ObjectOpts, action lifecycle.Action, isDeleteTierOnly bool) error { func deleteTransitionedObject(ctx context.Context, objectAPI ObjectLayer, bucket, object string, lcOpts lifecycle.ObjectOpts, restoredObject, isDeleteTierOnly bool) error {
if lcOpts.TransitionStatus == "" && !isDeleteTierOnly { if lcOpts.TransitionStatus == "" && !isDeleteTierOnly {
return nil return nil
} }
@@ -266,44 +319,41 @@ func deleteTransitionedObject(ctx context.Context, objectAPI ObjectLayer, bucket
var opts ObjectOptions var opts ObjectOptions
opts.Versioned = globalBucketVersioningSys.Enabled(bucket) opts.Versioned = globalBucketVersioningSys.Enabled(bucket)
opts.VersionID = lcOpts.VersionID opts.VersionID = lcOpts.VersionID
switch action { if restoredObject {
case lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction:
// delete locally restored copy of object or object version // delete locally restored copy of object or object version
// from the source, while leaving metadata behind. The data on // from the source, while leaving metadata behind. The data on
// transitioned tier lies untouched and still accessible // transitioned tier lies untouched and still accessible
opts.TransitionStatus = lcOpts.TransitionStatus opts.TransitionStatus = lcOpts.TransitionStatus
_, err = objectAPI.DeleteObject(ctx, bucket, object, opts) _, err = objectAPI.DeleteObject(ctx, bucket, object, opts)
return err return err
case lifecycle.DeleteAction, lifecycle.DeleteVersionAction:
// When an object is past expiry, delete the data from transitioned tier and
// metadata from source
if err := tgt.RemoveObject(context.Background(), arn.Bucket, object, miniogo.RemoveObjectOptions{VersionID: lcOpts.VersionID}); err != nil {
logger.LogIf(ctx, err)
}
if isDeleteTierOnly {
return nil
}
objInfo, err := objectAPI.DeleteObject(ctx, bucket, object, opts)
if err != nil {
return err
}
eventName := event.ObjectRemovedDelete
if lcOpts.DeleteMarker {
eventName = event.ObjectRemovedDeleteMarkerCreated
}
// Notify object deleted event.
sendEvent(eventArgs{
EventName: eventName,
BucketName: bucket,
Object: objInfo,
Host: "Internal: [ILM-EXPIRY]",
})
} }
// When an object is past expiry, delete the data from transitioned tier and
// metadata from source
if err := tgt.RemoveObject(context.Background(), arn.Bucket, object, miniogo.RemoveObjectOptions{VersionID: lcOpts.VersionID}); err != nil {
logger.LogIf(ctx, err)
}
if isDeleteTierOnly {
return nil
}
objInfo, err := objectAPI.DeleteObject(ctx, bucket, object, opts)
if err != nil {
return err
}
eventName := event.ObjectRemovedDelete
if lcOpts.DeleteMarker {
eventName = event.ObjectRemovedDeleteMarkerCreated
}
// Notify object deleted event.
sendEvent(eventArgs{
EventName: eventName,
BucketName: bucket,
Object: objInfo,
Host: "Internal: [ILM-EXPIRY]",
})
// should never reach here // should never reach here
return nil return nil
} }
@@ -343,7 +393,12 @@ func transitionObject(ctx context.Context, objectAPI ObjectLayer, objInfo Object
return nil return nil
} }
putOpts := putTransitionOpts(oi) putOpts, err := putTransitionOpts(oi)
if err != nil {
gr.Close()
return err
}
if _, err = tgt.PutObject(ctx, arn.Bucket, oi.Name, gr, oi.Size, putOpts); err != nil { if _, err = tgt.PutObject(ctx, arn.Bucket, oi.Name, gr, oi.Size, putOpts); err != nil {
gr.Close() gr.Close()
return err return err
@@ -368,6 +423,7 @@ func transitionObject(ctx context.Context, objectAPI ObjectLayer, objInfo Object
Object: objInfo, Object: objInfo,
Host: "Internal: [ILM-Transition]", Host: "Internal: [ILM-Transition]",
}) })
return err return err
} }
@@ -636,11 +692,11 @@ func restoreTransitionedObject(ctx context.Context, bucket, object string, objAP
return err return err
} }
defer gr.Close() defer gr.Close()
hashReader, err := hash.NewReader(gr, objInfo.Size, "", "", objInfo.Size, globalCLIContext.StrictS3Compat) hashReader, err := hash.NewReader(gr, objInfo.Size, "", "", objInfo.Size)
if err != nil { if err != nil {
return err return err
} }
pReader := NewPutObjReader(hashReader, nil, nil) pReader := NewPutObjReader(hashReader)
opts := putRestoreOpts(bucket, object, rreq, objInfo) opts := putRestoreOpts(bucket, object, rreq, objInfo)
opts.UserDefined[xhttp.AmzRestore] = fmt.Sprintf("ongoing-request=%t, expiry-date=%s", false, restoreExpiry.Format(http.TimeFormat)) opts.UserDefined[xhttp.AmzRestore] = fmt.Sprintf("ongoing-request=%t, expiry-date=%s", false, restoreExpiry.Format(http.TimeFormat))
if _, err := objAPI.PutObject(ctx, bucket, object, pReader, opts); err != nil { if _, err := objAPI.PutObject(ctx, bucket, object, pReader, opts); err != nil {
+14 -34
View File
@@ -26,32 +26,25 @@ import (
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/bucket/policy" "github.com/minio/minio/pkg/bucket/policy"
"github.com/minio/minio/pkg/handlers"
"github.com/minio/minio/pkg/sync/errgroup" "github.com/minio/minio/pkg/sync/errgroup"
) )
func concurrentDecryptETag(ctx context.Context, objects []ObjectInfo) { func concurrentDecryptETag(ctx context.Context, objects []ObjectInfo) {
inParallel := func(objects []ObjectInfo) { g := errgroup.WithNErrs(len(objects)).WithConcurrency(500)
g := errgroup.WithNErrs(len(objects)) _, cancel := g.WithCancelOnError(ctx)
for index := range objects { defer cancel()
index := index for index := range objects {
g.Go(func() error { index := index
objects[index].ETag = objects[index].GetActualETag(nil) g.Go(func() error {
objects[index].Size, _ = objects[index].GetActualSize() size, err := objects[index].GetActualSize()
return nil if err == nil {
}, index) objects[index].Size = size
} }
g.Wait() objects[index].ETag = objects[index].GetActualETag(nil)
} return nil
const maxConcurrent = 500 }, index)
for {
if len(objects) < maxConcurrent {
inParallel(objects)
return
}
inParallel(objects[:maxConcurrent])
objects = objects[maxConcurrent:]
} }
g.WaitErr()
} }
// Validate all the ListObjects query arguments, returns an APIErrorCode // Validate all the ListObjects query arguments, returns an APIErrorCode
@@ -301,10 +294,6 @@ func proxyRequestByNodeIndex(ctx context.Context, w http.ResponseWriter, r *http
return proxyRequest(ctx, w, r, ep) return proxyRequest(ctx, w, r, ep)
} }
func proxyRequestByStringHash(ctx context.Context, w http.ResponseWriter, r *http.Request, str string) (success bool) {
return proxyRequestByNodeIndex(ctx, w, r, crcHashMod(str, len(globalProxyEndpoints)))
}
// ListObjectsV1Handler - GET Bucket (List Objects) Version 1. // ListObjectsV1Handler - GET Bucket (List Objects) Version 1.
// -------------------------- // --------------------------
// This implementation of the GET operation returns some or all (up to 10000) // This implementation of the GET operation returns some or all (up to 10000)
@@ -343,15 +332,6 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
return return
} }
// Forward the request using Source IP or bucket
forwardStr := handlers.GetSourceIPFromHeaders(r)
if forwardStr == "" {
forwardStr = bucket
}
if proxyRequestByStringHash(ctx, w, r, forwardStr) {
return
}
listObjects := objectAPI.ListObjects listObjects := objectAPI.ListObjects
// Inititate a list objects operation based on the input params. // Inititate a list objects operation based on the input params.
+390 -200
View File
@@ -20,8 +20,9 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"runtime" "reflect"
"strings" "strings"
"sync"
"time" "time"
minio "github.com/minio/minio-go/v7" minio "github.com/minio/minio-go/v7"
@@ -169,14 +170,8 @@ func hasReplicationRules(ctx context.Context, bucket string, objects []ObjectToD
} }
// isStandardHeader returns true if header is a supported header and not a custom header // isStandardHeader returns true if header is a supported header and not a custom header
func isStandardHeader(headerKey string) bool { func isStandardHeader(matchHeaderKey string) bool {
key := strings.ToLower(headerKey) return equals(matchHeaderKey, standardHeaders...)
for _, header := range standardHeaders {
if strings.ToLower(header) == key {
return true
}
}
return false
} }
// returns whether object version is a deletemarker and if object qualifies for replication // returns whether object version is a deletemarker and if object qualifies for replication
@@ -185,6 +180,14 @@ func checkReplicateDelete(ctx context.Context, bucket string, dobj ObjectToDelet
if err != nil || rcfg == nil { if err != nil || rcfg == nil {
return false, false, sync return false, false, sync
} }
opts := replication.ObjectOpts{
Name: dobj.ObjectName,
SSEC: crypto.SSEC.IsEncrypted(oi.UserDefined),
UserTags: oi.UserTags,
DeleteMarker: oi.DeleteMarker,
VersionID: dobj.VersionID,
}
replicate = rcfg.Replicate(opts)
// when incoming delete is removal of a delete marker( a.k.a versioned delete), // when incoming delete is removal of a delete marker( a.k.a versioned delete),
// GetObjectInfo returns extra information even though it returns errFileNotFound // GetObjectInfo returns extra information even though it returns errFileNotFound
if gerr != nil { if gerr != nil {
@@ -193,10 +196,12 @@ func checkReplicateDelete(ctx context.Context, bucket string, dobj ObjectToDelet
case replication.Pending, replication.Completed, replication.Failed: case replication.Pending, replication.Completed, replication.Failed:
validReplStatus = true validReplStatus = true
} }
if oi.DeleteMarker && validReplStatus { if oi.DeleteMarker && (validReplStatus || replicate) {
return oi.DeleteMarker, true, sync return oi.DeleteMarker, true, sync
} }
return oi.DeleteMarker, false, sync // can be the case that other cluster is down and duplicate `mc rm --vid`
// is issued - this still needs to be replicated back to the other target
return oi.DeleteMarker, oi.VersionPurgeStatus == Pending || oi.VersionPurgeStatus == Failed, sync
} }
tgt := globalBucketTargetSys.GetRemoteTargetClient(ctx, rcfg.RoleArn) tgt := globalBucketTargetSys.GetRemoteTargetClient(ctx, rcfg.RoleArn)
// the target online status should not be used here while deciding // the target online status should not be used here while deciding
@@ -204,14 +209,7 @@ func checkReplicateDelete(ctx context.Context, bucket string, dobj ObjectToDelet
if tgt == nil { if tgt == nil {
return oi.DeleteMarker, false, false return oi.DeleteMarker, false, false
} }
opts := replication.ObjectOpts{ return oi.DeleteMarker, replicate, tgt.replicateSync
Name: dobj.ObjectName,
SSEC: crypto.SSEC.IsEncrypted(oi.UserDefined),
UserTags: oi.UserTags,
DeleteMarker: oi.DeleteMarker,
VersionID: dobj.VersionID,
}
return oi.DeleteMarker, rcfg.Replicate(opts), tgt.replicateSync
} }
// replicate deletes to the designated replication target if replication configuration // replicate deletes to the designated replication target if replication configuration
@@ -226,26 +224,52 @@ func checkReplicateDelete(ctx context.Context, bucket string, dobj ObjectToDelet
// on target. // on target.
func replicateDelete(ctx context.Context, dobj DeletedObjectVersionInfo, objectAPI ObjectLayer) { func replicateDelete(ctx context.Context, dobj DeletedObjectVersionInfo, objectAPI ObjectLayer) {
bucket := dobj.Bucket bucket := dobj.Bucket
rcfg, err := getReplicationConfig(ctx, bucket)
if err != nil || rcfg == nil {
logger.LogIf(ctx, err)
return
}
tgt := globalBucketTargetSys.GetRemoteTargetClient(ctx, rcfg.RoleArn)
if tgt == nil {
logger.LogIf(ctx, fmt.Errorf("failed to get target for bucket:%s arn:%s", bucket, rcfg.RoleArn))
return
}
versionID := dobj.DeleteMarkerVersionID versionID := dobj.DeleteMarkerVersionID
if versionID == "" { if versionID == "" {
versionID = dobj.VersionID versionID = dobj.VersionID
} }
rcfg, err := getReplicationConfig(ctx, bucket)
if err != nil || rcfg == nil {
logger.LogIf(ctx, err)
sendEvent(eventArgs{
BucketName: bucket,
Object: ObjectInfo{
Bucket: bucket,
Name: dobj.ObjectName,
VersionID: versionID,
DeleteMarker: dobj.DeleteMarker,
},
Host: "Internal: [Replication]",
EventName: event.ObjectReplicationNotTracked,
})
return
}
tgt := globalBucketTargetSys.GetRemoteTargetClient(ctx, rcfg.RoleArn)
if tgt == nil {
logger.LogIf(ctx, fmt.Errorf("failed to get target for bucket:%s arn:%s", bucket, rcfg.RoleArn))
sendEvent(eventArgs{
BucketName: bucket,
Object: ObjectInfo{
Bucket: bucket,
Name: dobj.ObjectName,
VersionID: versionID,
DeleteMarker: dobj.DeleteMarker,
},
Host: "Internal: [Replication]",
EventName: event.ObjectReplicationNotTracked,
})
return
}
rmErr := tgt.RemoveObject(ctx, rcfg.GetDestination().Bucket, dobj.ObjectName, miniogo.RemoveObjectOptions{ rmErr := tgt.RemoveObject(ctx, rcfg.GetDestination().Bucket, dobj.ObjectName, miniogo.RemoveObjectOptions{
VersionID: versionID, VersionID: versionID,
Internal: miniogo.AdvancedRemoveOptions{ Internal: miniogo.AdvancedRemoveOptions{
ReplicationDeleteMarker: dobj.DeleteMarkerVersionID != "", ReplicationDeleteMarker: dobj.DeleteMarkerVersionID != "",
ReplicationMTime: dobj.DeleteMarkerMTime.Time, ReplicationMTime: dobj.DeleteMarkerMTime.Time,
ReplicationStatus: miniogo.ReplicationStatusReplica, ReplicationStatus: miniogo.ReplicationStatusReplica,
ReplicationRequest: true, // always set this to distinguish between `mc mirror` replication and serverside
}, },
}) })
@@ -258,6 +282,7 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectVersionInfo, objectA
} else { } else {
versionPurgeStatus = Failed versionPurgeStatus = Failed
} }
logger.LogIf(ctx, fmt.Errorf("Unable to replicate delete marker to %s/%s(%s): %s", rcfg.GetDestination().Bucket, dobj.ObjectName, versionID, rmErr))
} else { } else {
if dobj.VersionID == "" { if dobj.VersionID == "" {
replicationStatus = string(replication.Completed) replicationStatus = string(replication.Completed)
@@ -272,66 +297,102 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectVersionInfo, objectA
} }
// Update metadata on the delete marker or purge permanent delete if replication success. // Update metadata on the delete marker or purge permanent delete if replication success.
objInfo, err := objectAPI.DeleteObject(ctx, bucket, dobj.ObjectName, ObjectOptions{ dobjInfo, err := objectAPI.DeleteObject(ctx, bucket, dobj.ObjectName, ObjectOptions{
VersionID: versionID, VersionID: versionID,
DeleteMarker: dobj.DeleteMarker,
DeleteMarkerReplicationStatus: replicationStatus, DeleteMarkerReplicationStatus: replicationStatus,
Versioned: globalBucketVersioningSys.Enabled(bucket),
VersionPurgeStatus: versionPurgeStatus, VersionPurgeStatus: versionPurgeStatus,
Versioned: globalBucketVersioningSys.Enabled(bucket),
VersionSuspended: globalBucketVersioningSys.Suspended(bucket), VersionSuspended: globalBucketVersioningSys.Suspended(bucket),
}) })
if err != nil { if err != nil && !isErrVersionNotFound(err) { // VersionNotFound would be reported by pool that object version is missing on.
logger.LogIf(ctx, fmt.Errorf("Unable to update replication metadata for %s/%s %s: %w", bucket, dobj.ObjectName, dobj.VersionID, err)) logger.LogIf(ctx, fmt.Errorf("Unable to update replication metadata for %s/%s(%s): %s", bucket, dobj.ObjectName, versionID, err))
sendEvent(eventArgs{
BucketName: bucket,
Object: ObjectInfo{
Bucket: bucket,
Name: dobj.ObjectName,
VersionID: versionID,
DeleteMarker: dobj.DeleteMarker,
},
Host: "Internal: [Replication]",
EventName: eventName,
})
} else {
sendEvent(eventArgs{
BucketName: bucket,
Object: dobjInfo,
Host: "Internal: [Replication]",
EventName: eventName,
})
} }
sendEvent(eventArgs{
BucketName: bucket,
Object: objInfo,
Host: "Internal: [Replication]",
EventName: eventName,
})
} }
func getCopyObjMetadata(oi ObjectInfo, dest replication.Destination) map[string]string { func getCopyObjMetadata(oi ObjectInfo, dest replication.Destination) map[string]string {
meta := make(map[string]string, len(oi.UserDefined)) meta := make(map[string]string, len(oi.UserDefined))
for k, v := range oi.UserDefined { for k, v := range oi.UserDefined {
if k == xhttp.AmzBucketReplicationStatus {
continue
}
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) { if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
continue continue
} }
if equals(k, xhttp.AmzBucketReplicationStatus) {
continue
}
// https://github.com/google/security-research/security/advisories/GHSA-76wf-9vgp-pj7w
if equals(k, xhttp.AmzMetaUnencryptedContentLength, xhttp.AmzMetaUnencryptedContentMD5) {
continue
}
meta[k] = v meta[k] = v
} }
if oi.ContentEncoding != "" { if oi.ContentEncoding != "" {
meta[xhttp.ContentEncoding] = oi.ContentEncoding meta[xhttp.ContentEncoding] = oi.ContentEncoding
} }
if oi.ContentType != "" { if oi.ContentType != "" {
meta[xhttp.ContentType] = oi.ContentType meta[xhttp.ContentType] = oi.ContentType
} }
tag, err := tags.ParseObjectTags(oi.UserTags)
if err != nil { if oi.UserTags != "" {
return nil meta[xhttp.AmzObjectTagging] = oi.UserTags
}
if tag != nil {
meta[xhttp.AmzObjectTagging] = tag.String()
meta[xhttp.AmzTagDirective] = "REPLACE" meta[xhttp.AmzTagDirective] = "REPLACE"
} }
sc := dest.StorageClass sc := dest.StorageClass
if sc == "" { if sc == "" {
sc = oi.StorageClass sc = oi.StorageClass
} }
meta[xhttp.AmzStorageClass] = sc if sc != "" {
if oi.UserTags != "" { meta[xhttp.AmzStorageClass] = sc
meta[xhttp.AmzObjectTagging] = oi.UserTags
} }
meta[xhttp.MinIOSourceMTime] = oi.ModTime.Format(time.RFC3339Nano)
meta[xhttp.MinIOSourceETag] = oi.ETag meta[xhttp.MinIOSourceETag] = oi.ETag
meta[xhttp.MinIOSourceMTime] = oi.ModTime.Format(time.RFC3339Nano)
meta[xhttp.AmzBucketReplicationStatus] = replication.Replica.String() meta[xhttp.AmzBucketReplicationStatus] = replication.Replica.String()
return meta return meta
} }
func putReplicationOpts(ctx context.Context, dest replication.Destination, objInfo ObjectInfo) (putOpts miniogo.PutObjectOptions) { type caseInsensitiveMap map[string]string
// Lookup map entry case insensitively.
func (m caseInsensitiveMap) Lookup(key string) (string, bool) {
if len(m) == 0 {
return "", false
}
for _, k := range []string{
key,
strings.ToLower(key),
http.CanonicalHeaderKey(key),
} {
v, ok := m[k]
if ok {
return v, ok
}
}
return "", false
}
func putReplicationOpts(ctx context.Context, dest replication.Destination, objInfo ObjectInfo) (putOpts miniogo.PutObjectOptions, err error) {
meta := make(map[string]string) meta := make(map[string]string)
for k, v := range objInfo.UserDefined { for k, v := range objInfo.UserDefined {
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) { if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
@@ -342,54 +403,58 @@ func putReplicationOpts(ctx context.Context, dest replication.Destination, objIn
} }
meta[k] = v meta[k] = v
} }
tag, err := tags.ParseObjectTags(objInfo.UserTags)
if err != nil {
return
}
sc := dest.StorageClass sc := dest.StorageClass
if sc == "" { if sc == "" {
sc = objInfo.StorageClass sc = objInfo.StorageClass
} }
putOpts = miniogo.PutObjectOptions{ putOpts = miniogo.PutObjectOptions{
UserMetadata: meta, UserMetadata: meta,
UserTags: tag.ToMap(),
ContentType: objInfo.ContentType, ContentType: objInfo.ContentType,
ContentEncoding: objInfo.ContentEncoding, ContentEncoding: objInfo.ContentEncoding,
StorageClass: sc, StorageClass: sc,
Internal: miniogo.AdvancedPutOptions{ Internal: miniogo.AdvancedPutOptions{
SourceVersionID: objInfo.VersionID, SourceVersionID: objInfo.VersionID,
ReplicationStatus: miniogo.ReplicationStatusReplica, ReplicationStatus: miniogo.ReplicationStatusReplica,
SourceMTime: objInfo.ModTime, SourceMTime: objInfo.ModTime,
SourceETag: objInfo.ETag, SourceETag: objInfo.ETag,
ReplicationRequest: true, // always set this to distinguish between `mc mirror` replication and serverside
}, },
} }
if lang, ok := objInfo.UserDefined[xhttp.ContentLanguage]; ok { if objInfo.UserTags != "" {
tag, _ := tags.ParseObjectTags(objInfo.UserTags)
if tag != nil {
putOpts.UserTags = tag.ToMap()
}
}
lkMap := caseInsensitiveMap(objInfo.UserDefined)
if lang, ok := lkMap.Lookup(xhttp.ContentLanguage); ok {
putOpts.ContentLanguage = lang putOpts.ContentLanguage = lang
} }
if disp, ok := objInfo.UserDefined[xhttp.ContentDisposition]; ok { if disp, ok := lkMap.Lookup(xhttp.ContentDisposition); ok {
putOpts.ContentDisposition = disp putOpts.ContentDisposition = disp
} }
if cc, ok := objInfo.UserDefined[xhttp.CacheControl]; ok { if cc, ok := lkMap.Lookup(xhttp.CacheControl); ok {
putOpts.CacheControl = cc putOpts.CacheControl = cc
} }
if mode, ok := objInfo.UserDefined[xhttp.AmzObjectLockMode]; ok { if mode, ok := lkMap.Lookup(xhttp.AmzObjectLockMode); ok {
rmode := miniogo.RetentionMode(mode) rmode := miniogo.RetentionMode(mode)
putOpts.Mode = rmode putOpts.Mode = rmode
} }
if retainDateStr, ok := objInfo.UserDefined[xhttp.AmzObjectLockRetainUntilDate]; ok { if retainDateStr, ok := lkMap.Lookup(xhttp.AmzObjectLockRetainUntilDate); ok {
rdate, err := time.Parse(time.RFC3339Nano, retainDateStr) rdate, err := time.Parse(time.RFC3339, retainDateStr)
if err != nil { if err != nil {
return return putOpts, err
} }
putOpts.RetainUntilDate = rdate putOpts.RetainUntilDate = rdate
} }
if lhold, ok := objInfo.UserDefined[xhttp.AmzObjectLockLegalHold]; ok { if lhold, ok := lkMap.Lookup(xhttp.AmzObjectLockLegalHold); ok {
putOpts.LegalHold = miniogo.LegalHoldStatus(lhold) putOpts.LegalHold = miniogo.LegalHoldStatus(lhold)
} }
if crypto.S3.IsEncrypted(objInfo.UserDefined) { if crypto.S3.IsEncrypted(objInfo.UserDefined) {
putOpts.ServerSideEncryption = encrypt.NewSSE() putOpts.ServerSideEncryption = encrypt.NewSSE()
} }
return return
} }
@@ -401,6 +466,16 @@ const (
replicateAll replicationAction = "all" replicateAll replicationAction = "all"
) )
// matches k1 with all keys, returns 'true' if one of them matches
func equals(k1 string, keys ...string) bool {
for _, k2 := range keys {
if strings.ToLower(k1) == strings.ToLower(k2) {
return true
}
}
return false
}
// returns replicationAction by comparing metadata between source and target // returns replicationAction by comparing metadata between source and target
func getReplicationAction(oi1 ObjectInfo, oi2 minio.ObjectInfo) replicationAction { func getReplicationAction(oi1 ObjectInfo, oi2 minio.ObjectInfo) replicationAction {
// needs full replication // needs full replication
@@ -408,83 +483,153 @@ func getReplicationAction(oi1 ObjectInfo, oi2 minio.ObjectInfo) replicationActio
oi1.VersionID != oi2.VersionID || oi1.VersionID != oi2.VersionID ||
oi1.Size != oi2.Size || oi1.Size != oi2.Size ||
oi1.DeleteMarker != oi2.IsDeleteMarker || oi1.DeleteMarker != oi2.IsDeleteMarker ||
!oi1.ModTime.Equal(oi2.LastModified) { oi1.ModTime.Unix() != oi2.LastModified.Unix() {
return replicateAll return replicateAll
} }
if oi1.ContentType != oi2.ContentType { if oi1.ContentType != oi2.ContentType {
return replicateMetadata return replicateMetadata
} }
if oi1.ContentEncoding != "" { if oi1.ContentEncoding != "" {
enc, ok := oi2.Metadata[xhttp.ContentEncoding] enc, ok := oi2.Metadata[xhttp.ContentEncoding]
if !ok || strings.Join(enc, "") != oi1.ContentEncoding { if !ok {
enc, ok = oi2.Metadata[strings.ToLower(xhttp.ContentEncoding)]
if !ok {
return replicateMetadata
}
}
if strings.Join(enc, ",") != oi1.ContentEncoding {
return replicateMetadata return replicateMetadata
} }
} }
t, _ := tags.ParseObjectTags(oi1.UserTags)
if !reflect.DeepEqual(oi2.UserTags, t.ToMap()) {
return replicateMetadata
}
// Compare only necessary headers
compareKeys := []string{
"Expires",
"Cache-Control",
"Content-Language",
"Content-Disposition",
"X-Amz-Object-Lock-Mode",
"X-Amz-Object-Lock-Retain-Until-Date",
"X-Amz-Object-Lock-Legal-Hold",
"X-Amz-Website-Redirect-Location",
"X-Amz-Meta-",
}
// compare metadata on both maps to see if meta is identical // compare metadata on both maps to see if meta is identical
for k1, v1 := range oi1.UserDefined { compareMeta1 := make(map[string]string)
if v2, ok := oi2.UserMetadata[k1]; ok && v1 == v2 { for k, v := range oi1.UserDefined {
continue var found bool
for _, prefix := range compareKeys {
if !strings.HasPrefix(strings.ToLower(k), strings.ToLower(prefix)) {
continue
}
found = true
break
} }
if v2, ok := oi2.Metadata[k1]; ok && v1 == strings.Join(v2, "") { if found {
continue compareMeta1[strings.ToLower(k)] = v
} }
}
compareMeta2 := make(map[string]string)
for k, v := range oi2.Metadata {
var found bool
for _, prefix := range compareKeys {
if !strings.HasPrefix(strings.ToLower(k), strings.ToLower(prefix)) {
continue
}
found = true
break
}
if found {
compareMeta2[strings.ToLower(k)] = strings.Join(v, ",")
}
}
if !reflect.DeepEqual(compareMeta1, compareMeta2) {
return replicateMetadata return replicateMetadata
} }
for k1, v1 := range oi2.UserMetadata {
if v2, ok := oi1.UserDefined[k1]; !ok || v1 != v2 {
return replicateMetadata
}
}
for k1, v1slc := range oi2.Metadata {
v1 := strings.Join(v1slc, "")
if k1 == xhttp.ContentEncoding { //already compared
continue
}
if v2, ok := oi1.UserDefined[k1]; !ok || v1 != v2 {
return replicateMetadata
}
}
t, _ := tags.MapToObjectTags(oi2.UserTags)
if t.String() != oi1.UserTags {
return replicateMetadata
}
return replicateNone return replicateNone
} }
// replicateObject replicates the specified version of the object to destination bucket // replicateObject replicates the specified version of the object to destination bucket
// The source object is then updated to reflect the replication status. // The source object is then updated to reflect the replication status.
func replicateObject(ctx context.Context, objInfo ObjectInfo, objectAPI ObjectLayer) { func replicateObject(ctx context.Context, objInfo ObjectInfo, objectAPI ObjectLayer) {
z, ok := objectAPI.(*erasureServerPools)
if !ok {
return
}
bucket := objInfo.Bucket bucket := objInfo.Bucket
object := objInfo.Name object := objInfo.Name
cfg, err := getReplicationConfig(ctx, bucket) cfg, err := getReplicationConfig(ctx, bucket)
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
sendEvent(eventArgs{
EventName: event.ObjectReplicationNotTracked,
BucketName: bucket,
Object: objInfo,
Host: "Internal: [Replication]",
})
return return
} }
tgt := globalBucketTargetSys.GetRemoteTargetClient(ctx, cfg.RoleArn) tgt := globalBucketTargetSys.GetRemoteTargetClient(ctx, cfg.RoleArn)
if tgt == nil { if tgt == nil {
logger.LogIf(ctx, fmt.Errorf("failed to get target for bucket:%s arn:%s", bucket, cfg.RoleArn)) logger.LogIf(ctx, fmt.Errorf("failed to get target for bucket:%s arn:%s", bucket, cfg.RoleArn))
sendEvent(eventArgs{
EventName: event.ObjectReplicationNotTracked,
BucketName: bucket,
Object: objInfo,
Host: "Internal: [Replication]",
})
return return
} }
gr, err := objectAPI.GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, readLock, ObjectOptions{ gr, err := objectAPI.GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, readLock, ObjectOptions{
VersionID: objInfo.VersionID, VersionID: objInfo.VersionID,
}) })
if err != nil { if err != nil {
sendEvent(eventArgs{
EventName: event.ObjectReplicationNotTracked,
BucketName: bucket,
Object: objInfo,
Host: "Internal: [Replication]",
})
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return return
} }
defer gr.Close() // hold read lock for entire transaction
objInfo = gr.ObjInfo objInfo = gr.ObjInfo
size, err := objInfo.GetActualSize() size, err := objInfo.GetActualSize()
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
gr.Close() sendEvent(eventArgs{
EventName: event.ObjectReplicationNotTracked,
BucketName: bucket,
Object: objInfo,
Host: "Internal: [Replication]",
})
return return
} }
dest := cfg.GetDestination() dest := cfg.GetDestination()
if dest.Bucket == "" { if dest.Bucket == "" {
gr.Close() logger.LogIf(ctx, fmt.Errorf("Unable to replicate object %s(%s), bucket is empty", objInfo.Name, objInfo.VersionID))
sendEvent(eventArgs{
EventName: event.ObjectReplicationNotTracked,
BucketName: bucket,
Object: objInfo,
Host: "Internal: [Replication]",
})
return return
} }
@@ -497,31 +642,54 @@ func replicateObject(ctx context.Context, objInfo ObjectInfo, objectAPI ObjectLa
if err == nil { if err == nil {
rtype = getReplicationAction(objInfo, oi) rtype = getReplicationAction(objInfo, oi)
if rtype == replicateNone { if rtype == replicateNone {
gr.Close()
// object with same VersionID already exists, replication kicked off by // object with same VersionID already exists, replication kicked off by
// PutObject might have completed. // PutObject might have completed.
return return
} }
} }
replicationStatus := replication.Completed replicationStatus := replication.Completed
// use core client to avoid doing multipart on PUT
c := &miniogo.Core{Client: tgt.Client}
if rtype != replicateAll { if rtype != replicateAll {
gr.Close()
// replicate metadata for object tagging/copy with metadata replacement // replicate metadata for object tagging/copy with metadata replacement
dstOpts := miniogo.PutObjectOptions{Internal: miniogo.AdvancedPutOptions{SourceVersionID: objInfo.VersionID}} srcOpts := miniogo.CopySrcOptions{
c := &miniogo.Core{Client: tgt.Client} Bucket: dest.Bucket,
_, err = c.CopyObject(ctx, dest.Bucket, object, dest.Bucket, object, getCopyObjMetadata(objInfo, dest), dstOpts) Object: object,
if err != nil { VersionID: objInfo.VersionID}
dstOpts := miniogo.PutObjectOptions{
Internal: miniogo.AdvancedPutOptions{
SourceVersionID: objInfo.VersionID,
ReplicationRequest: true, // always set this to distinguish between `mc mirror` replication and serverside
}}
if _, err = c.CopyObject(ctx, dest.Bucket, object, dest.Bucket, object, getCopyObjMetadata(objInfo, dest), srcOpts, dstOpts); err != nil {
replicationStatus = replication.Failed replicationStatus = replication.Failed
logger.LogIf(ctx, fmt.Errorf("Unable to replicate metadata for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err))
} }
} else { } else {
target, err := globalBucketMetadataSys.GetBucketTarget(bucket, cfg.RoleArn) target, err := globalBucketMetadataSys.GetBucketTarget(bucket, cfg.RoleArn)
if err != nil { if err != nil {
logger.LogIf(ctx, fmt.Errorf("failed to get target for replication bucket:%s cfg:%s err:%s", bucket, cfg.RoleArn, err)) logger.LogIf(ctx, fmt.Errorf("failed to get target for replication bucket:%s cfg:%s err:%s", bucket, cfg.RoleArn, err))
gr.Close() sendEvent(eventArgs{
EventName: event.ObjectReplicationNotTracked,
BucketName: bucket,
Object: objInfo,
Host: "Internal: [Replication]",
})
return
}
putOpts, err := putReplicationOpts(ctx, dest, objInfo)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("failed to get target for replication bucket:%s cfg:%s err:%w", bucket, cfg.RoleArn, err))
sendEvent(eventArgs{
EventName: event.ObjectReplicationNotTracked,
BucketName: bucket,
Object: objInfo,
Host: "Internal: [Replication]",
})
return return
} }
putOpts := putReplicationOpts(ctx, dest, objInfo)
// Setup bandwidth throttling // Setup bandwidth throttling
peers, _ := globalEndpoints.peers() peers, _ := globalEndpoints.peers()
totalNodesCount := len(peers) totalNodesCount := len(peers)
@@ -536,11 +704,11 @@ func replicateObject(ctx context.Context, objInfo ObjectInfo, objectAPI ObjectLa
// r takes over closing gr. // r takes over closing gr.
r := bandwidth.NewMonitoredReader(ctx, globalBucketMonitor, objInfo.Bucket, objInfo.Name, gr, headerSize, b, target.BandwidthLimit) r := bandwidth.NewMonitoredReader(ctx, globalBucketMonitor, objInfo.Bucket, objInfo.Name, gr, headerSize, b, target.BandwidthLimit)
_, err = tgt.PutObject(ctx, dest.Bucket, object, r, size, putOpts) if _, err = c.PutObject(ctx, dest.Bucket, object, r, size, "", "", putOpts); err != nil {
if err != nil {
replicationStatus = replication.Failed replicationStatus = replication.Failed
logger.LogIf(ctx, fmt.Errorf("Unable to replicate for object %s/%s(%s): %w", bucket, objInfo.Name, objInfo.VersionID, err))
} }
r.Close() defer r.Close()
} }
objInfo.UserDefined[xhttp.AmzBucketReplicationStatus] = replicationStatus.String() objInfo.UserDefined[xhttp.AmzBucketReplicationStatus] = replicationStatus.String()
@@ -549,7 +717,6 @@ func replicateObject(ctx context.Context, objInfo ObjectInfo, objectAPI ObjectLa
} }
// FIXME: add support for missing replication events // FIXME: add support for missing replication events
// - event.ObjectReplicationNotTracked
// - event.ObjectReplicationMissedThreshold // - event.ObjectReplicationMissedThreshold
// - event.ObjectReplicationReplicatedAfterThreshold // - event.ObjectReplicationReplicatedAfterThreshold
var eventName = event.ObjectReplicationComplete var eventName = event.ObjectReplicationComplete
@@ -557,16 +724,17 @@ func replicateObject(ctx context.Context, objInfo ObjectInfo, objectAPI ObjectLa
eventName = event.ObjectReplicationFailed eventName = event.ObjectReplicationFailed
} }
objInfo.metadataOnly = true // Perform only metadata updates. // This lower level implementation is necessary to avoid write locks from CopyObject.
objInfo, err = objectAPI.CopyObject(ctx, bucket, object, bucket, object, objInfo, ObjectOptions{ poolIdx, err := z.getPoolIdx(ctx, bucket, object, objInfo.Size)
VersionID: objInfo.VersionID,
}, ObjectOptions{
VersionID: objInfo.VersionID,
})
if err != nil { if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to update replication metadata for %s: %s", objInfo.VersionID, err)) logger.LogIf(ctx, fmt.Errorf("Unable to update replication metadata for %s/%s(%s): %w", bucket, objInfo.Name, objInfo.VersionID, err))
} else {
if err = z.serverPools[poolIdx].getHashedSet(object).updateObjectMeta(ctx, bucket, object, objInfo.UserDefined, ObjectOptions{
VersionID: objInfo.VersionID,
}); err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to update replication metadata for %s/%s(%s): %w", bucket, objInfo.Name, objInfo.VersionID, err))
}
} }
sendEvent(eventArgs{ sendEvent(eventArgs{
EventName: eventName, EventName: eventName,
BucketName: bucket, BucketName: bucket,
@@ -603,91 +771,102 @@ type DeletedObjectVersionInfo struct {
DeletedObject DeletedObject
Bucket string Bucket string
} }
type replicationState struct {
// add future metrics here
replicaCh chan ObjectInfo
replicaDeleteCh chan DeletedObjectVersionInfo
}
func (r *replicationState) queueReplicaTask(oi ObjectInfo) {
if r == nil {
return
}
select {
case r.replicaCh <- oi:
default:
}
}
func (r *replicationState) queueReplicaDeleteTask(doi DeletedObjectVersionInfo) {
if r == nil {
return
}
select {
case r.replicaDeleteCh <- doi:
default:
}
}
var ( var (
globalReplicationState *replicationState globalReplicationPool *ReplicationPool
// TODO: currently keeping it conservative
// but eventually can be tuned in future,
// take only half the CPUs for replication
// conservatively.
globalReplicationConcurrent = runtime.GOMAXPROCS(0) / 2
) )
func newReplicationState() *replicationState { // ReplicationPool describes replication pool
type ReplicationPool struct {
// fix minimum concurrent replication to 1 for single CPU setup mu sync.Mutex
if globalReplicationConcurrent == 0 { size int
globalReplicationConcurrent = 1 replicaCh chan ObjectInfo
} replicaDeleteCh chan DeletedObjectVersionInfo
rs := &replicationState{ killCh chan struct{}
replicaCh: make(chan ObjectInfo, 10000), wg sync.WaitGroup
replicaDeleteCh: make(chan DeletedObjectVersionInfo, 10000), ctx context.Context
} objLayer ObjectLayer
go func() {
<-GlobalContext.Done()
close(rs.replicaCh)
close(rs.replicaDeleteCh)
}()
return rs
} }
// addWorker creates a new worker to process tasks // NewReplicationPool creates a pool of replication workers of specified size
func (r *replicationState) addWorker(ctx context.Context, objectAPI ObjectLayer) { func NewReplicationPool(ctx context.Context, o ObjectLayer, sz int) *ReplicationPool {
// Add a new worker. pool := &ReplicationPool{
replicaCh: make(chan ObjectInfo, 10000),
replicaDeleteCh: make(chan DeletedObjectVersionInfo, 10000),
ctx: ctx,
objLayer: o,
}
go func() { go func() {
for { <-ctx.Done()
select { close(pool.replicaCh)
case <-ctx.Done(): close(pool.replicaDeleteCh)
return
case oi, ok := <-r.replicaCh:
if !ok {
return
}
replicateObject(ctx, oi, objectAPI)
case doi, ok := <-r.replicaDeleteCh:
if !ok {
return
}
replicateDelete(ctx, doi, objectAPI)
}
}
}() }()
pool.Resize(sz)
return pool
}
// AddWorker adds a replication worker to the pool
func (p *ReplicationPool) AddWorker() {
defer p.wg.Done()
for {
select {
case <-p.ctx.Done():
return
case oi, ok := <-p.replicaCh:
if !ok {
return
}
replicateObject(p.ctx, oi, p.objLayer)
case doi, ok := <-p.replicaDeleteCh:
if !ok {
return
}
replicateDelete(p.ctx, doi, p.objLayer)
case <-p.killCh:
return
}
}
}
//Resize replication pool to new size
func (p *ReplicationPool) Resize(n int) {
p.mu.Lock()
defer p.mu.Unlock()
for p.size < n {
p.size++
p.wg.Add(1)
go p.AddWorker()
}
for p.size > n {
p.size--
go func() { p.killCh <- struct{}{} }()
}
}
func (p *ReplicationPool) queueReplicaTask(oi ObjectInfo) {
if p == nil {
return
}
select {
case p.replicaCh <- oi:
default:
}
}
func (p *ReplicationPool) queueReplicaDeleteTask(doi DeletedObjectVersionInfo) {
if p == nil {
return
}
select {
case p.replicaDeleteCh <- doi:
default:
}
} }
func initBackgroundReplication(ctx context.Context, objectAPI ObjectLayer) { func initBackgroundReplication(ctx context.Context, objectAPI ObjectLayer) {
if globalReplicationState == nil { globalReplicationPool = NewReplicationPool(ctx, objectAPI, globalAPIConfig.getReplicationWorkers())
return
}
// Start with globalReplicationConcurrent.
for i := 0; i < globalReplicationConcurrent; i++ {
globalReplicationState.addWorker(ctx, objectAPI)
}
} }
// get Reader from replication target if active-active replication is in place and // get Reader from replication target if active-active replication is in place and
@@ -714,8 +893,11 @@ func proxyGetToReplicationTarget(ctx context.Context, bucket, object string, rs
return nil, false return nil, false
} }
} }
// Make sure to match ETag when proxying.
if err = gopts.SetMatchETag(oi.ETag); err != nil {
return nil, false
}
c := miniogo.Core{Client: tgt.Client} c := miniogo.Core{Client: tgt.Client}
obj, _, _, err := c.GetObject(ctx, bucket, object, gopts) obj, _, _, err := c.GetObject(ctx, bucket, object, gopts)
if err != nil { if err != nil {
return nil, false return nil, false
@@ -726,6 +908,7 @@ func proxyGetToReplicationTarget(ctx context.Context, bucket, object string, rs
if err != nil { if err != nil {
return nil, false return nil, false
} }
reader.ObjInfo = oi.Clone()
return reader, true return reader, true
} }
@@ -781,6 +964,7 @@ func proxyHeadToRepTarget(ctx context.Context, bucket, object string, opts Objec
if err != nil { if err != nil {
return nil, oi, false, err return nil, oi, false, err
} }
tags, _ := tags.MapToObjectTags(objInfo.UserTags) tags, _ := tags.MapToObjectTags(objInfo.UserTags)
oi = ObjectInfo{ oi = ObjectInfo{
Bucket: bucket, Bucket: bucket,
@@ -795,12 +979,18 @@ func proxyHeadToRepTarget(ctx context.Context, bucket, object string, opts Objec
Expires: objInfo.Expires, Expires: objInfo.Expires,
StorageClass: objInfo.StorageClass, StorageClass: objInfo.StorageClass,
ReplicationStatus: replication.StatusType(objInfo.ReplicationStatus), ReplicationStatus: replication.StatusType(objInfo.ReplicationStatus),
UserDefined: cloneMSS(objInfo.UserMetadata),
UserTags: tags.String(), UserTags: tags.String(),
} }
if ce, ok := oi.UserDefined[xhttp.ContentEncoding]; ok { oi.UserDefined = make(map[string]string, len(objInfo.Metadata))
for k, v := range objInfo.Metadata {
oi.UserDefined[k] = v[0]
}
ce, ok := oi.UserDefined[xhttp.ContentEncoding]
if !ok {
ce, ok = oi.UserDefined[strings.ToLower(xhttp.ContentEncoding)]
}
if ok {
oi.ContentEncoding = ce oi.ContentEncoding = ce
delete(oi.UserDefined, xhttp.ContentEncoding)
} }
return tgt, oi, true, nil return tgt, oi, true, nil
} }
@@ -816,7 +1006,7 @@ func scheduleReplication(ctx context.Context, objInfo ObjectInfo, o ObjectLayer,
if sync { if sync {
replicateObject(ctx, objInfo, o) replicateObject(ctx, objInfo, o)
} else { } else {
globalReplicationState.queueReplicaTask(objInfo) globalReplicationPool.queueReplicaTask(objInfo)
} }
} }
@@ -824,6 +1014,6 @@ func scheduleReplicationDelete(ctx context.Context, dv DeletedObjectVersionInfo,
if sync { if sync {
replicateDelete(ctx, dv, o) replicateDelete(ctx, dv, o)
} else { } else {
globalReplicationState.queueReplicaDeleteTask(dv) globalReplicationPool.queueReplicaDeleteTask(dv)
} }
} }
+2 -1
View File
@@ -18,6 +18,7 @@ package cmd
import ( import (
"context" "context"
"crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"net/http" "net/http"
@@ -32,7 +33,6 @@ import (
"github.com/minio/minio/cmd/crypto" "github.com/minio/minio/cmd/crypto"
"github.com/minio/minio/pkg/bucket/versioning" "github.com/minio/minio/pkg/bucket/versioning"
"github.com/minio/minio/pkg/madmin" "github.com/minio/minio/pkg/madmin"
sha256 "github.com/minio/sha256-simd"
) )
const ( const (
@@ -362,6 +362,7 @@ func (sys *BucketTargetSys) getRemoteTargetClient(tcfg *madmin.BucketTarget) (*T
api, err := minio.New(tcfg.Endpoint, &miniogo.Options{ api, err := minio.New(tcfg.Endpoint, &miniogo.Options{
Creds: creds, Creds: creds,
Secure: tcfg.Secure, Secure: tcfg.Secure,
Region: tcfg.Region,
Transport: getRemoteTargetInstanceTransport, Transport: getRemoteTargetInstanceTransport,
}) })
if err != nil { if err != nil {
+5 -2
View File
@@ -50,10 +50,14 @@ import (
var serverDebugLog = env.Get("_MINIO_SERVER_DEBUG", config.EnableOff) == config.EnableOn var serverDebugLog = env.Get("_MINIO_SERVER_DEBUG", config.EnableOff) == config.EnableOn
func init() { func init() {
rand.Seed(time.Now().UTC().UnixNano())
logger.Init(GOPATH, GOROOT) logger.Init(GOPATH, GOROOT)
logger.RegisterError(config.FmtError) logger.RegisterError(config.FmtError)
rand.Seed(time.Now().UTC().UnixNano()) // Inject into config package.
config.Logger.Info = logger.Info
config.Logger.LogIf = logger.LogIf
globalDNSCache = xhttp.NewDNSCache(10*time.Second, 10*time.Second, logger.LogOnceIf) globalDNSCache = xhttp.NewDNSCache(10*time.Second, 10*time.Second, logger.LogOnceIf)
@@ -69,7 +73,6 @@ func init() {
}, },
}) })
globalReplicationState = newReplicationState()
globalTransitionState = newTransitionState() globalTransitionState = newTransitionState()
console.SetColor("Debug", color.New()) console.SetColor("Debug", color.New())
+13 -8
View File
@@ -20,6 +20,8 @@ import (
"bytes" "bytes"
"context" "context"
"errors" "errors"
"io/ioutil"
"net/http"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
) )
@@ -27,9 +29,9 @@ import (
var errConfigNotFound = errors.New("config file not found") var errConfigNotFound = errors.New("config file not found")
func readConfig(ctx context.Context, objAPI ObjectLayer, configFile string) ([]byte, error) { func readConfig(ctx context.Context, objAPI ObjectLayer, configFile string) ([]byte, error) {
var buffer bytes.Buffer
// Read entire content by setting size to -1 // Read entire content by setting size to -1
if err := objAPI.GetObject(ctx, minioMetaBucket, configFile, 0, -1, &buffer, "", ObjectOptions{}); err != nil { r, err := objAPI.GetObjectNInfo(ctx, minioMetaBucket, configFile, nil, http.Header{}, readLock, ObjectOptions{})
if err != nil {
// Treat object not found as config not found. // Treat object not found as config not found.
if isErrObjectNotFound(err) { if isErrObjectNotFound(err) {
return nil, errConfigNotFound return nil, errConfigNotFound
@@ -37,13 +39,16 @@ func readConfig(ctx context.Context, objAPI ObjectLayer, configFile string) ([]b
return nil, err return nil, err
} }
defer r.Close()
// Return config not found on empty content. buf, err := ioutil.ReadAll(r)
if buffer.Len() == 0 { if err != nil {
return nil, err
}
if len(buf) == 0 {
return nil, errConfigNotFound return nil, errConfigNotFound
} }
return buf, nil
return buffer.Bytes(), nil
} }
type objectDeleter interface { type objectDeleter interface {
@@ -59,12 +64,12 @@ func deleteConfig(ctx context.Context, objAPI objectDeleter, configFile string)
} }
func saveConfig(ctx context.Context, objAPI ObjectLayer, configFile string, data []byte) error { func saveConfig(ctx context.Context, objAPI ObjectLayer, configFile string, data []byte) error {
hashReader, err := hash.NewReader(bytes.NewReader(data), int64(len(data)), "", getSHA256Hash(data), int64(len(data)), globalCLIContext.StrictS3Compat) hashReader, err := hash.NewReader(bytes.NewReader(data), int64(len(data)), "", getSHA256Hash(data), int64(len(data)))
if err != nil { if err != nil {
return err return err
} }
_, err = objAPI.PutObject(ctx, minioMetaBucket, configFile, NewPutObjReader(hashReader, nil, nil), ObjectOptions{}) _, err = objAPI.PutObject(ctx, minioMetaBucket, configFile, NewPutObjReader(hashReader), ObjectOptions{MaxParity: true})
return err return err
} }
+27 -18
View File
@@ -18,6 +18,7 @@ package cmd
import ( import (
"context" "context"
"crypto/tls"
"fmt" "fmt"
"strings" "strings"
"sync" "sync"
@@ -26,7 +27,6 @@ import (
"github.com/minio/minio/cmd/config/api" "github.com/minio/minio/cmd/config/api"
"github.com/minio/minio/cmd/config/cache" "github.com/minio/minio/cmd/config/cache"
"github.com/minio/minio/cmd/config/compress" "github.com/minio/minio/cmd/config/compress"
"github.com/minio/minio/cmd/config/crawler"
"github.com/minio/minio/cmd/config/dns" "github.com/minio/minio/cmd/config/dns"
"github.com/minio/minio/cmd/config/etcd" "github.com/minio/minio/cmd/config/etcd"
"github.com/minio/minio/cmd/config/heal" "github.com/minio/minio/cmd/config/heal"
@@ -34,6 +34,7 @@ import (
"github.com/minio/minio/cmd/config/identity/openid" "github.com/minio/minio/cmd/config/identity/openid"
"github.com/minio/minio/cmd/config/notify" "github.com/minio/minio/cmd/config/notify"
"github.com/minio/minio/cmd/config/policy/opa" "github.com/minio/minio/cmd/config/policy/opa"
"github.com/minio/minio/cmd/config/scanner"
"github.com/minio/minio/cmd/config/storageclass" "github.com/minio/minio/cmd/config/storageclass"
"github.com/minio/minio/cmd/crypto" "github.com/minio/minio/cmd/crypto"
xhttp "github.com/minio/minio/cmd/http" xhttp "github.com/minio/minio/cmd/http"
@@ -59,7 +60,7 @@ func initHelp() {
config.LoggerWebhookSubSys: logger.DefaultKVS, config.LoggerWebhookSubSys: logger.DefaultKVS,
config.AuditWebhookSubSys: logger.DefaultAuditKVS, config.AuditWebhookSubSys: logger.DefaultAuditKVS,
config.HealSubSys: heal.DefaultKVS, config.HealSubSys: heal.DefaultKVS,
config.CrawlerSubSys: crawler.DefaultKVS, config.ScannerSubSys: scanner.DefaultKVS,
} }
for k, v := range notify.DefaultNotificationKVS { for k, v := range notify.DefaultNotificationKVS {
kvs[k] = v kvs[k] = v
@@ -116,8 +117,8 @@ func initHelp() {
Description: "manage object healing frequency and bitrot verification checks", Description: "manage object healing frequency and bitrot verification checks",
}, },
config.HelpKV{ config.HelpKV{
Key: config.CrawlerSubSys, Key: config.ScannerSubSys,
Description: "manage crawling for usage calculation, lifecycle, healing and more", Description: "manage namespace scanning for usage calculation, lifecycle, healing and more",
}, },
config.HelpKV{ config.HelpKV{
Key: config.LoggerWebhookSubSys, Key: config.LoggerWebhookSubSys,
@@ -199,7 +200,7 @@ func initHelp() {
config.CacheSubSys: cache.Help, config.CacheSubSys: cache.Help,
config.CompressionSubSys: compress.Help, config.CompressionSubSys: compress.Help,
config.HealSubSys: heal.Help, config.HealSubSys: heal.Help,
config.CrawlerSubSys: crawler.Help, config.ScannerSubSys: scanner.Help,
config.IdentityOpenIDSubSys: openid.Help, config.IdentityOpenIDSubSys: openid.Help,
config.IdentityLDAPSubSys: xldap.Help, config.IdentityLDAPSubSys: xldap.Help,
config.PolicyOPASubSys: opa.Help, config.PolicyOPASubSys: opa.Help,
@@ -273,11 +274,11 @@ func validateConfig(s config.Config, setDriveCounts []int) error {
} }
} }
if _, err := heal.LookupConfig(s[config.HealSubSys][config.Default]); err != nil { if _, err = heal.LookupConfig(s[config.HealSubSys][config.Default]); err != nil {
return err return err
} }
if _, err := crawler.LookupConfig(s[config.CrawlerSubSys][config.Default]); err != nil { if _, err = scanner.LookupConfig(s[config.ScannerSubSys][config.Default]); err != nil {
return err return err
} }
@@ -295,7 +296,10 @@ func validateConfig(s config.Config, setDriveCounts []int) error {
} }
} }
{ {
kmsCfg, err := crypto.LookupConfig(s, globalCertsCADir.Get(), NewGatewayHTTPTransport()) kmsCfg, err := crypto.LookupConfig(s, globalCertsCADir.Get(), newCustomHTTPTransportWithHTTP2(
&tls.Config{
RootCAs: globalRootCAs,
}, defaultDialTimeout)())
if err != nil { if err != nil {
return err return err
} }
@@ -471,7 +475,10 @@ func lookupConfigs(s config.Config, setDriveCounts []int) {
} }
} }
kmsCfg, err := crypto.LookupConfig(s, globalCertsCADir.Get(), NewGatewayHTTPTransport()) kmsCfg, err := crypto.LookupConfig(s, globalCertsCADir.Get(), newCustomHTTPTransportWithHTTP2(
&tls.Config{
RootCAs: globalRootCAs,
}, defaultDialTimeout)())
if err != nil { if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to setup KMS config: %w", err)) logger.LogIf(ctx, fmt.Errorf("Unable to setup KMS config: %w", err))
} }
@@ -480,11 +487,13 @@ func lookupConfigs(s config.Config, setDriveCounts []int) {
if err != nil { if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to setup KMS with current KMS config: %w", err)) logger.LogIf(ctx, fmt.Errorf("Unable to setup KMS with current KMS config: %w", err))
} }
globalAutoEncryption = kmsCfg.AutoEncryption // Enable auto-encryption if enabled
// Enable auto-encryption if enabled if kmsCfg.Vault.Enabled {
globalAutoEncryption = kmsCfg.AutoEncryption const deprecationWarning = `Native Hashicorp Vault support is deprecated and will be removed on 2021-10-01. Please migrate to KES + Hashicorp Vault: https://github.com/minio/kes/wiki/Hashicorp-Vault-Keystore
if globalAutoEncryption && !globalIsGateway { Note that native Hashicorp Vault and KES + Hashicorp Vault are not compatible.
logger.LogIf(ctx, fmt.Errorf("%s env is deprecated please migrate to using `mc encrypt` at bucket level", crypto.EnvKMSAutoEncryption)) If you need help to migrate smoothly visit: https://min.io/pricing`
logger.LogIf(ctx, fmt.Errorf(deprecationWarning))
} }
globalOpenIDConfig, err = openid.LookupConfig(s[config.IdentityOpenIDSubSys][config.Default], globalOpenIDConfig, err = openid.LookupConfig(s[config.IdentityOpenIDSubSys][config.Default],
@@ -544,7 +553,7 @@ func lookupConfigs(s config.Config, setDriveCounts []int) {
http.WithAuthToken(l.AuthToken), http.WithAuthToken(l.AuthToken),
http.WithUserAgent(loggerUserAgent), http.WithUserAgent(loggerUserAgent),
http.WithLogKind(string(logger.All)), http.WithLogKind(string(logger.All)),
http.WithTransport(NewGatewayHTTPTransport()), http.WithTransport(NewGatewayHTTPTransportWithClientCerts(l.ClientCert, l.ClientKey)),
), ),
); err != nil { ); err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize audit HTTP target: %w", err)) logger.LogIf(ctx, fmt.Errorf("Unable to initialize audit HTTP target: %w", err))
@@ -597,10 +606,10 @@ func applyDynamicConfig(ctx context.Context, objAPI ObjectLayer, s config.Config
return fmt.Errorf("Unable to apply heal config: %w", err) return fmt.Errorf("Unable to apply heal config: %w", err)
} }
// Crawler // Scanner
crawlerCfg, err := crawler.LookupConfig(s[config.CrawlerSubSys][config.Default]) scannerCfg, err := scanner.LookupConfig(s[config.ScannerSubSys][config.Default])
if err != nil { if err != nil {
return fmt.Errorf("Unable to apply crawler config: %w", err) return fmt.Errorf("Unable to apply scanner config: %w", err)
} }
// Apply configurations. // Apply configurations.
@@ -615,7 +624,7 @@ func applyDynamicConfig(ctx context.Context, objAPI ObjectLayer, s config.Config
globalHealConfig = healCfg globalHealConfig = healCfg
globalHealConfigMu.Unlock() globalHealConfigMu.Unlock()
logger.LogIf(ctx, crawlerSleeper.Update(crawlerCfg.Delay, crawlerCfg.MaxWait)) logger.LogIf(ctx, scannerSleeper.Update(scannerCfg.Delay, scannerCfg.MaxWait))
// Update all dynamic config values in memory. // Update all dynamic config values in memory.
globalServerConfigMu.Lock() globalServerConfigMu.Lock()
+25 -9
View File
@@ -29,14 +29,14 @@ import (
// API sub-system constants // API sub-system constants
const ( const (
apiRequestsMax = "requests_max" apiRequestsMax = "requests_max"
apiRequestsDeadline = "requests_deadline" apiRequestsDeadline = "requests_deadline"
apiClusterDeadline = "cluster_deadline" apiClusterDeadline = "cluster_deadline"
apiCorsAllowOrigin = "cors_allow_origin" apiCorsAllowOrigin = "cors_allow_origin"
apiRemoteTransportDeadline = "remote_transport_deadline" apiRemoteTransportDeadline = "remote_transport_deadline"
apiListQuorum = "list_quorum" apiListQuorum = "list_quorum"
apiExtendListCacheLife = "extend_list_cache_life" apiExtendListCacheLife = "extend_list_cache_life"
apiReplicationWorkers = "replication_workers"
EnvAPIRequestsMax = "MINIO_API_REQUESTS_MAX" EnvAPIRequestsMax = "MINIO_API_REQUESTS_MAX"
EnvAPIRequestsDeadline = "MINIO_API_REQUESTS_DEADLINE" EnvAPIRequestsDeadline = "MINIO_API_REQUESTS_DEADLINE"
EnvAPIClusterDeadline = "MINIO_API_CLUSTER_DEADLINE" EnvAPIClusterDeadline = "MINIO_API_CLUSTER_DEADLINE"
@@ -45,6 +45,7 @@ const (
EnvAPIListQuorum = "MINIO_API_LIST_QUORUM" EnvAPIListQuorum = "MINIO_API_LIST_QUORUM"
EnvAPIExtendListCacheLife = "MINIO_API_EXTEND_LIST_CACHE_LIFE" EnvAPIExtendListCacheLife = "MINIO_API_EXTEND_LIST_CACHE_LIFE"
EnvAPISecureCiphers = "MINIO_API_SECURE_CIPHERS" EnvAPISecureCiphers = "MINIO_API_SECURE_CIPHERS"
EnvAPIReplicationWorkers = "MINIO_API_REPLICATION_WORKERS"
) )
// Deprecated key and ENVs // Deprecated key and ENVs
@@ -78,12 +79,16 @@ var (
}, },
config.KV{ config.KV{
Key: apiListQuorum, Key: apiListQuorum,
Value: "optimal", Value: "strict",
}, },
config.KV{ config.KV{
Key: apiExtendListCacheLife, Key: apiExtendListCacheLife,
Value: "0s", Value: "0s",
}, },
config.KV{
Key: apiReplicationWorkers,
Value: "100",
},
} }
) )
@@ -96,6 +101,7 @@ type Config struct {
RemoteTransportDeadline time.Duration `json:"remote_transport_deadline"` RemoteTransportDeadline time.Duration `json:"remote_transport_deadline"`
ListQuorum string `json:"list_strict_quorum"` ListQuorum string `json:"list_strict_quorum"`
ExtendListLife time.Duration `json:"extend_list_cache_life"` ExtendListLife time.Duration `json:"extend_list_cache_life"`
ReplicationWorkers int `json:"replication_workers"`
} }
// UnmarshalJSON - Validate SS and RRS parity when unmarshalling JSON. // UnmarshalJSON - Validate SS and RRS parity when unmarshalling JSON.
@@ -173,6 +179,15 @@ func LookupConfig(kvs config.KVS) (cfg Config, err error) {
return cfg, err return cfg, err
} }
replicationWorkers, err := strconv.Atoi(env.Get(EnvAPIReplicationWorkers, kvs.Get(apiReplicationWorkers)))
if err != nil {
return cfg, err
}
if replicationWorkers <= 0 {
return cfg, config.ErrInvalidReplicationWorkersValue(nil).Msg("Minimum number of replication workers should be 1")
}
return Config{ return Config{
RequestsMax: requestsMax, RequestsMax: requestsMax,
RequestsDeadline: requestsDeadline, RequestsDeadline: requestsDeadline,
@@ -181,5 +196,6 @@ func LookupConfig(kvs config.KVS) (cfg Config, err error) {
RemoteTransportDeadline: remoteTransportDeadline, RemoteTransportDeadline: remoteTransportDeadline,
ListQuorum: listQuorum, ListQuorum: listQuorum,
ExtendListLife: listLife, ExtendListLife: listLife,
ReplicationWorkers: replicationWorkers,
}, nil }, nil
} }
+6
View File
@@ -45,5 +45,11 @@ var (
Optional: true, Optional: true,
Type: "duration", Type: "duration",
}, },
config.HelpKV{
Key: apiReplicationWorkers,
Description: `set the number of replication workers, defaults to 100`,
Optional: true,
Type: "number",
},
} }
) )
+8 -8
View File
@@ -40,19 +40,13 @@ var (
}, },
config.HelpKV{ config.HelpKV{
Key: Exclude, Key: Exclude,
Description: `comma separated wildcard exclusion patterns e.g. "bucket/*.tmp,*.exe"`, Description: `exclude cache for following patterns e.g. "bucket/*.tmp,*.exe"`,
Optional: true, Optional: true,
Type: "csv", Type: "csv",
}, },
config.HelpKV{
Key: config.Comment,
Description: config.DefaultComment,
Optional: true,
Type: "sentence",
},
config.HelpKV{ config.HelpKV{
Key: After, Key: After,
Description: `minimum accesses before caching an object`, Description: `minimum number of access before caching an object`,
Optional: true, Optional: true,
Type: "number", Type: "number",
}, },
@@ -80,5 +74,11 @@ var (
Optional: true, Optional: true,
Type: "string", Type: "string",
}, },
config.HelpKV{
Key: config.Comment,
Description: config.DefaultComment,
Optional: true,
Type: "sentence",
},
} }
) )
+10
View File
@@ -23,6 +23,7 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"errors"
"io/ioutil" "io/ioutil"
"github.com/minio/minio/pkg/env" "github.com/minio/minio/pkg/env"
@@ -113,3 +114,12 @@ func LoadX509KeyPair(certFile, keyFile string) (tls.Certificate, error) {
} }
return cert, nil return cert, nil
} }
// EnsureCertAndKey checks if both client certificate and key paths are provided
func EnsureCertAndKey(ClientCert, ClientKey string) error {
if (ClientCert != "" && ClientKey == "") ||
(ClientCert == "" && ClientKey != "") {
return errors.New("cert and key must be specified as a pair")
}
return nil
}
+1 -1
View File
@@ -45,7 +45,7 @@ const (
// Include-list for compression. // Include-list for compression.
DefaultExtensions = ".txt,.log,.csv,.json,.tar,.xml,.bin" DefaultExtensions = ".txt,.log,.csv,.json,.tar,.xml,.bin"
DefaultMimeTypes = "text/*,application/json,application/xml" DefaultMimeTypes = "text/*,application/json,application/xml,binary/octet-stream"
) )
// DefaultKVS - default KV config for compression settings // DefaultKVS - default KV config for compression settings
+26 -6
View File
@@ -77,6 +77,7 @@ const (
LoggerWebhookSubSys = "logger_webhook" LoggerWebhookSubSys = "logger_webhook"
AuditWebhookSubSys = "audit_webhook" AuditWebhookSubSys = "audit_webhook"
HealSubSys = "heal" HealSubSys = "heal"
ScannerSubSys = "scanner"
CrawlerSubSys = "crawler" CrawlerSubSys = "crawler"
// Add new constants here if you add new fields to config. // Add new constants here if you add new fields to config.
@@ -114,7 +115,7 @@ var SubSystems = set.CreateStringSet(
PolicyOPASubSys, PolicyOPASubSys,
IdentityLDAPSubSys, IdentityLDAPSubSys,
IdentityOpenIDSubSys, IdentityOpenIDSubSys,
CrawlerSubSys, ScannerSubSys,
HealSubSys, HealSubSys,
NotifyAMQPSubSys, NotifyAMQPSubSys,
NotifyESSubSys, NotifyESSubSys,
@@ -132,7 +133,7 @@ var SubSystems = set.CreateStringSet(
var SubSystemsDynamic = set.CreateStringSet( var SubSystemsDynamic = set.CreateStringSet(
APISubSys, APISubSys,
CompressionSubSys, CompressionSubSys,
CrawlerSubSys, ScannerSubSys,
HealSubSys, HealSubSys,
) )
@@ -151,7 +152,7 @@ var SubSystemsSingleTargets = set.CreateStringSet([]string{
IdentityLDAPSubSys, IdentityLDAPSubSys,
IdentityOpenIDSubSys, IdentityOpenIDSubSys,
HealSubSys, HealSubSys,
CrawlerSubSys, ScannerSubSys,
}...) }...)
// Constant separators // Constant separators
@@ -462,6 +463,13 @@ func LookupWorm() (bool, error) {
return ParseBool(env.Get(EnvWorm, EnableOff)) return ParseBool(env.Get(EnvWorm, EnableOff))
} }
// Carries all the renamed sub-systems from their
// previously known names
var renamedSubsys = map[string]string{
CrawlerSubSys: ScannerSubSys,
// Add future sub-system renames
}
// Merge - merges a new config with all the // Merge - merges a new config with all the
// missing values for default configs, // missing values for default configs,
// returns a config. // returns a config.
@@ -477,9 +485,21 @@ func (c Config) Merge() Config {
} }
} }
if _, ok := cp[subSys]; !ok { if _, ok := cp[subSys]; !ok {
// A config subsystem was removed or server was downgraded. rnSubSys, ok := renamedSubsys[subSys]
Logger.Info("config: ignoring unknown subsystem config %q\n", subSys) if !ok {
continue // A config subsystem was removed or server was downgraded.
Logger.Info("config: ignoring unknown subsystem config %q\n", subSys)
continue
}
// Copy over settings from previous sub-system
// to newly renamed sub-system
for _, kv := range cp[rnSubSys][Default] {
_, ok := c[subSys][tgt].Lookup(kv.Key)
if !ok {
ckvs.Set(kv.Key, kv.Value)
}
}
subSys = rnSubSys
} }
cp[subSys][tgt] = ckvs cp[subSys][tgt] = ckvs
} }
+1 -1
View File
@@ -79,7 +79,7 @@ func (c *CoreDNS) Get(bucket string) ([]SrvRecord, error) {
var srvRecords []SrvRecord var srvRecords []SrvRecord
for _, domainName := range c.domainNames { for _, domainName := range c.domainNames {
key := msg.Path(fmt.Sprintf("%s.%s.", bucket, domainName), c.prefixPath) key := msg.Path(fmt.Sprintf("%s.%s.", bucket, domainName), c.prefixPath)
records, err := c.list(key+etcdPathSeparator, false) records, err := c.list(key, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+6
View File
@@ -281,4 +281,10 @@ Example 1:
"", "",
"Refer to https://docs.min.io/docs/minio-kms-quickstart-guide.html for setting up SSE", "Refer to https://docs.min.io/docs/minio-kms-quickstart-guide.html for setting up SSE",
) )
ErrInvalidReplicationWorkersValue = newErrFn(
"Invalid value for replication workers",
"",
"MINIO_API_REPLICATION_WORKERS: should be > 0",
)
) )
+1 -1
View File
@@ -66,7 +66,7 @@ var (
Help = config.HelpKVS{ Help = config.HelpKVS{
config.HelpKV{ config.HelpKV{
Key: Bitrot, Key: Bitrot,
Description: `perform bitrot scan on disks when checking objects during crawl`, Description: `perform bitrot scan on disks when checking objects during scanner`,
Optional: true, Optional: true,
Type: "on|off", Type: "on|off",
}, },
+62 -24
View File
@@ -21,13 +21,14 @@ import (
"crypto/x509" "crypto/x509"
"errors" "errors"
"fmt" "fmt"
"math/rand"
"net" "net"
"strings" "strings"
"time" "time"
ldap "github.com/go-ldap/ldap/v3"
"github.com/minio/minio/cmd/config" "github.com/minio/minio/cmd/config"
"github.com/minio/minio/pkg/env" "github.com/minio/minio/pkg/env"
ldap "gopkg.in/ldap.v3"
) )
const ( const (
@@ -58,7 +59,6 @@ type Config struct {
GroupSearchBaseDistName string `json:"groupSearchBaseDN"` GroupSearchBaseDistName string `json:"groupSearchBaseDN"`
GroupSearchBaseDistNames []string `json:"-"` GroupSearchBaseDistNames []string `json:"-"`
GroupSearchFilter string `json:"groupSearchFilter"` GroupSearchFilter string `json:"groupSearchFilter"`
GroupNameAttribute string `json:"groupNameAttribute"`
// Lookup bind LDAP service account // Lookup bind LDAP service account
LookupBindDN string `json:"lookupBindDN"` LookupBindDN string `json:"lookupBindDN"`
@@ -82,7 +82,6 @@ const (
UserDNSearchFilter = "user_dn_search_filter" UserDNSearchFilter = "user_dn_search_filter"
UsernameFormat = "username_format" UsernameFormat = "username_format"
GroupSearchFilter = "group_search_filter" GroupSearchFilter = "group_search_filter"
GroupNameAttribute = "group_name_attribute"
GroupSearchBaseDN = "group_search_base_dn" GroupSearchBaseDN = "group_search_base_dn"
TLSSkipVerify = "tls_skip_verify" TLSSkipVerify = "tls_skip_verify"
ServerInsecure = "server_insecure" ServerInsecure = "server_insecure"
@@ -97,7 +96,6 @@ const (
EnvUserDNSearchBaseDN = "MINIO_IDENTITY_LDAP_USER_DN_SEARCH_BASE_DN" EnvUserDNSearchBaseDN = "MINIO_IDENTITY_LDAP_USER_DN_SEARCH_BASE_DN"
EnvUserDNSearchFilter = "MINIO_IDENTITY_LDAP_USER_DN_SEARCH_FILTER" EnvUserDNSearchFilter = "MINIO_IDENTITY_LDAP_USER_DN_SEARCH_FILTER"
EnvGroupSearchFilter = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER" EnvGroupSearchFilter = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER"
EnvGroupNameAttribute = "MINIO_IDENTITY_LDAP_GROUP_NAME_ATTRIBUTE"
EnvGroupSearchBaseDN = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN" EnvGroupSearchBaseDN = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN"
EnvLookupBindDN = "MINIO_IDENTITY_LDAP_LOOKUP_BIND_DN" EnvLookupBindDN = "MINIO_IDENTITY_LDAP_LOOKUP_BIND_DN"
EnvLookupBindPassword = "MINIO_IDENTITY_LDAP_LOOKUP_BIND_PASSWORD" EnvLookupBindPassword = "MINIO_IDENTITY_LDAP_LOOKUP_BIND_PASSWORD"
@@ -106,6 +104,7 @@ const (
var removedKeys = []string{ var removedKeys = []string{
"username_search_filter", "username_search_filter",
"username_search_base_dn", "username_search_base_dn",
"group_name_attribute",
} }
// DefaultKVS - default config for LDAP config // DefaultKVS - default config for LDAP config
@@ -131,10 +130,6 @@ var (
Key: GroupSearchFilter, Key: GroupSearchFilter,
Value: "", Value: "",
}, },
config.KV{
Key: GroupNameAttribute,
Value: "",
},
config.KV{ config.KV{
Key: GroupSearchBaseDN, Key: GroupSearchBaseDN,
Value: "", Value: "",
@@ -180,13 +175,22 @@ func getGroups(conn *ldap.Conn, sreq *ldap.SearchRequest) ([]string, error) {
for _, entry := range sres.Entries { for _, entry := range sres.Entries {
// We only queried one attribute, // We only queried one attribute,
// so we only look up the first one. // so we only look up the first one.
groups = append(groups, entry.Attributes[0].Values...) groups = append(groups, entry.DN)
} }
return groups, nil return groups, nil
} }
func (l *Config) lookupBind(conn *ldap.Conn) error { func (l *Config) lookupBind(conn *ldap.Conn) error {
return conn.Bind(l.LookupBindDN, l.LookupBindPassword) var err error
if l.LookupBindPassword == "" {
err = conn.UnauthenticatedBind(l.LookupBindDN)
} else {
err = conn.Bind(l.LookupBindDN, l.LookupBindPassword)
}
if ldap.IsErrorWithCode(err, 49) {
return fmt.Errorf("LDAP Lookup Bind user invalid credentials error: %v", err)
}
return err
} }
// usernameFormatsBind - Iterates over all given username formats and expects // usernameFormatsBind - Iterates over all given username formats and expects
@@ -208,6 +212,8 @@ func (l *Config) usernameFormatsBind(conn *ldap.Conn, username, password string)
if errs[i] == nil { if errs[i] == nil {
bindDistNames = append(bindDistNames, bindDN) bindDistNames = append(bindDistNames, bindDN)
successCount++ successCount++
} else if !ldap.IsErrorWithCode(errs[i], 49) {
return "", fmt.Errorf("LDAP Bind request failed with unexpected error: %v", errs[i])
} }
} }
if successCount == 0 { if successCount == 0 {
@@ -217,7 +223,7 @@ func (l *Config) usernameFormatsBind(conn *ldap.Conn, username, password string)
errStrings = append(errStrings, err.Error()) errStrings = append(errStrings, err.Error())
} }
} }
outErr := fmt.Sprintf("All username formats failed with: %s", strings.Join(errStrings, "; ")) outErr := fmt.Sprintf("All username formats failed due to invalid credentials: %s", strings.Join(errStrings, "; "))
return "", errors.New(outErr) return "", errors.New(outErr)
} }
if successCount > 1 { if successCount > 1 {
@@ -307,12 +313,13 @@ func (l *Config) Bind(username, password string) (string, []string, error) {
var groups []string var groups []string
if l.GroupSearchFilter != "" { if l.GroupSearchFilter != "" {
for _, groupSearchBase := range l.GroupSearchBaseDistNames { for _, groupSearchBase := range l.GroupSearchBaseDistNames {
filter := strings.Replace(l.GroupSearchFilter, "%s", ldap.EscapeFilter(bindDN), -1) filter := strings.Replace(l.GroupSearchFilter, "%s", ldap.EscapeFilter(username), -1)
filter = strings.Replace(filter, "%d", ldap.EscapeFilter(bindDN), -1)
searchRequest := ldap.NewSearchRequest( searchRequest := ldap.NewSearchRequest(
groupSearchBase, groupSearchBase,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
filter, filter,
[]string{l.GroupNameAttribute}, nil,
nil, nil,
) )
@@ -368,6 +375,38 @@ func (l Config) GetExpiryDuration() time.Duration {
return l.stsExpiryDuration return l.stsExpiryDuration
} }
func (l Config) testConnection() error {
conn, err := l.Connect()
if err != nil {
return fmt.Errorf("Error creating connection to LDAP server: %v", err)
}
defer conn.Close()
if l.isUsingLookupBind {
if err = l.lookupBind(conn); err != nil {
return fmt.Errorf("Error connecting as LDAP Lookup Bind user: %v", err)
}
return nil
}
// Generate some random user credentials for username formats mode test.
username := fmt.Sprintf("sometestuser%09d", rand.Int31n(1000000000))
charset := []byte("abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
rand.Shuffle(len(charset), func(i, j int) {
charset[i], charset[j] = charset[j], charset[i]
})
password := string(charset[:20])
_, err = l.usernameFormatsBind(conn, username, password)
if err == nil {
// We don't expect to successfully guess a credential in this
// way.
return fmt.Errorf("Unexpected random credentials success for user=%s password=%s", username, password)
} else if strings.HasPrefix(err.Error(), "All username formats failed due to invalid credentials: ") {
return nil
}
return fmt.Errorf("LDAP connection test error: %v", err)
}
// Enabled returns if jwks is enabled. // Enabled returns if jwks is enabled.
func Enabled(kvs config.KVS) bool { func Enabled(kvs config.KVS) bool {
return kvs.Get(ServerAddr) != "" return kvs.Get(ServerAddr) != ""
@@ -427,14 +466,14 @@ func Lookup(kvs config.KVS, rootCAs *x509.CertPool) (l Config, err error) {
// Lookup bind user configuration // Lookup bind user configuration
lookupBindDN := env.Get(EnvLookupBindDN, kvs.Get(LookupBindDN)) lookupBindDN := env.Get(EnvLookupBindDN, kvs.Get(LookupBindDN))
lookupBindPassword := env.Get(EnvLookupBindPassword, kvs.Get(LookupBindPassword)) lookupBindPassword := env.Get(EnvLookupBindPassword, kvs.Get(LookupBindPassword))
if lookupBindDN != "" && lookupBindPassword != "" { if lookupBindDN != "" {
l.LookupBindDN = lookupBindDN l.LookupBindDN = lookupBindDN
l.LookupBindPassword = lookupBindPassword l.LookupBindPassword = lookupBindPassword
l.isUsingLookupBind = true l.isUsingLookupBind = true
// User DN search configuration // User DN search configuration
userDNSearchBaseDN := env.Get(EnvUserDNSearchBaseDN, kvs.Get(UserDNSearchBaseDN)) userDNSearchBaseDN := env.Get(EnvUserDNSearchBaseDN, kvs.Get(UserDNSearchBaseDN))
userDNSearchFilter := env.Get(EnvUserDNSearchFilter, kvs.Get(EnvUserDNSearchFilter)) userDNSearchFilter := env.Get(EnvUserDNSearchFilter, kvs.Get(UserDNSearchFilter))
if userDNSearchFilter == "" || userDNSearchBaseDN == "" { if userDNSearchFilter == "" || userDNSearchBaseDN == "" {
return l, errors.New("In lookup bind mode, userDN search base DN and userDN search filter are both required") return l, errors.New("In lookup bind mode, userDN search base DN and userDN search filter are both required")
} }
@@ -461,23 +500,22 @@ func Lookup(kvs config.KVS, rootCAs *x509.CertPool) (l Config, err error) {
return l, errors.New("Either Lookup Bind mode or Username Format mode is required.") return l, errors.New("Either Lookup Bind mode or Username Format mode is required.")
} }
// Test connection to LDAP server.
if err := l.testConnection(); err != nil {
return l, fmt.Errorf("Connection test for LDAP server failed: %v", err)
}
// Group search params configuration // Group search params configuration
grpSearchFilter := env.Get(EnvGroupSearchFilter, kvs.Get(GroupSearchFilter)) grpSearchFilter := env.Get(EnvGroupSearchFilter, kvs.Get(GroupSearchFilter))
grpSearchNameAttr := env.Get(EnvGroupNameAttribute, kvs.Get(GroupNameAttribute))
grpSearchBaseDN := env.Get(EnvGroupSearchBaseDN, kvs.Get(GroupSearchBaseDN)) grpSearchBaseDN := env.Get(EnvGroupSearchBaseDN, kvs.Get(GroupSearchBaseDN))
// Either all group params must be set or none must be set. // Either all group params must be set or none must be set.
var allSet bool if (grpSearchFilter != "" && grpSearchBaseDN == "") || (grpSearchFilter == "" && grpSearchBaseDN != "") {
if grpSearchFilter != "" { return l, errors.New("All group related parameters must be set")
if grpSearchNameAttr == "" || grpSearchBaseDN == "" {
return l, errors.New("All group related parameters must be set")
}
allSet = true
} }
if allSet { if grpSearchFilter != "" {
l.GroupSearchFilter = grpSearchFilter l.GroupSearchFilter = grpSearchFilter
l.GroupNameAttribute = grpSearchNameAttr
l.GroupSearchBaseDistName = grpSearchBaseDN l.GroupSearchBaseDistName = grpSearchBaseDN
l.GroupSearchBaseDistNames = strings.Split(l.GroupSearchBaseDistName, dnDelimiter) l.GroupSearchBaseDistNames = strings.Split(l.GroupSearchBaseDistName, dnDelimiter)
} }
-6
View File
@@ -68,12 +68,6 @@ var (
Optional: true, Optional: true,
Type: "string", Type: "string",
}, },
config.HelpKV{
Key: GroupNameAttribute,
Description: `search attribute for group name e.g. "cn"`,
Optional: true,
Type: "string",
},
config.HelpKV{ config.HelpKV{
Key: GroupSearchBaseDN, Key: GroupSearchBaseDN,
Description: `";" separated list of group search base DNs e.g. "dc=myldapserver,dc=com"`, Description: `";" separated list of group search base DNs e.g. "dc=myldapserver,dc=com"`,
-4
View File
@@ -41,10 +41,6 @@ func SetIdentityLDAP(s config.Config, ldapArgs Config) {
Key: GroupSearchFilter, Key: GroupSearchFilter,
Value: ldapArgs.GroupSearchFilter, Value: ldapArgs.GroupSearchFilter,
}, },
config.KV{
Key: GroupNameAttribute,
Value: ldapArgs.GroupNameAttribute,
},
config.KV{ config.KV{
Key: GroupSearchBaseDN, Key: GroupSearchBaseDN,
Value: ldapArgs.GroupSearchBaseDistName, Value: ldapArgs.GroupSearchBaseDistName,
+16 -16
View File
@@ -1,18 +1,18 @@
/* // MinIO Cloud Storage, (C) 2020 MinIO, Inc.
* MinIO Cloud Storage, (C) 2020 MinIO, Inc. //
* // Licensed under the Apache License, Version 2.0 (the "License");
* Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License.
* you may not use this file except in compliance with the License. // You may obtain a copy of the License at
* You may obtain a copy of the License at //
* // http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0 //
* // Unless required by applicable law or agreed to in writing, software
* Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS,
* distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and
* See the License for the specific language governing permissions and // limitations under the License.
* limitations under the License.
*/ // +build !fips
package openid package openid
@@ -22,7 +22,7 @@ import (
"github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go"
// Needed for SHA3 to work - See: https://golang.org/src/crypto/crypto.go?s=1034:1288 // Needed for SHA3 to work - See: https://golang.org/src/crypto/crypto.go?s=1034:1288
_ "golang.org/x/crypto/sha3" _ "golang.org/x/crypto/sha3" // There is no SHA-3 FIPS-140 2 compliant implementation
) )
// Specific instances for EC256 and company // Specific instances for EC256 and company
+16 -16
View File
@@ -1,18 +1,18 @@
/* // MinIO Cloud Storage, (C) 2020 MinIO, Inc.
* MinIO Cloud Storage, (C) 2020 MinIO, Inc. //
* // Licensed under the Apache License, Version 2.0 (the "License");
* Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License.
* you may not use this file except in compliance with the License. // You may obtain a copy of the License at
* You may obtain a copy of the License at //
* // http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0 //
* // Unless required by applicable law or agreed to in writing, software
* Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS,
* distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and
* See the License for the specific language governing permissions and // limitations under the License.
* limitations under the License.
*/ // +build !fips
package openid package openid
@@ -22,7 +22,7 @@ import (
"github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go"
// Needed for SHA3 to work - See: https://golang.org/src/crypto/crypto.go?s=1034:1288 // Needed for SHA3 to work - See: https://golang.org/src/crypto/crypto.go?s=1034:1288
_ "golang.org/x/crypto/sha3" _ "golang.org/x/crypto/sha3" // There is no SHA-3 FIPS-140 2 compliant implementation
) )
// Specific instances for RS256 and company // Specific instances for RS256 and company
@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package crawler package scanner
import ( import (
"strconv" "strconv"
@@ -29,8 +29,10 @@ const (
Delay = "delay" Delay = "delay"
MaxWait = "max_wait" MaxWait = "max_wait"
EnvDelay = "MINIO_CRAWLER_DELAY" EnvDelay = "MINIO_SCANNER_DELAY"
EnvMaxWait = "MINIO_CRAWLER_MAX_WAIT" EnvDelayLegacy = "MINIO_CRAWLER_DELAY"
EnvMaxWait = "MINIO_SCANNER_MAX_WAIT"
EnvMaxWaitLegacy = "MINIO_CRAWLER_MAX_WAIT"
) )
// Config represents the heal settings. // Config represents the heal settings.
@@ -58,7 +60,7 @@ var (
Help = config.HelpKVS{ Help = config.HelpKVS{
config.HelpKV{ config.HelpKV{
Key: Delay, Key: Delay,
Description: `crawler delay multiplier, defaults to '10.0'`, Description: `scanner delay multiplier, defaults to '10.0'`,
Optional: true, Optional: true,
Type: "float", Type: "float",
}, },
@@ -73,14 +75,22 @@ var (
// LookupConfig - lookup config and override with valid environment settings if any. // LookupConfig - lookup config and override with valid environment settings if any.
func LookupConfig(kvs config.KVS) (cfg Config, err error) { func LookupConfig(kvs config.KVS) (cfg Config, err error) {
if err = config.CheckValidKeys(config.CrawlerSubSys, kvs, DefaultKVS); err != nil { if err = config.CheckValidKeys(config.ScannerSubSys, kvs, DefaultKVS); err != nil {
return cfg, err return cfg, err
} }
cfg.Delay, err = strconv.ParseFloat(env.Get(EnvDelay, kvs.Get(Delay)), 64) delay := env.Get(EnvDelayLegacy, "")
if delay == "" {
delay = env.Get(EnvDelay, kvs.Get(Delay))
}
cfg.Delay, err = strconv.ParseFloat(delay, 64)
if err != nil { if err != nil {
return cfg, err return cfg, err
} }
cfg.MaxWait, err = time.ParseDuration(env.Get(EnvMaxWait, kvs.Get(MaxWait))) maxWait := env.Get(EnvMaxWaitLegacy, "")
if maxWait == "" {
maxWait = env.Get(EnvMaxWait, kvs.Get(MaxWait))
}
cfg.MaxWait, err = time.ParseDuration(maxWait)
if err != nil { if err != nil {
return cfg, err return cfg, err
} }
+4
View File
@@ -18,6 +18,7 @@ import (
"errors" "errors"
"math/rand" "math/rand"
"net/http" "net/http"
"os"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
@@ -410,6 +411,9 @@ func NewKMS(cfg KMSConfig) (kms KMS, err error) {
} else if cfg.Vault.Enabled && cfg.Kes.Enabled { } else if cfg.Vault.Enabled && cfg.Kes.Enabled {
return kms, errors.New("Ambiguous KMS configuration: vault configuration and kes configuration are provided at the same time") return kms, errors.New("Ambiguous KMS configuration: vault configuration and kes configuration are provided at the same time")
} else if cfg.Vault.Enabled { } else if cfg.Vault.Enabled {
if v, ok := os.LookupEnv("MINIO_KMS_VAULT_DEPRECATION"); !ok || v != "off" { // TODO(aead): Remove once Vault support has been removed
return kms, errors.New("Hashicorp Vault is deprecated and will be removed Oct. 2021. To temporarily enable Hashicorp Vault support, set MINIO_KMS_VAULT_DEPRECATION=off")
}
kms, err = NewVault(cfg.Vault) kms, err = NewVault(cfg.Vault)
if err != nil { if err != nil {
return kms, err return kms, err
+1 -1
View File
@@ -19,13 +19,13 @@ import (
"context" "context"
"crypto/hmac" "crypto/hmac"
"crypto/rand" "crypto/rand"
"crypto/sha256"
"encoding/binary" "encoding/binary"
"errors" "errors"
"io" "io"
"path" "path"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
sha256 "github.com/minio/sha256-simd"
"github.com/minio/sio" "github.com/minio/sio"
) )
+1 -1
View File
@@ -19,13 +19,13 @@ import (
"context" "context"
"crypto/hmac" "crypto/hmac"
"crypto/rand" "crypto/rand"
"crypto/sha256"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"sort" "sort"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
sha256 "github.com/minio/sha256-simd"
"github.com/minio/sio" "github.com/minio/sio"
) )
+32 -14
View File
@@ -44,6 +44,15 @@ const (
// MetaDataEncryptionKey is the sealed data encryption key (DEK) received from // MetaDataEncryptionKey is the sealed data encryption key (DEK) received from
// the KMS. // the KMS.
MetaDataEncryptionKey = "X-Minio-Internal-Server-Side-Encryption-S3-Kms-Sealed-Key" MetaDataEncryptionKey = "X-Minio-Internal-Server-Side-Encryption-S3-Kms-Sealed-Key"
// MetaContext is the KMS context provided by a client when encrypting an
// object with SSE-KMS. A client may not send a context in which case the
// MetaContext will not be present.
// MetaContext only contains the bucket/object name if the client explicitly
// added it. However, when decrypting an object the bucket/object name must
// be part of the object. Therefore, the bucket/object name must be added
// to the context, if not present, whenever a decryption is performed.
MetaContext = "X-Minio-Internal-Server-Side-Encryption-Context"
) )
// IsMultiPart returns true if the object metadata indicates // IsMultiPart returns true if the object metadata indicates
@@ -109,26 +118,35 @@ func IsSourceEncrypted(metadata map[string]string) bool {
// //
// IsEncrypted only checks whether the metadata contains at least // IsEncrypted only checks whether the metadata contains at least
// one entry indicating SSE-C or SSE-S3. // one entry indicating SSE-C or SSE-S3.
func IsEncrypted(metadata map[string]string) bool { func IsEncrypted(metadata map[string]string) (Type, bool) {
if _, ok := metadata[MetaIV]; ok { if S3KMS.IsEncrypted(metadata) {
return true return S3KMS, true
}
if _, ok := metadata[MetaAlgorithm]; ok {
return true
}
if IsMultiPart(metadata) {
return true
} }
if S3.IsEncrypted(metadata) { if S3.IsEncrypted(metadata) {
return true return S3, true
} }
if SSEC.IsEncrypted(metadata) { if SSEC.IsEncrypted(metadata) {
return true return SSEC, true
} }
if S3KMS.IsEncrypted(metadata) { if IsMultiPart(metadata) {
return true return nil, true
} }
return false if _, ok := metadata[MetaIV]; ok {
return nil, true
}
if _, ok := metadata[MetaAlgorithm]; ok {
return nil, true
}
if _, ok := metadata[MetaKeyID]; ok {
return nil, true
}
if _, ok := metadata[MetaDataEncryptionKey]; ok {
return nil, true
}
if _, ok := metadata[MetaContext]; ok {
return nil, true
}
return nil, false
} }
// CreateMultipartMetadata adds the multipart flag entry to metadata // CreateMultipartMetadata adds the multipart flag entry to metadata
+3 -3
View File
@@ -59,7 +59,7 @@ var isEncryptedTests = []struct {
func TestIsEncrypted(t *testing.T) { func TestIsEncrypted(t *testing.T) {
for i, test := range isEncryptedTests { for i, test := range isEncryptedTests {
if isEncrypted := IsEncrypted(test.Metadata); isEncrypted != test.Encrypted { if _, isEncrypted := IsEncrypted(test.Metadata); isEncrypted != test.Encrypted {
t.Errorf("Test %d: got '%v' - want '%v'", i, isEncrypted, test.Encrypted) t.Errorf("Test %d: got '%v' - want '%v'", i, isEncrypted, test.Encrypted)
} }
} }
@@ -74,8 +74,8 @@ var s3IsEncryptedTests = []struct {
{Encrypted: false, Metadata: map[string]string{MetaAlgorithm: ""}}, // 2 {Encrypted: false, Metadata: map[string]string{MetaAlgorithm: ""}}, // 2
{Encrypted: false, Metadata: map[string]string{MetaSealedKeySSEC: ""}}, // 3 {Encrypted: false, Metadata: map[string]string{MetaSealedKeySSEC: ""}}, // 3
{Encrypted: true, Metadata: map[string]string{MetaSealedKeyS3: ""}}, // 4 {Encrypted: true, Metadata: map[string]string{MetaSealedKeyS3: ""}}, // 4
{Encrypted: true, Metadata: map[string]string{MetaKeyID: ""}}, // 5 {Encrypted: false, Metadata: map[string]string{MetaKeyID: ""}}, // 5
{Encrypted: true, Metadata: map[string]string{MetaDataEncryptionKey: ""}}, // 6 {Encrypted: false, Metadata: map[string]string{MetaDataEncryptionKey: ""}}, // 6
{Encrypted: false, Metadata: map[string]string{"": ""}}, // 7 {Encrypted: false, Metadata: map[string]string{"": ""}}, // 7
{Encrypted: false, Metadata: map[string]string{"X-Minio-Internal-Server-Side-Encryption": ""}}, // 8 {Encrypted: false, Metadata: map[string]string{"X-Minio-Internal-Server-Side-Encryption": ""}}, // 8
} }
+27 -19
View File
@@ -82,12 +82,6 @@ func (ssekms) IsEncrypted(metadata map[string]string) bool {
if _, ok := metadata[MetaSealedKeyKMS]; ok { if _, ok := metadata[MetaSealedKeyKMS]; ok {
return true return true
} }
if _, ok := metadata[MetaKeyID]; ok {
return true
}
if _, ok := metadata[MetaDataEncryptionKey]; ok {
return true
}
return false return false
} }
@@ -95,11 +89,14 @@ func (ssekms) IsEncrypted(metadata map[string]string) bool {
// from the metadata using KMS and returns the decrypted object // from the metadata using KMS and returns the decrypted object
// key. // key.
func (s3 ssekms) UnsealObjectKey(kms KMS, metadata map[string]string, bucket, object string) (key ObjectKey, err error) { func (s3 ssekms) UnsealObjectKey(kms KMS, metadata map[string]string, bucket, object string) (key ObjectKey, err error) {
keyID, kmsKey, sealedKey, err := s3.ParseMetadata(metadata) keyID, kmsKey, sealedKey, ctx, err := s3.ParseMetadata(metadata)
if err != nil { if err != nil {
return key, err return key, err
} }
unsealKey, err := kms.UnsealKey(keyID, kmsKey, Context{bucket: path.Join(bucket, object)}) if _, ok := ctx[bucket]; !ok {
ctx[bucket] = path.Join(bucket, object)
}
unsealKey, err := kms.UnsealKey(keyID, kmsKey, ctx)
if err != nil { if err != nil {
return key, err return key, err
} }
@@ -147,19 +144,19 @@ func (ssekms) CreateMetadata(metadata map[string]string, keyID string, kmsKey []
// KMS data key it returns both. If the metadata does not contain neither a // KMS data key it returns both. If the metadata does not contain neither a
// KMS master key ID nor a sealed KMS data key it returns an empty keyID and // KMS master key ID nor a sealed KMS data key it returns an empty keyID and
// KMS data key. Otherwise, it returns an error. // KMS data key. Otherwise, it returns an error.
func (ssekms) ParseMetadata(metadata map[string]string) (keyID string, kmsKey []byte, sealedKey SealedKey, err error) { func (ssekms) ParseMetadata(metadata map[string]string) (keyID string, kmsKey []byte, sealedKey SealedKey, ctx Context, err error) {
// Extract all required values from object metadata // Extract all required values from object metadata
b64IV, ok := metadata[MetaIV] b64IV, ok := metadata[MetaIV]
if !ok { if !ok {
return keyID, kmsKey, sealedKey, errMissingInternalIV return keyID, kmsKey, sealedKey, ctx, errMissingInternalIV
} }
algorithm, ok := metadata[MetaAlgorithm] algorithm, ok := metadata[MetaAlgorithm]
if !ok { if !ok {
return keyID, kmsKey, sealedKey, errMissingInternalSealAlgorithm return keyID, kmsKey, sealedKey, ctx, errMissingInternalSealAlgorithm
} }
b64SealedKey, ok := metadata[MetaSealedKeyKMS] b64SealedKey, ok := metadata[MetaSealedKeyKMS]
if !ok { if !ok {
return keyID, kmsKey, sealedKey, Errorf("The object metadata is missing the internal sealed key for SSE-S3") return keyID, kmsKey, sealedKey, ctx, Errorf("The object metadata is missing the internal sealed key for SSE-S3")
} }
// There are two possibilites: // There are two possibilites:
@@ -169,33 +166,44 @@ func (ssekms) ParseMetadata(metadata map[string]string) (keyID string, kmsKey []
keyID, idPresent := metadata[MetaKeyID] keyID, idPresent := metadata[MetaKeyID]
b64KMSSealedKey, kmsKeyPresent := metadata[MetaDataEncryptionKey] b64KMSSealedKey, kmsKeyPresent := metadata[MetaDataEncryptionKey]
if !idPresent && kmsKeyPresent { if !idPresent && kmsKeyPresent {
return keyID, kmsKey, sealedKey, Errorf("The object metadata is missing the internal KMS key-ID for SSE-S3") return keyID, kmsKey, sealedKey, ctx, Errorf("The object metadata is missing the internal KMS key-ID for SSE-S3")
} }
if idPresent && !kmsKeyPresent { if idPresent && !kmsKeyPresent {
return keyID, kmsKey, sealedKey, Errorf("The object metadata is missing the internal sealed KMS data key for SSE-S3") return keyID, kmsKey, sealedKey, ctx, Errorf("The object metadata is missing the internal sealed KMS data key for SSE-S3")
} }
// Check whether all extracted values are well-formed // Check whether all extracted values are well-formed
iv, err := base64.StdEncoding.DecodeString(b64IV) iv, err := base64.StdEncoding.DecodeString(b64IV)
if err != nil || len(iv) != 32 { if err != nil || len(iv) != 32 {
return keyID, kmsKey, sealedKey, errInvalidInternalIV return keyID, kmsKey, sealedKey, ctx, errInvalidInternalIV
} }
if algorithm != SealAlgorithm { if algorithm != SealAlgorithm {
return keyID, kmsKey, sealedKey, errInvalidInternalSealAlgorithm return keyID, kmsKey, sealedKey, ctx, errInvalidInternalSealAlgorithm
} }
encryptedKey, err := base64.StdEncoding.DecodeString(b64SealedKey) encryptedKey, err := base64.StdEncoding.DecodeString(b64SealedKey)
if err != nil || len(encryptedKey) != 64 { if err != nil || len(encryptedKey) != 64 {
return keyID, kmsKey, sealedKey, Errorf("The internal sealed key for SSE-S3 is invalid") return keyID, kmsKey, sealedKey, ctx, Errorf("The internal sealed key for SSE-KMS is invalid")
} }
if idPresent && kmsKeyPresent { // We are using a KMS -> parse the sealed KMS data key. if idPresent && kmsKeyPresent { // We are using a KMS -> parse the sealed KMS data key.
kmsKey, err = base64.StdEncoding.DecodeString(b64KMSSealedKey) kmsKey, err = base64.StdEncoding.DecodeString(b64KMSSealedKey)
if err != nil { if err != nil {
return keyID, kmsKey, sealedKey, Errorf("The internal sealed KMS data key for SSE-S3 is invalid") return keyID, kmsKey, sealedKey, ctx, Errorf("The internal sealed KMS data key for SSE-KMS is invalid")
}
}
b64Ctx, ok := metadata[MetaContext]
if ok {
b, err := base64.StdEncoding.DecodeString(b64Ctx)
if err != nil {
return keyID, kmsKey, sealedKey, ctx, Errorf("The internal KMS context is not base64-encoded")
}
var json = jsoniter.ConfigCompatibleWithStandardLibrary
if err = json.Unmarshal(b, ctx); err != nil {
return keyID, kmsKey, sealedKey, ctx, Errorf("The internal sealed KMS context is invalid")
} }
} }
sealedKey.Algorithm = algorithm sealedKey.Algorithm = algorithm
copy(sealedKey.IV[:], iv) copy(sealedKey.IV[:], iv)
copy(sealedKey.Key[:], encryptedKey) copy(sealedKey.Key[:], encryptedKey)
return keyID, kmsKey, sealedKey, nil return keyID, kmsKey, sealedKey, ctx, nil
} }
-6
View File
@@ -62,12 +62,6 @@ func (sses3) IsEncrypted(metadata map[string]string) bool {
if _, ok := metadata[MetaSealedKeyS3]; ok { if _, ok := metadata[MetaSealedKeyS3]; ok {
return true return true
} }
if _, ok := metadata[MetaKeyID]; ok {
return true
}
if _, ok := metadata[MetaDataEncryptionKey]; ok {
return true
}
return false return false
} }
+221 -155
View File
@@ -23,6 +23,7 @@ import (
"errors" "errors"
"math" "math"
"math/rand" "math/rand"
"net/http"
"os" "os"
"path" "path"
"strings" "strings"
@@ -42,9 +43,9 @@ import (
) )
const ( const (
dataCrawlSleepPerFolder = time.Millisecond // Time to wait between folders. dataScannerSleepPerFolder = time.Millisecond // Time to wait between folders.
dataCrawlStartDelay = 1 * time.Minute // Time to wait on startup and between cycles. dataScannerStartDelay = 1 * time.Minute // Time to wait on startup and between cycles.
dataUsageUpdateDirCycles = 16 // Visit all folders every n cycles. dataUsageUpdateDirCycles = 16 // Visit all folders every n cycles.
healDeleteDangling = true healDeleteDangling = true
healFolderIncludeProb = 32 // Include a clean folder one in n cycles. healFolderIncludeProb = 32 // Include a clean folder one in n cycles.
@@ -55,27 +56,28 @@ var (
globalHealConfig heal.Config globalHealConfig heal.Config
globalHealConfigMu sync.Mutex globalHealConfigMu sync.Mutex
dataCrawlerLeaderLockTimeout = newDynamicTimeout(30*time.Second, 10*time.Second) dataScannerLeaderLockTimeout = newDynamicTimeout(30*time.Second, 10*time.Second)
// Sleeper values are updated when config is loaded. // Sleeper values are updated when config is loaded.
crawlerSleeper = newDynamicSleeper(10, 10*time.Second) scannerSleeper = newDynamicSleeper(10, 10*time.Second)
) )
// initDataCrawler will start the crawler in the background. // initDataScanner will start the scanner in the background.
func initDataCrawler(ctx context.Context, objAPI ObjectLayer) { func initDataScanner(ctx context.Context, objAPI ObjectLayer) {
go runDataCrawler(ctx, objAPI) go runDataScanner(ctx, objAPI)
} }
// runDataCrawler will start a data crawler. // runDataScanner will start a data scanner.
// The function will block until the context is canceled. // The function will block until the context is canceled.
// There should only ever be one crawler running per cluster. // There should only ever be one scanner running per cluster.
func runDataCrawler(ctx context.Context, objAPI ObjectLayer) { func runDataScanner(ctx context.Context, objAPI ObjectLayer) {
// Make sure only 1 crawler is running on the cluster. var err error
locker := objAPI.NewNSLock(minioMetaBucket, "runDataCrawler.lock") // Make sure only 1 scanner is running on the cluster.
locker := objAPI.NewNSLock(minioMetaBucket, "runDataScanner.lock")
r := rand.New(rand.NewSource(time.Now().UnixNano())) r := rand.New(rand.NewSource(time.Now().UnixNano()))
for { for {
err := locker.GetLock(ctx, dataCrawlerLeaderLockTimeout) ctx, err = locker.GetLock(ctx, dataScannerLeaderLockTimeout)
if err != nil { if err != nil {
time.Sleep(time.Duration(r.Float64() * float64(dataCrawlStartDelay))) time.Sleep(time.Duration(r.Float64() * float64(dataScannerStartDelay)))
continue continue
} }
break break
@@ -84,31 +86,34 @@ func runDataCrawler(ctx context.Context, objAPI ObjectLayer) {
// Load current bloom cycle // Load current bloom cycle
nextBloomCycle := intDataUpdateTracker.current() + 1 nextBloomCycle := intDataUpdateTracker.current() + 1
var buf bytes.Buffer
err := objAPI.GetObject(ctx, dataUsageBucket, dataUsageBloomName, 0, -1, &buf, "", ObjectOptions{}) br, err := objAPI.GetObjectNInfo(ctx, dataUsageBucket, dataUsageBloomName, nil, http.Header{}, readLock, ObjectOptions{})
if err != nil { if err != nil {
if !isErrObjectNotFound(err) && !isErrBucketNotFound(err) { if !isErrObjectNotFound(err) && !isErrBucketNotFound(err) {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
} }
} else { } else {
if buf.Len() == 8 { if br.ObjInfo.Size == 8 {
nextBloomCycle = binary.LittleEndian.Uint64(buf.Bytes()) if err = binary.Read(br, binary.LittleEndian, &nextBloomCycle); err != nil {
logger.LogIf(ctx, err)
}
} }
br.Close()
} }
crawlTimer := time.NewTimer(dataCrawlStartDelay) scannerTimer := time.NewTimer(dataScannerStartDelay)
defer crawlTimer.Stop() defer scannerTimer.Stop()
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case <-crawlTimer.C: case <-scannerTimer.C:
// Reset the timer for next cycle. // Reset the timer for next cycle.
crawlTimer.Reset(dataCrawlStartDelay) scannerTimer.Reset(dataScannerStartDelay)
if intDataUpdateTracker.debug { if intDataUpdateTracker.debug {
console.Debugln("starting crawler cycle") console.Debugln("starting scanner cycle")
} }
// Wait before starting next cycle and wait on startup. // Wait before starting next cycle and wait on startup.
@@ -116,7 +121,7 @@ func runDataCrawler(ctx context.Context, objAPI ObjectLayer) {
go storeDataUsageInBackend(ctx, objAPI, results) go storeDataUsageInBackend(ctx, objAPI, results)
bf, err := globalNotificationSys.updateBloomFilter(ctx, nextBloomCycle) bf, err := globalNotificationSys.updateBloomFilter(ctx, nextBloomCycle)
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
err = objAPI.CrawlAndGetDataUsage(ctx, bf, results) err = objAPI.NSScanner(ctx, bf, results)
close(results) close(results)
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
if err == nil { if err == nil {
@@ -124,13 +129,13 @@ func runDataCrawler(ctx context.Context, objAPI ObjectLayer) {
nextBloomCycle++ nextBloomCycle++
var tmp [8]byte var tmp [8]byte
binary.LittleEndian.PutUint64(tmp[:], nextBloomCycle) binary.LittleEndian.PutUint64(tmp[:], nextBloomCycle)
r, err := hash.NewReader(bytes.NewReader(tmp[:]), int64(len(tmp)), "", "", int64(len(tmp)), false) r, err := hash.NewReader(bytes.NewReader(tmp[:]), int64(len(tmp)), "", "", int64(len(tmp)))
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
continue continue
} }
_, err = objAPI.PutObject(ctx, dataUsageBucket, dataUsageBloomName, NewPutObjReader(r, nil, nil), ObjectOptions{}) _, err = objAPI.PutObject(ctx, dataUsageBucket, dataUsageBloomName, NewPutObjReader(r), ObjectOptions{})
if !isErrBucketNotFound(err) { if !isErrBucketNotFound(err) {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
} }
@@ -152,27 +157,27 @@ type folderScanner struct {
newCache dataUsageCache newCache dataUsageCache
withFilter *bloomFilter withFilter *bloomFilter
dataUsageCrawlDebug bool dataUsageScannerDebug bool
healFolderInclude uint32 // Include a clean folder one in n cycles. healFolderInclude uint32 // Include a clean folder one in n cycles.
healObjectSelect uint32 // Do a heal check on an object once every n cycles. Must divide into healFolderInclude healObjectSelect uint32 // Do a heal check on an object once every n cycles. Must divide into healFolderInclude
newFolders []cachedFolder newFolders []cachedFolder
existingFolders []cachedFolder existingFolders []cachedFolder
disks []StorageAPI disks []StorageAPI
} }
// crawlDataFolder will crawl the basepath+cache.Info.Name and return an updated cache. // scanDataFolder will scanner the basepath+cache.Info.Name and return an updated cache.
// The returned cache will always be valid, but may not be updated from the existing. // The returned cache will always be valid, but may not be updated from the existing.
// Before each operation sleepDuration is called which can be used to temporarily halt the crawler. // Before each operation sleepDuration is called which can be used to temporarily halt the scanner.
// If the supplied context is canceled the function will return at the first chance. // If the supplied context is canceled the function will return at the first chance.
func crawlDataFolder(ctx context.Context, basePath string, cache dataUsageCache, getSize getSizeFn) (dataUsageCache, error) { func scanDataFolder(ctx context.Context, basePath string, cache dataUsageCache, getSize getSizeFn) (dataUsageCache, error) {
t := UTCNow() t := UTCNow()
logPrefix := color.Green("data-usage: ") logPrefix := color.Green("data-usage: ")
logSuffix := color.Blue("- %v + %v", basePath, cache.Info.Name) logSuffix := color.Blue("- %v + %v", basePath, cache.Info.Name)
if intDataUpdateTracker.debug { if intDataUpdateTracker.debug {
defer func() { defer func() {
console.Debugf(logPrefix+" Crawl time: %v %s\n", time.Since(t), logSuffix) console.Debugf(logPrefix+" Scanner time: %v %s\n", time.Since(t), logSuffix)
}() }()
} }
@@ -185,15 +190,15 @@ func crawlDataFolder(ctx context.Context, basePath string, cache dataUsageCache,
skipHeal := cache.Info.SkipHealing skipHeal := cache.Info.SkipHealing
s := folderScanner{ s := folderScanner{
root: basePath, root: basePath,
getSize: getSize, getSize: getSize,
oldCache: cache, oldCache: cache,
newCache: dataUsageCache{Info: cache.Info}, newCache: dataUsageCache{Info: cache.Info},
newFolders: nil, newFolders: nil,
existingFolders: nil, existingFolders: nil,
dataUsageCrawlDebug: intDataUpdateTracker.debug, dataUsageScannerDebug: intDataUpdateTracker.debug,
healFolderInclude: 0, healFolderInclude: 0,
healObjectSelect: 0, healObjectSelect: 0,
} }
// Add disks for set healing. // Add disks for set healing.
@@ -223,21 +228,21 @@ func crawlDataFolder(ctx context.Context, basePath string, cache dataUsageCache,
s.withFilter = nil s.withFilter = nil
} }
} }
if s.dataUsageCrawlDebug { if s.dataUsageScannerDebug {
console.Debugf(logPrefix+"Start crawling. Bloom filter: %v %s\n", s.withFilter != nil, logSuffix) console.Debugf(logPrefix+"Start scanning. Bloom filter: %v %s\n", s.withFilter != nil, logSuffix)
} }
done := ctx.Done() done := ctx.Done()
var flattenLevels = 2 var flattenLevels = 2
if s.dataUsageCrawlDebug { if s.dataUsageScannerDebug {
console.Debugf(logPrefix+"Cycle: %v, Entries: %v %s\n", cache.Info.NextCycle, len(cache.Cache), logSuffix) console.Debugf(logPrefix+"Cycle: %v, Entries: %v %s\n", cache.Info.NextCycle, len(cache.Cache), logSuffix)
} }
// Always scan flattenLevels deep. Cache root is level 0. // Always scan flattenLevels deep. Cache root is level 0.
todo := []cachedFolder{{name: cache.Info.Name, objectHealProbDiv: 1}} todo := []cachedFolder{{name: cache.Info.Name, objectHealProbDiv: 1}}
for i := 0; i < flattenLevels; i++ { for i := 0; i < flattenLevels; i++ {
if s.dataUsageCrawlDebug { if s.dataUsageScannerDebug {
console.Debugf(logPrefix+"Level %v, scanning %v directories. %s\n", i, len(todo), logSuffix) console.Debugf(logPrefix+"Level %v, scanning %v directories. %s\n", i, len(todo), logSuffix)
} }
select { select {
@@ -253,7 +258,7 @@ func crawlDataFolder(ctx context.Context, basePath string, cache dataUsageCache,
} }
} }
if s.dataUsageCrawlDebug { if s.dataUsageScannerDebug {
console.Debugf(logPrefix+"New folders: %v %s\n", s.newFolders, logSuffix) console.Debugf(logPrefix+"New folders: %v %s\n", s.newFolders, logSuffix)
} }
@@ -282,7 +287,7 @@ func crawlDataFolder(ctx context.Context, basePath string, cache dataUsageCache,
} }
} }
if s.dataUsageCrawlDebug { if s.dataUsageScannerDebug {
console.Debugf(logPrefix+"Existing folders: %v %s\n", len(s.existingFolders), logSuffix) console.Debugf(logPrefix+"Existing folders: %v %s\n", len(s.existingFolders), logSuffix)
} }
@@ -309,13 +314,13 @@ func crawlDataFolder(ctx context.Context, basePath string, cache dataUsageCache,
// If folder isn't in filter, skip it completely. // If folder isn't in filter, skip it completely.
if !s.withFilter.containsDir(folder.name) { if !s.withFilter.containsDir(folder.name) {
if !h.mod(s.oldCache.Info.NextCycle, s.healFolderInclude/folder.objectHealProbDiv) { if !h.mod(s.oldCache.Info.NextCycle, s.healFolderInclude/folder.objectHealProbDiv) {
if s.dataUsageCrawlDebug { if s.dataUsageScannerDebug {
console.Debugf(logPrefix+"Skipping non-updated folder: %v %s\n", folder, logSuffix) console.Debugf(logPrefix+"Skipping non-updated folder: %v %s\n", folder, logSuffix)
} }
s.newCache.replaceHashed(h, folder.parent, s.oldCache.Cache[h.Key()]) s.newCache.replaceHashed(h, folder.parent, s.oldCache.Cache[h.Key()])
continue continue
} else { } else {
if s.dataUsageCrawlDebug { if s.dataUsageScannerDebug {
console.Debugf(logPrefix+"Adding non-updated folder to heal check: %v %s\n", folder.name, logSuffix) console.Debugf(logPrefix+"Adding non-updated folder to heal check: %v %s\n", folder.name, logSuffix)
} }
// Update probability of including objects // Update probability of including objects
@@ -337,8 +342,8 @@ func crawlDataFolder(ctx context.Context, basePath string, cache dataUsageCache,
} }
s.newCache.replaceHashed(h, folder.parent, *du) s.newCache.replaceHashed(h, folder.parent, *du)
} }
if s.dataUsageCrawlDebug { if s.dataUsageScannerDebug {
console.Debugf(logPrefix+"Finished crawl, %v entries %s\n", len(s.newCache.Cache), logSuffix) console.Debugf(logPrefix+"Finished scanner, %v entries %s\n", len(s.newCache.Cache), logSuffix)
} }
s.newCache.Info.LastUpdate = UTCNow() s.newCache.Info.LastUpdate = UTCNow()
s.newCache.Info.NextCycle++ s.newCache.Info.NextCycle++
@@ -367,7 +372,7 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
_, prefix := path2BucketObjectWithBasePath(f.root, folder.name) _, prefix := path2BucketObjectWithBasePath(f.root, folder.name)
var activeLifeCycle *lifecycle.Lifecycle var activeLifeCycle *lifecycle.Lifecycle
if f.oldCache.Info.lifeCycle != nil && f.oldCache.Info.lifeCycle.HasActiveRules(prefix, true) { if f.oldCache.Info.lifeCycle != nil && f.oldCache.Info.lifeCycle.HasActiveRules(prefix, true) {
if f.dataUsageCrawlDebug { if f.dataUsageScannerDebug {
console.Debugf(scannerLogPrefix+" Prefix %q has active rules\n", prefix) console.Debugf(scannerLogPrefix+" Prefix %q has active rules\n", prefix)
} }
activeLifeCycle = f.oldCache.Info.lifeCycle activeLifeCycle = f.oldCache.Info.lifeCycle
@@ -378,20 +383,20 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
if folder.name != dataUsageRoot && !filter.containsDir(folder.name) { if folder.name != dataUsageRoot && !filter.containsDir(folder.name) {
if !thisHash.mod(f.oldCache.Info.NextCycle, f.healFolderInclude/folder.objectHealProbDiv) { if !thisHash.mod(f.oldCache.Info.NextCycle, f.healFolderInclude/folder.objectHealProbDiv) {
f.newCache.copyWithChildren(&f.oldCache, thisHash, folder.parent) f.newCache.copyWithChildren(&f.oldCache, thisHash, folder.parent)
if f.dataUsageCrawlDebug { if f.dataUsageScannerDebug {
console.Debugf(scannerLogPrefix+" Skipping non-updated folder: %v\n", folder.name) console.Debugf(scannerLogPrefix+" Skipping non-updated folder: %v\n", folder.name)
} }
continue continue
} else { } else {
if f.dataUsageCrawlDebug { if f.dataUsageScannerDebug {
console.Debugf(scannerLogPrefix+" Adding non-updated folder to heal check: %v\n", folder.name) console.Debugf(scannerLogPrefix+" Adding non-updated folder to heal check: %v\n", folder.name)
} }
// If probability was already crawlerHealFolderInclude, keep it. // If probability was already scannerHealFolderInclude, keep it.
folder.objectHealProbDiv = f.healFolderInclude folder.objectHealProbDiv = f.healFolderInclude
} }
} }
} }
crawlerSleeper.Sleep(ctx, dataCrawlSleepPerFolder) scannerSleeper.Sleep(ctx, dataScannerSleepPerFolder)
cache := dataUsageEntry{} cache := dataUsageEntry{}
@@ -400,22 +405,22 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
entName = path.Clean(path.Join(folder.name, entName)) entName = path.Clean(path.Join(folder.name, entName))
bucket, prefix := path2BucketObjectWithBasePath(f.root, entName) bucket, prefix := path2BucketObjectWithBasePath(f.root, entName)
if bucket == "" { if bucket == "" {
if f.dataUsageCrawlDebug { if f.dataUsageScannerDebug {
console.Debugf(scannerLogPrefix+" no bucket (%s,%s)\n", f.root, entName) console.Debugf(scannerLogPrefix+" no bucket (%s,%s)\n", f.root, entName)
} }
return nil return errDoneForNow
} }
if isReservedOrInvalidBucket(bucket, false) { if isReservedOrInvalidBucket(bucket, false) {
if f.dataUsageCrawlDebug { if f.dataUsageScannerDebug {
console.Debugf(scannerLogPrefix+" invalid bucket: %v, entry: %v\n", bucket, entName) console.Debugf(scannerLogPrefix+" invalid bucket: %v, entry: %v\n", bucket, entName)
} }
return nil return errDoneForNow
} }
select { select {
case <-done: case <-done:
return ctx.Err() return errDoneForNow
default: default:
} }
@@ -443,16 +448,16 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
} }
// Dynamic time delay. // Dynamic time delay.
wait := crawlerSleeper.Timer(ctx) wait := scannerSleeper.Timer(ctx)
// Get file size, ignore errors. // Get file size, ignore errors.
item := crawlItem{ item := scannerItem{
Path: path.Join(f.root, entName), Path: path.Join(f.root, entName),
Typ: typ, Typ: typ,
bucket: bucket, bucket: bucket,
prefix: path.Dir(prefix), prefix: path.Dir(prefix),
objectName: path.Base(entName), objectName: path.Base(entName),
debug: f.dataUsageCrawlDebug, debug: f.dataUsageScannerDebug,
lifeCycle: activeLifeCycle, lifeCycle: activeLifeCycle,
heal: thisHash.mod(f.oldCache.Info.NextCycle, f.healObjectSelect/folder.objectHealProbDiv) && globalIsErasure, heal: thisHash.mod(f.oldCache.Info.NextCycle, f.healObjectSelect/folder.objectHealProbDiv) && globalIsErasure,
} }
@@ -528,17 +533,18 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
healObjectsPrefix := color.Green("healObjects:") healObjectsPrefix := color.Green("healObjects:")
for k := range existing { for k := range existing {
bucket, prefix := path2BucketObject(k) bucket, prefix := path2BucketObject(k)
if f.dataUsageCrawlDebug { if f.dataUsageScannerDebug {
console.Debugf(scannerLogPrefix+" checking disappeared folder: %v/%v\n", bucket, prefix) console.Debugf(scannerLogPrefix+" checking disappeared folder: %v/%v\n", bucket, prefix)
} }
// Dynamic time delay. // Dynamic time delay.
wait := crawlerSleeper.Timer(ctx) wait := scannerSleeper.Timer(ctx)
resolver.bucket = bucket resolver.bucket = bucket
foundObjs := false foundObjs := false
dangling := false dangling := false
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
err := listPathRaw(ctx, listPathRawOptions{ err := listPathRaw(ctx, listPathRawOptions{
disks: f.disks, disks: f.disks,
bucket: bucket, bucket: bucket,
@@ -548,13 +554,13 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
minDisks: len(f.disks), // We want full consistency. minDisks: len(f.disks), // We want full consistency.
// Weird, maybe transient error. // Weird, maybe transient error.
agreed: func(entry metaCacheEntry) { agreed: func(entry metaCacheEntry) {
if f.dataUsageCrawlDebug { if f.dataUsageScannerDebug {
console.Debugf(healObjectsPrefix+" got agreement: %v\n", entry.name) console.Debugf(healObjectsPrefix+" got agreement: %v\n", entry.name)
} }
}, },
// Some disks have data for this. // Some disks have data for this.
partial: func(entries metaCacheEntries, nAgreed int, errs []error) { partial: func(entries metaCacheEntries, nAgreed int, errs []error) {
if f.dataUsageCrawlDebug { if f.dataUsageScannerDebug {
console.Debugf(healObjectsPrefix+" got partial, %d agreed, errs: %v\n", nAgreed, errs) console.Debugf(healObjectsPrefix+" got partial, %d agreed, errs: %v\n", nAgreed, errs)
} }
@@ -563,7 +569,7 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
// Sleep and reset. // Sleep and reset.
wait() wait()
wait = crawlerSleeper.Timer(ctx) wait = scannerSleeper.Timer(ctx)
entry, ok := entries.resolve(&resolver) entry, ok := entries.resolve(&resolver)
if !ok { if !ok {
for _, err := range errs { for _, err := range errs {
@@ -576,7 +582,7 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
entry, _ = entries.firstFound() entry, _ = entries.firstFound()
} }
if f.dataUsageCrawlDebug { if f.dataUsageScannerDebug {
console.Debugf(healObjectsPrefix+" resolved to: %v, dir: %v\n", entry.name, entry.isDir()) console.Debugf(healObjectsPrefix+" resolved to: %v, dir: %v\n", entry.name, entry.isDir())
} }
@@ -600,7 +606,7 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
for _, ver := range fiv.Versions { for _, ver := range fiv.Versions {
// Sleep and reset. // Sleep and reset.
wait() wait()
wait = crawlerSleeper.Timer(ctx) wait = scannerSleeper.Timer(ctx)
err := bgSeq.queueHealTask(healSource{ err := bgSeq.queueHealTask(healSource{
bucket: bucket, bucket: bucket,
object: fiv.Name, object: fiv.Name,
@@ -614,31 +620,31 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
}, },
// Too many disks failed. // Too many disks failed.
finished: func(errs []error) { finished: func(errs []error) {
if f.dataUsageCrawlDebug { if f.dataUsageScannerDebug {
console.Debugf(healObjectsPrefix+" too many errors: %v\n", errs) console.Debugf(healObjectsPrefix+" too many errors: %v\n", errs)
} }
cancel() cancel()
}, },
}) })
if f.dataUsageCrawlDebug && err != nil && err != errFileNotFound { if f.dataUsageScannerDebug && err != nil && err != errFileNotFound {
console.Debugf(healObjectsPrefix+" checking returned value %v (%T)\n", err, err) console.Debugf(healObjectsPrefix+" checking returned value %v (%T)\n", err, err)
} }
// If we found one or more disks with this folder, delete it. // If we found one or more disks with this folder, delete it.
if err == nil && dangling { if err == nil && dangling {
if f.dataUsageCrawlDebug { if f.dataUsageScannerDebug {
console.Debugf(healObjectsPrefix+" deleting dangling directory %s\n", prefix) console.Debugf(healObjectsPrefix+" deleting dangling directory %s\n", prefix)
} }
objAPI.HealObjects(ctx, bucket, prefix, madmin.HealOpts{ objAPI.HealObjects(ctx, bucket, prefix, madmin.HealOpts{
Recursive: true, Recursive: true,
Remove: true, Remove: healDeleteDangling,
}, },
func(bucket, object, versionID string) error { func(bucket, object, versionID string) error {
// Wait for each heal as per crawler frequency. // Wait for each heal as per scanner frequency.
wait() wait()
wait = crawlerSleeper.Timer(ctx) wait = scannerSleeper.Timer(ctx)
return bgSeq.queueHealTask(healSource{ return bgSeq.queueHealTask(healSource{
bucket: bucket, bucket: bucket,
object: object, object: object,
@@ -678,7 +684,7 @@ func (f *folderScanner) deepScanFolder(ctx context.Context, folder cachedFolder,
addDir = func(entName string, typ os.FileMode) error { addDir = func(entName string, typ os.FileMode) error {
select { select {
case <-done: case <-done:
return ctx.Err() return errDoneForNow
default: default:
} }
@@ -686,12 +692,12 @@ func (f *folderScanner) deepScanFolder(ctx context.Context, folder cachedFolder,
dirStack = append(dirStack, entName) dirStack = append(dirStack, entName)
err := readDirFn(path.Join(dirStack...), addDir) err := readDirFn(path.Join(dirStack...), addDir)
dirStack = dirStack[:len(dirStack)-1] dirStack = dirStack[:len(dirStack)-1]
crawlerSleeper.Sleep(ctx, dataCrawlSleepPerFolder) scannerSleeper.Sleep(ctx, dataScannerSleepPerFolder)
return err return err
} }
// Dynamic time delay. // Dynamic time delay.
wait := crawlerSleeper.Timer(ctx) wait := scannerSleeper.Timer(ctx)
// Get file size, ignore errors. // Get file size, ignore errors.
dirStack = append(dirStack, entName) dirStack = append(dirStack, entName)
@@ -701,19 +707,19 @@ func (f *folderScanner) deepScanFolder(ctx context.Context, folder cachedFolder,
bucket, prefix := path2BucketObjectWithBasePath(f.root, fileName) bucket, prefix := path2BucketObjectWithBasePath(f.root, fileName)
var activeLifeCycle *lifecycle.Lifecycle var activeLifeCycle *lifecycle.Lifecycle
if f.oldCache.Info.lifeCycle != nil && f.oldCache.Info.lifeCycle.HasActiveRules(prefix, false) { if f.oldCache.Info.lifeCycle != nil && f.oldCache.Info.lifeCycle.HasActiveRules(prefix, false) {
if f.dataUsageCrawlDebug { if f.dataUsageScannerDebug {
console.Debugf(deepScannerLogPrefix+" Prefix %q has active rules\n", prefix) console.Debugf(deepScannerLogPrefix+" Prefix %q has active rules\n", prefix)
} }
activeLifeCycle = f.oldCache.Info.lifeCycle activeLifeCycle = f.oldCache.Info.lifeCycle
} }
item := crawlItem{ item := scannerItem{
Path: fileName, Path: fileName,
Typ: typ, Typ: typ,
bucket: bucket, bucket: bucket,
prefix: path.Dir(prefix), prefix: path.Dir(prefix),
objectName: path.Base(entName), objectName: path.Base(entName),
debug: f.dataUsageCrawlDebug, debug: f.dataUsageScannerDebug,
lifeCycle: activeLifeCycle, lifeCycle: activeLifeCycle,
heal: hashPath(path.Join(prefix, entName)).mod(f.oldCache.Info.NextCycle, f.healObjectSelect/folder.objectHealProbDiv) && globalIsErasure, heal: hashPath(path.Join(prefix, entName)).mod(f.oldCache.Info.NextCycle, f.healObjectSelect/folder.objectHealProbDiv) && globalIsErasure,
} }
@@ -748,8 +754,8 @@ func (f *folderScanner) deepScanFolder(ctx context.Context, folder cachedFolder,
return &cache, nil return &cache, nil
} }
// crawlItem represents each file while walking. // scannerItem represents each file while walking.
type crawlItem struct { type scannerItem struct {
Path string Path string
Typ os.FileMode Typ os.FileMode
@@ -769,10 +775,10 @@ type sizeSummary struct {
replicaSize int64 replicaSize int64
} }
type getSizeFn func(item crawlItem) (sizeSummary, error) type getSizeFn func(item scannerItem) (sizeSummary, error)
// transformMetaDir will transform a directory to prefix/file.ext // transformMetaDir will transform a directory to prefix/file.ext
func (i *crawlItem) transformMetaDir() { func (i *scannerItem) transformMetaDir() {
split := strings.Split(i.prefix, SlashSeparator) split := strings.Split(i.prefix, SlashSeparator)
if len(split) > 1 { if len(split) > 1 {
i.prefix = path.Join(split[:len(split)-1]...) i.prefix = path.Join(split[:len(split)-1]...)
@@ -785,22 +791,21 @@ func (i *crawlItem) transformMetaDir() {
// actionMeta contains information used to apply actions. // actionMeta contains information used to apply actions.
type actionMeta struct { type actionMeta struct {
oi ObjectInfo oi ObjectInfo
successorModTime time.Time // The modtime of the successor version bitRotScan bool // indicates if bitrot check was requested.
numVersions int // The number of versions of this object
bitRotScan bool // indicates if bitrot check was requested.
} }
var applyActionsLogPrefix = color.Green("applyActions:")
// applyActions will apply lifecycle checks on to a scanned item. // applyActions will apply lifecycle checks on to a scanned item.
// The resulting size on disk will always be returned. // The resulting size on disk will always be returned.
// The metadata will be compared to consensus on the object layer before any changes are applied. // The metadata will be compared to consensus on the object layer before any changes are applied.
// If no metadata is supplied, -1 is returned if no action is taken. // If no metadata is supplied, -1 is returned if no action is taken.
func (i *crawlItem) applyActions(ctx context.Context, o ObjectLayer, meta actionMeta) (size int64) { func (i *scannerItem) applyActions(ctx context.Context, o ObjectLayer, meta actionMeta) (size int64) {
size, err := meta.oi.GetActualSize() size, err := meta.oi.GetActualSize()
if i.debug { if i.debug {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
} }
applyActionsLogPrefix := color.Green("applyActions:")
if i.heal { if i.heal {
if i.debug { if i.debug {
if meta.oi.VersionID != "" { if meta.oi.VersionID != "" {
@@ -839,8 +844,8 @@ func (i *crawlItem) applyActions(ctx context.Context, o ObjectLayer, meta action
VersionID: meta.oi.VersionID, VersionID: meta.oi.VersionID,
DeleteMarker: meta.oi.DeleteMarker, DeleteMarker: meta.oi.DeleteMarker,
IsLatest: meta.oi.IsLatest, IsLatest: meta.oi.IsLatest,
NumVersions: meta.numVersions, NumVersions: meta.oi.NumVersions,
SuccessorModTime: meta.successorModTime, SuccessorModTime: meta.oi.SuccessorModTime,
RestoreOngoing: meta.oi.RestoreOngoing, RestoreOngoing: meta.oi.RestoreOngoing,
RestoreExpires: meta.oi.RestoreExpires, RestoreExpires: meta.oi.RestoreExpires,
TransitionStatus: meta.oi.TransitionStatus, TransitionStatus: meta.oi.TransitionStatus,
@@ -884,96 +889,135 @@ func (i *crawlItem) applyActions(ctx context.Context, o ObjectLayer, meta action
return size return size
} }
} }
size = obj.Size
// Recalculate action. var applied bool
action = evalActionFromLifecycle(ctx, *i.lifeCycle, obj, i.debug)
if action != lifecycle.NoneAction {
applied = applyLifecycleAction(ctx, action, o, obj)
}
if applied {
switch action {
case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
default: // for all lifecycle actions that remove data
return 0
}
}
return size
}
func evalActionFromLifecycle(ctx context.Context, lc lifecycle.Lifecycle, obj ObjectInfo, debug bool) (action lifecycle.Action) {
lcOpts := lifecycle.ObjectOpts{ lcOpts := lifecycle.ObjectOpts{
Name: i.objectPath(), Name: obj.Name,
UserTags: obj.UserTags, UserTags: obj.UserTags,
ModTime: obj.ModTime, ModTime: obj.ModTime,
VersionID: obj.VersionID, VersionID: obj.VersionID,
DeleteMarker: obj.DeleteMarker, DeleteMarker: obj.DeleteMarker,
IsLatest: obj.IsLatest, IsLatest: obj.IsLatest,
NumVersions: meta.numVersions, NumVersions: obj.NumVersions,
SuccessorModTime: meta.successorModTime, SuccessorModTime: obj.SuccessorModTime,
RestoreOngoing: obj.RestoreOngoing, RestoreOngoing: obj.RestoreOngoing,
RestoreExpires: obj.RestoreExpires, RestoreExpires: obj.RestoreExpires,
TransitionStatus: obj.TransitionStatus, TransitionStatus: obj.TransitionStatus,
} }
action = i.lifeCycle.ComputeAction(lcOpts)
if i.debug { action = lc.ComputeAction(lcOpts)
if debug {
console.Debugf(applyActionsLogPrefix+" lifecycle: Secondary scan: %v\n", action) console.Debugf(applyActionsLogPrefix+" lifecycle: Secondary scan: %v\n", action)
} }
switch action {
case lifecycle.DeleteAction, lifecycle.DeleteVersionAction: if action == lifecycle.NoneAction {
case lifecycle.TransitionAction, lifecycle.TransitionVersionAction: return action
case lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction:
default:
// No action.
return size
} }
opts := ObjectOptions{}
switch action { switch action {
case lifecycle.DeleteVersionAction, lifecycle.DeleteRestoredVersionAction: case lifecycle.DeleteVersionAction, lifecycle.DeleteRestoredVersionAction:
// Defensive code, should never happen // Defensive code, should never happen
if obj.VersionID == "" { if obj.VersionID == "" {
return size return lifecycle.NoneAction
} }
if rcfg, _ := globalBucketObjectLockSys.Get(i.bucket); rcfg.LockEnabled { if rcfg, _ := globalBucketObjectLockSys.Get(obj.Bucket); rcfg.LockEnabled {
locked := enforceRetentionForDeletion(ctx, obj) locked := enforceRetentionForDeletion(ctx, obj)
if locked { if locked {
if i.debug { if debug {
if obj.VersionID != "" { if obj.VersionID != "" {
console.Debugf(applyActionsLogPrefix+" lifecycle: %s v(%s) is locked, not deleting\n", i.objectPath(), obj.VersionID) console.Debugf(applyActionsLogPrefix+" lifecycle: %s v(%s) is locked, not deleting\n", obj.Name, obj.VersionID)
} else { } else {
console.Debugf(applyActionsLogPrefix+" lifecycle: %s is locked, not deleting\n", i.objectPath()) console.Debugf(applyActionsLogPrefix+" lifecycle: %s is locked, not deleting\n", obj.Name)
} }
} }
return size return lifecycle.NoneAction
} }
} }
}
return action
}
func applyTransitionAction(ctx context.Context, action lifecycle.Action, objLayer ObjectLayer, obj ObjectInfo) bool {
opts := ObjectOptions{}
if obj.TransitionStatus == "" {
opts.Versioned = globalBucketVersioningSys.Enabled(obj.Bucket)
opts.VersionID = obj.VersionID opts.VersionID = obj.VersionID
case lifecycle.DeleteAction, lifecycle.DeleteRestoredAction: opts.TransitionStatus = lifecycle.TransitionPending
opts.Versioned = globalBucketVersioningSys.Enabled(i.bucket) if _, err := objLayer.DeleteObject(ctx, obj.Bucket, obj.Name, opts); err != nil {
case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
if obj.TransitionStatus == "" {
opts.Versioned = globalBucketVersioningSys.Enabled(obj.Bucket)
opts.VersionID = obj.VersionID
opts.TransitionStatus = lifecycle.TransitionPending
if _, err = o.DeleteObject(ctx, obj.Bucket, obj.Name, opts); err != nil {
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
return 0
}
// Assume it is still there.
logger.LogIf(ctx, err)
return size
}
}
globalTransitionState.queueTransitionTask(obj)
return 0
}
if obj.TransitionStatus != "" {
if err := deleteTransitionedObject(ctx, o, i.bucket, i.objectPath(), lcOpts, action, false); err != nil {
if isErrObjectNotFound(err) || isErrVersionNotFound(err) { if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
return 0 return false
} }
// Assume it is still there.
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return size return false
} }
// Notification already sent at *deleteTransitionedObject*, return '0' here. }
return 0 globalTransitionState.queueTransitionTask(obj)
return true
}
func applyExpiryOnTransitionedObject(ctx context.Context, objLayer ObjectLayer, obj ObjectInfo, restoredObject bool) bool {
lcOpts := lifecycle.ObjectOpts{
Name: obj.Name,
UserTags: obj.UserTags,
ModTime: obj.ModTime,
VersionID: obj.VersionID,
DeleteMarker: obj.DeleteMarker,
IsLatest: obj.IsLatest,
NumVersions: obj.NumVersions,
SuccessorModTime: obj.SuccessorModTime,
RestoreOngoing: obj.RestoreOngoing,
RestoreExpires: obj.RestoreExpires,
TransitionStatus: obj.TransitionStatus,
} }
obj, err = o.DeleteObject(ctx, i.bucket, i.objectPath(), opts) if err := deleteTransitionedObject(ctx, objLayer, obj.Bucket, obj.Name, lcOpts, restoredObject, false); err != nil {
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
return false
}
logger.LogIf(ctx, err)
return false
}
// Notification already sent at *deleteTransitionedObject*, just return 'true' here.
return true
}
func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLayer, obj ObjectInfo, applyOnVersion bool) bool {
opts := ObjectOptions{}
if applyOnVersion {
opts.VersionID = obj.VersionID
}
if opts.VersionID == "" {
opts.Versioned = globalBucketVersioningSys.Enabled(obj.Bucket)
}
obj, err := objLayer.DeleteObject(ctx, obj.Bucket, obj.Name, opts)
if err != nil { if err != nil {
if isErrObjectNotFound(err) || isErrVersionNotFound(err) { if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
return 0 return false
} }
// Assume it is still there. // Assume it is still there.
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return size return false
} }
eventName := event.ObjectRemovedDelete eventName := event.ObjectRemovedDelete
@@ -984,20 +1028,42 @@ func (i *crawlItem) applyActions(ctx context.Context, o ObjectLayer, meta action
// Notify object deleted event. // Notify object deleted event.
sendEvent(eventArgs{ sendEvent(eventArgs{
EventName: eventName, EventName: eventName,
BucketName: i.bucket, BucketName: obj.Bucket,
Object: obj, Object: obj,
Host: "Internal: [ILM-EXPIRY]", Host: "Internal: [ILM-EXPIRY]",
}) })
return 0
return true
}
// Apply object, object version, restored object or restored object version action on the given object
func applyExpiryRule(ctx context.Context, objLayer ObjectLayer, obj ObjectInfo, restoredObject, applyOnVersion bool) bool {
if obj.TransitionStatus != "" {
return applyExpiryOnTransitionedObject(ctx, objLayer, obj, restoredObject)
}
return applyExpiryOnNonTransitionedObjects(ctx, objLayer, obj, applyOnVersion)
}
// Perform actions (removal or transitioning of objects), return true the action is successfully performed
func applyLifecycleAction(ctx context.Context, action lifecycle.Action, objLayer ObjectLayer, obj ObjectInfo) (success bool) {
switch action {
case lifecycle.DeleteVersionAction, lifecycle.DeleteAction:
success = applyExpiryRule(ctx, objLayer, obj, false, action == lifecycle.DeleteVersionAction)
case lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction:
success = applyExpiryRule(ctx, objLayer, obj, true, action == lifecycle.DeleteRestoredVersionAction)
case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
success = applyTransitionAction(ctx, action, objLayer, obj)
}
return
} }
// objectPath returns the prefix and object name. // objectPath returns the prefix and object name.
func (i *crawlItem) objectPath() string { func (i *scannerItem) objectPath() string {
return path.Join(i.prefix, i.objectName) return path.Join(i.prefix, i.objectName)
} }
// healReplication will heal a scanned item that has failed replication. // healReplication will heal a scanned item that has failed replication.
func (i *crawlItem) healReplication(ctx context.Context, o ObjectLayer, oi ObjectInfo, sizeS *sizeSummary) { func (i *scannerItem) healReplication(ctx context.Context, o ObjectLayer, oi ObjectInfo, sizeS *sizeSummary) {
if oi.DeleteMarker || !oi.VersionPurgeStatus.Empty() { if oi.DeleteMarker || !oi.VersionPurgeStatus.Empty() {
// heal delete marker replication failure or versioned delete replication failure // heal delete marker replication failure or versioned delete replication failure
if oi.ReplicationStatus == replication.Pending || if oi.ReplicationStatus == replication.Pending ||
@@ -1010,10 +1076,10 @@ func (i *crawlItem) healReplication(ctx context.Context, o ObjectLayer, oi Objec
switch oi.ReplicationStatus { switch oi.ReplicationStatus {
case replication.Pending: case replication.Pending:
sizeS.pendingSize += oi.Size sizeS.pendingSize += oi.Size
globalReplicationState.queueReplicaTask(oi) globalReplicationPool.queueReplicaTask(oi)
case replication.Failed: case replication.Failed:
sizeS.failedSize += oi.Size sizeS.failedSize += oi.Size
globalReplicationState.queueReplicaTask(oi) globalReplicationPool.queueReplicaTask(oi)
case replication.Completed, "COMPLETE": case replication.Completed, "COMPLETE":
sizeS.replicatedSize += oi.Size sizeS.replicatedSize += oi.Size
case replication.Replica: case replication.Replica:
@@ -1022,7 +1088,7 @@ func (i *crawlItem) healReplication(ctx context.Context, o ObjectLayer, oi Objec
} }
// healReplicationDeletes will heal a scanned deleted item that failed to replicate deletes. // healReplicationDeletes will heal a scanned deleted item that failed to replicate deletes.
func (i *crawlItem) healReplicationDeletes(ctx context.Context, o ObjectLayer, oi ObjectInfo) { func (i *scannerItem) healReplicationDeletes(ctx context.Context, o ObjectLayer, oi ObjectInfo) {
// handle soft delete and permanent delete failures here. // handle soft delete and permanent delete failures here.
if oi.DeleteMarker || !oi.VersionPurgeStatus.Empty() { if oi.DeleteMarker || !oi.VersionPurgeStatus.Empty() {
versionID := "" versionID := ""
@@ -1032,7 +1098,7 @@ func (i *crawlItem) healReplicationDeletes(ctx context.Context, o ObjectLayer, o
} else { } else {
versionID = oi.VersionID versionID = oi.VersionID
} }
globalReplicationState.queueReplicaDeleteTask(DeletedObjectVersionInfo{ globalReplicationPool.queueReplicaDeleteTask(DeletedObjectVersionInfo{
DeletedObject: DeletedObject{ DeletedObject: DeletedObject{
ObjectName: oi.Name, ObjectName: oi.Name,
DeleteMarkerVersionID: dmVersionID, DeleteMarkerVersionID: dmVersionID,
+1 -1
View File
@@ -80,7 +80,7 @@ func newDataUpdateTracker() *dataUpdateTracker {
Current: dataUpdateFilter{ Current: dataUpdateFilter{
idx: 1, idx: 1,
}, },
debug: env.Get(envDataUsageCrawlDebug, config.EnableOff) == config.EnableOn || serverDebugLog, debug: env.Get(envDataUsageScannerDebug, config.EnableOff) == config.EnableOn || serverDebugLog,
input: make(chan string, dataUpdateTrackerQueueSize), input: make(chan string, dataUpdateTrackerQueueSize),
save: make(chan struct{}, 1), save: make(chan struct{}, 1),
saveExited: make(chan struct{}), saveExited: make(chan struct{}),
+5 -5
View File
@@ -91,7 +91,7 @@ type dataUsageCacheInfo struct {
Name string Name string
LastUpdate time.Time LastUpdate time.Time
NextCycle uint32 NextCycle uint32
// indicates if the disk is being healed and crawler // indicates if the disk is being healed and scanner
// should skip healing the disk // should skip healing the disk
SkipHealing bool SkipHealing bool
BloomFilter []byte `msg:"BloomFilter,omitempty"` BloomFilter []byte `msg:"BloomFilter,omitempty"`
@@ -485,7 +485,7 @@ type objectIO interface {
// Only backend errors are returned as errors. // Only backend errors are returned as errors.
// If the object is not found or unable to deserialize d is cleared and nil error is returned. // If the object is not found or unable to deserialize d is cleared and nil error is returned.
func (d *dataUsageCache) load(ctx context.Context, store objectIO, name string) error { func (d *dataUsageCache) load(ctx context.Context, store objectIO, name string) error {
r, err := store.GetObjectNInfo(ctx, dataUsageBucket, name, nil, http.Header{}, readLock, ObjectOptions{}) r, err := store.GetObjectNInfo(ctx, dataUsageBucket, name, nil, http.Header{}, noLock, ObjectOptions{})
if err != nil { if err != nil {
switch err.(type) { switch err.(type) {
case ObjectNotFound: case ObjectNotFound:
@@ -513,7 +513,7 @@ func (d *dataUsageCache) save(ctx context.Context, store objectIO, name string)
}() }()
defer pr.Close() defer pr.Close()
r, err := hash.NewReader(pr, -1, "", "", -1, false) r, err := hash.NewReader(pr, -1, "", "", -1)
if err != nil { if err != nil {
return err return err
} }
@@ -521,8 +521,8 @@ func (d *dataUsageCache) save(ctx context.Context, store objectIO, name string)
_, err = store.PutObject(ctx, _, err = store.PutObject(ctx,
dataUsageBucket, dataUsageBucket,
name, name,
NewPutObjReader(r, nil, nil), NewPutObjReader(r),
ObjectOptions{}) ObjectOptions{NoLock: true})
if isErrBucketNotFound(err) { if isErrBucketNotFound(err) {
return nil return nil
} }
+7 -8
View File
@@ -20,6 +20,7 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"net/http"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
@@ -27,7 +28,7 @@ import (
) )
const ( const (
envDataUsageCrawlDebug = "MINIO_DISK_USAGE_CRAWL_DEBUG" envDataUsageScannerDebug = "MINIO_DISK_USAGE_SCANNER_DEBUG"
dataUsageRoot = SlashSeparator dataUsageRoot = SlashSeparator
dataUsageBucket = minioMetaBucket + SlashSeparator + bucketMetaPrefix dataUsageBucket = minioMetaBucket + SlashSeparator + bucketMetaPrefix
@@ -46,12 +47,12 @@ func storeDataUsageInBackend(ctx context.Context, objAPI ObjectLayer, dui <-chan
continue continue
} }
size := int64(len(dataUsageJSON)) size := int64(len(dataUsageJSON))
r, err := hash.NewReader(bytes.NewReader(dataUsageJSON), size, "", "", size, false) r, err := hash.NewReader(bytes.NewReader(dataUsageJSON), size, "", "", size)
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
continue continue
} }
_, err = objAPI.PutObject(ctx, dataUsageBucket, dataUsageObjName, NewPutObjReader(r, nil, nil), ObjectOptions{}) _, err = objAPI.PutObject(ctx, dataUsageBucket, dataUsageObjName, NewPutObjReader(r), ObjectOptions{})
if !isErrBucketNotFound(err) { if !isErrBucketNotFound(err) {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
} }
@@ -59,20 +60,18 @@ func storeDataUsageInBackend(ctx context.Context, objAPI ObjectLayer, dui <-chan
} }
func loadDataUsageFromBackend(ctx context.Context, objAPI ObjectLayer) (DataUsageInfo, error) { func loadDataUsageFromBackend(ctx context.Context, objAPI ObjectLayer) (DataUsageInfo, error) {
var dataUsageInfoJSON bytes.Buffer r, err := objAPI.GetObjectNInfo(ctx, dataUsageBucket, dataUsageObjName, nil, http.Header{}, readLock, ObjectOptions{})
err := objAPI.GetObject(ctx, dataUsageBucket, dataUsageObjName, 0, -1, &dataUsageInfoJSON, "", ObjectOptions{})
if err != nil { if err != nil {
if isErrObjectNotFound(err) || isErrBucketNotFound(err) { if isErrObjectNotFound(err) || isErrBucketNotFound(err) {
return DataUsageInfo{}, nil return DataUsageInfo{}, nil
} }
return DataUsageInfo{}, toObjectErr(err, dataUsageBucket, dataUsageObjName) return DataUsageInfo{}, toObjectErr(err, dataUsageBucket, dataUsageObjName)
} }
defer r.Close()
var dataUsageInfo DataUsageInfo var dataUsageInfo DataUsageInfo
var json = jsoniter.ConfigCompatibleWithStandardLibrary var json = jsoniter.ConfigCompatibleWithStandardLibrary
err = json.Unmarshal(dataUsageInfoJSON.Bytes(), &dataUsageInfo) if err = json.NewDecoder(r).Decode(&dataUsageInfo); err != nil {
if err != nil {
return DataUsageInfo{}, err return DataUsageInfo{}, err
} }
+10 -10
View File
@@ -51,7 +51,7 @@ func TestDataUsageUpdate(t *testing.T) {
} }
createUsageTestFiles(t, base, bucket, files) createUsageTestFiles(t, base, bucket, files)
getSize := func(item crawlItem) (sizeS sizeSummary, err error) { getSize := func(item scannerItem) (sizeS sizeSummary, err error) {
if item.Typ&os.ModeDir == 0 { if item.Typ&os.ModeDir == 0 {
var s os.FileInfo var s os.FileInfo
s, err = os.Stat(item.Path) s, err = os.Stat(item.Path)
@@ -64,7 +64,7 @@ func TestDataUsageUpdate(t *testing.T) {
return return
} }
got, err := crawlDataFolder(context.Background(), base, dataUsageCache{Info: dataUsageCacheInfo{Name: bucket}}, getSize) got, err := scanDataFolder(context.Background(), base, dataUsageCache{Info: dataUsageCacheInfo{Name: bucket}}, getSize)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -185,7 +185,7 @@ func TestDataUsageUpdate(t *testing.T) {
}, },
} }
createUsageTestFiles(t, base, bucket, files) createUsageTestFiles(t, base, bucket, files)
got, err = crawlDataFolder(context.Background(), base, got, getSize) got, err = scanDataFolder(context.Background(), base, got, getSize)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -270,7 +270,7 @@ func TestDataUsageUpdate(t *testing.T) {
} }
// Changed dir must be picked up in this many cycles. // Changed dir must be picked up in this many cycles.
for i := 0; i < dataUsageUpdateDirCycles; i++ { for i := 0; i < dataUsageUpdateDirCycles; i++ {
got, err = crawlDataFolder(context.Background(), base, got, getSize) got, err = scanDataFolder(context.Background(), base, got, getSize)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -347,7 +347,7 @@ func TestDataUsageUpdatePrefix(t *testing.T) {
} }
createUsageTestFiles(t, base, "", files) createUsageTestFiles(t, base, "", files)
getSize := func(item crawlItem) (sizeS sizeSummary, err error) { getSize := func(item scannerItem) (sizeS sizeSummary, err error) {
if item.Typ&os.ModeDir == 0 { if item.Typ&os.ModeDir == 0 {
var s os.FileInfo var s os.FileInfo
s, err = os.Stat(item.Path) s, err = os.Stat(item.Path)
@@ -359,7 +359,7 @@ func TestDataUsageUpdatePrefix(t *testing.T) {
} }
return return
} }
got, err := crawlDataFolder(context.Background(), base, dataUsageCache{Info: dataUsageCacheInfo{Name: "bucket"}}, getSize) got, err := scanDataFolder(context.Background(), base, dataUsageCache{Info: dataUsageCacheInfo{Name: "bucket"}}, getSize)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -469,7 +469,7 @@ func TestDataUsageUpdatePrefix(t *testing.T) {
}, },
} }
createUsageTestFiles(t, base, "", files) createUsageTestFiles(t, base, "", files)
got, err = crawlDataFolder(context.Background(), base, got, getSize) got, err = scanDataFolder(context.Background(), base, got, getSize)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -552,7 +552,7 @@ func TestDataUsageUpdatePrefix(t *testing.T) {
} }
// Changed dir must be picked up in this many cycles. // Changed dir must be picked up in this many cycles.
for i := 0; i < dataUsageUpdateDirCycles; i++ { for i := 0; i < dataUsageUpdateDirCycles; i++ {
got, err = crawlDataFolder(context.Background(), base, got, getSize) got, err = scanDataFolder(context.Background(), base, got, getSize)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -646,7 +646,7 @@ func TestDataUsageCacheSerialize(t *testing.T) {
} }
createUsageTestFiles(t, base, bucket, files) createUsageTestFiles(t, base, bucket, files)
getSize := func(item crawlItem) (sizeS sizeSummary, err error) { getSize := func(item scannerItem) (sizeS sizeSummary, err error) {
if item.Typ&os.ModeDir == 0 { if item.Typ&os.ModeDir == 0 {
var s os.FileInfo var s os.FileInfo
s, err = os.Stat(item.Path) s, err = os.Stat(item.Path)
@@ -658,7 +658,7 @@ func TestDataUsageCacheSerialize(t *testing.T) {
} }
return return
} }
want, err := crawlDataFolder(context.Background(), base, dataUsageCache{Info: dataUsageCacheInfo{Name: bucket}}, getSize) want, err := scanDataFolder(context.Background(), base, dataUsageCache{Info: dataUsageCacheInfo{Name: bucket}}, getSize)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+19 -17
View File
@@ -23,7 +23,6 @@ import (
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@@ -103,7 +102,6 @@ func (m *cacheMeta) ToObjectInfo(bucket, object string) (o ObjectInfo) {
} }
// We set file info only if its valid. // We set file info only if its valid.
o.ModTime = m.Stat.ModTime
o.Size = m.Stat.Size o.Size = m.Stat.Size
o.ETag = extractETag(m.Meta) o.ETag = extractETag(m.Meta)
o.ContentType = m.Meta["content-type"] o.ContentType = m.Meta["content-type"]
@@ -122,6 +120,12 @@ func (m *cacheMeta) ToObjectInfo(bucket, object string) (o ObjectInfo) {
o.Expires = t.UTC() o.Expires = t.UTC()
} }
} }
if mtime, ok := m.Meta["last-modified"]; ok {
if t, e = time.Parse(http.TimeFormat, mtime); e == nil {
o.ModTime = t.UTC()
}
}
// etag/md5Sum has already been extracted. We need to // etag/md5Sum has already been extracted. We need to
// remove to avoid it from appearing as part of user-defined metadata // remove to avoid it from appearing as part of user-defined metadata
o.UserDefined = cleanMetadata(m.Meta) o.UserDefined = cleanMetadata(m.Meta)
@@ -264,10 +268,6 @@ func (c *diskCache) toClear() uint64 {
return bytesToClear(int64(di.Total), int64(di.Free), uint64(c.quotaPct), uint64(c.lowWatermark), uint64(c.highWatermark)) return bytesToClear(int64(di.Total), int64(di.Free), uint64(c.quotaPct), uint64(c.lowWatermark), uint64(c.highWatermark))
} }
var (
errDoneForNow = errors.New("done for now")
)
func (c *diskCache) purgeWait(ctx context.Context) { func (c *diskCache) purgeWait(ctx context.Context) {
for { for {
select { select {
@@ -377,7 +377,7 @@ func (c *diskCache) purge(ctx context.Context) {
return nil return nil
} }
if err := readDirFilterFn(c.dir, filterFn); err != nil { if err := readDirFn(c.dir, filterFn); err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return return
} }
@@ -437,7 +437,7 @@ func (c *diskCache) Stat(ctx context.Context, bucket, object string) (oi ObjectI
func (c *diskCache) statCachedMeta(ctx context.Context, cacheObjPath string) (meta *cacheMeta, partial bool, numHits int, err error) { func (c *diskCache) statCachedMeta(ctx context.Context, cacheObjPath string) (meta *cacheMeta, partial bool, numHits int, err error) {
cLock := c.NewNSLockFn(cacheObjPath) cLock := c.NewNSLockFn(cacheObjPath)
if err = cLock.GetRLock(ctx, globalOperationTimeout); err != nil { if ctx, err = cLock.GetRLock(ctx, globalOperationTimeout); err != nil {
return return
} }
@@ -506,9 +506,7 @@ func (c *diskCache) statCache(ctx context.Context, cacheObjPath string) (meta *c
} }
// get metadata of part.1 if full file has been cached. // get metadata of part.1 if full file has been cached.
partial = true partial = true
fi, err := os.Stat(pathJoin(cacheObjPath, cacheDataFile)) if _, err := os.Stat(pathJoin(cacheObjPath, cacheDataFile)); err == nil {
if err == nil {
meta.Stat.ModTime = atime.Get(fi)
partial = false partial = false
} }
return meta, partial, meta.Hits, nil return meta, partial, meta.Hits, nil
@@ -517,9 +515,11 @@ func (c *diskCache) statCache(ctx context.Context, cacheObjPath string) (meta *c
// saves object metadata to disk cache // saves object metadata to disk cache
// incHitsOnly is true if metadata update is incrementing only the hit counter // incHitsOnly is true if metadata update is incrementing only the hit counter
func (c *diskCache) SaveMetadata(ctx context.Context, bucket, object string, meta map[string]string, actualSize int64, rs *HTTPRangeSpec, rsFileName string, incHitsOnly bool) error { func (c *diskCache) SaveMetadata(ctx context.Context, bucket, object string, meta map[string]string, actualSize int64, rs *HTTPRangeSpec, rsFileName string, incHitsOnly bool) error {
var err error
cachedPath := getCacheSHADir(c.dir, bucket, object) cachedPath := getCacheSHADir(c.dir, bucket, object)
cLock := c.NewNSLockFn(cachedPath) cLock := c.NewNSLockFn(cachedPath)
if err := cLock.GetLock(ctx, globalOperationTimeout); err != nil { ctx, err = cLock.GetLock(ctx, globalOperationTimeout)
if err != nil {
return err return err
} }
defer cLock.Unlock() defer cLock.Unlock()
@@ -570,7 +570,6 @@ func (c *diskCache) saveMetadata(ctx context.Context, bucket, object string, met
} }
} }
m.Stat.Size = actualSize m.Stat.Size = actualSize
m.Stat.ModTime = UTCNow()
if !incHitsOnly { if !incHitsOnly {
// reset meta // reset meta
m.Meta = meta m.Meta = meta
@@ -697,7 +696,8 @@ func (c *diskCache) Put(ctx context.Context, bucket, object string, data io.Read
} }
cachePath := getCacheSHADir(c.dir, bucket, object) cachePath := getCacheSHADir(c.dir, bucket, object)
cLock := c.NewNSLockFn(cachePath) cLock := c.NewNSLockFn(cachePath)
if err := cLock.GetLock(ctx, globalOperationTimeout); err != nil { ctx, err = cLock.GetLock(ctx, globalOperationTimeout)
if err != nil {
return oi, err return oi, err
} }
defer cLock.Unlock() defer cLock.Unlock()
@@ -911,7 +911,8 @@ func (c *diskCache) bitrotReadFromCache(ctx context.Context, filePath string, of
func (c *diskCache) Get(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions) (gr *GetObjectReader, numHits int, err error) { func (c *diskCache) Get(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions) (gr *GetObjectReader, numHits int, err error) {
cacheObjPath := getCacheSHADir(c.dir, bucket, object) cacheObjPath := getCacheSHADir(c.dir, bucket, object)
cLock := c.NewNSLockFn(cacheObjPath) cLock := c.NewNSLockFn(cacheObjPath)
if err := cLock.GetRLock(ctx, globalOperationTimeout); err != nil { ctx, err = cLock.GetRLock(ctx, globalOperationTimeout)
if err != nil {
return nil, numHits, err return nil, numHits, err
} }
@@ -975,7 +976,8 @@ func (c *diskCache) Get(ctx context.Context, bucket, object string, rs *HTTPRang
// Deletes the cached object // Deletes the cached object
func (c *diskCache) delete(ctx context.Context, cacheObjPath string) (err error) { func (c *diskCache) delete(ctx context.Context, cacheObjPath string) (err error) {
cLock := c.NewNSLockFn(cacheObjPath) cLock := c.NewNSLockFn(cacheObjPath)
if err := cLock.GetLock(ctx, globalOperationTimeout); err != nil { _, err = cLock.GetLock(ctx, globalOperationTimeout)
if err != nil {
return err return err
} }
defer cLock.Unlock() defer cLock.Unlock()
@@ -1023,7 +1025,7 @@ func (c *diskCache) scanCacheWritebackFailures(ctx context.Context) {
return nil return nil
} }
if err := readDirFilterFn(c.dir, filterFn); err != nil { if err := readDirFn(c.dir, filterFn); err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return return
} }
+5 -1
View File
@@ -173,7 +173,11 @@ func backendDownError(err error) bool {
// IsCacheable returns if the object should be saved in the cache. // IsCacheable returns if the object should be saved in the cache.
func (o ObjectInfo) IsCacheable() bool { func (o ObjectInfo) IsCacheable() bool {
return !crypto.IsEncrypted(o.UserDefined) || globalCacheKMS != nil if globalCacheKMS != nil {
return true
}
_, ok := crypto.IsEncrypted(o.UserDefined)
return !ok
} }
// reads file cached on disk from offset upto length // reads file cached on disk from offset upto length
+3 -2
View File
@@ -200,6 +200,7 @@ func getMetadata(objInfo ObjectInfo) map[string]string {
if !objInfo.Expires.Equal(timeSentinel) { if !objInfo.Expires.Equal(timeSentinel) {
metadata["expires"] = objInfo.Expires.Format(http.TimeFormat) metadata["expires"] = objInfo.Expires.Format(http.TimeFormat)
} }
metadata["last-modified"] = objInfo.ModTime.Format(http.TimeFormat)
for k, v := range objInfo.UserDefined { for k, v := range objInfo.UserDefined {
metadata[k] = v metadata[k] = v
} }
@@ -704,14 +705,14 @@ func (c *cacheObjects) uploadObject(ctx context.Context, oi ObjectInfo) {
if st == CommitComplete || st.String() == "" { if st == CommitComplete || st.String() == "" {
return return
} }
hashReader, err := hash.NewReader(cReader, oi.Size, "", "", oi.Size, globalCLIContext.StrictS3Compat) hashReader, err := hash.NewReader(cReader, oi.Size, "", "", oi.Size)
if err != nil { if err != nil {
return return
} }
var opts ObjectOptions var opts ObjectOptions
opts.UserDefined = make(map[string]string) opts.UserDefined = make(map[string]string)
opts.UserDefined[xhttp.ContentMD5] = oi.UserDefined["content-md5"] opts.UserDefined[xhttp.ContentMD5] = oi.UserDefined["content-md5"]
objInfo, err := c.InnerPutObjectFn(ctx, oi.Bucket, oi.Name, NewPutObjReader(hashReader, nil, nil), opts) objInfo, err := c.InnerPutObjectFn(ctx, oi.Bucket, oi.Name, NewPutObjReader(hashReader), opts)
wbCommitStatus := CommitComplete wbCommitStatus := CommitComplete
if err != nil { if err != nil {
wbCommitStatus = CommitFailed wbCommitStatus = CommitFailed
+2 -1
View File
@@ -18,6 +18,7 @@ package cmd
import ( import (
"testing" "testing"
"time"
) )
// Tests ToObjectInfo function. // Tests ToObjectInfo function.
@@ -27,7 +28,7 @@ func TestCacheMetadataObjInfo(t *testing.T) {
if objInfo.Size != 0 { if objInfo.Size != 0 {
t.Fatal("Unexpected object info value for Size", objInfo.Size) t.Fatal("Unexpected object info value for Size", objInfo.Size)
} }
if !objInfo.ModTime.Equal(timeSentinel) { if !objInfo.ModTime.Equal(time.Time{}) {
t.Fatal("Unexpected object info value for ModTime ", objInfo.ModTime) t.Fatal("Unexpected object info value for ModTime ", objInfo.ModTime)
} }
if objInfo.IsDir { if objInfo.IsDir {
+23 -34
View File
@@ -20,6 +20,7 @@ import (
"bufio" "bufio"
"crypto/hmac" "crypto/hmac"
"crypto/rand" "crypto/rand"
"crypto/sha256"
"crypto/subtle" "crypto/subtle"
"encoding/binary" "encoding/binary"
"encoding/hex" "encoding/hex"
@@ -33,7 +34,6 @@ import (
"github.com/minio/minio/cmd/crypto" "github.com/minio/minio/cmd/crypto"
xhttp "github.com/minio/minio/cmd/http" xhttp "github.com/minio/minio/cmd/http"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
sha256 "github.com/minio/sha256-simd"
"github.com/minio/sio" "github.com/minio/sio"
) )
@@ -247,56 +247,45 @@ func EncryptRequest(content io.Reader, r *http.Request, bucket, object string, m
} }
func decryptObjectInfo(key []byte, bucket, object string, metadata map[string]string) ([]byte, error) { func decryptObjectInfo(key []byte, bucket, object string, metadata map[string]string) ([]byte, error) {
switch { switch kind, _ := crypto.IsEncrypted(metadata); kind {
default: case crypto.S3:
return nil, errObjectTampered var KMS crypto.KMS = GlobalKMS
case crypto.S3.IsEncrypted(metadata) && isCacheEncrypted(metadata): if isCacheEncrypted(metadata) {
if globalCacheKMS == nil { KMS = globalCacheKMS
}
if KMS == nil {
return nil, errKMSNotConfigured return nil, errKMSNotConfigured
} }
keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(metadata) objectKey, err := crypto.S3.UnsealObjectKey(KMS, metadata, bucket, object)
if err != nil { if err != nil {
return nil, err return nil, err
} }
extKey, err := globalCacheKMS.UnsealKey(keyID, kmsKey, crypto.Context{bucket: path.Join(bucket, object)})
if err != nil {
return nil, err
}
var objectKey crypto.ObjectKey
if err = objectKey.Unseal(extKey, sealedKey, crypto.S3.String(), bucket, object); err != nil {
return nil, err
}
return objectKey[:], nil return objectKey[:], nil
case crypto.S3.IsEncrypted(metadata): case crypto.S3KMS:
if GlobalKMS == nil { if GlobalKMS == nil {
return nil, errKMSNotConfigured return nil, errKMSNotConfigured
} }
keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(metadata) objectKey, err := crypto.S3KMS.UnsealObjectKey(GlobalKMS, metadata, bucket, object)
if err != nil { if err != nil {
return nil, err return nil, err
} }
extKey, err := GlobalKMS.UnsealKey(keyID, kmsKey, crypto.Context{bucket: path.Join(bucket, object)})
if err != nil {
return nil, err
}
var objectKey crypto.ObjectKey
if err = objectKey.Unseal(extKey, sealedKey, crypto.S3.String(), bucket, object); err != nil {
return nil, err
}
return objectKey[:], nil return objectKey[:], nil
case crypto.SSEC.IsEncrypted(metadata): case crypto.SSEC:
var extKey [32]byte
copy(extKey[:], key)
sealedKey, err := crypto.SSEC.ParseMetadata(metadata) sealedKey, err := crypto.SSEC.ParseMetadata(metadata)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var objectKey crypto.ObjectKey var (
objectKey crypto.ObjectKey
extKey [32]byte
)
copy(extKey[:], key)
if err = objectKey.Unseal(extKey, sealedKey, crypto.SSEC.String(), bucket, object); err != nil { if err = objectKey.Unseal(extKey, sealedKey, crypto.SSEC.String(), bucket, object); err != nil {
return nil, err return nil, err
} }
return objectKey[:], nil return objectKey[:], nil
default:
return nil, errObjectTampered
} }
} }
@@ -516,7 +505,7 @@ func (d *DecryptBlocksReader) Read(p []byte) (int, error) {
// It returns an error if the object is not encrypted or marked as encrypted // It returns an error if the object is not encrypted or marked as encrypted
// but has an invalid size. // but has an invalid size.
func (o *ObjectInfo) DecryptedSize() (int64, error) { func (o *ObjectInfo) DecryptedSize() (int64, error) {
if !crypto.IsEncrypted(o.UserDefined) { if _, ok := crypto.IsEncrypted(o.UserDefined); !ok {
return 0, errors.New("Cannot compute decrypted size of an unencrypted object") return 0, errors.New("Cannot compute decrypted size of an unencrypted object")
} }
if !isEncryptedMultipart(*o) { if !isEncryptedMultipart(*o) {
@@ -649,7 +638,7 @@ func tryDecryptETag(key []byte, encryptedETag string, ssec bool) string {
// requested range starts, along with the DARE sequence number within // requested range starts, along with the DARE sequence number within
// that part. For single part objects, the partStart will be 0. // that part. For single part objects, the partStart will be 0.
func (o *ObjectInfo) GetDecryptedRange(rs *HTTPRangeSpec) (encOff, encLength, skipLen int64, seqNumber uint32, partStart int, err error) { func (o *ObjectInfo) GetDecryptedRange(rs *HTTPRangeSpec) (encOff, encLength, skipLen int64, seqNumber uint32, partStart int, err error) {
if !crypto.IsEncrypted(o.UserDefined) { if _, ok := crypto.IsEncrypted(o.UserDefined); !ok {
err = errors.New("Object is not encrypted") err = errors.New("Object is not encrypted")
return return
} }
@@ -796,7 +785,7 @@ func DecryptObjectInfo(info *ObjectInfo, r *http.Request) (encrypted bool, err e
} }
} }
encrypted = crypto.IsEncrypted(info.UserDefined) _, encrypted = crypto.IsEncrypted(info.UserDefined)
if !encrypted && crypto.SSEC.IsRequested(headers) && r.Header.Get(xhttp.AmzCopySource) == "" { if !encrypted && crypto.SSEC.IsRequested(headers) && r.Header.Get(xhttp.AmzCopySource) == "" {
return false, errInvalidEncryptionParameters return false, errInvalidEncryptionParameters
} }
@@ -818,7 +807,7 @@ func DecryptObjectInfo(info *ObjectInfo, r *http.Request) (encrypted bool, err e
return encrypted, err return encrypted, err
} }
if crypto.IsEncrypted(info.UserDefined) && !crypto.IsMultiPart(info.UserDefined) { if _, ok := crypto.IsEncrypted(info.UserDefined); ok && !crypto.IsMultiPart(info.UserDefined) {
info.ETag = getDecryptedETag(headers, *info, false) info.ETag = getDecryptedETag(headers, *info, false)
} }
} }
+1 -1
View File
@@ -129,7 +129,7 @@ func TestDecryptObjectInfo(t *testing.T) {
for i, test := range decryptObjectInfoTests { for i, test := range decryptObjectInfoTests {
if encrypted, err := DecryptObjectInfo(&test.info, test.request); err != test.expErr { if encrypted, err := DecryptObjectInfo(&test.info, test.request); err != test.expErr {
t.Errorf("Test %d: Decryption returned wrong error code: got %d , want %d", i, err, test.expErr) t.Errorf("Test %d: Decryption returned wrong error code: got %d , want %d", i, err, test.expErr)
} else if enc := crypto.IsEncrypted(test.info.UserDefined); encrypted && enc != encrypted { } else if _, enc := crypto.IsEncrypted(test.info.UserDefined); encrypted && enc != encrypted {
t.Errorf("Test %d: Decryption thinks object is encrypted but it is not", i) t.Errorf("Test %d: Decryption thinks object is encrypted but it is not", i)
} else if !encrypted && enc != encrypted { } else if !encrypted && enc != encrypted {
t.Errorf("Test %d: Decryption thinks object is not encrypted but it is", i) t.Errorf("Test %d: Decryption thinks object is not encrypted but it is", i)
+11 -1
View File
@@ -23,12 +23,22 @@ import (
"github.com/minio/minio/pkg/sync/errgroup" "github.com/minio/minio/pkg/sync/errgroup"
) )
func (er erasureObjects) getLocalDisks() (localDisks []StorageAPI) {
disks := er.getDisks()
for _, disk := range disks {
if disk != nil && disk.IsLocal() {
localDisks = append(localDisks, disk)
}
}
return localDisks
}
func (er erasureObjects) getLoadBalancedLocalDisks() (newDisks []StorageAPI) { func (er erasureObjects) getLoadBalancedLocalDisks() (newDisks []StorageAPI) {
disks := er.getDisks() disks := er.getDisks()
// Based on the random shuffling return back randomized disks. // Based on the random shuffling return back randomized disks.
for _, i := range hashOrder(UTCNow().String(), len(disks)) { for _, i := range hashOrder(UTCNow().String(), len(disks)) {
if disks[i-1] != nil && disks[i-1].IsLocal() { if disks[i-1] != nil && disks[i-1].IsLocal() {
if !disks[i-1].Healing() && disks[i-1].IsOnline() { if disks[i-1].Healing() == nil && disks[i-1].IsOnline() {
newDisks = append(newDisks, disks[i-1]) newDisks = append(newDisks, disks[i-1])
} }
} }
+31 -31
View File
@@ -41,46 +41,46 @@ var erasureDecodeTests = []struct {
algorithm BitrotAlgorithm algorithm BitrotAlgorithm
shouldFail, shouldFailQuorum bool shouldFail, shouldFailQuorum bool
}{ }{
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 0 {dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 0
{dataBlocks: 3, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false}, // 1 {dataBlocks: 3, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false}, // 1
{dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 2 {dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 2
{dataBlocks: 5, onDisks: 10, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 1, length: oneMiByte - 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 3 {dataBlocks: 5, onDisks: 10, offDisks: 0, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 1, length: oneMiByte - 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 3
{dataBlocks: 6, onDisks: 12, offDisks: 0, blocksize: int64(oneMiByte), data: oneMiByte, offset: oneMiByte, length: 0, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, {dataBlocks: 6, onDisks: 12, offDisks: 0, blocksize: int64(oneMiByte), data: oneMiByte, offset: oneMiByte, length: 0, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false},
// 4 // 4
{dataBlocks: 7, onDisks: 14, offDisks: 0, blocksize: int64(oneMiByte), data: oneMiByte, offset: 3, length: 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 5 {dataBlocks: 7, onDisks: 14, offDisks: 0, blocksize: int64(oneMiByte), data: oneMiByte, offset: 3, length: 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 5
{dataBlocks: 8, onDisks: 16, offDisks: 0, blocksize: int64(oneMiByte), data: oneMiByte, offset: 4, length: 8 * 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 6 {dataBlocks: 8, onDisks: 16, offDisks: 0, blocksize: int64(oneMiByte), data: oneMiByte, offset: 4, length: 8 * 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 6
{dataBlocks: 7, onDisks: 14, offDisks: 7, blocksize: int64(blockSizeV1), data: oneMiByte, offset: oneMiByte, length: 1, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 7 {dataBlocks: 7, onDisks: 14, offDisks: 7, blocksize: int64(blockSizeV2), data: oneMiByte, offset: oneMiByte, length: 1, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 7
{dataBlocks: 6, onDisks: 12, offDisks: 6, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 8 {dataBlocks: 6, onDisks: 12, offDisks: 6, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 8
{dataBlocks: 5, onDisks: 10, offDisks: 5, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 9 {dataBlocks: 5, onDisks: 10, offDisks: 5, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 9
{dataBlocks: 4, onDisks: 8, offDisks: 4, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false}, // 10 {dataBlocks: 4, onDisks: 8, offDisks: 4, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false}, // 10
{dataBlocks: 3, onDisks: 6, offDisks: 3, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 11 {dataBlocks: 3, onDisks: 6, offDisks: 3, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 11
{dataBlocks: 2, onDisks: 4, offDisks: 2, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 12 {dataBlocks: 2, onDisks: 4, offDisks: 2, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 12
{dataBlocks: 2, onDisks: 4, offDisks: 1, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 13 {dataBlocks: 2, onDisks: 4, offDisks: 1, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 13
{dataBlocks: 3, onDisks: 6, offDisks: 2, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 14 {dataBlocks: 3, onDisks: 6, offDisks: 2, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 14
{dataBlocks: 4, onDisks: 8, offDisks: 3, blocksize: int64(2 * oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 15 {dataBlocks: 4, onDisks: 8, offDisks: 3, blocksize: int64(2 * oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 15
{dataBlocks: 5, onDisks: 10, offDisks: 6, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 16 {dataBlocks: 5, onDisks: 10, offDisks: 6, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 16
{dataBlocks: 5, onDisks: 10, offDisks: 2, blocksize: int64(blockSizeV1), data: 2 * oneMiByte, offset: oneMiByte, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 17 {dataBlocks: 5, onDisks: 10, offDisks: 2, blocksize: int64(blockSizeV2), data: 2 * oneMiByte, offset: oneMiByte, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 17
{dataBlocks: 5, onDisks: 10, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 18 {dataBlocks: 5, onDisks: 10, offDisks: 1, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 18
{dataBlocks: 6, onDisks: 12, offDisks: 3, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false}, {dataBlocks: 6, onDisks: 12, offDisks: 3, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false},
// 19 // 19
{dataBlocks: 6, onDisks: 12, offDisks: 7, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 20 {dataBlocks: 6, onDisks: 12, offDisks: 7, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 20
{dataBlocks: 8, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 21 {dataBlocks: 8, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 21
{dataBlocks: 8, onDisks: 16, offDisks: 9, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 22 {dataBlocks: 8, onDisks: 16, offDisks: 9, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 22
{dataBlocks: 8, onDisks: 16, offDisks: 7, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 23 {dataBlocks: 8, onDisks: 16, offDisks: 7, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 23
{dataBlocks: 2, onDisks: 4, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 24 {dataBlocks: 2, onDisks: 4, offDisks: 1, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 24
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 25 {dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 25
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(blockSizeV1) + 1, offset: 0, length: int64(blockSizeV1) + 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 26 {dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV2), data: int64(blockSizeV2) + 1, offset: 0, length: int64(blockSizeV2) + 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 26
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 12, length: int64(blockSizeV1) + 17, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 27 {dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV2), data: int64(2 * blockSizeV2), offset: 12, length: int64(blockSizeV2) + 17, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 27
{dataBlocks: 3, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 1023, length: int64(blockSizeV1) + 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 28 {dataBlocks: 3, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV2), data: int64(2 * blockSizeV2), offset: 1023, length: int64(blockSizeV2) + 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 28
{dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 11, length: int64(blockSizeV1) + 2*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 29 {dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(blockSizeV2), data: int64(2 * blockSizeV2), offset: 11, length: int64(blockSizeV2) + 2*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 29
{dataBlocks: 6, onDisks: 12, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 512, length: int64(blockSizeV1) + 8*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 30 {dataBlocks: 6, onDisks: 12, offDisks: 0, blocksize: int64(blockSizeV2), data: int64(2 * blockSizeV2), offset: 512, length: int64(blockSizeV2) + 8*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 30
{dataBlocks: 8, onDisks: 16, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: int64(blockSizeV1), length: int64(blockSizeV1) - 1, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 31 {dataBlocks: 8, onDisks: 16, offDisks: 0, blocksize: int64(blockSizeV2), data: int64(2 * blockSizeV2), offset: int64(blockSizeV2), length: int64(blockSizeV2) - 1, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 31
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(oneMiByte), offset: -1, length: 3, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 32 {dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV2), data: int64(oneMiByte), offset: -1, length: 3, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 32
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(oneMiByte), offset: 1024, length: -1, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 33 {dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV2), data: int64(oneMiByte), offset: 1024, length: -1, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 33
{dataBlocks: 4, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(blockSizeV1), offset: 0, length: int64(blockSizeV1), algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 34 {dataBlocks: 4, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV2), data: int64(blockSizeV2), offset: 0, length: int64(blockSizeV2), algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 34
{dataBlocks: 4, onDisks: 6, offDisks: 1, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 12, length: int64(blockSizeV1) + 17, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 35 {dataBlocks: 4, onDisks: 6, offDisks: 1, blocksize: int64(blockSizeV2), data: int64(2 * blockSizeV2), offset: 12, length: int64(blockSizeV2) + 17, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 35
{dataBlocks: 4, onDisks: 6, offDisks: 3, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 1023, length: int64(blockSizeV1) + 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 36 {dataBlocks: 4, onDisks: 6, offDisks: 3, blocksize: int64(blockSizeV2), data: int64(2 * blockSizeV2), offset: 1023, length: int64(blockSizeV2) + 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 36
{dataBlocks: 8, onDisks: 12, offDisks: 4, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 11, length: int64(blockSizeV1) + 2*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 37 {dataBlocks: 8, onDisks: 12, offDisks: 4, blocksize: int64(blockSizeV2), data: int64(2 * blockSizeV2), offset: 11, length: int64(blockSizeV2) + 2*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 37
} }
func TestErasureDecode(t *testing.T) { func TestErasureDecode(t *testing.T) {
@@ -288,13 +288,13 @@ func TestErasureDecodeRandomOffsetLength(t *testing.T) {
// Benchmarks // Benchmarks
func benchmarkErasureDecode(data, parity, dataDown, parityDown int, size int64, b *testing.B) { func benchmarkErasureDecode(data, parity, dataDown, parityDown int, size int64, b *testing.B) {
setup, err := newErasureTestSetup(data, parity, blockSizeV1) setup, err := newErasureTestSetup(data, parity, blockSizeV2)
if err != nil { if err != nil {
b.Fatalf("failed to create test setup: %v", err) b.Fatalf("failed to create test setup: %v", err)
} }
defer setup.Remove() defer setup.Remove()
disks := setup.disks disks := setup.disks
erasure, err := NewErasure(context.Background(), data, parity, blockSizeV1) erasure, err := NewErasure(context.Background(), data, parity, blockSizeV2)
if err != nil { if err != nil {
b.Fatalf("failed to create ErasureStorage: %v", err) b.Fatalf("failed to create ErasureStorage: %v", err)
} }
@@ -308,7 +308,7 @@ func benchmarkErasureDecode(data, parity, dataDown, parityDown int, size int64,
} }
content := make([]byte, size) content := make([]byte, size)
buffer := make([]byte, blockSizeV1, 2*blockSizeV1) buffer := make([]byte, blockSizeV2, 2*blockSizeV2)
_, err = erasure.Encode(context.Background(), bytes.NewReader(content), writers, buffer, erasure.dataBlocks+1) _, err = erasure.Encode(context.Background(), bytes.NewReader(content), writers, buffer, erasure.dataBlocks+1)
closeBitrotWriters(writers) closeBitrotWriters(writers)
if err != nil { if err != nil {
+19 -19
View File
@@ -62,26 +62,26 @@ var erasureEncodeTests = []struct {
algorithm BitrotAlgorithm algorithm BitrotAlgorithm
shouldFail, shouldFailQuorum bool shouldFail, shouldFailQuorum bool
}{ }{
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 0 {dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 0
{dataBlocks: 3, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 1, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false}, // 1 {dataBlocks: 3, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 1, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false}, // 1
{dataBlocks: 4, onDisks: 8, offDisks: 2, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 2 {dataBlocks: 4, onDisks: 8, offDisks: 2, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 2
{dataBlocks: 5, onDisks: 10, offDisks: 3, blocksize: int64(blockSizeV1), data: oneMiByte, offset: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 3 {dataBlocks: 5, onDisks: 10, offDisks: 3, blocksize: int64(blockSizeV2), data: oneMiByte, offset: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 3
{dataBlocks: 6, onDisks: 12, offDisks: 4, blocksize: int64(blockSizeV1), data: oneMiByte, offset: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 4 {dataBlocks: 6, onDisks: 12, offDisks: 4, blocksize: int64(blockSizeV2), data: oneMiByte, offset: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 4
{dataBlocks: 7, onDisks: 14, offDisks: 5, blocksize: int64(blockSizeV1), data: 0, offset: 0, shouldFail: false, algorithm: SHA256, shouldFailQuorum: false}, // 5 {dataBlocks: 7, onDisks: 14, offDisks: 5, blocksize: int64(blockSizeV2), data: 0, offset: 0, shouldFail: false, algorithm: SHA256, shouldFailQuorum: false}, // 5
{dataBlocks: 8, onDisks: 16, offDisks: 7, blocksize: int64(blockSizeV1), data: 0, offset: 0, shouldFail: false, algorithm: DefaultBitrotAlgorithm, shouldFailQuorum: false}, // 6 {dataBlocks: 8, onDisks: 16, offDisks: 7, blocksize: int64(blockSizeV2), data: 0, offset: 0, shouldFail: false, algorithm: DefaultBitrotAlgorithm, shouldFailQuorum: false}, // 6
{dataBlocks: 2, onDisks: 4, offDisks: 2, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: true}, // 7 {dataBlocks: 2, onDisks: 4, offDisks: 2, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: true}, // 7
{dataBlocks: 4, onDisks: 8, offDisks: 4, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: SHA256, shouldFail: false, shouldFailQuorum: true}, // 8 {dataBlocks: 4, onDisks: 8, offDisks: 4, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, algorithm: SHA256, shouldFail: false, shouldFailQuorum: true}, // 8
{dataBlocks: 7, onDisks: 14, offDisks: 7, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 9 {dataBlocks: 7, onDisks: 14, offDisks: 7, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 9
{dataBlocks: 8, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 10 {dataBlocks: 8, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 10
{dataBlocks: 5, onDisks: 10, offDisks: 3, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 11 {dataBlocks: 5, onDisks: 10, offDisks: 3, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 11
{dataBlocks: 3, onDisks: 6, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: oneMiByte / 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 12 {dataBlocks: 3, onDisks: 6, offDisks: 1, blocksize: int64(blockSizeV2), data: oneMiByte, offset: oneMiByte / 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 12
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(oneMiByte / 2), data: oneMiByte, offset: oneMiByte/2 + 1, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 13 {dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(oneMiByte / 2), data: oneMiByte, offset: oneMiByte/2 + 1, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 13
{dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(oneMiByte - 1), data: oneMiByte, offset: oneMiByte - 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 14 {dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(oneMiByte - 1), data: oneMiByte, offset: oneMiByte - 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 14
{dataBlocks: 8, onDisks: 12, offDisks: 2, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 15 {dataBlocks: 8, onDisks: 12, offDisks: 2, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 15
{dataBlocks: 8, onDisks: 10, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 16 {dataBlocks: 8, onDisks: 10, offDisks: 1, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 16
{dataBlocks: 10, onDisks: 14, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 17, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 17 {dataBlocks: 10, onDisks: 14, offDisks: 0, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 17, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 17
{dataBlocks: 2, onDisks: 6, offDisks: 2, blocksize: int64(oneMiByte), data: oneMiByte, offset: oneMiByte / 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 18 {dataBlocks: 2, onDisks: 6, offDisks: 2, blocksize: int64(oneMiByte), data: oneMiByte, offset: oneMiByte / 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 18
{dataBlocks: 10, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 19 {dataBlocks: 10, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 19
} }
func TestErasureEncode(t *testing.T) { func TestErasureEncode(t *testing.T) {
@@ -166,17 +166,17 @@ func TestErasureEncode(t *testing.T) {
// Benchmarks // Benchmarks
func benchmarkErasureEncode(data, parity, dataDown, parityDown int, size int64, b *testing.B) { func benchmarkErasureEncode(data, parity, dataDown, parityDown int, size int64, b *testing.B) {
setup, err := newErasureTestSetup(data, parity, blockSizeV1) setup, err := newErasureTestSetup(data, parity, blockSizeV2)
if err != nil { if err != nil {
b.Fatalf("failed to create test setup: %v", err) b.Fatalf("failed to create test setup: %v", err)
} }
defer setup.Remove() defer setup.Remove()
erasure, err := NewErasure(context.Background(), data, parity, blockSizeV1) erasure, err := NewErasure(context.Background(), data, parity, blockSizeV2)
if err != nil { if err != nil {
b.Fatalf("failed to create ErasureStorage: %v", err) b.Fatalf("failed to create ErasureStorage: %v", err)
} }
disks := setup.disks disks := setup.disks
buffer := make([]byte, blockSizeV1, 2*blockSizeV1) buffer := make([]byte, blockSizeV2, 2*blockSizeV2)
content := make([]byte, size) content := make([]byte, size)
for i := 0; i < dataDown; i++ { for i := 0; i < dataDown; i++ {
+17 -17
View File
@@ -39,26 +39,26 @@ var erasureHealTests = []struct {
algorithm BitrotAlgorithm algorithm BitrotAlgorithm
shouldFail bool shouldFail bool
}{ }{
{dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: SHA256, shouldFail: false}, // 0 {dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: SHA256, shouldFail: false}, // 0
{dataBlocks: 3, disks: 6, offDisks: 2, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 1 {dataBlocks: 3, disks: 6, offDisks: 2, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 1
{dataBlocks: 4, disks: 8, offDisks: 2, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 2 {dataBlocks: 4, disks: 8, offDisks: 2, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 2
{dataBlocks: 5, disks: 10, offDisks: 3, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 3 {dataBlocks: 5, disks: 10, offDisks: 3, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 3
{dataBlocks: 6, disks: 12, offDisks: 2, badDisks: 3, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: SHA256, shouldFail: false}, // 4 {dataBlocks: 6, disks: 12, offDisks: 2, badDisks: 3, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: SHA256, shouldFail: false}, // 4
{dataBlocks: 7, disks: 14, offDisks: 4, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 5 {dataBlocks: 7, disks: 14, offDisks: 4, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 5
{dataBlocks: 8, disks: 16, offDisks: 6, badDisks: 1, badStaleDisks: 1, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 6 {dataBlocks: 8, disks: 16, offDisks: 6, badDisks: 1, badStaleDisks: 1, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 6
{dataBlocks: 7, disks: 14, offDisks: 2, badDisks: 3, badStaleDisks: 0, blocksize: int64(oneMiByte / 2), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 7 {dataBlocks: 7, disks: 14, offDisks: 2, badDisks: 3, badStaleDisks: 0, blocksize: int64(oneMiByte / 2), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 7
{dataBlocks: 6, disks: 12, offDisks: 1, badDisks: 0, badStaleDisks: 1, blocksize: int64(oneMiByte - 1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: true}, // 8 {dataBlocks: 6, disks: 12, offDisks: 1, badDisks: 0, badStaleDisks: 1, blocksize: int64(oneMiByte - 1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: true}, // 8
{dataBlocks: 5, disks: 10, offDisks: 3, badDisks: 0, badStaleDisks: 3, blocksize: int64(oneMiByte / 2), size: oneMiByte, algorithm: SHA256, shouldFail: true}, // 9 {dataBlocks: 5, disks: 10, offDisks: 3, badDisks: 0, badStaleDisks: 3, blocksize: int64(oneMiByte / 2), size: oneMiByte, algorithm: SHA256, shouldFail: true}, // 9
{dataBlocks: 4, disks: 8, offDisks: 1, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 10 {dataBlocks: 4, disks: 8, offDisks: 1, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 10
{dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 1, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: true}, // 11 {dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 1, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: true}, // 11
{dataBlocks: 6, disks: 12, offDisks: 8, badDisks: 3, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: true}, // 12 {dataBlocks: 6, disks: 12, offDisks: 8, badDisks: 3, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: true}, // 12
{dataBlocks: 7, disks: 14, offDisks: 3, badDisks: 4, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 13 {dataBlocks: 7, disks: 14, offDisks: 3, badDisks: 4, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 13
{dataBlocks: 7, disks: 14, offDisks: 6, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 14 {dataBlocks: 7, disks: 14, offDisks: 6, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 14
{dataBlocks: 8, disks: 16, offDisks: 4, badDisks: 5, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: true}, // 15 {dataBlocks: 8, disks: 16, offDisks: 4, badDisks: 5, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: true}, // 15
{dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 16 {dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 16
{dataBlocks: 12, disks: 16, offDisks: 2, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 17 {dataBlocks: 12, disks: 16, offDisks: 2, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 17
{dataBlocks: 6, disks: 8, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 18 {dataBlocks: 6, disks: 8, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 18
{dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte * 64, algorithm: SHA256, shouldFail: false}, // 19 {dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte * 64, algorithm: SHA256, shouldFail: false}, // 19
} }
func TestErasureHeal(t *testing.T) { func TestErasureHeal(t *testing.T) {
+16 -8
View File
@@ -239,13 +239,6 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
storageDisks := er.getDisks() storageDisks := er.getDisks()
storageEndpoints := er.getEndpoints() storageEndpoints := er.getEndpoints()
// List of disks having latest version of the object er.meta
// (by modtime).
latestDisks, modTime := listOnlineDisks(storageDisks, partsMetadata, errs)
// List of disks having all parts as per latest er.meta.
availableDisks, dataErrs := disksWithAllParts(ctx, latestDisks, partsMetadata, errs, bucket, object, scanMode)
// Initialize heal result object // Initialize heal result object
result = madmin.HealResultItem{ result = madmin.HealResultItem{
Type: madmin.HealItemObject, Type: madmin.HealItemObject,
@@ -256,6 +249,21 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
DataBlocks: len(storageDisks) - er.defaultParityCount, DataBlocks: len(storageDisks) - er.defaultParityCount,
} }
if !opts.NoLock {
lk := er.NewNSLock(bucket, object)
if ctx, err = lk.GetLock(ctx, globalOperationTimeout); err != nil {
return result, err
}
defer lk.Unlock()
}
// List of disks having latest version of the object er.meta
// (by modtime).
latestDisks, modTime := listOnlineDisks(storageDisks, partsMetadata, errs)
// List of disks having all parts as per latest er.meta.
availableDisks, dataErrs := disksWithAllParts(ctx, latestDisks, partsMetadata, errs, bucket, object, scanMode)
// Loop to find number of disks with valid data, per-drive // Loop to find number of disks with valid data, per-drive
// data state and a list of outdated disks on which data needs // data state and a list of outdated disks on which data needs
// to be healed. // to be healed.
@@ -709,7 +717,7 @@ func (er erasureObjects) purgeObjectDangling(ctx context.Context, bucket, object
if versionID == "" { if versionID == "" {
err = er.deleteObject(ctx, bucket, object, writeQuorum) err = er.deleteObject(ctx, bucket, object, writeQuorum)
} else { } else {
err = er.deleteObjectVersion(ctx, bucket, object, writeQuorum, FileInfo{VersionID: versionID}) err = er.deleteObjectVersion(ctx, bucket, object, writeQuorum, FileInfo{VersionID: versionID}, false)
} }
// If Delete was successful, make sure to return the appropriate error // If Delete was successful, make sure to return the appropriate error
+22
View File
@@ -18,6 +18,9 @@ package cmd
import ( import (
"context" "context"
"encoding/hex"
"fmt"
"math/rand"
"reflect" "reflect"
"testing" "testing"
) )
@@ -199,3 +202,22 @@ func TestEvalDisks(t *testing.T) {
z := objLayer.(*erasureServerPools) z := objLayer.(*erasureServerPools)
testShuffleDisks(t, z) testShuffleDisks(t, z)
} }
func Test_hashOrder(t *testing.T) {
for x := 1; x < 17; x++ {
t.Run(fmt.Sprintf("%d", x), func(t *testing.T) {
var first [17]int
rng := rand.New(rand.NewSource(0))
var tmp [16]byte
rng.Read(tmp[:])
prefix := hex.EncodeToString(tmp[:])
for i := 0; i < 10000; i++ {
rng.Read(tmp[:])
y := hashOrder(fmt.Sprintf("%s/%x", prefix, hex.EncodeToString(tmp[:3])), x)
first[y[0]]++
}
t.Log("first:", first[:x])
})
}
}
+16 -13
View File
@@ -18,6 +18,7 @@ package cmd
import ( import (
"context" "context"
"crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"net/http" "net/http"
@@ -28,7 +29,6 @@ import (
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/bucket/replication" "github.com/minio/minio/pkg/bucket/replication"
"github.com/minio/minio/pkg/sync/errgroup" "github.com/minio/minio/pkg/sync/errgroup"
"github.com/minio/sha256-simd"
) )
const erasureAlgorithm = "rs-vandermonde" const erasureAlgorithm = "rs-vandermonde"
@@ -102,23 +102,26 @@ func (fi FileInfo) IsValid() bool {
func (fi FileInfo) ToObjectInfo(bucket, object string) ObjectInfo { func (fi FileInfo) ToObjectInfo(bucket, object string) ObjectInfo {
object = decodeDirObject(object) object = decodeDirObject(object)
versionID := fi.VersionID versionID := fi.VersionID
if globalBucketVersioningSys.Enabled(bucket) && versionID == "" { if (globalBucketVersioningSys.Enabled(bucket) || globalBucketVersioningSys.Suspended(bucket)) && versionID == "" {
versionID = nullVersionID versionID = nullVersionID
} }
objInfo := ObjectInfo{ objInfo := ObjectInfo{
IsDir: HasSuffix(object, SlashSeparator), IsDir: HasSuffix(object, SlashSeparator),
Bucket: bucket, Bucket: bucket,
Name: object, Name: object,
VersionID: versionID, VersionID: versionID,
IsLatest: fi.IsLatest, IsLatest: fi.IsLatest,
DeleteMarker: fi.Deleted, DeleteMarker: fi.Deleted,
Size: fi.Size, Size: fi.Size,
ModTime: fi.ModTime, ModTime: fi.ModTime,
Legacy: fi.XLV1, Legacy: fi.XLV1,
ContentType: fi.Metadata["content-type"], ContentType: fi.Metadata["content-type"],
ContentEncoding: fi.Metadata["content-encoding"], ContentEncoding: fi.Metadata["content-encoding"],
NumVersions: fi.NumVersions,
SuccessorModTime: fi.SuccessorModTime,
} }
// Update expires // Update expires
var ( var (
t time.Time t time.Time
+89 -37
View File
@@ -20,10 +20,12 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"os"
"path" "path"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio-go/v7/pkg/set"
@@ -91,50 +93,85 @@ func (er erasureObjects) removeObjectPart(bucket, object, uploadID, dataDir stri
// Clean-up the old multipart uploads. Should be run in a Go routine. // Clean-up the old multipart uploads. Should be run in a Go routine.
func (er erasureObjects) cleanupStaleUploads(ctx context.Context, expiry time.Duration) { func (er erasureObjects) cleanupStaleUploads(ctx context.Context, expiry time.Duration) {
// run multiple cleanup's local to this server. // run multiple cleanup's local to this server.
var wg sync.WaitGroup
for _, disk := range er.getLoadBalancedLocalDisks() { for _, disk := range er.getLoadBalancedLocalDisks() {
if disk != nil { if disk != nil {
er.cleanupStaleUploadsOnDisk(ctx, disk, expiry) wg.Add(1)
return go func(disk StorageAPI) {
defer wg.Done()
er.cleanupStaleUploadsOnDisk(ctx, disk, expiry)
}(disk)
} }
} }
wg.Wait()
}
func (er erasureObjects) renameAll(ctx context.Context, bucket, prefix string) {
var wg sync.WaitGroup
for _, disk := range er.getDisks() {
if disk == nil {
continue
}
wg.Add(1)
go func(disk StorageAPI) {
defer wg.Done()
disk.RenameFile(ctx, bucket, prefix, minioMetaTmpBucket, mustGetUUID())
}(disk)
}
wg.Wait()
}
func (er erasureObjects) deleteAll(ctx context.Context, bucket, prefix string) {
var wg sync.WaitGroup
for _, disk := range er.getDisks() {
if disk == nil {
continue
}
wg.Add(1)
go func(disk StorageAPI) {
defer wg.Done()
disk.Delete(ctx, bucket, prefix, true)
}(disk)
}
wg.Wait()
} }
// Remove the old multipart uploads on the given disk. // Remove the old multipart uploads on the given disk.
func (er erasureObjects) cleanupStaleUploadsOnDisk(ctx context.Context, disk StorageAPI, expiry time.Duration) { func (er erasureObjects) cleanupStaleUploadsOnDisk(ctx context.Context, disk StorageAPI, expiry time.Duration) {
now := time.Now() now := time.Now()
shaDirs, err := disk.ListDir(ctx, minioMetaMultipartBucket, "", -1) diskPath := disk.Endpoint().Path
if err != nil {
return readDirFn(pathJoin(diskPath, minioMetaMultipartBucket), func(shaDir string, typ os.FileMode) error {
} return readDirFn(pathJoin(diskPath, minioMetaMultipartBucket, shaDir), func(uploadIDDir string, typ os.FileMode) error {
for _, shaDir := range shaDirs {
uploadIDDirs, err := disk.ListDir(ctx, minioMetaMultipartBucket, shaDir, -1)
if err != nil {
continue
}
for _, uploadIDDir := range uploadIDDirs {
uploadIDPath := pathJoin(shaDir, uploadIDDir) uploadIDPath := pathJoin(shaDir, uploadIDDir)
fi, err := disk.ReadVersion(ctx, minioMetaMultipartBucket, uploadIDPath, "", false) fi, err := disk.ReadVersion(ctx, minioMetaMultipartBucket, uploadIDPath, "", false)
if err != nil { if err != nil {
continue return nil
} }
wait := er.deletedCleanupSleeper.Timer(ctx)
if now.Sub(fi.ModTime) > expiry { if now.Sub(fi.ModTime) > expiry {
er.deleteObject(ctx, minioMetaMultipartBucket, uploadIDPath, fi.Erasure.DataBlocks+1) er.renameAll(ctx, minioMetaMultipartBucket, uploadIDPath)
} }
wait()
return nil
})
})
readDirFn(pathJoin(diskPath, minioMetaTmpBucket), func(tmpDir string, typ os.FileMode) error {
if tmpDir == ".trash/" { // do not remove .trash/ here, it has its own routines
return nil
} }
} vi, err := disk.StatVol(ctx, pathJoin(minioMetaTmpBucket, tmpDir))
tmpDirs, err := disk.ListDir(ctx, minioMetaTmpBucket, "", -1)
if err != nil {
return
}
for _, tmpDir := range tmpDirs {
fi, err := disk.ReadVersion(ctx, minioMetaTmpBucket, tmpDir, "", false)
if err != nil { if err != nil {
continue return nil
} }
if now.Sub(fi.ModTime) > expiry { wait := er.deletedCleanupSleeper.Timer(ctx)
er.deleteObject(ctx, minioMetaTmpBucket, tmpDir, fi.Erasure.DataBlocks+1) if now.Sub(vi.Created) > expiry {
er.deleteAll(ctx, minioMetaTmpBucket, tmpDir)
} }
} wait()
return nil
})
} }
// ListMultipartUploads - lists all the pending multipart // ListMultipartUploads - lists all the pending multipart
@@ -247,7 +284,7 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
} }
dataBlocks := len(onlineDisks) - parityBlocks dataBlocks := len(onlineDisks) - parityBlocks
fi := newFileInfo(object, dataBlocks, parityBlocks) fi := newFileInfo(pathJoin(bucket, object), dataBlocks, parityBlocks)
// we now know the number of blocks this object needs for data and parity. // we now know the number of blocks this object needs for data and parity.
// establish the writeQuorum using this data // establish the writeQuorum using this data
@@ -325,7 +362,7 @@ func (er erasureObjects) NewMultipartUpload(ctx context.Context, bucket, object
// //
// Implements S3 compatible Upload Part Copy API. // Implements S3 compatible Upload Part Copy API.
func (er erasureObjects) 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) { func (er erasureObjects) 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) {
partInfo, err := er.PutObjectPart(ctx, dstBucket, dstObject, uploadID, partID, NewPutObjReader(srcInfo.Reader, nil, nil), dstOpts) partInfo, err := er.PutObjectPart(ctx, dstBucket, dstObject, uploadID, partID, NewPutObjReader(srcInfo.Reader), dstOpts)
if err != nil { if err != nil {
return pi, toObjectErr(err, dstBucket, dstObject) return pi, toObjectErr(err, dstBucket, dstObject)
} }
@@ -341,7 +378,8 @@ func (er erasureObjects) CopyObjectPart(ctx context.Context, srcBucket, srcObjec
// Implements S3 compatible Upload Part API. // Implements S3 compatible Upload Part API.
func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, r *PutObjReader, opts ObjectOptions) (pi PartInfo, err error) { func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, r *PutObjReader, opts ObjectOptions) (pi PartInfo, err error) {
uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID)) uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
if err = uploadIDLock.GetRLock(ctx, globalOperationTimeout); err != nil { ctx, err = uploadIDLock.GetRLock(ctx, globalOperationTimeout)
if err != nil {
return PartInfo{}, err return PartInfo{}, err
} }
readLocked := true readLocked := true
@@ -415,7 +453,14 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
switch size := data.Size(); { switch size := data.Size(); {
case size == 0: case size == 0:
buffer = make([]byte, 1) // Allocate atleast a byte to reach EOF buffer = make([]byte, 1) // Allocate atleast a byte to reach EOF
case size == -1 || size >= fi.Erasure.BlockSize: case size == -1:
if size := data.ActualSize(); size > 0 && size < fi.Erasure.BlockSize {
buffer = make([]byte, data.ActualSize()+256, data.ActualSize()*2+512)
} else {
buffer = er.bp.Get()
defer er.bp.Put(buffer)
}
case size >= fi.Erasure.BlockSize:
buffer = er.bp.Get() buffer = er.bp.Get()
defer er.bp.Put(buffer) defer er.bp.Put(buffer)
case size < fi.Erasure.BlockSize: case size < fi.Erasure.BlockSize:
@@ -456,7 +501,8 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
// PutObjectParts would serialize here updating `xl.meta` // PutObjectParts would serialize here updating `xl.meta`
uploadIDLock.RUnlock() uploadIDLock.RUnlock()
readLocked = false readLocked = false
if err = uploadIDLock.GetLock(ctx, globalOperationTimeout); err != nil { ctx, err = uploadIDLock.GetLock(ctx, globalOperationTimeout)
if err != nil {
return PartInfo{}, err return PartInfo{}, err
} }
defer uploadIDLock.Unlock() defer uploadIDLock.Unlock()
@@ -537,8 +583,10 @@ func (er erasureObjects) GetMultipartInfo(ctx context.Context, bucket, object, u
UploadID: uploadID, UploadID: uploadID,
} }
var err error
uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID)) uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
if err := uploadIDLock.GetRLock(ctx, globalOperationTimeout); err != nil { ctx, err = uploadIDLock.GetRLock(ctx, globalOperationTimeout)
if err != nil {
return MultipartInfo{}, err return MultipartInfo{}, err
} }
defer uploadIDLock.RUnlock() defer uploadIDLock.RUnlock()
@@ -584,9 +632,10 @@ func (er erasureObjects) GetMultipartInfo(ctx context.Context, bucket, object, u
// Implements S3 compatible ListObjectParts API. The resulting // Implements S3 compatible ListObjectParts API. The resulting
// ListPartsInfo structure is marshaled directly into XML and // ListPartsInfo structure is marshaled directly into XML and
// replied back to the client. // replied back to the client.
func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker, maxParts int, opts ObjectOptions) (result ListPartsInfo, e error) { func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker, maxParts int, opts ObjectOptions) (result ListPartsInfo, err error) {
uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID)) uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
if err := uploadIDLock.GetRLock(ctx, globalOperationTimeout); err != nil { ctx, err = uploadIDLock.GetRLock(ctx, globalOperationTimeout)
if err != nil {
return ListPartsInfo{}, err return ListPartsInfo{}, err
} }
defer uploadIDLock.RUnlock() defer uploadIDLock.RUnlock()
@@ -679,7 +728,8 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
// Hold read-locks to verify uploaded parts, also disallows // Hold read-locks to verify uploaded parts, also disallows
// parallel part uploads as well. // parallel part uploads as well.
uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID)) uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
if err = uploadIDLock.GetRLock(ctx, globalOperationTimeout); err != nil { ctx, err = uploadIDLock.GetRLock(ctx, globalOperationTimeout)
if err != nil {
return oi, err return oi, err
} }
defer uploadIDLock.RUnlock() defer uploadIDLock.RUnlock()
@@ -834,7 +884,8 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
// Hold namespace to complete the transaction // Hold namespace to complete the transaction
lk := er.NewNSLock(bucket, object) lk := er.NewNSLock(bucket, object)
if err = lk.GetLock(ctx, globalOperationTimeout); err != nil { ctx, err = lk.GetLock(ctx, globalOperationTimeout)
if err != nil {
return oi, err return oi, err
} }
defer lk.Unlock() defer lk.Unlock()
@@ -874,9 +925,10 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
// All parts are purged from all disks and reference to the uploadID // All parts are purged from all disks and reference to the uploadID
// would be removed from the system, rollback is not possible on this // would be removed from the system, rollback is not possible on this
// operation. // operation.
func (er erasureObjects) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) error { func (er erasureObjects) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) (err error) {
lk := er.NewNSLock(bucket, pathJoin(object, uploadID)) lk := er.NewNSLock(bucket, pathJoin(object, uploadID))
if err := lk.GetLock(ctx, globalOperationTimeout); err != nil { ctx, err = lk.GetLock(ctx, globalOperationTimeout)
if err != nil {
return err return err
} }
defer lk.Unlock() defer lk.Unlock()
+78 -57
View File
@@ -45,7 +45,7 @@ var objectOpIgnoredErrs = append(baseIgnoredErrs, errDiskAccessDenied, errUnform
// CopyObject - copy object source object to destination object. // CopyObject - copy object source object to destination object.
// if source object and destination object are same we only // if source object and destination object are same we only
// update metadata. // update metadata.
func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (oi ObjectInfo, e error) { func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (oi ObjectInfo, err error) {
// This call shouldn't be used for anything other than metadata updates or adding self referential versions. // This call shouldn't be used for anything other than metadata updates or adding self referential versions.
if !srcInfo.metadataOnly { if !srcInfo.metadataOnly {
return oi, NotImplemented{} return oi, NotImplemented{}
@@ -54,7 +54,8 @@ func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, d
defer ObjectPathUpdated(pathJoin(dstBucket, dstObject)) defer ObjectPathUpdated(pathJoin(dstBucket, dstObject))
lk := er.NewNSLock(dstBucket, dstObject) lk := er.NewNSLock(dstBucket, dstObject)
if err := lk.GetLock(ctx, globalOperationTimeout); err != nil { ctx, err = lk.GetLock(ctx, globalOperationTimeout)
if err != nil {
return oi, err return oi, err
} }
defer lk.Unlock() defer lk.Unlock()
@@ -147,12 +148,14 @@ func (er erasureObjects) GetObjectNInfo(ctx context.Context, bucket, object stri
lock := er.NewNSLock(bucket, object) lock := er.NewNSLock(bucket, object)
switch lockType { switch lockType {
case writeLock: case writeLock:
if err = lock.GetLock(ctx, globalOperationTimeout); err != nil { ctx, err = lock.GetLock(ctx, globalOperationTimeout)
if err != nil {
return nil, err return nil, err
} }
nsUnlocker = lock.Unlock nsUnlocker = lock.Unlock
case readLock: case readLock:
if err = lock.GetRLock(ctx, globalOperationTimeout); err != nil { ctx, err = lock.GetRLock(ctx, globalOperationTimeout)
if err != nil {
return nil, err return nil, err
} }
nsUnlocker = lock.RUnlock nsUnlocker = lock.RUnlock
@@ -162,14 +165,6 @@ func (er erasureObjects) GetObjectNInfo(ctx context.Context, bucket, object stri
fi, metaArr, onlineDisks, err := er.getObjectFileInfo(ctx, bucket, object, opts, true) fi, metaArr, onlineDisks, err := er.getObjectFileInfo(ctx, bucket, object, opts, true)
if err != nil { if err != nil {
if isProxyable(ctx, bucket) && (errors.Is(err, errFileNotFound) || errors.Is(err, errFileVersionNotFound)) {
// proxy to replication target if active-active replication is in place.
reader, proxy := proxyGetToReplicationTarget(ctx, bucket, object, rs, h, opts)
if reader == nil || !proxy {
return nil, toObjectErr(err, bucket, object)
}
return reader, nil
}
return nil, toObjectErr(err, bucket, object) return nil, toObjectErr(err, bucket, object)
} }
@@ -187,7 +182,7 @@ func (er erasureObjects) GetObjectNInfo(ctx context.Context, bucket, object stri
} }
if objInfo.TransitionStatus == lifecycle.TransitionComplete { if objInfo.TransitionStatus == lifecycle.TransitionComplete {
// If transitioned, stream from transition tier unless object is restored locally or restore date is past. // If transitioned, stream from transition tier unless object is restored locally or restore date is past.
restoreHdr, ok := objInfo.UserDefined[xhttp.AmzRestore] restoreHdr, ok := caseInsensitiveMap(objInfo.UserDefined).Lookup(xhttp.AmzRestore)
if !ok || !strings.HasPrefix(restoreHdr, "ongoing-request=false") || (!objInfo.RestoreExpires.IsZero() && time.Now().After(objInfo.RestoreExpires)) { if !ok || !strings.HasPrefix(restoreHdr, "ongoing-request=false") || (!objInfo.RestoreExpires.IsZero() && time.Now().After(objInfo.RestoreExpires)) {
return getTransitionedObjectReader(ctx, bucket, object, rs, h, objInfo, opts) return getTransitionedObjectReader(ctx, bucket, object, rs, h, objInfo, opts)
} }
@@ -216,10 +211,11 @@ func (er erasureObjects) GetObjectNInfo(ctx context.Context, bucket, object stri
// //
// startOffset indicates the starting read location of the object. // startOffset indicates the starting read location of the object.
// length indicates the total length of the object. // length indicates the total length of the object.
func (er erasureObjects) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) error { func (er erasureObjects) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) (err error) {
// Lock the object before reading. // Lock the object before reading.
lk := er.NewNSLock(bucket, object) lk := er.NewNSLock(bucket, object)
if err := lk.GetRLock(ctx, globalOperationTimeout); err != nil { ctx, err = lk.GetRLock(ctx, globalOperationTimeout)
if err != nil {
return err return err
} }
defer lk.RUnlock() defer lk.RUnlock()
@@ -336,7 +332,9 @@ func (er erasureObjects) getObjectWithFileInfo(ctx context.Context, bucket, obje
} }
if scan != madmin.HealUnknownScan { if scan != madmin.HealUnknownScan {
healOnce.Do(func() { healOnce.Do(func() {
go healObject(bucket, object, fi.VersionID, scan) if _, healing := er.getOnlineDisksWithHealing(); !healing {
go healObject(bucket, object, fi.VersionID, scan)
}
}) })
} }
} }
@@ -378,12 +376,15 @@ func (er erasureObjects) getObject(ctx context.Context, bucket, object string, s
// GetObjectInfo - reads object metadata and replies back ObjectInfo. // GetObjectInfo - reads object metadata and replies back ObjectInfo.
func (er erasureObjects) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (info ObjectInfo, err error) { func (er erasureObjects) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (info ObjectInfo, err error) {
// Lock the object before reading. if !opts.NoLock {
lk := er.NewNSLock(bucket, object) // Lock the object before reading.
if err := lk.GetRLock(ctx, globalOperationTimeout); err != nil { lk := er.NewNSLock(bucket, object)
return ObjectInfo{}, err ctx, err = lk.GetRLock(ctx, globalOperationTimeout)
if err != nil {
return ObjectInfo{}, err
}
defer lk.RUnlock()
} }
defer lk.RUnlock()
return er.getObjectInfo(ctx, bucket, object, opts) return er.getObjectInfo(ctx, bucket, object, opts)
} }
@@ -414,7 +415,7 @@ func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object s
er.deleteObjectVersion(ctx, bucket, object, 1, FileInfo{ er.deleteObjectVersion(ctx, bucket, object, 1, FileInfo{
Name: object, Name: object,
VersionID: opts.VersionID, VersionID: opts.VersionID,
}) }, false)
} }
} }
} }
@@ -444,7 +445,9 @@ func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object s
// if missing metadata can be reconstructed, attempt to reconstruct. // if missing metadata can be reconstructed, attempt to reconstruct.
if missingBlocks > 0 && missingBlocks < readQuorum { if missingBlocks > 0 && missingBlocks < readQuorum {
go healObject(bucket, object, fi.VersionID, madmin.HealNormalScan) if _, healing := er.getOnlineDisksWithHealing(); !healing {
go healObject(bucket, object, fi.VersionID, madmin.HealNormalScan)
}
} }
return fi, metaArr, onlineDisks, nil return fi, metaArr, onlineDisks, nil
@@ -454,13 +457,6 @@ func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object s
func (er erasureObjects) getObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) { func (er erasureObjects) getObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
fi, _, _, err := er.getObjectFileInfo(ctx, bucket, object, opts, false) fi, _, _, err := er.getObjectFileInfo(ctx, bucket, object, opts, false)
if err != nil { if err != nil {
// proxy HEAD to replication target if active-active replication configured on bucket
if isProxyable(ctx, bucket) && (errors.Is(err, errFileNotFound) || errors.Is(err, errFileVersionNotFound)) {
oi, proxy, err := proxyHeadToReplicationTarget(ctx, bucket, object, opts)
if proxy {
return oi, err
}
}
return objInfo, toObjectErr(err, bucket, object) return objInfo, toObjectErr(err, bucket, object)
} }
@@ -471,10 +467,11 @@ func (er erasureObjects) getObjectInfo(ctx context.Context, bucket, object strin
objInfo.StorageClass = sc objInfo.StorageClass = sc
} }
} }
if !fi.VersionPurgeStatus.Empty() { if !fi.VersionPurgeStatus.Empty() && opts.VersionID != "" {
// Make sure to return object info to provide extra information. // Make sure to return object info to provide extra information.
return objInfo, toObjectErr(errMethodNotAllowed, bucket, object) return objInfo, toObjectErr(errMethodNotAllowed, bucket, object)
} }
if fi.Deleted { if fi.Deleted {
if opts.VersionID == "" || opts.DeleteMarker { if opts.VersionID == "" || opts.DeleteMarker {
return objInfo, toObjectErr(errFileNotFound, bucket, object) return objInfo, toObjectErr(errFileNotFound, bucket, object)
@@ -627,10 +624,13 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
storageDisks := er.getDisks() storageDisks := er.getDisks()
// Get parity and data drive count based on storage class metadata parityDrives := len(storageDisks) / 2
parityDrives := globalStorageClass.GetParityForSC(opts.UserDefined[xhttp.AmzStorageClass]) if !opts.MaxParity {
if parityDrives <= 0 { // Get parity and data drive count based on storage class metadata
parityDrives = er.defaultParityCount parityDrives = globalStorageClass.GetParityForSC(opts.UserDefined[xhttp.AmzStorageClass])
if parityDrives <= 0 {
parityDrives = er.defaultParityCount
}
} }
dataDrives := len(storageDisks) - parityDrives dataDrives := len(storageDisks) - parityDrives
@@ -662,7 +662,7 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
// Initialize parts metadata // Initialize parts metadata
partsMetadata := make([]FileInfo, len(storageDisks)) partsMetadata := make([]FileInfo, len(storageDisks))
fi := newFileInfo(object, dataDrives, parityDrives) fi := newFileInfo(pathJoin(bucket, object), dataDrives, parityDrives)
if opts.Versioned { if opts.Versioned {
fi.VersionID = opts.VersionID fi.VersionID = opts.VersionID
@@ -691,7 +691,14 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
switch size := data.Size(); { switch size := data.Size(); {
case size == 0: case size == 0:
buffer = make([]byte, 1) // Allocate atleast a byte to reach EOF buffer = make([]byte, 1) // Allocate atleast a byte to reach EOF
case size == -1 || size >= fi.Erasure.BlockSize: case size == -1:
if size := data.ActualSize(); size > 0 && size < fi.Erasure.BlockSize {
buffer = make([]byte, data.ActualSize()+256, data.ActualSize()*2+512)
} else {
buffer = er.bp.Get()
defer er.bp.Put(buffer)
}
case size >= fi.Erasure.BlockSize:
buffer = er.bp.Get() buffer = er.bp.Get()
defer er.bp.Put(buffer) defer er.bp.Put(buffer)
case size < fi.Erasure.BlockSize: case size < fi.Erasure.BlockSize:
@@ -727,8 +734,10 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
} }
if !opts.NoLock { if !opts.NoLock {
var err error
lk := er.NewNSLock(bucket, object) lk := er.NewNSLock(bucket, object)
if err := lk.GetLock(ctx, globalOperationTimeout); err != nil { ctx, err = lk.GetLock(ctx, globalOperationTimeout)
if err != nil {
return ObjectInfo{}, err return ObjectInfo{}, err
} }
defer lk.Unlock() defer lk.Unlock()
@@ -800,7 +809,7 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
return fi.ToObjectInfo(bucket, object), nil return fi.ToObjectInfo(bucket, object), nil
} }
func (er erasureObjects) deleteObjectVersion(ctx context.Context, bucket, object string, writeQuorum int, fi FileInfo) error { func (er erasureObjects) deleteObjectVersion(ctx context.Context, bucket, object string, writeQuorum int, fi FileInfo, forceDelMarker bool) error {
defer ObjectPathUpdated(pathJoin(bucket, object)) defer ObjectPathUpdated(pathJoin(bucket, object))
disks := er.getDisks() disks := er.getDisks()
g := errgroup.WithNErrs(len(disks)) g := errgroup.WithNErrs(len(disks))
@@ -810,7 +819,7 @@ func (er erasureObjects) deleteObjectVersion(ctx context.Context, bucket, object
if disks[index] == nil { if disks[index] == nil {
return errDiskNotFound return errDiskNotFound
} }
return disks[index].DeleteVersion(ctx, bucket, object, fi) return disks[index].DeleteVersion(ctx, bucket, object, fi, forceDelMarker)
}, index) }, index)
} }
// return errors if any during deletion // return errors if any during deletion
@@ -1037,7 +1046,8 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
} }
// Acquire a write lock before deleting the object. // Acquire a write lock before deleting the object.
lk := er.NewNSLock(bucket, object) lk := er.NewNSLock(bucket, object)
if err = lk.GetLock(ctx, globalDeleteOperationTimeout); err != nil { ctx, err = lk.GetLock(ctx, globalDeleteOperationTimeout)
if err != nil {
return ObjectInfo{}, err return ObjectInfo{}, err
} }
defer lk.Unlock() defer lk.Unlock()
@@ -1049,11 +1059,10 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
if goi.VersionID != "" { if goi.VersionID != "" {
markDelete = true markDelete = true
} }
// Default deleteMarker to true if object is under versioning // Default deleteMarker to true if object is under versioning
deleteMarker := true deleteMarker := opts.Versioned
if gerr == nil {
deleteMarker = goi.VersionID != ""
}
if opts.VersionID != "" { if opts.VersionID != "" {
// case where replica version needs to be deleted on target cluster // case where replica version needs to be deleted on target cluster
if versionFound && opts.DeleteMarkerReplicationStatus == replication.Replica.String() { if versionFound && opts.DeleteMarkerReplicationStatus == replication.Replica.String() {
@@ -1062,7 +1071,7 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
if opts.VersionPurgeStatus.Empty() && opts.DeleteMarkerReplicationStatus == "" { if opts.VersionPurgeStatus.Empty() && opts.DeleteMarkerReplicationStatus == "" {
markDelete = false markDelete = false
} }
if opts.DeleteMarker && opts.VersionPurgeStatus == Complete { if opts.VersionPurgeStatus == Complete {
markDelete = false markDelete = false
} }
// determine if the version represents an object delete // determine if the version represents an object delete
@@ -1098,7 +1107,7 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
// version as delete marker // version as delete marker
// Add delete marker, since we don't have any version specified explicitly. // Add delete marker, since we don't have any version specified explicitly.
// Or if a particular version id needs to be replicated. // Or if a particular version id needs to be replicated.
if err = er.deleteObjectVersion(ctx, bucket, object, writeQuorum, fi); err != nil { if err = er.deleteObjectVersion(ctx, bucket, object, writeQuorum, fi, opts.DeleteMarker); err != nil {
return objInfo, toObjectErr(err, bucket, object) return objInfo, toObjectErr(err, bucket, object)
} }
return fi.ToObjectInfo(bucket, object), nil return fi.ToObjectInfo(bucket, object), nil
@@ -1115,7 +1124,7 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
DeleteMarkerReplicationStatus: opts.DeleteMarkerReplicationStatus, DeleteMarkerReplicationStatus: opts.DeleteMarkerReplicationStatus,
VersionPurgeStatus: opts.VersionPurgeStatus, VersionPurgeStatus: opts.VersionPurgeStatus,
TransitionStatus: opts.TransitionStatus, TransitionStatus: opts.TransitionStatus,
}); err != nil { }, opts.DeleteMarker); err != nil {
return objInfo, toObjectErr(err, bucket, object) return objInfo, toObjectErr(err, bucket, object)
} }
@@ -1146,7 +1155,16 @@ func (er erasureObjects) addPartial(bucket, object, versionID string) {
} }
// PutObjectTags - replace or add tags to an existing object // PutObjectTags - replace or add tags to an existing object
func (er erasureObjects) PutObjectTags(ctx context.Context, bucket, object string, tags string, opts ObjectOptions) error { func (er erasureObjects) PutObjectTags(ctx context.Context, bucket, object string, tags string, opts ObjectOptions) (ObjectInfo, error) {
var err error
// Lock the object before updating tags.
lk := er.NewNSLock(bucket, object)
ctx, err = lk.GetLock(ctx, globalOperationTimeout)
if err != nil {
return ObjectInfo{}, err
}
defer lk.Unlock()
disks := er.getDisks() disks := er.getDisks()
// Read metadata associated with the object from all disks. // Read metadata associated with the object from all disks.
@@ -1154,7 +1172,7 @@ func (er erasureObjects) PutObjectTags(ctx context.Context, bucket, object strin
readQuorum, writeQuorum, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount) readQuorum, writeQuorum, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
if err != nil { if err != nil {
return toObjectErr(err, bucket, object) return ObjectInfo{}, toObjectErr(err, bucket, object)
} }
// List all online disks. // List all online disks.
@@ -1163,13 +1181,13 @@ func (er erasureObjects) PutObjectTags(ctx context.Context, bucket, object strin
// Pick latest valid metadata. // Pick latest valid metadata.
fi, err := pickValidFileInfo(ctx, metaArr, modTime, readQuorum) fi, err := pickValidFileInfo(ctx, metaArr, modTime, readQuorum)
if err != nil { if err != nil {
return toObjectErr(err, bucket, object) return ObjectInfo{}, toObjectErr(err, bucket, object)
} }
if fi.Deleted { if fi.Deleted {
if opts.VersionID == "" { if opts.VersionID == "" {
return toObjectErr(errFileNotFound, bucket, object) return ObjectInfo{}, toObjectErr(errFileNotFound, bucket, object)
} }
return toObjectErr(errMethodNotAllowed, bucket, object) return ObjectInfo{}, toObjectErr(errMethodNotAllowed, bucket, object)
} }
onlineDisks, metaArr = shuffleDisksAndPartsMetadataByIndex(onlineDisks, metaArr, fi.Erasure.Distribution) onlineDisks, metaArr = shuffleDisksAndPartsMetadataByIndex(onlineDisks, metaArr, fi.Erasure.Distribution)
@@ -1192,15 +1210,18 @@ func (er erasureObjects) PutObjectTags(ctx context.Context, bucket, object strin
// Write unique `xl.meta` for each disk. // Write unique `xl.meta` for each disk.
if onlineDisks, err = writeUniqueFileInfo(ctx, onlineDisks, minioMetaTmpBucket, tempObj, metaArr, writeQuorum); err != nil { if onlineDisks, err = writeUniqueFileInfo(ctx, onlineDisks, minioMetaTmpBucket, tempObj, metaArr, writeQuorum); err != nil {
return toObjectErr(err, bucket, object) return ObjectInfo{}, toObjectErr(err, bucket, object)
} }
// Atomically rename metadata from tmp location to destination for each disk. // Atomically rename metadata from tmp location to destination for each disk.
if _, err = renameFileInfo(ctx, onlineDisks, minioMetaTmpBucket, tempObj, bucket, object, writeQuorum); err != nil { if _, err = renameFileInfo(ctx, onlineDisks, minioMetaTmpBucket, tempObj, bucket, object, writeQuorum); err != nil {
return toObjectErr(err, bucket, object) return ObjectInfo{}, toObjectErr(err, bucket, object)
} }
return nil objInfo := fi.ToObjectInfo(bucket, object)
objInfo.UserTags = tags
return objInfo, nil
} }
// updateObjectMeta will update the metadata of a file. // updateObjectMeta will update the metadata of a file.
@@ -1264,7 +1285,7 @@ func (er erasureObjects) updateObjectMeta(ctx context.Context, bucket, object st
} }
// DeleteObjectTags - delete object tags from an existing object // DeleteObjectTags - delete object tags from an existing object
func (er erasureObjects) DeleteObjectTags(ctx context.Context, bucket, object string, opts ObjectOptions) error { func (er erasureObjects) DeleteObjectTags(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
return er.PutObjectTags(ctx, bucket, object, "", opts) return er.PutObjectTags(ctx, bucket, object, "", opts)
} }
+383 -241
View File
@@ -33,9 +33,9 @@ import (
"github.com/minio/minio-go/v7/pkg/tags" "github.com/minio/minio-go/v7/pkg/tags"
"github.com/minio/minio/cmd/config/storageclass" "github.com/minio/minio/cmd/config/storageclass"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/color"
"github.com/minio/minio/pkg/madmin" "github.com/minio/minio/pkg/madmin"
"github.com/minio/minio/pkg/sync/errgroup" "github.com/minio/minio/pkg/sync/errgroup"
"github.com/minio/minio/pkg/wildcard"
) )
type erasureServerPools struct { type erasureServerPools struct {
@@ -107,16 +107,10 @@ func newErasureServerPools(ctx context.Context, endpointServerPools EndpointServ
return nil, fmt.Errorf("All serverPools should have same deployment ID expected %s, got %s", deploymentID, formats[i].ID) return nil, fmt.Errorf("All serverPools should have same deployment ID expected %s, got %s", deploymentID, formats[i].ID)
} }
// Validate if users brought different different distribution algo pools. z.serverPools[i], err = newErasureSets(ctx, ep.Endpoints, storageDisks[i], formats[i], commonParityDrives, i)
if distributionAlgo != formats[i].Erasure.DistributionAlgo {
return nil, fmt.Errorf("All serverPools should have same distributionAlgo expected %s, got %s", distributionAlgo, formats[i].Erasure.DistributionAlgo)
}
z.serverPools[i], err = newErasureSets(ctx, ep.Endpoints, storageDisks[i], formats[i], commonParityDrives)
if err != nil { if err != nil {
return nil, err return nil, err
} }
z.serverPools[i].poolNumber = i
} }
ctx, z.shutdown = context.WithCancel(ctx) ctx, z.shutdown = context.WithCancel(ctx)
go intDataUpdateTracker.start(ctx, localDrives...) go intDataUpdateTracker.start(ctx, localDrives...)
@@ -252,29 +246,39 @@ func (z *erasureServerPools) getServerPoolsAvailableSpace(ctx context.Context, s
// getPoolIdx returns the found previous object and its corresponding pool idx, // getPoolIdx returns the found previous object and its corresponding pool idx,
// if none are found falls back to most available space pool. // if none are found falls back to most available space pool.
func (z *erasureServerPools) getPoolIdx(ctx context.Context, bucket, object string, opts ObjectOptions, size int64) (idx int, err error) { func (z *erasureServerPools) getPoolIdx(ctx context.Context, bucket, object string, size int64) (idx int, err error) {
if z.SinglePool() { if z.SinglePool() {
return 0, nil return 0, nil
} }
errs := make([]error, len(z.serverPools))
objInfos := make([]ObjectInfo, len(z.serverPools))
var wg sync.WaitGroup
for i, pool := range z.serverPools { for i, pool := range z.serverPools {
objInfo, err := pool.GetObjectInfo(ctx, bucket, object, opts) wg.Add(1)
switch err.(type) { go func(i int, pool *erasureSets) {
case ObjectNotFound: defer wg.Done()
// VersionId was not specified but found delete marker or no versions exist. objInfos[i], errs[i] = pool.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
case MethodNotAllowed: }(i, pool)
// VersionId was specified but found delete marker }
default: wg.Wait()
if err != nil {
// any other un-handled errors return right here. for i, err := range errs {
return -1, err if err != nil && !isErrObjectNotFound(err) {
} return -1, err
} }
// delete marker not specified means no versions if isErrObjectNotFound(err) {
// exist continue to next pool. // No object exists or its a delete marker,
if !objInfo.DeleteMarker && err != nil { // check objInfo to confirm.
if objInfos[i].DeleteMarker && objInfos[i].Name != "" {
return i, nil
}
// objInfo is not valid, truly the object doesn't
// exist proceed to next pool.
continue continue
} }
// Success case and when DeleteMarker is true return. // object exists at this pool.
return i, nil return i, nil
} }
@@ -307,8 +311,8 @@ func (z *erasureServerPools) Shutdown(ctx context.Context) error {
return nil return nil
} }
func (z *erasureServerPools) BackendInfo() (b BackendInfo) { func (z *erasureServerPools) BackendInfo() (b madmin.BackendInfo) {
b.Type = BackendErasure b.Type = madmin.Erasure
scParity := globalStorageClass.GetParityForSC(storageclass.STANDARD) scParity := globalStorageClass.GetParityForSC(storageclass.STANDARD)
if scParity <= 0 { if scParity <= 0 {
@@ -327,6 +331,35 @@ func (z *erasureServerPools) BackendInfo() (b BackendInfo) {
return return
} }
func (z *erasureServerPools) LocalStorageInfo(ctx context.Context) (StorageInfo, []error) {
var storageInfo StorageInfo
storageInfos := make([]StorageInfo, len(z.serverPools))
storageInfosErrs := make([][]error, len(z.serverPools))
g := errgroup.WithNErrs(len(z.serverPools))
for index := range z.serverPools {
index := index
g.Go(func() error {
storageInfos[index], storageInfosErrs[index] = z.serverPools[index].LocalStorageInfo(ctx)
return nil
}, index)
}
// Wait for the go routines.
g.Wait()
storageInfo.Backend = z.BackendInfo()
for _, lstorageInfo := range storageInfos {
storageInfo.Disks = append(storageInfo.Disks, lstorageInfo.Disks...)
}
var errs []error
for i := range z.serverPools {
errs = append(errs, storageInfosErrs[i]...)
}
return storageInfo, errs
}
func (z *erasureServerPools) StorageInfo(ctx context.Context) (StorageInfo, []error) { func (z *erasureServerPools) StorageInfo(ctx context.Context) (StorageInfo, []error) {
var storageInfo StorageInfo var storageInfo StorageInfo
@@ -356,7 +389,7 @@ func (z *erasureServerPools) StorageInfo(ctx context.Context) (StorageInfo, []er
return storageInfo, errs return storageInfo, errs
} }
func (z *erasureServerPools) CrawlAndGetDataUsage(ctx context.Context, bf *bloomFilter, updates chan<- DataUsageInfo) error { func (z *erasureServerPools) NSScanner(ctx context.Context, bf *bloomFilter, updates chan<- DataUsageInfo) error {
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
@@ -371,12 +404,11 @@ func (z *erasureServerPools) CrawlAndGetDataUsage(ctx context.Context, bf *bloom
} }
if len(allBuckets) == 0 { if len(allBuckets) == 0 {
logger.Info(color.Green("data-crawl:") + " No buckets found, skipping crawl")
updates <- DataUsageInfo{} // no buckets found update data usage to reflect latest state updates <- DataUsageInfo{} // no buckets found update data usage to reflect latest state
return nil return nil
} }
// Crawl latest allBuckets first. // Scanner latest allBuckets first.
sort.Slice(allBuckets, func(i, j int) bool { sort.Slice(allBuckets, func(i, j int) bool {
return allBuckets[i].Created.After(allBuckets[j].Created) return allBuckets[i].Created.After(allBuckets[j].Created)
}) })
@@ -398,8 +430,8 @@ func (z *erasureServerPools) CrawlAndGetDataUsage(ctx context.Context, bf *bloom
mu.Unlock() mu.Unlock()
} }
}() }()
// Start crawler. Blocks until done. // Start scanner. Blocks until done.
err := erObj.crawlAndGetDataUsage(ctx, allBuckets, bf, updates) err := erObj.nsScanner(ctx, allBuckets, bf, updates)
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
mu.Lock() mu.Lock()
@@ -521,60 +553,129 @@ func (z *erasureServerPools) GetObjectNInfo(ctx context.Context, bucket, object
object = encodeDirObject(object) object = encodeDirObject(object)
for _, pool := range z.serverPools { if z.SinglePool() {
gr, err = pool.GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts) return z.serverPools[0].GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts)
if err != nil { }
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
continue var unlockOnDefer bool
var nsUnlocker = func() {}
defer func() {
if unlockOnDefer {
nsUnlocker()
}
}()
// Acquire lock
if lockType != noLock {
lock := z.NewNSLock(bucket, object)
switch lockType {
case writeLock:
ctx, err = lock.GetLock(ctx, globalOperationTimeout)
if err != nil {
return nil, err
}
nsUnlocker = lock.Unlock
case readLock:
ctx, err = lock.GetRLock(ctx, globalOperationTimeout)
if err != nil {
return nil, err
}
nsUnlocker = lock.RUnlock
}
unlockOnDefer = true
}
errs := make([]error, len(z.serverPools))
grs := make([]*GetObjectReader, len(z.serverPools))
lockType = noLock // do not take locks at lower levels
var wg sync.WaitGroup
for i, pool := range z.serverPools {
wg.Add(1)
go func(i int, pool *erasureSets) {
defer wg.Done()
grs[i], errs[i] = pool.GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts)
}(i, pool)
}
wg.Wait()
var found int = -1
for i, err := range errs {
if err == nil {
found = i
break
}
if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
for _, grr := range grs {
if grr != nil {
grr.Close()
}
} }
return gr, err return gr, err
} }
return gr, nil
} }
if found >= 0 {
return grs[found], nil
}
object = decodeDirObject(object)
if opts.VersionID != "" { if opts.VersionID != "" {
return gr, VersionNotFound{Bucket: bucket, Object: object, VersionID: opts.VersionID} return gr, VersionNotFound{Bucket: bucket, Object: object, VersionID: opts.VersionID}
} }
return gr, ObjectNotFound{Bucket: bucket, Object: object} return gr, ObjectNotFound{Bucket: bucket, Object: object}
} }
func (z *erasureServerPools) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) error {
if err := checkGetObjArgs(ctx, bucket, object); err != nil {
return err
}
object = encodeDirObject(object)
for _, pool := range z.serverPools {
if err := pool.GetObject(ctx, bucket, object, startOffset, length, writer, etag, opts); err != nil {
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
continue
}
return err
}
return nil
}
if opts.VersionID != "" {
return VersionNotFound{Bucket: bucket, Object: object, VersionID: opts.VersionID}
}
return ObjectNotFound{Bucket: bucket, Object: object}
}
func (z *erasureServerPools) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) { func (z *erasureServerPools) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
if err = checkGetObjArgs(ctx, bucket, object); err != nil { if err = checkGetObjArgs(ctx, bucket, object); err != nil {
return objInfo, err return objInfo, err
} }
object = encodeDirObject(object) object = encodeDirObject(object)
for _, pool := range z.serverPools {
objInfo, err = pool.GetObjectInfo(ctx, bucket, object, opts) if z.SinglePool() {
if err != nil { return z.serverPools[0].GetObjectInfo(ctx, bucket, object, opts)
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
continue
}
return objInfo, err
}
return objInfo, nil
} }
// Lock the object before reading.
lk := z.NewNSLock(bucket, object)
ctx, err = lk.GetRLock(ctx, globalOperationTimeout)
if err != nil {
return ObjectInfo{}, err
}
defer lk.RUnlock()
errs := make([]error, len(z.serverPools))
objInfos := make([]ObjectInfo, len(z.serverPools))
opts.NoLock = true // avoid taking locks at lower levels for multi-pool setups.
var wg sync.WaitGroup
for i, pool := range z.serverPools {
wg.Add(1)
go func(i int, pool *erasureSets) {
defer wg.Done()
objInfos[i], errs[i] = pool.GetObjectInfo(ctx, bucket, object, opts)
}(i, pool)
}
wg.Wait()
var found int = -1
for i, err := range errs {
if err == nil {
found = i
break
}
if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
// some errors such as MethodNotAllowed for delete marker
// should be returned upwards.
return objInfos[i], err
}
}
if found >= 0 {
return objInfos[found], nil
}
object = decodeDirObject(object) object = decodeDirObject(object)
if opts.VersionID != "" { if opts.VersionID != "" {
return objInfo, VersionNotFound{Bucket: bucket, Object: object, VersionID: opts.VersionID} return objInfo, VersionNotFound{Bucket: bucket, Object: object, VersionID: opts.VersionID}
@@ -595,7 +696,7 @@ func (z *erasureServerPools) PutObject(ctx context.Context, bucket string, objec
return z.serverPools[0].PutObject(ctx, bucket, object, data, opts) return z.serverPools[0].PutObject(ctx, bucket, object, data, opts)
} }
idx, err := z.getPoolIdx(ctx, bucket, object, opts, data.Size()) idx, err := z.getPoolIdx(ctx, bucket, object, data.Size())
if err != nil { if err != nil {
return ObjectInfo{}, err return ObjectInfo{}, err
} }
@@ -610,21 +711,17 @@ func (z *erasureServerPools) DeleteObject(ctx context.Context, bucket string, ob
} }
object = encodeDirObject(object) object = encodeDirObject(object)
if z.SinglePool() { if z.SinglePool() {
return z.serverPools[0].DeleteObject(ctx, bucket, object, opts) return z.serverPools[0].DeleteObject(ctx, bucket, object, opts)
} }
for _, pool := range z.serverPools {
objInfo, err = pool.DeleteObject(ctx, bucket, object, opts) // We don't know the size here set 1GiB atleast.
if err == nil { idx, err := z.getPoolIdx(ctx, bucket, object, 1<<30)
return objInfo, nil if err != nil {
} return objInfo, err
if err != nil && !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
break
}
} }
return objInfo, err return z.serverPools[idx].DeleteObject(ctx, bucket, object, opts)
} }
func (z *erasureServerPools) DeleteObjects(ctx context.Context, bucket string, objects []ObjectToDelete, opts ObjectOptions) ([]DeletedObject, []error) { func (z *erasureServerPools) DeleteObjects(ctx context.Context, bucket string, objects []ObjectToDelete, opts ObjectOptions) ([]DeletedObject, []error) {
@@ -638,9 +735,28 @@ func (z *erasureServerPools) DeleteObjects(ctx context.Context, bucket string, o
objSets.Add(objects[i].ObjectName) objSets.Add(objects[i].ObjectName)
} }
poolObjIdxMap := map[int][]ObjectToDelete{}
origIndexMap := map[int][]int{}
if !z.SinglePool() {
for j, obj := range objects {
idx, err := z.getPoolIdx(ctx, bucket, obj.ObjectName, 1<<30)
if err != nil {
// Unhandled errors return right here.
for i := range derrs {
derrs[i] = err
}
return dobjects, derrs
}
poolObjIdxMap[idx] = append(poolObjIdxMap[idx], obj)
origIndexMap[idx] = append(origIndexMap[idx], j)
}
}
var err error
// Acquire a bulk write lock across 'objects' // Acquire a bulk write lock across 'objects'
multiDeleteLock := z.NewNSLock(bucket, objSets.ToSlice()...) multiDeleteLock := z.NewNSLock(bucket, objSets.ToSlice()...)
if err := multiDeleteLock.GetLock(ctx, globalOperationTimeout); err != nil { ctx, err = multiDeleteLock.GetLock(ctx, globalOperationTimeout)
if err != nil {
for i := range derrs { for i := range derrs {
derrs[i] = err derrs[i] = err
} }
@@ -652,13 +768,15 @@ func (z *erasureServerPools) DeleteObjects(ctx context.Context, bucket string, o
return z.serverPools[0].DeleteObjects(ctx, bucket, objects, opts) return z.serverPools[0].DeleteObjects(ctx, bucket, objects, opts)
} }
for _, pool := range z.serverPools { for idx, pool := range z.serverPools {
deletedObjects, errs := pool.DeleteObjects(ctx, bucket, objects, opts) objs := poolObjIdxMap[idx]
orgIndexes := origIndexMap[idx]
deletedObjects, errs := pool.DeleteObjects(ctx, bucket, objs, opts)
for i, derr := range errs { for i, derr := range errs {
if derr != nil { if derr != nil {
derrs[i] = derr derrs[orgIndexes[i]] = derr
} }
dobjects[i] = deletedObjects[i] dobjects[orgIndexes[i]] = deletedObjects[i]
} }
} }
return dobjects, derrs return dobjects, derrs
@@ -670,7 +788,7 @@ func (z *erasureServerPools) CopyObject(ctx context.Context, srcBucket, srcObjec
cpSrcDstSame := isStringEqual(pathJoin(srcBucket, srcObject), pathJoin(dstBucket, dstObject)) cpSrcDstSame := isStringEqual(pathJoin(srcBucket, srcObject), pathJoin(dstBucket, dstObject))
poolIdx, err := z.getPoolIdx(ctx, dstBucket, dstObject, dstOpts, srcInfo.Size) poolIdx, err := z.getPoolIdx(ctx, dstBucket, dstObject, srcInfo.Size)
if err != nil { if err != nil {
return objInfo, err return objInfo, err
} }
@@ -739,7 +857,7 @@ func (z *erasureServerPools) ListObjectVersions(ctx context.Context, bucket, pre
Bucket: bucket, Bucket: bucket,
Prefix: prefix, Prefix: prefix,
Separator: delimiter, Separator: delimiter,
Limit: maxKeys, Limit: maxKeysPlusOne(maxKeys, marker != ""),
Marker: marker, Marker: marker,
InclDeleted: true, InclDeleted: true,
AskDisks: globalAPIConfig.getListQuorum(), AskDisks: globalAPIConfig.getListQuorum(),
@@ -759,6 +877,11 @@ func (z *erasureServerPools) ListObjectVersions(ctx context.Context, bucket, pre
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
return loi, err return loi, err
} }
if versionMarker == "" {
// If we are not looking for a specific version skip it.
marker, _ = parseMarker(marker)
merged.forwardPast(marker)
}
objects := merged.fileInfoVersions(bucket, prefix, delimiter, versionMarker) objects := merged.fileInfoVersions(bucket, prefix, delimiter, versionMarker)
loi.IsTruncated = err == nil && len(objects) > 0 loi.IsTruncated = err == nil && len(objects) > 0
if maxKeys > 0 && len(objects) > maxKeys { if maxKeys > 0 && len(objects) > maxKeys {
@@ -766,7 +889,7 @@ func (z *erasureServerPools) ListObjectVersions(ctx context.Context, bucket, pre
loi.IsTruncated = true loi.IsTruncated = true
} }
for _, obj := range objects { for _, obj := range objects {
if obj.IsDir && delimiter != "" { if obj.IsDir && obj.ModTime.IsZero() && delimiter != "" {
loi.Prefixes = append(loi.Prefixes, obj.Name) loi.Prefixes = append(loi.Prefixes, obj.Name)
} else { } else {
loi.Objects = append(loi.Objects, obj) loi.Objects = append(loi.Objects, obj)
@@ -780,6 +903,16 @@ func (z *erasureServerPools) ListObjectVersions(ctx context.Context, bucket, pre
return loi, nil return loi, nil
} }
func maxKeysPlusOne(maxKeys int, addOne bool) int {
if maxKeys < 0 || maxKeys > maxObjectList {
maxKeys = maxObjectList
}
if addOne {
maxKeys++
}
return maxKeys
}
func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsInfo, error) { func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsInfo, error) {
var loi ListObjectsInfo var loi ListObjectsInfo
@@ -787,7 +920,7 @@ func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, ma
Bucket: bucket, Bucket: bucket,
Prefix: prefix, Prefix: prefix,
Separator: delimiter, Separator: delimiter,
Limit: maxKeys, Limit: maxKeysPlusOne(maxKeys, marker != ""),
Marker: marker, Marker: marker,
InclDeleted: false, InclDeleted: false,
AskDisks: globalAPIConfig.getListQuorum(), AskDisks: globalAPIConfig.getListQuorum(),
@@ -796,6 +929,8 @@ func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, ma
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return loi, err return loi, err
} }
marker, _ = parseMarker(marker)
merged.forwardPast(marker)
// Default is recursive, if delimiter is set then list non recursive. // Default is recursive, if delimiter is set then list non recursive.
objects := merged.fileInfos(bucket, prefix, delimiter) objects := merged.fileInfos(bucket, prefix, delimiter)
@@ -805,7 +940,7 @@ func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, ma
loi.IsTruncated = true loi.IsTruncated = true
} }
for _, obj := range objects { for _, obj := range objects {
if obj.IsDir && delimiter != "" { if obj.IsDir && obj.ModTime.IsZero() && delimiter != "" {
loi.Prefixes = append(loi.Prefixes, obj.Name) loi.Prefixes = append(loi.Prefixes, obj.Name)
} else { } else {
loi.Objects = append(loi.Objects, obj) loi.Objects = append(loi.Objects, obj)
@@ -854,7 +989,7 @@ func (z *erasureServerPools) NewMultipartUpload(ctx context.Context, bucket, obj
} }
// We don't know the exact size, so we ask for at least 1GiB file. // We don't know the exact size, so we ask for at least 1GiB file.
idx, err := z.getPoolIdx(ctx, bucket, object, opts, 1<<30) idx, err := z.getPoolIdx(ctx, bucket, object, 1<<30)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -869,7 +1004,7 @@ func (z *erasureServerPools) CopyObjectPart(ctx context.Context, srcBucket, srcO
} }
return z.PutObjectPart(ctx, destBucket, destObject, uploadID, partID, return z.PutObjectPart(ctx, destBucket, destObject, uploadID, partID,
NewPutObjReader(srcInfo.Reader, nil, nil), dstOpts) NewPutObjReader(srcInfo.Reader), dstOpts)
} }
// PutObjectPart - writes part of an object to hashedSet based on the object name. // PutObjectPart - writes part of an object to hashedSet based on the object name.
@@ -1115,22 +1250,24 @@ func (z *erasureServerPools) DeleteBucket(ctx context.Context, bucket string, fo
// data is not distributed across sets. // data is not distributed across sets.
// Errors are logged but individual disk failures are not returned. // Errors are logged but individual disk failures are not returned.
func (z *erasureServerPools) deleteAll(ctx context.Context, bucket, prefix string) { func (z *erasureServerPools) deleteAll(ctx context.Context, bucket, prefix string) {
var wg sync.WaitGroup
for _, servers := range z.serverPools { for _, servers := range z.serverPools {
for _, set := range servers.sets { for _, set := range servers.sets {
for _, disk := range set.getDisks() { set.deleteAll(ctx, bucket, prefix)
if disk == nil { }
continue }
} }
wg.Add(1)
go func(disk StorageAPI) { // renameAll will rename bucket+prefix unconditionally across all disks to
defer wg.Done() // minioMetaTmpBucket + unique uuid,
disk.Delete(ctx, bucket, prefix, true) // Note that set distribution is ignored so it should only be used in cases where
}(disk) // data is not distributed across sets. Errors are logged but individual
} // disk failures are not returned.
func (z *erasureServerPools) renameAll(ctx context.Context, bucket, prefix string) {
for _, servers := range z.serverPools {
for _, set := range servers.sets {
set.renameAll(ctx, bucket, prefix)
} }
} }
wg.Wait()
} }
// This function is used to undo a successful DeleteBucket operation. // This function is used to undo a successful DeleteBucket operation.
@@ -1180,9 +1317,11 @@ func (z *erasureServerPools) ListBuckets(ctx context.Context) (buckets []BucketI
} }
func (z *erasureServerPools) HealFormat(ctx context.Context, dryRun bool) (madmin.HealResultItem, error) { func (z *erasureServerPools) HealFormat(ctx context.Context, dryRun bool) (madmin.HealResultItem, error) {
var err error
// Acquire lock on format.json // Acquire lock on format.json
formatLock := z.NewNSLock(minioMetaBucket, formatConfigFile) formatLock := z.NewNSLock(minioMetaBucket, formatConfigFile)
if err := formatLock.GetLock(ctx, globalOperationTimeout); err != nil { ctx, err = formatLock.GetLock(ctx, globalOperationTimeout)
if err != nil {
return madmin.HealResultItem{}, err return madmin.HealResultItem{}, err
} }
defer formatLock.Unlock() defer formatLock.Unlock()
@@ -1312,90 +1451,99 @@ func (z *erasureServerPools) Walk(ctx context.Context, bucket, prefix string, re
type HealObjectFn func(bucket, object, versionID string) error type HealObjectFn func(bucket, object, versionID string) error
func (z *erasureServerPools) HealObjects(ctx context.Context, bucket, prefix string, opts madmin.HealOpts, healObject HealObjectFn) error { func (z *erasureServerPools) HealObjects(ctx context.Context, bucket, prefix string, opts madmin.HealOpts, healObject HealObjectFn) error {
// If listing did not return any entries upon first attempt, we errCh := make(chan error)
// return `ObjectNotFound`, to indicate the caller for any ctx, cancel := context.WithCancel(ctx)
// actions they may want to take as if `prefix` is missing. go func() {
err := toObjectErr(errFileNotFound, bucket, prefix) defer close(errCh)
for _, erasureSet := range z.serverPools { defer cancel()
for _, set := range erasureSet.sets {
var entryChs []FileInfoVersionsCh for _, erasureSet := range z.serverPools {
var mu sync.Mutex
var wg sync.WaitGroup var wg sync.WaitGroup
for _, disk := range set.getOnlineDisks() { for _, set := range erasureSet.sets {
disk := disk set := set
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
entryCh, err := disk.WalkVersions(ctx, bucket, prefix, "", true, ctx.Done())
if err != nil { disks, _ := set.getOnlineDisksWithHealing()
// Disk walk returned error, ignore it. if len(disks) == 0 {
errCh <- errors.New("HealObjects: No non-healing disks found")
cancel()
return
}
healEntry := func(entry metaCacheEntry) {
if entry.isDir() {
return
}
// We might land at .metacache, .trash, .multipart
// no need to heal them skip, only when bucket
// is '.minio.sys'
if bucket == minioMetaBucket {
if wildcard.Match("buckets/*/.metacache/*", entry.name) {
return
}
if wildcard.Match("tmp/.trash/*", entry.name) {
return
}
if wildcard.Match("multipart/*", entry.name) {
return
}
}
fivs, err := entry.fileInfoVersions(bucket)
if err != nil {
errCh <- err
cancel()
return
}
waitForLowHTTPReq(globalHealConfig.IOCount, globalHealConfig.Sleep)
for _, version := range fivs.Versions {
if err := healObject(bucket, version.Name, version.VersionID); err != nil {
errCh <- err
cancel()
return
}
}
}
// How to resolve partial results.
resolver := metadataResolutionParams{
dirQuorum: 1,
objQuorum: 1,
bucket: bucket,
}
if err := listPathRaw(ctx, listPathRawOptions{
disks: disks,
bucket: bucket,
path: baseDirFromPrefix(prefix),
recursive: true,
forwardTo: "",
minDisks: 1,
reportNotFound: false,
agreed: healEntry,
partial: func(entries metaCacheEntries, nAgreed int, errs []error) {
entry, ok := entries.resolve(&resolver)
if ok {
healEntry(*entry)
}
},
finished: nil,
}); err != nil {
cancel()
return return
} }
mu.Lock()
entryChs = append(entryChs, FileInfoVersionsCh{
Ch: entryCh,
})
mu.Unlock()
}() }()
} }
wg.Wait() wg.Wait()
entriesValid := make([]bool, len(entryChs))
entries := make([]FileInfoVersions, len(entryChs))
for {
entry, quorumCount, ok := lexicallySortedEntryVersions(entryChs, entries, entriesValid)
if !ok {
break
}
// Remove empty directories if found - they have no meaning.
// Can be left over from highly concurrent put/remove.
if quorumCount > set.setDriveCount/2 && entry.IsEmptyDir {
if !opts.DryRun && opts.Remove {
set.deleteEmptyDir(ctx, bucket, entry.Name)
}
}
// Indicate that first attempt was a success and subsequent loop
// knows that its not our first attempt at 'prefix'
err = nil
if quorumCount == set.setDriveCount && opts.ScanMode == madmin.HealNormalScan {
continue
}
for _, version := range entry.Versions {
if err := healObject(bucket, version.Name, version.VersionID); err != nil {
return toObjectErr(err, bucket, version.Name)
}
}
}
} }
} }()
return <-errCh
return err
} }
func (z *erasureServerPools) HealObject(ctx context.Context, bucket, object, versionID string, opts madmin.HealOpts) (madmin.HealResultItem, error) { func (z *erasureServerPools) HealObject(ctx context.Context, bucket, object, versionID string, opts madmin.HealOpts) (madmin.HealResultItem, error) {
object = encodeDirObject(object) object = encodeDirObject(object)
lk := z.NewNSLock(bucket, object)
if bucket == minioMetaBucket {
// For .minio.sys bucket heals we should hold write locks.
if err := lk.GetLock(ctx, globalOperationTimeout); err != nil {
return madmin.HealResultItem{}, err
}
defer lk.Unlock()
} else {
// Lock the object before healing. Use read lock since healing
// will only regenerate parts & xl.meta of outdated disks.
if err := lk.GetRLock(ctx, globalOperationTimeout); err != nil {
return madmin.HealResultItem{}, err
}
defer lk.RUnlock()
}
for _, pool := range z.serverPools { for _, pool := range z.serverPools {
result, err := pool.HealObject(ctx, bucket, object, versionID, opts) result, err := pool.HealObject(ctx, bucket, object, versionID, opts)
result.Object = decodeDirObject(result.Object) result.Object = decodeDirObject(result.Object)
@@ -1426,18 +1574,18 @@ func (z *erasureServerPools) GetMetrics(ctx context.Context) (*BackendMetrics, e
return &BackendMetrics{}, NotImplemented{} return &BackendMetrics{}, NotImplemented{}
} }
func (z *erasureServerPools) getPoolAndSet(id string) (int, int, error) { func (z *erasureServerPools) getPoolAndSet(id string) (poolIdx, setIdx, diskIdx int, err error) {
for poolIdx := range z.serverPools { for poolIdx := range z.serverPools {
format := z.serverPools[poolIdx].format format := z.serverPools[poolIdx].format
for setIdx, set := range format.Erasure.Sets { for setIdx, set := range format.Erasure.Sets {
for _, diskID := range set { for i, diskID := range set {
if diskID == id { if diskID == id {
return poolIdx, setIdx, nil return poolIdx, setIdx, i, nil
} }
} }
} }
} }
return 0, 0, fmt.Errorf("DiskID(%s) %w", id, errDiskNotFound) return -1, -1, -1, fmt.Errorf("DiskID(%s) %w", id, errDiskNotFound)
} }
// HealthOptions takes input options to return sepcific information // HealthOptions takes input options to return sepcific information
@@ -1455,6 +1603,40 @@ type HealthResult struct {
WriteQuorum int WriteQuorum int
} }
// ReadHealth returns if the cluster can serve read requests
func (z *erasureServerPools) ReadHealth(ctx context.Context) bool {
erasureSetUpCount := make([][]int, len(z.serverPools))
for i := range z.serverPools {
erasureSetUpCount[i] = make([]int, len(z.serverPools[i].sets))
}
diskIDs := globalNotificationSys.GetLocalDiskIDs(ctx)
diskIDs = append(diskIDs, getLocalDiskIDs(z))
for _, localDiskIDs := range diskIDs {
for _, id := range localDiskIDs {
poolIdx, setIdx, _, err := z.getPoolAndSet(id)
if err != nil {
logger.LogIf(ctx, err)
continue
}
erasureSetUpCount[poolIdx][setIdx]++
}
}
b := z.BackendInfo()
readQuorum := b.StandardSCData[0]
for poolIdx := range erasureSetUpCount {
for setIdx := range erasureSetUpCount[poolIdx] {
if erasureSetUpCount[poolIdx][setIdx] < readQuorum {
return false
}
}
}
return true
}
// Health - returns current status of the object layer health, // Health - returns current status of the object layer health,
// provides if write access exists across sets, additionally // provides if write access exists across sets, additionally
// can be used to query scenarios if health may be lost // can be used to query scenarios if health may be lost
@@ -1472,7 +1654,7 @@ func (z *erasureServerPools) Health(ctx context.Context, opts HealthOptions) Hea
for _, localDiskIDs := range diskIDs { for _, localDiskIDs := range diskIDs {
for _, id := range localDiskIDs { for _, id := range localDiskIDs {
poolIdx, setIdx, err := z.getPoolAndSet(id) poolIdx, setIdx, _, err := z.getPoolAndSet(id)
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
continue continue
@@ -1495,7 +1677,7 @@ func (z *erasureServerPools) Health(ctx context.Context, opts HealthOptions) Hea
// we need to tell healthy status as 'false' so that this server // we need to tell healthy status as 'false' so that this server
// is not taken down for maintenance // is not taken down for maintenance
var err error var err error
aggHealStateResult, err = getAggregatedBackgroundHealState(ctx) aggHealStateResult, err = getAggregatedBackgroundHealState(ctx, nil)
if err != nil { if err != nil {
logger.LogIf(logger.SetReqInfo(ctx, reqInfo), fmt.Errorf("Unable to verify global heal status: %w", err)) logger.LogIf(logger.SetReqInfo(ctx, reqInfo), fmt.Errorf("Unable to verify global heal status: %w", err))
return HealthResult{ return HealthResult{
@@ -1542,62 +1724,35 @@ func (z *erasureServerPools) Health(ctx context.Context, opts HealthOptions) Hea
} }
// PutObjectTags - replace or add tags to an existing object // PutObjectTags - replace or add tags to an existing object
func (z *erasureServerPools) PutObjectTags(ctx context.Context, bucket, object string, tags string, opts ObjectOptions) error { func (z *erasureServerPools) PutObjectTags(ctx context.Context, bucket, object string, tags string, opts ObjectOptions) (ObjectInfo, error) {
object = encodeDirObject(object) object = encodeDirObject(object)
if z.SinglePool() { if z.SinglePool() {
return z.serverPools[0].PutObjectTags(ctx, bucket, object, tags, opts) return z.serverPools[0].PutObjectTags(ctx, bucket, object, tags, opts)
} }
for _, pool := range z.serverPools { // We don't know the size here set 1GiB atleast.
err := pool.PutObjectTags(ctx, bucket, object, tags, opts) idx, err := z.getPoolIdx(ctx, bucket, object, 1<<30)
if err != nil { if err != nil {
if isErrObjectNotFound(err) || isErrVersionNotFound(err) { return ObjectInfo{}, err
continue
}
return err
}
return nil
}
if opts.VersionID != "" {
return VersionNotFound{
Bucket: bucket,
Object: object,
VersionID: opts.VersionID,
}
}
return ObjectNotFound{
Bucket: bucket,
Object: object,
} }
return z.serverPools[idx].PutObjectTags(ctx, bucket, object, tags, opts)
} }
// DeleteObjectTags - delete object tags from an existing object // DeleteObjectTags - delete object tags from an existing object
func (z *erasureServerPools) DeleteObjectTags(ctx context.Context, bucket, object string, opts ObjectOptions) error { func (z *erasureServerPools) DeleteObjectTags(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
object = encodeDirObject(object) object = encodeDirObject(object)
if z.SinglePool() { if z.SinglePool() {
return z.serverPools[0].DeleteObjectTags(ctx, bucket, object, opts) return z.serverPools[0].DeleteObjectTags(ctx, bucket, object, opts)
} }
for _, pool := range z.serverPools {
err := pool.DeleteObjectTags(ctx, bucket, object, opts) // We don't know the size here set 1GiB atleast.
if err != nil { idx, err := z.getPoolIdx(ctx, bucket, object, 1<<30)
if isErrObjectNotFound(err) || isErrVersionNotFound(err) { if err != nil {
continue return ObjectInfo{}, err
}
return err
}
return nil
}
if opts.VersionID != "" {
return VersionNotFound{
Bucket: bucket,
Object: object,
VersionID: opts.VersionID,
}
}
return ObjectNotFound{
Bucket: bucket,
Object: object,
} }
return z.serverPools[idx].DeleteObjectTags(ctx, bucket, object, opts)
} }
// GetObjectTags - get object tags from an existing object // GetObjectTags - get object tags from an existing object
@@ -1606,25 +1761,12 @@ func (z *erasureServerPools) GetObjectTags(ctx context.Context, bucket, object s
if z.SinglePool() { if z.SinglePool() {
return z.serverPools[0].GetObjectTags(ctx, bucket, object, opts) return z.serverPools[0].GetObjectTags(ctx, bucket, object, opts)
} }
for _, pool := range z.serverPools {
tags, err := pool.GetObjectTags(ctx, bucket, object, opts) // We don't know the size here set 1GiB atleast.
if err != nil { idx, err := z.getPoolIdx(ctx, bucket, object, 1<<30)
if isErrObjectNotFound(err) || isErrVersionNotFound(err) { if err != nil {
continue return nil, err
}
return tags, err
}
return tags, nil
}
if opts.VersionID != "" {
return nil, VersionNotFound{
Bucket: bucket,
Object: object,
VersionID: opts.VersionID,
}
}
return nil, ObjectNotFound{
Bucket: bucket,
Object: object,
} }
return z.serverPools[idx].GetObjectTags(ctx, bucket, object, opts)
} }
+120 -201
View File
@@ -18,10 +18,10 @@ package cmd
import ( import (
"context" "context"
"encoding/binary"
"errors" "errors"
"fmt" "fmt"
"hash/crc32" "hash/crc32"
"io"
"math/rand" "math/rand"
"net/http" "net/http"
"sort" "sort"
@@ -29,6 +29,7 @@ import (
"time" "time"
"github.com/dchest/siphash" "github.com/dchest/siphash"
"github.com/dustin/go-humanize"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/minio-go/v7/pkg/tags" "github.com/minio/minio-go/v7/pkg/tags"
@@ -83,7 +84,7 @@ type erasureSets struct {
setCount, setDriveCount int setCount, setDriveCount int
defaultParityCount int defaultParityCount int
poolNumber int poolIndex int
disksConnectEvent chan diskConnectInfo disksConnectEvent chan diskConnectInfo
@@ -217,7 +218,7 @@ func (s *erasureSets) connectDisks() {
} }
return return
} }
if disk.IsLocal() && disk.Healing() { if disk.IsLocal() && disk.Healing() != nil {
globalBackgroundHealState.pushHealLocalDisks(disk.Endpoint()) globalBackgroundHealState.pushHealLocalDisks(disk.Endpoint())
logger.Info(fmt.Sprintf("Found the drive %s that needs healing, attempting to heal...", disk)) logger.Info(fmt.Sprintf("Found the drive %s that needs healing, attempting to heal...", disk))
} }
@@ -246,6 +247,7 @@ func (s *erasureSets) connectDisks() {
disk.SetDiskID(format.Erasure.This) disk.SetDiskID(format.Erasure.This)
s.erasureDisks[setIndex][diskIndex] = disk s.erasureDisks[setIndex][diskIndex] = disk
} }
disk.SetDiskLoc(s.poolIndex, setIndex, diskIndex)
s.endpointStrings[setIndex*s.setDriveCount+diskIndex] = disk.String() s.endpointStrings[setIndex*s.setDriveCount+diskIndex] = disk.String()
s.erasureDisksMu.Unlock() s.erasureDisksMu.Unlock()
go func(setIndex int) { go func(setIndex int) {
@@ -331,7 +333,7 @@ func (s *erasureSets) GetDisks(setIndex int) func() []StorageAPI {
const defaultMonitorConnectEndpointInterval = defaultMonitorNewDiskInterval + time.Second*5 const defaultMonitorConnectEndpointInterval = defaultMonitorNewDiskInterval + time.Second*5
// Initialize new set of erasure coded sets. // Initialize new set of erasure coded sets.
func newErasureSets(ctx context.Context, endpoints Endpoints, storageDisks []StorageAPI, format *formatErasureV3, defaultParityCount int) (*erasureSets, error) { func newErasureSets(ctx context.Context, endpoints Endpoints, storageDisks []StorageAPI, format *formatErasureV3, defaultParityCount, poolIdx int) (*erasureSets, error) {
setCount := len(format.Erasure.Sets) setCount := len(format.Erasure.Sets)
setDriveCount := len(format.Erasure.Sets[0]) setDriveCount := len(format.Erasure.Sets[0])
@@ -353,13 +355,17 @@ func newErasureSets(ctx context.Context, endpoints Endpoints, storageDisks []Sto
distributionAlgo: format.Erasure.DistributionAlgo, distributionAlgo: format.Erasure.DistributionAlgo,
deploymentID: uuid.MustParse(format.ID), deploymentID: uuid.MustParse(format.ID),
mrfOperations: make(map[healSource]int), mrfOperations: make(map[healSource]int),
poolIndex: poolIdx,
} }
mutex := newNSLock(globalIsDistErasure) mutex := newNSLock(globalIsDistErasure)
// Number of buffers, max 2GB
n := (2 * humanize.GiByte) / (blockSizeV2 * 2)
// Initialize byte pool once for all sets, bpool size is set to // Initialize byte pool once for all sets, bpool size is set to
// setCount * setDriveCount with each memory upto blockSizeV1. // setCount * setDriveCount with each memory upto blockSizeV2.
bp := bpool.NewBytePoolCap(setCount*setDriveCount, blockSizeV1, blockSizeV1*2) bp := bpool.NewBytePoolCap(n, blockSizeV2, blockSizeV2*2)
for i := 0; i < setCount; i++ { for i := 0; i < setCount; i++ {
s.erasureDisks[i] = make([]StorageAPI, setDriveCount) s.erasureDisks[i] = make([]StorageAPI, setDriveCount)
@@ -393,27 +399,36 @@ func newErasureSets(ctx context.Context, endpoints Endpoints, storageDisks []Sto
if err != nil { if err != nil {
continue continue
} }
disk.SetDiskLoc(s.poolIndex, m, n)
s.endpointStrings[m*setDriveCount+n] = disk.String() s.endpointStrings[m*setDriveCount+n] = disk.String()
s.erasureDisks[m][n] = disk s.erasureDisks[m][n] = disk
} }
// Initialize erasure objects for a given set. // Initialize erasure objects for a given set.
s.sets[i] = &erasureObjects{ s.sets[i] = &erasureObjects{
setNumber: i, setIndex: i,
setDriveCount: setDriveCount, poolIndex: poolIdx,
defaultParityCount: defaultParityCount, setDriveCount: setDriveCount,
getDisks: s.GetDisks(i), defaultParityCount: defaultParityCount,
getLockers: s.GetLockers(i), getDisks: s.GetDisks(i),
getEndpoints: s.GetEndpoints(i), getLockers: s.GetLockers(i),
nsMutex: mutex, getEndpoints: s.GetEndpoints(i),
bp: bp, deletedCleanupSleeper: newDynamicSleeper(10, 10*time.Second),
mrfOpCh: make(chan partialOperation, 10000), nsMutex: mutex,
bp: bp,
mrfOpCh: make(chan partialOperation, 10000),
} }
} }
// cleanup ".trash/" folder every 30 minutes with sufficient sleep cycles.
const deletedObjectsCleanupInterval = 10 * time.Minute
// start cleanup stale uploads go-routine. // start cleanup stale uploads go-routine.
go s.cleanupStaleUploads(ctx, GlobalStaleUploadsCleanupInterval, GlobalStaleUploadsExpiry) go s.cleanupStaleUploads(ctx, GlobalStaleUploadsCleanupInterval, GlobalStaleUploadsExpiry)
// start cleanup of deleted objects.
go s.cleanupDeletedObjects(ctx, deletedObjectsCleanupInterval)
// Start the disk monitoring and connect routine. // Start the disk monitoring and connect routine.
go s.monitorAndConnectEndpoints(ctx, defaultMonitorConnectEndpointInterval) go s.monitorAndConnectEndpoints(ctx, defaultMonitorConnectEndpointInterval)
go s.maintainMRFList() go s.maintainMRFList()
@@ -422,15 +437,37 @@ func newErasureSets(ctx context.Context, endpoints Endpoints, storageDisks []Sto
return s, nil return s, nil
} }
func (s *erasureSets) cleanupStaleUploads(ctx context.Context, cleanupInterval, expiry time.Duration) { func (s *erasureSets) cleanupDeletedObjects(ctx context.Context, cleanupInterval time.Duration) {
ticker := time.NewTicker(cleanupInterval) timer := time.NewTimer(cleanupInterval)
defer ticker.Stop() defer timer.Stop()
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case <-ticker.C: case <-timer.C:
// Reset for the next interval
timer.Reset(cleanupInterval)
for _, set := range s.sets {
set.cleanupDeletedObjects(ctx)
}
}
}
}
func (s *erasureSets) cleanupStaleUploads(ctx context.Context, cleanupInterval, expiry time.Duration) {
timer := time.NewTimer(cleanupInterval)
defer timer.Stop()
for {
select {
case <-ctx.Done():
return
case <-timer.C:
// Reset for the next interval
timer.Reset(cleanupInterval)
for _, set := range s.sets { for _, set := range s.sets {
set.cleanupStaleUploads(ctx, expiry) set.cleanupStaleUploads(ctx, expiry)
} }
@@ -446,7 +483,7 @@ type auditObjectOp struct {
Disks []string `json:"disks"` Disks []string `json:"disks"`
} }
func auditObjectErasureSet(ctx context.Context, object string, set *erasureObjects, poolNum int) { func auditObjectErasureSet(ctx context.Context, object string, set *erasureObjects) {
if len(logger.AuditTargets) == 0 { if len(logger.AuditTargets) == 0 {
return return
} }
@@ -454,8 +491,8 @@ func auditObjectErasureSet(ctx context.Context, object string, set *erasureObjec
object = decodeDirObject(object) object = decodeDirObject(object)
op := auditObjectOp{ op := auditObjectOp{
Pool: poolNum + 1, Pool: set.poolIndex + 1,
Set: set.setNumber + 1, Set: set.setIndex + 1,
Disks: set.getEndpoints(), Disks: set.getEndpoints(),
} }
@@ -503,7 +540,7 @@ func (s *erasureSets) StorageUsageInfo(ctx context.Context) StorageInfo {
storageUsageInfo := func() StorageInfo { storageUsageInfo := func() StorageInfo {
var storageInfo StorageInfo var storageInfo StorageInfo
storageInfos := make([]StorageInfo, len(s.sets)) storageInfos := make([]StorageInfo, len(s.sets))
storageInfo.Backend.Type = BackendErasure storageInfo.Backend.Type = madmin.Erasure
g := errgroup.WithNErrs(len(s.sets)) g := errgroup.WithNErrs(len(s.sets))
for index := range s.sets { for index := range s.sets {
@@ -538,6 +575,37 @@ func (s *erasureSets) StorageUsageInfo(ctx context.Context) StorageInfo {
// StorageInfo - combines output of StorageInfo across all erasure coded object sets. // StorageInfo - combines output of StorageInfo across all erasure coded object sets.
func (s *erasureSets) StorageInfo(ctx context.Context) (StorageInfo, []error) { func (s *erasureSets) StorageInfo(ctx context.Context) (StorageInfo, []error) {
var storageInfo madmin.StorageInfo
storageInfos := make([]madmin.StorageInfo, len(s.sets))
storageInfoErrs := make([][]error, len(s.sets))
g := errgroup.WithNErrs(len(s.sets))
for index := range s.sets {
index := index
g.Go(func() error {
storageInfos[index], storageInfoErrs[index] = s.sets[index].StorageInfo(ctx)
return nil
}, index)
}
// Wait for the go routines.
g.Wait()
for _, lstorageInfo := range storageInfos {
storageInfo.Disks = append(storageInfo.Disks, lstorageInfo.Disks...)
}
var errs []error
for i := range s.sets {
errs = append(errs, storageInfoErrs[i]...)
}
return storageInfo, errs
}
// StorageInfo - combines output of StorageInfo across all erasure coded object sets.
func (s *erasureSets) LocalStorageInfo(ctx context.Context) (StorageInfo, []error) {
var storageInfo StorageInfo var storageInfo StorageInfo
storageInfos := make([]StorageInfo, len(s.sets)) storageInfos := make([]StorageInfo, len(s.sets))
@@ -547,7 +615,7 @@ func (s *erasureSets) StorageInfo(ctx context.Context) (StorageInfo, []error) {
for index := range s.sets { for index := range s.sets {
index := index index := index
g.Go(func() error { g.Go(func() error {
storageInfos[index], storageInfoErrs[index] = s.sets[index].StorageInfo(ctx) storageInfos[index], storageInfoErrs[index] = s.sets[index].LocalStorageInfo(ctx)
return nil return nil
}, index) }, index)
} }
@@ -630,9 +698,11 @@ func sipHashMod(key string, cardinality int, id [16]byte) int {
if cardinality <= 0 { if cardinality <= 0 {
return -1 return -1
} }
sip := siphash.New(id[:]) // use the faster version as per siphash docs
sip.Write([]byte(key)) // https://github.com/dchest/siphash#usage
return int(sip.Sum64() % uint64(cardinality)) k0, k1 := binary.LittleEndian.Uint64(id[0:8]), binary.LittleEndian.Uint64(id[8:16])
sum64 := siphash.Hash(k0, k1, []byte(key))
return int(sum64 % uint64(cardinality))
} }
func crcHashMod(key string, cardinality int) int { func crcHashMod(key string, cardinality int) int {
@@ -772,17 +842,10 @@ func (s *erasureSets) ListBuckets(ctx context.Context) (buckets []BucketInfo, er
// GetObjectNInfo - returns object info and locked object ReadCloser // GetObjectNInfo - returns object info and locked object ReadCloser
func (s *erasureSets) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) { func (s *erasureSets) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) {
set := s.getHashedSet(object) set := s.getHashedSet(object)
auditObjectErasureSet(ctx, object, set, s.poolNumber) auditObjectErasureSet(ctx, object, set)
return set.GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts) return set.GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts)
} }
// GetObject - reads an object from the hashedSet based on the object name.
func (s *erasureSets) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) error {
set := s.getHashedSet(object)
auditObjectErasureSet(ctx, object, set, s.poolNumber)
return set.GetObject(ctx, bucket, object, startOffset, length, writer, etag, opts)
}
func (s *erasureSets) parentDirIsObject(ctx context.Context, bucket, parent string) bool { func (s *erasureSets) parentDirIsObject(ctx context.Context, bucket, parent string) bool {
if parent == "." { if parent == "." {
return false return false
@@ -793,7 +856,7 @@ func (s *erasureSets) parentDirIsObject(ctx context.Context, bucket, parent stri
// PutObject - writes an object to hashedSet based on the object name. // PutObject - writes an object to hashedSet based on the object name.
func (s *erasureSets) PutObject(ctx context.Context, bucket string, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) { func (s *erasureSets) PutObject(ctx context.Context, bucket string, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) {
set := s.getHashedSet(object) set := s.getHashedSet(object)
auditObjectErasureSet(ctx, object, set, s.poolNumber) auditObjectErasureSet(ctx, object, set)
opts.ParentIsObject = s.parentDirIsObject opts.ParentIsObject = s.parentDirIsObject
return set.PutObject(ctx, bucket, object, data, opts) return set.PutObject(ctx, bucket, object, data, opts)
} }
@@ -801,14 +864,14 @@ func (s *erasureSets) PutObject(ctx context.Context, bucket string, object strin
// GetObjectInfo - reads object metadata from the hashedSet based on the object name. // GetObjectInfo - reads object metadata from the hashedSet based on the object name.
func (s *erasureSets) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) { func (s *erasureSets) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
set := s.getHashedSet(object) set := s.getHashedSet(object)
auditObjectErasureSet(ctx, object, set, s.poolNumber) auditObjectErasureSet(ctx, object, set)
return set.GetObjectInfo(ctx, bucket, object, opts) return set.GetObjectInfo(ctx, bucket, object, opts)
} }
// DeleteObject - deletes an object from the hashedSet based on the object name. // DeleteObject - deletes an object from the hashedSet based on the object name.
func (s *erasureSets) DeleteObject(ctx context.Context, bucket string, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) { func (s *erasureSets) DeleteObject(ctx context.Context, bucket string, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
set := s.getHashedSet(object) set := s.getHashedSet(object)
auditObjectErasureSet(ctx, object, set, s.poolNumber) auditObjectErasureSet(ctx, object, set)
return set.DeleteObject(ctx, bucket, object, opts) return set.DeleteObject(ctx, bucket, object, opts)
} }
@@ -860,7 +923,7 @@ func (s *erasureSets) DeleteObjects(ctx context.Context, bucket string, objects
delErrs[obj.origIndex] = errs[i] delErrs[obj.origIndex] = errs[i]
delObjects[obj.origIndex] = dobjects[i] delObjects[obj.origIndex] = dobjects[i]
if errs[i] == nil { if errs[i] == nil {
auditObjectErasureSet(ctx, obj.object.ObjectName, set, s.poolNumber) auditObjectErasureSet(ctx, obj.object.ObjectName, set)
} }
} }
} }
@@ -873,7 +936,7 @@ func (s *erasureSets) CopyObject(ctx context.Context, srcBucket, srcObject, dstB
srcSet := s.getHashedSet(srcObject) srcSet := s.getHashedSet(srcObject)
dstSet := s.getHashedSet(dstObject) dstSet := s.getHashedSet(dstObject)
auditObjectErasureSet(ctx, dstObject, dstSet, s.poolNumber) auditObjectErasureSet(ctx, dstObject, dstSet)
cpSrcDstSame := srcSet == dstSet cpSrcDstSame := srcSet == dstSet
// Check if this request is only metadata update. // Check if this request is only metadata update.
@@ -909,162 +972,18 @@ func (s *erasureSets) CopyObject(ctx context.Context, srcBucket, srcObject, dstB
return dstSet.putObject(ctx, dstBucket, dstObject, srcInfo.PutObjReader, putOpts) return dstSet.putObject(ctx, dstBucket, dstObject, srcInfo.PutObjReader, putOpts)
} }
// FileInfoVersionsCh - file info versions channel
type FileInfoVersionsCh struct {
Ch chan FileInfoVersions
Prev FileInfoVersions
Valid bool
}
// Pop - pops a cached entry if any, or from the cached channel.
func (f *FileInfoVersionsCh) Pop() (fi FileInfoVersions, ok bool) {
if f.Valid {
f.Valid = false
return f.Prev, true
} // No cached entries found, read from channel
f.Prev, ok = <-f.Ch
return f.Prev, ok
}
// Push - cache an entry, for Pop() later.
func (f *FileInfoVersionsCh) Push(fi FileInfoVersions) {
f.Prev = fi
f.Valid = true
}
// FileInfoCh - file info channel
type FileInfoCh struct {
Ch chan FileInfo
Prev FileInfo
Valid bool
}
// Pop - pops a cached entry if any, or from the cached channel.
func (f *FileInfoCh) Pop() (fi FileInfo, ok bool) {
if f.Valid {
f.Valid = false
return f.Prev, true
} // No cached entries found, read from channel
f.Prev, ok = <-f.Ch
return f.Prev, ok
}
// Push - cache an entry, for Pop() later.
func (f *FileInfoCh) Push(fi FileInfo) {
f.Prev = fi
f.Valid = true
}
// Calculate lexically least entry across multiple FileInfo channels,
// returns the lexically common entry and the total number of times
// we found this entry. Additionally also returns a boolean
// to indicate if the caller needs to call this function
// again to list the next entry. It is callers responsibility
// if the caller wishes to list N entries to call lexicallySortedEntry
// N times until this boolean is 'false'.
func lexicallySortedEntryVersions(entryChs []FileInfoVersionsCh, entries []FileInfoVersions, entriesValid []bool) (FileInfoVersions, int, bool) {
for j := range entryChs {
entries[j], entriesValid[j] = entryChs[j].Pop()
}
var isTruncated = false
for _, valid := range entriesValid {
if !valid {
continue
}
isTruncated = true
break
}
var lentry FileInfoVersions
var found bool
for i, valid := range entriesValid {
if !valid {
continue
}
if !found {
lentry = entries[i]
found = true
continue
}
if entries[i].Name < lentry.Name {
lentry = entries[i]
}
}
// We haven't been able to find any lexically least entry,
// this would mean that we don't have valid entry.
if !found {
return lentry, 0, isTruncated
}
lexicallySortedEntryCount := 0
for i, valid := range entriesValid {
if !valid {
continue
}
// Entries are duplicated across disks,
// we should simply skip such entries.
if lentry.Name == entries[i].Name && lentry.LatestModTime.Equal(entries[i].LatestModTime) {
lexicallySortedEntryCount++
continue
}
// Push all entries which are lexically higher
// and will be returned later in Pop()
entryChs[i].Push(entries[i])
}
return lentry, lexicallySortedEntryCount, isTruncated
}
func (s *erasureSets) startMergeWalksVersions(ctx context.Context, bucket, prefix, marker string, recursive bool, endWalkCh <-chan struct{}) []FileInfoVersionsCh {
return s.startMergeWalksVersionsN(ctx, bucket, prefix, marker, recursive, endWalkCh, -1)
}
// Starts a walk versions channel across N number of disks and returns a slice.
// FileInfoCh which can be read from.
func (s *erasureSets) startMergeWalksVersionsN(ctx context.Context, bucket, prefix, marker string, recursive bool, endWalkCh <-chan struct{}, ndisks int) []FileInfoVersionsCh {
var entryChs []FileInfoVersionsCh
var wg sync.WaitGroup
var mutex sync.Mutex
for _, set := range s.sets {
// Reset for the next erasure set.
for _, disk := range set.getLoadBalancedNDisks(ndisks) {
wg.Add(1)
go func(disk StorageAPI) {
defer wg.Done()
entryCh, err := disk.WalkVersions(GlobalContext, bucket, prefix, marker, recursive, endWalkCh)
if err != nil {
return
}
mutex.Lock()
entryChs = append(entryChs, FileInfoVersionsCh{
Ch: entryCh,
})
mutex.Unlock()
}(disk)
}
}
wg.Wait()
return entryChs
}
func (s *erasureSets) ListMultipartUploads(ctx context.Context, bucket, prefix, keyMarker, uploadIDMarker, delimiter string, maxUploads int) (result ListMultipartsInfo, err error) { func (s *erasureSets) ListMultipartUploads(ctx context.Context, bucket, prefix, keyMarker, uploadIDMarker, delimiter string, maxUploads int) (result ListMultipartsInfo, err error) {
// In list multipart uploads we are going to treat input prefix as the object, // In list multipart uploads we are going to treat input prefix as the object,
// this means that we are not supporting directory navigation. // this means that we are not supporting directory navigation.
set := s.getHashedSet(prefix) set := s.getHashedSet(prefix)
auditObjectErasureSet(ctx, prefix, set, s.poolNumber) auditObjectErasureSet(ctx, prefix, set)
return set.ListMultipartUploads(ctx, bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads) return set.ListMultipartUploads(ctx, bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
} }
// Initiate a new multipart upload on a hashedSet based on object name. // Initiate a new multipart upload on a hashedSet based on object name.
func (s *erasureSets) NewMultipartUpload(ctx context.Context, bucket, object string, opts ObjectOptions) (uploadID string, err error) { func (s *erasureSets) NewMultipartUpload(ctx context.Context, bucket, object string, opts ObjectOptions) (uploadID string, err error) {
set := s.getHashedSet(object) set := s.getHashedSet(object)
auditObjectErasureSet(ctx, object, set, s.poolNumber) auditObjectErasureSet(ctx, object, set)
return set.NewMultipartUpload(ctx, bucket, object, opts) return set.NewMultipartUpload(ctx, bucket, object, opts)
} }
@@ -1072,42 +991,42 @@ func (s *erasureSets) NewMultipartUpload(ctx context.Context, bucket, object str
func (s *erasureSets) CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, uploadID string, partID int, func (s *erasureSets) CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, uploadID string, partID int,
startOffset int64, length int64, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (partInfo PartInfo, err error) { startOffset int64, length int64, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (partInfo PartInfo, err error) {
destSet := s.getHashedSet(destObject) destSet := s.getHashedSet(destObject)
auditObjectErasureSet(ctx, destObject, destSet, s.poolNumber) auditObjectErasureSet(ctx, destObject, destSet)
return destSet.PutObjectPart(ctx, destBucket, destObject, uploadID, partID, NewPutObjReader(srcInfo.Reader, nil, nil), dstOpts) return destSet.PutObjectPart(ctx, destBucket, destObject, uploadID, partID, NewPutObjReader(srcInfo.Reader), dstOpts)
} }
// PutObjectPart - writes part of an object to hashedSet based on the object name. // PutObjectPart - writes part of an object to hashedSet based on the object name.
func (s *erasureSets) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, data *PutObjReader, opts ObjectOptions) (info PartInfo, err error) { func (s *erasureSets) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, data *PutObjReader, opts ObjectOptions) (info PartInfo, err error) {
set := s.getHashedSet(object) set := s.getHashedSet(object)
auditObjectErasureSet(ctx, object, set, s.poolNumber) auditObjectErasureSet(ctx, object, set)
return set.PutObjectPart(ctx, bucket, object, uploadID, partID, data, opts) return set.PutObjectPart(ctx, bucket, object, uploadID, partID, data, opts)
} }
// GetMultipartInfo - return multipart metadata info uploaded at hashedSet. // GetMultipartInfo - return multipart metadata info uploaded at hashedSet.
func (s *erasureSets) GetMultipartInfo(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) (result MultipartInfo, err error) { func (s *erasureSets) GetMultipartInfo(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) (result MultipartInfo, err error) {
set := s.getHashedSet(object) set := s.getHashedSet(object)
auditObjectErasureSet(ctx, object, set, s.poolNumber) auditObjectErasureSet(ctx, object, set)
return set.GetMultipartInfo(ctx, bucket, object, uploadID, opts) return set.GetMultipartInfo(ctx, bucket, object, uploadID, opts)
} }
// ListObjectParts - lists all uploaded parts to an object in hashedSet. // ListObjectParts - lists all uploaded parts to an object in hashedSet.
func (s *erasureSets) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int, opts ObjectOptions) (result ListPartsInfo, err error) { func (s *erasureSets) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int, opts ObjectOptions) (result ListPartsInfo, err error) {
set := s.getHashedSet(object) set := s.getHashedSet(object)
auditObjectErasureSet(ctx, object, set, s.poolNumber) auditObjectErasureSet(ctx, object, set)
return set.ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts) return set.ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts)
} }
// Aborts an in-progress multipart operation on hashedSet based on the object name. // Aborts an in-progress multipart operation on hashedSet based on the object name.
func (s *erasureSets) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) error { func (s *erasureSets) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) error {
set := s.getHashedSet(object) set := s.getHashedSet(object)
auditObjectErasureSet(ctx, object, set, s.poolNumber) auditObjectErasureSet(ctx, object, set)
return set.AbortMultipartUpload(ctx, bucket, object, uploadID, opts) return set.AbortMultipartUpload(ctx, bucket, object, uploadID, opts)
} }
// CompleteMultipartUpload - completes a pending multipart transaction, on hashedSet based on object name. // CompleteMultipartUpload - completes a pending multipart transaction, on hashedSet based on object name.
func (s *erasureSets) CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart, opts ObjectOptions) (objInfo ObjectInfo, err error) { func (s *erasureSets) CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart, opts ObjectOptions) (objInfo ObjectInfo, err error) {
set := s.getHashedSet(object) set := s.getHashedSet(object)
auditObjectErasureSet(ctx, object, set, s.poolNumber) auditObjectErasureSet(ctx, object, set)
opts.ParentIsObject = s.parentDirIsObject opts.ParentIsObject = s.parentDirIsObject
return set.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts) return set.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts)
} }
@@ -1291,8 +1210,8 @@ func (s *erasureSets) HealFormat(ctx context.Context, dryRun bool) (res madmin.H
res.Before.Drives = make([]madmin.HealDriveInfo, len(beforeDrives)) res.Before.Drives = make([]madmin.HealDriveInfo, len(beforeDrives))
// Copy "after" drive state too from before. // Copy "after" drive state too from before.
for k, v := range beforeDrives { for k, v := range beforeDrives {
res.Before.Drives[k] = madmin.HealDriveInfo(v) res.Before.Drives[k] = v
res.After.Drives[k] = madmin.HealDriveInfo(v) res.After.Drives[k] = v
} }
if countErrs(sErrs, errUnformattedDisk) == 0 { if countErrs(sErrs, errUnformattedDisk) == 0 {
@@ -1335,7 +1254,7 @@ func (s *erasureSets) HealFormat(ctx context.Context, dryRun bool) (res madmin.H
if s.erasureDisks[m][n] != nil { if s.erasureDisks[m][n] != nil {
s.erasureDisks[m][n].Close() s.erasureDisks[m][n].Close()
} }
storageDisks[index].SetDiskLoc(s.poolIndex, m, n)
s.erasureDisks[m][n] = storageDisks[index] s.erasureDisks[m][n] = storageDisks[index]
s.endpointStrings[m*s.setDriveCount+n] = storageDisks[index].String() s.endpointStrings[m*s.setDriveCount+n] = storageDisks[index].String()
} }
@@ -1359,11 +1278,11 @@ func (s *erasureSets) HealBucket(ctx context.Context, bucket string, opts madmin
SetCount: s.setCount, SetCount: s.setCount,
} }
for _, s := range s.sets { for _, set := range s.sets {
var healResult madmin.HealResultItem var healResult madmin.HealResultItem
healResult, err = s.HealBucket(ctx, bucket, opts) healResult, err = set.HealBucket(ctx, bucket, opts)
if err != nil { if err != nil {
return result, err return result, toObjectErr(err, bucket)
} }
result.Before.Drives = append(result.Before.Drives, healResult.Before.Drives...) result.Before.Drives = append(result.Before.Drives, healResult.Before.Drives...)
result.After.Drives = append(result.After.Drives, healResult.After.Drives...) result.After.Drives = append(result.After.Drives, healResult.After.Drives...)
@@ -1384,13 +1303,13 @@ func (s *erasureSets) HealObject(ctx context.Context, bucket, object, versionID
} }
// PutObjectTags - replace or add tags to an existing object // PutObjectTags - replace or add tags to an existing object
func (s *erasureSets) PutObjectTags(ctx context.Context, bucket, object string, tags string, opts ObjectOptions) error { func (s *erasureSets) PutObjectTags(ctx context.Context, bucket, object string, tags string, opts ObjectOptions) (ObjectInfo, error) {
er := s.getHashedSet(object) er := s.getHashedSet(object)
return er.PutObjectTags(ctx, bucket, object, tags, opts) return er.PutObjectTags(ctx, bucket, object, tags, opts)
} }
// DeleteObjectTags - delete object tags from an existing object // DeleteObjectTags - delete object tags from an existing object
func (s *erasureSets) DeleteObjectTags(ctx context.Context, bucket, object string, opts ObjectOptions) error { func (s *erasureSets) DeleteObjectTags(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
er := s.getHashedSet(object) er := s.getHashedSet(object)
return er.DeleteObjectTags(ctx, bucket, object, opts) return er.DeleteObjectTags(ctx, bucket, object, opts)
} }

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