Compare commits

..

113 Commits

Author SHA1 Message Date
Harshavardhana bebcf4f004 unlock() only if locking was successful 2020-09-25 19:36:47 -07:00
Harshavardhana eafa775952 fix: add lock ownership to expire locks (#10571)
- Add owner information for expiry, locking, unlocking a resource
- TopLocks returns now locks in quorum by default, provides
  a way to capture stale locks as well with `?stale=true`
- Simplify the quorum handling for locks to avoid from storage
  class, because there were challenges to make it consistent
  across all situations.
- And other tiny simplifications to reset locks.
2020-09-25 19:21:52 -07:00
Harshavardhana 66b4a862e0 fix: network failure err check should ignore context canceled errors (#10567)
context canceled errors bubbling up from the network
layer has the potential to be misconstrued as network
errors, taking prematurely a server offline and triggering
a health check routine avoid this potential occurrence.
2020-09-25 14:35:47 -07:00
Anis Elleuch 9603489dd3 federation: Honor range with UploadObjectPart to a different cluster (#10570)
Use gr & length instead of srcInfo.Reader & srcInfo.Size because 
they don't honor range header
2020-09-25 12:06:42 -07:00
Anis Elleuch b302c8a5f4 heal: Fix periodic healing cleanup (#10569)
isEnded() was incorrectly calculating if the current healing sequence is
ended or not. h.currentStatus.Items could be empty if healing is very
slow and mc admin heal consumed all items.
2020-09-25 10:29:00 -07:00
飞雪无情 4de88e87bb os.SEEK_SET is deprecated,use io.SeekStart. (#10563) 2020-09-25 03:12:25 -07:00
Praveen raj Mani b880796aef Set the maximum open connections limit in PG and MySQL target configs (#10558)
As the bulk/recursive delete will require multiple connections to open at an instance,
The default open connections limit will be reached which results in the following error

```FATAL:  sorry, too many clients already```

By setting the open connections to a reasonable value - `2`, We ensure that the max open connections
will not be exhausted and lie under bounds.

The queries are simple inserts/updates/deletes which is operational and sufficient with the
the maximum open connection limit is 2.

Fixes #10553

Allow user configuration for MaxOpenConnections
2020-09-24 22:20:30 -07:00
Harshavardhana 37a5d5d7a0 reduce timeouts between servers for faster disconnects (#10562) 2020-09-24 20:10:07 -07:00
Harshavardhana 3cac262dd1 report heal drives properly, also from global state (#10561)
It is possible the heal drives are not reported from
the maintenance check because the background heal
state simply relied on the `format.json` for capturing
unformatted drives. It is possible that drives might
be still healing - make sure that applications which
rely on cluster health check respond back this detail.
2020-09-24 15:36:47 -07:00
poornas e6ab4db6b8 Fix minimum replication workers started (#10560)
This PR also fixes GetReplicationConfiguration permission
in web-handlers.go to use bucket as resource
2020-09-24 12:25:41 -07:00
Harshavardhana ca989eb0b3 avoid ListBuckets returning quorum errors when node is down (#10555)
Also, revamp the way ListBuckets work make few portions
of the healing logic parallel

- walk objects for healing disks in parallel
- collect the list of buckets in parallel across drives
- provide consistent view for listBuckets()
2020-09-24 09:53:38 -07:00
飞雪无情 d778d034e7 Remove redundant mgmtQueryKey type. (#10557)
Remove redundant type conversion.
2020-09-24 08:40:21 -07:00
Espen Finnesand df08fd1f03 Update Kubernetes Helm Readme.md (#10559)
The link to the Minio Helm Chart repository was wrong.
2020-09-24 08:38:46 -07:00
Minio Trusted ac82f416a4 Update yaml files to latest version RELEASE.2020-09-23T19-18-30Z 2020-09-23 19:38:14 +00:00
Harshavardhana f7f9517b6a fix: host extraction without port 2020-09-23 12:10:14 -07:00
Harshavardhana 90cff10e2b avoid crash if disks are not initialized 2020-09-23 12:00:29 -07:00
Harshavardhana 81caf35926 fix: reduce healthcheck interval for storage rest client (#10544) 2020-09-23 10:43:42 -07:00
poornas 5726cef3ca validate bucket exists in ListRemoteTargets api (#10552) 2020-09-23 10:37:54 -07:00
飞雪无情 5fdf47b118 Add Chinese documentation for distributed design (#10549) 2020-09-23 09:26:47 -07:00
Harshavardhana 8b74a72b21 fix: rename READY deadline to CLUSTER deadline ENV (#10535) 2020-09-23 09:14:33 -07:00
Klaus Post eec69d6796 Fix stale context for bucket retrieval (#10551)
The provided context gets captured by the closure making all subsequent calls fail.
2020-09-23 08:30:31 -07:00
Harshavardhana 0537a21b79 avoid concurrenct use of rand.NewSource (#10543) 2020-09-22 15:34:27 -07:00
poornas 4c54ed8748 Close replica channel only once (#10542)
Also enforce s3:GetReplicationConfiguration permission check as a
bucket level resource.
2020-09-22 12:47:24 -07:00
poornas a4006e23a0 Update replication docs to clarify permissions (#10536)
Co-authored-by: Klaus Post <klauspost@gmail.com>
2020-09-22 11:58:04 -07:00
Shireesh Anjal b17dc81540 Change "disks" node to "drives" in OBD output (#10540) 2020-09-22 11:53:19 -07:00
Minio Trusted d73c4f09f3 Update yaml files to latest version RELEASE.2020-09-21T22-31-59Z 2020-09-21 22:58:40 +00:00
Anis Elleuch 4c81201f95 fix: healing delete marker on versioned buckets (#10530)
Healing was not working correctly in the distributed mode because
errFileVersionNotFound was not properly converted in storage rest
client.

Besides, fixing the healing delete marker is not working as expected.
2020-09-21 15:16:16 -07:00
Harshavardhana cd8d511d3d move versionsOrder struct to xl-storage-utils 2020-09-21 14:24:42 -07:00
Kaan Kabalak 899a2fa1c7 re-order right hand side dropdown options (#10534) 2020-09-21 13:59:42 -07:00
Harshavardhana 17e17da00d add parallel workers to perform replication in parallel (#10525)
set the concurrency for replication be to runtime.NumCPU()/2
2020-09-21 13:43:29 -07:00
Harshavardhana a5da9120f3 fix: [fs] an error upon rwPool.Write() just attempt rwPool.Create() (#10533)
On some NFS clients looks like errno is incorrectly set,
which leads to incorrect errors thrown upwards.
2020-09-21 12:54:23 -07:00
poornas aa12d75d75 fix crawler to detect lifecycle on bucket even if filter nil (#10532) 2020-09-21 11:41:07 -07:00
飞雪无情 dd4a2d7419 update distributed zh_CN document. (#10531) 2020-09-21 10:54:56 -07:00
Harshavardhana 6fcbdd5607 remove unused putObjectDir code (#10528) 2020-09-21 09:41:39 -07:00
Harshavardhana 3831cc9e3b fix: [fs] CompleteMultipart use trie structure for partMatch (#10522)
performance improves by around 100x or more

```
go test -v -run NONE -bench BenchmarkGetPartFile
goos: linux
goarch: amd64
pkg: github.com/minio/minio/cmd
BenchmarkGetPartFileWithTrie
BenchmarkGetPartFileWithTrie-4          1000000000               0.140 ns/op           0 B/op          0 allocs/op
PASS
ok      github.com/minio/minio/cmd      1.737s
```

fixes #10520
2020-09-21 01:18:13 -07:00
Krishna Srinivas 230fc0d186 Support for "directory" objects (#10499) 2020-09-19 08:39:41 -07:00
Harshavardhana 7f9498f43f fix: ignore faulty drives and continue (#10511)
drives might return different types of errors
handle them individually, and for some errors
just log an error and continue
2020-09-18 12:09:05 -07:00
Harshavardhana 1cf322b7d4 change leader locker only for crawler (#10509) 2020-09-18 11:15:54 -07:00
Derek Bender 3168e93730 fix typo in healthcheck README.md (#10518) 2020-09-18 09:52:37 -07:00
Klaus Post 0b1c824618 Fix incorrect request start time (#10516)
Log request start time BEFORE starting processing the request
2020-09-18 09:30:52 -07:00
Klaus Post c851e022b7 Tweaks to dynamic locks (#10508)
* Fix cases where minimum timeout > default timeout.
* Add defensive code for too small/negative timeouts.
* Never set timeout below the maximum value of a request.
* Protect against (unlikely) int64 wraps.
* Decrease timeout slower.
* Don't re-lock before copying.
2020-09-18 09:18:18 -07:00
Aleksey Pogibelev 6f45e303f5 fix tls flag (#10487) 2020-09-18 15:35:54 +05:30
Klaus Post 5ad032826a Add a reasonable if unable to get total RAM (#10506)
Though unlikely we shouldn't skip initializing the API if we cannot get RAM.

Add 16GiB as a default and log the error.
2020-09-18 02:03:02 -07:00
Harshavardhana 84bf4624a4 fix: make sure to preserve metadata during overwrite in FS mode (#10512)
This bug was introduced in 14f0047295
almost 3yrs ago, as a side affect of removing stale `fs.json`
but we in-fact end up removing existing good `fs.json` for an
existing object, leading to some form of a data loss.

fixes #10496
2020-09-18 00:16:16 -07:00
飞雪无情 dff37aa33d update disk caching zh_CN document. (#10472)
add disk caching DESIGN zh_CN document.
2020-09-18 00:07:48 -07:00
saurabh29789 d12831eb07 Add support for searching objects (#10424) 2020-09-17 23:01:37 -07:00
Harshavardhana 4a36cd7035 fix: improve performance ListObjectParts in FS mode (#10510)
from 20s for 10000 parts to less than 1sec

Without the patch
```
~ time aws --endpoint-url=http://localhost:9000 --profile minio s3api \
       list-parts --bucket testbucket --key test \
       --upload-id c1cd1f50-ea9a-4824-881c-63b5de95315a

real    0m20.394s
user    0m0.589s
sys     0m0.174s
```

With the patch
```
~ time aws --endpoint-url=http://localhost:9000 --profile minio s3api \
       list-parts --bucket testbucket --key test \
       --upload-id c1cd1f50-ea9a-4824-881c-63b5de95315a

real    0m0.891s
user    0m0.624s
sys     0m0.182s
```

fixes #10503
2020-09-17 18:51:16 -07:00
poornas 00555c747e Strip standard ports off remote target url (#10498) 2020-09-17 11:09:50 -07:00
Klaus Post 03490c811b Fix obd goroutine leak (#10504)
The gouroutine collecting transfer stats never exits. Add missing channel close.
2020-09-17 10:10:20 -07:00
Minio Trusted 48d2c03250 Update yaml files to latest version RELEASE.2020-09-17T04-49-20Z 2020-09-17 05:07:58 +00:00
Harshavardhana ed78854cea fix: list across all drives to avoid stale disks 2020-09-16 21:17:10 -07:00
Harshavardhana e60834838f fix: background disk heal, to reload format consistently (#10502)
It was observed in VMware vsphere environment during a
pod replacement, `mc admin info` might report incorrect
offline nodes for the replaced drive. This issue eventually
goes away but requires quite a lot of time for all servers
to be in sync.

This PR fixes this behavior properly.
2020-09-16 21:14:35 -07:00
Harshavardhana d616d8a857 serialize replication and feed it through task model (#10500)
this allows for eventually controlling the concurrency
of replication and overally control of throughput
2020-09-16 16:04:55 -07:00
Anis Elleuch 24cab7f9df ilm: Remove a 'null' version if not latest (#10494)
If the ILM document requires removing noncurrent versions, the 
the server should be able to remove 'null' versions as well. 
'null' versions are created when versioning is not enabled 
or suspended.
2020-09-16 10:21:50 -07:00
Minio Trusted b2536476c9 Update yaml files to latest version RELEASE.2020-09-16T04-22-35Z 2020-09-16 04:41:00 +00:00
Harshavardhana 02c1a08a5b fix: make sure to lock CopyObject for in-place updates (#10492) 2020-09-15 20:44:48 -07:00
Ritesh H Shukla 5c47ce456e Run replication in the background (#10491) 2020-09-15 18:44:58 -07:00
Anis Elleuch 8ea55f9dba obd: Add console log to OBD output (#10372) 2020-09-15 18:02:54 -07:00
poornas 80e3dce631 azure: update content-md5 to metadata after upload (#10482)
Fixes #10453
2020-09-15 16:31:47 -07:00
Harshavardhana 80fab03b63 fix: S3 gateway doesn't support full passthrough for encryption (#10484)
The entire encryption layer is dependent on the fact that
KMS should be configured for S3 encryption to work properly
and we only support passing the headers as is to the backend
for encryption only if KMS is configured.

Make sure that this predictability is maintained, currently
the code was allowing encryption to go through and fail
at later to indicate that KMS was not configured. We should
simply reply "NotImplemented" if KMS is not configured, this
allows clients to simply proceed with their tests.
2020-09-15 13:57:15 -07:00
Harshavardhana 730d2dc7be fix: allow CopyObject/PutObjecTags on pre-existing content (#10485)
fixes #10475
2020-09-15 09:18:41 -07:00
Harshavardhana 0ee9678190 fix: add missing delete marker created filter (#10481) 2020-09-14 21:32:52 -07:00
Klaus Post 34859c6d4b Preallocate (safe) slices when we know the size (#10459) 2020-09-14 20:44:18 -07:00
Klaus Post b1c99e88ac reduce CPU usage upto 50% in readdir (#10466) 2020-09-14 17:19:54 -07:00
Harshavardhana 0104af6bcc delayed locks until we have started reading the body (#10474)
This is to ensure that Go contexts work properly, after some
interesting experiments I found that Go net/http doesn't
cancel the context when Body is non-zero and hasn't been
read till EOF.

The following gist explains this, this can lead to pile up
of go-routines on the server which will never be canceled
and will die at a really later point in time, which can
simply overwhelm the server.

https://gist.github.com/harshavardhana/c51dcfd055780eaeb71db54f9c589150

To avoid this refactor the locking such that we take locks after we
have started reading from the body and only take locks when needed.

Also, remove contextReader as it's not useful, doesn't work as expected
context is not canceled until the body reaches EOF so there is no point
in wrapping it with context and putting a `select {` on it which
can unnecessarily increase the CPU overhead.

We will still use the context to cancel the lockers etc.
Additional simplification in the locker code to avoid timers
as re-using them is a complicated ordeal avoid them in
the hot path, since locking is very common this may avoid
lots of allocations.
2020-09-14 15:57:13 -07:00
Andreas Auernhammer 224daee391 fix nats TLS unit tests (#10476)
This commit fixes the nats TLS tests by generating new certificates
(root CA, server and client) - each valid for 10y. The new certificates
don't have a common name (deprecated by X.509) but SANs instead.

Since Go 1.15 the Go `crypto/x509` package rejects certificates that
only have a common name and no SAN. See: https://golang.org/doc/go1.15#commonname
2020-09-14 13:19:46 -07:00
Harshavardhana 34ea1d2167 fix: return correct error code for MetadataTooLarge (#10470)
fixes #10469
2020-09-13 21:26:35 -07:00
Harshavardhana 9d95937018 update KMS docs indicating deprecation of AUTO_ENCRYPTION env 2020-09-13 16:23:28 -07:00
Derek Bender 74a7889a3e Fix typo in README.md (#10471) 2020-09-13 09:02:10 -07:00
Klaus Post fa01e640f5 Continous healing: add optional bitrot check (#10417) 2020-09-12 00:08:12 -07:00
Harshavardhana f355374962 add support for configurable remote transport deadline (#10447)
configurable remote transport timeouts for some special cases
where this value needs to be bumped to a higher value when
transferring large data between federated instances.
2020-09-11 23:03:08 -07:00
Harshavardhana bda0fe3150 fix: allow LDAP identity to support form body POST (#10468)
similar to other STS APIs
2020-09-11 23:02:32 -07:00
Harshavardhana b70995dd60 Revert "ilm: Remove null version if not latest with proper config (#10467)"
This reverts commit 4b6264da7d.
2020-09-11 18:15:49 -07:00
Anis Elleuch 4b6264da7d ilm: Remove null version if not latest with proper config (#10467) 2020-09-11 14:20:09 -07:00
Harshavardhana 48919de301 fix: for defer'ed deleteObject use internal context (#10463) 2020-09-11 06:39:19 -07:00
Minio Trusted eb3ded420e Update yaml files to latest version RELEASE.2020-09-10T22-02-45Z 2020-09-10 22:22:28 +00:00
Harshavardhana eb2934f0c1 simplify webhook DNS further generalize for gateway (#10448)
continuation of the changes from eaaf05a7cc
this further simplifies, enables this for gateway deployments as well
2020-09-10 14:19:32 -07:00
Klaus Post b7438fe4e6 Copy metadata before spawning goroutine + prealloc maps (#10458)
In `(*cacheObjects).GetObjectNInfo` copy the metadata before spawning a goroutine.

Clean up a few map[string]string copies as well, reducing allocs and simplifying the code.

Fixes #10426
2020-09-10 11:37:22 -07:00
Anis Elleuch ce6cef6855 erasure: Call Walk() from all disks (#10445)
It does not make sense to call Walk() in only N/2 disks and then
requires N/2 quorum, just keep it N/2+1 

The commit fixes this behavior.
2020-09-10 09:27:52 -07:00
飞雪无情 a966ccd17d Support nginx LB with docker-compose example (#10434) 2020-09-10 09:19:17 -07:00
Klaus Post 493c714663 Remove erasureSets and erasureObjects from ObjectLayer (#10442) 2020-09-10 09:18:19 -07:00
Harshavardhana e959c5d71c fix: server panic in FS mode (#10455)
fixes #10454
2020-09-10 09:16:26 -07:00
Harshavardhana 4a2928eb49 generate missing object delete bucket notifications (#10449)
fixes #10381
2020-09-09 18:23:08 -07:00
Anis Elleuch af88772a78 lifecycle: NoncurrentVersionExpiration considers noncurrent version age (#10444)
From https://docs.aws.amazon.com/AmazonS3/latest/dev/intro-lifecycle-rules.html#intro-lifecycle-rules-actions

```
When specifying the number of days in the NoncurrentVersionTransition
and NoncurrentVersionExpiration actions in a Lifecycle configuration,
note the following:

It is the number of days from when the version of the object becomes
noncurrent (that is, when the object is overwritten or deleted), that
Amazon S3 will perform the action on the specified object or objects.

Amazon S3 calculates the time by adding the number of days specified in
the rule to the time when the new successor version of the object is
created and rounding the resulting time to the next day midnight UTC.
For example, in your bucket, suppose that you have a current version of
an object that was created at 1/1/2014 10:30 AM UTC. If the new version
of the object that replaces the current version is created at 1/15/2014
10:30 AM UTC, and you specify 3 days in a transition rule, the
transition date of the object is calculated as 1/19/2014 00:00 UTC.
```
2020-09-09 18:11:24 -07:00
Li Yi 1dce6918c2 fix sysctl.sh quotes which are incompatible with sysctl (#10446) 2020-09-09 17:29:23 -07:00
Harshavardhana 9109148474 add support for new UA values for update an check (#10451) 2020-09-09 17:21:39 -07:00
Nitish Tiwari eaaf05a7cc Add Kubernetes operator webook server as DNS target (#10404)
This PR adds a DNS target that ensures to update an entry
into Kubernetes operator when a bucket is created or deleted.

See minio/operator#264 for details.

Co-authored-by: Harshavardhana <harsha@minio.io>
2020-09-09 12:20:49 -07:00
Harshavardhana 52e21bc853 update docs to indicate go1.14 is minimum 2020-09-09 11:27:48 -07:00
Klaus Post 16e1a25bc0 go.mod: Require Go 1.14 (#10441)
MinIO requires Go 1.14, so bump mod version.
2020-09-09 11:23:54 -07:00
Harshavardhana 958661cbb5 skip subdomain from bucket DNS which start with minio.domain (#10390)
extend host matcher to reject the host match
2020-09-09 09:57:37 -07:00
Minio Trusted 6019628f7d Update yaml files to latest version RELEASE.2020-09-08T23-05-18Z 2020-09-08 23:25:40 +00:00
Klaus Post 0987069e37 select: Fix integer conversion overflow (#10437)
Do not convert float value to integer if it will over/underflow.

The comparison cannot be `<=` since rounding may overflow it.

Fixes #10436
2020-09-08 15:56:11 -07:00
Harshavardhana 6a0372be6c cleanup tmpDir any older entries automatically just like multipart (#10439)
also consider multipart uploads, temporary files in `.minio.sys/tmp`
as stale beyond 24hrs and clean them up automatically
2020-09-08 15:55:40 -07:00
Harshavardhana c13afd56e8 Remove MaxConnsPerHost settings to avoid potential hangs (#10438)
MaxConnsPerHost can potentially hang a call without any
way to timeout, we do not need this setting for our proxy
and gateway implementations instead IdleConn settings are
good enough.

Also ensure to use NewRequestWithContext and make sure to
take the disks offline only for network errors.

Fixes #10304
2020-09-08 14:22:04 -07:00
Harshavardhana 96997d2b21 allow ctrl+c to be consistent at early startup (#10435)
fixes #10431
2020-09-08 09:10:55 -07:00
Klaus Post 86a3319d41 Ignore config values from unknown subsystems (#10432) 2020-09-08 08:57:04 -07:00
飞雪无情 a694ba93d9 update kernel tuning zh_CN document (#10433) 2020-09-07 19:21:52 -07:00
Harshavardhana 9f60e84ce1 always copy UserDefined metadata map (#10427)
fixes #10426
2020-09-07 09:25:28 -07:00
Minio Trusted a9aaea0d67 Update yaml files to latest version RELEASE.2020-09-05T07-14-49Z 2020-09-05 07:32:10 +00:00
Harshavardhana 572b1721b2 set max API requests automatically based on RAM (#10421) 2020-09-04 19:37:37 -07:00
Harshavardhana b0e1d4ce78 re-attach offline drive after new drive replacement (#10416)
inconsistent drive healing when one of the drive is offline
while a new drive was replaced, this change is to ensure
that we can add the offline drive back into the mix by
healing it again.
2020-09-04 17:09:02 -07:00
Harshavardhana eb19c8af40 Bump response header timeout for proxying list request (#10420) 2020-09-04 16:07:40 -07:00
飞雪无情 746f1585eb add debugging zh_CN document. (#10401) 2020-09-04 13:03:46 -07:00
Klaus Post 2d58a8d861 Add storage layer contexts (#10321)
Add context to all (non-trivial) calls to the storage layer. 

Contexts are propagated through the REST client.

- `context.TODO()` is left in place for the places where it needs to be added to the caller.
- `endWalkCh` could probably be removed from the walkers, but no changes so far.

The "dangerous" part is that now a caller disconnecting *will* propagate down,  so a 
"delete" operation will now be interrupted. In some cases we might want to disconnect 
this functionality so the operation completes if it has started, leaving the system in a cleaner state.
2020-09-04 09:45:06 -07:00
poornas 0037951b6e improve error message when remote target missing (#10412) 2020-09-04 08:48:38 -07:00
Andreas Auernhammer fbd1c5f51a certs: refactor cert manager to support multiple certificates (#10207)
This commit refactors the certificate management implementation
in the `certs` package such that multiple certificates can be
specified at the same time. Therefore, the following layout of
the `certs/` directory is expected:
```
certs/
 │
 ├─ public.crt
 ├─ private.key
 ├─ CAs/          // CAs directory is ignored
 │   │
 │    ...
 │
 ├─ example.com/
 │   │
 │   ├─ public.crt
 │   └─ private.key
 └─ foobar.org/
     │
     ├─ public.crt
     └─ private.key
   ...
```

However, directory names like `example.com` are just for human
readability/organization and don't have any meaning w.r.t whether
a particular certificate is served or not. This decision is made based
on the SNI sent by the client and the SAN of the certificate.

***

The `Manager` will pick a certificate based on the client trying
to establish a TLS connection. In particular, it looks at the client
hello (i.e. SNI) to determine which host the client tries to access.
If the manager can find a certificate that matches the SNI it
returns this certificate to the client.

However, the client may choose to not send an SNI or tries to access
a server directly via IP (`https://<ip>:<port>`). In this case, we
cannot use the SNI to determine which certificate to serve. However,
we also should not pick "the first" certificate that would be accepted
by the client (based on crypto. parameters - like a signature algorithm)
because it may be an internal certificate that contains internal hostnames. 
We would disclose internal infrastructure details doing so.

Therefore, the `Manager` returns the "default" certificate when the
client does not specify an SNI. The default certificate the top-level
`public.crt` - i.e. `certs/public.crt`.

This approach has some consequences:
 - It's the operator's responsibility to ensure that the top-level
   `public.crt` does not disclose any information (i.e. hostnames)
   that are not publicly visible. However, this was the case in the
   past already.
 - Any other `public.crt` - except for the top-level one - must not
   contain any IP SAN. The reason for this restriction is that the
   Manager cannot match a SNI to an IP b/c the SNI is the server host
   name. The entire purpose of SNI is to indicate which host the client
   tries to connect to when multiple hosts run on the same IP. So, a
   client will not set the SNI to an IP.
   If we would allow IP SANs in a lower-level `public.crt` a user would
   expect that it is possible to connect to MinIO directly via IP address
   and that the MinIO server would pick "the right" certificate. However,
   the MinIO server cannot determine which certificate to serve, and
   therefore always picks the "default" one. This may lead to all sorts
   of confusing errors like:
   "It works if I use `https:instance.minio.local` but not when I use
   `https://10.0.2.1`.

These consequences/limitations should be pointed out / explained in our
docs in an appropriate way. However, the support for multiple
certificates should not have any impact on how deployment with a single
certificate function today.

Co-authored-by: Harshavardhana <harsha@minio.io>
2020-09-03 23:33:37 -07:00
Harshavardhana 1c6781757c add missing ListBucketVersions from policy actions (#10414) 2020-09-03 18:25:06 -07:00
Harshavardhana b4e3956e69 update KES docs to talk about 'mc encrypt' command (#10400)
add a deprecation notice for KMS_AUTO_ENCRYPTION
2020-09-03 12:43:45 -07:00
Krishna Srinivas c51229493b azure docs remove default chunk size example (#10406) 2020-09-03 01:31:56 -07:00
Rushan 631d55aa22 Update License link in Readme.md (#10409) 2020-09-03 01:30:53 -07:00
Harshavardhana 8a291e1dc0 Cluster healthcheck improvements (#10408)
- do not fail the healthcheck if heal status
  was not obtained from one of the nodes,
  if many nodes fail then report this as a
  catastrophic error.
- add "x-minio-write-quorum" value to match
  the write tolerance supported by server.
- admin info now states if a drive is healing
  where madmin.Disk.Healing is set to true
  and madmin.Disk.State is "ok"
2020-09-02 22:54:56 -07:00
Klaus Post 650dccfa9e cache: Only start at high watermark (#10403)
Currently, cache purges are triggered as soon as the low watermark is exceeded.
To reduce IO this should only be done when reaching the high watermark.
This simplifies checks and reduces all calls for a GC to go through
`dcache.diskSpaceAvailable(size)`. While a comment claims that 
`dcache.triggerGC <- struct{}{}` was non-blocking I don't see how 
that was possible. Instead, we add a 1 size to the queue channel 
and use channel  semantics to avoid blocking when a GC has 
already been requested.

`bytesToClear` now takes the high watermark into account to it will 
not request any bytes to be cleared until that is reached.
2020-09-02 17:48:44 -07:00
Minio Trusted d08b4b147d Update yaml files to latest version RELEASE.2020-09-02T18-19-50Z 2020-09-02 18:40:57 +00:00
235 changed files with 5056 additions and 3376 deletions
+6
View File
@@ -0,0 +1,6 @@
CVE-2018-17075
CVE-2018-17848
CVE-2018-17846
CVE-2018-17847
CVE-2018-17143
CVE-2018-17142
+3 -3
View File
@@ -88,7 +88,7 @@ service minio start
``` ```
## Install from Source ## Install 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.13](https://golang.org/dl/#stable) 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.14](https://golang.org/dl/#stable)
```sh ```sh
GO111MODULE=on go get github.com/minio/minio GO111MODULE=on go get github.com/minio/minio
@@ -177,7 +177,7 @@ mc admin update <minio alias, e.g., myminio>
- `mc admin update` updates and restarts all servers simultaneously, applications would retry and continue their respective operations upon upgrade. - `mc admin update` updates and restarts all servers simultaneously, applications would retry and continue their respective operations upon upgrade.
- `mc admin update` is disabled in kubernetes/container environments, container environments provide their own mechanisms to rollout of updates. - `mc admin update` is disabled in kubernetes/container environments, container environments provide their own mechanisms to rollout of updates.
- In the case of federated setups `mc admin update` should be run against each cluster individually. Avoid updating `mc` to any new releases until all clusters have been successfully updated. - In the case of federated setups `mc admin update` should be run against each cluster individually. Avoid updating `mc` to any new releases until all clusters have been successfully updated.
- If using `kes` as KMS with MinIO, just replace the binary and restart `kes` more information about `kes` can be found [here](https://github.com/minio/kes/wiki)x - If using `kes` as KMS with MinIO, just replace the binary and restart `kes` more information about `kes` can be found [here](https://github.com/minio/kes/wiki)
- If using Vault as KMS with MinIO, ensure you have followed the Vault upgrade procedure outlined here: https://www.vaultproject.io/docs/upgrading/index.html - If using Vault as KMS with MinIO, ensure you have followed the Vault upgrade procedure outlined here: https://www.vaultproject.io/docs/upgrading/index.html
- If using etcd with MinIO for the federation, ensure you have followed the etcd upgrade procedure outlined here: https://github.com/etcd-io/etcd/blob/master/Documentation/upgrades/upgrading-etcd.md - If using etcd with MinIO for the federation, ensure you have followed the etcd upgrade procedure outlined here: https://github.com/etcd-io/etcd/blob/master/Documentation/upgrades/upgrading-etcd.md
@@ -193,4 +193,4 @@ mc admin update <minio alias, e.g., myminio>
Please follow MinIO [Contributor's Guide](https://github.com/minio/minio/blob/master/CONTRIBUTING.md) Please follow MinIO [Contributor's Guide](https://github.com/minio/minio/blob/master/CONTRIBUTING.md)
## License ## License
Use of MinIO is governed by the Apache 2.0 License found at [LICENSE](./LICENSE). Use of MinIO is governed by the Apache 2.0 License found at [LICENSE](https://github.com/minio/minio/blob/master/LICENSE).
+1 -1
View File
@@ -89,7 +89,7 @@ service minio start
## 使用源码安装 ## 使用源码安装
采用源码安装仅供开发人员和高级用户使用,如果你还没有Golang环境, 请参考 [How to install Golang](https://golang.org/doc/install)。最低需要Golang版本为 [go1.13](https://golang.org/dl/#stable) 采用源码安装仅供开发人员和高级用户使用,如果你还没有Golang环境, 请参考 [How to install Golang](https://golang.org/doc/install)。最低需要Golang版本为 [go1.14](https://golang.org/dl/#stable)
```sh ```sh
GO111MODULE=on go get github.com/minio/minio GO111MODULE=on go get github.com/minio/minio
+19 -40
View File
@@ -57,22 +57,6 @@ export class BrowserDropdown extends React.Component {
const { fetchServerInfo } = this.props const { fetchServerInfo } = this.props
fetchServerInfo() fetchServerInfo()
} }
fullScreen(e) {
e.preventDefault()
let el = document.documentElement
if (el.requestFullscreen) {
el.requestFullscreen()
}
if (el.mozRequestFullScreen) {
el.mozRequestFullScreen()
}
if (el.webkitRequestFullscreen) {
el.webkitRequestFullscreen()
}
if (el.msRequestFullscreen) {
el.msRequestFullscreen()
}
}
logout(e) { logout(e) {
e.preventDefault() e.preventDefault()
web.Logout() web.Logout()
@@ -87,24 +71,30 @@ export class BrowserDropdown extends React.Component {
<i className="fas fa-bars" /> <i className="fas fa-bars" />
</Dropdown.Toggle> </Dropdown.Toggle>
<Dropdown.Menu className="dropdown-menu-right"> <Dropdown.Menu className="dropdown-menu-right">
<li>
<a href="" onClick={this.showChangePassword.bind(this)}>
Change Password <i className="fas fa-cog" />
</a>
{this.state.showChangePasswordModal && (
<ChangePasswordModal
serverInfo={serverInfo}
hideChangePassword={this.hideChangePassword.bind(this)}
/>
)}
</li>
<li>
<a target="_blank" href="https://docs.min.io/?ref=ob">
Documentation <i className="fas fa-book" />
</a>
</li>
<li> <li>
<a target="_blank" href="https://github.com/minio/minio"> <a target="_blank" href="https://github.com/minio/minio">
GitHub <i className="fab fa-github" /> GitHub <i className="fab fa-github" />
</a> </a>
</li> </li>
<li> <li>
<a href="" onClick={this.fullScreen}> <a target="_blank" href="https://min.io/pricing?ref=ob">
Fullscreen <i className="fas fa-expand" /> Get Support <i className="fas fa-question-circle" />
</a>
</li>
<li>
<a target="_blank" href="https://docs.min.io/">
Documentation <i className="fas fa-book" />
</a>
</li>
<li>
<a target="_blank" href="https://slack.min.io">
Ask for help <i className="fas fa-question-circle" />
</a> </a>
</li> </li>
<li> <li>
@@ -118,20 +108,9 @@ export class BrowserDropdown extends React.Component {
/> />
)} )}
</li> </li>
<li>
<a href="" onClick={this.showChangePassword.bind(this)}>
Change Password <i className="fas fa-cog" />
</a>
{this.state.showChangePasswordModal && (
<ChangePasswordModal
serverInfo={serverInfo}
hideChangePassword={this.hideChangePassword.bind(this)}
/>
)}
</li>
<li> <li>
<a href="" id="logout" onClick={this.logout}> <a href="" id="logout" onClick={this.logout}>
Sign Out <i className="fas fa-sign-out-alt" /> Logout <i className="fas fa-sign-out-alt" />
</a> </a>
</li> </li>
</Dropdown.Menu> </Dropdown.Menu>
+2
View File
@@ -15,6 +15,7 @@
*/ */
import React from "react" import React from "react"
import ObjectsSearch from "../objects/ObjectsSearch"
import Path from "../objects/Path" import Path from "../objects/Path"
import StorageInfo from "./StorageInfo" import StorageInfo from "./StorageInfo"
import BrowserDropdown from "./BrowserDropdown" import BrowserDropdown from "./BrowserDropdown"
@@ -27,6 +28,7 @@ export const Header = () => {
<header className="fe-header"> <header className="fe-header">
<Path /> <Path />
{loggedIn && <StorageInfo />} {loggedIn && <StorageInfo />}
{loggedIn && <ObjectsSearch />}
<ul className="feh-actions"> <ul className="feh-actions">
{loggedIn ? ( {loggedIn ? (
<BrowserDropdown /> <BrowserDropdown />
+2 -1
View File
@@ -22,7 +22,8 @@ const bucketsFilterSelector = state => state.buckets.filter
export const getFilteredBuckets = createSelector( export const getFilteredBuckets = createSelector(
bucketsSelector, bucketsSelector,
bucketsFilterSelector, bucketsFilterSelector,
(buckets, filter) => buckets.filter(bucket => bucket.indexOf(filter) > -1) (buckets, filter) => buckets.filter(
bucket => bucket.toLowerCase().indexOf(filter.toLowerCase()) > -1)
) )
export const getCurrentBucket = state => state.buckets.currentBucket export const getCurrentBucket = state => state.buckets.currentBucket
+13 -4
View File
@@ -18,6 +18,7 @@ import React from "react"
import { connect } from "react-redux" import { connect } from "react-redux"
import InfiniteScroll from "react-infinite-scroller" import InfiniteScroll from "react-infinite-scroller"
import ObjectsList from "./ObjectsList" import ObjectsList from "./ObjectsList"
import { getFilteredObjects } from "./selectors"
export class ObjectsListContainer extends React.Component { export class ObjectsListContainer extends React.Component {
constructor(props) { constructor(props) {
@@ -39,22 +40,29 @@ export class ObjectsListContainer extends React.Component {
}) })
} }
} }
componentDidUpdate(prevProps) {
if (this.props.filter !== prevProps.filter) {
this.setState({
page: 1
})
}
}
loadNextPage() { loadNextPage() {
this.setState(state => { this.setState(state => {
return { page: state.page + 1 } return { page: state.page + 1 }
}) })
} }
render() { render() {
const { objects, listLoading } = this.props const { filteredObjects, listLoading } = this.props
const visibleObjects = objects.slice(0, this.state.page * 100) const visibleObjects = filteredObjects.slice(0, this.state.page * 100)
return ( return (
<div style={{ position: "relative" }}> <div style={{ position: "relative" }}>
<InfiniteScroll <InfiniteScroll
pageStart={0} pageStart={0}
loadMore={this.loadNextPage} loadMore={this.loadNextPage}
hasMore={objects.length > visibleObjects.length} hasMore={filteredObjects.length > visibleObjects.length}
useWindow={true} useWindow={true}
initialLoad={false} initialLoad={false}
> >
@@ -70,7 +78,8 @@ const mapStateToProps = state => {
return { return {
currentBucket: state.buckets.currentBucket, currentBucket: state.buckets.currentBucket,
currentPrefix: state.objects.currentPrefix, currentPrefix: state.objects.currentPrefix,
objects: state.objects.list, filteredObjects: getFilteredObjects(state),
filter: state.objects.filter,
sortBy: state.objects.sortBy, sortBy: state.objects.sortBy,
sortOrder: state.objects.sortOrder, sortOrder: state.objects.sortOrder,
listLoading: state.objects.listLoading listLoading: state.objects.listLoading
+43
View File
@@ -0,0 +1,43 @@
/*
* MinIO Cloud Storage (C) 2020 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from "react"
import { connect } from "react-redux"
import * as actionsObjects from "./actions"
export const ObjectsSearch = ({ onChange }) => (
<div
className="input-group ig-left ig-search-dark"
style={{ display: "block" }}
>
<input
className="ig-text"
type="input"
placeholder="Search Objects..."
onChange={e => onChange(e.target.value)}
/>
<i className="ig-helpers" />
</div>
)
const mapDispatchToProps = dispatch => {
return {
onChange: filter =>
dispatch(actionsObjects.setFilter(filter))
}
}
export default connect(undefined, mapDispatchToProps)(ObjectsSearch)
@@ -20,13 +20,13 @@ import { ObjectsListContainer } from "../ObjectsListContainer"
describe("ObjectsList", () => { describe("ObjectsList", () => {
it("should render without crashing", () => { it("should render without crashing", () => {
shallow(<ObjectsListContainer objects={[]} />) shallow(<ObjectsListContainer filteredObjects={[]} />)
}) })
it("should render ObjectsList with objects", () => { it("should render ObjectsList with objects", () => {
const wrapper = shallow( const wrapper = shallow(
<ObjectsListContainer <ObjectsListContainer
objects={[{ name: "test1.jpg" }, { name: "test2.jpg" }]} filteredObjects={[{ name: "test1.jpg" }, { name: "test2.jpg" }]}
/> />
) )
expect(wrapper.find("ObjectsList").length).toBe(1) expect(wrapper.find("ObjectsList").length).toBe(1)
@@ -40,7 +40,7 @@ describe("ObjectsList", () => {
const wrapper = shallow( const wrapper = shallow(
<ObjectsListContainer <ObjectsListContainer
currentBucket="test1" currentBucket="test1"
objects={[]} filteredObjects={[]}
listLoading={true} listLoading={true}
/> />
) )
@@ -0,0 +1,32 @@
/*
* MinIO Cloud Storage (C) 2018 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from "react"
import { shallow } from "enzyme"
import { ObjectsSearch } from "../ObjectsSearch"
describe("ObjectsSearch", () => {
it("should render without crashing", () => {
shallow(<ObjectsSearch />)
})
it("should call onChange with search text", () => {
const onChange = jest.fn()
const wrapper = shallow(<ObjectsSearch onChange={onChange} />)
wrapper.find("input").simulate("change", { target: { value: "test" } })
expect(onChange).toHaveBeenCalledWith("test")
})
})
@@ -23,6 +23,7 @@ describe("objects reducer", () => {
const initialState = reducer(undefined, {}) const initialState = reducer(undefined, {})
expect(initialState).toEqual({ expect(initialState).toEqual({
list: [], list: [],
filter: "",
listLoading: false, listLoading: false,
sortBy: "", sortBy: "",
sortOrder: SORT_ORDER_ASC, sortOrder: SORT_ORDER_ASC,
+8
View File
@@ -36,6 +36,7 @@ import { getServerInfo, hasServerPublicDomain } from '../browser/selectors'
export const SET_LIST = "objects/SET_LIST" export const SET_LIST = "objects/SET_LIST"
export const RESET_LIST = "objects/RESET_LIST" export const RESET_LIST = "objects/RESET_LIST"
export const SET_FILTER = "objects/SET_FILTER"
export const APPEND_LIST = "objects/APPEND_LIST" export const APPEND_LIST = "objects/APPEND_LIST"
export const REMOVE = "objects/REMOVE" export const REMOVE = "objects/REMOVE"
export const SET_SORT_BY = "objects/SET_SORT_BY" export const SET_SORT_BY = "objects/SET_SORT_BY"
@@ -57,6 +58,13 @@ export const resetList = () => ({
type: RESET_LIST, type: RESET_LIST,
}) })
export const setFilter = filter => {
return {
type: SET_FILTER,
filter
}
}
export const setListLoading = (listLoading) => ({ export const setListLoading = (listLoading) => ({
type: SET_LIST_LOADING, type: SET_LIST_LOADING,
listLoading, listLoading,
+6
View File
@@ -28,6 +28,7 @@ const removeObject = (list, objectToRemove, lookup) => {
export default ( export default (
state = { state = {
list: [], list: [],
filter: "",
listLoading: false, listLoading: false,
sortBy: "", sortBy: "",
sortOrder: SORT_ORDER_ASC, sortOrder: SORT_ORDER_ASC,
@@ -53,6 +54,11 @@ export default (
...state, ...state,
list: [] list: []
} }
case actionsObjects.SET_FILTER:
return {
...state,
filter: action.filter
}
case actionsObjects.SET_LIST_LOADING: case actionsObjects.SET_LIST_LOADING:
return { return {
...state, ...state,
+10
View File
@@ -21,3 +21,13 @@ export const getCurrentPrefix = state => state.objects.currentPrefix
export const getCheckedList = state => state.objects.checkedList export const getCheckedList = state => state.objects.checkedList
export const getPrefixWritable = state => state.objects.prefixWritable export const getPrefixWritable = state => state.objects.prefixWritable
const objectsSelector = state => state.objects.list
const objectsFilterSelector = state => state.objects.filter
export const getFilteredObjects = createSelector(
objectsSelector,
objectsFilterSelector,
(objects, filter) => objects.filter(
object => object.name.toLowerCase().startsWith(filter.toLowerCase()))
)
+18
View File
@@ -169,6 +169,24 @@ select.form-control {
} }
} }
.ig-search-dark {
&:before {
font-family: @font-family-icon;
font-weight: 900;
content: '\f002';
font-size: 15px;
position: absolute;
left: 2px;
top: 8px;
color: rgba(0, 0, 0, 0.5);
}
.ig-text {
padding-left: 25px;
.placeholder(rgba(0, 0, 0, 0.5))
}
}
.ig-search { .ig-search {
&:before { &:before {
font-family: @font-family-icon; font-family: @font-family-icon;
+42 -31
View File
File diff suppressed because one or more lines are too long
+12 -43
View File
@@ -45,88 +45,63 @@ FUNCTIONAL_TESTS="$WORK_DIR/functional-tests.sh"
function start_minio_fs() function start_minio_fs()
{ {
"${MINIO[@]}" server "${WORK_DIR}/fs-disk" >"$WORK_DIR/fs-minio.log" 2>&1 & "${MINIO[@]}" server "${WORK_DIR}/fs-disk" >"$WORK_DIR/fs-minio.log" 2>&1 &
minio_pid=$!
sleep 10 sleep 10
echo "$minio_pid"
} }
function start_minio_erasure() function start_minio_erasure()
{ {
"${MINIO[@]}" server "${WORK_DIR}/erasure-disk1" "${WORK_DIR}/erasure-disk2" "${WORK_DIR}/erasure-disk3" "${WORK_DIR}/erasure-disk4" >"$WORK_DIR/erasure-minio.log" 2>&1 & "${MINIO[@]}" server "${WORK_DIR}/erasure-disk1" "${WORK_DIR}/erasure-disk2" "${WORK_DIR}/erasure-disk3" "${WORK_DIR}/erasure-disk4" >"$WORK_DIR/erasure-minio.log" 2>&1 &
minio_pid=$!
sleep 15 sleep 15
echo "$minio_pid"
} }
function start_minio_erasure_sets() function start_minio_erasure_sets()
{ {
"${MINIO[@]}" server "${WORK_DIR}/erasure-disk-sets{1...32}" >"$WORK_DIR/erasure-minio-sets.log" 2>&1 & "${MINIO[@]}" server "${WORK_DIR}/erasure-disk-sets{1...32}" >"$WORK_DIR/erasure-minio-sets.log" 2>&1 &
minio_pid=$!
sleep 15 sleep 15
echo "$minio_pid"
} }
function start_minio_zone_erasure_sets() function start_minio_zone_erasure_sets()
{ {
declare -a minio_pids
export MINIO_ACCESS_KEY=$ACCESS_KEY export MINIO_ACCESS_KEY=$ACCESS_KEY
export MINIO_SECRET_KEY=$SECRET_KEY export MINIO_SECRET_KEY=$SECRET_KEY
"${MINIO[@]}" server --address=:9000 "http://127.0.0.1:9000${WORK_DIR}/zone-disk-sets{1...4}" "http://127.0.0.1:9001${WORK_DIR}/zone-disk-sets{5...8}" >"$WORK_DIR/zone-minio-9000.log" 2>&1 & "${MINIO[@]}" server --address=:9000 "http://127.0.0.1:9000${WORK_DIR}/zone-disk-sets{1...4}" "http://127.0.0.1:9001${WORK_DIR}/zone-disk-sets{5...8}" >"$WORK_DIR/zone-minio-9000.log" 2>&1 &
minio_pids[0]=$!
"${MINIO[@]}" server --address=:9001 "http://127.0.0.1:9000${WORK_DIR}/zone-disk-sets{1...4}" "http://127.0.0.1:9001${WORK_DIR}/zone-disk-sets{5...8}" >"$WORK_DIR/zone-minio-9001.log" 2>&1 & "${MINIO[@]}" server --address=:9001 "http://127.0.0.1:9000${WORK_DIR}/zone-disk-sets{1...4}" "http://127.0.0.1:9001${WORK_DIR}/zone-disk-sets{5...8}" >"$WORK_DIR/zone-minio-9001.log" 2>&1 &
minio_pids[1]=$!
sleep 40 sleep 40
echo "${minio_pids[@]}"
} }
function start_minio_zone_erasure_sets_ipv6() function start_minio_zone_erasure_sets_ipv6()
{ {
declare -a minio_pids
export MINIO_ACCESS_KEY=$ACCESS_KEY export MINIO_ACCESS_KEY=$ACCESS_KEY
export MINIO_SECRET_KEY=$SECRET_KEY export MINIO_SECRET_KEY=$SECRET_KEY
"${MINIO[@]}" server --address="[::1]:9000" "http://[::1]:9000${WORK_DIR}/zone-disk-sets{1...4}" "http://[::1]:9001${WORK_DIR}/zone-disk-sets{5...8}" >"$WORK_DIR/zone-minio-9000.log" 2>&1 & "${MINIO[@]}" server --address="[::1]:9000" "http://[::1]:9000${WORK_DIR}/zone-disk-sets{1...4}" "http://[::1]:9001${WORK_DIR}/zone-disk-sets{5...8}" >"$WORK_DIR/zone-minio-9000.log" 2>&1 &
minio_pids[0]=$!
"${MINIO[@]}" server --address="[::1]:9001" "http://[::1]:9000${WORK_DIR}/zone-disk-sets{1...4}" "http://[::1]:9001${WORK_DIR}/zone-disk-sets{5...8}" >"$WORK_DIR/zone-minio-9001.log" 2>&1 & "${MINIO[@]}" server --address="[::1]:9001" "http://[::1]:9000${WORK_DIR}/zone-disk-sets{1...4}" "http://[::1]:9001${WORK_DIR}/zone-disk-sets{5...8}" >"$WORK_DIR/zone-minio-9001.log" 2>&1 &
minio_pids[1]=$!
sleep 40 sleep 40
echo "${minio_pids[@]}"
} }
function start_minio_dist_erasure() function start_minio_dist_erasure()
{ {
declare -a minio_pids
export MINIO_ACCESS_KEY=$ACCESS_KEY export MINIO_ACCESS_KEY=$ACCESS_KEY
export MINIO_SECRET_KEY=$SECRET_KEY export MINIO_SECRET_KEY=$SECRET_KEY
"${MINIO[@]}" server --address=:9000 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9000.log" 2>&1 & "${MINIO[@]}" server --address=:9000 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9000.log" 2>&1 &
minio_pids[0]=$!
"${MINIO[@]}" server --address=:9001 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9001.log" 2>&1 & "${MINIO[@]}" server --address=:9001 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9001.log" 2>&1 &
minio_pids[1]=$!
"${MINIO[@]}" server --address=:9002 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9002.log" 2>&1 & "${MINIO[@]}" server --address=:9002 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9002.log" 2>&1 &
minio_pids[2]=$!
"${MINIO[@]}" server --address=:9003 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9003.log" 2>&1 & "${MINIO[@]}" server --address=:9003 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9003.log" 2>&1 &
minio_pids[3]=$!
sleep 40 sleep 40
echo "${minio_pids[@]}"
} }
function run_test_fs() function run_test_fs()
{ {
minio_pid="$(start_minio_fs)" start_minio_fs
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS") (cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
rv=$? rv=$?
kill "$minio_pid" pkill minio
sleep 3 sleep 3
if [ "$rv" -ne 0 ]; then if [ "$rv" -ne 0 ]; then
@@ -138,12 +113,12 @@ function run_test_fs()
} }
function run_test_erasure_sets() { function run_test_erasure_sets() {
minio_pid="$(start_minio_erasure_sets)" start_minio_erasure_sets
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS") (cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
rv=$? rv=$?
kill "$minio_pid" pkill minio
sleep 3 sleep 3
if [ "$rv" -ne 0 ]; then if [ "$rv" -ne 0 ]; then
@@ -156,14 +131,12 @@ function run_test_erasure_sets() {
function run_test_zone_erasure_sets() function run_test_zone_erasure_sets()
{ {
minio_pids=( $(start_minio_zone_erasure_sets) ) start_minio_zone_erasure_sets
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS") (cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
rv=$? rv=$?
for pid in "${minio_pids[@]}"; do pkill minio
kill "$pid"
done
sleep 3 sleep 3
if [ "$rv" -ne 0 ]; then if [ "$rv" -ne 0 ]; then
@@ -182,16 +155,14 @@ function run_test_zone_erasure_sets()
function run_test_zone_erasure_sets_ipv6() function run_test_zone_erasure_sets_ipv6()
{ {
minio_pids=( $(start_minio_zone_erasure_sets_ipv6) ) start_minio_zone_erasure_sets_ipv6
export SERVER_ENDPOINT="[::1]:9000" export SERVER_ENDPOINT="[::1]:9000"
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS") (cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
rv=$? rv=$?
for pid in "${minio_pids[@]}"; do pkill minio
kill "$pid"
done
sleep 3 sleep 3
if [ "$rv" -ne 0 ]; then if [ "$rv" -ne 0 ]; then
@@ -210,12 +181,12 @@ function run_test_zone_erasure_sets_ipv6()
function run_test_erasure() function run_test_erasure()
{ {
minio_pid="$(start_minio_erasure)" start_minio_erasure
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS") (cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
rv=$? rv=$?
kill "$minio_pid" pkill minio
sleep 3 sleep 3
if [ "$rv" -ne 0 ]; then if [ "$rv" -ne 0 ]; then
@@ -228,14 +199,12 @@ function run_test_erasure()
function run_test_dist_erasure() function run_test_dist_erasure()
{ {
minio_pids=( $(start_minio_dist_erasure) ) start_minio_dist_erasure
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS") (cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
rv=$? rv=$?
for pid in "${minio_pids[@]}"; do pkill minio
kill "$pid"
done
sleep 3 sleep 3
if [ "$rv" -ne 0 ]; then if [ "$rv" -ne 0 ]; then
+5
View File
@@ -226,6 +226,11 @@ func (a adminAPIHandlers) ListRemoteTargetsHandler(w http.ResponseWriter, r *htt
return return
} }
if bucket != "" { if bucket != "" {
// Check if bucket exists.
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
return
}
if _, err := globalBucketMetadataSys.GetBucketTargetsConfig(bucket); err != nil { if _, err := globalBucketMetadataSys.GetBucketTargetsConfig(bucket); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return return
+83 -59
View File
@@ -54,16 +54,13 @@ const (
maxEConfigJSONSize = 262272 maxEConfigJSONSize = 262272
) )
// Type-safe query params.
type mgmtQueryKey string
// Only valid query params for mgmt admin APIs. // Only valid query params for mgmt admin APIs.
const ( const (
mgmtBucket mgmtQueryKey = "bucket" mgmtBucket = "bucket"
mgmtPrefix = "prefix" mgmtPrefix = "prefix"
mgmtClientToken = "clientToken" mgmtClientToken = "clientToken"
mgmtForceStart = "forceStart" mgmtForceStart = "forceStart"
mgmtForceStop = "forceStop" mgmtForceStop = "forceStop"
) )
func updateServer(u *url.URL, sha256Sum []byte, lrTime time.Time, mode string) (us madmin.ServerUpdateStatus, err error) { func updateServer(u *url.URL, sha256Sum []byte, lrTime time.Time, mode string) (us madmin.ServerUpdateStatus, err error) {
@@ -339,23 +336,26 @@ func (a adminAPIHandlers) DataUsageInfoHandler(w http.ResponseWriter, r *http.Re
writeSuccessResponseJSON(w, dataUsageInfoJSON) writeSuccessResponseJSON(w, dataUsageInfoJSON)
} }
func lriToLockEntry(l lockRequesterInfo, resource, server string) *madmin.LockEntry { func lriToLockEntry(l lockRequesterInfo, resource, server string, rquorum, wquorum int) *madmin.LockEntry {
entry := &madmin.LockEntry{ entry := &madmin.LockEntry{
Timestamp: l.Timestamp, Timestamp: l.Timestamp,
Resource: resource, Resource: resource,
ServerList: []string{server}, ServerList: []string{server},
Source: l.Source, Source: l.Source,
Owner: l.Owner,
ID: l.UID, ID: l.UID,
} }
if l.Writer { if l.Writer {
entry.Type = "WRITE" entry.Type = "WRITE"
entry.Quorum = wquorum
} else { } else {
entry.Type = "READ" entry.Type = "READ"
entry.Quorum = rquorum
} }
return entry return entry
} }
func topLockEntries(peerLocks []*PeerLocks, count int) madmin.LockEntries { func topLockEntries(peerLocks []*PeerLocks, count int, rquorum, wquorum int, stale bool) madmin.LockEntries {
entryMap := make(map[string]*madmin.LockEntry) entryMap := make(map[string]*madmin.LockEntry)
for _, peerLock := range peerLocks { for _, peerLock := range peerLocks {
if peerLock == nil { if peerLock == nil {
@@ -367,20 +367,26 @@ func topLockEntries(peerLocks []*PeerLocks, count int) madmin.LockEntries {
if val, ok := entryMap[lockReqInfo.UID]; ok { if val, ok := entryMap[lockReqInfo.UID]; ok {
val.ServerList = append(val.ServerList, peerLock.Addr) val.ServerList = append(val.ServerList, peerLock.Addr)
} else { } else {
entryMap[lockReqInfo.UID] = lriToLockEntry(lockReqInfo, k, peerLock.Addr) entryMap[lockReqInfo.UID] = lriToLockEntry(lockReqInfo, k, peerLock.Addr, rquorum, wquorum)
} }
} }
} }
} }
} }
var lockEntries = make(madmin.LockEntries, 0, len(entryMap)) var lockEntries madmin.LockEntries
for _, v := range entryMap { for _, v := range entryMap {
lockEntries = append(lockEntries, *v) if len(lockEntries) == count {
break
}
if stale {
lockEntries = append(lockEntries, *v)
continue
}
if len(v.ServerList) >= v.Quorum {
lockEntries = append(lockEntries, *v)
}
} }
sort.Sort(lockEntries) sort.Sort(lockEntries)
if len(lockEntries) > count {
lockEntries = lockEntries[:count]
}
return lockEntries return lockEntries
} }
@@ -410,21 +416,14 @@ func (a adminAPIHandlers) TopLocksHandler(w http.ResponseWriter, r *http.Request
return return
} }
} }
stale := r.URL.Query().Get("stale") == "true" // list also stale locks
peerLocks := globalNotificationSys.GetLocks(ctx) peerLocks := globalNotificationSys.GetLocks(ctx, r)
// Once we have received all the locks currently used from peers
// add the local peer locks list as well.
var getRespLocks GetLocksResp
for _, llocker := range globalLockServers {
getRespLocks = append(getRespLocks, llocker.DupLockMap())
}
peerLocks = append(peerLocks, &PeerLocks{ rquorum := getReadQuorum(objectAPI.SetDriveCount())
Addr: getHostName(r), wquorum := getWriteQuorum(objectAPI.SetDriveCount())
Locks: getRespLocks,
})
topLocks := topLockEntries(peerLocks, count) topLocks := topLockEntries(peerLocks, count, rquorum, wquorum, stale)
// Marshal API response // Marshal API response
jsonBytes, err := json.Marshal(topLocks) jsonBytes, err := json.Marshal(topLocks)
@@ -572,8 +571,8 @@ type healInitParams struct {
// extractHealInitParams - Validates params for heal init API. // extractHealInitParams - Validates params for heal init API.
func extractHealInitParams(vars map[string]string, qParms url.Values, r io.Reader) (hip healInitParams, err APIErrorCode) { func extractHealInitParams(vars map[string]string, qParms url.Values, r io.Reader) (hip healInitParams, err APIErrorCode) {
hip.bucket = vars[string(mgmtBucket)] hip.bucket = vars[mgmtBucket]
hip.objPrefix = vars[string(mgmtPrefix)] hip.objPrefix = vars[mgmtPrefix]
if hip.bucket == "" { if hip.bucket == "" {
if hip.objPrefix != "" { if hip.objPrefix != "" {
@@ -592,13 +591,13 @@ func extractHealInitParams(vars map[string]string, qParms url.Values, r io.Reade
return return
} }
if len(qParms[string(mgmtClientToken)]) > 0 { if len(qParms[mgmtClientToken]) > 0 {
hip.clientToken = qParms[string(mgmtClientToken)][0] hip.clientToken = qParms[mgmtClientToken][0]
} }
if _, ok := qParms[string(mgmtForceStart)]; ok { if _, ok := qParms[mgmtForceStart]; ok {
hip.forceStart = true hip.forceStart = true
} }
if _, ok := qParms[string(mgmtForceStop)]; ok { if _, ok := qParms[mgmtForceStop]; ok {
hip.forceStop = true hip.forceStop = true
} }
@@ -799,14 +798,12 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
keepConnLive(w, r, respCh) keepConnLive(w, r, respCh)
} }
func getAggregatedBackgroundHealState(ctx context.Context, failOnErr bool) (madmin.BgHealState, error) { func getAggregatedBackgroundHealState(ctx context.Context) (madmin.BgHealState, error) {
var bgHealStates []madmin.BgHealState var bgHealStates []madmin.BgHealState
localHealState, ok := getLocalBackgroundHealStatus() localHealState, ok := getLocalBackgroundHealStatus()
if !ok { if !ok {
if failOnErr { return madmin.BgHealState{}, errServerNotInitialized
return madmin.BgHealState{}, errServerNotInitialized
}
} }
// Get local heal status first // Get local heal status first
@@ -815,14 +812,16 @@ func getAggregatedBackgroundHealState(ctx context.Context, failOnErr bool) (madm
if globalIsDistErasure { if globalIsDistErasure {
// Get heal status from other peers // Get heal status from other peers
peersHealStates, nerrs := globalNotificationSys.BackgroundHealStatus() peersHealStates, nerrs := globalNotificationSys.BackgroundHealStatus()
var errCount int
for _, nerr := range nerrs { for _, nerr := range nerrs {
if nerr.Err != nil { if nerr.Err != nil {
if failOnErr {
return madmin.BgHealState{}, nerr.Err
}
logger.LogIf(ctx, nerr.Err) logger.LogIf(ctx, nerr.Err)
errCount++
} }
} }
if errCount == len(nerrs) {
return madmin.BgHealState{}, fmt.Errorf("all remote servers failed to report heal status, cluster is unhealthy")
}
bgHealStates = append(bgHealStates, peersHealStates...) bgHealStates = append(bgHealStates, peersHealStates...)
} }
@@ -868,7 +867,12 @@ func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r *
return return
} }
aggregateHealStateResult, _ := getAggregatedBackgroundHealState(r.Context(), false) aggregateHealStateResult, err := getAggregatedBackgroundHealState(r.Context())
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
if err := json.NewEncoder(w).Encode(aggregateHealStateResult); err != nil { if err := json.NewEncoder(w).Encode(aggregateHealStateResult); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return return
@@ -1236,7 +1240,7 @@ func (a adminAPIHandlers) OBDInfoHandler(w http.ResponseWriter, r *http.Request)
return return
} }
vars := mux.Vars(r) query := r.URL.Query()
obdInfo := madmin.OBDInfo{} obdInfo := madmin.OBDInfo{}
obdInfoCh := make(chan madmin.OBDInfo) obdInfoCh := make(chan madmin.OBDInfo)
@@ -1282,7 +1286,13 @@ func (a adminAPIHandlers) OBDInfoHandler(w http.ResponseWriter, r *http.Request)
go func() { go func() {
defer close(obdInfoCh) defer close(obdInfoCh)
if cpu, ok := vars["syscpu"]; ok && cpu == "true" { if log := query.Get("log"); log == "true" {
obdInfo.Logging.ServersLog = append(obdInfo.Logging.ServersLog, getLocalLogOBD(deadlinedCtx, r))
obdInfo.Logging.ServersLog = append(obdInfo.Logging.ServersLog, globalNotificationSys.LogOBDInfo(deadlinedCtx)...)
partialWrite(obdInfo)
}
if cpu := query.Get("syscpu"); cpu == "true" {
cpuInfo := getLocalCPUOBDInfo(deadlinedCtx, r) cpuInfo := getLocalCPUOBDInfo(deadlinedCtx, r)
obdInfo.Sys.CPUInfo = append(obdInfo.Sys.CPUInfo, cpuInfo) obdInfo.Sys.CPUInfo = append(obdInfo.Sys.CPUInfo, cpuInfo)
@@ -1290,7 +1300,7 @@ func (a adminAPIHandlers) OBDInfoHandler(w http.ResponseWriter, r *http.Request)
partialWrite(obdInfo) partialWrite(obdInfo)
} }
if diskHw, ok := vars["sysdiskhw"]; ok && diskHw == "true" { if diskHw := query.Get("sysdiskhw"); diskHw == "true" {
diskHwInfo := getLocalDiskHwOBD(deadlinedCtx, r) diskHwInfo := getLocalDiskHwOBD(deadlinedCtx, r)
obdInfo.Sys.DiskHwInfo = append(obdInfo.Sys.DiskHwInfo, diskHwInfo) obdInfo.Sys.DiskHwInfo = append(obdInfo.Sys.DiskHwInfo, diskHwInfo)
@@ -1298,7 +1308,7 @@ func (a adminAPIHandlers) OBDInfoHandler(w http.ResponseWriter, r *http.Request)
partialWrite(obdInfo) partialWrite(obdInfo)
} }
if osInfo, ok := vars["sysosinfo"]; ok && osInfo == "true" { if osInfo := query.Get("sysosinfo"); osInfo == "true" {
osInfo := getLocalOsInfoOBD(deadlinedCtx, r) osInfo := getLocalOsInfoOBD(deadlinedCtx, r)
obdInfo.Sys.OsInfo = append(obdInfo.Sys.OsInfo, osInfo) obdInfo.Sys.OsInfo = append(obdInfo.Sys.OsInfo, osInfo)
@@ -1306,7 +1316,7 @@ func (a adminAPIHandlers) OBDInfoHandler(w http.ResponseWriter, r *http.Request)
partialWrite(obdInfo) partialWrite(obdInfo)
} }
if mem, ok := vars["sysmem"]; ok && mem == "true" { if mem := query.Get("sysmem"); mem == "true" {
memInfo := getLocalMemOBD(deadlinedCtx, r) memInfo := getLocalMemOBD(deadlinedCtx, r)
obdInfo.Sys.MemInfo = append(obdInfo.Sys.MemInfo, memInfo) obdInfo.Sys.MemInfo = append(obdInfo.Sys.MemInfo, memInfo)
@@ -1314,7 +1324,7 @@ func (a adminAPIHandlers) OBDInfoHandler(w http.ResponseWriter, r *http.Request)
partialWrite(obdInfo) partialWrite(obdInfo)
} }
if proc, ok := vars["sysprocess"]; ok && proc == "true" { if proc := query.Get("sysprocess"); proc == "true" {
procInfo := getLocalProcOBD(deadlinedCtx, r) procInfo := getLocalProcOBD(deadlinedCtx, r)
obdInfo.Sys.ProcInfo = append(obdInfo.Sys.ProcInfo, procInfo) obdInfo.Sys.ProcInfo = append(obdInfo.Sys.ProcInfo, procInfo)
@@ -1322,14 +1332,14 @@ func (a adminAPIHandlers) OBDInfoHandler(w http.ResponseWriter, r *http.Request)
partialWrite(obdInfo) partialWrite(obdInfo)
} }
if config, ok := vars["minioconfig"]; ok && config == "true" { if config := query.Get("minioconfig"); config == "true" {
cfg, err := readServerConfig(ctx, objectAPI) cfg, err := readServerConfig(ctx, objectAPI)
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
obdInfo.Minio.Config = cfg obdInfo.Minio.Config = cfg
partialWrite(obdInfo) partialWrite(obdInfo)
} }
if drive, ok := vars["perfdrive"]; ok && drive == "true" { if drive := query.Get("perfdrive"); drive == "true" {
// Get drive obd details from local server's drive(s) // Get drive obd details from local server's drive(s)
driveOBDSerial := getLocalDrivesOBD(deadlinedCtx, false, globalEndpoints, r) driveOBDSerial := getLocalDrivesOBD(deadlinedCtx, false, globalEndpoints, r)
driveOBDParallel := getLocalDrivesOBD(deadlinedCtx, true, globalEndpoints, r) driveOBDParallel := getLocalDrivesOBD(deadlinedCtx, true, globalEndpoints, r)
@@ -1360,7 +1370,7 @@ func (a adminAPIHandlers) OBDInfoHandler(w http.ResponseWriter, r *http.Request)
partialWrite(obdInfo) partialWrite(obdInfo)
} }
if net, ok := vars["perfnet"]; ok && net == "true" && globalIsDistErasure { if net := query.Get("perfnet"); net == "true" && globalIsDistErasure {
obdInfo.Perf.Net = append(obdInfo.Perf.Net, globalNotificationSys.NetOBDInfo(deadlinedCtx)) obdInfo.Perf.Net = append(obdInfo.Perf.Net, globalNotificationSys.NetOBDInfo(deadlinedCtx))
partialWrite(obdInfo) partialWrite(obdInfo)
@@ -1374,6 +1384,7 @@ func (a adminAPIHandlers) OBDInfoHandler(w http.ResponseWriter, r *http.Request)
obdInfo.Perf.NetParallel = globalNotificationSys.NetOBDParallelInfo(deadlinedCtx) obdInfo.Perf.NetParallel = globalNotificationSys.NetOBDParallelInfo(deadlinedCtx)
partialWrite(obdInfo) partialWrite(obdInfo)
} }
}() }()
ticker := time.NewTicker(30 * time.Second) ticker := time.NewTicker(30 * time.Second)
@@ -1489,20 +1500,34 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
Notifications: notifyTarget, Notifications: notifyTarget,
} }
// Collect any disk healing.
healing, _ := getAggregatedBackgroundHealState(ctx)
healDisks := make(map[string]struct{}, len(healing.HealDisks))
for _, disk := range healing.HealDisks {
healDisks[disk] = struct{}{}
}
// find all disks which belong to each respective endpoints // find all disks which belong to each respective endpoints
for i := range servers { for i := range servers {
for _, disk := range storageInfo.Disks { for _, disk := range storageInfo.Disks {
if strings.Contains(disk.Endpoint, servers[i].Endpoint) { if strings.Contains(disk.Endpoint, servers[i].Endpoint) {
if _, ok := healDisks[disk.Endpoint]; ok {
disk.Healing = true
}
servers[i].Disks = append(servers[i].Disks, disk) servers[i].Disks = append(servers[i].Disks, disk)
} }
} }
} }
// add all the disks local to this server. // add all the disks local to this server.
for _, disk := range storageInfo.Disks { for _, disk := range storageInfo.Disks {
if disk.DrivePath == "" && disk.Endpoint == "" { if disk.DrivePath == "" && disk.Endpoint == "" {
continue continue
} }
if disk.Endpoint == disk.DrivePath { if disk.Endpoint == disk.DrivePath {
if _, ok := healDisks[disk.Endpoint]; ok {
disk.Healing = true
}
servers[len(servers)-1].Disks = append(servers[len(servers)-1].Disks, disk) servers[len(servers)-1].Disks = append(servers[len(servers)-1].Disks, disk)
} }
} }
@@ -1538,7 +1563,7 @@ func fetchLambdaInfo(cfg config.Config) []map[string][]madmin.TargetIDStatus {
// Fetch the configured targets // Fetch the configured targets
tr := NewGatewayHTTPTransport() tr := NewGatewayHTTPTransport()
defer tr.CloseIdleConnections() defer tr.CloseIdleConnections()
targetList, err := notify.FetchRegisteredTargets(cfg, GlobalContext.Done(), tr, true, false) targetList, err := notify.FetchRegisteredTargets(GlobalContext, cfg, tr, true, false)
if err != nil && err != notify.ErrTargetsOffline { if err != nil && err != notify.ErrTargetsOffline {
logger.LogIf(GlobalContext, err) logger.LogIf(GlobalContext, err)
return nil return nil
@@ -1658,11 +1683,6 @@ func checkConnection(endpointStr string, timeout time.Duration) error {
ctx, cancel := context.WithTimeout(GlobalContext, timeout) ctx, cancel := context.WithTimeout(GlobalContext, timeout)
defer cancel() defer cancel()
req, err := http.NewRequest(http.MethodHead, endpointStr, nil)
if err != nil {
return err
}
client := &http.Client{Transport: &http.Transport{ client := &http.Client{Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment, Proxy: http.ProxyFromEnvironment,
DialContext: xhttp.NewCustomDialContext(timeout), DialContext: xhttp.NewCustomDialContext(timeout),
@@ -1677,11 +1697,15 @@ func checkConnection(endpointStr string, timeout time.Duration) error {
}} }}
defer client.CloseIdleConnections() defer client.CloseIdleConnections()
resp, err := client.Do(req.WithContext(ctx)) req, err := http.NewRequestWithContext(ctx, http.MethodHead, endpointStr, nil)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil { if err != nil {
return err return err
} }
defer xhttp.DrainBody(resp.Body) defer xhttp.DrainBody(resp.Body)
resp.Body.Close()
return nil return nil
} }
+6 -6
View File
@@ -327,13 +327,13 @@ func TestExtractHealInitParams(t *testing.T) {
mkParams := func(clientToken string, forceStart, forceStop bool) url.Values { mkParams := func(clientToken string, forceStart, forceStop bool) url.Values {
v := url.Values{} v := url.Values{}
if clientToken != "" { if clientToken != "" {
v.Add(string(mgmtClientToken), clientToken) v.Add(mgmtClientToken, clientToken)
} }
if forceStart { if forceStart {
v.Add(string(mgmtForceStart), "") v.Add(mgmtForceStart, "")
} }
if forceStop { if forceStop {
v.Add(string(mgmtForceStop), "") v.Add(mgmtForceStop, "")
} }
return v return v
} }
@@ -351,11 +351,11 @@ func TestExtractHealInitParams(t *testing.T) {
} }
varsArr := []map[string]string{ varsArr := []map[string]string{
// Invalid cases // Invalid cases
{string(mgmtPrefix): "objprefix"}, {mgmtPrefix: "objprefix"},
// Valid cases // Valid cases
{}, {},
{string(mgmtBucket): "bucket"}, {mgmtBucket: "bucket"},
{string(mgmtBucket): "bucket", string(mgmtPrefix): "objprefix"}, {mgmtBucket: "bucket", mgmtPrefix: "objprefix"},
} }
// Body is always valid - we do not test JSON decoding. // Body is always valid - we do not test JSON decoding.
+29 -8
View File
@@ -89,13 +89,14 @@ type allHealState struct {
// map of heal path to heal sequence // map of heal path to heal sequence
healSeqMap map[string]*healSequence healSeqMap map[string]*healSequence
healLocalDisks []Endpoints healLocalDisks map[Endpoint]struct{}
} }
// newHealState - initialize global heal state management // newHealState - initialize global heal state management
func newHealState() *allHealState { func newHealState() *allHealState {
healState := &allHealState{ healState := &allHealState{
healSeqMap: make(map[string]*healSequence), healSeqMap: make(map[string]*healSequence),
healLocalDisks: map[Endpoint]struct{}{},
} }
go healState.periodicHealSeqsClean(GlobalContext) go healState.periodicHealSeqsClean(GlobalContext)
@@ -103,20 +104,40 @@ func newHealState() *allHealState {
return healState return healState
} }
func (ahs *allHealState) getHealLocalDisks() []Endpoints { func (ahs *allHealState) healDriveCount() int {
ahs.Lock() ahs.Lock()
defer ahs.Unlock() defer ahs.Unlock()
healLocalDisks := make([]Endpoints, len(ahs.healLocalDisks)) return len(ahs.healLocalDisks)
copy(healLocalDisks, ahs.healLocalDisks) }
func (ahs *allHealState) getHealLocalDisks() Endpoints {
ahs.Lock()
defer ahs.Unlock()
var healLocalDisks Endpoints
for ep := range ahs.healLocalDisks {
healLocalDisks = append(healLocalDisks, ep)
}
return healLocalDisks return healLocalDisks
} }
func (ahs *allHealState) updateHealLocalDisks(healLocalDisks []Endpoints) { func (ahs *allHealState) popHealLocalDisks(healLocalDisks ...Endpoint) {
ahs.Lock() ahs.Lock()
defer ahs.Unlock() defer ahs.Unlock()
ahs.healLocalDisks = healLocalDisks for _, ep := range healLocalDisks {
delete(ahs.healLocalDisks, ep)
}
}
func (ahs *allHealState) pushHealLocalDisks(healLocalDisks ...Endpoint) {
ahs.Lock()
defer ahs.Unlock()
for _, ep := range healLocalDisks {
ahs.healLocalDisks[ep] = struct{}{}
}
} }
func (ahs *allHealState) periodicHealSeqsClean(ctx context.Context) { func (ahs *allHealState) periodicHealSeqsClean(ctx context.Context) {
@@ -507,7 +528,7 @@ func (h *healSequence) hasEnded() bool {
if h.clientToken == bgHealingUUID { if h.clientToken == bgHealingUUID {
return false return false
} }
return len(h.currentStatus.Items) == 0 || h.currentStatus.Summary == healStoppedStatus || h.currentStatus.Summary == healFinishedStatus return !h.endTime.IsZero()
} }
// stops the heal sequence - safe to call multiple times. // stops the heal sequence - safe to call multiple times.
+2 -12
View File
@@ -212,18 +212,8 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
if !globalIsGateway { if !globalIsGateway {
// -- OBD API -- // -- OBD API --
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/obdinfo"). adminRouter.Methods(http.MethodGet).Path(adminVersion + "/obdinfo").
HandlerFunc(httpTraceHdrs(adminAPI.OBDInfoHandler)). HandlerFunc(httpTraceHdrs(adminAPI.OBDInfoHandler))
Queries("perfdrive", "{perfdrive:true|false}",
"perfnet", "{perfnet:true|false}",
"minioinfo", "{minioinfo:true|false}",
"minioconfig", "{minioconfig:true|false}",
"syscpu", "{syscpu:true|false}",
"sysdiskhw", "{sysdiskhw:true|false}",
"sysosinfo", "{sysosinfo:true|false}",
"sysmem", "{sysmem:true|false}",
"sysprocess", "{sysprocess:true|false}",
)
} }
} }
+10 -2
View File
@@ -29,7 +29,7 @@ import (
minio "github.com/minio/minio-go/v7" minio "github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/tags" "github.com/minio/minio-go/v7/pkg/tags"
"github.com/minio/minio/cmd/config/etcd/dns" "github.com/minio/minio/cmd/config/dns"
"github.com/minio/minio/cmd/crypto" "github.com/minio/minio/cmd/crypto"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
@@ -109,6 +109,7 @@ const (
ErrReplicationDestinationNotFoundError ErrReplicationDestinationNotFoundError
ErrReplicationDestinationMissingLock ErrReplicationDestinationMissingLock
ErrReplicationTargetNotFoundError ErrReplicationTargetNotFoundError
ErrReplicationRemoteConnectionError
ErrBucketRemoteIdenticalToSource ErrBucketRemoteIdenticalToSource
ErrBucketRemoteAlreadyExists ErrBucketRemoteAlreadyExists
ErrBucketRemoteArnTypeInvalid ErrBucketRemoteArnTypeInvalid
@@ -823,6 +824,11 @@ var errorCodes = errorCodeMap{
Description: "The replication target does not exist", Description: "The replication target does not exist",
HTTPStatusCode: http.StatusNotFound, HTTPStatusCode: http.StatusNotFound,
}, },
ErrReplicationRemoteConnectionError: {
Code: "XminioAdminReplicationRemoteConnectionError",
Description: "Remote service endpoint or target bucket not available",
HTTPStatusCode: http.StatusNotFound,
},
ErrBucketRemoteIdenticalToSource: { ErrBucketRemoteIdenticalToSource: {
Code: "XminioAdminRemoteIdenticalToSource", Code: "XminioAdminRemoteIdenticalToSource",
Description: "The remote target cannot be identical to source", Description: "The remote target cannot be identical to source",
@@ -965,7 +971,7 @@ var errorCodes = errorCodeMap{
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrMetadataTooLarge: { ErrMetadataTooLarge: {
Code: "InvalidArgument", Code: "MetadataTooLarge",
Description: "Your metadata headers exceed the maximum allowed metadata size.", Description: "Your metadata headers exceed the maximum allowed metadata size.",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
@@ -1906,6 +1912,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
apiErr = ErrReplicationDestinationMissingLock apiErr = ErrReplicationDestinationMissingLock
case BucketRemoteTargetNotFound: case BucketRemoteTargetNotFound:
apiErr = ErrReplicationTargetNotFoundError apiErr = ErrReplicationTargetNotFoundError
case BucketRemoteConnectionErr:
apiErr = ErrReplicationRemoteConnectionError
case BucketRemoteAlreadyExists: case BucketRemoteAlreadyExists:
apiErr = ErrBucketRemoteAlreadyExists apiErr = ErrBucketRemoteAlreadyExists
case BucketRemoteArnTypeInvalid: case BucketRemoteArnTypeInvalid:
+9 -8
View File
@@ -408,7 +408,7 @@ func getObjectLocation(r *http.Request, domains []string, bucket, object string)
// generates ListBucketsResponse from array of BucketInfo which can be // generates ListBucketsResponse from array of BucketInfo which can be
// serialized to match XML and JSON API spec output. // serialized to match XML and JSON API spec output.
func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse { func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
var listbuckets []Bucket listbuckets := make([]Bucket, 0, len(buckets))
var data = ListBucketsResponse{} var data = ListBucketsResponse{}
var owner = Owner{} var owner = Owner{}
@@ -428,8 +428,7 @@ 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 {
var versions []ObjectVersion versions := make([]ObjectVersion, 0, len(resp.Objects))
var prefixes []CommonPrefix
var owner = Owner{} var owner = Owner{}
var data = ListVersionsResponse{} var data = ListVersionsResponse{}
@@ -473,6 +472,7 @@ func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delim
data.VersionIDMarker = versionIDMarker data.VersionIDMarker = versionIDMarker
data.IsTruncated = resp.IsTruncated data.IsTruncated = resp.IsTruncated
prefixes := make([]CommonPrefix, 0, len(resp.Prefixes))
for _, prefix := range resp.Prefixes { for _, prefix := range resp.Prefixes {
var prefixItem = CommonPrefix{} var prefixItem = CommonPrefix{}
prefixItem.Prefix = s3EncodeName(prefix, encodingType) prefixItem.Prefix = s3EncodeName(prefix, encodingType)
@@ -484,8 +484,7 @@ 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 {
var contents []Object contents := make([]Object, 0, len(resp.Objects))
var prefixes []CommonPrefix
var owner = Owner{} var owner = Owner{}
var data = ListObjectsResponse{} var data = ListObjectsResponse{}
@@ -517,9 +516,10 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingTy
data.Marker = s3EncodeName(marker, encodingType) data.Marker = s3EncodeName(marker, encodingType)
data.Delimiter = s3EncodeName(delimiter, encodingType) data.Delimiter = s3EncodeName(delimiter, encodingType)
data.MaxKeys = maxKeys data.MaxKeys = maxKeys
data.NextMarker = s3EncodeName(resp.NextMarker, encodingType) data.NextMarker = s3EncodeName(resp.NextMarker, encodingType)
data.IsTruncated = resp.IsTruncated data.IsTruncated = resp.IsTruncated
prefixes := make([]CommonPrefix, 0, len(resp.Prefixes))
for _, prefix := range resp.Prefixes { for _, prefix := range resp.Prefixes {
var prefixItem = CommonPrefix{} var prefixItem = CommonPrefix{}
prefixItem.Prefix = s3EncodeName(prefix, encodingType) prefixItem.Prefix = s3EncodeName(prefix, encodingType)
@@ -531,8 +531,7 @@ 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 {
var contents []Object contents := make([]Object, 0, len(objects))
var commonPrefixes []CommonPrefix
var owner = Owner{} var owner = Owner{}
var data = ListObjectsV2Response{} var data = ListObjectsV2Response{}
@@ -585,6 +584,8 @@ func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter,
data.ContinuationToken = base64.StdEncoding.EncodeToString([]byte(token)) data.ContinuationToken = base64.StdEncoding.EncodeToString([]byte(token))
data.NextContinuationToken = base64.StdEncoding.EncodeToString([]byte(nextToken)) data.NextContinuationToken = base64.StdEncoding.EncodeToString([]byte(nextToken))
data.IsTruncated = isTruncated data.IsTruncated = isTruncated
commonPrefixes := make([]CommonPrefix, 0, len(prefixes))
for _, prefix := range prefixes { for _, prefix := range prefixes {
var prefixItem = CommonPrefix{} var prefixItem = CommonPrefix{}
prefixItem.Prefix = s3EncodeName(prefix, encodingType) prefixItem.Prefix = s3EncodeName(prefix, encodingType)
+41 -14
View File
@@ -17,6 +17,7 @@
package cmd package cmd
import ( import (
"net"
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@@ -60,31 +61,53 @@ func newCachedObjectLayerFn() CacheObjectLayer {
type objectAPIHandlers struct { type objectAPIHandlers struct {
ObjectAPI func() ObjectLayer ObjectAPI func() ObjectLayer
CacheAPI func() CacheObjectLayer CacheAPI func() CacheObjectLayer
// Returns true of handlers should interpret encryption. }
EncryptionEnabled func() bool
// Returns true if handlers allow SSE-KMS encryption headers. // getHost tries its best to return the request host.
AllowSSEKMS func() bool // According to section 14.23 of RFC 2616 the Host header
// can include the port number if the default value of 80 is not used.
func getHost(r *http.Request) string {
if r.URL.IsAbs() {
return r.URL.Host
}
return r.Host
} }
// registerAPIRouter - registers S3 compatible APIs. // registerAPIRouter - registers S3 compatible APIs.
func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool) { func registerAPIRouter(router *mux.Router) {
// Initialize API. // Initialize API.
api := objectAPIHandlers{ api := objectAPIHandlers{
ObjectAPI: newObjectLayerFn, ObjectAPI: newObjectLayerFn,
CacheAPI: newCachedObjectLayerFn, CacheAPI: newCachedObjectLayerFn,
EncryptionEnabled: func() bool {
return encryptionEnabled
},
AllowSSEKMS: func() bool {
return allowSSEKMS
},
} }
// API Router // API Router
apiRouter := router.PathPrefix(SlashSeparator).Subrouter() apiRouter := router.PathPrefix(SlashSeparator).Subrouter()
var routers []*mux.Router var routers []*mux.Router
for _, domainName := range globalDomainNames { for _, domainName := range globalDomainNames {
routers = append(routers, apiRouter.Host("{bucket:.+}."+domainName).Subrouter()) if IsKubernetes() {
routers = append(routers, apiRouter.MatcherFunc(func(r *http.Request, match *mux.RouteMatch) bool {
host, _, err := net.SplitHostPort(getHost(r))
if err != nil {
host = r.Host
}
// Make sure to skip matching minio.<domain>` this is
// specifically meant for operator/k8s deployment
// The reason we need to skip this is for a special
// usecase where we need to make sure that
// minio.<namespace>.svc.<cluster_domain> is ignored
// by the bucketDNS style to ensure that path style
// is available and honored at this domain.
//
// All other `<bucket>.<namespace>.svc.<cluster_domain>`
// makes sure that buckets are routed through this matcher
// to match for `<bucket>`
return host != minioReservedBucket+"."+domainName
}).Host("{bucket:.+}."+domainName).Subrouter())
} else {
routers = append(routers, apiRouter.Host("{bucket:.+}."+domainName).Subrouter())
}
} }
routers = append(routers, apiRouter.PathPrefix("/{bucket}").Subrouter()) routers = append(routers, apiRouter.PathPrefix("/{bucket}").Subrouter())
@@ -94,7 +117,10 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool)
bucket.Methods(http.MethodHead).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodHead).Path("/{object:.+}").HandlerFunc(
maxClients(collectAPIStats("headobject", httpTraceAll(api.HeadObjectHandler)))) maxClients(collectAPIStats("headobject", httpTraceAll(api.HeadObjectHandler))))
// CopyObjectPart // CopyObjectPart
bucket.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").HandlerFunc(maxClients(collectAPIStats("copyobjectpart", httpTraceAll(api.CopyObjectPartHandler)))).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}") bucket.Methods(http.MethodPut).Path("/{object:.+}").
HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").
HandlerFunc(maxClients(collectAPIStats("copyobjectpart", httpTraceAll(api.CopyObjectPartHandler)))).
Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
// PutObjectPart // PutObjectPart
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
maxClients(collectAPIStats("putobjectpart", httpTraceHdrs(api.PutObjectPartHandler)))).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}") maxClients(collectAPIStats("putobjectpart", httpTraceHdrs(api.PutObjectPartHandler)))).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
@@ -138,7 +164,8 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool)
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
maxClients(collectAPIStats("getobject", httpTraceHdrs(api.GetObjectHandler)))) maxClients(collectAPIStats("getobject", httpTraceHdrs(api.GetObjectHandler))))
// CopyObject // CopyObject
bucket.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").HandlerFunc(maxClients(collectAPIStats("copyobject", httpTraceAll(api.CopyObjectHandler)))) bucket.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").
HandlerFunc(maxClients(collectAPIStats("copyobject", httpTraceAll(api.CopyObjectHandler))))
// PutObjectRetention // PutObjectRetention
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
maxClients(collectAPIStats("putobjectretention", httpTraceAll(api.PutObjectRetentionHandler)))).Queries("retention", "") maxClients(collectAPIStats("putobjectretention", httpTraceAll(api.PutObjectRetentionHandler)))).Queries("retention", "")
+34
View File
@@ -346,8 +346,26 @@ func checkRequestAuthTypeToAccessKey(ctx context.Context, r *http.Request, actio
// Request is allowed return the appropriate access key. // Request is allowed return the appropriate access key.
return cred.AccessKey, owner, ErrNone return cred.AccessKey, owner, ErrNone
} }
if action == policy.ListBucketVersionsAction {
// In AWS S3 s3:ListBucket permission is same as s3:ListBucketVersions permission
// verify as a fallback.
if globalPolicySys.IsAllowed(policy.Args{
AccountName: cred.AccessKey,
Action: policy.ListBucketAction,
BucketName: bucketName,
ConditionValues: getConditionValues(r, locationConstraint, "", nil),
IsOwner: false,
ObjectName: objectName,
}) {
// Request is allowed return the appropriate access key.
return cred.AccessKey, owner, ErrNone
}
}
return cred.AccessKey, owner, ErrAccessDenied return cred.AccessKey, owner, ErrAccessDenied
} }
if globalIAMSys.IsAllowed(iampolicy.Args{ if globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
Action: iampolicy.Action(action), Action: iampolicy.Action(action),
@@ -360,6 +378,22 @@ func checkRequestAuthTypeToAccessKey(ctx context.Context, r *http.Request, actio
// Request is allowed return the appropriate access key. // Request is allowed return the appropriate access key.
return cred.AccessKey, owner, ErrNone return cred.AccessKey, owner, ErrNone
} }
if action == policy.ListBucketVersionsAction {
// In AWS S3 s3:ListBucket permission is same as s3:ListBucketVersions permission
// verify as a fallback.
if globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey,
Action: iampolicy.Action(policy.ListBucketAction),
BucketName: bucketName,
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
ObjectName: objectName,
IsOwner: owner,
Claims: claims,
}) {
// Request is allowed return the appropriate access key.
return cred.AccessKey, owner, ErrNone
}
}
return cred.AccessKey, owner, ErrAccessDenied return cred.AccessKey, owner, ErrAccessDenied
} }
-20
View File
@@ -21,7 +21,6 @@ import (
"path" "path"
"time" "time"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/madmin" "github.com/minio/minio/pkg/madmin"
) )
@@ -129,24 +128,5 @@ func healDiskFormat(ctx context.Context, objAPI ObjectLayer, opts madmin.HealOpt
return madmin.HealResultItem{}, err return madmin.HealResultItem{}, err
} }
// Healing succeeded notify the peers to reload format and re-initialize disks.
// We will not notify peers if healing is not required.
if err == nil {
// Notify servers in background and retry if needed.
go func() {
retry:
for _, nerr := range globalNotificationSys.ReloadFormat(opts.DryRun) {
if nerr.Err != nil {
if nerr.Err.Error() == errServerNotInitialized.Error() {
time.Sleep(time.Second)
goto retry
}
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
}()
}
return res, nil return res, nil
} }
+70 -90
View File
@@ -26,7 +26,7 @@ import (
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
) )
const defaultMonitorNewDiskInterval = time.Minute * 3 const defaultMonitorNewDiskInterval = time.Second * 10
func initAutoHeal(ctx context.Context, objAPI ObjectLayer) { func initAutoHeal(ctx context.Context, objAPI ObjectLayer) {
z, ok := objAPI.(*erasureZones) z, ok := objAPI.(*erasureZones)
@@ -36,15 +36,6 @@ func initAutoHeal(ctx context.Context, objAPI ObjectLayer) {
initBackgroundHealing(ctx, objAPI) // start quick background healing initBackgroundHealing(ctx, objAPI) // start quick background healing
localDisksInZoneHeal := getLocalDisksToHeal(objAPI)
globalBackgroundHealState.updateHealLocalDisks(localDisksInZoneHeal)
drivesToHeal := getDrivesToHealCount(localDisksInZoneHeal)
if drivesToHeal != 0 {
logger.Info(fmt.Sprintf("Found drives to heal %d, waiting until %s to heal the content...",
drivesToHeal, defaultMonitorNewDiskInterval))
}
var bgSeq *healSequence var bgSeq *healSequence
var found bool var found bool
@@ -56,7 +47,14 @@ func initAutoHeal(ctx context.Context, objAPI ObjectLayer) {
time.Sleep(time.Second) time.Sleep(time.Second)
} }
if drivesToHeal != 0 { for _, ep := range getLocalDisksToHeal() {
globalBackgroundHealState.pushHealLocalDisks(ep)
}
if drivesToHeal := globalBackgroundHealState.healDriveCount(); drivesToHeal > 0 {
logger.Info(fmt.Sprintf("Found drives to heal %d, waiting until %s to heal the content...",
drivesToHeal, defaultMonitorNewDiskInterval))
// Heal any disk format and metadata early, if possible. // Heal any disk format and metadata early, if possible.
if err := bgSeq.healDiskMeta(); err != nil { if err := bgSeq.healDiskMeta(); err != nil {
if newObjectLayerFn() != nil { if newObjectLayerFn() != nil {
@@ -67,19 +65,11 @@ func initAutoHeal(ctx context.Context, objAPI ObjectLayer) {
} }
} }
go monitorLocalDisksAndHeal(ctx, z, drivesToHeal, localDisksInZoneHeal, bgSeq) go monitorLocalDisksAndHeal(ctx, z, bgSeq)
} }
func getLocalDisksToHeal(objAPI ObjectLayer) []Endpoints { func getLocalDisksToHeal() (disksToHeal Endpoints) {
z, ok := objAPI.(*erasureZones) for _, ep := range globalEndpoints {
if !ok {
return nil
}
// Attempt a heal as the server starts-up first.
localDisksInZoneHeal := make([]Endpoints, len(z.zones))
for i, ep := range globalEndpoints {
localDisksToHeal := Endpoints{}
for _, endpoint := range ep.Endpoints { for _, endpoint := range ep.Endpoints {
if !endpoint.IsLocal { if !endpoint.IsLocal {
continue continue
@@ -88,28 +78,14 @@ func getLocalDisksToHeal(objAPI ObjectLayer) []Endpoints {
// and reformat if the current disk is not formatted // and reformat if the current disk is not formatted
_, _, err := connectEndpoint(endpoint) _, _, err := connectEndpoint(endpoint)
if errors.Is(err, errUnformattedDisk) { if errors.Is(err, errUnformattedDisk) {
localDisksToHeal = append(localDisksToHeal, endpoint) disksToHeal = append(disksToHeal, endpoint)
} }
} }
if len(localDisksToHeal) == 0 {
continue
}
localDisksInZoneHeal[i] = localDisksToHeal
} }
return localDisksInZoneHeal return disksToHeal
} }
func getDrivesToHealCount(localDisksInZoneHeal []Endpoints) int {
var drivesToHeal int
for _, eps := range localDisksInZoneHeal {
for range eps {
drivesToHeal++
}
}
return drivesToHeal
}
func initBackgroundHealing(ctx context.Context, objAPI ObjectLayer) { func initBackgroundHealing(ctx context.Context, objAPI ObjectLayer) {
// Run the background healer // Run the background healer
globalBackgroundHealRoutine = newHealRoutine() globalBackgroundHealRoutine = newHealRoutine()
@@ -121,76 +97,80 @@ func initBackgroundHealing(ctx context.Context, objAPI ObjectLayer) {
// monitorLocalDisksAndHeal - ensures that detected new disks are healed // monitorLocalDisksAndHeal - ensures that detected new disks are healed
// 1. Only the concerned erasure set will be listed and healed // 1. Only the concerned erasure set will be listed and healed
// 2. Only the node hosting the disk is responsible to perform the heal // 2. Only the node hosting the disk is responsible to perform the heal
func monitorLocalDisksAndHeal(ctx context.Context, z *erasureZones, drivesToHeal int, localDisksInZoneHeal []Endpoints, bgSeq *healSequence) { func monitorLocalDisksAndHeal(ctx context.Context, z *erasureZones, bgSeq *healSequence) {
// Perform automatic disk healing when a disk is replaced locally. // Perform automatic disk healing when a disk is replaced locally.
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case <-time.After(defaultMonitorNewDiskInterval): case <-time.After(defaultMonitorNewDiskInterval):
// heal only if new disks found. waitForLowHTTPReq(int32(globalEndpoints.NEndpoints()), time.Second)
if drivesToHeal == 0 {
localDisksInZoneHeal = getLocalDisksToHeal(z)
drivesToHeal = getDrivesToHealCount(localDisksInZoneHeal)
if drivesToHeal == 0 {
// No drives to heal.
globalBackgroundHealState.updateHealLocalDisks(nil)
continue
}
globalBackgroundHealState.updateHealLocalDisks(localDisksInZoneHeal)
logger.Info(fmt.Sprintf("Found drives to heal %d, proceeding to heal content...",
drivesToHeal))
var erasureSetInZoneEndpointToHeal []map[int]Endpoints
healDisks := globalBackgroundHealState.getHealLocalDisks()
if len(healDisks) > 0 {
// Reformat disks // Reformat disks
bgSeq.sourceCh <- healSource{bucket: SlashSeparator} bgSeq.sourceCh <- healSource{bucket: SlashSeparator}
// Ensure that reformatting disks is finished // Ensure that reformatting disks is finished
bgSeq.sourceCh <- healSource{bucket: nopHeal} bgSeq.sourceCh <- healSource{bucket: nopHeal}
}
var erasureSetInZoneToHeal = make([][]int, len(localDisksInZoneHeal)) logger.Info(fmt.Sprintf("Found drives to heal %d, proceeding to heal content...",
// Compute the list of erasure set to heal len(healDisks)))
for i, localDisksToHeal := range localDisksInZoneHeal {
var erasureSetToHeal []int
for _, endpoint := range localDisksToHeal {
// Load the new format of this passed endpoint
_, format, err := connectEndpoint(endpoint)
if err != nil {
printEndpointError(endpoint, err, true)
continue
}
// Calculate the set index where the current endpoint belongs erasureSetInZoneEndpointToHeal = make([]map[int]Endpoints, len(z.zones))
setIndex, _, err := findDiskIndex(z.zones[i].format, format) for i := range z.zones {
if err != nil { erasureSetInZoneEndpointToHeal[i] = map[int]Endpoints{}
printEndpointError(endpoint, err, false)
continue
}
erasureSetToHeal = append(erasureSetToHeal, setIndex)
}
erasureSetInZoneToHeal[i] = erasureSetToHeal
}
logger.Info("New unformatted drives detected attempting to heal the content...")
for i, disks := range localDisksInZoneHeal {
for _, disk := range disks {
logger.Info("Healing disk '%s' on %s zone", disk, humanize.Ordinal(i+1))
} }
} }
// Heal all erasure sets that need // heal only if new disks found.
for i, erasureSetToHeal := range erasureSetInZoneToHeal { for _, endpoint := range healDisks {
for _, setIndex := range erasureSetToHeal { // Load the new format of this passed endpoint
err := healErasureSet(ctx, setIndex, z.zones[i].sets[setIndex], z.zones[i].setDriveCount) _, format, err := connectEndpoint(endpoint)
if err != nil { if err != nil {
logger.LogIf(ctx, err) printEndpointError(endpoint, err, true)
} continue
}
// Only upon success reduce the counter zoneIdx := globalEndpoints.GetLocalZoneIdx(endpoint)
if err == nil { if zoneIdx < 0 {
drivesToHeal-- continue
}
// Calculate the set index where the current endpoint belongs
setIndex, _, err := findDiskIndex(z.zones[zoneIdx].format, format)
if err != nil {
printEndpointError(endpoint, err, false)
continue
}
erasureSetInZoneEndpointToHeal[zoneIdx][setIndex] = append(erasureSetInZoneEndpointToHeal[zoneIdx][setIndex], endpoint)
}
for i, setMap := range erasureSetInZoneEndpointToHeal {
for setIndex, endpoints := range setMap {
for _, ep := range endpoints {
logger.Info("Healing disk '%s' on %s zone", ep, humanize.Ordinal(i+1))
buckets, err := z.ListBucketsHeal(ctx)
if err != nil {
logger.LogIf(ctx, err)
continue
}
if len(buckets) > 0 {
disks := z.zones[i].sets[setIndex].getLoadBalancedDisks()
if err := healErasureSet(ctx, setIndex, buckets, disks, z.zones[i].setDriveCount); err != nil {
logger.LogIf(ctx, err)
continue
}
}
logger.Info("Healing disk '%s' on %s zone complete", ep, humanize.Ordinal(i+1))
// Only upon success pop the healed disk.
globalBackgroundHealState.popHealLocalDisks(ep)
} }
} }
} }
+5 -4
View File
@@ -18,6 +18,7 @@ package cmd
import ( import (
"bytes" "bytes"
"context"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"hash" "hash"
@@ -80,7 +81,7 @@ func newStreamingBitrotWriter(disk StorageAPI, volume, filePath string, length i
bitrotSumsTotalSize := ceilFrac(length, shardSize) * int64(h.Size()) // Size used for storing bitrot checksums. bitrotSumsTotalSize := ceilFrac(length, shardSize) * int64(h.Size()) // Size used for storing bitrot checksums.
totalFileSize = bitrotSumsTotalSize + length totalFileSize = bitrotSumsTotalSize + length
} }
err := disk.CreateFile(volume, filePath, totalFileSize, r) err := disk.CreateFile(context.TODO(), volume, filePath, totalFileSize, r)
r.CloseWithError(err) r.CloseWithError(err)
close(bw.canClose) close(bw.canClose)
}() }()
@@ -118,7 +119,7 @@ func (b *streamingBitrotReader) ReadAt(buf []byte, offset int64) (int, error) {
// For the first ReadAt() call we need to open the stream for reading. // For the first ReadAt() call we need to open the stream for reading.
b.currOffset = offset b.currOffset = offset
streamOffset := (offset/b.shardSize)*int64(b.h.Size()) + offset streamOffset := (offset/b.shardSize)*int64(b.h.Size()) + offset
b.rc, err = b.disk.ReadFileStream(b.volume, b.filePath, streamOffset, b.tillOffset-streamOffset) b.rc, err = b.disk.ReadFileStream(context.TODO(), b.volume, b.filePath, streamOffset, b.tillOffset-streamOffset)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -139,8 +140,8 @@ func (b *streamingBitrotReader) ReadAt(buf []byte, offset int64) (int, error) {
b.h.Write(buf) b.h.Write(buf)
if !bytes.Equal(b.h.Sum(nil), b.hashBytes) { if !bytes.Equal(b.h.Sum(nil), b.hashBytes) {
err := &errHashMismatch{fmt.Sprintf("Disk: %s - content hash does not match - expected %s, got %s", err := &errHashMismatch{fmt.Sprintf("Disk: %s -> %s/%s - content hash does not match - expected %s, got %s",
b.disk, hex.EncodeToString(b.hashBytes), hex.EncodeToString(b.h.Sum(nil)))} b.disk, b.volume, b.filePath, hex.EncodeToString(b.hashBytes), hex.EncodeToString(b.h.Sum(nil)))}
logger.LogIf(GlobalContext, err) logger.LogIf(GlobalContext, err)
return 0, err return 0, err
} }
+5 -4
View File
@@ -17,6 +17,7 @@
package cmd package cmd
import ( import (
"context"
"fmt" "fmt"
"hash" "hash"
"io" "io"
@@ -34,7 +35,7 @@ type wholeBitrotWriter struct {
} }
func (b *wholeBitrotWriter) Write(p []byte) (int, error) { func (b *wholeBitrotWriter) Write(p []byte) (int, error) {
err := b.disk.AppendFile(b.volume, b.filePath, p) err := b.disk.AppendFile(context.TODO(), b.volume, b.filePath, p)
if err != nil { if err != nil {
logger.LogIf(GlobalContext, fmt.Errorf("Disk: %s returned %w", b.disk, err)) logger.LogIf(GlobalContext, fmt.Errorf("Disk: %s returned %w", b.disk, err))
return 0, err return 0, err
@@ -69,13 +70,13 @@ type wholeBitrotReader struct {
func (b *wholeBitrotReader) ReadAt(buf []byte, offset int64) (n int, err error) { func (b *wholeBitrotReader) ReadAt(buf []byte, offset int64) (n int, err error) {
if b.buf == nil { if b.buf == nil {
b.buf = make([]byte, b.tillOffset-offset) b.buf = make([]byte, b.tillOffset-offset)
if _, err := b.disk.ReadFile(b.volume, b.filePath, offset, b.buf, b.verifier); err != nil { if _, err := b.disk.ReadFile(context.TODO(), b.volume, b.filePath, offset, b.buf, b.verifier); err != nil {
logger.LogIf(GlobalContext, fmt.Errorf("Disk: %s returned %w", b.disk, err)) logger.LogIf(GlobalContext, fmt.Errorf("Disk: %s -> %s/%s returned %w", b.disk, b.volume, b.filePath, err))
return 0, err return 0, err
} }
} }
if len(b.buf) < len(buf) { if len(b.buf) < len(buf) {
logger.LogIf(GlobalContext, errLessData) logger.LogIf(GlobalContext, fmt.Errorf("Disk: %s -> %s/%s returned %w", b.disk, b.volume, b.filePath, errLessData))
return 0, errLessData return 0, errLessData
} }
n = copy(buf, b.buf) n = copy(buf, b.buf)
+2 -1
View File
@@ -17,6 +17,7 @@
package cmd package cmd
import ( import (
"context"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
@@ -39,7 +40,7 @@ func testBitrotReaderWriterAlgo(t *testing.T, bitrotAlgo BitrotAlgorithm) {
t.Fatal(err) t.Fatal(err)
} }
disk.MakeVol(volume) disk.MakeVol(context.Background(), volume)
writer := newBitrotWriter(disk, volume, filePath, 35, bitrotAlgo, 10) writer := newBitrotWriter(disk, volume, filePath, 35, bitrotAlgo, 10)
+17 -12
View File
@@ -131,7 +131,7 @@ func (client *bootstrapRESTClient) callWithContext(ctx context.Context, method s
values = make(url.Values) values = make(url.Values)
} }
respBody, err = client.restClient.CallWithContext(ctx, method, values, body, length) respBody, err = client.restClient.Call(ctx, method, values, body, length)
if err == nil { if err == nil {
return respBody, nil return respBody, nil
} }
@@ -178,17 +178,22 @@ func verifyServerSystemConfig(ctx context.Context, endpointZones EndpointZones)
} }
onlineServers++ onlineServers++
} }
// Sleep for a while - so that we don't go into select {
// 100% CPU when half the endpoints are offline. case <-ctx.Done():
time.Sleep(500 * time.Millisecond) return ctx.Err()
retries++ default:
// after 5 retries start logging that servers are not reachable yet // Sleep for a while - so that we don't go into
if retries >= 5 { // 100% CPU when half the endpoints are offline.
logger.Info(fmt.Sprintf("Waiting for atleast %d servers to be online for bootstrap check", len(clnts)/2)) time.Sleep(100 * time.Millisecond)
logger.Info(fmt.Sprintf("Following servers are currently offline or unreachable %s", offlineEndpoints)) retries++
retries = 0 // reset to log again after 5 retries. // after 5 retries start logging that servers are not reachable yet
if retries >= 5 {
logger.Info(fmt.Sprintf("Waiting for atleast %d remote servers to be online for bootstrap check", len(clnts)/2))
logger.Info(fmt.Sprintf("Following servers are currently offline or unreachable %s", offlineEndpoints))
retries = 0 // reset to log again after 5 retries.
}
offlineEndpoints = nil
} }
offlineEndpoints = nil
} }
return nil return nil
} }
@@ -234,7 +239,7 @@ func newBootstrapRESTClient(endpoint Endpoint) *bootstrapRESTClient {
ctx, cancel := context.WithTimeout(GlobalContext, restClient.HealthCheckTimeout) ctx, cancel := context.WithTimeout(GlobalContext, restClient.HealthCheckTimeout)
// Instantiate a new rest client for healthcheck // Instantiate a new rest client for healthcheck
// to avoid recursive healthCheckFn() // to avoid recursive healthCheckFn()
respBody, err := rest.NewClient(serverURL, trFn, newAuthToken).CallWithContext(ctx, bootstrapRESTMethodHealth, nil, nil, -1) respBody, err := rest.NewClient(serverURL, trFn, newAuthToken).Call(ctx, bootstrapRESTMethodHealth, nil, nil, -1)
xhttp.DrainBody(respBody) xhttp.DrainBody(respBody)
cancel() cancel()
var ne *rest.NetworkError var ne *rest.NetworkError
+34 -32
View File
@@ -33,7 +33,7 @@ import (
"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"
"github.com/minio/minio/cmd/config" "github.com/minio/minio/cmd/config"
"github.com/minio/minio/cmd/config/etcd/dns" "github.com/minio/minio/cmd/config/dns"
"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"
@@ -72,7 +72,7 @@ func initFederatorBackend(buckets []BucketInfo, objLayer ObjectLayer) {
// Get buckets in the DNS // Get buckets in the DNS
dnsBuckets, err := globalDNSConfig.List() dnsBuckets, err := globalDNSConfig.List()
if err != nil && err != dns.ErrNoEntriesFound { if err != nil && err != dns.ErrNoEntriesFound && err != dns.ErrNotImplemented {
logger.LogIf(GlobalContext, err) logger.LogIf(GlobalContext, err)
return return
} }
@@ -80,33 +80,35 @@ func initFederatorBackend(buckets []BucketInfo, objLayer ObjectLayer) {
bucketsSet := set.NewStringSet() bucketsSet := set.NewStringSet()
bucketsToBeUpdated := set.NewStringSet() bucketsToBeUpdated := set.NewStringSet()
bucketsInConflict := set.NewStringSet() bucketsInConflict := set.NewStringSet()
for _, bucket := range buckets { if dnsBuckets != nil {
bucketsSet.Add(bucket.Name) for _, bucket := range buckets {
r, ok := dnsBuckets[bucket.Name] bucketsSet.Add(bucket.Name)
if !ok { r, ok := dnsBuckets[bucket.Name]
bucketsToBeUpdated.Add(bucket.Name) if !ok {
continue bucketsToBeUpdated.Add(bucket.Name)
}
if !globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(r)...)).IsEmpty() {
if globalDomainIPs.Difference(set.CreateStringSet(getHostsSlice(r)...)).IsEmpty() {
// No difference in terms of domainIPs and nothing
// has changed so we don't change anything on the etcd.
continue continue
} }
// if domain IPs intersect then it won't be an empty set. if !globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(r)...)).IsEmpty() {
// such an intersection means that bucket exists on etcd. if globalDomainIPs.Difference(set.CreateStringSet(getHostsSlice(r)...)).IsEmpty() {
// but if we do see a difference with local domain IPs with // No difference in terms of domainIPs and nothing
// hostSlice from etcd then we should update with newer // has changed so we don't change anything on the etcd.
// domainIPs, we proceed to do that here. continue
bucketsToBeUpdated.Add(bucket.Name) }
continue // if domain IPs intersect then it won't be an empty set.
// such an intersection means that bucket exists on etcd.
// but if we do see a difference with local domain IPs with
// hostSlice from etcd then we should update with newer
// domainIPs, we proceed to do that here.
bucketsToBeUpdated.Add(bucket.Name)
continue
}
// No IPs seem to intersect, this means that bucket exists but has
// different IP addresses perhaps from a different deployment.
// bucket names are globally unique in federation at a given
// path prefix, name collision is not allowed. We simply log
// an error and continue.
bucketsInConflict.Add(bucket.Name)
} }
// No IPs seem to intersect, this means that bucket exists but has
// different IP addresses perhaps from a different deployment.
// bucket names are globally unique in federation at a given
// path prefix, name collision is not allowed. We simply log
// an error and continue.
bucketsInConflict.Add(bucket.Name)
} }
// Add/update buckets that are not registered with the DNS // Add/update buckets that are not registered with the DNS
@@ -562,7 +564,9 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
if globalDNSConfig != nil { if globalDNSConfig != nil {
sr, err := globalDNSConfig.Get(bucket) sr, err := globalDNSConfig.Get(bucket)
if err != nil { if err != nil {
if err == dns.ErrNoEntriesFound { // ErrNotImplemented indicates a DNS backend that doesn't need to check if bucket already
// exists elsewhere
if err == dns.ErrNoEntriesFound || err == dns.ErrNotImplemented {
// Proceed to creating a bucket. // Proceed to creating a bucket.
if err = objectAPI.MakeBucketWithLocation(ctx, bucket, opts); err != nil { if err = objectAPI.MakeBucketWithLocation(ctx, bucket, opts); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
@@ -655,16 +659,14 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL, guessIsBrowserReq(r))
return return
} }
if !api.EncryptionEnabled() && crypto.IsRequested(r.Header) {
if !objectAPI.IsEncryptionSupported() && crypto.IsRequested(r.Header) {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL, guessIsBrowserReq(r))
return return
} }
bucket := mux.Vars(r)["bucket"] bucket := mux.Vars(r)["bucket"]
// To detect if the client has disconnected.
r.Body = &contextReader{r.Body, r.Context()}
// Require Content-Length to be set in the request // Require Content-Length to be set in the request
size := r.ContentLength size := r.ContentLength
if size < 0 { if size < 0 {
@@ -1000,7 +1002,7 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
if globalDNSConfig != nil { if globalDNSConfig != nil {
if err := globalDNSConfig.Delete(bucket); err != nil { if err := globalDNSConfig.Delete(bucket); err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to delete bucket DNS entry %w, please delete it manually using etcdctl", err)) logger.LogIf(ctx, fmt.Errorf("Unable to delete bucket DNS entry %w, please delete it manually", err))
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return return
} }
+1 -1
View File
@@ -93,7 +93,7 @@ func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r
return return
} }
if s3Error := checkRequestAuthType(ctx, r, policy.ListBucketAction, bucket, ""); s3Error != ErrNone { if s3Error := checkRequestAuthType(ctx, r, policy.ListBucketVersionsAction, bucket, ""); s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
return return
} }
+1 -1
View File
@@ -354,7 +354,7 @@ func (b *BucketMetadata) Save(ctx context.Context, api ObjectLayer) error {
// deleteBucketMetadata deletes bucket metadata // deleteBucketMetadata deletes bucket metadata
// If config does not exist no error is returned. // If config does not exist no error is returned.
func deleteBucketMetadata(ctx context.Context, obj ObjectLayer, bucket string) error { func deleteBucketMetadata(ctx context.Context, obj objectDeleter, bucket string) error {
metadataFiles := []string{ metadataFiles := []string{
dataUsageCacheName, dataUsageCacheName,
bucketMetadataFile, bucketMetadataFile,
+2
View File
@@ -71,6 +71,8 @@ func (sys *BucketQuotaSys) check(ctx context.Context, bucket string, size int64)
sys.bucketStorageCache.Once.Do(func() { sys.bucketStorageCache.Once.Do(func() {
sys.bucketStorageCache.TTL = 1 * time.Second sys.bucketStorageCache.TTL = 1 * time.Second
sys.bucketStorageCache.Update = func() (interface{}, error) { sys.bucketStorageCache.Update = func() (interface{}, error) {
ctx, done := context.WithTimeout(context.Background(), 5*time.Second)
defer done()
return loadDataUsageFromBackend(ctx, objAPI) return loadDataUsageFromBackend(ctx, objAPI)
} }
}) })
+107 -18
View File
@@ -18,7 +18,9 @@ package cmd
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"runtime"
"strings" "strings"
"time" "time"
@@ -85,7 +87,7 @@ func mustReplicateWeb(ctx context.Context, r *http.Request, bucket, object strin
// mustReplicate returns true if object meets replication criteria. // mustReplicate returns true if object meets replication criteria.
func mustReplicate(ctx context.Context, r *http.Request, bucket, object string, meta map[string]string, replStatus string) bool { func mustReplicate(ctx context.Context, r *http.Request, bucket, object string, meta map[string]string, replStatus string) bool {
if s3Err := isPutActionAllowed(getRequestAuthType(r), bucket, object, r, iampolicy.GetReplicationConfigurationAction); s3Err != ErrNone { if s3Err := isPutActionAllowed(getRequestAuthType(r), bucket, "", r, iampolicy.GetReplicationConfigurationAction); s3Err != ErrNone {
return false return false
} }
return mustReplicater(ctx, r, bucket, object, meta, replStatus) return mustReplicater(ctx, r, bucket, object, meta, replStatus)
@@ -169,38 +171,53 @@ func putReplicationOpts(dest replication.Destination, objInfo ObjectInfo) (putOp
// 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, bucket, object, versionID string, objectAPI ObjectLayer, eventArg *eventArgs, healPending bool) { func replicateObject(ctx context.Context, objInfo ObjectInfo, objectAPI ObjectLayer) {
bucket := objInfo.Bucket
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)
return return
} }
tgt := globalBucketTargetSys.GetReplicationTargetClient(ctx, cfg.RoleArn) tgt := globalBucketTargetSys.GetReplicationTargetClient(ctx, cfg.RoleArn)
if tgt == nil { if tgt == nil {
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,
})
if err != nil { if err != nil {
return return
} }
defer gr.Close()
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()
return return
} }
dest := cfg.GetDestination() dest := cfg.GetDestination()
if dest.Bucket == "" { if dest.Bucket == "" {
gr.Close()
return return
} }
// if heal encounters a pending replication status, either replication
// has failed due to server shutdown or crawler and PutObject replication are in contention.
healPending := objInfo.ReplicationStatus == replication.Pending
// In the rare event that replication is in pending state either due to // In the rare event that replication is in pending state either due to
// server shut down/crash before replication completed or healing and PutObject // server shut down/crash before replication completed or healing and PutObject
// race - do an additional stat to see if the version ID exists // race - do an additional stat to see if the version ID exists
if healPending { if healPending {
_, err := tgt.StatObject(ctx, dest.Bucket, object, miniogo.StatObjectOptions{VersionID: objInfo.VersionID}) _, err := tgt.StatObject(ctx, dest.Bucket, object, miniogo.StatObjectOptions{VersionID: objInfo.VersionID})
if err == nil { if err == nil {
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
@@ -210,29 +227,35 @@ func replicateObject(ctx context.Context, bucket, object, versionID string, obje
replicationStatus := replication.Complete replicationStatus := replication.Complete
_, err = tgt.PutObject(ctx, dest.Bucket, object, gr, size, "", "", putOpts) _, err = tgt.PutObject(ctx, dest.Bucket, object, gr, size, "", "", putOpts)
gr.Close()
if err != nil { if err != nil {
replicationStatus = replication.Failed replicationStatus = replication.Failed
// Notify replication failure event.
if eventArg == nil {
eventArg = &eventArgs{
BucketName: bucket,
Object: objInfo,
Host: "Internal: [Replication]",
}
}
eventArg.EventName = event.OperationReplicationFailed
eventArg.Object.UserDefined[xhttp.AmzBucketReplicationStatus] = replicationStatus.String()
sendEvent(*eventArg)
} }
objInfo.UserDefined[xhttp.AmzBucketReplicationStatus] = replicationStatus.String() objInfo.UserDefined[xhttp.AmzBucketReplicationStatus] = replicationStatus.String()
if objInfo.UserTags != "" { if objInfo.UserTags != "" {
objInfo.UserDefined[xhttp.AmzObjectTagging] = objInfo.UserTags objInfo.UserDefined[xhttp.AmzObjectTagging] = objInfo.UserTags
} }
// FIXME: add support for missing replication events
// - event.ObjectReplicationNotTracked
// - event.ObjectReplicationMissedThreshold
// - event.ObjectReplicationReplicatedAfterThreshold
if replicationStatus == replication.Failed {
sendEvent(eventArgs{
EventName: event.ObjectReplicationFailed,
BucketName: bucket,
Object: objInfo,
Host: "Internal: [Replication]",
})
}
objInfo.metadataOnly = true // Perform only metadata updates. objInfo.metadataOnly = true // Perform only metadata updates.
if _, err = objectAPI.CopyObject(ctx, bucket, object, bucket, object, objInfo, ObjectOptions{ if _, err = objectAPI.CopyObject(ctx, bucket, object, bucket, object, objInfo, ObjectOptions{
VersionID: objInfo.VersionID, VersionID: objInfo.VersionID,
}, ObjectOptions{VersionID: objInfo.VersionID}); err != nil { }, ObjectOptions{
logger.LogIf(ctx, err) VersionID: objInfo.VersionID,
}); err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to update replication metadata for %s: %s", objInfo.VersionID, err))
} }
} }
@@ -258,3 +281,69 @@ func filterReplicationStatusMetadata(metadata map[string]string) map[string]stri
delKey(xhttp.AmzBucketReplicationStatus) delKey(xhttp.AmzBucketReplicationStatus)
return dst return dst
} }
type replicationState struct {
// add future metrics here
replicaCh chan ObjectInfo
}
func (r *replicationState) queueReplicaTask(oi ObjectInfo) {
select {
case r.replicaCh <- oi:
default:
}
}
var (
globalReplicationState *replicationState
// 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 {
// fix minimum concurrent replication to 1 for single CPU setup
if globalReplicationConcurrent == 0 {
globalReplicationConcurrent = 1
}
rs := &replicationState{
replicaCh: make(chan ObjectInfo, globalReplicationConcurrent*2),
}
go func() {
<-GlobalContext.Done()
close(rs.replicaCh)
}()
return rs
}
// addWorker creates a new worker to process tasks
func (r *replicationState) addWorker(ctx context.Context, objectAPI ObjectLayer) {
// Add a new worker.
go func() {
for {
select {
case <-ctx.Done():
return
case oi, ok := <-r.replicaCh:
if !ok {
return
}
replicateObject(ctx, oi, objectAPI)
}
}
}()
}
func initBackgroundReplication(ctx context.Context, objectAPI ObjectLayer) {
if globalReplicationState == nil {
return
}
// Start with globalReplicationConcurrent.
for i := 0; i < globalReplicationConcurrent; i++ {
globalReplicationState.addWorker(ctx, objectAPI)
}
}
+6 -5
View File
@@ -21,6 +21,7 @@ import (
"net/http" "net/http"
"sync" "sync"
minio "github.com/minio/minio-go/v7"
miniogo "github.com/minio/minio-go/v7" miniogo "github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials" "github.com/minio/minio-go/v7/pkg/credentials"
"github.com/minio/minio/pkg/bucket/versioning" "github.com/minio/minio/pkg/bucket/versioning"
@@ -91,13 +92,13 @@ func (sys *BucketTargetSys) SetTarget(ctx context.Context, bucket string, tgt *m
} }
vcfg, err := clnt.GetBucketVersioning(ctx, tgt.TargetBucket) vcfg, err := clnt.GetBucketVersioning(ctx, tgt.TargetBucket)
if err != nil { if err != nil {
if isErrBucketNotFound(err) { if minio.ToErrorResponse(err).Code == "NoSuchBucket" {
return BucketRemoteTargetNotFound{Bucket: tgt.TargetBucket} return BucketRemoteTargetNotFound{Bucket: tgt.TargetBucket}
} }
if vcfg.Status != string(versioning.Enabled) { return BucketRemoteConnectionErr{Bucket: tgt.TargetBucket}
return BucketReplicationTargetNotVersioned{Bucket: tgt.TargetBucket} }
} if vcfg.Status != string(versioning.Enabled) {
return err return BucketReplicationTargetNotVersioned{Bucket: tgt.TargetBucket}
} }
} }
+56 -8
View File
@@ -20,6 +20,7 @@ import (
"crypto/x509" "crypto/x509"
"encoding/gob" "encoding/gob"
"errors" "errors"
"fmt"
"net" "net"
"net/url" "net/url"
"os" "os"
@@ -42,15 +43,11 @@ func init() {
logger.Init(GOPATH, GOROOT) logger.Init(GOPATH, GOROOT)
logger.RegisterError(config.FmtError) logger.RegisterError(config.FmtError)
// Initialize globalConsoleSys system
globalConsoleSys = NewConsoleLogger(GlobalContext)
logger.AddTarget(globalConsoleSys)
gob.Register(StorageErr("")) gob.Register(StorageErr(""))
} }
func verifyObjectLayerFeatures(name string, objAPI ObjectLayer) { func verifyObjectLayerFeatures(name string, objAPI ObjectLayer) {
if (globalAutoEncryption || GlobalKMS != nil) && !objAPI.IsEncryptionSupported() { if (GlobalKMS != nil) && !objAPI.IsEncryptionSupported() {
logger.Fatal(errInvalidArgument, logger.Fatal(errInvalidArgument,
"Encryption support is requested but '%s' does not support encryption", name) "Encryption support is requested but '%s' does not support encryption", name)
} }
@@ -293,7 +290,7 @@ func logStartupMessage(msg string) {
logger.StartupMessage(msg) logger.StartupMessage(msg)
} }
func getTLSConfig() (x509Certs []*x509.Certificate, c *certs.Certs, secureConn bool, err error) { func getTLSConfig() (x509Certs []*x509.Certificate, manager *certs.Manager, secureConn bool, err error) {
if !(isFile(getPublicCertFile()) && isFile(getPrivateKeyFile())) { if !(isFile(getPublicCertFile()) && isFile(getPrivateKeyFile())) {
return nil, nil, false, nil return nil, nil, false, nil
} }
@@ -302,11 +299,62 @@ func getTLSConfig() (x509Certs []*x509.Certificate, c *certs.Certs, secureConn b
return nil, nil, false, err return nil, nil, false, err
} }
c, err = certs.New(getPublicCertFile(), getPrivateKeyFile(), config.LoadX509KeyPair) manager, err = certs.NewManager(GlobalContext, getPublicCertFile(), getPrivateKeyFile(), config.LoadX509KeyPair)
if err != nil { if err != nil {
return nil, nil, false, err return nil, nil, false, err
} }
// MinIO has support for multiple certificates. It expects the following structure:
// certs/
// │
// ├─ public.crt
// ├─ private.key
// │
// ├─ example.com/
// │ │
// │ ├─ public.crt
// │ └─ private.key
// └─ foobar.org/
// │
// ├─ public.crt
// └─ private.key
// ...
//
// Therefore, we read all filenames in the cert directory and check
// for each directory whether it contains a public.crt and private.key.
// If so, we try to add it to certificate manager.
root, err := os.Open(globalCertsDir.Get())
if err != nil {
return nil, nil, false, err
}
defer root.Close()
files, err := root.Readdir(-1)
if err != nil {
return nil, nil, false, err
}
for _, file := range files {
// We exclude any regular file and the "CAs/" directory.
// The "CAs/" directory contains (root) CA certificates
// that MinIO adds to its list of trusted roots (tls.Config.RootCAs).
// Therefore, "CAs/" does not contain X.509 certificates that
// are meant to be served by MinIO.
if !file.IsDir() || file.Name() == "CAs" {
continue
}
var (
certFile = filepath.Join(root.Name(), file.Name(), publicCertFile)
keyFile = filepath.Join(root.Name(), file.Name(), privateKeyFile)
)
if !isFile(certFile) || !isFile(keyFile) {
continue
}
if err := manager.AddCertificate(certFile, keyFile); err != nil {
err = fmt.Errorf("Failed to load TLS certificate '%s': %v", certFile, err)
logger.LogIf(GlobalContext, err, logger.Minio)
}
}
secureConn = true secureConn = true
return x509Certs, c, secureConn, nil return x509Certs, manager, secureConn, nil
} }
+5 -1
View File
@@ -46,7 +46,11 @@ func readConfig(ctx context.Context, objAPI ObjectLayer, configFile string) ([]b
return buffer.Bytes(), nil return buffer.Bytes(), nil
} }
func deleteConfig(ctx context.Context, objAPI ObjectLayer, configFile string) error { type objectDeleter interface {
DeleteObject(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error)
}
func deleteConfig(ctx context.Context, objAPI objectDeleter, configFile string) error {
_, err := objAPI.DeleteObject(ctx, minioMetaBucket, configFile, ObjectOptions{}) _, err := objAPI.DeleteObject(ctx, minioMetaBucket, configFile, ObjectOptions{})
if err != nil && isErrObjectNotFound(err) { if err != nil && isErrObjectNotFound(err) {
return errConfigNotFound return errConfigNotFound
+60 -19
View File
@@ -25,8 +25,9 @@ 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/etcd" "github.com/minio/minio/cmd/config/etcd"
"github.com/minio/minio/cmd/config/etcd/dns"
xldap "github.com/minio/minio/cmd/config/identity/ldap" xldap "github.com/minio/minio/cmd/config/identity/ldap"
"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"
@@ -55,6 +56,7 @@ func initHelp() {
config.KmsKesSubSys: crypto.DefaultKesKVS, config.KmsKesSubSys: crypto.DefaultKesKVS,
config.LoggerWebhookSubSys: logger.DefaultKVS, config.LoggerWebhookSubSys: logger.DefaultKVS,
config.AuditWebhookSubSys: logger.DefaultAuditKVS, config.AuditWebhookSubSys: logger.DefaultAuditKVS,
config.CrawlerSubSys: crawler.DefaultKVS,
} }
for k, v := range notify.DefaultNotificationKVS { for k, v := range notify.DefaultNotificationKVS {
kvs[k] = v kvs[k] = v
@@ -106,6 +108,10 @@ func initHelp() {
Key: config.APISubSys, Key: config.APISubSys,
Description: "manage global HTTP API call specific features, such as throttling, authentication types, etc.", Description: "manage global HTTP API call specific features, such as throttling, authentication types, etc.",
}, },
config.HelpKV{
Key: config.CrawlerSubSys,
Description: "manage continuous disk crawling for bucket disk usage, lifecycle, quota and data integrity checks",
},
config.HelpKV{ config.HelpKV{
Key: config.LoggerWebhookSubSys, Key: config.LoggerWebhookSubSys,
Description: "send server logs to webhook endpoints", Description: "send server logs to webhook endpoints",
@@ -185,6 +191,7 @@ func initHelp() {
config.EtcdSubSys: etcd.Help, config.EtcdSubSys: etcd.Help,
config.CacheSubSys: cache.Help, config.CacheSubSys: cache.Help,
config.CompressionSubSys: compress.Help, config.CompressionSubSys: compress.Help,
config.CrawlerSubSys: crawler.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,
@@ -246,6 +253,10 @@ func validateConfig(s config.Config, setDriveCount int) error {
return err return err
} }
if _, err := crawler.LookupConfig(s[config.CrawlerSubSys][config.Default]); err != nil {
return err
}
{ {
etcdCfg, err := etcd.LookupConfig(s[config.EtcdSubSys][config.Default], globalRootCAs) etcdCfg, err := etcd.LookupConfig(s[config.EtcdSubSys][config.Default], globalRootCAs)
if err != nil { if err != nil {
@@ -306,8 +317,7 @@ func validateConfig(s config.Config, setDriveCount int) error {
return err return err
} }
return notify.TestNotificationTargets(s, GlobalContext.Done(), NewGatewayHTTPTransport(), return notify.TestNotificationTargets(GlobalContext, s, NewGatewayHTTPTransport(), globalNotificationSys.ConfiguredTargetIDs())
globalNotificationSys.ConfiguredTargetIDs())
} }
func lookupConfigs(s config.Config, setDriveCount int) { func lookupConfigs(s config.Config, setDriveCount int) {
@@ -322,6 +332,19 @@ func lookupConfigs(s config.Config, setDriveCount int) {
} }
} }
if dnsURL, dnsUser, dnsPass, ok := env.LookupEnv(config.EnvDNSWebhook); ok {
globalDNSConfig, err = dns.NewOperatorDNS(dnsURL,
dns.Authentication(dnsUser, dnsPass),
dns.RootCAs(globalRootCAs))
if err != nil {
if globalIsGateway {
logger.FatalIf(err, "Unable to initialize remote webhook DNS config")
} else {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize remote webhook DNS config %w", err))
}
}
}
etcdCfg, err := etcd.LookupConfig(s[config.EtcdSubSys][config.Default], globalRootCAs) etcdCfg, err := etcd.LookupConfig(s[config.EtcdSubSys][config.Default], globalRootCAs)
if err != nil { if err != nil {
if globalIsGateway { if globalIsGateway {
@@ -343,19 +366,25 @@ func lookupConfigs(s config.Config, setDriveCount int) {
} }
} }
if len(globalDomainNames) != 0 && !globalDomainIPs.IsEmpty() && globalEtcdClient != nil && globalDNSConfig == nil { if len(globalDomainNames) != 0 && !globalDomainIPs.IsEmpty() && globalEtcdClient != nil {
globalDNSConfig, err = dns.NewCoreDNS(etcdCfg.Config, if globalDNSConfig != nil {
dns.DomainNames(globalDomainNames), // if global DNS is already configured, indicate with a warning, incase
dns.DomainIPs(globalDomainIPs), // users are confused.
dns.DomainPort(globalMinioPort), logger.LogIf(ctx, fmt.Errorf("DNS store is already configured with %s, not using etcd for DNS store", globalDNSConfig))
dns.CoreDNSPath(etcdCfg.CoreDNSPath), } else {
) globalDNSConfig, err = dns.NewCoreDNS(etcdCfg.Config,
if err != nil { dns.DomainNames(globalDomainNames),
if globalIsGateway { dns.DomainIPs(globalDomainIPs),
logger.FatalIf(err, "Unable to initialize DNS config") dns.DomainPort(globalMinioPort),
} else { dns.CoreDNSPath(etcdCfg.CoreDNSPath),
logger.LogIf(ctx, fmt.Errorf("Unable to initialize DNS config for %s: %w", )
globalDomainNames, err)) if err != nil {
if globalIsGateway {
logger.FatalIf(err, "Unable to initialize DNS config")
} else {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize DNS config for %s: %w",
globalDomainNames, err))
}
} }
} }
} }
@@ -378,7 +407,12 @@ func lookupConfigs(s config.Config, setDriveCount int) {
logger.LogIf(ctx, fmt.Errorf("Invalid api configuration: %w", err)) logger.LogIf(ctx, fmt.Errorf("Invalid api configuration: %w", err))
} }
globalAPIConfig.init(apiConfig) globalAPIConfig.init(apiConfig, setDriveCount)
// Initialize remote instance transport once.
getRemoteInstanceTransportOnce.Do(func() {
getRemoteInstanceTransport = newGatewayHTTPTransport(apiConfig.RemoteTransportDeadline)
})
if globalIsErasure { if globalIsErasure {
globalStorageClass, err = storageclass.LookupConfig(s[config.StorageClassSubSys][config.Default], setDriveCount) globalStorageClass, err = storageclass.LookupConfig(s[config.StorageClassSubSys][config.Default], setDriveCount)
@@ -404,6 +438,10 @@ func lookupConfigs(s config.Config, setDriveCount int) {
} }
} }
} }
globalCrawlerConfig, err = crawler.LookupConfig(s[config.CrawlerSubSys][config.Default])
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to read crawler config: %w", err))
}
kmsCfg, err := crypto.LookupConfig(s, globalCertsCADir.Get(), NewGatewayHTTPTransport()) kmsCfg, err := crypto.LookupConfig(s, globalCertsCADir.Get(), NewGatewayHTTPTransport())
if err != nil { if err != nil {
@@ -417,6 +455,9 @@ func lookupConfigs(s config.Config, setDriveCount int) {
// Enable auto-encryption if enabled // Enable auto-encryption if enabled
globalAutoEncryption = kmsCfg.AutoEncryption globalAutoEncryption = kmsCfg.AutoEncryption
if globalAutoEncryption && !globalIsGateway {
logger.LogIf(ctx, fmt.Errorf("%s env is deprecated please migrate to using `mc encrypt` at bucket level", crypto.EnvKMSAutoEncryption))
}
globalCompressConfig, err = compress.LookupConfig(s[config.CompressionSubSys][config.Default]) globalCompressConfig, err = compress.LookupConfig(s[config.CompressionSubSys][config.Default])
if err != nil { if err != nil {
@@ -484,12 +525,12 @@ func lookupConfigs(s config.Config, setDriveCount int) {
} }
} }
globalConfigTargetList, err = notify.GetNotificationTargets(s, GlobalContext.Done(), NewGatewayHTTPTransport(), false) globalConfigTargetList, err = notify.GetNotificationTargets(GlobalContext, s, NewGatewayHTTPTransport(), false)
if err != nil { if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize notification target(s): %w", err)) logger.LogIf(ctx, fmt.Errorf("Unable to initialize notification target(s): %w", err))
} }
globalEnvTargetList, err = notify.GetNotificationTargets(newServerConfig(), GlobalContext.Done(), NewGatewayHTTPTransport(), true) globalEnvTargetList, err = notify.GetNotificationTargets(GlobalContext, newServerConfig(), NewGatewayHTTPTransport(), true)
if err != nil { if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize notification target(s): %w", err)) logger.LogIf(ctx, fmt.Errorf("Unable to initialize notification target(s): %w", err))
} }
+41 -18
View File
@@ -29,15 +29,23 @@ import (
// API sub-system constants // API sub-system constants
const ( const (
apiRequestsMax = "requests_max" apiRequestsMax = "requests_max"
apiRequestsDeadline = "requests_deadline" apiRequestsDeadline = "requests_deadline"
apiReadyDeadline = "ready_deadline" apiClusterDeadline = "cluster_deadline"
apiCorsAllowOrigin = "cors_allow_origin" apiCorsAllowOrigin = "cors_allow_origin"
apiRemoteTransportDeadline = "remote_transport_deadline"
EnvAPIRequestsMax = "MINIO_API_REQUESTS_MAX" EnvAPIRequestsMax = "MINIO_API_REQUESTS_MAX"
EnvAPIRequestsDeadline = "MINIO_API_REQUESTS_DEADLINE" EnvAPIRequestsDeadline = "MINIO_API_REQUESTS_DEADLINE"
EnvAPIReadyDeadline = "MINIO_API_READY_DEADLINE" EnvAPIClusterDeadline = "MINIO_API_CLUSTER_DEADLINE"
EnvAPICorsAllowOrigin = "MINIO_API_CORS_ALLOW_ORIGIN" EnvAPICorsAllowOrigin = "MINIO_API_CORS_ALLOW_ORIGIN"
EnvAPIRemoteTransportDeadline = "MINIO_API_REMOTE_TRANSPORT_DEADLINE"
)
// Deprecated key and ENVs
const (
apiReadyDeadline = "ready_deadline"
EnvAPIReadyDeadline = "MINIO_API_READY_DEADLINE"
) )
// DefaultKVS - default storage class config // DefaultKVS - default storage class config
@@ -52,22 +60,27 @@ var (
Value: "10s", Value: "10s",
}, },
config.KV{ config.KV{
Key: apiReadyDeadline, Key: apiClusterDeadline,
Value: "10s", Value: "10s",
}, },
config.KV{ config.KV{
Key: apiCorsAllowOrigin, Key: apiCorsAllowOrigin,
Value: "*", Value: "*",
}, },
config.KV{
Key: apiRemoteTransportDeadline,
Value: "2h",
},
} }
) )
// Config storage class configuration // Config storage class configuration
type Config struct { type Config struct {
APIRequestsMax int `json:"requests_max"` RequestsMax int `json:"requests_max"`
APIRequestsDeadline time.Duration `json:"requests_deadline"` RequestsDeadline time.Duration `json:"requests_deadline"`
APIReadyDeadline time.Duration `json:"ready_deadline"` ClusterDeadline time.Duration `json:"cluster_deadline"`
APICorsAllowOrigin []string `json:"cors_allow_origin"` CorsAllowOrigin []string `json:"cors_allow_origin"`
RemoteTransportDeadline time.Duration `json:"remote_transport_deadline"`
} }
// UnmarshalJSON - Validate SS and RRS parity when unmarshalling JSON. // UnmarshalJSON - Validate SS and RRS parity when unmarshalling JSON.
@@ -83,6 +96,9 @@ func (sCfg *Config) UnmarshalJSON(data []byte) error {
// LookupConfig - lookup api config and override with valid environment settings if any. // LookupConfig - lookup api 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) {
// remove this since we have removed this already.
kvs.Delete(apiReadyDeadline)
if err = config.CheckValidKeys(config.APISubSys, kvs, DefaultKVS); err != nil { if err = config.CheckValidKeys(config.APISubSys, kvs, DefaultKVS); err != nil {
return cfg, err return cfg, err
} }
@@ -102,16 +118,23 @@ func LookupConfig(kvs config.KVS) (cfg Config, err error) {
return cfg, err return cfg, err
} }
readyDeadline, err := time.ParseDuration(env.Get(EnvAPIReadyDeadline, kvs.Get(apiReadyDeadline))) clusterDeadline, err := time.ParseDuration(env.Get(EnvAPIClusterDeadline, kvs.Get(apiClusterDeadline)))
if err != nil { if err != nil {
return cfg, err return cfg, err
} }
corsAllowOrigin := strings.Split(env.Get(EnvAPICorsAllowOrigin, kvs.Get(apiCorsAllowOrigin)), ",") corsAllowOrigin := strings.Split(env.Get(EnvAPICorsAllowOrigin, kvs.Get(apiCorsAllowOrigin)), ",")
remoteTransportDeadline, err := time.ParseDuration(env.Get(EnvAPIRemoteTransportDeadline, kvs.Get(apiRemoteTransportDeadline)))
if err != nil {
return cfg, err
}
return Config{ return Config{
APIRequestsMax: requestsMax, RequestsMax: requestsMax,
APIRequestsDeadline: requestsDeadline, RequestsDeadline: requestsDeadline,
APIReadyDeadline: readyDeadline, ClusterDeadline: clusterDeadline,
APICorsAllowOrigin: corsAllowOrigin, CorsAllowOrigin: corsAllowOrigin,
RemoteTransportDeadline: remoteTransportDeadline,
}, nil }, nil
} }
+6
View File
@@ -39,5 +39,11 @@ var (
Optional: true, Optional: true,
Type: "csv", Type: "csv",
}, },
config.HelpKV{
Key: apiRemoteTransportDeadline,
Description: `set the deadline for API requests on remote transports while proxying between federated instances e.g. "2h"`,
Optional: true,
Type: "duration",
},
} }
) )
+18
View File
@@ -76,6 +76,7 @@ const (
KmsKesSubSys = "kms_kes" KmsKesSubSys = "kms_kes"
LoggerWebhookSubSys = "logger_webhook" LoggerWebhookSubSys = "logger_webhook"
AuditWebhookSubSys = "audit_webhook" AuditWebhookSubSys = "audit_webhook"
CrawlerSubSys = "crawler"
// Add new constants here if you add new fields to config. // Add new constants here if you add new fields to config.
) )
@@ -112,6 +113,7 @@ var SubSystems = set.CreateStringSet([]string{
PolicyOPASubSys, PolicyOPASubSys,
IdentityLDAPSubSys, IdentityLDAPSubSys,
IdentityOpenIDSubSys, IdentityOpenIDSubSys,
CrawlerSubSys,
NotifyAMQPSubSys, NotifyAMQPSubSys,
NotifyESSubSys, NotifyESSubSys,
NotifyKafkaSubSys, NotifyKafkaSubSys,
@@ -138,6 +140,7 @@ var SubSystemsSingleTargets = set.CreateStringSet([]string{
PolicyOPASubSys, PolicyOPASubSys,
IdentityLDAPSubSys, IdentityLDAPSubSys,
IdentityOpenIDSubSys, IdentityOpenIDSubSys,
CrawlerSubSys,
}...) }...)
// Constant separators // Constant separators
@@ -264,6 +267,16 @@ func (kvs KVS) Get(key string) string {
return "" return ""
} }
// Delete - deletes the key if present from the KV list.
func (kvs *KVS) Delete(key string) {
for i, kv := range *kvs {
if kv.Key == key {
*kvs = append((*kvs)[:i], (*kvs)[i+1:]...)
return
}
}
}
// Lookup - lookup a key in a list of KVS // Lookup - lookup a key in a list of KVS
func (kvs KVS) Lookup(key string) (string, bool) { func (kvs KVS) Lookup(key string) (string, bool) {
for _, kv := range kvs { for _, kv := range kvs {
@@ -448,6 +461,11 @@ func (c Config) Merge() Config {
ckvs.Set(kv.Key, kv.Value) ckvs.Set(kv.Key, kv.Value)
} }
} }
if _, ok := cp[subSys]; !ok {
// A config subsystem was removed or server was downgraded.
Logger.Info("config: ignoring unknown subsystem config %q\n", subSys)
continue
}
cp[subSys][tgt] = ckvs cp[subSys][tgt] = ckvs
} }
} }
+1
View File
@@ -33,6 +33,7 @@ const (
EnvPublicIPs = "MINIO_PUBLIC_IPS" EnvPublicIPs = "MINIO_PUBLIC_IPS"
EnvFSOSync = "MINIO_FS_OSYNC" EnvFSOSync = "MINIO_FS_OSYNC"
EnvArgs = "MINIO_ARGS" EnvArgs = "MINIO_ARGS"
EnvDNSWebhook = "MINIO_DNS_WEBHOOK_ENDPOINT"
EnvUpdate = "MINIO_UPDATE" EnvUpdate = "MINIO_UPDATE"
+67
View File
@@ -0,0 +1,67 @@
/*
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package crawler
import (
"errors"
"github.com/minio/minio/cmd/config"
)
// Compression environment variables
const (
BitrotScan = "bitrotscan"
)
// Config represents the crawler settings.
type Config struct {
// Bitrot will perform bitrot scan on local disk when checking objects.
Bitrot bool `json:"bitrotscan"`
}
var (
// DefaultKVS - default KV config for crawler settings
DefaultKVS = config.KVS{
config.KV{
Key: BitrotScan,
Value: config.EnableOff,
},
}
// Help provides help for config values
Help = config.HelpKVS{
config.HelpKV{
Key: BitrotScan,
Description: `perform bitrot scan on disks when checking objects during crawl`,
Optional: true,
Type: "on|off",
},
}
)
// LookupConfig - lookup config and override with valid environment settings if any.
func LookupConfig(kvs config.KVS) (cfg Config, err error) {
if err = config.CheckValidKeys(config.CrawlerSubSys, kvs, DefaultKVS); err != nil {
return cfg, err
}
bitrot := kvs.Get(BitrotScan)
if bitrot != config.EnableOn && bitrot != config.EnableOff {
return cfg, errors.New(BitrotScan + ": must be 'on' or 'off'")
}
cfg.Bitrot = bitrot == config.EnableOn
return cfg, nil
}
@@ -26,9 +26,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/minio/minio-go/v7/pkg/set"
"github.com/coredns/coredns/plugin/etcd/msg" "github.com/coredns/coredns/plugin/etcd/msg"
"github.com/minio/minio-go/v7/pkg/set"
"go.etcd.io/etcd/v3/clientv3" "go.etcd.io/etcd/v3/clientv3"
) )
@@ -214,6 +213,11 @@ func (c *CoreDNS) DeleteRecord(record SrvRecord) error {
return nil return nil
} }
// String stringer name for this implementation of dns.Store
func (c *CoreDNS) String() string {
return "etcdDNS"
}
// CoreDNS - represents dns config for coredns server. // CoreDNS - represents dns config for coredns server.
type CoreDNS struct { type CoreDNS struct {
domainNames []string domainNames []string
@@ -223,13 +227,13 @@ type CoreDNS struct {
etcdClient *clientv3.Client etcdClient *clientv3.Client
} }
// Option - functional options pattern style // EtcdOption - functional options pattern style
type Option func(*CoreDNS) type EtcdOption func(*CoreDNS)
// DomainNames set a list of domain names used by this CoreDNS // DomainNames set a list of domain names used by this CoreDNS
// client setting, note this will fail if set to empty when // client setting, note this will fail if set to empty when
// constructor initializes. // constructor initializes.
func DomainNames(domainNames []string) Option { func DomainNames(domainNames []string) EtcdOption {
return func(args *CoreDNS) { return func(args *CoreDNS) {
args.domainNames = domainNames args.domainNames = domainNames
} }
@@ -237,14 +241,14 @@ func DomainNames(domainNames []string) Option {
// DomainIPs set a list of custom domain IPs, note this will // DomainIPs set a list of custom domain IPs, note this will
// fail if set to empty when constructor initializes. // fail if set to empty when constructor initializes.
func DomainIPs(domainIPs set.StringSet) Option { func DomainIPs(domainIPs set.StringSet) EtcdOption {
return func(args *CoreDNS) { return func(args *CoreDNS) {
args.domainIPs = domainIPs args.domainIPs = domainIPs
} }
} }
// DomainPort - is a string version of server port // DomainPort - is a string version of server port
func DomainPort(domainPort string) Option { func DomainPort(domainPort string) EtcdOption {
return func(args *CoreDNS) { return func(args *CoreDNS) {
args.domainPort = domainPort args.domainPort = domainPort
} }
@@ -253,14 +257,14 @@ func DomainPort(domainPort string) Option {
// CoreDNSPath - custom prefix on etcd to populate DNS // CoreDNSPath - custom prefix on etcd to populate DNS
// service records, optional and can be empty. // service records, optional and can be empty.
// if empty then c.prefixPath is used i.e "/skydns" // if empty then c.prefixPath is used i.e "/skydns"
func CoreDNSPath(prefix string) Option { func CoreDNSPath(prefix string) EtcdOption {
return func(args *CoreDNS) { return func(args *CoreDNS) {
args.prefixPath = prefix args.prefixPath = prefix
} }
} }
// NewCoreDNS - initialize a new coreDNS set/unset values. // NewCoreDNS - initialize a new coreDNS set/unset values.
func NewCoreDNS(cfg clientv3.Config, setters ...Option) (Store, error) { func NewCoreDNS(cfg clientv3.Config, setters ...EtcdOption) (Store, error) {
etcdClient, err := clientv3.New(cfg) etcdClient, err := clientv3.New(cfg)
if err != nil { if err != nil {
return nil, err return nil, err
+221
View File
@@ -0,0 +1,221 @@
/*
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dns
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"strconv"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/minio/minio/cmd/config"
xhttp "github.com/minio/minio/cmd/http"
)
var (
defaultOperatorContextTimeout = 10 * time.Second
// ErrNotImplemented - Indicates the functionality which is not implemented
ErrNotImplemented = errors.New("The method is not implemented")
)
func (c *OperatorDNS) addAuthHeader(r *http.Request) error {
if c.username == "" || c.password == "" {
return nil
}
claims := &jwt.StandardClaims{
ExpiresAt: int64(15 * time.Minute),
Issuer: c.username,
Subject: config.EnvDNSWebhook,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
ss, err := token.SignedString([]byte(c.password))
if err != nil {
return err
}
r.Header.Set("Authorization", "Bearer "+ss)
return nil
}
func (c *OperatorDNS) endpoint(bucket string, delete bool) (string, error) {
u, err := url.Parse(c.Endpoint)
if err != nil {
return "", err
}
q := u.Query()
q.Add("bucket", bucket)
q.Add("delete", strconv.FormatBool(delete))
u.RawQuery = q.Encode()
return u.String(), nil
}
// Put - Adds DNS entries into operator webhook server
func (c *OperatorDNS) Put(bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultOperatorContextTimeout)
defer cancel()
e, err := c.endpoint(bucket, false)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, e, nil)
if err != nil {
return err
}
if err = c.addAuthHeader(req); err != nil {
return err
}
resp, err := c.httpClient.Do(req)
if err != nil {
if derr := c.Delete(bucket); derr != nil {
return derr
}
}
xhttp.DrainBody(resp.Body)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("request to create the service for bucket %s, failed with status %s", bucket, resp.Status)
}
return nil
}
// Delete - Removes DNS entries added in Put().
func (c *OperatorDNS) Delete(bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultOperatorContextTimeout)
defer cancel()
e, err := c.endpoint(bucket, true)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, e, nil)
if err != nil {
return err
}
if err = c.addAuthHeader(req); err != nil {
return err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
xhttp.DrainBody(resp.Body)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("request to delete the service for bucket %s, failed with status %s", bucket, resp.Status)
}
return nil
}
// DeleteRecord - Removes a specific DNS entry
// No Op for Operator because operator deals on with bucket entries
func (c *OperatorDNS) DeleteRecord(record SrvRecord) error {
return ErrNotImplemented
}
// Close closes the internal http client
func (c *OperatorDNS) Close() error {
c.httpClient.CloseIdleConnections()
return nil
}
// List - Retrieves list of DNS entries for the domain.
// This is a No Op for Operator because, there is no intent to enforce global
// namespace at MinIO level with this DNS entry. The global namespace in
// enforced by the Kubernetes Operator
func (c *OperatorDNS) List() (srvRecords map[string][]SrvRecord, err error) {
return nil, ErrNotImplemented
}
// Get - Retrieves DNS records for a bucket.
// This is a No Op for Operator because, there is no intent to enforce global
// namespace at MinIO level with this DNS entry. The global namespace in
// enforced by the Kubernetes Operator
func (c *OperatorDNS) Get(bucket string) (srvRecords []SrvRecord, err error) {
return nil, ErrNotImplemented
}
// String stringer name for this implementation of dns.Store
func (c *OperatorDNS) String() string {
return "webhookDNS"
}
// OperatorDNS - represents dns config for MinIO k8s operator.
type OperatorDNS struct {
httpClient *http.Client
Endpoint string
rootCAs *x509.CertPool
username string
password string
}
// OperatorOption - functional options pattern style for OperatorDNS
type OperatorOption func(*OperatorDNS)
// Authentication - custom username and password for authenticating at the endpoint
func Authentication(username, password string) OperatorOption {
return func(args *OperatorDNS) {
args.username = username
args.password = password
}
}
// RootCAs - add custom trust certs pool
func RootCAs(CAs *x509.CertPool) OperatorOption {
return func(args *OperatorDNS) {
args.rootCAs = CAs
}
}
// NewOperatorDNS - initialize a new K8S Operator DNS set/unset values.
func NewOperatorDNS(endpoint string, setters ...OperatorOption) (Store, error) {
if endpoint == "" {
return nil, errors.New("invalid argument")
}
args := &OperatorDNS{
Endpoint: endpoint,
}
for _, setter := range setters {
setter(args)
}
args.httpClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 3 * time.Second,
KeepAlive: 5 * time.Second,
}).DialContext,
ResponseHeaderTimeout: 3 * time.Second,
TLSHandshakeTimeout: 3 * time.Second,
ExpectContinueTimeout: 3 * time.Second,
TLSClientConfig: &tls.Config{
RootCAs: args.rootCAs,
},
// Go net/http automatically unzip if content-type is
// gzip disable this feature, as we are always interested
// in raw stream.
DisableCompression: true,
},
}
return args, nil
}
@@ -24,4 +24,5 @@ type Store interface {
List() (map[string][]SrvRecord, error) List() (map[string][]SrvRecord, error)
DeleteRecord(record SrvRecord) error DeleteRecord(record SrvRecord) error
Close() error Close() error
String() string
} }
+27
View File
@@ -0,0 +1,27 @@
/*
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package config
import "context"
// Logger contains injected logger methods.
var Logger = struct {
Info func(msg string, data ...interface{})
LogIf func(ctx context.Context, err error, errKind ...interface{})
}{
// Initialized via injection.
}
+12
View File
@@ -340,6 +340,12 @@ var (
Optional: true, Optional: true,
Type: "sentence", Type: "sentence",
}, },
config.HelpKV{
Key: target.PostgresMaxOpenConnections,
Description: "To set the maximum number of open connections to the database. The value is set to `2` by default.",
Optional: true,
Type: "number",
},
} }
HelpMySQL = config.HelpKVS{ HelpMySQL = config.HelpKVS{
@@ -377,6 +383,12 @@ var (
Optional: true, Optional: true,
Type: "sentence", Type: "sentence",
}, },
config.HelpKV{
Key: target.MySQLMaxOpenConnections,
Description: "To set the maximum number of open connections to the database. The value is set to `2` by default.",
Optional: true,
Type: "number",
},
} }
HelpNATS = config.HelpKVS{ HelpNATS = config.HelpKVS{
+8
View File
@@ -357,6 +357,10 @@ func SetNotifyPostgres(s config.Config, psqName string, cfg target.PostgreSQLArg
Key: target.PostgresQueueLimit, Key: target.PostgresQueueLimit,
Value: strconv.Itoa(int(cfg.QueueLimit)), Value: strconv.Itoa(int(cfg.QueueLimit)),
}, },
config.KV{
Key: target.PostgresMaxOpenConnections,
Value: strconv.Itoa(cfg.MaxOpenConnections),
},
} }
return nil return nil
@@ -554,6 +558,10 @@ func SetNotifyMySQL(s config.Config, sqlName string, cfg target.MySQLArgs) error
Key: target.MySQLQueueLimit, Key: target.MySQLQueueLimit,
Value: strconv.Itoa(int(cfg.QueueLimit)), Value: strconv.Itoa(int(cfg.QueueLimit)),
}, },
config.KV{
Key: target.MySQLMaxOpenConnections,
Value: strconv.Itoa(cfg.MaxOpenConnections),
},
} }
return nil return nil
+60 -30
View File
@@ -43,11 +43,10 @@ var ErrTargetsOffline = errors.New("one or more targets are offline. Please use
// TestNotificationTargets is similar to GetNotificationTargets() // TestNotificationTargets is similar to GetNotificationTargets()
// avoids explicit registration. // avoids explicit registration.
func TestNotificationTargets(cfg config.Config, doneCh <-chan struct{}, transport *http.Transport, func TestNotificationTargets(ctx context.Context, cfg config.Config, transport *http.Transport, targetIDs []event.TargetID) error {
targetIDs []event.TargetID) error {
test := true test := true
returnOnTargetError := true returnOnTargetError := true
targets, err := RegisterNotificationTargets(cfg, doneCh, transport, targetIDs, test, returnOnTargetError) targets, err := RegisterNotificationTargets(ctx, cfg, transport, targetIDs, test, returnOnTargetError)
if err == nil { if err == nil {
// Close all targets since we are only testing connections. // Close all targets since we are only testing connections.
for _, t := range targets.TargetMap() { for _, t := range targets.TargetMap() {
@@ -60,9 +59,9 @@ func TestNotificationTargets(cfg config.Config, doneCh <-chan struct{}, transpor
// GetNotificationTargets registers and initializes all notification // GetNotificationTargets registers and initializes all notification
// targets, returns error if any. // targets, returns error if any.
func GetNotificationTargets(cfg config.Config, doneCh <-chan struct{}, transport *http.Transport, test bool) (*event.TargetList, error) { func GetNotificationTargets(ctx context.Context, cfg config.Config, transport *http.Transport, test bool) (*event.TargetList, error) {
returnOnTargetError := false returnOnTargetError := false
return RegisterNotificationTargets(cfg, doneCh, transport, nil, test, returnOnTargetError) return RegisterNotificationTargets(ctx, cfg, transport, nil, test, returnOnTargetError)
} }
// RegisterNotificationTargets - returns TargetList which contains enabled targets in serverConfig. // RegisterNotificationTargets - returns TargetList which contains enabled targets in serverConfig.
@@ -70,8 +69,8 @@ func GetNotificationTargets(cfg config.Config, doneCh <-chan struct{}, transport
// * Add a new target in pkg/event/target package. // * Add a new target in pkg/event/target package.
// * Add newly added target configuration to serverConfig.Notify.<TARGET_NAME>. // * Add newly added target configuration to serverConfig.Notify.<TARGET_NAME>.
// * Handle the configuration in this function to create/add into TargetList. // * Handle the configuration in this function to create/add into TargetList.
func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, transport *http.Transport, targetIDs []event.TargetID, test bool, returnOnTargetError bool) (*event.TargetList, error) { func RegisterNotificationTargets(ctx context.Context, cfg config.Config, transport *http.Transport, targetIDs []event.TargetID, test bool, returnOnTargetError bool) (*event.TargetList, error) {
targetList, err := FetchRegisteredTargets(cfg, doneCh, transport, test, returnOnTargetError) targetList, err := FetchRegisteredTargets(ctx, cfg, transport, test, returnOnTargetError)
if err != nil { if err != nil {
return targetList, err return targetList, err
} }
@@ -94,7 +93,7 @@ func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, tran
// FetchRegisteredTargets - Returns a set of configured TargetList // FetchRegisteredTargets - Returns a set of configured TargetList
// If `returnOnTargetError` is set to true, The function returns when a target initialization fails // If `returnOnTargetError` is set to true, The function returns when a target initialization fails
// Else, the function will return a complete TargetList irrespective of errors // Else, the function will return a complete TargetList irrespective of errors
func FetchRegisteredTargets(cfg config.Config, doneCh <-chan struct{}, transport *http.Transport, test bool, returnOnTargetError bool) (_ *event.TargetList, err error) { func FetchRegisteredTargets(ctx context.Context, cfg config.Config, transport *http.Transport, test bool, returnOnTargetError bool) (_ *event.TargetList, err error) {
targetList := event.NewTargetList() targetList := event.NewTargetList()
var targetsOffline bool var targetsOffline bool
@@ -167,7 +166,7 @@ func FetchRegisteredTargets(cfg config.Config, doneCh <-chan struct{}, transport
if !args.Enable { if !args.Enable {
continue continue
} }
newTarget, err := target.NewAMQPTarget(id, args, doneCh, logger.LogOnceIf, test) newTarget, err := target.NewAMQPTarget(id, args, ctx.Done(), logger.LogOnceIf, test)
if err != nil { if err != nil {
targetsOffline = true targetsOffline = true
if returnOnTargetError { if returnOnTargetError {
@@ -188,7 +187,7 @@ func FetchRegisteredTargets(cfg config.Config, doneCh <-chan struct{}, transport
if !args.Enable { if !args.Enable {
continue continue
} }
newTarget, err := target.NewElasticsearchTarget(id, args, doneCh, logger.LogOnceIf, test) newTarget, err := target.NewElasticsearchTarget(id, args, ctx.Done(), logger.LogOnceIf, test)
if err != nil { if err != nil {
targetsOffline = true targetsOffline = true
if returnOnTargetError { if returnOnTargetError {
@@ -209,7 +208,7 @@ func FetchRegisteredTargets(cfg config.Config, doneCh <-chan struct{}, transport
continue continue
} }
args.TLS.RootCAs = transport.TLSClientConfig.RootCAs args.TLS.RootCAs = transport.TLSClientConfig.RootCAs
newTarget, err := target.NewKafkaTarget(id, args, doneCh, logger.LogOnceIf, test) newTarget, err := target.NewKafkaTarget(id, args, ctx.Done(), logger.LogOnceIf, test)
if err != nil { if err != nil {
targetsOffline = true targetsOffline = true
if returnOnTargetError { if returnOnTargetError {
@@ -230,7 +229,7 @@ func FetchRegisteredTargets(cfg config.Config, doneCh <-chan struct{}, transport
continue continue
} }
args.RootCAs = transport.TLSClientConfig.RootCAs args.RootCAs = transport.TLSClientConfig.RootCAs
newTarget, err := target.NewMQTTTarget(id, args, doneCh, logger.LogOnceIf, test) newTarget, err := target.NewMQTTTarget(id, args, ctx.Done(), logger.LogOnceIf, test)
if err != nil { if err != nil {
targetsOffline = true targetsOffline = true
if returnOnTargetError { if returnOnTargetError {
@@ -250,7 +249,7 @@ func FetchRegisteredTargets(cfg config.Config, doneCh <-chan struct{}, transport
if !args.Enable { if !args.Enable {
continue continue
} }
newTarget, err := target.NewMySQLTarget(id, args, doneCh, logger.LogOnceIf, test) newTarget, err := target.NewMySQLTarget(id, args, ctx.Done(), logger.LogOnceIf, test)
if err != nil { if err != nil {
targetsOffline = true targetsOffline = true
if returnOnTargetError { if returnOnTargetError {
@@ -270,7 +269,7 @@ func FetchRegisteredTargets(cfg config.Config, doneCh <-chan struct{}, transport
if !args.Enable { if !args.Enable {
continue continue
} }
newTarget, err := target.NewNATSTarget(id, args, doneCh, logger.LogOnceIf, test) newTarget, err := target.NewNATSTarget(id, args, ctx.Done(), logger.LogOnceIf, test)
if err != nil { if err != nil {
targetsOffline = true targetsOffline = true
if returnOnTargetError { if returnOnTargetError {
@@ -290,7 +289,7 @@ func FetchRegisteredTargets(cfg config.Config, doneCh <-chan struct{}, transport
if !args.Enable { if !args.Enable {
continue continue
} }
newTarget, err := target.NewNSQTarget(id, args, doneCh, logger.LogOnceIf, test) newTarget, err := target.NewNSQTarget(id, args, ctx.Done(), logger.LogOnceIf, test)
if err != nil { if err != nil {
targetsOffline = true targetsOffline = true
if returnOnTargetError { if returnOnTargetError {
@@ -310,7 +309,7 @@ func FetchRegisteredTargets(cfg config.Config, doneCh <-chan struct{}, transport
if !args.Enable { if !args.Enable {
continue continue
} }
newTarget, err := target.NewPostgreSQLTarget(id, args, doneCh, logger.LogOnceIf, test) newTarget, err := target.NewPostgreSQLTarget(id, args, ctx.Done(), logger.LogOnceIf, test)
if err != nil { if err != nil {
targetsOffline = true targetsOffline = true
if returnOnTargetError { if returnOnTargetError {
@@ -330,7 +329,7 @@ func FetchRegisteredTargets(cfg config.Config, doneCh <-chan struct{}, transport
if !args.Enable { if !args.Enable {
continue continue
} }
newTarget, err := target.NewRedisTarget(id, args, doneCh, logger.LogOnceIf, test) newTarget, err := target.NewRedisTarget(id, args, ctx.Done(), logger.LogOnceIf, test)
if err != nil { if err != nil {
targetsOffline = true targetsOffline = true
if returnOnTargetError { if returnOnTargetError {
@@ -350,7 +349,7 @@ func FetchRegisteredTargets(cfg config.Config, doneCh <-chan struct{}, transport
if !args.Enable { if !args.Enable {
continue continue
} }
newTarget, err := target.NewWebhookTarget(id, args, doneCh, logger.LogOnceIf, transport, test) newTarget, err := target.NewWebhookTarget(ctx, id, args, logger.LogOnceIf, transport, test)
if err != nil { if err != nil {
targetsOffline = true targetsOffline = true
if returnOnTargetError { if returnOnTargetError {
@@ -808,6 +807,10 @@ var (
Key: target.MySQLQueueLimit, Key: target.MySQLQueueLimit,
Value: "0", Value: "0",
}, },
config.KV{
Key: target.MySQLMaxOpenConnections,
Value: "2",
},
} }
) )
@@ -856,13 +859,25 @@ func GetNotifyMySQL(mysqlKVS map[string]config.KVS) (map[string]target.MySQLArgs
if k != config.Default { if k != config.Default {
queueDirEnv = queueDirEnv + config.Default + k queueDirEnv = queueDirEnv + config.Default + k
} }
maxOpenConnectionsEnv := target.EnvMySQLMaxOpenConnections
if k != config.Default {
maxOpenConnectionsEnv = maxOpenConnectionsEnv + config.Default + k
}
maxOpenConnections, cErr := strconv.Atoi(env.Get(maxOpenConnectionsEnv, kv.Get(target.MySQLMaxOpenConnections)))
if cErr != nil {
return nil, cErr
}
mysqlArgs := target.MySQLArgs{ mysqlArgs := target.MySQLArgs{
Enable: enabled, Enable: enabled,
Format: env.Get(formatEnv, kv.Get(target.MySQLFormat)), Format: env.Get(formatEnv, kv.Get(target.MySQLFormat)),
DSN: env.Get(dsnStringEnv, kv.Get(target.MySQLDSNString)), DSN: env.Get(dsnStringEnv, kv.Get(target.MySQLDSNString)),
Table: env.Get(tableEnv, kv.Get(target.MySQLTable)), Table: env.Get(tableEnv, kv.Get(target.MySQLTable)),
QueueDir: env.Get(queueDirEnv, kv.Get(target.MySQLQueueDir)), QueueDir: env.Get(queueDirEnv, kv.Get(target.MySQLQueueDir)),
QueueLimit: queueLimit, QueueLimit: queueLimit,
MaxOpenConnections: maxOpenConnections,
} }
if err = mysqlArgs.Validate(); err != nil { if err = mysqlArgs.Validate(); err != nil {
return nil, err return nil, err
@@ -1236,6 +1251,10 @@ var (
Key: target.PostgresQueueLimit, Key: target.PostgresQueueLimit,
Value: "0", Value: "0",
}, },
config.KV{
Key: target.PostgresMaxOpenConnections,
Value: "2",
},
} }
) )
@@ -1286,13 +1305,24 @@ func GetNotifyPostgres(postgresKVS map[string]config.KVS) (map[string]target.Pos
queueDirEnv = queueDirEnv + config.Default + k queueDirEnv = queueDirEnv + config.Default + k
} }
maxOpenConnectionsEnv := target.EnvPostgresMaxOpenConnections
if k != config.Default {
maxOpenConnectionsEnv = maxOpenConnectionsEnv + config.Default + k
}
maxOpenConnections, cErr := strconv.Atoi(env.Get(maxOpenConnectionsEnv, kv.Get(target.PostgresMaxOpenConnections)))
if cErr != nil {
return nil, cErr
}
psqlArgs := target.PostgreSQLArgs{ psqlArgs := target.PostgreSQLArgs{
Enable: enabled, Enable: enabled,
Format: env.Get(formatEnv, kv.Get(target.PostgresFormat)), Format: env.Get(formatEnv, kv.Get(target.PostgresFormat)),
ConnectionString: env.Get(connectionStringEnv, kv.Get(target.PostgresConnectionString)), ConnectionString: env.Get(connectionStringEnv, kv.Get(target.PostgresConnectionString)),
Table: env.Get(tableEnv, kv.Get(target.PostgresTable)), Table: env.Get(tableEnv, kv.Get(target.PostgresTable)),
QueueDir: env.Get(queueDirEnv, kv.Get(target.PostgresQueueDir)), QueueDir: env.Get(queueDirEnv, kv.Get(target.PostgresQueueDir)),
QueueLimit: uint64(queueLimit), QueueLimit: uint64(queueLimit),
MaxOpenConnections: maxOpenConnections,
} }
if err = psqlArgs.Validate(); err != nil { if err = psqlArgs.Validate(); err != nil {
return nil, err return nil, err
+18
View File
@@ -122,6 +122,24 @@ func (sys *HTTPConsoleLoggerSys) Validate() error {
return nil return nil
} }
// Content returns the console stdout log
func (sys *HTTPConsoleLoggerSys) Content() (logs []log.Entry) {
sys.RLock()
sys.logBuf.Do(func(p interface{}) {
if p != nil {
lg, ok := p.(log.Info)
if ok {
if (lg.Entry != log.Entry{}) {
logs = append(logs, lg.Entry)
}
}
}
})
sys.RUnlock()
return
}
// Send log message 'e' to console and publish to console // Send log message 'e' to console and publish to console
// log pubsub system // log pubsub system
func (sys *HTTPConsoleLoggerSys) Send(e interface{}, logKind string) error { func (sys *HTTPConsoleLoggerSys) Send(e interface{}, logKind string) error {
+3 -3
View File
@@ -129,7 +129,7 @@ func (ssec) IsEncrypted(metadata map[string]string) bool {
// metadata is nil. // metadata is nil.
func CreateMultipartMetadata(metadata map[string]string) map[string]string { func CreateMultipartMetadata(metadata map[string]string) map[string]string {
if metadata == nil { if metadata == nil {
metadata = map[string]string{} return map[string]string{SSEMultipart: ""}
} }
metadata[SSEMultipart] = "" metadata[SSEMultipart] = ""
return metadata return metadata
@@ -156,7 +156,7 @@ func (s3) CreateMetadata(metadata map[string]string, keyID string, kmsKey []byte
} }
if metadata == nil { if metadata == nil {
metadata = map[string]string{} metadata = make(map[string]string, 5)
} }
metadata[SSESealAlgorithm] = sealedKey.Algorithm metadata[SSESealAlgorithm] = sealedKey.Algorithm
@@ -236,7 +236,7 @@ func (ssec) CreateMetadata(metadata map[string]string, sealedKey SealedKey) map[
} }
if metadata == nil { if metadata == nil {
metadata = map[string]string{} metadata = make(map[string]string, 3)
} }
metadata[SSESealAlgorithm] = SealAlgorithm metadata[SSESealAlgorithm] = SealAlgorithm
metadata[SSEIV] = base64.StdEncoding.EncodeToString(sealedKey.IV[:]) metadata[SSEIV] = base64.StdEncoding.EncodeToString(sealedKey.IV[:])
+48 -27
View File
@@ -21,15 +21,15 @@ import (
"context" "context"
"encoding/binary" "encoding/binary"
"errors" "errors"
"math/rand"
"os" "os"
"path" "path"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/minio/minio/pkg/madmin"
"github.com/minio/minio/cmd/config" "github.com/minio/minio/cmd/config"
"github.com/minio/minio/cmd/config/crawler"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/bucket/lifecycle" "github.com/minio/minio/pkg/bucket/lifecycle"
"github.com/minio/minio/pkg/bucket/replication" "github.com/minio/minio/pkg/bucket/replication"
@@ -37,6 +37,7 @@ import (
"github.com/minio/minio/pkg/env" "github.com/minio/minio/pkg/env"
"github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/event"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/madmin"
"github.com/willf/bloom" "github.com/willf/bloom"
) )
@@ -46,7 +47,14 @@ const (
dataCrawlStartDelay = 5 * time.Minute // Time to wait on startup and between cycles. dataCrawlStartDelay = 5 * 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.
healObjectSelectProb = 512 // Overall probability of a file being scanned; one in n.
)
var (
globalCrawlerConfig crawler.Config
dataCrawlerLeaderLockTimeout = newDynamicTimeout(1*time.Minute, 30*time.Second)
) )
// initDataCrawler will start the crawler unless disabled. // initDataCrawler will start the crawler unless disabled.
@@ -60,6 +68,19 @@ func initDataCrawler(ctx context.Context, objAPI ObjectLayer) {
// 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 crawler running per cluster.
func runDataCrawler(ctx context.Context, objAPI ObjectLayer) { func runDataCrawler(ctx context.Context, objAPI ObjectLayer) {
// Make sure only 1 crawler is running on the cluster.
locker := objAPI.NewNSLock(ctx, minioMetaBucket, "runDataCrawler.lock")
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for {
err := locker.GetLock(dataCrawlerLeaderLockTimeout)
if err != nil {
time.Sleep(time.Duration(r.Float64() * float64(dataCrawlStartDelay)))
continue
}
break
// No unlock for "leader" lock.
}
// Load current bloom cycle // Load current bloom cycle
nextBloomCycle := intDataUpdateTracker.current() + 1 nextBloomCycle := intDataUpdateTracker.current() + 1
var buf bytes.Buffer var buf bytes.Buffer
@@ -174,9 +195,9 @@ func crawlDataFolder(ctx context.Context, basePath string, cache dataUsageCache,
// Enable healing in XL mode. // Enable healing in XL mode.
if globalIsErasure { if globalIsErasure {
// Include a clean folder one in n cycles. // Include a clean folder one in n cycles.
s.healFolderInclude = 32 s.healFolderInclude = healFolderIncludeProb
// Do a heal check on an object once every n cycles. Must divide into healFolderInclude // Do a heal check on an object once every n cycles. Must divide into healFolderInclude
s.healObjectSelect = 512 s.healObjectSelect = healObjectSelectProb
} }
if len(cache.Info.BloomFilter) > 0 { if len(cache.Info.BloomFilter) > 0 {
s.withFilter = &bloomFilter{BloomFilter: &bloom.BloomFilter{}} s.withFilter = &bloomFilter{BloomFilter: &bloom.BloomFilter{}}
@@ -327,7 +348,7 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
// If there are lifecycle rules for the prefix, remove the filter. // If there are lifecycle rules for the prefix, remove the filter.
filter := f.withFilter filter := f.withFilter
var activeLifeCycle *lifecycle.Lifecycle var activeLifeCycle *lifecycle.Lifecycle
if f.oldCache.Info.lifeCycle != nil && filter != nil { if f.oldCache.Info.lifeCycle != nil {
_, prefix := path2BucketObjectWithBasePath(f.root, folder.name) _, prefix := path2BucketObjectWithBasePath(f.root, folder.name)
if f.oldCache.Info.lifeCycle.HasActiveRules(prefix, true) { if f.oldCache.Info.lifeCycle.HasActiveRules(prefix, true) {
if f.dataUsageCrawlDebug { if f.dataUsageCrawlDebug {
@@ -602,8 +623,9 @@ 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
numVersions int // The number of versions of this object successorModTime time.Time // The modtime of the successor version
numVersions int // The number of versions of this object
} }
// applyActions will apply lifecycle checks on to a scanned item. // applyActions will apply lifecycle checks on to a scanned item.
@@ -623,7 +645,7 @@ func (i *crawlItem) applyActions(ctx context.Context, o ObjectLayer, meta action
if isErrObjectNotFound(err) || isErrVersionNotFound(err) { if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
return 0 return 0
} }
if !errors.Is(err, NotImplemented{}) { if err != nil && !errors.Is(err, NotImplemented{}) {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return 0 return 0
} }
@@ -636,13 +658,14 @@ func (i *crawlItem) applyActions(ctx context.Context, o ObjectLayer, meta action
versionID := meta.oi.VersionID versionID := meta.oi.VersionID
action := i.lifeCycle.ComputeAction( action := i.lifeCycle.ComputeAction(
lifecycle.ObjectOpts{ lifecycle.ObjectOpts{
Name: i.objectPath(), Name: i.objectPath(),
UserTags: meta.oi.UserTags, UserTags: meta.oi.UserTags,
ModTime: meta.oi.ModTime, ModTime: meta.oi.ModTime,
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.numVersions,
SuccessorModTime: meta.successorModTime,
}) })
if i.debug { if i.debug {
logger.Info(color.Green("applyActions:")+" lifecycle: %q (version-id=%s), Initial scan: %v", i.objectPath(), versionID, action) logger.Info(color.Green("applyActions:")+" lifecycle: %q (version-id=%s), Initial scan: %v", i.objectPath(), versionID, action)
@@ -679,13 +702,14 @@ func (i *crawlItem) applyActions(ctx context.Context, o ObjectLayer, meta action
// Recalculate action. // Recalculate action.
action = i.lifeCycle.ComputeAction( action = i.lifeCycle.ComputeAction(
lifecycle.ObjectOpts{ lifecycle.ObjectOpts{
Name: i.objectPath(), Name: i.objectPath(),
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: meta.numVersions,
SuccessorModTime: meta.successorModTime,
}) })
if i.debug { if i.debug {
logger.Info(color.Green("applyActions:")+" lifecycle: Secondary scan: %v", action) logger.Info(color.Green("applyActions:")+" lifecycle: Secondary scan: %v", action)
@@ -756,9 +780,6 @@ func sleepDuration(d time.Duration, x float64) {
func (i *crawlItem) healReplication(ctx context.Context, o ObjectLayer, meta actionMeta) { func (i *crawlItem) healReplication(ctx context.Context, o ObjectLayer, meta actionMeta) {
if meta.oi.ReplicationStatus == replication.Pending || if meta.oi.ReplicationStatus == replication.Pending ||
meta.oi.ReplicationStatus == replication.Failed { meta.oi.ReplicationStatus == replication.Failed {
// if heal encounters a pending replication status, either replication globalReplicationState.queueReplicaTask(meta.oi)
// has failed due to server shutdown or crawler and PutObject replication are in contention.
healPending := meta.oi.ReplicationStatus == replication.Pending
replicateObject(ctx, meta.oi.Bucket, meta.oi.Name, meta.oi.VersionID, o, nil, healPending)
} }
} }
+2
View File
@@ -426,6 +426,8 @@ func (d *dataUpdateTracker) deserialize(src io.Reader, newerThan time.Time) erro
} }
// Ignore what remains on the stream. // Ignore what remains on the stream.
// Update d: // Update d:
d.mu.Lock()
defer d.mu.Unlock()
d.Current = dst.Current d.Current = dst.Current
d.History = dst.History d.History = dst.History
d.Saved = dst.Saved d.Saved = dst.Saved
+7 -2
View File
@@ -428,10 +428,15 @@ func (d *dataUsageCache) merge(other dataUsageCache) {
} }
} }
type objectIO interface {
GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) (err error)
PutObject(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error)
}
// load the cache content with name from minioMetaBackgroundOpsBucket. // load the cache content with name from minioMetaBackgroundOpsBucket.
// 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 ObjectLayer, name string) error { func (d *dataUsageCache) load(ctx context.Context, store objectIO, name string) error {
var buf bytes.Buffer var buf bytes.Buffer
err := store.GetObject(ctx, dataUsageBucket, name, 0, -1, &buf, "", ObjectOptions{}) err := store.GetObject(ctx, dataUsageBucket, name, 0, -1, &buf, "", ObjectOptions{})
if err != nil { if err != nil {
@@ -450,7 +455,7 @@ func (d *dataUsageCache) load(ctx context.Context, store ObjectLayer, name strin
} }
// save the content of the cache to minioMetaBackgroundOpsBucket with the provided name. // save the content of the cache to minioMetaBackgroundOpsBucket with the provided name.
func (d *dataUsageCache) save(ctx context.Context, store ObjectLayer, name string) error { func (d *dataUsageCache) save(ctx context.Context, store objectIO, name string) error {
b := d.serialize() b := d.serialize()
size := int64(len(b)) size := int64(len(b))
r, err := hash.NewReader(bytes.NewReader(b), size, "", "", size, false) r, err := hash.NewReader(bytes.NewReader(b), size, "", "", size, false)
-1
View File
@@ -240,7 +240,6 @@ func TestDataUsageUpdate(t *testing.T) {
t.Fatal("got nil result") t.Fatal("got nil result")
} }
if w.flatten { if w.flatten {
t.Log(e.Children)
*e = got.flatten(*e) *e = got.flatten(*e)
} }
if e.Size != int64(w.size) { if e.Size != int64(w.size) {
+35 -39
View File
@@ -157,7 +157,7 @@ func newDiskCache(ctx context.Context, dir string, config cache.Config) (*diskCa
} }
cache := diskCache{ cache := diskCache{
dir: dir, dir: dir,
triggerGC: make(chan struct{}), triggerGC: make(chan struct{}, 1),
stats: CacheDiskStats{Dir: dir}, stats: CacheDiskStats{Dir: dir},
quotaPct: quotaPct, quotaPct: quotaPct,
after: config.After, after: config.After,
@@ -174,7 +174,7 @@ func newDiskCache(ctx context.Context, dir string, config cache.Config) (*diskCa
nsMutex: newNSLock(false), nsMutex: newNSLock(false),
} }
go cache.purgeWait(ctx) go cache.purgeWait(ctx)
cache.diskUsageHigh() // update if cache usage is already high. cache.diskSpaceAvailable(0) // update if cache usage is already high.
cache.NewNSLockFn = func(ctx context.Context, cachePath string) RWLocker { cache.NewNSLockFn = func(ctx context.Context, cachePath string) RWLocker {
return cache.nsMutex.NewNSLock(ctx, nil, cachePath, "") return cache.nsMutex.NewNSLock(ctx, nil, cachePath, "")
} }
@@ -203,9 +203,9 @@ func (c *diskCache) diskUsageLow() bool {
return low return low
} }
// Returns if the disk usage reaches high water mark w.r.t the configured cache quota. // Returns if the disk usage reaches or exceeds configured cache quota when size is added.
// gc starts if high water mark reached. // If current usage without size exceeds high watermark a GC is automatically queued.
func (c *diskCache) diskUsageHigh() bool { func (c *diskCache) diskSpaceAvailable(size int64) bool {
gcTriggerPct := c.quotaPct * c.highWatermark / 100 gcTriggerPct := c.quotaPct * c.highWatermark / 100
di, err := disk.GetInfo(c.dir) di, err := disk.GetInfo(c.dir)
if err != nil { if err != nil {
@@ -214,27 +214,30 @@ func (c *diskCache) diskUsageHigh() bool {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return false return false
} }
usedPercent := (di.Total - di.Free) * 100 / di.Total if di.Total == 0 {
high := int(usedPercent) >= gcTriggerPct logger.Info("diskCache: Received 0 total disk size")
atomic.StoreUint64(&c.stats.UsagePercent, usedPercent)
if high {
atomic.StoreInt32(&c.stats.UsageState, 1)
}
return high
}
// Returns if size space can be allocated without exceeding
// max disk usable for caching
func (c *diskCache) diskAvailable(size int64) bool {
di, err := disk.GetInfo(c.dir)
if err != nil {
reqInfo := (&logger.ReqInfo{}).AppendTags("cachePath", c.dir)
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
logger.LogIf(ctx, err)
return false return false
} }
usedPercent := (di.Total - (di.Free - uint64(size))) * 100 / di.Total usedPercent := float64(di.Total-di.Free) * 100 / float64(di.Total)
return int(usedPercent) < c.quotaPct if usedPercent >= float64(gcTriggerPct) {
atomic.StoreInt32(&c.stats.UsageState, 1)
c.queueGC()
}
atomic.StoreUint64(&c.stats.UsagePercent, uint64(usedPercent))
// Recalculate percentage with provided size added.
usedPercent = float64(di.Total-di.Free+uint64(size)) * 100 / float64(di.Total)
return usedPercent < float64(c.quotaPct)
}
// queueGC will queue a GC.
// Calling this function is always non-blocking.
func (c *diskCache) queueGC() {
select {
case c.triggerGC <- struct{}{}:
default:
}
} }
// toClear returns how many bytes should be cleared to reach the low watermark quota. // toClear returns how many bytes should be cleared to reach the low watermark quota.
@@ -247,7 +250,7 @@ func (c *diskCache) toClear() uint64 {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return 0 return 0
} }
return bytesToClear(int64(di.Total), int64(di.Free), uint64(c.quotaPct), uint64(c.lowWatermark)) return bytesToClear(int64(di.Total), int64(di.Free), uint64(c.quotaPct), uint64(c.lowWatermark), uint64(c.highWatermark))
} }
var ( var (
@@ -658,8 +661,7 @@ func newCacheEncryptMetadata(bucket, object string, metadata map[string]string)
// Caches the object to disk // Caches the object to disk
func (c *diskCache) Put(ctx context.Context, bucket, object string, data io.Reader, size int64, rs *HTTPRangeSpec, opts ObjectOptions, incHitsOnly bool) error { func (c *diskCache) Put(ctx context.Context, bucket, object string, data io.Reader, size int64, rs *HTTPRangeSpec, opts ObjectOptions, incHitsOnly bool) error {
if c.diskUsageHigh() { if !c.diskSpaceAvailable(size) {
c.triggerGC <- struct{}{}
io.Copy(ioutil.Discard, data) io.Copy(ioutil.Discard, data)
return errDiskFull return errDiskFull
} }
@@ -688,16 +690,13 @@ func (c *diskCache) Put(ctx context.Context, bucket, object string, data io.Read
if rs != nil { if rs != nil {
return c.putRange(ctx, bucket, object, data, size, rs, opts) return c.putRange(ctx, bucket, object, data, size, rs, opts)
} }
if !c.diskAvailable(size) { if !c.diskSpaceAvailable(size) {
return errDiskFull return errDiskFull
} }
if err := os.MkdirAll(cachePath, 0777); err != nil { if err := os.MkdirAll(cachePath, 0777); err != nil {
return err return err
} }
var metadata = make(map[string]string) var metadata = cloneMSS(opts.UserDefined)
for k, v := range opts.UserDefined {
metadata[k] = v
}
var reader = data var reader = data
var actualSize = uint64(size) var actualSize = uint64(size)
if globalCacheKMS != nil { if globalCacheKMS != nil {
@@ -719,7 +718,7 @@ func (c *diskCache) Put(ctx context.Context, bucket, object string, data io.Read
if actualSize != uint64(n) { if actualSize != uint64(n) {
removeAll(cachePath) removeAll(cachePath)
return IncompleteBody{} return IncompleteBody{Bucket: bucket, Object: object}
} }
return c.saveMetadata(ctx, bucket, object, metadata, n, nil, "", incHitsOnly) return c.saveMetadata(ctx, bucket, object, metadata, n, nil, "", incHitsOnly)
} }
@@ -730,17 +729,14 @@ func (c *diskCache) putRange(ctx context.Context, bucket, object string, data io
if err != nil { if err != nil {
return err return err
} }
if !c.diskAvailable(rlen) { if !c.diskSpaceAvailable(rlen) {
return errDiskFull return errDiskFull
} }
cachePath := getCacheSHADir(c.dir, bucket, object) cachePath := getCacheSHADir(c.dir, bucket, object)
if err := os.MkdirAll(cachePath, 0777); err != nil { if err := os.MkdirAll(cachePath, 0777); err != nil {
return err return err
} }
var metadata = make(map[string]string) var metadata = cloneMSS(opts.UserDefined)
for k, v := range opts.UserDefined {
metadata[k] = v
}
var reader = data var reader = data
var actualSize = uint64(rlen) var actualSize = uint64(rlen)
// objSize is the actual size of object (with encryption overhead if any) // objSize is the actual size of object (with encryption overhead if any)
@@ -766,7 +762,7 @@ func (c *diskCache) putRange(ctx context.Context, bucket, object string, data io
} }
if actualSize != uint64(n) { if actualSize != uint64(n) {
removeAll(cachePath) removeAll(cachePath)
return IncompleteBody{} return IncompleteBody{Bucket: bucket, Object: object}
} }
return c.saveMetadata(ctx, bucket, object, metadata, int64(objSize), rs, cacheFile, false) return c.saveMetadata(ctx, bucket, object, metadata, int64(objSize), rs, cacheFile, false)
} }
+9 -3
View File
@@ -489,9 +489,15 @@ func (f *fileScorer) queueString() string {
// bytesToClear() returns the number of bytes to clear to reach low watermark // bytesToClear() returns the number of bytes to clear to reach low watermark
// w.r.t quota given disk total and free space, quota in % allocated to cache // w.r.t quota given disk total and free space, quota in % allocated to cache
// and low watermark % w.r.t allowed quota. // and low watermark % w.r.t allowed quota.
func bytesToClear(total, free int64, quotaPct, lowWatermark uint64) uint64 { // If the high watermark hasn't been reached 0 will be returned.
used := (total - free) func bytesToClear(total, free int64, quotaPct, lowWatermark, highWatermark uint64) uint64 {
used := total - free
quotaAllowed := total * (int64)(quotaPct) / 100 quotaAllowed := total * (int64)(quotaPct) / 100
lowWMUsage := (total * (int64)(lowWatermark*quotaPct) / (100 * 100)) highWMUsage := total * (int64)(highWatermark*quotaPct) / (100 * 100)
if used < highWMUsage {
return 0
}
// Return bytes needed to reach low watermark.
lowWMUsage := total * (int64)(lowWatermark*quotaPct) / (100 * 100)
return (uint64)(math.Min(float64(quotaAllowed), math.Max(0.0, float64(used-lowWMUsage)))) return (uint64)(math.Min(float64(quotaAllowed), math.Max(0.0, float64(used-lowWMUsage))))
} }
+17 -13
View File
@@ -149,22 +149,26 @@ func TestNewFileScorer(t *testing.T) {
} }
func TestBytesToClear(t *testing.T) { func TestBytesToClear(t *testing.T) {
testCases := []struct { testCases := []struct {
total int64 total int64
free int64 free int64
quotaPct uint64 quotaPct uint64
watermarkLow uint64 watermarkLow uint64
expected uint64 watermarkHigh uint64
expected uint64
}{ }{
{1000, 800, 40, 90, 0}, {total: 1000, free: 800, quotaPct: 40, watermarkLow: 90, watermarkHigh: 90, expected: 0},
{1000, 200, 40, 90, 400}, {total: 1000, free: 200, quotaPct: 40, watermarkLow: 90, watermarkHigh: 90, expected: 400},
{1000, 400, 40, 90, 240}, {total: 1000, free: 400, quotaPct: 40, watermarkLow: 90, watermarkHigh: 90, expected: 240},
{1000, 600, 40, 90, 40}, {total: 1000, free: 600, quotaPct: 40, watermarkLow: 90, watermarkHigh: 90, expected: 40},
{1000, 600, 40, 70, 120}, {total: 1000, free: 600, quotaPct: 40, watermarkLow: 70, watermarkHigh: 70, expected: 120},
{1000, 1000, 90, 70, 0}, {total: 1000, free: 1000, quotaPct: 90, watermarkLow: 70, watermarkHigh: 70, expected: 0},
{1000, 0, 90, 70, 370},
// High not yet reached..
{total: 1000, free: 250, quotaPct: 100, watermarkLow: 50, watermarkHigh: 90, expected: 0},
{total: 1000, free: 250, quotaPct: 100, watermarkLow: 50, watermarkHigh: 90, expected: 0},
} }
for i, tc := range testCases { for i, tc := range testCases {
toClear := bytesToClear(tc.total, tc.free, tc.quotaPct, tc.watermarkLow) toClear := bytesToClear(tc.total, tc.free, tc.quotaPct, tc.watermarkLow, tc.watermarkHigh)
if tc.expected != toClear { if tc.expected != toClear {
t.Errorf("test %d expected %v, got %v", i, tc.expected, toClear) t.Errorf("test %d expected %v, got %v", i, tc.expected, toClear)
} }
+14 -15
View File
@@ -85,16 +85,15 @@ type cacheObjects struct {
} }
func (c *cacheObjects) incHitsToMeta(ctx context.Context, dcache *diskCache, bucket, object string, size int64, eTag string, rs *HTTPRangeSpec) error { func (c *cacheObjects) incHitsToMeta(ctx context.Context, dcache *diskCache, bucket, object string, size int64, eTag string, rs *HTTPRangeSpec) error {
metadata := make(map[string]string) metadata := map[string]string{"etag": eTag}
metadata["etag"] = eTag
return dcache.SaveMetadata(ctx, bucket, object, metadata, size, rs, "", true) return dcache.SaveMetadata(ctx, bucket, object, metadata, size, rs, "", true)
} }
// Backend metadata could have changed through server side copy - reset cache metadata if that is the case // Backend metadata could have changed through server side copy - reset cache metadata if that is the case
func (c *cacheObjects) updateMetadataIfChanged(ctx context.Context, dcache *diskCache, bucket, object string, bkObjectInfo, cacheObjInfo ObjectInfo, rs *HTTPRangeSpec) error { func (c *cacheObjects) updateMetadataIfChanged(ctx context.Context, dcache *diskCache, bucket, object string, bkObjectInfo, cacheObjInfo ObjectInfo, rs *HTTPRangeSpec) error {
bkMeta := make(map[string]string) bkMeta := make(map[string]string, len(bkObjectInfo.UserDefined))
cacheMeta := make(map[string]string) cacheMeta := make(map[string]string, len(cacheObjInfo.UserDefined))
for k, v := range bkObjectInfo.UserDefined { for k, v := range bkObjectInfo.UserDefined {
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) { if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
// Do not need to send any internal metadata // Do not need to send any internal metadata
@@ -166,7 +165,7 @@ func (c *cacheObjects) DeleteObjects(ctx context.Context, bucket string, objects
// construct a metadata k-v map // construct a metadata k-v map
func getMetadata(objInfo ObjectInfo) map[string]string { func getMetadata(objInfo ObjectInfo) map[string]string {
metadata := make(map[string]string) metadata := make(map[string]string, len(objInfo.UserDefined)+4)
metadata["etag"] = objInfo.ETag metadata["etag"] = objInfo.ETag
metadata["content-type"] = objInfo.ContentType metadata["content-type"] = objInfo.ContentType
if objInfo.ContentEncoding != "" { if objInfo.ContentEncoding != "" {
@@ -284,12 +283,6 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
// Reaching here implies cache miss // Reaching here implies cache miss
c.cacheStats.incMiss() c.cacheStats.incMiss()
// Since we got here, we are serving the request from backend,
// and also adding the object to the cache.
if dcache.diskUsageHigh() {
dcache.triggerGC <- struct{}{} // this is non-blocking
}
bkReader, bkErr := c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts) bkReader, bkErr := c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
if bkErr != nil { if bkErr != nil {
@@ -306,7 +299,9 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
if cacheErr == nil { if cacheErr == nil {
bkReader.ObjInfo.CacheLookupStatus = CacheHit bkReader.ObjInfo.CacheLookupStatus = CacheHit
} }
if !dcache.diskAvailable(objInfo.Size) {
// Check if we can add it without exceeding total cache size.
if !dcache.diskSpaceAvailable(objInfo.Size) {
return bkReader, bkErr return bkReader, bkErr
} }
@@ -338,11 +333,12 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
// Initialize pipe. // Initialize pipe.
pipeReader, pipeWriter := io.Pipe() pipeReader, pipeWriter := io.Pipe()
teeReader := io.TeeReader(bkReader, pipeWriter) teeReader := io.TeeReader(bkReader, pipeWriter)
userDefined := getMetadata(bkReader.ObjInfo)
go func() { go func() {
putErr := dcache.Put(ctx, bucket, object, putErr := dcache.Put(ctx, bucket, object,
io.LimitReader(pipeReader, bkReader.ObjInfo.Size), io.LimitReader(pipeReader, bkReader.ObjInfo.Size),
bkReader.ObjInfo.Size, nil, ObjectOptions{ bkReader.ObjInfo.Size, nil, ObjectOptions{
UserDefined: getMetadata(bkReader.ObjInfo), UserDefined: userDefined,
}, false) }, false)
// close the write end of the pipe, so the error gets // close the write end of the pipe, so the error gets
// propagated to getObjReader // propagated to getObjReader
@@ -612,9 +608,10 @@ func (c *cacheObjects) PutObject(ctx context.Context, bucket, object string, r *
} }
// fetch from backend if there is no space on cache drive // fetch from backend if there is no space on cache drive
if !dcache.diskAvailable(size) { if !dcache.diskSpaceAvailable(size) {
return putObjectFn(ctx, bucket, object, r, opts) return putObjectFn(ctx, bucket, object, r, opts)
} }
if opts.ServerSideEncryption != nil { if opts.ServerSideEncryption != nil {
dcache.Delete(ctx, bucket, object) dcache.Delete(ctx, bucket, object)
return putObjectFn(ctx, bucket, object, r, opts) return putObjectFn(ctx, bucket, object, r, opts)
@@ -721,7 +718,9 @@ func (c *cacheObjects) gc(ctx context.Context) {
} }
for _, dcache := range c.cache { for _, dcache := range c.cache {
if dcache != nil { if dcache != nil {
dcache.triggerGC <- struct{}{} // Check if there is disk.
// Will queue a GC scan if at high watermark.
dcache.diskSpaceAvailable(0)
} }
} }
} }
+53 -32
View File
@@ -17,6 +17,7 @@
package cmd package cmd
import ( import (
"math"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@@ -26,7 +27,8 @@ const (
dynamicTimeoutIncreaseThresholdPct = 0.33 // Upper threshold for failures in order to increase timeout dynamicTimeoutIncreaseThresholdPct = 0.33 // Upper threshold for failures in order to increase timeout
dynamicTimeoutDecreaseThresholdPct = 0.10 // Lower threshold for failures in order to decrease timeout dynamicTimeoutDecreaseThresholdPct = 0.10 // Lower threshold for failures in order to decrease timeout
dynamicTimeoutLogSize = 16 dynamicTimeoutLogSize = 16
maxDuration = 1<<63 - 1 maxDuration = math.MaxInt64
maxDynamicTimeout = 24 * time.Hour // Never set timeout bigger than this.
) )
// timeouts that are dynamically adapted based on actual usage results // timeouts that are dynamically adapted based on actual usage results
@@ -40,6 +42,12 @@ type dynamicTimeout struct {
// newDynamicTimeout returns a new dynamic timeout initialized with timeout value // newDynamicTimeout returns a new dynamic timeout initialized with timeout value
func newDynamicTimeout(timeout, minimum time.Duration) *dynamicTimeout { func newDynamicTimeout(timeout, minimum time.Duration) *dynamicTimeout {
if timeout <= 0 || minimum <= 0 {
panic("newDynamicTimeout: negative or zero timeout")
}
if minimum > timeout {
minimum = timeout
}
return &dynamicTimeout{timeout: int64(timeout), minimum: int64(minimum)} return &dynamicTimeout{timeout: int64(timeout), minimum: int64(minimum)}
} }
@@ -61,60 +69,73 @@ func (dt *dynamicTimeout) LogFailure() {
// logEntry stores a log entry // logEntry stores a log entry
func (dt *dynamicTimeout) logEntry(duration time.Duration) { func (dt *dynamicTimeout) logEntry(duration time.Duration) {
if duration < 0 {
return
}
entries := int(atomic.AddInt64(&dt.entries, 1)) entries := int(atomic.AddInt64(&dt.entries, 1))
index := entries - 1 index := entries - 1
if index < dynamicTimeoutLogSize { if index < dynamicTimeoutLogSize {
dt.mutex.Lock() dt.mutex.Lock()
dt.log[index] = duration dt.log[index] = duration
// We leak entries while we copy
if entries == dynamicTimeoutLogSize {
// Make copy on stack in order to call adjust()
logCopy := [dynamicTimeoutLogSize]time.Duration{}
copy(logCopy[:], dt.log[:])
// reset log entries
atomic.StoreInt64(&dt.entries, 0)
dt.mutex.Unlock()
dt.adjust(logCopy)
return
}
dt.mutex.Unlock() dt.mutex.Unlock()
} }
if entries == dynamicTimeoutLogSize {
dt.mutex.Lock()
// Make copy on stack in order to call adjust()
logCopy := [dynamicTimeoutLogSize]time.Duration{}
copy(logCopy[:], dt.log[:])
// reset log entries
atomic.StoreInt64(&dt.entries, 0)
dt.mutex.Unlock()
dt.adjust(logCopy)
}
} }
// adjust changes the value of the dynamic timeout based on the // adjust changes the value of the dynamic timeout based on the
// previous results // previous results
func (dt *dynamicTimeout) adjust(entries [dynamicTimeoutLogSize]time.Duration) { func (dt *dynamicTimeout) adjust(entries [dynamicTimeoutLogSize]time.Duration) {
failures, max := 0, time.Duration(0)
failures, average := 0, int64(0) for _, dur := range entries[:] {
for i := 0; i < len(entries); i++ { if dur == maxDuration {
if entries[i] == maxDuration {
failures++ failures++
} else { } else if dur > max {
average += int64(entries[i]) max = dur
} }
} }
if failures < len(entries) {
average /= int64(len(entries) - failures)
}
timeOutHitPct := float64(failures) / float64(len(entries)) failPct := float64(failures) / float64(len(entries))
if timeOutHitPct > dynamicTimeoutIncreaseThresholdPct { if failPct > dynamicTimeoutIncreaseThresholdPct {
// We are hitting the timeout too often, so increase the timeout by 25% // We are hitting the timeout too often, so increase the timeout by 25%
timeout := atomic.LoadInt64(&dt.timeout) * 125 / 100 timeout := atomic.LoadInt64(&dt.timeout) * 125 / 100
atomic.StoreInt64(&dt.timeout, timeout)
} else if timeOutHitPct < dynamicTimeoutDecreaseThresholdPct {
// We are hitting the timeout relatively few times, so decrease the timeout
average = average * 125 / 100 // Add buffer of 25% on top of average
timeout := (atomic.LoadInt64(&dt.timeout) + average) / 2 // Middle between current timeout and average success // Set upper cap.
if timeout > int64(maxDynamicTimeout) {
timeout = int64(maxDynamicTimeout)
}
// Safety, shouldn't happen
if timeout < dt.minimum {
timeout = dt.minimum
}
atomic.StoreInt64(&dt.timeout, timeout)
} else if failPct < dynamicTimeoutDecreaseThresholdPct {
// We are hitting the timeout relatively few times,
// so decrease the timeout towards 25 % of maximum time spent.
max = max * 125 / 100
timeout := atomic.LoadInt64(&dt.timeout)
if max < time.Duration(timeout) {
// Move 50% toward the max.
timeout = (int64(max) + timeout) / 2
}
if timeout < dt.minimum { if timeout < dt.minimum {
timeout = dt.minimum timeout = dt.minimum
} }
atomic.StoreInt64(&dt.timeout, timeout) atomic.StoreInt64(&dt.timeout, timeout)
} }
} }
+29 -3
View File
@@ -18,6 +18,8 @@ package cmd
import ( import (
"math/rand" "math/rand"
"runtime"
"sync"
"testing" "testing"
"time" "time"
) )
@@ -98,7 +100,7 @@ func TestDynamicTimeoutDualDecrease(t *testing.T) {
adjustedAgain := timeout.Timeout() adjustedAgain := timeout.Timeout()
if initial <= adjusted || adjusted <= adjustedAgain { if initial <= adjusted || adjusted <= adjustedAgain {
t.Errorf("Failure to decrease timeout multiple times") t.Errorf("Failure to decrease timeout multiple times, initial: %v, adjusted: %v, again: %v", initial, adjusted, adjustedAgain)
} }
} }
@@ -123,6 +125,30 @@ func TestDynamicTimeoutManyDecreases(t *testing.T) {
} }
} }
func TestDynamicTimeoutConcurrent(t *testing.T) {
// Race test.
timeout := newDynamicTimeout(time.Second, time.Millisecond)
var wg sync.WaitGroup
for i := 0; i < runtime.GOMAXPROCS(0); i++ {
wg.Add(1)
rng := rand.New(rand.NewSource(int64(i)))
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
timeout.LogFailure()
for j := 0; j < 100; j++ {
timeout.LogSuccess(time.Duration(float64(time.Second) * rng.Float64()))
}
to := timeout.Timeout()
if to < time.Millisecond || to > time.Second {
panic(to)
}
}
}()
}
wg.Wait()
}
func TestDynamicTimeoutHitMinimum(t *testing.T) { func TestDynamicTimeoutHitMinimum(t *testing.T) {
const minimum = 30 * time.Second const minimum = 30 * time.Second
@@ -168,7 +194,7 @@ func TestDynamicTimeoutAdjustExponential(t *testing.T) {
timeout := newDynamicTimeout(time.Minute, time.Second) timeout := newDynamicTimeout(time.Minute, time.Second)
rand.Seed(time.Now().UTC().UnixNano()) rand.Seed(0)
initial := timeout.Timeout() initial := timeout.Timeout()
@@ -188,7 +214,7 @@ func TestDynamicTimeoutAdjustNormalized(t *testing.T) {
timeout := newDynamicTimeout(time.Minute, time.Second) timeout := newDynamicTimeout(time.Minute, time.Second)
rand.Seed(time.Now().UTC().UnixNano()) rand.Seed(0)
initial := timeout.Timeout() initial := timeout.Timeout()
+2 -10
View File
@@ -388,12 +388,7 @@ func DecryptBlocksRequestR(inputReader io.Reader, h http.Header, offset,
object: object, object: object,
customerKeyHeader: h.Get(crypto.SSECKey), customerKeyHeader: h.Get(crypto.SSECKey),
copySource: copySource, copySource: copySource,
} metadata: cloneMSS(oi.UserDefined),
w.metadata = map[string]string{}
// Copy encryption metadata for internal use.
for k, v := range oi.UserDefined {
w.metadata[k] = v
} }
if w.copySource { if w.copySource {
@@ -432,10 +427,7 @@ type DecryptBlocksReader struct {
} }
func (d *DecryptBlocksReader) buildDecrypter(partID int) error { func (d *DecryptBlocksReader) buildDecrypter(partID int) error {
m := make(map[string]string) m := cloneMSS(d.metadata)
for k, v := range d.metadata {
m[k] = v
}
// Initialize the first decrypter; new decrypters will be // Initialize the first decrypter; new decrypters will be
// initialized in Read() operation as needed. // initialized in Read() operation as needed.
var key []byte var key []byte
+24 -1
View File
@@ -24,6 +24,7 @@ import (
"net/url" "net/url"
"path" "path"
"path/filepath" "path/filepath"
"reflect"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
@@ -203,6 +204,21 @@ type ZoneEndpoints struct {
// EndpointZones - list of list of endpoints // EndpointZones - list of list of endpoints
type EndpointZones []ZoneEndpoints type EndpointZones []ZoneEndpoints
// GetLocalZoneIdx returns the zone which endpoint belongs to locally.
// if ep is remote this code will return -1 zoneIndex
func (l EndpointZones) GetLocalZoneIdx(ep Endpoint) int {
for i, zep := range l {
for _, cep := range zep.Endpoints {
if cep.IsLocal && ep.IsLocal {
if reflect.DeepEqual(cep, ep) {
return i
}
}
}
}
return -1
}
// Add add zone endpoints // Add add zone endpoints
func (l *EndpointZones) Add(zeps ZoneEndpoints) error { func (l *EndpointZones) Add(zeps ZoneEndpoints) error {
existSet := set.NewStringSet() existSet := set.NewStringSet()
@@ -753,9 +769,16 @@ func GetProxyEndpoints(endpointZones EndpointZones) ([]ProxyEndpoint, error) {
RootCAs: globalRootCAs, RootCAs: globalRootCAs,
} }
} }
tr := newCustomHTTPTransport(tlsConfig, rest.DefaultRESTTimeout)()
// Allow more requests to be in flight with higher response header timeout.
tr.ResponseHeaderTimeout = 30 * time.Minute
tr.MaxIdleConns = 64
tr.MaxIdleConnsPerHost = 64
proxyEps = append(proxyEps, ProxyEndpoint{ proxyEps = append(proxyEps, ProxyEndpoint{
Endpoint: endpoint, Endpoint: endpoint,
Transport: newCustomHTTPTransport(tlsConfig, rest.DefaultRESTTimeout)(), Transport: tr,
}) })
} }
} }
+10 -55
View File
@@ -18,7 +18,6 @@ package cmd
import ( import (
"context" "context"
"sort"
"github.com/minio/minio-go/v7/pkg/s3utils" "github.com/minio/minio-go/v7/pkg/s3utils"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
@@ -49,7 +48,7 @@ func (er erasureObjects) MakeBucketWithLocation(ctx context.Context, bucket stri
index := index index := index
g.Go(func() error { g.Go(func() error {
if storageDisks[index] != nil { if storageDisks[index] != nil {
if err := storageDisks[index].MakeVol(bucket); err != nil { if err := storageDisks[index].MakeVol(ctx, bucket); err != nil {
if err != errVolumeExists { if err != errVolumeExists {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
} }
@@ -75,7 +74,7 @@ func undoDeleteBucket(storageDisks []StorageAPI, bucket string) {
} }
index := index index := index
g.Go(func() error { g.Go(func() error {
_ = storageDisks[index].MakeVol(bucket) _ = storageDisks[index].MakeVol(context.Background(), bucket)
return nil return nil
}, index) }, index)
} }
@@ -92,7 +91,7 @@ func (er erasureObjects) getBucketInfo(ctx context.Context, bucketName string) (
bucketErrs = append(bucketErrs, errDiskNotFound) bucketErrs = append(bucketErrs, errDiskNotFound)
continue continue
} }
volInfo, serr := disk.StatVol(bucketName) volInfo, serr := disk.StatVol(ctx, bucketName)
if serr == nil { if serr == nil {
return BucketInfo(volInfo), nil return BucketInfo(volInfo), nil
} }
@@ -122,53 +121,6 @@ func (er erasureObjects) GetBucketInfo(ctx context.Context, bucket string) (bi B
return bucketInfo, nil return bucketInfo, nil
} }
// listBuckets - returns list of all buckets from a disk picked at random.
func (er erasureObjects) listBuckets(ctx context.Context) (bucketsInfo []BucketInfo, err error) {
for _, disk := range er.getLoadBalancedDisks() {
if disk == nil {
continue
}
var volsInfo []VolInfo
volsInfo, err = disk.ListVols()
if err == nil {
// NOTE: The assumption here is that volumes across all disks in
// readQuorum have consistent view i.e they all have same number
// of buckets.
var bucketsInfo []BucketInfo
for _, volInfo := range volsInfo {
if isReservedOrInvalidBucket(volInfo.Name, true) {
continue
}
bucketsInfo = append(bucketsInfo, BucketInfo(volInfo))
}
// For buckets info empty, loop once again to check
// if we have, can happen if disks were down.
if len(bucketsInfo) == 0 {
continue
}
return bucketsInfo, nil
}
logger.LogIf(ctx, err)
// Ignore any disks not found.
if IsErrIgnored(err, bucketMetadataOpIgnoredErrs...) {
continue
}
break
}
return nil, err
}
// ListBuckets - lists all the buckets, sorted by its name.
func (er erasureObjects) ListBuckets(ctx context.Context) ([]BucketInfo, error) {
bucketInfos, err := er.listBuckets(ctx)
if err != nil {
return nil, toObjectErr(err)
}
// Sort by bucket name before returning.
sort.Sort(byBucketName(bucketInfos))
return bucketInfos, nil
}
// Dangling buckets should be handled appropriately, in this following situation // Dangling buckets should be handled appropriately, in this following situation
// we actually have quorum error to be `nil` but we have some disks where // we actually have quorum error to be `nil` but we have some disks where
// the bucket delete returned `errVolumeNotEmpty` but this is not correct // the bucket delete returned `errVolumeNotEmpty` but this is not correct
@@ -181,10 +133,10 @@ func deleteDanglingBucket(ctx context.Context, storageDisks []StorageAPI, dErrs
for index, err := range dErrs { for index, err := range dErrs {
if err == errVolumeNotEmpty { if err == errVolumeNotEmpty {
// Attempt to delete bucket again. // Attempt to delete bucket again.
if derr := storageDisks[index].DeleteVol(bucket, false); derr == errVolumeNotEmpty { if derr := storageDisks[index].DeleteVol(ctx, bucket, false); derr == errVolumeNotEmpty {
_ = cleanupDir(ctx, storageDisks[index], bucket, "") _ = cleanupDir(ctx, storageDisks[index], bucket, "")
_ = storageDisks[index].DeleteVol(bucket, false) _ = storageDisks[index].DeleteVol(ctx, bucket, false)
// Cleanup all the previously incomplete multiparts. // Cleanup all the previously incomplete multiparts.
_ = cleanupDir(ctx, storageDisks[index], minioMetaMultipartBucket, bucket) _ = cleanupDir(ctx, storageDisks[index], minioMetaMultipartBucket, bucket)
@@ -204,7 +156,7 @@ func (er erasureObjects) DeleteBucket(ctx context.Context, bucket string, forceD
index := index index := index
g.Go(func() error { g.Go(func() error {
if storageDisks[index] != nil { if storageDisks[index] != nil {
if err := storageDisks[index].DeleteVol(bucket, forceDelete); err != nil { if err := storageDisks[index].DeleteVol(ctx, bucket, forceDelete); err != nil {
return err return err
} }
err := cleanupDir(ctx, storageDisks[index], minioMetaMultipartBucket, bucket) err := cleanupDir(ctx, storageDisks[index], minioMetaMultipartBucket, bucket)
@@ -243,7 +195,10 @@ func (er erasureObjects) DeleteBucket(ctx context.Context, bucket string, forceD
// If we reduce quorum to nil, means we have deleted buckets properly // If we reduce quorum to nil, means we have deleted buckets properly
// on some servers in quorum, we should look for volumeNotEmpty errors // on some servers in quorum, we should look for volumeNotEmpty errors
// and delete those buckets as well. // and delete those buckets as well.
deleteDanglingBucket(ctx, storageDisks, dErrs, bucket) //
// let this call succeed, even if client cancels the context
// this is to ensure that we don't leave any stale content
deleteDanglingBucket(context.Background(), storageDisks, dErrs, bucket)
return nil return nil
} }
+19 -1
View File
@@ -34,6 +34,24 @@ func (er erasureObjects) getLoadBalancedLocalDisks() (newDisks []StorageAPI) {
return newDisks return newDisks
} }
// getLoadBalancedNDisks - fetches load balanced (sufficiently randomized) disk slice
// with N disks online. If ndisks is zero or negative, then it will returns all disks,
// same if ndisks is greater than the number of all disks.
func (er erasureObjects) getLoadBalancedNDisks(ndisks int) (newDisks []StorageAPI) {
disks := er.getLoadBalancedDisks()
for _, disk := range disks {
if disk == nil {
continue
}
newDisks = append(newDisks, disk)
ndisks--
if ndisks == 0 {
break
}
}
return
}
// getLoadBalancedDisks - fetches load balanced (sufficiently randomized) disk slice. // getLoadBalancedDisks - fetches load balanced (sufficiently randomized) disk slice.
func (er erasureObjects) getLoadBalancedDisks() (newDisks []StorageAPI) { func (er erasureObjects) getLoadBalancedDisks() (newDisks []StorageAPI) {
disks := er.getDisks() disks := er.getDisks()
@@ -77,7 +95,7 @@ func (er erasureObjects) isObject(ctx context.Context, bucket, prefix string) (o
return errDiskNotFound return errDiskNotFound
} }
// Check if 'prefix' is an object on this 'disk', else continue the check the next disk // Check if 'prefix' is an object on this 'disk', else continue the check the next disk
return storageDisks[index].CheckFile(bucket, prefix) return storageDisks[index].CheckFile(ctx, bucket, prefix)
}, index) }, index)
} }
+1
View File
@@ -32,6 +32,7 @@ func TestErasureParentDirIsObject(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Unable to initialize 'Erasure' object layer.") t.Fatalf("Unable to initialize 'Erasure' object layer.")
} }
defer obj.Shutdown(context.Background())
// Remove all disks. // Remove all disks.
for _, disk := range fsDisks { for _, disk := range fsDisks {
+1 -1
View File
@@ -28,7 +28,7 @@ import (
humanize "github.com/dustin/go-humanize" humanize "github.com/dustin/go-humanize"
) )
func (a badDisk) ReadFile(volume string, path string, offset int64, buf []byte, verifier *BitrotVerifier) (n int64, err error) { func (a badDisk) ReadFile(ctx context.Context, volume string, path string, offset int64, buf []byte, verifier *BitrotVerifier) (n int64, err error) {
return 0, errFaultyDisk return 0, errFaultyDisk
} }
+4 -4
View File
@@ -32,11 +32,11 @@ func (a badDisk) String() string {
return "bad-disk" return "bad-disk"
} }
func (a badDisk) AppendFile(volume string, path string, buf []byte) error { func (a badDisk) AppendFile(ctx context.Context, volume string, path string, buf []byte) error {
return errFaultyDisk return errFaultyDisk
} }
func (a badDisk) ReadFileStream(volume, path string, offset, length int64) (io.ReadCloser, error) { func (a badDisk) ReadFileStream(ctx context.Context, volume, path string, offset, length int64) (io.ReadCloser, error) {
return nil, errFaultyDisk return nil, errFaultyDisk
} }
@@ -44,7 +44,7 @@ func (a badDisk) UpdateBloomFilter(ctx context.Context, oldest, current uint64)
return nil, errFaultyDisk return nil, errFaultyDisk
} }
func (a badDisk) CreateFile(volume, path string, size int64, reader io.Reader) error { func (a badDisk) CreateFile(ctx context.Context, volume, path string, size int64, reader io.Reader) error {
return errFaultyDisk return errFaultyDisk
} }
@@ -195,7 +195,7 @@ func benchmarkErasureEncode(data, parity, dataDown, parityDown int, size int64,
if disk == OfflineDisk { if disk == OfflineDisk {
continue continue
} }
disk.DeleteFile("testbucket", "object") disk.DeleteFile(context.Background(), "testbucket", "object")
writers[i] = newBitrotWriter(disk, "testbucket", "object", erasure.ShardFileSize(size), DefaultBitrotAlgorithm, erasure.ShardSize()) writers[i] = newBitrotWriter(disk, "testbucket", "object", erasure.ShardFileSize(size), DefaultBitrotAlgorithm, erasure.ShardSize())
} }
_, err := erasure.Encode(context.Background(), bytes.NewReader(content), writers, buffer, erasure.dataBlocks+1) _, err := erasure.Encode(context.Background(), bytes.NewReader(content), writers, buffer, erasure.dataBlocks+1)
+2 -2
View File
@@ -175,9 +175,9 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
// disk has a valid xl.meta but may not have all the // disk has a valid xl.meta but may not have all the
// parts. This is considered an outdated disk, since // parts. This is considered an outdated disk, since
// it needs healing too. // it needs healing too.
dataErrs[i] = onlineDisk.VerifyFile(bucket, object, partsMetadata[i]) dataErrs[i] = onlineDisk.VerifyFile(ctx, bucket, object, partsMetadata[i])
case madmin.HealNormalScan: case madmin.HealNormalScan:
dataErrs[i] = onlineDisk.CheckParts(bucket, object, partsMetadata[i]) dataErrs[i] = onlineDisk.CheckParts(ctx, bucket, object, partsMetadata[i])
} }
if dataErrs[i] == nil { if dataErrs[i] == nil {
+3 -1
View File
@@ -99,6 +99,7 @@ func TestListOnlineDisks(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Prepare Erasure backend failed - %v", err) t.Fatalf("Prepare Erasure backend failed - %v", err)
} }
defer obj.Shutdown(context.Background())
defer removeRoots(disks) defer removeRoots(disks)
type tamperKind int type tamperKind int
@@ -210,7 +211,7 @@ func TestListOnlineDisks(t *testing.T) {
// and check if that disk // and check if that disk
// appears in outDatedDisks. // appears in outDatedDisks.
tamperedIndex = index tamperedIndex = index
dErr := erasureDisks[index].DeleteFile(bucket, pathJoin(object, fi.DataDir, "part.1")) dErr := erasureDisks[index].DeleteFile(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"))
if dErr != nil { if dErr != nil {
t.Fatalf("Test %d: Failed to delete %s - %v", i+1, t.Fatalf("Test %d: Failed to delete %s - %v", i+1,
filepath.Join(object, "part.1"), dErr) filepath.Join(object, "part.1"), dErr)
@@ -265,6 +266,7 @@ func TestDisksWithAllParts(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Prepare Erasure backend failed - %v", err) t.Fatalf("Prepare Erasure backend failed - %v", err)
} }
defer obj.Shutdown(context.Background())
defer removeRoots(disks) defer removeRoots(disks)
bucket := "bucket" bucket := "bucket"
+34 -41
View File
@@ -29,16 +29,6 @@ import (
"github.com/minio/minio/pkg/sync/errgroup" "github.com/minio/minio/pkg/sync/errgroup"
) )
func (er erasureObjects) ReloadFormat(ctx context.Context, dryRun bool) error {
logger.LogIf(ctx, NotImplemented{})
return NotImplemented{}
}
func (er erasureObjects) HealFormat(ctx context.Context, dryRun bool) (madmin.HealResultItem, error) {
logger.LogIf(ctx, NotImplemented{})
return madmin.HealResultItem{}, NotImplemented{}
}
// Heals a bucket if it doesn't exist on one of the disks, additionally // Heals a bucket if it doesn't exist on one of the disks, additionally
// also heals the missing entries for bucket metadata files // also heals the missing entries for bucket metadata files
// `policy.json, notification.xml, listeners.json`. // `policy.json, notification.xml, listeners.json`.
@@ -78,7 +68,7 @@ func healBucket(ctx context.Context, storageDisks []StorageAPI, storageEndpoints
afterState[index] = madmin.DriveStateOffline afterState[index] = madmin.DriveStateOffline
return errDiskNotFound return errDiskNotFound
} }
if _, serr := storageDisks[index].StatVol(bucket); serr != nil { if _, serr := storageDisks[index].StatVol(ctx, bucket); serr != nil {
if serr == errDiskNotFound { if serr == errDiskNotFound {
beforeState[index] = madmin.DriveStateOffline beforeState[index] = madmin.DriveStateOffline
afterState[index] = madmin.DriveStateOffline afterState[index] = madmin.DriveStateOffline
@@ -136,7 +126,7 @@ func healBucket(ctx context.Context, storageDisks []StorageAPI, storageEndpoints
index := index index := index
g.Go(func() error { g.Go(func() error {
if beforeState[index] == madmin.DriveStateMissing { if beforeState[index] == madmin.DriveStateMissing {
makeErr := storageDisks[index].MakeVol(bucket) makeErr := storageDisks[index].MakeVol(ctx, bucket)
if makeErr == nil { if makeErr == nil {
afterState[index] = madmin.DriveStateOk afterState[index] = madmin.DriveStateOk
} }
@@ -165,34 +155,37 @@ func healBucket(ctx context.Context, storageDisks []StorageAPI, storageEndpoints
// listAllBuckets lists all buckets from all disks. It also // listAllBuckets lists all buckets from all disks. It also
// returns the occurrence of each buckets in all disks // returns the occurrence of each buckets in all disks
func listAllBuckets(storageDisks []StorageAPI, healBuckets map[string]VolInfo) (err error) { func listAllBuckets(ctx context.Context, storageDisks []StorageAPI, healBuckets map[string]VolInfo) error {
for _, disk := range storageDisks { g := errgroup.WithNErrs(len(storageDisks))
if disk == nil { var mu sync.Mutex
continue for index := range storageDisks {
} index := index
var volsInfo []VolInfo g.Go(func() error {
volsInfo, err = disk.ListVols() if storageDisks[index] == nil {
if err != nil { // we ignore disk not found errors
if IsErrIgnored(err, bucketMetadataOpIgnoredErrs...) { return nil
continue
} }
return err volsInfo, err := storageDisks[index].ListVols(ctx)
} if err != nil {
for _, volInfo := range volsInfo { return err
// StorageAPI can send volume names which are
// incompatible with buckets - these are
// skipped, like the meta-bucket.
if isReservedOrInvalidBucket(volInfo.Name, false) {
continue
} }
// always save unique buckets across drives. for _, volInfo := range volsInfo {
if _, ok := healBuckets[volInfo.Name]; !ok { // StorageAPI can send volume names which are
healBuckets[volInfo.Name] = volInfo // incompatible with buckets - these are
// skipped, like the meta-bucket.
if isReservedOrInvalidBucket(volInfo.Name, false) {
continue
}
mu.Lock()
if _, ok := healBuckets[volInfo.Name]; !ok {
healBuckets[volInfo.Name] = volInfo
}
mu.Unlock()
} }
return nil
} }, index)
} }
return nil return reduceReadQuorumErrs(ctx, g.Wait(), bucketMetadataOpIgnoredErrs, len(storageDisks)/2)
} }
// Only heal on disks where we are sure that healing is needed. We can expand // Only heal on disks where we are sure that healing is needed. We can expand
@@ -275,7 +268,7 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
} }
case errs[i] == errDiskNotFound, dataErrs[i] == errDiskNotFound: case errs[i] == errDiskNotFound, dataErrs[i] == errDiskNotFound:
driveState = madmin.DriveStateOffline driveState = madmin.DriveStateOffline
case errs[i] == errFileNotFound, errs[i] == errVolumeNotFound: case errs[i] == errFileNotFound, errs[i] == errFileVersionNotFound, errs[i] == errVolumeNotFound:
fallthrough fallthrough
case dataErrs[i] == errFileNotFound, dataErrs[i] == errFileVersionNotFound, dataErrs[i] == errVolumeNotFound: case dataErrs[i] == errFileNotFound, dataErrs[i] == errFileVersionNotFound, dataErrs[i] == errVolumeNotFound:
driveState = madmin.DriveStateMissing driveState = madmin.DriveStateMissing
@@ -472,7 +465,7 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
} }
// Attempt a rename now from healed data to final location. // Attempt a rename now from healed data to final location.
if err = disk.RenameData(minioMetaTmpBucket, tmpID, partsMetadata[i].DataDir, bucket, object); err != nil { if err = disk.RenameData(ctx, minioMetaTmpBucket, tmpID, partsMetadata[i].DataDir, bucket, object); err != nil {
if err != errIsNotRegular && err != errFileNotFound { if err != errIsNotRegular && err != errFileNotFound {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
} }
@@ -525,7 +518,7 @@ func (er erasureObjects) healObjectDir(ctx context.Context, bucket, object strin
wg.Add(1) wg.Add(1)
go func(index int, disk StorageAPI) { go func(index int, disk StorageAPI) {
defer wg.Done() defer wg.Done()
_ = disk.DeleteFile(bucket, object) _ = disk.DeleteFile(ctx, bucket, object)
}(index, disk) }(index, disk)
} }
wg.Wait() wg.Wait()
@@ -557,7 +550,7 @@ func (er erasureObjects) healObjectDir(ctx context.Context, bucket, object strin
for i, err := range errs { for i, err := range errs {
if err == errVolumeNotFound || err == errFileNotFound { if err == errVolumeNotFound || err == errFileNotFound {
// Bucket or prefix/directory not found // Bucket or prefix/directory not found
merr := storageDisks[i].MakeVol(pathJoin(bucket, object)) merr := storageDisks[i].MakeVol(ctx, pathJoin(bucket, object))
switch merr { switch merr {
case nil, errVolumeExists: case nil, errVolumeExists:
hr.After.Drives[i].State = madmin.DriveStateOk hr.After.Drives[i].State = madmin.DriveStateOk
@@ -642,7 +635,7 @@ func statAllDirs(ctx context.Context, storageDisks []StorageAPI, bucket, prefix
} }
index := index index := index
g.Go(func() error { g.Go(func() error {
entries, err := storageDisks[index].ListDir(bucket, prefix, 1) entries, err := storageDisks[index].ListDir(ctx, bucket, prefix, 1)
if err != nil { if err != nil {
return err return err
} }
+17 -16
View File
@@ -39,6 +39,7 @@ func TestHealing(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer obj.Shutdown(context.Background())
defer removeRoots(fsDirs) defer removeRoots(fsDirs)
z := obj.(*erasureZones) z := obj.(*erasureZones)
@@ -66,7 +67,7 @@ func TestHealing(t *testing.T) {
} }
disk := er.getDisks()[0] disk := er.getDisks()[0]
fileInfoPreHeal, err := disk.ReadVersion(bucket, object, "") fileInfoPreHeal, err := disk.ReadVersion(context.Background(), bucket, object, "")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -83,7 +84,7 @@ func TestHealing(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
fileInfoPostHeal, err := disk.ReadVersion(bucket, object, "") fileInfoPostHeal, err := disk.ReadVersion(context.Background(), bucket, object, "")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -102,7 +103,7 @@ func TestHealing(t *testing.T) {
// gone down when an object was replaced by a new object. // gone down when an object was replaced by a new object.
fileInfoOutDated := fileInfoPreHeal fileInfoOutDated := fileInfoPreHeal
fileInfoOutDated.ModTime = time.Now() fileInfoOutDated.ModTime = time.Now()
err = disk.WriteMetadata(bucket, object, fileInfoOutDated) err = disk.WriteMetadata(context.Background(), bucket, object, fileInfoOutDated)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -112,7 +113,7 @@ func TestHealing(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
fileInfoPostHeal, err = disk.ReadVersion(bucket, object, "") fileInfoPostHeal, err = disk.ReadVersion(context.Background(), bucket, object, "")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -134,7 +135,7 @@ func TestHealing(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// Stat the bucket to make sure that it was created. // Stat the bucket to make sure that it was created.
_, err = er.getDisks()[0].StatVol(bucket) _, err = er.getDisks()[0].StatVol(context.Background(), bucket)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -200,7 +201,7 @@ func TestHealObjectCorrupted(t *testing.T) {
er := z.zones[0].sets[0] er := z.zones[0].sets[0]
erasureDisks := er.getDisks() erasureDisks := er.getDisks()
firstDisk := erasureDisks[0] firstDisk := erasureDisks[0]
err = firstDisk.DeleteFile(bucket, pathJoin(object, xlStorageFormatFile)) err = firstDisk.DeleteFile(context.Background(), bucket, pathJoin(object, xlStorageFormatFile))
if err != nil { if err != nil {
t.Fatalf("Failed to delete a file - %v", err) t.Fatalf("Failed to delete a file - %v", err)
} }
@@ -216,16 +217,16 @@ func TestHealObjectCorrupted(t *testing.T) {
t.Fatalf("Failed to getLatestFileInfo - %v", err) t.Fatalf("Failed to getLatestFileInfo - %v", err)
} }
if err = firstDisk.CheckFile(bucket, object); err != nil { if err = firstDisk.CheckFile(context.Background(), bucket, object); err != nil {
t.Errorf("Expected er.meta file to be present but stat failed - %v", err) t.Errorf("Expected er.meta file to be present but stat failed - %v", err)
} }
err = firstDisk.DeleteFile(bucket, pathJoin(object, fi.DataDir, "part.1")) err = firstDisk.DeleteFile(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"))
if err != nil { if err != nil {
t.Errorf("Failure during deleting part.1 - %v", err) t.Errorf("Failure during deleting part.1 - %v", err)
} }
err = firstDisk.WriteAll(bucket, pathJoin(object, fi.DataDir, "part.1"), bytes.NewReader([]byte{})) err = firstDisk.WriteAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), bytes.NewReader([]byte{}))
if err != nil { if err != nil {
t.Errorf("Failure during creating part.1 - %v", err) t.Errorf("Failure during creating part.1 - %v", err)
} }
@@ -245,13 +246,13 @@ func TestHealObjectCorrupted(t *testing.T) {
t.Fatalf("FileInfo not equal after healing") t.Fatalf("FileInfo not equal after healing")
} }
err = firstDisk.DeleteFile(bucket, pathJoin(object, fi.DataDir, "part.1")) err = firstDisk.DeleteFile(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"))
if err != nil { if err != nil {
t.Errorf("Failure during deleting part.1 - %v", err) t.Errorf("Failure during deleting part.1 - %v", err)
} }
bdata := bytes.Repeat([]byte("b"), int(nfi.Size)) bdata := bytes.Repeat([]byte("b"), int(nfi.Size))
err = firstDisk.WriteAll(bucket, pathJoin(object, fi.DataDir, "part.1"), bytes.NewReader(bdata)) err = firstDisk.WriteAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), bytes.NewReader(bdata))
if err != nil { if err != nil {
t.Errorf("Failure during creating part.1 - %v", err) t.Errorf("Failure during creating part.1 - %v", err)
} }
@@ -274,7 +275,7 @@ func TestHealObjectCorrupted(t *testing.T) {
// Test 4: checks if HealObject returns an error when xl.meta is not found // Test 4: checks if HealObject returns an error when xl.meta is not found
// in more than read quorum number of disks, to create a corrupted situation. // in more than read quorum number of disks, to create a corrupted situation.
for i := 0; i <= len(er.getDisks())/2; i++ { for i := 0; i <= len(er.getDisks())/2; i++ {
er.getDisks()[i].DeleteFile(bucket, pathJoin(object, xlStorageFormatFile)) er.getDisks()[i].DeleteFile(context.Background(), bucket, pathJoin(object, xlStorageFormatFile))
} }
// Try healing now, expect to receive errFileNotFound. // Try healing now, expect to receive errFileNotFound.
@@ -350,7 +351,7 @@ func TestHealObjectErasure(t *testing.T) {
t.Fatalf("Failed to complete multipart upload - %v", err) t.Fatalf("Failed to complete multipart upload - %v", err)
} }
err = firstDisk.DeleteFile(bucket, pathJoin(object, xlStorageFormatFile)) err = firstDisk.DeleteFile(context.Background(), bucket, pathJoin(object, xlStorageFormatFile))
if err != nil { if err != nil {
t.Fatalf("Failed to delete a file - %v", err) t.Fatalf("Failed to delete a file - %v", err)
} }
@@ -360,7 +361,7 @@ func TestHealObjectErasure(t *testing.T) {
t.Fatalf("Failed to heal object - %v", err) t.Fatalf("Failed to heal object - %v", err)
} }
if err = firstDisk.CheckFile(bucket, object); err != nil { if err = firstDisk.CheckFile(context.Background(), bucket, object); err != nil {
t.Errorf("Expected er.meta file to be present but stat failed - %v", err) t.Errorf("Expected er.meta file to be present but stat failed - %v", err)
} }
@@ -421,7 +422,7 @@ func TestHealEmptyDirectoryErasure(t *testing.T) {
z := obj.(*erasureZones) z := obj.(*erasureZones)
er := z.zones[0].sets[0] er := z.zones[0].sets[0]
firstDisk := er.getDisks()[0] firstDisk := er.getDisks()[0]
err = firstDisk.DeleteFile(bucket, object) err = firstDisk.DeleteVol(context.Background(), pathJoin(bucket, encodeDirObject(object)), true)
if err != nil { if err != nil {
t.Fatalf("Failed to delete a file - %v", err) t.Fatalf("Failed to delete a file - %v", err)
} }
@@ -433,7 +434,7 @@ func TestHealEmptyDirectoryErasure(t *testing.T) {
} }
// Check if the empty directory is restored in the first disk // Check if the empty directory is restored in the first disk
_, err = firstDisk.StatVol(pathJoin(bucket, object)) _, err = firstDisk.StatVol(context.Background(), pathJoin(bucket, encodeDirObject(object)))
if err != nil { if err != nil {
t.Fatalf("Expected object to be present but stat failed - %v", err) t.Fatalf("Expected object to be present but stat failed - %v", err)
} }
-58
View File
@@ -1,58 +0,0 @@
/*
* MinIO Cloud Storage, (C) 2016-2020 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
"context"
"github.com/minio/minio/pkg/madmin"
)
// ListObjectVersions - This is not implemented, look for erasure-zones.ListObjectVersions()
func (er erasureObjects) ListObjectVersions(ctx context.Context, bucket, prefix, marker, versionMarker, delimiter string, maxKeys int) (loi ListObjectVersionsInfo, e error) {
return loi, NotImplemented{}
}
// ListObjectsV2 - This is not implemented/needed anymore, look for erasure-zones.ListObjectsV2()
func (er erasureObjects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (loi ListObjectsV2Info, e error) {
return loi, NotImplemented{}
}
// ListObjects - This is not implemented/needed anymore, look for erasure-zones.ListObjects()
func (er erasureObjects) ListObjects(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (loi ListObjectsInfo, e error) {
return loi, NotImplemented{}
}
// ListBucketsHeal - This is not implemented/needed anymore, look for erasure-zones.ListBucketHeal()
func (er erasureObjects) ListBucketsHeal(ctx context.Context) ([]BucketInfo, error) {
return nil, NotImplemented{}
}
// ListObjectsHeal - This is not implemented, look for erasure-zones.ListObjectsHeal()
func (er erasureObjects) ListObjectsHeal(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (loi ListObjectsInfo, err error) {
return ListObjectsInfo{}, NotImplemented{}
}
// HealObjects - This is not implemented/needed anymore, look for erasure-zones.HealObjects()
func (er erasureObjects) HealObjects(ctx context.Context, bucket, prefix string, _ madmin.HealOpts, _ HealObjectFn) (e error) {
return NotImplemented{}
}
// Walk - This is not implemented/needed anymore, look for erasure-zones.Walk()
func (er erasureObjects) Walk(ctx context.Context, bucket, prefix string, results chan<- ObjectInfo, _ ObjectOptions) error {
return NotImplemented{}
}
+2 -2
View File
@@ -119,14 +119,14 @@ func readAllFileInfo(ctx context.Context, disks []StorageAPI, bucket, object, ve
metadataArray := make([]FileInfo, len(disks)) metadataArray := make([]FileInfo, len(disks))
g := errgroup.WithNErrs(len(disks)) g := errgroup.WithNErrs(len(disks))
// Read `xl.meta` parallelly across disks. // Read `xl.meta` in parallel across disks.
for index := range disks { for index := range disks {
index := index index := index
g.Go(func() (err error) { g.Go(func() (err error) {
if disks[index] == nil { if disks[index] == nil {
return errDiskNotFound return errDiskNotFound
} }
metadataArray[index], err = disks[index].ReadVersion(bucket, object, versionID) metadataArray[index], err = disks[index].ReadVersion(ctx, bucket, object, versionID)
if err != nil { if err != nil {
if err != errFileNotFound && err != errVolumeNotFound && err != errFileVersionNotFound { if err != errFileNotFound && err != errVolumeNotFound && err != errFileVersionNotFound {
logger.GetReqInfo(ctx).AppendTags("disk", disks[index].String()) logger.GetReqInfo(ctx).AppendTags("disk", disks[index].String())
+9 -10
View File
@@ -96,18 +96,17 @@ func (fi FileInfo) IsValid() bool {
// ToObjectInfo - Converts metadata to object info. // ToObjectInfo - Converts metadata to object info.
func (fi FileInfo) ToObjectInfo(bucket, object string) ObjectInfo { func (fi FileInfo) ToObjectInfo(bucket, object string) ObjectInfo {
if HasSuffix(object, SlashSeparator) { object = decodeDirObject(object)
return ObjectInfo{ versionID := fi.VersionID
Bucket: bucket, if globalBucketVersioningSys.Enabled(bucket) && versionID == "" {
Name: object, versionID = nullVersionID
IsDir: true,
}
} }
objInfo := ObjectInfo{ objInfo := ObjectInfo{
IsDir: false, IsDir: HasSuffix(object, SlashSeparator),
Bucket: bucket, Bucket: bucket,
Name: object, Name: object,
VersionID: fi.VersionID, VersionID: versionID,
IsLatest: fi.IsLatest, IsLatest: fi.IsLatest,
DeleteMarker: fi.Deleted, DeleteMarker: fi.Deleted,
Size: fi.Size, Size: fi.Size,
@@ -273,7 +272,7 @@ func renameFileInfo(ctx context.Context, disks []StorageAPI, srcBucket, srcEntry
if disks[index] == nil { if disks[index] == nil {
return errDiskNotFound return errDiskNotFound
} }
if err := disks[index].RenameData(srcBucket, srcEntry, "", dstBucket, dstEntry); err != nil { if err := disks[index].RenameData(ctx, srcBucket, srcEntry, "", dstBucket, dstEntry); err != nil {
if !IsErrIgnored(err, ignoredErr...) { if !IsErrIgnored(err, ignoredErr...) {
return err return err
} }
@@ -304,7 +303,7 @@ func writeUniqueFileInfo(ctx context.Context, disks []StorageAPI, bucket, prefix
} }
// Pick one FileInfo for a disk at index. // Pick one FileInfo for a disk at index.
files[index].Erasure.Index = index + 1 files[index].Erasure.Index = index + 1
return disks[index].WriteMetadata(bucket, prefix, files[index]) return disks[index].WriteMetadata(ctx, bucket, prefix, files[index])
}, index) }, index)
} }
+85 -27
View File
@@ -63,7 +63,7 @@ func (er erasureObjects) removeObjectPart(bucket, object, uploadID, dataDir stri
// Ignoring failure to remove parts that weren't present in CompleteMultipartUpload // Ignoring failure to remove parts that weren't present in CompleteMultipartUpload
// requests. xl.meta is the authoritative source of truth on which parts constitute // requests. xl.meta is the authoritative source of truth on which parts constitute
// the object. The presence of parts that don't belong in the object doesn't affect correctness. // the object. The presence of parts that don't belong in the object doesn't affect correctness.
_ = storageDisks[index].DeleteFile(minioMetaMultipartBucket, curpartPath) _ = storageDisks[index].DeleteFile(context.TODO(), minioMetaMultipartBucket, curpartPath)
return nil return nil
}, index) }, index)
} }
@@ -71,7 +71,7 @@ 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) cleanupStaleMultipartUploads(ctx context.Context, cleanupInterval, expiry time.Duration) { func (er erasureObjects) cleanupStaleUploads(ctx context.Context, cleanupInterval, expiry time.Duration) {
ticker := time.NewTicker(cleanupInterval) ticker := time.NewTicker(cleanupInterval)
defer ticker.Stop() defer ticker.Stop()
@@ -91,26 +91,26 @@ func (er erasureObjects) cleanupStaleMultipartUploads(ctx context.Context, clean
if disk == nil { if disk == nil {
continue continue
} }
er.cleanupStaleMultipartUploadsOnDisk(ctx, disk, expiry) er.cleanupStaleUploadsOnDisk(ctx, disk, expiry)
} }
} }
} }
// Remove the old multipart uploads on the given disk. // Remove the old multipart uploads on the given disk.
func (er erasureObjects) cleanupStaleMultipartUploadsOnDisk(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(minioMetaMultipartBucket, "", -1) shaDirs, err := disk.ListDir(ctx, minioMetaMultipartBucket, "", -1)
if err != nil { if err != nil {
return return
} }
for _, shaDir := range shaDirs { for _, shaDir := range shaDirs {
uploadIDDirs, err := disk.ListDir(minioMetaMultipartBucket, shaDir, -1) uploadIDDirs, err := disk.ListDir(ctx, minioMetaMultipartBucket, shaDir, -1)
if err != nil { if err != nil {
continue continue
} }
for _, uploadIDDir := range uploadIDDirs { for _, uploadIDDir := range uploadIDDirs {
uploadIDPath := pathJoin(shaDir, uploadIDDir) uploadIDPath := pathJoin(shaDir, uploadIDDir)
fi, err := disk.ReadVersion(minioMetaMultipartBucket, uploadIDPath, "") fi, err := disk.ReadVersion(ctx, minioMetaMultipartBucket, uploadIDPath, "")
if err != nil { if err != nil {
continue continue
} }
@@ -119,6 +119,19 @@ func (er erasureObjects) cleanupStaleMultipartUploadsOnDisk(ctx context.Context,
} }
} }
} }
tmpDirs, err := disk.ListDir(ctx, minioMetaTmpBucket, "", -1)
if err != nil {
return
}
for _, tmpDir := range tmpDirs {
fi, err := disk.ReadVersion(ctx, minioMetaTmpBucket, tmpDir, "")
if err != nil {
continue
}
if now.Sub(fi.ModTime) > expiry {
er.deleteObject(ctx, minioMetaTmpBucket, tmpDir, fi.Erasure.DataBlocks+1)
}
}
} }
// ListMultipartUploads - lists all the pending multipart // ListMultipartUploads - lists all the pending multipart
@@ -139,7 +152,7 @@ func (er erasureObjects) ListMultipartUploads(ctx context.Context, bucket, objec
if disk == nil { if disk == nil {
continue continue
} }
uploadIDs, err = disk.ListDir(minioMetaMultipartBucket, er.getMultipartSHADir(bucket, object), -1) uploadIDs, err = disk.ListDir(ctx, minioMetaMultipartBucket, er.getMultipartSHADir(bucket, object), -1)
if err != nil { if err != nil {
if err == errDiskNotFound { if err == errDiskNotFound {
continue continue
@@ -172,7 +185,7 @@ retry:
if populatedUploadIds.Contains(uploadID) { if populatedUploadIds.Contains(uploadID) {
continue continue
} }
fi, err := disk.ReadVersion(minioMetaMultipartBucket, pathJoin(er.getUploadIDDir(bucket, object, uploadID)), "") fi, err := disk.ReadVersion(ctx, minioMetaMultipartBucket, pathJoin(er.getUploadIDDir(bucket, object, uploadID)), "")
if err != nil { if err != nil {
if err == errDiskNotFound || err == errFileNotFound { if err == errDiskNotFound || err == errFileNotFound {
goto retry goto retry
@@ -267,11 +280,7 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
fi.DataDir = mustGetUUID() fi.DataDir = mustGetUUID()
fi.ModTime = UTCNow() fi.ModTime = UTCNow()
if opts.UserDefined != nil { fi.Metadata = cloneMSS(opts.UserDefined)
fi.Metadata = opts.UserDefined
} else {
fi.Metadata = make(map[string]string)
}
uploadID := mustGetUUID() uploadID := mustGetUUID()
uploadIDPath := er.getUploadIDDir(bucket, object, uploadID) uploadIDPath := er.getUploadIDDir(bucket, object, uploadID)
@@ -337,7 +346,18 @@ func (er erasureObjects) CopyObjectPart(ctx context.Context, srcBucket, srcObjec
// of the multipart transaction. // of the multipart transaction.
// //
// 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, e 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(ctx, bucket, pathJoin(object, uploadID))
if err = uploadIDLock.GetRLock(globalOperationTimeout); err != nil {
return PartInfo{}, err
}
readLocked := true
defer func() {
if readLocked {
uploadIDLock.RUnlock()
}
}()
data := r.Reader data := r.Reader
// Validate input data size and it can never be less than zero. // Validate input data size and it can never be less than zero.
if data.Size() < -1 { if data.Size() < -1 {
@@ -350,7 +370,7 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
uploadIDPath := er.getUploadIDDir(bucket, object, uploadID) uploadIDPath := er.getUploadIDDir(bucket, object, uploadID)
// Validates if upload ID exists. // Validates if upload ID exists.
if err := er.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil { if err = er.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil {
return pi, toObjectErr(err, bucket, object, uploadID) return pi, toObjectErr(err, bucket, object, uploadID)
} }
@@ -428,7 +448,7 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
// Should return IncompleteBody{} error when reader has fewer bytes // Should return IncompleteBody{} error when reader has fewer bytes
// than specified in request header. // than specified in request header.
if n < data.Size() { if n < data.Size() {
return pi, IncompleteBody{} return pi, IncompleteBody{Bucket: bucket, Object: object}
} }
for i := range writers { for i := range writers {
@@ -437,8 +457,17 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
} }
} }
// Unlock here before acquiring write locks all concurrent
// PutObjectParts would serialize here updating `xl.meta`
uploadIDLock.RUnlock()
readLocked = false
if err = uploadIDLock.GetLock(globalOperationTimeout); err != nil {
return PartInfo{}, err
}
defer uploadIDLock.Unlock()
// Validates if upload ID exists. // Validates if upload ID exists.
if err := er.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil { if err = er.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil {
return pi, toObjectErr(err, bucket, object, uploadID) return pi, toObjectErr(err, bucket, object, uploadID)
} }
@@ -513,6 +542,12 @@ func (er erasureObjects) GetMultipartInfo(ctx context.Context, bucket, object, u
UploadID: uploadID, UploadID: uploadID,
} }
uploadIDLock := er.NewNSLock(ctx, bucket, pathJoin(object, uploadID))
if err := uploadIDLock.GetRLock(globalOperationTimeout); err != nil {
return MultipartInfo{}, err
}
defer uploadIDLock.RUnlock()
if err := er.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil { if err := er.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil {
return result, toObjectErr(err, bucket, object, uploadID) return result, toObjectErr(err, bucket, object, uploadID)
} }
@@ -543,7 +578,7 @@ func (er erasureObjects) GetMultipartInfo(ctx context.Context, bucket, object, u
return result, err return result, err
} }
result.UserDefined = fi.Metadata result.UserDefined = cloneMSS(fi.Metadata)
return result, nil return result, nil
} }
@@ -555,6 +590,12 @@ func (er erasureObjects) GetMultipartInfo(ctx context.Context, bucket, object, u
// 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, e error) {
uploadIDLock := er.NewNSLock(ctx, bucket, pathJoin(object, uploadID))
if err := uploadIDLock.GetRLock(globalOperationTimeout); err != nil {
return ListPartsInfo{}, err
}
defer uploadIDLock.RUnlock()
if err := er.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil { if err := er.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil {
return result, toObjectErr(err, bucket, object, uploadID) return result, toObjectErr(err, bucket, object, uploadID)
} }
@@ -591,7 +632,7 @@ func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, up
result.UploadID = uploadID result.UploadID = uploadID
result.MaxParts = maxParts result.MaxParts = maxParts
result.PartNumberMarker = partNumberMarker result.PartNumberMarker = partNumberMarker
result.UserDefined = fi.Metadata result.UserDefined = cloneMSS(fi.Metadata)
// For empty number of parts or maxParts as zero, return right here. // For empty number of parts or maxParts as zero, return right here.
if len(fi.Parts) == 0 || maxParts == 0 { if len(fi.Parts) == 0 || maxParts == 0 {
@@ -639,8 +680,16 @@ func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, up
// md5sums of all the parts. // md5sums of all the parts.
// //
// Implements S3 compatible Complete multipart API. // Implements S3 compatible Complete multipart API.
func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket string, object string, uploadID string, parts []CompletePart, opts ObjectOptions) (oi ObjectInfo, e error) { func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket string, object string, uploadID string, parts []CompletePart, opts ObjectOptions) (oi ObjectInfo, err error) {
if err := er.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil { // Hold read-locks to verify uploaded parts, also disallows
// parallel part uploads as well.
uploadIDLock := er.NewNSLock(ctx, bucket, pathJoin(object, uploadID))
if err = uploadIDLock.GetRLock(globalOperationTimeout); err != nil {
return oi, err
}
defer uploadIDLock.RUnlock()
if err = er.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil {
return oi, toObjectErr(err, bucket, object, uploadID) return oi, toObjectErr(err, bucket, object, uploadID)
} }
@@ -788,6 +837,13 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
} }
} }
// Hold namespace to complete the transaction
lk := er.NewNSLock(ctx, bucket, object)
if err = lk.GetLock(globalOperationTimeout); err != nil {
return oi, err
}
defer lk.Unlock()
// Rename the multipart object to final location. // Rename the multipart object to final location.
if onlineDisks, err = renameData(ctx, onlineDisks, minioMetaMultipartBucket, uploadIDPath, if onlineDisks, err = renameData(ctx, onlineDisks, minioMetaMultipartBucket, uploadIDPath,
fi.DataDir, bucket, object, writeQuorum, nil); err != nil { fi.DataDir, bucket, object, writeQuorum, nil); err != nil {
@@ -823,11 +879,13 @@ 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 {
// Implements S3 compatible Abort multipart API, slight difference is lk := er.NewNSLock(ctx, bucket, pathJoin(object, uploadID))
// that this is an atomic idempotent operation. Subsequent calls have if err := lk.GetLock(globalOperationTimeout); err != nil {
// no affect and further requests to the same uploadID would not be honored. return err
func (er erasureObjects) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string) error { }
defer lk.Unlock()
// Validates if upload ID exists. // Validates if upload ID exists.
if err := er.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil { if err := er.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil {
return toObjectErr(err, bucket, object, uploadID) return toObjectErr(err, bucket, object, uploadID)
+78 -57
View File
@@ -25,6 +25,7 @@ import (
"net/http" "net/http"
"path" "path"
"sync" "sync"
"time"
"github.com/minio/minio-go/v7/pkg/tags" "github.com/minio/minio-go/v7/pkg/tags"
xhttp "github.com/minio/minio/cmd/http" xhttp "github.com/minio/minio/cmd/http"
@@ -36,30 +37,6 @@ import (
// list all errors which can be ignored in object operations. // list all errors which can be ignored in object operations.
var objectOpIgnoredErrs = append(baseIgnoredErrs, errDiskAccessDenied, errUnformattedDisk) var objectOpIgnoredErrs = append(baseIgnoredErrs, errDiskAccessDenied, errUnformattedDisk)
// putObjectDir hints the bottom layer to create a new directory.
func (er erasureObjects) putObjectDir(ctx context.Context, bucket, object string, writeQuorum int) error {
storageDisks := er.getDisks()
g := errgroup.WithNErrs(len(storageDisks))
// Prepare object creation in all disks
for index := range storageDisks {
if storageDisks[index] == nil {
continue
}
index := index
g.Go(func() error {
err := storageDisks[index].MakeVol(pathJoin(bucket, object))
if err != nil && err != errVolumeExists {
return err
}
return nil
}, index)
}
return reduceWriteQuorumErrs(ctx, g.Wait(), objectOpIgnoredErrs, writeQuorum)
}
/// Object Operations /// Object Operations
// CopyObject - copy object source object to destination object. // CopyObject - copy object source object to destination object.
@@ -71,6 +48,11 @@ func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, d
return oi, NotImplemented{} return oi, NotImplemented{}
} }
defer ObjectPathUpdated(path.Join(dstBucket, dstObject)) defer ObjectPathUpdated(path.Join(dstBucket, dstObject))
lk := er.NewNSLock(ctx, dstBucket, dstObject)
if err := lk.GetLock(globalOperationTimeout); err != nil {
return oi, err
}
defer lk.Unlock()
// Read metadata associated with the object from all disks. // Read metadata associated with the object from all disks.
storageDisks := er.getDisks() storageDisks := er.getDisks()
@@ -111,18 +93,19 @@ func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, d
fi.VersionID = versionID // set any new versionID we might have created fi.VersionID = versionID // set any new versionID we might have created
fi.ModTime = modTime // set modTime for the new versionID fi.ModTime = modTime // set modTime for the new versionID
srcInfo.UserDefined["etag"] = srcInfo.ETag
// Update `xl.meta` content on each disks. // Update `xl.meta` content on each disks.
for index := range metaArr { for index := range metaArr {
metaArr[index].ModTime = modTime metaArr[index].ModTime = modTime
metaArr[index].VersionID = versionID metaArr[index].VersionID = versionID
metaArr[index].Metadata = srcInfo.UserDefined metaArr[index].Metadata = srcInfo.UserDefined
metaArr[index].Metadata["etag"] = srcInfo.ETag
} }
tempObj := mustGetUUID() tempObj := mustGetUUID()
// Cleanup in case of xl.meta writing failure // Cleanup in case of xl.meta writing failure
defer er.deleteObject(ctx, minioMetaTmpBucket, tempObj, writeQuorum) defer er.deleteObject(context.Background(), minioMetaTmpBucket, tempObj, writeQuorum)
// 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 {
@@ -144,6 +127,32 @@ func (er erasureObjects) GetObjectNInfo(ctx context.Context, bucket, object stri
return nil, err return nil, err
} }
var unlockOnDefer bool
var nsUnlocker = func() {}
defer func() {
if unlockOnDefer {
nsUnlocker()
}
}()
// Acquire lock
if lockType != noLock {
lock := er.NewNSLock(ctx, bucket, object)
switch lockType {
case writeLock:
if err = lock.GetLock(globalOperationTimeout); err != nil {
return nil, err
}
nsUnlocker = lock.Unlock
case readLock:
if err = lock.GetRLock(globalOperationTimeout); err != nil {
return nil, err
}
nsUnlocker = lock.RUnlock
}
unlockOnDefer = true
}
// Handler directory request by returning a reader that // Handler directory request by returning a reader that
// returns no bytes. // returns no bytes.
if HasSuffix(object, SlashSeparator) { if HasSuffix(object, SlashSeparator) {
@@ -151,7 +160,8 @@ func (er erasureObjects) GetObjectNInfo(ctx context.Context, bucket, object stri
if objInfo, err = er.getObjectInfoDir(ctx, bucket, object); err != nil { if objInfo, err = er.getObjectInfoDir(ctx, bucket, object); err != nil {
return nil, toObjectErr(err, bucket, object) return nil, toObjectErr(err, bucket, object)
} }
return NewGetObjectReaderFromReader(bytes.NewBuffer(nil), objInfo, opts) unlockOnDefer = false
return NewGetObjectReaderFromReader(bytes.NewBuffer(nil), objInfo, opts, nsUnlocker)
} }
fi, metaArr, onlineDisks, err := er.getObjectFileInfo(ctx, bucket, object, opts) fi, metaArr, onlineDisks, err := er.getObjectFileInfo(ctx, bucket, object, opts)
@@ -172,7 +182,8 @@ func (er erasureObjects) GetObjectNInfo(ctx context.Context, bucket, object stri
}, toObjectErr(errMethodNotAllowed, bucket, object) }, toObjectErr(errMethodNotAllowed, bucket, object)
} }
fn, off, length, nErr := NewGetObjectReader(rs, objInfo, opts) unlockOnDefer = false
fn, off, length, nErr := NewGetObjectReader(rs, objInfo, opts, nsUnlocker)
if nErr != nil { if nErr != nil {
return nil, nErr return nil, nErr
} }
@@ -201,6 +212,13 @@ func (er erasureObjects) GetObject(ctx context.Context, bucket, object string, s
return err return err
} }
// Lock the object before reading.
lk := er.NewNSLock(ctx, bucket, object)
if err := lk.GetRLock(globalOperationTimeout); err != nil {
return err
}
defer lk.RUnlock()
// Start offset cannot be negative. // Start offset cannot be negative.
if startOffset < 0 { if startOffset < 0 {
logger.LogIf(ctx, errUnexpected, logger.Application) logger.LogIf(ctx, errUnexpected, logger.Application)
@@ -362,7 +380,7 @@ func (er erasureObjects) getObjectInfoDir(ctx context.Context, bucket, object st
index := index index := index
g.Go(func() error { g.Go(func() error {
// Check if 'prefix' is an object on this 'disk'. // Check if 'prefix' is an object on this 'disk'.
entries, err := storageDisks[index].ListDir(bucket, object, 1) entries, err := storageDisks[index].ListDir(ctx, bucket, object, 1)
if err != nil { if err != nil {
return err return err
} }
@@ -385,6 +403,13 @@ func (er erasureObjects) GetObjectInfo(ctx context.Context, bucket, object strin
return info, err return info, err
} }
// Lock the object before reading.
lk := er.NewNSLock(ctx, bucket, object)
if err := lk.GetRLock(globalOperationTimeout); err != nil {
return ObjectInfo{}, err
}
defer lk.RUnlock()
if HasSuffix(object, SlashSeparator) { if HasSuffix(object, SlashSeparator) {
info, err = er.getObjectInfoDir(ctx, bucket, object) info, err = er.getObjectInfoDir(ctx, bucket, object)
if err != nil { if err != nil {
@@ -459,7 +484,7 @@ func undoRename(disks []StorageAPI, srcBucket, srcEntry, dstBucket, dstEntry str
index := index index := index
g.Go(func() error { g.Go(func() error {
if errs[index] == nil { if errs[index] == nil {
_ = disks[index].RenameFile(dstBucket, dstEntry, srcBucket, srcEntry) _ = disks[index].RenameFile(context.TODO(), dstBucket, dstEntry, srcBucket, srcEntry)
} }
return nil return nil
}, index) }, index)
@@ -479,7 +504,7 @@ func renameData(ctx context.Context, disks []StorageAPI, srcBucket, srcEntry, da
if disks[index] == nil { if disks[index] == nil {
return errDiskNotFound return errDiskNotFound
} }
if err := disks[index].RenameData(srcBucket, srcEntry, dataDir, dstBucket, dstEntry); err != nil { if err := disks[index].RenameData(ctx, srcBucket, srcEntry, dataDir, dstBucket, dstEntry); err != nil {
if !IsErrIgnored(err, ignoredErr...) { if !IsErrIgnored(err, ignoredErr...) {
return err return err
} }
@@ -504,7 +529,7 @@ func renameData(ctx context.Context, disks []StorageAPI, srcBucket, srcEntry, da
ug.Go(func() error { ug.Go(func() error {
// Undo all the partial rename operations. // Undo all the partial rename operations.
if errs[index] == nil { if errs[index] == nil {
_ = disks[index].RenameData(dstBucket, dstEntry, dataDir, srcBucket, srcEntry) _ = disks[index].RenameData(context.Background(), dstBucket, dstEntry, dataDir, srcBucket, srcEntry)
} }
return nil return nil
}, index) }, index)
@@ -532,7 +557,7 @@ func rename(ctx context.Context, disks []StorageAPI, srcBucket, srcEntry, dstBuc
if disks[index] == nil { if disks[index] == nil {
return errDiskNotFound return errDiskNotFound
} }
if err := disks[index].RenameFile(srcBucket, srcEntry, dstBucket, dstEntry); err != nil { if err := disks[index].RenameFile(ctx, srcBucket, srcEntry, dstBucket, dstEntry); err != nil {
if !IsErrIgnored(err, ignoredErr...) { if !IsErrIgnored(err, ignoredErr...) {
return err return err
} }
@@ -599,25 +624,7 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
// Delete temporary object in the event of failure. // Delete temporary object in the event of failure.
// If PutObject succeeded there would be no temporary // If PutObject succeeded there would be no temporary
// object to delete. // object to delete.
defer er.deleteObject(ctx, minioMetaTmpBucket, tempObj, writeQuorum) defer er.deleteObject(context.Background(), minioMetaTmpBucket, tempObj, writeQuorum)
// This is a special case with size as '0' and object ends with
// a slash separator, we treat it like a valid operation and
// return success.
if isObjectDir(object, data.Size()) {
// Check if an object is present as one of the parent dir.
// -- FIXME. (needs a new kind of lock).
// -- FIXME (this also causes performance issue when disks are down).
if er.parentDirIsObject(ctx, bucket, path.Dir(object)) {
return ObjectInfo{}, toObjectErr(errFileParentIsFile, bucket, object)
}
if err = er.putObjectDir(ctx, bucket, object, writeQuorum); err != nil {
return ObjectInfo{}, toObjectErr(err, bucket, object)
}
return dirObjectInfo(bucket, object, data.Size(), opts.UserDefined), nil
}
// Validate input data size and it can never be less than zero. // Validate input data size and it can never be less than zero.
if data.Size() < -1 { if data.Size() < -1 {
@@ -695,10 +702,15 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
// Should return IncompleteBody{} error when reader has fewer bytes // Should return IncompleteBody{} error when reader has fewer bytes
// than specified in request header. // than specified in request header.
if n < data.Size() { if n < data.Size() {
logger.LogIf(ctx, IncompleteBody{}, logger.Application) return ObjectInfo{}, IncompleteBody{Bucket: bucket, Object: object}
return ObjectInfo{}, IncompleteBody{}
} }
lk := er.NewNSLock(ctx, bucket, object)
if err := lk.GetLock(globalOperationTimeout); err != nil {
return ObjectInfo{}, err
}
defer lk.Unlock()
for i, w := range writers { for i, w := range writers {
if w == nil { if w == nil {
onlineDisks[i] = nil onlineDisks[i] = nil
@@ -776,7 +788,7 @@ func (er erasureObjects) deleteObjectVersion(ctx context.Context, bucket, object
if disks[index] == nil { if disks[index] == nil {
return errDiskNotFound return errDiskNotFound
} }
err := disks[index].DeleteVersion(bucket, object, fi) err := disks[index].DeleteVersion(ctx, bucket, object, fi)
if err != nil && err != errVolumeNotFound { if err != nil && err != errVolumeNotFound {
return err return err
} }
@@ -819,7 +831,9 @@ func (er erasureObjects) deleteObject(ctx context.Context, bucket, object string
if disks[index] == nil { if disks[index] == nil {
return errDiskNotFound return errDiskNotFound
} }
err := cleanupDir(ctx, disks[index], minioMetaTmpBucket, tmpObj) tctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
err := cleanupDir(tctx, disks[index], minioMetaTmpBucket, tmpObj)
if err != nil && err != errVolumeNotFound { if err != nil && err != errVolumeNotFound {
return err return err
} }
@@ -899,7 +913,7 @@ func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objec
wg.Add(1) wg.Add(1)
go func(index int, disk StorageAPI) { go func(index int, disk StorageAPI) {
defer wg.Done() defer wg.Done()
delObjErrs[index] = disk.DeleteVersions(bucket, versions) delObjErrs[index] = disk.DeleteVersions(ctx, bucket, versions)
}(index, disk) }(index, disk)
} }
@@ -968,6 +982,13 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
return objInfo, err return objInfo, err
} }
// Acquire a write lock before deleting the object.
lk := er.NewNSLock(ctx, bucket, object)
if err = lk.GetLock(globalOperationTimeout); err != nil {
return ObjectInfo{}, err
}
defer lk.Unlock()
storageDisks := er.getDisks() storageDisks := er.getDisks()
writeQuorum := len(storageDisks)/2 + 1 writeQuorum := len(storageDisks)/2 + 1
+5
View File
@@ -42,6 +42,7 @@ func TestRepeatPutObjectPart(t *testing.T) {
} }
// cleaning up of temporary test directories // cleaning up of temporary test directories
defer objLayer.Shutdown(context.Background())
defer removeRoots(disks) defer removeRoots(disks)
err = objLayer.MakeBucketWithLocation(ctx, "bucket1", BucketOptions{}) err = objLayer.MakeBucketWithLocation(ctx, "bucket1", BucketOptions{})
@@ -90,6 +91,7 @@ func TestErasureDeleteObjectBasic(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer xl.Shutdown(context.Background())
err = xl.MakeBucketWithLocation(ctx, "bucket", BucketOptions{}) err = xl.MakeBucketWithLocation(ctx, "bucket", BucketOptions{})
if err != nil { if err != nil {
@@ -200,6 +202,7 @@ func TestErasureDeleteObjectDiskNotFound(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// Cleanup backend directories // Cleanup backend directories
defer obj.Shutdown(context.Background())
defer removeRoots(fsDirs) defer removeRoots(fsDirs)
z := obj.(*erasureZones) z := obj.(*erasureZones)
@@ -269,6 +272,7 @@ func TestGetObjectNoQuorum(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// Cleanup backend directories. // Cleanup backend directories.
defer obj.Shutdown(context.Background())
defer removeRoots(fsDirs) defer removeRoots(fsDirs)
z := obj.(*erasureZones) z := obj.(*erasureZones)
@@ -331,6 +335,7 @@ func TestPutObjectNoQuorum(t *testing.T) {
} }
// Cleanup backend directories. // Cleanup backend directories.
defer obj.Shutdown(context.Background())
defer removeRoots(fsDirs) defer removeRoots(fsDirs)
z := obj.(*erasureZones) z := obj.(*erasureZones)
+114 -173
View File
@@ -66,6 +66,9 @@ type erasureSets struct {
// Distributed locker clients. // Distributed locker clients.
erasureLockers setsDsyncLockers erasureLockers setsDsyncLockers
// Distributed lock owner (constant per running instance).
erasureLockOwner string
// List of endpoints provided on the command line. // List of endpoints provided on the command line.
endpoints Endpoints endpoints Endpoints
@@ -80,9 +83,6 @@ type erasureSets struct {
disksConnectEvent chan diskConnectInfo disksConnectEvent chan diskConnectInfo
// Done channel to control monitoring loop.
disksConnectDoneCh chan struct{}
// Distribution algorithm of choice. // Distribution algorithm of choice.
distributionAlgo string distributionAlgo string
deploymentID [16]byte deploymentID [16]byte
@@ -115,7 +115,7 @@ func (s *erasureSets) getDiskMap() map[string]StorageAPI {
for i := 0; i < s.setCount; i++ { for i := 0; i < s.setCount; i++ {
for j := 0; j < s.setDriveCount; j++ { for j := 0; j < s.setDriveCount; j++ {
disk := s.erasureDisks[i][j] disk := s.erasureDisks[i][j]
if disk == nil { if disk == OfflineDisk {
continue continue
} }
if !disk.IsOnline() { if !disk.IsOnline() {
@@ -137,13 +137,10 @@ func connectEndpoint(endpoint Endpoint) (StorageAPI, *formatErasureV3, error) {
format, err := loadFormatErasure(disk) format, err := loadFormatErasure(disk)
if err != nil { if err != nil {
// Close the internal connection to avoid connection leaks.
disk.Close()
if errors.Is(err, errUnformattedDisk) { if errors.Is(err, errUnformattedDisk) {
info, derr := disk.DiskInfo() info, derr := disk.DiskInfo(context.TODO())
if derr != nil && info.RootDisk { if derr != nil && info.RootDisk {
return nil, nil, fmt.Errorf("Disk: %s returned %w but its a root disk refusing to use it", return nil, nil, fmt.Errorf("Disk: %s returned %w", disk, derr) // make sure to '%w' to wrap the error
disk, derr) // make sure to '%w' to wrap the error
} }
} }
return nil, nil, fmt.Errorf("Disk: %s returned %w", disk, err) // make sure to '%w' to wrap the error return nil, nil, fmt.Errorf("Disk: %s returned %w", disk, err) // make sure to '%w' to wrap the error
@@ -213,14 +210,24 @@ func (s *erasureSets) connectDisks() {
defer wg.Done() defer wg.Done()
disk, format, err := connectEndpoint(endpoint) disk, format, err := connectEndpoint(endpoint)
if err != nil { if err != nil {
printEndpointError(endpoint, err, true) if endpoint.IsLocal && errors.Is(err, errUnformattedDisk) {
globalBackgroundHealState.pushHealLocalDisks(endpoint)
logger.Info(fmt.Sprintf("Found unformatted drive %s, attempting to heal...", endpoint))
} else {
printEndpointError(endpoint, err, true)
}
return return
} }
s.erasureDisksMu.RLock()
setIndex, diskIndex, err := findDiskIndex(s.format, format) setIndex, diskIndex, err := findDiskIndex(s.format, format)
s.erasureDisksMu.RUnlock()
if err != nil { if err != nil {
// Close the internal connection to avoid connection leaks. if endpoint.IsLocal {
disk.Close() globalBackgroundHealState.pushHealLocalDisks(endpoint)
printEndpointError(endpoint, err, false) logger.Info(fmt.Sprintf("Found inconsistent drive %s with format.json, attempting to heal...", endpoint))
} else {
printEndpointError(endpoint, err, false)
}
return return
} }
disk.SetDiskID(format.Erasure.This) disk.SetDiskID(format.Erasure.This)
@@ -251,19 +258,17 @@ func (s *erasureSets) monitorAndConnectEndpoints(ctx context.Context, monitorInt
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case <-s.disksConnectDoneCh:
return
case <-time.After(monitorInterval): case <-time.After(monitorInterval):
s.connectDisks() s.connectDisks()
} }
} }
} }
func (s *erasureSets) GetLockers(setIndex int) func() []dsync.NetLocker { func (s *erasureSets) GetLockers(setIndex int) func() ([]dsync.NetLocker, string) {
return func() []dsync.NetLocker { return func() ([]dsync.NetLocker, string) {
lockers := make([]dsync.NetLocker, s.setDriveCount) lockers := make([]dsync.NetLocker, s.setDriveCount)
copy(lockers, s.erasureLockers[setIndex]) copy(lockers, s.erasureLockers[setIndex])
return lockers return lockers, s.erasureLockOwner
} }
} }
@@ -291,7 +296,9 @@ func (s *erasureSets) GetDisks(setIndex int) func() []StorageAPI {
} }
} }
const defaultMonitorConnectEndpointInterval = time.Second * 10 // Set to 10 secs. // defaultMonitorConnectEndpointInterval is the interval to monitor endpoint connections.
// Must be bigger than defaultMonitorNewDiskInterval.
const defaultMonitorConnectEndpointInterval = defaultMonitorNewDiskInterval + time.Second*5
// Initialize new set of erasure coded sets. // Initialize new set of erasure coded sets.
func newErasureSets(ctx context.Context, endpoints Endpoints, storageDisks []StorageAPI, format *formatErasureV3) (*erasureSets, error) { func newErasureSets(ctx context.Context, endpoints Endpoints, storageDisks []StorageAPI, format *formatErasureV3) (*erasureSets, error) {
@@ -304,6 +311,7 @@ func newErasureSets(ctx context.Context, endpoints Endpoints, storageDisks []Sto
sets: make([]*erasureObjects, setCount), sets: make([]*erasureObjects, setCount),
erasureDisks: make([][]StorageAPI, setCount), erasureDisks: make([][]StorageAPI, setCount),
erasureLockers: make([][]dsync.NetLocker, setCount), erasureLockers: make([][]dsync.NetLocker, setCount),
erasureLockOwner: mustGetUUID(),
endpoints: endpoints, endpoints: endpoints,
endpointStrings: endpointStrings, endpointStrings: endpointStrings,
setCount: setCount, setCount: setCount,
@@ -311,7 +319,6 @@ func newErasureSets(ctx context.Context, endpoints Endpoints, storageDisks []Sto
listTolerancePerSet: setDriveCount / 2, listTolerancePerSet: setDriveCount / 2,
format: format, format: format,
disksConnectEvent: make(chan diskConnectInfo), disksConnectEvent: make(chan diskConnectInfo),
disksConnectDoneCh: make(chan struct{}),
distributionAlgo: format.Erasure.DistributionAlgo, distributionAlgo: format.Erasure.DistributionAlgo,
deploymentID: uuid.MustParse(format.ID), deploymentID: uuid.MustParse(format.ID),
pool: NewMergeWalkPool(globalMergeLookupTimeout), pool: NewMergeWalkPool(globalMergeLookupTimeout),
@@ -342,12 +349,10 @@ func newErasureSets(ctx context.Context, endpoints Endpoints, storageDisks []Sto
} }
diskID, derr := disk.GetDiskID() diskID, derr := disk.GetDiskID()
if derr != nil { if derr != nil {
disk.Close()
continue continue
} }
m, n, err := findDiskIndexByDiskID(format, diskID) m, n, err := findDiskIndexByDiskID(format, diskID)
if err != nil { if err != nil {
disk.Close()
continue continue
} }
s.endpointStrings[m*setDriveCount+n] = disk.String() s.endpointStrings[m*setDriveCount+n] = disk.String()
@@ -364,8 +369,8 @@ func newErasureSets(ctx context.Context, endpoints Endpoints, storageDisks []Sto
mrfOpCh: make(chan partialOperation, 10000), mrfOpCh: make(chan partialOperation, 10000),
} }
go s.sets[i].cleanupStaleMultipartUploads(ctx, go s.sets[i].cleanupStaleUploads(ctx,
GlobalMultipartCleanupInterval, GlobalMultipartExpiry) GlobalStaleUploadsCleanupInterval, GlobalStaleUploadsExpiry)
} }
// Start the disk monitoring and connect routine. // Start the disk monitoring and connect routine.
@@ -483,11 +488,6 @@ func (s *erasureSets) StorageInfo(ctx context.Context, local bool) (StorageInfo,
return storageInfo, errs return storageInfo, errs
} }
func (s *erasureSets) CrawlAndGetDataUsage(ctx context.Context, bf *bloomFilter, updates chan<- DataUsageInfo) error {
// Use the zone-level implementation instead.
return NotImplemented{API: "CrawlAndGetDataUsage"}
}
// Shutdown shutsdown all erasure coded sets in parallel // Shutdown shutsdown all erasure coded sets in parallel
// returns error upon first error. // returns error upon first error.
func (s *erasureSets) Shutdown(ctx context.Context) error { func (s *erasureSets) Shutdown(ctx context.Context) error {
@@ -505,7 +505,14 @@ func (s *erasureSets) Shutdown(ctx context.Context) error {
return err return err
} }
} }
select {
case _, ok := <-s.disksConnectEvent:
if ok {
close(s.disksConnectEvent)
}
default:
close(s.disksConnectEvent)
}
return nil return nil
} }
@@ -584,11 +591,6 @@ func (s *erasureSets) GetBucketInfo(ctx context.Context, bucket string) (bucketI
return s.getHashedSet("").GetBucketInfo(ctx, bucket) return s.getHashedSet("").GetBucketInfo(ctx, bucket)
} }
// ListObjectsV2 lists all objects in bucket filtered by prefix
func (s *erasureSets) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (result ListObjectsV2Info, err error) {
return result, NotImplemented{}
}
// IsNotificationSupported returns whether bucket notification is applicable for this layer. // IsNotificationSupported returns whether bucket notification is applicable for this layer.
func (s *erasureSets) IsNotificationSupported() bool { func (s *erasureSets) IsNotificationSupported() bool {
return s.getHashedSet("").IsNotificationSupported() return s.getHashedSet("").IsNotificationSupported()
@@ -667,7 +669,7 @@ func undoDeleteBucketSets(ctx context.Context, bucket string, sets []*erasureObj
// that all buckets are present on all sets. // that all buckets are present on all sets.
func (s *erasureSets) ListBuckets(ctx context.Context) (buckets []BucketInfo, err error) { func (s *erasureSets) ListBuckets(ctx context.Context) (buckets []BucketInfo, err error) {
// Always lists from the same set signified by the empty string. // Always lists from the same set signified by the empty string.
return s.getHashedSet("").ListBuckets(ctx) return s.ListBucketsHeal(ctx)
} }
// --- Object Operations --- // --- Object Operations ---
@@ -760,9 +762,13 @@ func (s *erasureSets) CopyObject(ctx context.Context, srcBucket, srcObject, dstB
// Check if this request is only metadata update. // Check if this request is only metadata update.
if cpSrcDstSame && srcInfo.metadataOnly { if cpSrcDstSame && srcInfo.metadataOnly {
// Version ID is set for the destination and source == destination version ID.
// perform an in-place update.
if dstOpts.VersionID != "" && srcOpts.VersionID == dstOpts.VersionID { if dstOpts.VersionID != "" && srcOpts.VersionID == dstOpts.VersionID {
return srcSet.CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts) return srcSet.CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts)
} }
// Destination is not versioned and source version ID is empty
// perform an in-place update.
if !dstOpts.Versioned && srcOpts.VersionID == "" { if !dstOpts.Versioned && srcOpts.VersionID == "" {
return srcSet.CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts) return srcSet.CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts)
} }
@@ -961,7 +967,7 @@ func lexicallySortedEntryVersions(entryChs []FileInfoVersionsCh, entries []FileI
} }
func (s *erasureSets) startMergeWalks(ctx context.Context, bucket, prefix, marker string, recursive bool, endWalkCh <-chan struct{}) []FileInfoCh { func (s *erasureSets) startMergeWalks(ctx context.Context, bucket, prefix, marker string, recursive bool, endWalkCh <-chan struct{}) []FileInfoCh {
return s.startMergeWalksN(ctx, bucket, prefix, marker, recursive, endWalkCh, -1) return s.startMergeWalksN(ctx, bucket, prefix, marker, recursive, endWalkCh, -1, false)
} }
func (s *erasureSets) startMergeWalksVersions(ctx context.Context, bucket, prefix, marker string, recursive bool, endWalkCh <-chan struct{}) []FileInfoVersionsCh { func (s *erasureSets) startMergeWalksVersions(ctx context.Context, bucket, prefix, marker string, recursive bool, endWalkCh <-chan struct{}) []FileInfoVersionsCh {
@@ -972,109 +978,67 @@ func (s *erasureSets) startMergeWalksVersions(ctx context.Context, bucket, prefi
// FileInfoCh which can be read from. // 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 { func (s *erasureSets) startMergeWalksVersionsN(ctx context.Context, bucket, prefix, marker string, recursive bool, endWalkCh <-chan struct{}, ndisks int) []FileInfoVersionsCh {
var entryChs []FileInfoVersionsCh var entryChs []FileInfoVersionsCh
var success int var wg sync.WaitGroup
var mutex sync.Mutex
for _, set := range s.sets { for _, set := range s.sets {
// Reset for the next erasure set. // Reset for the next erasure set.
success = ndisks for _, disk := range set.getLoadBalancedNDisks(ndisks) {
for _, disk := range set.getLoadBalancedDisks() { wg.Add(1)
if disk == nil { go func(disk StorageAPI) {
// Disk can be offline defer wg.Done()
continue entryCh, err := disk.WalkVersions(GlobalContext, bucket, prefix, marker, recursive, endWalkCh)
} if err != nil {
entryCh, err := disk.WalkVersions(bucket, prefix, marker, recursive, endWalkCh) return
if err != nil { }
logger.LogIf(ctx, err)
// Disk walk returned error, ignore it. mutex.Lock()
continue entryChs = append(entryChs, FileInfoVersionsCh{
} Ch: entryCh,
entryChs = append(entryChs, FileInfoVersionsCh{ })
Ch: entryCh, mutex.Unlock()
}) }(disk)
success--
if success == 0 {
break
}
} }
} }
wg.Wait()
return entryChs return entryChs
} }
// Starts a walk channel across n number of disks and returns a slice of // Starts a walk channel across n number of disks and returns a slice of
// FileInfoCh which can be read from. // FileInfoCh which can be read from.
func (s *erasureSets) startMergeWalksN(ctx context.Context, bucket, prefix, marker string, recursive bool, endWalkCh <-chan struct{}, ndisks int) []FileInfoCh { func (s *erasureSets) startMergeWalksN(ctx context.Context, bucket, prefix, marker string, recursive bool, endWalkCh <-chan struct{}, ndisks int, splunk bool) []FileInfoCh {
var entryChs []FileInfoCh var entryChs []FileInfoCh
var success int var wg sync.WaitGroup
var mutex sync.Mutex
for _, set := range s.sets { for _, set := range s.sets {
// Reset for the next erasure set. // Reset for the next erasure set.
success = ndisks for _, disk := range set.getLoadBalancedNDisks(ndisks) {
for _, disk := range set.getLoadBalancedDisks() { wg.Add(1)
if disk == nil { go func(disk StorageAPI) {
// Disk can be offline defer wg.Done()
continue
} var entryCh chan FileInfo
entryCh, err := disk.Walk(bucket, prefix, marker, recursive, endWalkCh) var err error
if err != nil { if splunk {
// Disk walk returned error, ignore it. entryCh, err = disk.WalkSplunk(GlobalContext, bucket, prefix, marker, endWalkCh)
continue } else {
} entryCh, err = disk.Walk(GlobalContext, bucket, prefix, marker, recursive, endWalkCh)
entryChs = append(entryChs, FileInfoCh{ }
Ch: entryCh, if err != nil {
}) // Disk walk returned error, ignore it.
success-- return
if success == 0 { }
break mutex.Lock()
} entryChs = append(entryChs, FileInfoCh{
Ch: entryCh,
})
mutex.Unlock()
}(disk)
} }
} }
wg.Wait()
return entryChs return entryChs
} }
// Starts a walk channel across n number of disks and returns a slice of
// FileInfo channels which can be read from.
func (s *erasureSets) startSplunkMergeWalksN(ctx context.Context, bucket, prefix, marker string, endWalkCh <-chan struct{}, ndisks int) []FileInfoCh {
var entryChs []FileInfoCh
var success int
for _, set := range s.sets {
// Reset for the next erasure set.
success = ndisks
for _, disk := range set.getLoadBalancedDisks() {
if disk == nil {
// Disk can be offline
continue
}
entryCh, err := disk.WalkSplunk(bucket, prefix, marker, endWalkCh)
if err != nil {
// Disk walk returned error, ignore it.
continue
}
entryChs = append(entryChs, FileInfoCh{
Ch: entryCh,
})
success--
if success == 0 {
break
}
}
}
return entryChs
}
// ListObjectVersions - implements listing of objects across disks, each disk is indepenently
// walked and merged at this layer. Resulting value through the merge process sends
// the data in lexically sorted order.
func (s *erasureSets) ListObjectVersions(ctx context.Context, bucket, prefix, marker, versionIDMarker, delimiter string, maxKeys int) (loi ListObjectVersionsInfo, err error) {
// Shouldn't be called directly, caller Zones already has an implementation
return loi, NotImplemented{}
}
// ListObjects - implements listing of objects across disks, each disk is indepenently
// walked and merged at this layer. Resulting value through the merge process sends
// the data in lexically sorted order.
func (s *erasureSets) ListObjects(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (loi ListObjectsInfo, err error) {
// Shouldn't be called directly, caller Zones already has an implementation
return loi, NotImplemented{}
}
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.
@@ -1110,8 +1074,8 @@ func (s *erasureSets) ListObjectParts(ctx context.Context, bucket, object, uploa
} }
// 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) error { func (s *erasureSets) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) error {
return s.getHashedSet(object).AbortMultipartUpload(ctx, bucket, object, uploadID) return s.getHashedSet(object).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.
@@ -1227,16 +1191,12 @@ func (s *erasureSets) ReloadFormat(ctx context.Context, dryRun bool) (err error)
return err return err
} }
// kill the monitoring loop such that we stop writing s.erasureDisksMu.Lock()
// to indicate that we will re-initialize everything
// with new format.
s.disksConnectDoneCh <- struct{}{}
// Replace with new reference format. // Replace with new reference format.
s.format = refFormat s.format = refFormat
// Close all existing disks and reconnect all the disks. // Close all existing disks and reconnect all the disks.
s.erasureDisksMu.Lock()
for _, disk := range storageDisks { for _, disk := range storageDisks {
if disk == nil { if disk == nil {
continue continue
@@ -1244,13 +1204,11 @@ func (s *erasureSets) ReloadFormat(ctx context.Context, dryRun bool) (err error)
diskID, err := disk.GetDiskID() diskID, err := disk.GetDiskID()
if err != nil { if err != nil {
disk.Close()
continue continue
} }
m, n, err := findDiskIndexByDiskID(refFormat, diskID) m, n, err := findDiskIndexByDiskID(refFormat, diskID)
if err != nil { if err != nil {
disk.Close()
continue continue
} }
@@ -1261,10 +1219,8 @@ func (s *erasureSets) ReloadFormat(ctx context.Context, dryRun bool) (err error)
s.endpointStrings[m*s.setDriveCount+n] = disk.String() s.endpointStrings[m*s.setDriveCount+n] = disk.String()
s.erasureDisks[m][n] = disk s.erasureDisks[m][n] = disk
} }
s.erasureDisksMu.Unlock()
// Restart monitoring loop to monitor reformatted disks again. s.erasureDisksMu.Unlock()
go s.monitorAndConnectEndpoints(GlobalContext, defaultMonitorConnectEndpointInterval)
return nil return nil
} }
@@ -1274,17 +1230,14 @@ func (s *erasureSets) ReloadFormat(ctx context.Context, dryRun bool) (err error)
func isTestSetup(infos []DiskInfo, errs []error) bool { func isTestSetup(infos []DiskInfo, errs []error) bool {
rootDiskCount := 0 rootDiskCount := 0
for i := range errs { for i := range errs {
if errs[i] != nil && errs[i] != errUnformattedDisk { if errs[i] == nil || errs[i] == errUnformattedDisk {
// On any error which is not unformatted disk if infos[i].RootDisk {
// it is safer to reject healing. rootDiskCount++
return false }
}
if infos[i].RootDisk {
rootDiskCount++
} }
} }
// It is a test setup if all disks are root disks. // It is a test setup if all disks are root disks in quorum.
return rootDiskCount == len(infos) return rootDiskCount >= len(infos)/2+1
} }
func getHealDiskInfos(storageDisks []StorageAPI, errs []error) ([]DiskInfo, []error) { func getHealDiskInfos(storageDisks []StorageAPI, errs []error) ([]DiskInfo, []error) {
@@ -1300,7 +1253,7 @@ func getHealDiskInfos(storageDisks []StorageAPI, errs []error) ([]DiskInfo, []er
return errDiskNotFound return errDiskNotFound
} }
var err error var err error
infos[index], err = storageDisks[index].DiskInfo() infos[index], err = storageDisks[index].DiskInfo(context.TODO())
return err return err
}, index) }, index)
} }
@@ -1347,6 +1300,19 @@ func (s *erasureSets) HealFormat(ctx context.Context, dryRun bool) (res madmin.H
// Mark all root disks down // Mark all root disks down
markRootDisksAsDown(storageDisks, sErrs) markRootDisksAsDown(storageDisks, sErrs)
refFormat, err := getFormatErasureInQuorum(formats)
if err != nil {
return res, err
}
for i, format := range formats {
if format != nil {
if ferr := formatErasureV3Check(refFormat, format); ferr != nil {
sErrs[i] = errUnformattedDisk
}
}
}
// Prepare heal-result // Prepare heal-result
res = madmin.HealResultItem{ res = madmin.HealResultItem{
Type: madmin.HealItemMetadata, Type: madmin.HealItemMetadata,
@@ -1372,11 +1338,6 @@ func (s *erasureSets) HealFormat(ctx context.Context, dryRun bool) (res madmin.H
return res, errNoHealRequired return res, errNoHealRequired
} }
refFormat, err := getFormatErasureInQuorum(formats)
if err != nil {
return res, err
}
// Mark all UUIDs which might be offline, use list // Mark all UUIDs which might be offline, use list
// of formats to mark them appropriately. // of formats to mark them appropriately.
markUUIDsOffline(refFormat, formats) markUUIDsOffline(refFormat, formats)
@@ -1433,16 +1394,12 @@ func (s *erasureSets) HealFormat(ctx context.Context, dryRun bool) (res madmin.H
return madmin.HealResultItem{}, err return madmin.HealResultItem{}, err
} }
// kill the monitoring loop such that we stop writing s.erasureDisksMu.Lock()
// to indicate that we will re-initialize everything
// with new format.
s.disksConnectDoneCh <- struct{}{}
// Replace with new reference format. // Replace with new reference format.
s.format = refFormat s.format = refFormat
// Disconnect/relinquish all existing disks, lockers and reconnect the disks, lockers. // Disconnect/relinquish all existing disks, lockers and reconnect the disks, lockers.
s.erasureDisksMu.Lock()
for _, disk := range storageDisks { for _, disk := range storageDisks {
if disk == nil { if disk == nil {
continue continue
@@ -1450,13 +1407,11 @@ func (s *erasureSets) HealFormat(ctx context.Context, dryRun bool) (res madmin.H
diskID, err := disk.GetDiskID() diskID, err := disk.GetDiskID()
if err != nil { if err != nil {
disk.Close()
continue continue
} }
m, n, err := findDiskIndexByDiskID(refFormat, diskID) m, n, err := findDiskIndexByDiskID(refFormat, diskID)
if err != nil { if err != nil {
disk.Close()
continue continue
} }
@@ -1467,10 +1422,8 @@ func (s *erasureSets) HealFormat(ctx context.Context, dryRun bool) (res madmin.H
s.endpointStrings[m*s.setDriveCount+n] = disk.String() s.endpointStrings[m*s.setDriveCount+n] = disk.String()
s.erasureDisks[m][n] = disk s.erasureDisks[m][n] = disk
} }
s.erasureDisksMu.Unlock()
// Restart our monitoring loop to start monitoring newly formatted disks. s.erasureDisksMu.Unlock()
go s.monitorAndConnectEndpoints(GlobalContext, defaultMonitorConnectEndpointInterval)
} }
return res, nil return res, nil
@@ -1516,7 +1469,7 @@ func (s *erasureSets) ListBucketsHeal(ctx context.Context) ([]BucketInfo, error)
var healBuckets = map[string]VolInfo{} var healBuckets = map[string]VolInfo{}
for _, set := range s.sets { for _, set := range s.sets {
// lists all unique buckets across drives. // lists all unique buckets across drives.
if err := listAllBuckets(set.getDisks(), healBuckets); err != nil { if err := listAllBuckets(ctx, set.getDisks(), healBuckets); err != nil {
return nil, err return nil, err
} }
} }
@@ -1641,18 +1594,6 @@ func (s *erasureSets) GetObjectTags(ctx context.Context, bucket, object string,
return s.getHashedSet(object).GetObjectTags(ctx, bucket, object, opts) return s.getHashedSet(object).GetObjectTags(ctx, bucket, object, opts)
} }
// GetMetrics - no op
func (s *erasureSets) GetMetrics(ctx context.Context) (*Metrics, error) {
logger.LogIf(ctx, NotImplemented{})
return &Metrics{}, NotImplemented{}
}
// Health shouldn't be called directly - will panic
func (s *erasureSets) Health(ctx context.Context, _ HealthOptions) HealthResult {
logger.CriticalIf(ctx, NotImplemented{})
return HealthResult{}
}
// maintainMRFList gathers the list of successful partial uploads // maintainMRFList gathers the list of successful partial uploads
// from all underlying er.sets and puts them in a global map which // from all underlying er.sets and puts them in a global map which
// should not have more than 10000 entries. // should not have more than 10000 entries.
+1
View File
@@ -206,6 +206,7 @@ func TestHashedLayer(t *testing.T) {
if err != nil { if err != nil {
t.Fatal("Unable to initialize 'Erasure' object layer.", err) t.Fatal("Unable to initialize 'Erasure' object layer.", err)
} }
defer obj.Shutdown(ctx)
// Remove all dirs. // Remove all dirs.
for _, dir := range fsDirs { for _, dir := range fsDirs {
+168 -199
View File
@@ -18,6 +18,7 @@ package cmd
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"math/rand" "math/rand"
@@ -40,6 +41,9 @@ type erasureZones struct {
GatewayUnsupported GatewayUnsupported
zones []*erasureSets zones []*erasureSets
// Shut down async operations
shutdown context.CancelFunc
} }
func (z *erasureZones) SingleZone() bool { func (z *erasureZones) SingleZone() bool {
@@ -79,7 +83,7 @@ func newErasureZones(ctx context.Context, endpointZones EndpointZones) (ObjectLa
return nil, err return nil, err
} }
} }
ctx, z.shutdown = context.WithCancel(ctx)
go intDataUpdateTracker.start(ctx, localDrives...) go intDataUpdateTracker.start(ctx, localDrives...)
return z, nil return z, nil
} }
@@ -218,6 +222,7 @@ func (z *erasureZones) getZoneIdx(ctx context.Context, bucket, object string, op
} }
func (z *erasureZones) Shutdown(ctx context.Context) error { func (z *erasureZones) Shutdown(ctx context.Context) error {
defer z.shutdown()
if z.SingleZone() { if z.SingleZone() {
return z.zones[0].Shutdown(ctx) return z.zones[0].Shutdown(ctx)
} }
@@ -237,7 +242,6 @@ func (z *erasureZones) Shutdown(ctx context.Context) error {
} }
// let's the rest shutdown // let's the rest shutdown
} }
return nil return nil
} }
@@ -294,19 +298,19 @@ func (z *erasureZones) CrawlAndGetDataUsage(ctx context.Context, bf *bloomFilter
// Collect for each set in zones. // Collect for each set in zones.
for _, z := range z.zones { for _, z := range z.zones {
buckets, err := z.ListBuckets(ctx)
if err != nil {
return err
}
// Add new buckets.
for _, b := range buckets {
if _, ok := knownBuckets[b.Name]; ok {
continue
}
allBuckets = append(allBuckets, b)
knownBuckets[b.Name] = struct{}{}
}
for _, erObj := range z.sets { for _, erObj := range z.sets {
// Add new buckets.
buckets, err := erObj.ListBuckets(ctx)
if err != nil {
return err
}
for _, b := range buckets {
if _, ok := knownBuckets[b.Name]; ok {
continue
}
allBuckets = append(allBuckets, b)
knownBuckets[b.Name] = struct{}{}
}
wg.Add(1) wg.Add(1)
results = append(results, dataUsageCache{}) results = append(results, dataUsageCache{})
go func(i int, erObj *erasureObjects) { go func(i int, erObj *erasureObjects) {
@@ -456,24 +460,7 @@ func (z *erasureZones) MakeBucketWithLocation(ctx context.Context, bucket string
} }
func (z *erasureZones) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) { func (z *erasureZones) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) {
var nsUnlocker = func() {} object = encodeDirObject(object)
// Acquire lock
if lockType != noLock {
lock := z.NewNSLock(ctx, bucket, object)
switch lockType {
case writeLock:
if err = lock.GetLock(globalOperationTimeout); err != nil {
return nil, err
}
nsUnlocker = lock.Unlock
case readLock:
if err = lock.GetRLock(globalOperationTimeout); err != nil {
return nil, err
}
nsUnlocker = lock.RUnlock
}
}
for _, zone := range z.zones { for _, zone := range z.zones {
gr, err = zone.GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts) gr, err = zone.GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts)
@@ -481,13 +468,10 @@ func (z *erasureZones) GetObjectNInfo(ctx context.Context, bucket, object string
if isErrObjectNotFound(err) || isErrVersionNotFound(err) { if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
continue continue
} }
nsUnlocker()
return gr, err return gr, err
} }
gr.cleanUpFns = append(gr.cleanUpFns, nsUnlocker)
return gr, nil return gr, nil
} }
nsUnlocker()
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}
} }
@@ -495,16 +479,7 @@ func (z *erasureZones) GetObjectNInfo(ctx context.Context, bucket, object string
} }
func (z *erasureZones) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) error { func (z *erasureZones) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) error {
// Lock the object before reading. object = encodeDirObject(object)
lk := z.NewNSLock(ctx, bucket, object)
if err := lk.GetRLock(globalOperationTimeout); err != nil {
return err
}
defer lk.RUnlock()
if z.SingleZone() {
return z.zones[0].GetObject(ctx, bucket, object, startOffset, length, writer, etag, opts)
}
for _, zone := range z.zones { for _, zone := range z.zones {
if err := zone.GetObject(ctx, bucket, object, startOffset, length, writer, etag, opts); err != nil { if err := zone.GetObject(ctx, bucket, object, startOffset, length, writer, etag, opts); err != nil {
@@ -515,20 +490,14 @@ func (z *erasureZones) GetObject(ctx context.Context, bucket, object string, sta
} }
return nil return nil
} }
if opts.VersionID != "" {
return VersionNotFound{Bucket: bucket, Object: object, VersionID: opts.VersionID}
}
return ObjectNotFound{Bucket: bucket, Object: object} return ObjectNotFound{Bucket: bucket, Object: object}
} }
func (z *erasureZones) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) { func (z *erasureZones) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
// Lock the object before reading. object = encodeDirObject(object)
lk := z.NewNSLock(ctx, bucket, object)
if err := lk.GetRLock(globalOperationTimeout); err != nil {
return ObjectInfo{}, err
}
defer lk.RUnlock()
if z.SingleZone() {
return z.zones[0].GetObjectInfo(ctx, bucket, object, opts)
}
for _, zone := range z.zones { for _, zone := range z.zones {
objInfo, err = zone.GetObjectInfo(ctx, bucket, object, opts) objInfo, err = zone.GetObjectInfo(ctx, bucket, object, opts)
if err != nil { if err != nil {
@@ -539,6 +508,7 @@ func (z *erasureZones) GetObjectInfo(ctx context.Context, bucket, object string,
} }
return objInfo, nil return objInfo, nil
} }
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}
} }
@@ -547,12 +517,7 @@ func (z *erasureZones) GetObjectInfo(ctx context.Context, bucket, object string,
// PutObject - writes an object to least used erasure zone. // PutObject - writes an object to least used erasure zone.
func (z *erasureZones) PutObject(ctx context.Context, bucket string, object string, data *PutObjReader, opts ObjectOptions) (ObjectInfo, error) { func (z *erasureZones) PutObject(ctx context.Context, bucket string, object string, data *PutObjReader, opts ObjectOptions) (ObjectInfo, error) {
// Lock the object. object = encodeDirObject(object)
lk := z.NewNSLock(ctx, bucket, object)
if err := lk.GetLock(globalOperationTimeout); err != nil {
return ObjectInfo{}, err
}
defer lk.Unlock()
if z.SingleZone() { if z.SingleZone() {
return z.zones[0].PutObject(ctx, bucket, object, data, opts) return z.zones[0].PutObject(ctx, bucket, object, data, opts)
@@ -568,12 +533,7 @@ func (z *erasureZones) PutObject(ctx context.Context, bucket string, object stri
} }
func (z *erasureZones) DeleteObject(ctx context.Context, bucket string, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) { func (z *erasureZones) DeleteObject(ctx context.Context, bucket string, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
// Acquire a write lock before deleting the object. object = encodeDirObject(object)
lk := z.NewNSLock(ctx, bucket, object)
if err = lk.GetLock(globalOperationTimeout); err != nil {
return ObjectInfo{}, err
}
defer lk.Unlock()
if z.SingleZone() { if z.SingleZone() {
return z.zones[0].DeleteObject(ctx, bucket, object, opts) return z.zones[0].DeleteObject(ctx, bucket, object, opts)
@@ -595,6 +555,8 @@ func (z *erasureZones) DeleteObjects(ctx context.Context, bucket string, objects
dobjects := make([]DeletedObject, len(objects)) dobjects := make([]DeletedObject, len(objects))
objSets := set.NewStringSet() objSets := set.NewStringSet()
for i := range derrs { for i := range derrs {
objects[i].ObjectName = encodeDirObject(objects[i].ObjectName)
derrs[i] = checkDelObjArgs(ctx, bucket, objects[i].ObjectName) derrs[i] = checkDelObjArgs(ctx, bucket, objects[i].ObjectName)
objSets.Add(objects[i].ObjectName) objSets.Add(objects[i].ObjectName)
} }
@@ -626,15 +588,10 @@ func (z *erasureZones) DeleteObjects(ctx context.Context, bucket string, objects
} }
func (z *erasureZones) CopyObject(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error) { func (z *erasureZones) CopyObject(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error) {
// Check if this request is only metadata update. srcObject = encodeDirObject(srcObject)
dstObject = encodeDirObject(dstObject)
cpSrcDstSame := isStringEqual(pathJoin(srcBucket, srcObject), pathJoin(dstBucket, dstObject)) cpSrcDstSame := isStringEqual(pathJoin(srcBucket, srcObject), pathJoin(dstBucket, dstObject))
if !cpSrcDstSame {
lk := z.NewNSLock(ctx, dstBucket, dstObject)
if err := lk.GetLock(globalOperationTimeout); err != nil {
return objInfo, err
}
defer lk.Unlock()
}
zoneIdx, err := z.getZoneIdx(ctx, dstBucket, dstObject, dstOpts, srcInfo.Size) zoneIdx, err := z.getZoneIdx(ctx, dstBucket, dstObject, dstOpts, srcInfo.Size)
if err != nil { if err != nil {
@@ -642,12 +599,19 @@ func (z *erasureZones) CopyObject(ctx context.Context, srcBucket, srcObject, dst
} }
if cpSrcDstSame && srcInfo.metadataOnly { if cpSrcDstSame && srcInfo.metadataOnly {
// Version ID is set for the destination and source == destination version ID.
if dstOpts.VersionID != "" && srcOpts.VersionID == dstOpts.VersionID { if dstOpts.VersionID != "" && srcOpts.VersionID == dstOpts.VersionID {
return z.zones[zoneIdx].CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts) return z.zones[zoneIdx].CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts)
} }
// Destination is not versioned and source version ID is empty
// perform an in-place update.
if !dstOpts.Versioned && srcOpts.VersionID == "" { if !dstOpts.Versioned && srcOpts.VersionID == "" {
return z.zones[zoneIdx].CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts) return z.zones[zoneIdx].CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts)
} }
// Destination is versioned, source is not destination version,
// as a special case look for if the source object is not legacy
// from older format, for older format we will rewrite them as
// newer using PutObject() - this is an optimization to save space
if dstOpts.Versioned && srcOpts.VersionID != dstOpts.VersionID && !srcInfo.Legacy { if dstOpts.Versioned && srcOpts.VersionID != dstOpts.VersionID && !srcInfo.Legacy {
// CopyObject optimization where we don't create an entire copy // CopyObject optimization where we don't create an entire copy
// of the content, instead we add a reference. // of the content, instead we add a reference.
@@ -689,15 +653,15 @@ func (z *erasureZones) ListObjectsV2(ctx context.Context, bucket, prefix, contin
func (z *erasureZones) listObjectsNonSlash(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (loi ListObjectsInfo, err error) { func (z *erasureZones) listObjectsNonSlash(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (loi ListObjectsInfo, err error) {
var zonesEntryChs [][]FileInfoCh zonesEntryChs := make([][]FileInfoCh, 0, len(z.zones))
var zonesListTolerancePerSet []int zonesListTolerancePerSet := make([]int, 0, len(z.zones))
endWalkCh := make(chan struct{}) endWalkCh := make(chan struct{})
defer close(endWalkCh) defer close(endWalkCh)
for _, zone := range z.zones { for _, zone := range z.zones {
zonesEntryChs = append(zonesEntryChs, zonesEntryChs = append(zonesEntryChs,
zone.startMergeWalksN(ctx, bucket, prefix, "", true, endWalkCh, zone.listTolerancePerSet)) zone.startMergeWalksN(ctx, bucket, prefix, "", true, endWalkCh, zone.setDriveCount, false))
zonesListTolerancePerSet = append(zonesListTolerancePerSet, zone.listTolerancePerSet) zonesListTolerancePerSet = append(zonesListTolerancePerSet, zone.listTolerancePerSet)
} }
@@ -705,8 +669,8 @@ func (z *erasureZones) listObjectsNonSlash(ctx context.Context, bucket, prefix,
var eof bool var eof bool
var prevPrefix string var prevPrefix string
var zonesEntriesInfos [][]FileInfo zonesEntriesInfos := make([][]FileInfo, 0, len(zonesEntryChs))
var zonesEntriesValid [][]bool zonesEntriesValid := make([][]bool, 0, len(zonesEntryChs))
for _, entryChs := range zonesEntryChs { for _, entryChs := range zonesEntryChs {
zonesEntriesInfos = append(zonesEntriesInfos, make([]FileInfo, len(entryChs))) zonesEntriesInfos = append(zonesEntriesInfos, make([]FileInfo, len(entryChs)))
zonesEntriesValid = append(zonesEntriesValid, make([]bool, len(entryChs))) zonesEntriesValid = append(zonesEntriesValid, make([]bool, len(entryChs)))
@@ -808,15 +772,15 @@ func (z *erasureZones) listObjectsSplunk(ctx context.Context, bucket, prefix, ma
recursive := true recursive := true
var zonesEntryChs [][]FileInfoCh zonesEntryChs := make([][]FileInfoCh, 0, len(z.zones))
var zonesEndWalkCh []chan struct{} zonesEndWalkCh := make([]chan struct{}, 0, len(z.zones))
var zonesListTolerancePerSet []int zonesListTolerancePerSet := make([]int, 0, len(z.zones))
for _, zone := range z.zones { for _, zone := range z.zones {
entryChs, endWalkCh := zone.poolSplunk.Release(listParams{bucket, recursive, marker, prefix}) entryChs, endWalkCh := zone.poolSplunk.Release(listParams{bucket, recursive, marker, prefix})
if entryChs == nil { if entryChs == nil {
endWalkCh = make(chan struct{}) endWalkCh = make(chan struct{})
entryChs = zone.startSplunkMergeWalksN(ctx, bucket, prefix, marker, endWalkCh, zone.listTolerancePerSet) entryChs = zone.startMergeWalksN(ctx, bucket, prefix, marker, recursive, endWalkCh, zone.setDriveCount, true)
} }
zonesEntryChs = append(zonesEntryChs, entryChs) zonesEntryChs = append(zonesEntryChs, entryChs)
zonesEndWalkCh = append(zonesEndWalkCh, endWalkCh) zonesEndWalkCh = append(zonesEndWalkCh, endWalkCh)
@@ -900,15 +864,15 @@ func (z *erasureZones) listObjects(ctx context.Context, bucket, prefix, marker,
recursive = false recursive = false
} }
var zonesEntryChs [][]FileInfoCh zonesEntryChs := make([][]FileInfoCh, 0, len(z.zones))
var zonesEndWalkCh []chan struct{} zonesEndWalkCh := make([]chan struct{}, 0, len(z.zones))
var zonesListTolerancePerSet []int zonesListTolerancePerSet := make([]int, 0, len(z.zones))
for _, zone := range z.zones { for _, zone := range z.zones {
entryChs, endWalkCh := zone.pool.Release(listParams{bucket, recursive, marker, prefix}) entryChs, endWalkCh := zone.pool.Release(listParams{bucket, recursive, marker, prefix})
if entryChs == nil { if entryChs == nil {
endWalkCh = make(chan struct{}) endWalkCh = make(chan struct{})
entryChs = zone.startMergeWalksN(ctx, bucket, prefix, marker, recursive, endWalkCh, zone.listTolerancePerSet) entryChs = zone.startMergeWalksN(ctx, bucket, prefix, marker, recursive, endWalkCh, zone.setDriveCount, false)
} }
zonesEntryChs = append(zonesEntryChs, entryChs) zonesEntryChs = append(zonesEntryChs, entryChs)
zonesEndWalkCh = append(zonesEndWalkCh, endWalkCh) zonesEndWalkCh = append(zonesEndWalkCh, endWalkCh)
@@ -986,7 +950,16 @@ func lexicallySortedEntryZone(zoneEntryChs [][]FileInfoCh, zoneEntries [][]FileI
zoneIndex = i zoneIndex = i
continue continue
} }
if zoneEntries[i][j].Name < lentry.Name { str1 := zoneEntries[i][j].Name
str2 := lentry.Name
if HasSuffix(str1, globalDirSuffix) {
str1 = strings.TrimSuffix(str1, globalDirSuffix) + slashSeparator
}
if HasSuffix(str2, globalDirSuffix) {
str2 = strings.TrimSuffix(str2, globalDirSuffix) + slashSeparator
}
if str1 < str2 {
lentry = zoneEntries[i][j] lentry = zoneEntries[i][j]
zoneIndex = i zoneIndex = i
} }
@@ -1019,6 +992,10 @@ func lexicallySortedEntryZone(zoneEntryChs [][]FileInfoCh, zoneEntries [][]FileI
} }
} }
if HasSuffix(lentry.Name, globalDirSuffix) {
lentry.Name = strings.TrimSuffix(lentry.Name, globalDirSuffix) + slashSeparator
}
return lentry, lexicallySortedEntryCount, zoneIndex, isTruncated return lentry, lexicallySortedEntryCount, zoneIndex, isTruncated
} }
@@ -1064,7 +1041,16 @@ func lexicallySortedEntryZoneVersions(zoneEntryChs [][]FileInfoVersionsCh, zoneE
zoneIndex = i zoneIndex = i
continue continue
} }
if zoneEntries[i][j].Name < lentry.Name { str1 := zoneEntries[i][j].Name
str2 := lentry.Name
if HasSuffix(str1, globalDirSuffix) {
str1 = strings.TrimSuffix(str1, globalDirSuffix) + slashSeparator
}
if HasSuffix(str2, globalDirSuffix) {
str2 = strings.TrimSuffix(str2, globalDirSuffix) + slashSeparator
}
if str1 < str2 {
lentry = zoneEntries[i][j] lentry = zoneEntries[i][j]
zoneIndex = i zoneIndex = i
} }
@@ -1097,18 +1083,23 @@ func lexicallySortedEntryZoneVersions(zoneEntryChs [][]FileInfoVersionsCh, zoneE
} }
} }
if HasSuffix(lentry.Name, globalDirSuffix) {
lentry.Name = strings.TrimSuffix(lentry.Name, globalDirSuffix) + slashSeparator
}
return lentry, lexicallySortedEntryCount, zoneIndex, isTruncated return lentry, lexicallySortedEntryCount, zoneIndex, isTruncated
} }
// mergeZonesEntriesVersionsCh - merges FileInfoVersions channel to entries upto maxKeys. // mergeZonesEntriesVersionsCh - merges FileInfoVersions channel to entries upto maxKeys.
func mergeZonesEntriesVersionsCh(zonesEntryChs [][]FileInfoVersionsCh, maxKeys int, zonesListTolerancePerSet []int) (entries FilesInfoVersions) { func mergeZonesEntriesVersionsCh(zonesEntryChs [][]FileInfoVersionsCh, maxKeys int, zonesListTolerancePerSet []int) (entries FilesInfoVersions) {
var i = 0 var i = 0
var zonesEntriesInfos [][]FileInfoVersions zonesEntriesInfos := make([][]FileInfoVersions, 0, len(zonesEntryChs))
var zonesEntriesValid [][]bool zonesEntriesValid := make([][]bool, 0, len(zonesEntryChs))
for _, entryChs := range zonesEntryChs { for _, entryChs := range zonesEntryChs {
zonesEntriesInfos = append(zonesEntriesInfos, make([]FileInfoVersions, len(entryChs))) zonesEntriesInfos = append(zonesEntriesInfos, make([]FileInfoVersions, len(entryChs)))
zonesEntriesValid = append(zonesEntriesValid, make([]bool, len(entryChs))) zonesEntriesValid = append(zonesEntriesValid, make([]bool, len(entryChs)))
} }
for { for {
fi, quorumCount, zoneIndex, ok := lexicallySortedEntryZoneVersions(zonesEntryChs, zonesEntriesInfos, zonesEntriesValid) fi, quorumCount, zoneIndex, ok := lexicallySortedEntryZoneVersions(zonesEntryChs, zonesEntriesInfos, zonesEntriesValid)
if !ok { if !ok {
@@ -1134,12 +1125,13 @@ func mergeZonesEntriesVersionsCh(zonesEntryChs [][]FileInfoVersionsCh, maxKeys i
// mergeZonesEntriesCh - merges FileInfo channel to entries upto maxKeys. // mergeZonesEntriesCh - merges FileInfo channel to entries upto maxKeys.
func mergeZonesEntriesCh(zonesEntryChs [][]FileInfoCh, maxKeys int, zonesListTolerancePerSet []int) (entries FilesInfo) { func mergeZonesEntriesCh(zonesEntryChs [][]FileInfoCh, maxKeys int, zonesListTolerancePerSet []int) (entries FilesInfo) {
var i = 0 var i = 0
var zonesEntriesInfos [][]FileInfo zonesEntriesInfos := make([][]FileInfo, 0, len(zonesEntryChs))
var zonesEntriesValid [][]bool zonesEntriesValid := make([][]bool, 0, len(zonesEntryChs))
for _, entryChs := range zonesEntryChs { for _, entryChs := range zonesEntryChs {
zonesEntriesInfos = append(zonesEntriesInfos, make([]FileInfo, len(entryChs))) zonesEntriesInfos = append(zonesEntriesInfos, make([]FileInfo, len(entryChs)))
zonesEntriesValid = append(zonesEntriesValid, make([]bool, len(entryChs))) zonesEntriesValid = append(zonesEntriesValid, make([]bool, len(entryChs)))
} }
var prevEntry string
for { for {
fi, quorumCount, zoneIndex, ok := lexicallySortedEntryZone(zonesEntryChs, zonesEntriesInfos, zonesEntriesValid) fi, quorumCount, zoneIndex, ok := lexicallySortedEntryZone(zonesEntryChs, zonesEntriesInfos, zonesEntriesValid)
if !ok { if !ok {
@@ -1152,12 +1144,17 @@ func mergeZonesEntriesCh(zonesEntryChs [][]FileInfoCh, maxKeys int, zonesListTol
continue continue
} }
if HasSuffix(fi.Name, slashSeparator) && fi.Name == prevEntry {
continue
}
entries.Files = append(entries.Files, fi) entries.Files = append(entries.Files, fi)
i++ i++
if i == maxKeys { if i == maxKeys {
entries.IsTruncated = isTruncatedZones(zonesEntryChs, zonesEntriesInfos, zonesEntriesValid) entries.IsTruncated = isTruncatedZones(zonesEntryChs, zonesEntriesInfos, zonesEntriesValid)
break break
} }
prevEntry = fi.Name
} }
return entries return entries
} }
@@ -1270,14 +1267,14 @@ func (z *erasureZones) listObjectVersions(ctx context.Context, bucket, prefix, m
recursive = false recursive = false
} }
var zonesEntryChs [][]FileInfoVersionsCh zonesEntryChs := make([][]FileInfoVersionsCh, 0, len(z.zones))
var zonesEndWalkCh []chan struct{} zonesEndWalkCh := make([]chan struct{}, 0, len(z.zones))
var zonesListTolerancePerSet []int zonesListTolerancePerSet := make([]int, 0, len(z.zones))
for _, zone := range z.zones { for _, zone := range z.zones {
entryChs, endWalkCh := zone.poolVersions.Release(listParams{bucket, recursive, marker, prefix}) entryChs, endWalkCh := zone.poolVersions.Release(listParams{bucket, recursive, marker, prefix})
if entryChs == nil { if entryChs == nil {
endWalkCh = make(chan struct{}) endWalkCh = make(chan struct{})
entryChs = zone.startMergeWalksVersionsN(ctx, bucket, prefix, marker, recursive, endWalkCh, zone.listTolerancePerSet) entryChs = zone.startMergeWalksVersionsN(ctx, bucket, prefix, marker, recursive, endWalkCh, zone.setDriveCount)
} }
zonesEntryChs = append(zonesEntryChs, entryChs) zonesEntryChs = append(zonesEntryChs, entryChs)
zonesEndWalkCh = append(zonesEndWalkCh, endWalkCh) zonesEndWalkCh = append(zonesEndWalkCh, endWalkCh)
@@ -1381,12 +1378,6 @@ func (z *erasureZones) PutObjectPart(ctx context.Context, bucket, object, upload
return PartInfo{}, err return PartInfo{}, err
} }
uploadIDLock := z.NewNSLock(ctx, bucket, pathJoin(object, uploadID))
if err := uploadIDLock.GetLock(globalOperationTimeout); err != nil {
return PartInfo{}, err
}
defer uploadIDLock.Unlock()
if z.SingleZone() { if z.SingleZone() {
return z.zones[0].PutObjectPart(ctx, bucket, object, uploadID, partID, data, opts) return z.zones[0].PutObjectPart(ctx, bucket, object, uploadID, partID, data, opts)
} }
@@ -1417,12 +1408,6 @@ func (z *erasureZones) GetMultipartInfo(ctx context.Context, bucket, object, upl
return MultipartInfo{}, err return MultipartInfo{}, err
} }
uploadIDLock := z.NewNSLock(ctx, bucket, pathJoin(object, uploadID))
if err := uploadIDLock.GetRLock(globalOperationTimeout); err != nil {
return MultipartInfo{}, err
}
defer uploadIDLock.RUnlock()
if z.SingleZone() { if z.SingleZone() {
return z.zones[0].GetMultipartInfo(ctx, bucket, object, uploadID, opts) return z.zones[0].GetMultipartInfo(ctx, bucket, object, uploadID, opts)
} }
@@ -1453,12 +1438,6 @@ func (z *erasureZones) ListObjectParts(ctx context.Context, bucket, object, uplo
return ListPartsInfo{}, err return ListPartsInfo{}, err
} }
uploadIDLock := z.NewNSLock(ctx, bucket, pathJoin(object, uploadID))
if err := uploadIDLock.GetRLock(globalOperationTimeout); err != nil {
return ListPartsInfo{}, err
}
defer uploadIDLock.RUnlock()
if z.SingleZone() { if z.SingleZone() {
return z.zones[0].ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts) return z.zones[0].ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts)
} }
@@ -1481,25 +1460,19 @@ func (z *erasureZones) ListObjectParts(ctx context.Context, bucket, object, uplo
} }
// 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 (z *erasureZones) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string) error { func (z *erasureZones) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) error {
if err := checkAbortMultipartArgs(ctx, bucket, object, z); err != nil { if err := checkAbortMultipartArgs(ctx, bucket, object, z); err != nil {
return err return err
} }
uploadIDLock := z.NewNSLock(ctx, bucket, pathJoin(object, uploadID))
if err := uploadIDLock.GetLock(globalOperationTimeout); err != nil {
return err
}
defer uploadIDLock.Unlock()
if z.SingleZone() { if z.SingleZone() {
return z.zones[0].AbortMultipartUpload(ctx, bucket, object, uploadID) return z.zones[0].AbortMultipartUpload(ctx, bucket, object, uploadID, opts)
} }
for _, zone := range z.zones { for _, zone := range z.zones {
_, err := zone.GetMultipartInfo(ctx, bucket, object, uploadID, ObjectOptions{}) _, err := zone.GetMultipartInfo(ctx, bucket, object, uploadID, opts)
if err == nil { if err == nil {
return zone.AbortMultipartUpload(ctx, bucket, object, uploadID) return zone.AbortMultipartUpload(ctx, bucket, object, uploadID, opts)
} }
switch err.(type) { switch err.(type) {
case InvalidUploadID: case InvalidUploadID:
@@ -1521,22 +1494,6 @@ func (z *erasureZones) CompleteMultipartUpload(ctx context.Context, bucket, obje
return objInfo, err return objInfo, err
} }
// Hold read-locks to verify uploaded parts, also disallows
// parallel part uploads as well.
uploadIDLock := z.NewNSLock(ctx, bucket, pathJoin(object, uploadID))
if err = uploadIDLock.GetRLock(globalOperationTimeout); err != nil {
return objInfo, err
}
defer uploadIDLock.RUnlock()
// Hold namespace to complete the transaction, only hold
// if uploadID can be held exclusively.
lk := z.NewNSLock(ctx, bucket, object)
if err = lk.GetLock(globalOperationTimeout); err != nil {
return objInfo, err
}
defer lk.Unlock()
if z.SingleZone() { if z.SingleZone() {
return z.zones[0].CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts) return z.zones[0].CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts)
} }
@@ -1700,13 +1657,8 @@ func (z *erasureZones) ListBuckets(ctx context.Context) (buckets []BucketInfo, e
} }
func (z *erasureZones) ReloadFormat(ctx context.Context, dryRun bool) error { func (z *erasureZones) ReloadFormat(ctx context.Context, dryRun bool) error {
// Acquire lock on format.json // No locks needed since reload happens in HealFormat under
formatLock := z.NewNSLock(ctx, minioMetaBucket, formatConfigFile) // write lock across all nodes.
if err := formatLock.GetRLock(globalOperationTimeout); err != nil {
return err
}
defer formatLock.RUnlock()
for _, zone := range z.zones { for _, zone := range z.zones {
if err := zone.ReloadFormat(ctx, dryRun); err != nil { if err := zone.ReloadFormat(ctx, dryRun); err != nil {
return err return err
@@ -1731,13 +1683,13 @@ func (z *erasureZones) HealFormat(ctx context.Context, dryRun bool) (madmin.Heal
var countNoHeal int var countNoHeal int
for _, zone := range z.zones { for _, zone := range z.zones {
result, err := zone.HealFormat(ctx, dryRun) result, err := zone.HealFormat(ctx, dryRun)
if err != nil && err != errNoHealRequired { if err != nil && !errors.Is(err, errNoHealRequired) {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
continue continue
} }
// Count errNoHealRequired across all zones, // Count errNoHealRequired across all zones,
// to return appropriate error to the caller // to return appropriate error to the caller
if err == errNoHealRequired { if errors.Is(err, errNoHealRequired) {
countNoHeal++ countNoHeal++
} }
r.DiskCount += result.DiskCount r.DiskCount += result.DiskCount
@@ -1745,10 +1697,21 @@ func (z *erasureZones) HealFormat(ctx context.Context, dryRun bool) (madmin.Heal
r.Before.Drives = append(r.Before.Drives, result.Before.Drives...) r.Before.Drives = append(r.Before.Drives, result.Before.Drives...)
r.After.Drives = append(r.After.Drives, result.After.Drives...) r.After.Drives = append(r.After.Drives, result.After.Drives...)
} }
// Healing succeeded notify the peers to reload format and re-initialize disks.
// We will not notify peers if healing is not required.
for _, nerr := range globalNotificationSys.ReloadFormat(dryRun) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
// No heal returned by all zones, return errNoHealRequired // No heal returned by all zones, return errNoHealRequired
if countNoHeal == len(z.zones) { if countNoHeal == len(z.zones) {
return r, errNoHealRequired return r, errNoHealRequired
} }
return r, nil return r, nil
} }
@@ -1829,18 +1792,15 @@ func (z *erasureZones) Walk(ctx context.Context, bucket, prefix string, results
return nil return nil
} }
var zonesEntryChs [][]FileInfoCh zonesEntryChs := make([][]FileInfoCh, 0, len(z.zones))
zoneDrivesPerSet := make([]int, 0, len(z.zones))
for _, zone := range z.zones { for _, zone := range z.zones {
zonesEntryChs = append(zonesEntryChs, zone.startMergeWalks(ctx, bucket, prefix, "", true, ctx.Done())) zonesEntryChs = append(zonesEntryChs, zone.startMergeWalks(ctx, bucket, prefix, "", true, ctx.Done()))
}
var zoneDrivesPerSet []int
for _, zone := range z.zones {
zoneDrivesPerSet = append(zoneDrivesPerSet, zone.setDriveCount) zoneDrivesPerSet = append(zoneDrivesPerSet, zone.setDriveCount)
} }
var zonesEntriesInfos [][]FileInfo zonesEntriesInfos := make([][]FileInfo, 0, len(zonesEntryChs))
var zonesEntriesValid [][]bool zonesEntriesValid := make([][]bool, 0, len(zonesEntryChs))
for _, entryChs := range zonesEntryChs { for _, entryChs := range zonesEntryChs {
zonesEntriesInfos = append(zonesEntriesInfos, make([]FileInfo, len(entryChs))) zonesEntriesInfos = append(zonesEntriesInfos, make([]FileInfo, len(entryChs)))
zonesEntriesValid = append(zonesEntriesValid, make([]bool, len(entryChs))) zonesEntriesValid = append(zonesEntriesValid, make([]bool, len(entryChs)))
@@ -1871,23 +1831,20 @@ func (z *erasureZones) Walk(ctx context.Context, bucket, prefix string, results
type HealObjectFn func(bucket, object, versionID string) error type HealObjectFn func(bucket, object, versionID string) error
func (z *erasureZones) HealObjects(ctx context.Context, bucket, prefix string, opts madmin.HealOpts, healObject HealObjectFn) error { func (z *erasureZones) HealObjects(ctx context.Context, bucket, prefix string, opts madmin.HealOpts, healObject HealObjectFn) error {
var zonesEntryChs [][]FileInfoVersionsCh
endWalkCh := make(chan struct{}) endWalkCh := make(chan struct{})
defer close(endWalkCh) defer close(endWalkCh)
zonesEntryChs := make([][]FileInfoVersionsCh, 0, len(z.zones))
zoneDrivesPerSet := make([]int, 0, len(z.zones))
for _, zone := range z.zones { for _, zone := range z.zones {
zonesEntryChs = append(zonesEntryChs, zonesEntryChs = append(zonesEntryChs,
zone.startMergeWalksVersions(ctx, bucket, prefix, "", true, endWalkCh)) zone.startMergeWalksVersions(ctx, bucket, prefix, "", true, endWalkCh))
}
var zoneDrivesPerSet []int
for _, zone := range z.zones {
zoneDrivesPerSet = append(zoneDrivesPerSet, zone.setDriveCount) zoneDrivesPerSet = append(zoneDrivesPerSet, zone.setDriveCount)
} }
var zonesEntriesInfos [][]FileInfoVersions zonesEntriesInfos := make([][]FileInfoVersions, 0, len(zonesEntryChs))
var zonesEntriesValid [][]bool zonesEntriesValid := make([][]bool, 0, len(zonesEntryChs))
for _, entryChs := range zonesEntryChs { for _, entryChs := range zonesEntryChs {
zonesEntriesInfos = append(zonesEntriesInfos, make([]FileInfoVersions, len(entryChs))) zonesEntriesInfos = append(zonesEntriesInfos, make([]FileInfoVersions, len(entryChs)))
zonesEntriesValid = append(zonesEntriesValid, make([]bool, len(entryChs))) zonesEntriesValid = append(zonesEntriesValid, make([]bool, len(entryChs)))
@@ -1927,6 +1884,8 @@ func (z *erasureZones) HealObjects(ctx context.Context, bucket, prefix string, o
} }
func (z *erasureZones) HealObject(ctx context.Context, bucket, object, versionID string, opts madmin.HealOpts) (madmin.HealResultItem, error) { func (z *erasureZones) HealObject(ctx context.Context, bucket, object, versionID string, opts madmin.HealOpts) (madmin.HealResultItem, error) {
object = encodeDirObject(object)
lk := z.NewNSLock(ctx, bucket, object) lk := z.NewNSLock(ctx, bucket, object)
if bucket == minioMetaBucket { if bucket == minioMetaBucket {
// For .minio.sys bucket heals we should hold write locks. // For .minio.sys bucket heals we should hold write locks.
@@ -2045,27 +2004,49 @@ func (z *erasureZones) Health(ctx context.Context, opts HealthOptions) HealthRes
reqInfo := (&logger.ReqInfo{}).AppendTags("maintenance", strconv.FormatBool(opts.Maintenance)) reqInfo := (&logger.ReqInfo{}).AppendTags("maintenance", strconv.FormatBool(opts.Maintenance))
parityDrives := globalStorageClass.GetParityForSC(storageclass.STANDARD)
diskCount := z.SetDriveCount()
if parityDrives == 0 {
parityDrives = getDefaultParityBlocks(diskCount)
}
dataDrives := diskCount - parityDrives
writeQuorum := dataDrives
if dataDrives == parityDrives {
writeQuorum++
}
var aggHealStateResult madmin.BgHealState
if opts.Maintenance {
// check if local disks are being healed, if they are being healed
// we need to tell healthy status as 'false' so that this server
// is not taken down for maintenance
var err error
aggHealStateResult, err = getAggregatedBackgroundHealState(ctx)
if err != nil {
logger.LogIf(logger.SetReqInfo(ctx, reqInfo), fmt.Errorf("Unable to verify global heal status: %w", err))
return HealthResult{
Healthy: false,
}
}
if len(aggHealStateResult.HealDisks) > 0 {
logger.LogIf(logger.SetReqInfo(ctx, reqInfo), fmt.Errorf("Total drives to be healed %d", len(aggHealStateResult.HealDisks)))
}
}
for zoneIdx := range erasureSetUpCount { for zoneIdx := range erasureSetUpCount {
parityDrives := globalStorageClass.GetParityForSC(storageclass.STANDARD)
diskCount := z.zones[zoneIdx].setDriveCount
if parityDrives == 0 {
parityDrives = getDefaultParityBlocks(diskCount)
}
dataDrives := diskCount - parityDrives
writeQuorum := dataDrives
if dataDrives == parityDrives {
writeQuorum++
}
for setIdx := range erasureSetUpCount[zoneIdx] { for setIdx := range erasureSetUpCount[zoneIdx] {
if erasureSetUpCount[zoneIdx][setIdx] < writeQuorum { if erasureSetUpCount[zoneIdx][setIdx] < writeQuorum {
logger.LogIf(logger.SetReqInfo(ctx, reqInfo), logger.LogIf(logger.SetReqInfo(ctx, reqInfo),
fmt.Errorf("Write quorum may be lost on zone: %d, set: %d, expected write quorum: %d", fmt.Errorf("Write quorum may be lost on zone: %d, set: %d, expected write quorum: %d",
zoneIdx, setIdx, writeQuorum)) zoneIdx, setIdx, writeQuorum))
return HealthResult{ return HealthResult{
Healthy: false, Healthy: false,
ZoneID: zoneIdx, HealingDrives: len(aggHealStateResult.HealDisks),
SetID: setIdx, ZoneID: zoneIdx,
WriteQuorum: writeQuorum, SetID: setIdx,
WriteQuorum: writeQuorum,
} }
} }
} }
@@ -2075,35 +2056,21 @@ func (z *erasureZones) Health(ctx context.Context, opts HealthOptions) HealthRes
// to look at the healing side of the code. // to look at the healing side of the code.
if !opts.Maintenance { if !opts.Maintenance {
return HealthResult{ return HealthResult{
Healthy: true, Healthy: true,
WriteQuorum: writeQuorum,
} }
} }
// check if local disks are being healed, if they are being healed
// we need to tell healthy status as 'false' so that this server
// is not taken down for maintenance
aggHealStateResult, err := getAggregatedBackgroundHealState(ctx, true)
if err != nil {
logger.LogIf(logger.SetReqInfo(ctx, reqInfo), fmt.Errorf("Unable to verify global heal status: %w", err))
return HealthResult{
Healthy: false,
}
}
if len(aggHealStateResult.HealDisks) > 0 {
logger.LogIf(logger.SetReqInfo(ctx, reqInfo), fmt.Errorf("Total drives to be healed %d", len(aggHealStateResult.HealDisks)))
}
healthy := len(aggHealStateResult.HealDisks) == 0
return HealthResult{ return HealthResult{
Healthy: healthy, Healthy: len(aggHealStateResult.HealDisks) == 0,
HealingDrives: len(aggHealStateResult.HealDisks), HealingDrives: len(aggHealStateResult.HealDisks),
WriteQuorum: writeQuorum,
} }
} }
// PutObjectTags - replace or add tags to an existing object // PutObjectTags - replace or add tags to an existing object
func (z *erasureZones) PutObjectTags(ctx context.Context, bucket, object string, tags string, opts ObjectOptions) error { func (z *erasureZones) PutObjectTags(ctx context.Context, bucket, object string, tags string, opts ObjectOptions) error {
object = encodeDirObject(object)
if z.SingleZone() { if z.SingleZone() {
return z.zones[0].PutObjectTags(ctx, bucket, object, tags, opts) return z.zones[0].PutObjectTags(ctx, bucket, object, tags, opts)
} }
@@ -2133,6 +2100,7 @@ func (z *erasureZones) PutObjectTags(ctx context.Context, bucket, object string,
// DeleteObjectTags - delete object tags from an existing object // DeleteObjectTags - delete object tags from an existing object
func (z *erasureZones) DeleteObjectTags(ctx context.Context, bucket, object string, opts ObjectOptions) error { func (z *erasureZones) DeleteObjectTags(ctx context.Context, bucket, object string, opts ObjectOptions) error {
object = encodeDirObject(object)
if z.SingleZone() { if z.SingleZone() {
return z.zones[0].DeleteObjectTags(ctx, bucket, object, opts) return z.zones[0].DeleteObjectTags(ctx, bucket, object, opts)
} }
@@ -2161,6 +2129,7 @@ func (z *erasureZones) DeleteObjectTags(ctx context.Context, bucket, object stri
// GetObjectTags - get object tags from an existing object // GetObjectTags - get object tags from an existing object
func (z *erasureZones) GetObjectTags(ctx context.Context, bucket, object string, opts ObjectOptions) (*tags.Tags, error) { func (z *erasureZones) GetObjectTags(ctx context.Context, bucket, object string, opts ObjectOptions) (*tags.Tags, error) {
object = encodeDirObject(object)
if z.SingleZone() { if z.SingleZone() {
return z.zones[0].GetObjectTags(ctx, bucket, object, opts) return z.zones[0].GetObjectTags(ctx, bucket, object, opts)
} }
+17 -28
View File
@@ -52,7 +52,7 @@ type erasureObjects struct {
getDisks func() []StorageAPI getDisks func() []StorageAPI
// getLockers returns list of remote and local lockers. // getLockers returns list of remote and local lockers.
getLockers func() []dsync.NetLocker getLockers func() ([]dsync.NetLocker, string)
// getEndpoints returns list of endpoint strings belonging this set. // getEndpoints returns list of endpoint strings belonging this set.
// some may be local and some remote. // some may be local and some remote.
@@ -81,6 +81,14 @@ func (er erasureObjects) SetDriveCount() int {
func (er erasureObjects) Shutdown(ctx context.Context) error { func (er erasureObjects) Shutdown(ctx context.Context) error {
// Add any object layer shutdown activities here. // Add any object layer shutdown activities here.
closeStorageDisks(er.getDisks()) closeStorageDisks(er.getDisks())
select {
case _, ok := <-er.mrfOpCh:
if ok {
close(er.mrfOpCh)
}
default:
close(er.mrfOpCh)
}
return nil return nil
} }
@@ -132,6 +140,7 @@ func getDisksInfo(disks []StorageAPI, endpoints []string) (disksInfo []madmin.Di
index := index index := index
g.Go(func() error { g.Go(func() error {
if disks[index] == OfflineDisk { if disks[index] == OfflineDisk {
logger.LogIf(GlobalContext, fmt.Errorf("%s: %s", errDiskNotFound, endpoints[index]))
disksInfo[index] = madmin.Disk{ disksInfo[index] = madmin.Disk{
State: diskErrToDriveState(errDiskNotFound), State: diskErrToDriveState(errDiskNotFound),
Endpoint: endpoints[index], Endpoint: endpoints[index],
@@ -139,13 +148,11 @@ func getDisksInfo(disks []StorageAPI, endpoints []string) (disksInfo []madmin.Di
// Storage disk is empty, perhaps ignored disk or not available. // Storage disk is empty, perhaps ignored disk or not available.
return errDiskNotFound return errDiskNotFound
} }
info, err := disks[index].DiskInfo() info, err := disks[index].DiskInfo(context.TODO())
if err != nil { if err != nil {
if !IsErr(err, baseErrs...) { reqInfo := (&logger.ReqInfo{}).AppendTags("disk", disks[index].String())
reqInfo := (&logger.ReqInfo{}).AppendTags("disk", disks[index].String()) ctx := logger.SetReqInfo(GlobalContext, reqInfo)
ctx := logger.SetReqInfo(GlobalContext, reqInfo) logger.LogIf(ctx, err)
logger.LogIf(ctx, err)
}
disksInfo[index] = madmin.Disk{ disksInfo[index] = madmin.Disk{
State: diskErrToDriveState(err), State: diskErrToDriveState(err),
Endpoint: endpoints[index], Endpoint: endpoints[index],
@@ -245,19 +252,6 @@ func (er erasureObjects) StorageInfo(ctx context.Context, local bool) (StorageIn
return getStorageInfo(disks, endpoints) return getStorageInfo(disks, endpoints)
} }
// GetMetrics - is not implemented and shouldn't be called.
func (er erasureObjects) GetMetrics(ctx context.Context) (*Metrics, error) {
logger.LogIf(ctx, NotImplemented{})
return &Metrics{}, NotImplemented{}
}
// CrawlAndGetDataUsage collects usage from all buckets.
// updates are sent as different parts of the underlying
// structure has been traversed.
func (er erasureObjects) CrawlAndGetDataUsage(ctx context.Context, bf *bloomFilter, updates chan<- DataUsageInfo) error {
return NotImplemented{API: "CrawlAndGetDataUsage"}
}
// CrawlAndGetDataUsage will start crawling buckets and send updated totals as they are traversed. // CrawlAndGetDataUsage will start crawling buckets and send updated totals as they are traversed.
// Updates are sent on a regular basis and the caller *must* consume them. // Updates are sent on a regular basis and the caller *must* consume them.
func (er erasureObjects) crawlAndGetDataUsage(ctx context.Context, buckets []BucketInfo, bf *bloomFilter, updates chan<- dataUsageCache) error { func (er erasureObjects) crawlAndGetDataUsage(ctx context.Context, buckets []BucketInfo, bf *bloomFilter, updates chan<- dataUsageCache) error {
@@ -266,10 +260,11 @@ func (er erasureObjects) crawlAndGetDataUsage(ctx context.Context, buckets []Buc
} }
// Collect any disk healing. // Collect any disk healing.
healing, err := getAggregatedBackgroundHealState(ctx, true) healing, err := getAggregatedBackgroundHealState(ctx)
if err != nil { if err != nil {
return err return err
} }
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{}{}
@@ -281,7 +276,7 @@ func (er erasureObjects) crawlAndGetDataUsage(ctx context.Context, buckets []Buc
if d == nil || !d.IsOnline() { if d == nil || !d.IsOnline() {
continue continue
} }
di, err := d.DiskInfo() di, err := d.DiskInfo(ctx)
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
continue continue
@@ -438,9 +433,3 @@ func (er erasureObjects) crawlAndGetDataUsage(ctx context.Context, buckets []Buc
return nil return nil
} }
// Health shouldn't be called directly - will panic
func (er erasureObjects) Health(ctx context.Context, _ HealthOptions) HealthResult {
logger.CriticalIf(ctx, NotImplemented{})
return HealthResult{}
}
+1 -1
View File
@@ -134,7 +134,7 @@ func newErasureTestSetup(dataBlocks int, parityBlocks int, blockSize int64) (*er
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = disks[i].MakeVol("testbucket") err = disks[i].MakeVol(context.Background(), "testbucket")
if err != nil { if err != nil {
return nil, err return nil, err
} }
+6 -6
View File
@@ -355,15 +355,15 @@ func saveFormatErasure(disk StorageAPI, format *formatErasureV3) error {
tmpFormat := mustGetUUID() tmpFormat := mustGetUUID()
// Purge any existing temporary file, okay to ignore errors here. // Purge any existing temporary file, okay to ignore errors here.
defer disk.DeleteFile(minioMetaBucket, tmpFormat) defer disk.DeleteFile(context.TODO(), minioMetaBucket, tmpFormat)
// write to unique file. // write to unique file.
if err = disk.WriteAll(minioMetaBucket, tmpFormat, bytes.NewReader(formatBytes)); err != nil { if err = disk.WriteAll(context.TODO(), minioMetaBucket, tmpFormat, bytes.NewReader(formatBytes)); err != nil {
return err return err
} }
// Rename file `uuid.json` --> `format.json`. // Rename file `uuid.json` --> `format.json`.
if err = disk.RenameFile(minioMetaBucket, tmpFormat, minioMetaBucket, formatConfigFile); err != nil { if err = disk.RenameFile(context.TODO(), minioMetaBucket, tmpFormat, minioMetaBucket, formatConfigFile); err != nil {
return err return err
} }
@@ -392,13 +392,13 @@ func isHiddenDirectories(vols ...VolInfo) bool {
// loadFormatErasure - loads format.json from disk. // loadFormatErasure - loads format.json from disk.
func loadFormatErasure(disk StorageAPI) (format *formatErasureV3, err error) { func loadFormatErasure(disk StorageAPI) (format *formatErasureV3, err error) {
buf, err := disk.ReadAll(minioMetaBucket, formatConfigFile) buf, err := disk.ReadAll(context.TODO(), minioMetaBucket, formatConfigFile)
if err != nil { if err != nil {
// 'file not found' and 'volume not found' as // 'file not found' and 'volume not found' as
// same. 'volume not found' usually means its a fresh disk. // same. 'volume not found' usually means its a fresh disk.
if err == errFileNotFound || err == errVolumeNotFound { if err == errFileNotFound || err == errVolumeNotFound {
var vols []VolInfo var vols []VolInfo
vols, err = disk.ListVols() vols, err = disk.ListVols(context.TODO())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -862,7 +862,7 @@ func makeFormatErasureMetaVolumes(disk StorageAPI) error {
return errDiskNotFound return errDiskNotFound
} }
// Attempt to create MinIO internal buckets. // Attempt to create MinIO internal buckets.
return disk.MakeVolBulk(minioMetaBucket, minioMetaTmpBucket, minioMetaMultipartBucket, dataUsageBucket) return disk.MakeVolBulk(context.TODO(), minioMetaBucket, minioMetaTmpBucket, minioMetaMultipartBucket, dataUsageBucket)
} }
// Get all UUIDs which are present in reference format should // Get all UUIDs which are present in reference format should
-1
View File
@@ -225,7 +225,6 @@ func (m *fsMetaV1) ReadFrom(ctx context.Context, lk *lock.LockedFile) (n int64,
} }
if len(fsMetaBuf) == 0 { if len(fsMetaBuf) == 0 {
logger.LogIf(ctx, io.EOF)
return 0, io.EOF return 0, io.EOF
} }
+93 -63
View File
@@ -19,6 +19,7 @@ package cmd
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@@ -31,6 +32,7 @@ import (
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
mioutil "github.com/minio/minio/pkg/ioutil" mioutil "github.com/minio/minio/pkg/ioutil"
"github.com/minio/minio/pkg/trie"
) )
// Returns EXPORT/.minio.sys/multipart/SHA256/UPLOADID // Returns EXPORT/.minio.sys/multipart/SHA256/UPLOADID
@@ -306,7 +308,7 @@ func (fs *FSObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
_, err := fsStatFile(ctx, pathJoin(uploadIDDir, fs.metaJSONFile)) _, err := fsStatFile(ctx, pathJoin(uploadIDDir, fs.metaJSONFile))
if err != nil { if err != nil {
if err == errFileNotFound || err == errFileAccessDenied { if err == errFileNotFound || err == errFileAccessDenied {
return pi, InvalidUploadID{UploadID: uploadID} return pi, InvalidUploadID{Bucket: bucket, Object: object, UploadID: uploadID}
} }
return pi, toObjectErr(err, bucket, object) return pi, toObjectErr(err, bucket, object)
} }
@@ -332,7 +334,7 @@ func (fs *FSObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
// Should return IncompleteBody{} error when reader has fewer // Should return IncompleteBody{} error when reader has fewer
// bytes than specified in request header. // bytes than specified in request header.
if bytesWritten < data.Size() { if bytesWritten < data.Size() {
return pi, IncompleteBody{} return pi, IncompleteBody{Bucket: bucket, Object: object}
} }
etag := r.MD5CurrentHexString() etag := r.MD5CurrentHexString()
@@ -346,7 +348,7 @@ func (fs *FSObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
// Make sure not to create parent directories if they don't exist - the upload might have been aborted. // Make sure not to create parent directories if they don't exist - the upload might have been aborted.
if err = fsSimpleRenameFile(ctx, tmpPartPath, partPath); err != nil { if err = fsSimpleRenameFile(ctx, tmpPartPath, partPath); err != nil {
if err == errFileNotFound || err == errFileAccessDenied { if err == errFileNotFound || err == errFileAccessDenied {
return pi, InvalidUploadID{UploadID: uploadID} return pi, InvalidUploadID{Bucket: bucket, Object: object, UploadID: uploadID}
} }
return pi, toObjectErr(err, minioMetaMultipartBucket, partPath) return pi, toObjectErr(err, minioMetaMultipartBucket, partPath)
} }
@@ -389,7 +391,7 @@ func (fs *FSObjects) GetMultipartInfo(ctx context.Context, bucket, object, uploa
uploadIDDir := fs.getUploadIDDir(bucket, object, uploadID) uploadIDDir := fs.getUploadIDDir(bucket, object, uploadID)
if _, err := fsStatFile(ctx, pathJoin(uploadIDDir, fs.metaJSONFile)); err != nil { if _, err := fsStatFile(ctx, pathJoin(uploadIDDir, fs.metaJSONFile)); err != nil {
if err == errFileNotFound || err == errFileAccessDenied { if err == errFileNotFound || err == errFileAccessDenied {
return minfo, InvalidUploadID{UploadID: uploadID} return minfo, InvalidUploadID{Bucket: bucket, Object: object, UploadID: uploadID}
} }
return minfo, toObjectErr(err, bucket, object) return minfo, toObjectErr(err, bucket, object)
} }
@@ -435,7 +437,7 @@ func (fs *FSObjects) ListObjectParts(ctx context.Context, bucket, object, upload
uploadIDDir := fs.getUploadIDDir(bucket, object, uploadID) uploadIDDir := fs.getUploadIDDir(bucket, object, uploadID)
if _, err := fsStatFile(ctx, pathJoin(uploadIDDir, fs.metaJSONFile)); err != nil { if _, err := fsStatFile(ctx, pathJoin(uploadIDDir, fs.metaJSONFile)); err != nil {
if err == errFileNotFound || err == errFileAccessDenied { if err == errFileNotFound || err == errFileAccessDenied {
return result, InvalidUploadID{UploadID: uploadID} return result, InvalidUploadID{Bucket: bucket, Object: object, UploadID: uploadID}
} }
return result, toObjectErr(err, bucket, object) return result, toObjectErr(err, bucket, object)
} }
@@ -446,52 +448,51 @@ func (fs *FSObjects) ListObjectParts(ctx context.Context, bucket, object, upload
return result, toObjectErr(err, bucket) return result, toObjectErr(err, bucket)
} }
partsMap := make(map[int]string) partsMap := make(map[int]PartInfo)
for _, entry := range entries { for _, entry := range entries {
if entry == fs.metaJSONFile { if entry == fs.metaJSONFile {
continue continue
} }
partNumber, etag1, _, derr := fs.decodePartFile(entry)
partNumber, currentEtag, actualSize, derr := fs.decodePartFile(entry)
if derr != nil { if derr != nil {
// Skip part files whose name don't match expected format. These could be backend filesystem specific files. // Skip part files whose name don't match expected format. These could be backend filesystem specific files.
continue continue
} }
etag2, ok := partsMap[partNumber]
if !ok { entryStat, err := fsStatFile(ctx, pathJoin(uploadIDDir, entry))
partsMap[partNumber] = etag1 if err != nil {
continue continue
} }
stat1, serr := fsStatFile(ctx, pathJoin(uploadIDDir, getPartFile(entries, partNumber, etag1)))
if serr != nil { currentMeta := PartInfo{
return result, toObjectErr(serr) PartNumber: partNumber,
ETag: currentEtag,
ActualSize: actualSize,
Size: entryStat.Size(),
LastModified: entryStat.ModTime(),
} }
stat2, serr := fsStatFile(ctx, pathJoin(uploadIDDir, getPartFile(entries, partNumber, etag2)))
if serr != nil { cachedMeta, ok := partsMap[partNumber]
return result, toObjectErr(serr) if !ok {
partsMap[partNumber] = currentMeta
continue
} }
if stat1.ModTime().After(stat2.ModTime()) {
partsMap[partNumber] = etag1 if currentMeta.LastModified.After(cachedMeta.LastModified) {
partsMap[partNumber] = currentMeta
} }
} }
var parts []PartInfo var parts []PartInfo
var actualSize int64 for _, partInfo := range partsMap {
for partNumber, etag := range partsMap { parts = append(parts, partInfo)
partFile := getPartFile(entries, partNumber, etag)
if partFile == "" {
return result, InvalidPart{}
}
// Read the actualSize from the pathFileName.
subParts := strings.Split(partFile, ".")
actualSize, err = strconv.ParseInt(subParts[len(subParts)-1], 10, 64)
if err != nil {
return result, InvalidPart{}
}
parts = append(parts, PartInfo{PartNumber: partNumber, ETag: etag, ActualSize: actualSize})
} }
sort.Slice(parts, func(i int, j int) bool { sort.Slice(parts, func(i int, j int) bool {
return parts[i].PartNumber < parts[j].PartNumber return parts[i].PartNumber < parts[j].PartNumber
}) })
i := 0 i := 0
if partNumberMarker != 0 { if partNumberMarker != 0 {
// If the marker was set, skip the entries till the marker. // If the marker was set, skip the entries till the marker.
@@ -515,21 +516,19 @@ func (fs *FSObjects) ListObjectParts(ctx context.Context, bucket, object, upload
result.NextPartNumberMarker = result.Parts[partsCount-1].PartNumber result.NextPartNumberMarker = result.Parts[partsCount-1].PartNumber
} }
} }
for i, part := range result.Parts {
var stat os.FileInfo
stat, err = fsStatFile(ctx, pathJoin(uploadIDDir,
fs.encodePartFile(part.PartNumber, part.ETag, part.ActualSize)))
if err != nil {
return result, toObjectErr(err)
}
result.Parts[i].LastModified = stat.ModTime()
result.Parts[i].Size = part.ActualSize
}
fsMetaBytes, err := ioutil.ReadFile(pathJoin(uploadIDDir, fs.metaJSONFile)) rc, _, err := fsOpenFile(ctx, pathJoin(uploadIDDir, fs.metaJSONFile), 0)
if err != nil { if err != nil {
logger.LogIf(ctx, err) if err == errFileNotFound || err == errFileAccessDenied {
return result, err return result, InvalidUploadID{Bucket: bucket, Object: object, UploadID: uploadID}
}
return result, toObjectErr(err, bucket, object)
}
defer rc.Close()
fsMetaBytes, err := ioutil.ReadAll(rc)
if err != nil {
return result, toObjectErr(err, bucket, object)
} }
var fsMeta fsMetaV1 var fsMeta fsMetaV1
@@ -571,7 +570,7 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
_, err := fsStatFile(ctx, pathJoin(uploadIDDir, fs.metaJSONFile)) _, err := fsStatFile(ctx, pathJoin(uploadIDDir, fs.metaJSONFile))
if err != nil { if err != nil {
if err == errFileNotFound || err == errFileAccessDenied { if err == errFileNotFound || err == errFileAccessDenied {
return oi, InvalidUploadID{UploadID: uploadID} return oi, InvalidUploadID{Bucket: bucket, Object: object, UploadID: uploadID}
} }
return oi, toObjectErr(err, bucket, object) return oi, toObjectErr(err, bucket, object)
} }
@@ -579,7 +578,10 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
// Calculate s3 compatible md5sum for complete multipart. // Calculate s3 compatible md5sum for complete multipart.
s3MD5 := getCompleteMultipartMD5(parts) s3MD5 := getCompleteMultipartMD5(parts)
partSize := int64(-1) // Used later to ensure that all parts sizes are same. // ensure that part ETag is canonicalized to strip off extraneous quotes
for i := range parts {
parts[i].ETag = canonicalizeETag(parts[i].ETag)
}
fsMeta := fsMetaV1{} fsMeta := fsMetaV1{}
@@ -593,16 +595,17 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
return oi, err return oi, err
} }
// ensure that part ETag is canonicalized to strip off extraneous quotes // Create entries trie structure for prefix match
for i := range parts { entriesTrie := trie.NewTrie()
parts[i].ETag = canonicalizeETag(parts[i].ETag) for _, entry := range entries {
entriesTrie.Insert(entry)
} }
// Save consolidated actual size. // Save consolidated actual size.
var objectActualSize int64 var objectActualSize int64
// Validate all parts and then commit to disk. // Validate all parts and then commit to disk.
for i, part := range parts { for i, part := range parts {
partFile := getPartFile(entries, part.PartNumber, part.ETag) partFile := getPartFile(entriesTrie, part.PartNumber, part.ETag)
if partFile == "" { if partFile == "" {
return oi, InvalidPart{ return oi, InvalidPart{
PartNumber: part.PartNumber, PartNumber: part.PartNumber,
@@ -630,9 +633,6 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
} }
return oi, err return oi, err
} }
if partSize == -1 {
partSize = actualSize
}
fsMeta.Parts[i] = ObjectPartInfo{ fsMeta.Parts[i] = ObjectPartInfo{
Number: part.PartNumber, Number: part.PartNumber,
@@ -697,8 +697,16 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
fsRemoveFile(ctx, file.filePath) fsRemoveFile(ctx, file.filePath)
} }
for _, part := range parts { for _, part := range parts {
partPath := getPartFile(entries, part.PartNumber, part.ETag) partFile := getPartFile(entriesTrie, part.PartNumber, part.ETag)
if err = mioutil.AppendFile(appendFilePath, pathJoin(uploadIDDir, partPath), globalFSOSync); err != nil { if partFile == "" {
logger.LogIf(ctx, fmt.Errorf("%.5d.%s missing will not proceed",
part.PartNumber, part.ETag))
return oi, InvalidPart{
PartNumber: part.PartNumber,
GotETag: part.ETag,
}
}
if err = mioutil.AppendFile(appendFilePath, pathJoin(uploadIDDir, partFile), globalFSOSync); err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return oi, toObjectErr(err) return oi, toObjectErr(err)
} }
@@ -711,13 +719,35 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
return oi, err return oi, err
} }
defer destLock.Unlock() defer destLock.Unlock()
fsMetaPath := pathJoin(fs.fsPath, minioMetaBucket, bucketMetaPrefix, bucket, object, fs.metaJSONFile)
metaFile, err := fs.rwPool.Create(fsMetaPath) bucketMetaDir := pathJoin(fs.fsPath, minioMetaBucket, bucketMetaPrefix)
fsMetaPath := pathJoin(bucketMetaDir, bucket, object, fs.metaJSONFile)
metaFile, err := fs.rwPool.Write(fsMetaPath)
var freshFile bool
if err != nil { if err != nil {
logger.LogIf(ctx, err) if !errors.Is(err, errFileNotFound) {
return oi, toObjectErr(err, bucket, object) logger.LogIf(ctx, err)
return oi, toObjectErr(err, bucket, object)
}
metaFile, err = fs.rwPool.Create(fsMetaPath)
if err != nil {
logger.LogIf(ctx, err)
return oi, toObjectErr(err, bucket, object)
}
freshFile = true
} }
defer metaFile.Close() defer metaFile.Close()
defer func() {
// Remove meta file when CompleteMultipart encounters
// any error and it is a fresh file.
//
// We should preserve the `fs.json` of any
// existing object
if e != nil && freshFile {
tmpDir := pathJoin(fs.fsPath, minioMetaTmpBucket, fs.fsUUID)
fsRemoveMeta(ctx, bucketMetaDir, fsMetaPath, tmpDir)
}
}()
// Read saved fs metadata for ongoing multipart. // Read saved fs metadata for ongoing multipart.
fsMetaBuf, err := ioutil.ReadFile(pathJoin(uploadIDDir, fs.metaJSONFile)) fsMetaBuf, err := ioutil.ReadFile(pathJoin(uploadIDDir, fs.metaJSONFile))
@@ -731,7 +761,7 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
return oi, toObjectErr(err, bucket, object) return oi, toObjectErr(err, bucket, object)
} }
// Save additional metadata. // Save additional metadata.
if len(fsMeta.Meta) == 0 { if fsMeta.Meta == nil {
fsMeta.Meta = make(map[string]string) fsMeta.Meta = make(map[string]string)
} }
fsMeta.Meta["etag"] = s3MD5 fsMeta.Meta["etag"] = s3MD5
@@ -779,7 +809,7 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
// that this is an atomic idempotent operation. Subsequent calls have // that this is an atomic idempotent operation. Subsequent calls have
// no affect and further requests to the same uploadID would not be // no affect and further requests to the same uploadID would not be
// honored. // honored.
func (fs *FSObjects) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string) error { func (fs *FSObjects) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) error {
if err := checkAbortMultipartArgs(ctx, bucket, object, fs); err != nil { if err := checkAbortMultipartArgs(ctx, bucket, object, fs); err != nil {
return err return err
} }
@@ -802,7 +832,7 @@ func (fs *FSObjects) AbortMultipartUpload(ctx context.Context, bucket, object, u
_, err := fsStatFile(ctx, pathJoin(uploadIDDir, fs.metaJSONFile)) _, err := fsStatFile(ctx, pathJoin(uploadIDDir, fs.metaJSONFile))
if err != nil { if err != nil {
if err == errFileNotFound || err == errFileAccessDenied { if err == errFileNotFound || err == errFileAccessDenied {
return InvalidUploadID{UploadID: uploadID} return InvalidUploadID{Bucket: bucket, Object: object, UploadID: uploadID}
} }
return toObjectErr(err, bucket, object) return toObjectErr(err, bucket, object)
} }
@@ -824,7 +854,7 @@ func (fs *FSObjects) AbortMultipartUpload(ctx context.Context, bucket, object, u
// Removes multipart uploads if any older than `expiry` duration // Removes multipart uploads if any older than `expiry` duration
// on all buckets for every `cleanupInterval`, this function is // on all buckets for every `cleanupInterval`, this function is
// blocking and should be run in a go-routine. // blocking and should be run in a go-routine.
func (fs *FSObjects) cleanupStaleMultipartUploads(ctx context.Context, cleanupInterval, expiry time.Duration) { func (fs *FSObjects) cleanupStaleUploads(ctx context.Context, cleanupInterval, expiry time.Duration) {
ticker := time.NewTicker(cleanupInterval) ticker := time.NewTicker(cleanupInterval)
defer ticker.Stop() defer ticker.Stop()
+5 -4
View File
@@ -51,7 +51,7 @@ func TestFSCleanupMultipartUploadsInRoutine(t *testing.T) {
cleanupWg.Add(1) cleanupWg.Add(1)
go func() { go func() {
defer cleanupWg.Done() defer cleanupWg.Done()
fs.cleanupStaleMultipartUploads(ctx, time.Millisecond, 0) fs.cleanupStaleUploads(ctx, time.Millisecond, 0)
}() }()
// Wait for 100ms such that - we have given enough time for // Wait for 100ms such that - we have given enough time for
@@ -61,7 +61,7 @@ func TestFSCleanupMultipartUploadsInRoutine(t *testing.T) {
cleanupWg.Wait() cleanupWg.Wait()
// Check if upload id was already purged. // Check if upload id was already purged.
if err = obj.AbortMultipartUpload(GlobalContext, bucketName, objectName, uploadID); err != nil { if err = obj.AbortMultipartUpload(GlobalContext, bucketName, objectName, uploadID, ObjectOptions{}); err != nil {
if _, ok := err.(InvalidUploadID); !ok { if _, ok := err.(InvalidUploadID); !ok {
t.Fatal("Unexpected err: ", err) t.Fatal("Unexpected err: ", err)
} }
@@ -215,11 +215,12 @@ func TestAbortMultipartUpload(t *testing.T) {
md5Hex := getMD5Hash(data) md5Hex := getMD5Hash(data)
if _, err := obj.PutObjectPart(GlobalContext, bucketName, objectName, uploadID, 1, mustGetPutObjReader(t, bytes.NewReader(data), 5, md5Hex, ""), ObjectOptions{}); err != nil { opts := ObjectOptions{}
if _, err := obj.PutObjectPart(GlobalContext, bucketName, objectName, uploadID, 1, mustGetPutObjReader(t, bytes.NewReader(data), 5, md5Hex, ""), opts); err != nil {
t.Fatal("Unexpected error ", err) t.Fatal("Unexpected error ", err)
} }
time.Sleep(time.Second) // Without Sleep on windows, the fs.AbortMultipartUpload() fails with "The process cannot access the file because it is being used by another process." time.Sleep(time.Second) // Without Sleep on windows, the fs.AbortMultipartUpload() fails with "The process cannot access the file because it is being used by another process."
if err := obj.AbortMultipartUpload(GlobalContext, bucketName, objectName, uploadID); err != nil { if err := obj.AbortMultipartUpload(GlobalContext, bucketName, objectName, uploadID, opts); err != nil {
t.Fatal("Unexpected error ", err) t.Fatal("Unexpected error ", err)
} }
} }
+3
View File
@@ -157,6 +157,9 @@ func (fsi *fsIOPool) Write(path string) (wlk *lock.LockedFile, err error) {
case isSysErrIsDir(err): case isSysErrIsDir(err):
return nil, errIsNotRegular return nil, errIsNotRegular
default: default:
if isSysErrPathNotFound(err) {
return nil, errFileNotFound
}
return nil, err return nil, err
} }
} }
+37 -26
View File
@@ -178,7 +178,7 @@ func NewFSObjectLayer(fsPath string) (ObjectLayer, error) {
// or cause changes on backend format. // or cause changes on backend format.
fs.fsFormatRlk = rlk fs.fsFormatRlk = rlk
go fs.cleanupStaleMultipartUploads(ctx, GlobalMultipartCleanupInterval, GlobalMultipartExpiry) go fs.cleanupStaleUploads(ctx, GlobalStaleUploadsCleanupInterval, GlobalStaleUploadsExpiry)
go intDataUpdateTracker.start(ctx, fsPath) go intDataUpdateTracker.start(ctx, fsPath)
// Return successfully initialized object layer. // Return successfully initialized object layer.
@@ -502,13 +502,13 @@ func (fs *FSObjects) ListBuckets(ctx context.Context) ([]BucketInfo, error) {
atomic.AddInt64(&fs.activeIOCount, -1) atomic.AddInt64(&fs.activeIOCount, -1)
}() }()
var bucketInfos []BucketInfo
entries, err := readDir(fs.fsPath) entries, err := readDir(fs.fsPath)
if err != nil { if err != nil {
logger.LogIf(ctx, errDiskNotFound) logger.LogIf(ctx, errDiskNotFound)
return nil, toObjectErr(errDiskNotFound) return nil, toObjectErr(errDiskNotFound)
} }
bucketInfos := make([]BucketInfo, 0, len(entries))
for _, entry := range entries { for _, entry := range entries {
// Ignore all reserved bucket names and invalid bucket names. // Ignore all reserved bucket names and invalid bucket names.
if isReservedOrInvalidBucket(entry, false) { if isReservedOrInvalidBucket(entry, false) {
@@ -622,8 +622,11 @@ func (fs *FSObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBu
fsMetaPath := pathJoin(fs.fsPath, minioMetaBucket, bucketMetaPrefix, srcBucket, srcObject, fs.metaJSONFile) fsMetaPath := pathJoin(fs.fsPath, minioMetaBucket, bucketMetaPrefix, srcBucket, srcObject, fs.metaJSONFile)
wlk, err := fs.rwPool.Write(fsMetaPath) wlk, err := fs.rwPool.Write(fsMetaPath)
if err != nil { if err != nil {
logger.LogIf(ctx, err) wlk, err = fs.rwPool.Create(fsMetaPath)
return oi, toObjectErr(err, srcBucket, srcObject) if err != nil {
logger.LogIf(ctx, err)
return oi, toObjectErr(err, srcBucket, srcObject)
}
} }
// This close will allow for locks to be synchronized on `fs.json`. // This close will allow for locks to be synchronized on `fs.json`.
defer wlk.Close() defer wlk.Close()
@@ -635,7 +638,7 @@ func (fs *FSObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBu
fsMeta = fs.defaultFsJSON(srcObject) fsMeta = fs.defaultFsJSON(srcObject)
} }
fsMeta.Meta = srcInfo.UserDefined fsMeta.Meta = cloneMSS(srcInfo.UserDefined)
fsMeta.Meta["etag"] = srcInfo.ETag fsMeta.Meta["etag"] = srcInfo.ETag
if _, err = fsMeta.WriteTo(wlk); err != nil { if _, err = fsMeta.WriteTo(wlk); err != nil {
return oi, toObjectErr(err, srcBucket, srcObject) return oi, toObjectErr(err, srcBucket, srcObject)
@@ -885,10 +888,10 @@ func (fs *FSObjects) getObject(ctx context.Context, bucket, object string, offse
// Create a new fs.json file, if the existing one is corrupt. Should happen very rarely. // Create a new fs.json file, if the existing one is corrupt. Should happen very rarely.
func (fs *FSObjects) createFsJSON(object, fsMetaPath string) error { func (fs *FSObjects) createFsJSON(object, fsMetaPath string) error {
fsMeta := newFSMetaV1() fsMeta := newFSMetaV1()
fsMeta.Meta = make(map[string]string) fsMeta.Meta = map[string]string{
fsMeta.Meta["etag"] = GenETag() "etag": GenETag(),
contentType := mimedb.TypeByExtension(path.Ext(object)) "content-type": mimedb.TypeByExtension(path.Ext(object)),
fsMeta.Meta["content-type"] = contentType }
wlk, werr := fs.rwPool.Create(fsMetaPath) wlk, werr := fs.rwPool.Create(fsMetaPath)
if werr == nil { if werr == nil {
_, err := fsMeta.WriteTo(wlk) _, err := fsMeta.WriteTo(wlk)
@@ -901,10 +904,10 @@ func (fs *FSObjects) createFsJSON(object, fsMetaPath string) error {
// Used to return default etag values when a pre-existing object's meta data is queried. // Used to return default etag values when a pre-existing object's meta data is queried.
func (fs *FSObjects) defaultFsJSON(object string) fsMetaV1 { func (fs *FSObjects) defaultFsJSON(object string) fsMetaV1 {
fsMeta := newFSMetaV1() fsMeta := newFSMetaV1()
fsMeta.Meta = make(map[string]string) fsMeta.Meta = map[string]string{
fsMeta.Meta["etag"] = defaultEtag "etag": defaultEtag,
contentType := mimedb.TypeByExtension(path.Ext(object)) "content-type": mimedb.TypeByExtension(path.Ext(object)),
fsMeta.Meta["content-type"] = contentType }
return fsMeta return fsMeta
} }
@@ -1121,10 +1124,7 @@ func (fs *FSObjects) putObject(ctx context.Context, bucket string, object string
data := r.Reader data := r.Reader
// No metadata is set, allocate a new one. // No metadata is set, allocate a new one.
meta := make(map[string]string) meta := cloneMSS(opts.UserDefined)
for k, v := range opts.UserDefined {
meta[k] = v
}
var err error var err error
// Validate if bucket name is valid and exists. // Validate if bucket name is valid and exists.
@@ -1168,18 +1168,26 @@ func (fs *FSObjects) putObject(ctx context.Context, bucket string, object string
var wlk *lock.LockedFile var wlk *lock.LockedFile
if bucket != minioMetaBucket { if bucket != minioMetaBucket {
bucketMetaDir := pathJoin(fs.fsPath, minioMetaBucket, bucketMetaPrefix) bucketMetaDir := pathJoin(fs.fsPath, minioMetaBucket, bucketMetaPrefix)
fsMetaPath := pathJoin(bucketMetaDir, bucket, object, fs.metaJSONFile) fsMetaPath := pathJoin(bucketMetaDir, bucket, object, fs.metaJSONFile)
wlk, err = fs.rwPool.Create(fsMetaPath) wlk, err = fs.rwPool.Write(fsMetaPath)
var freshFile bool
if err != nil { if err != nil {
logger.LogIf(ctx, err) wlk, err = fs.rwPool.Create(fsMetaPath)
return ObjectInfo{}, toObjectErr(err, bucket, object) if err != nil {
logger.LogIf(ctx, err)
return ObjectInfo{}, toObjectErr(err, bucket, object)
}
freshFile = true
} }
// This close will allow for locks to be synchronized on `fs.json`. // This close will allow for locks to be synchronized on `fs.json`.
defer wlk.Close() defer wlk.Close()
defer func() { defer func() {
// Remove meta file when PutObject encounters any error // Remove meta file when PutObject encounters
if retErr != nil { // any error and it is a fresh file.
//
// We should preserve the `fs.json` of any
// existing object
if retErr != nil && freshFile {
tmpDir := pathJoin(fs.fsPath, minioMetaTmpBucket, fs.fsUUID) tmpDir := pathJoin(fs.fsPath, minioMetaTmpBucket, fs.fsUUID)
fsRemoveMeta(ctx, bucketMetaDir, fsMetaPath, tmpDir) fsRemoveMeta(ctx, bucketMetaDir, fsMetaPath, tmpDir)
} }
@@ -1214,7 +1222,7 @@ func (fs *FSObjects) putObject(ctx context.Context, bucket string, object string
// Should return IncompleteBody{} error when reader has fewer // Should return IncompleteBody{} error when reader has fewer
// bytes than specified in request header. // bytes than specified in request header.
if bytesWritten < data.Size() { if bytesWritten < data.Size() {
return ObjectInfo{}, IncompleteBody{} return ObjectInfo{}, IncompleteBody{Bucket: bucket, Object: object}
} }
// Entire object was written to the temp location, now it's safe to rename it to the actual location. // Entire object was written to the temp location, now it's safe to rename it to the actual location.
@@ -1494,8 +1502,11 @@ func (fs *FSObjects) PutObjectTags(ctx context.Context, bucket, object string, t
fsMeta := fsMetaV1{} fsMeta := fsMetaV1{}
wlk, err := fs.rwPool.Write(fsMetaPath) wlk, err := fs.rwPool.Write(fsMetaPath)
if err != nil { if err != nil {
logger.LogIf(ctx, err) wlk, err = fs.rwPool.Create(fsMetaPath)
return toObjectErr(err, bucket, object) if err != nil {
logger.LogIf(ctx, err)
return toObjectErr(err, bucket, object)
}
} }
// This close will allow for locks to be synchronized on `fs.json`. // This close will allow for locks to be synchronized on `fs.json`.
defer wlk.Close() defer wlk.Close()
+13 -7
View File
@@ -57,7 +57,7 @@ var (
// FromMinioClientMetadata converts minio metadata to map[string]string // FromMinioClientMetadata converts minio metadata to map[string]string
func FromMinioClientMetadata(metadata map[string][]string) map[string]string { func FromMinioClientMetadata(metadata map[string][]string) map[string]string {
mm := map[string]string{} mm := make(map[string]string, len(metadata))
for k, v := range metadata { for k, v := range metadata {
mm[http.CanonicalHeaderKey(k)] = v[0] mm[http.CanonicalHeaderKey(k)] = v[0]
} }
@@ -227,7 +227,7 @@ func ToMinioClientObjectInfoMetadata(metadata map[string]string) map[string][]st
// ToMinioClientMetadata converts metadata to map[string]string // ToMinioClientMetadata converts metadata to map[string]string
func ToMinioClientMetadata(metadata map[string]string) map[string]string { func ToMinioClientMetadata(metadata map[string]string) map[string]string {
mm := make(map[string]string) mm := make(map[string]string, len(metadata))
for k, v := range metadata { for k, v := range metadata {
mm[http.CanonicalHeaderKey(k)] = v mm[http.CanonicalHeaderKey(k)] = v
} }
@@ -349,16 +349,22 @@ func ComputeCompleteMultipartMD5(parts []CompletePart) string {
// parse gateway sse env variable // parse gateway sse env variable
func parseGatewaySSE(s string) (gatewaySSE, error) { func parseGatewaySSE(s string) (gatewaySSE, error) {
l := strings.Split(s, ";") l := strings.Split(s, ";")
var gwSlice = make([]string, 0) var gwSlice gatewaySSE
for _, val := range l { for _, val := range l {
v := strings.ToUpper(val) v := strings.ToUpper(val)
if v == gatewaySSES3 || v == gatewaySSEC { switch v {
case "":
continue
case gatewaySSES3:
fallthrough
case gatewaySSEC:
gwSlice = append(gwSlice, v) gwSlice = append(gwSlice, v)
continue continue
default:
return nil, config.ErrInvalidGWSSEValue(nil).Msg("gateway SSE cannot be (%s) ", v)
} }
return nil, config.ErrInvalidGWSSEValue(nil).Msg("gateway SSE cannot be (%s) ", v)
} }
return gatewaySSE(gwSlice), nil return gwSlice, nil
} }
// handle gateway env vars // handle gateway env vars
@@ -372,7 +378,7 @@ func gatewayHandleEnvVars() {
} }
gwsseVal := env.Get("MINIO_GATEWAY_SSE", "") gwsseVal := env.Get("MINIO_GATEWAY_SSE", "")
if len(gwsseVal) != 0 { if gwsseVal != "" {
var err error var err error
GlobalGatewaySSE, err = parseGatewaySSE(gwsseVal) GlobalGatewaySSE, err = parseGatewaySSE(gwsseVal)
if err != nil { if err != nil {
+5 -8
View File
@@ -168,6 +168,10 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
cli.ShowCommandHelpAndExit(ctx, gatewayName, 1) cli.ShowCommandHelpAndExit(ctx, gatewayName, 1)
} }
// Initialize globalConsoleSys system
globalConsoleSys = NewConsoleLogger(GlobalContext)
logger.AddTarget(globalConsoleSys)
// Handle common command args. // Handle common command args.
handleCommonCmdArgs(ctx) handleCommonCmdArgs(ctx)
@@ -261,12 +265,8 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
logger.FatalIf(registerWebRouter(router), "Unable to configure web browser") logger.FatalIf(registerWebRouter(router), "Unable to configure web browser")
} }
// Currently only NAS and S3 gateway support encryption headers.
encryptionEnabled := gatewayName == S3BackendGateway || gatewayName == NASBackendGateway
allowSSEKMS := gatewayName == S3BackendGateway // Only S3 can support SSE-KMS (as pass-through)
// Add API router. // Add API router.
registerAPIRouter(router, encryptionEnabled, allowSSEKMS) registerAPIRouter(router)
// Use all the middlewares // Use all the middlewares
router.Use(registerMiddlewares) router.Use(registerMiddlewares)
@@ -293,9 +293,6 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
newObject, err := gw.NewGatewayLayer(globalActiveCred) newObject, err := gw.NewGatewayLayer(globalActiveCred)
if err != nil { if err != nil {
// Stop watching for any certificate changes.
globalTLSCerts.Stop()
globalHTTPServer.Shutdown() globalHTTPServer.Shutdown()
logger.FatalIf(err, "Unable to initialize gateway backend") logger.FatalIf(err, "Unable to initialize gateway backend")
} }
+1 -1
View File
@@ -91,7 +91,7 @@ func (a GatewayUnsupported) ListObjectParts(ctx context.Context, bucket string,
} }
// AbortMultipartUpload aborts a ongoing multipart upload // AbortMultipartUpload aborts a ongoing multipart upload
func (a GatewayUnsupported) AbortMultipartUpload(ctx context.Context, bucket string, object string, uploadID string) error { func (a GatewayUnsupported) AbortMultipartUpload(ctx context.Context, bucket string, object string, uploadID string, opts ObjectOptions) error {
return NotImplemented{} return NotImplemented{}
} }
+22 -11
View File
@@ -70,7 +70,6 @@ const (
azureChunkSizeEnvVar = "MINIO_AZURE_CHUNK_SIZE_MB" azureChunkSizeEnvVar = "MINIO_AZURE_CHUNK_SIZE_MB"
azureDownloadRetryAttempts = 5 azureDownloadRetryAttempts = 5
azureBlockSize = 100 * humanize.MiByte
azureS3MinPartSize = 5 * humanize.MiByte azureS3MinPartSize = 5 * humanize.MiByte
metadataObjectNameTemplate = minio.GatewayMinioSysTmp + "multipart/v1/%s.%x/azure.json" metadataObjectNameTemplate = minio.GatewayMinioSysTmp + "multipart/v1/%s.%x/azure.json"
azureMarkerPrefix = "{minio}" azureMarkerPrefix = "{minio}"
@@ -473,6 +472,11 @@ func azureCodesToObjectError(err error, serviceCode string, statusCode int, buck
err = minio.UnsupportedMetadata{} err = minio.UnsupportedMetadata{}
case "BlobAccessTierNotSupportedForAccountType": case "BlobAccessTierNotSupportedForAccountType":
err = minio.NotImplemented{} err = minio.NotImplemented{}
case "OutOfRangeInput":
err = minio.ObjectNameInvalid{
Bucket: bucket,
Object: object,
}
default: default:
switch statusCode { switch statusCode {
case http.StatusNotFound: case http.StatusNotFound:
@@ -875,20 +879,15 @@ func (a *azureObjects) GetObjectInfo(ctx context.Context, bucket, object string,
// uses Azure equivalent `UploadStreamToBlockBlob`. // uses Azure equivalent `UploadStreamToBlockBlob`.
func (a *azureObjects) PutObject(ctx context.Context, bucket, object string, r *minio.PutObjReader, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) { func (a *azureObjects) PutObject(ctx context.Context, bucket, object string, r *minio.PutObjReader, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
data := r.Reader data := r.Reader
if opts.UserDefined == nil {
if data.Size() > azureBlockSize/2 { opts.UserDefined = map[string]string{}
if len(opts.UserDefined) == 0 {
opts.UserDefined = map[string]string{}
}
// Save md5sum for future processing on the object.
opts.UserDefined["x-amz-meta-md5sum"] = r.MD5CurrentHexString()
} }
metadata, properties, err := s3MetaToAzureProperties(ctx, opts.UserDefined) metadata, properties, err := s3MetaToAzureProperties(ctx, opts.UserDefined)
if err != nil { if err != nil {
return objInfo, azureToObjectError(err, bucket, object) return objInfo, azureToObjectError(err, bucket, object)
} }
blobURL := a.client.NewContainerURL(bucket).NewBlockBlobURL(object) blobURL := a.client.NewContainerURL(bucket).NewBlockBlobURL(object)
_, err = azblob.UploadStreamToBlockBlob(ctx, data, blobURL, azblob.UploadStreamToBlockBlobOptions{ _, err = azblob.UploadStreamToBlockBlob(ctx, data, blobURL, azblob.UploadStreamToBlockBlobOptions{
@@ -900,7 +899,18 @@ func (a *azureObjects) PutObject(ctx context.Context, bucket, object string, r *
if err != nil { if err != nil {
return objInfo, azureToObjectError(err, bucket, object) return objInfo, azureToObjectError(err, bucket, object)
} }
// Query the blob's properties and metadata
get, err := blobURL.GetProperties(ctx, azblob.BlobAccessConditions{})
if err != nil {
return objInfo, azureToObjectError(err, bucket, object)
}
// Update the blob's metadata with Content-MD5 after the upload
metadata = get.NewMetadata()
metadata["md5sum"] = r.MD5CurrentHexString()
_, err = blobURL.SetMetadata(ctx, metadata, azblob.BlobAccessConditions{})
if err != nil {
return objInfo, azureToObjectError(err, bucket, object)
}
return a.GetObjectInfo(ctx, bucket, object, opts) return a.GetObjectInfo(ctx, bucket, object, opts)
} }
@@ -924,6 +934,7 @@ func (a *azureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, des
return objInfo, azureToObjectError(err, srcBucket, srcObject) return objInfo, azureToObjectError(err, srcBucket, srcObject)
} }
props.ContentMD5 = srcProps.ContentMD5() props.ContentMD5 = srcProps.ContentMD5()
azureMeta["md5sum"] = srcInfo.ETag
res, err := destBlob.StartCopyFromURL(ctx, srcBlobURL, azureMeta, azblob.ModifiedAccessConditions{}, azblob.BlobAccessConditions{}) res, err := destBlob.StartCopyFromURL(ctx, srcBlobURL, azureMeta, azblob.ModifiedAccessConditions{}, azblob.BlobAccessConditions{})
if err != nil { if err != nil {
return objInfo, azureToObjectError(err, srcBucket, srcObject) return objInfo, azureToObjectError(err, srcBucket, srcObject)
@@ -1233,7 +1244,7 @@ func (a *azureObjects) ListObjectParts(ctx context.Context, bucket, object, uplo
// AbortMultipartUpload - Not Implemented. // AbortMultipartUpload - Not Implemented.
// There is no corresponding API in azure to abort an incomplete upload. The uncommmitted blocks // There is no corresponding API in azure to abort an incomplete upload. The uncommmitted blocks
// gets deleted after one week. // gets deleted after one week.
func (a *azureObjects) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string) (err error) { func (a *azureObjects) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string, opts minio.ObjectOptions) (err error) {
if err = a.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil { if err = a.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil {
return err return err
} }
+1 -1
View File
@@ -1253,7 +1253,7 @@ func (l *gcsGateway) cleanupMultipartUpload(ctx context.Context, bucket, key, up
} }
// AbortMultipartUpload aborts a ongoing multipart upload // AbortMultipartUpload aborts a ongoing multipart upload
func (l *gcsGateway) AbortMultipartUpload(ctx context.Context, bucket string, key string, uploadID string) error { func (l *gcsGateway) AbortMultipartUpload(ctx context.Context, bucket string, key string, uploadID string, opts minio.ObjectOptions) error {
if err := l.checkUploadIDExists(ctx, bucket, key, uploadID); err != nil { if err := l.checkUploadIDExists(ctx, bucket, key, uploadID); err != nil {
return err return err
} }
+1 -1
View File
@@ -859,7 +859,7 @@ func (n *hdfsObjects) CompleteMultipartUpload(ctx context.Context, bucket, objec
}, nil }, nil
} }
func (n *hdfsObjects) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string) (err error) { func (n *hdfsObjects) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string, opts minio.ObjectOptions) (err error) {
_, err = n.clnt.Stat(n.hdfsPathJoin(bucket)) _, err = n.clnt.Stat(n.hdfsPathJoin(bucket))
if err != nil { if err != nil {
return hdfsToObjectErr(ctx, err, bucket) return hdfsToObjectErr(ctx, err, bucket)

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