Compare commits

...

243 Commits

Author SHA1 Message Date
Harshavardhana 0bcb1b679d fix: disallow update if dates are same (#10890)
fixes #10889
2020-11-12 14:18:59 -08:00
Klaus Post a3017c724e Sort directory objects correctly (#10886)
Decode dir objects when listing and sort them correctly.
2020-11-12 13:09:34 -08:00
Omar Alvarez 07859ef48b docs: clarify notifications support for gateways (#10729)
Co-authored-by: Harshavardhana <harsha@minio.io>
2020-11-12 12:19:04 -08:00
Harshavardhana 267d7bf0a9 Revert "Add delete marker replication support (#10396)"
This reverts commit 50c10a5087.

PR is moved to origin/dev branch
2020-11-12 11:43:14 -08:00
cksac be83dfc52a fix: HDFS list bucket when subpath is provided (#10884) 2020-11-12 11:26:51 -08:00
Harshavardhana ca88ca753c ignore typed errors correctly in list cache layer (#10879)
bonus write bucket metadata cache with enough quorum

Possible fix for #10868
2020-11-12 09:28:56 -08:00
Klaus Post f86d3538f6 Allow deeper sleep (#10883)
Allow each crawler operation to sleep up to 10 seconds on very heavily loaded systems.

This will of course make minimum crawler speed less, but should be more effective at stopping.
2020-11-12 09:17:56 -08:00
Klaus Post 1c3590078d Skip 0 byte stream writes (#10875)
Don't send a packet when receiving 0 bytes or there is an error recorded
2020-11-11 18:07:40 -08:00
Harshavardhana aa158228f9 fix: simplify healing metadata objects per set (#10867) 2020-11-11 10:58:16 -08:00
Klaus Post 8747834c69 DeletedObjects: Return objects on lock failure (#10874)
Return objects when locking fails.

<details>
<summary>Panic</summary>

```
: 2020/11/10 04:15:55 http: panic serving 10.10.62.153:44858: runtime error: index out of range [0] with length 0
: goroutine 363537270 [running]:
: net/http.(*conn).serve.func1(0xc019232780)
:         net/http/server.go:1801 +0x147
: panic(0x1cadd60, 0xc001719260)
:         runtime/panic.go:975 +0x47a
: github.com/minio/minio/cmd.criticalErrorHandler.ServeHTTP.func1(0xc0121d1200, 0x210cda0, 0xc0141940e0)
:         github.com/minio/minio/cmd/generic-handlers.go:781 +0x1a8
: panic(0x1cadd60, 0xc001719260)
:         runtime/panic.go:969 +0x1b9
: github.com/minio/minio/cmd.objectAPIHandlers.DeleteMultipleObjectsHandler(0x1e71ce8, 0x1e71cc8, 0x2108420, 0xc0192328c0, 0xc0121d1400)
:         github.com/minio/minio/cmd/bucket-handlers.go:465 +0x2490
: net/http.HandlerFunc.ServeHTTP(...)
:         net/http/server.go:2042
: github.com/minio/minio/cmd.httpTraceAll.func1(0x2108420, 0xc0192328c0, 0xc0121d1400)
:         github.com/minio/minio/cmd/handler-utils.go:353 +0x158
: net/http.HandlerFunc.ServeHTTP(...)
:         net/http/server.go:2042
: github.com/minio/minio/cmd.collectAPIStats.func1(0x2108420, 0xc019232820, 0xc0121d1400)
:         github.com/minio/minio/cmd/handler-utils.go:380 +0xed
: net/http.HandlerFunc.ServeHTTP(...)
:         net/http/server.go:2042
: github.com/minio/minio/cmd.maxClients.func1(0x2108420, 0xc019232820, 0xc0121d1400)
:         github.com/minio/minio/cmd/handler-api.go:132 +0x33b
: net/http.HandlerFunc.ServeHTTP(0xc00271d590, 0x2108420, 0xc019232820, 0xc0121d1400)
:         net/http/server.go:2042 +0x44
: github.com/minio/minio/cmd.redirectHandler.ServeHTTP(0x20e2180, 0xc00271d590, 0x2108420, 0xc019232820, 0xc0121d1400)
:         github.com/minio/minio/cmd/generic-handlers.go:192 +0x156
: github.com/minio/minio/cmd.customHeaderHandler.ServeHTTP(0x20e1060, 0xc0141a22b0, 0x21083e0, 0xc01814d2e0, 0xc0121d1400)
:         github.com/minio/minio/cmd/generic-handlers.go:751 +0x162
: github.com/minio/minio/cmd.securityHeaderHandler.ServeHTTP(0x20e0fc0, 0xc0141a22c0, 0x21083e0, 0xc01814d2e0, 0xc0121d1400)
:         github.com/minio/minio/cmd/generic-handlers.go:766 +0x1d6
: github.com/minio/minio/cmd.bucketForwardingHandler.ServeHTTP(0xc0121c7a40, 0x20e1120, 0xc0141a22d0, 0x21083e0, 0xc01814d2e0, 0xc0121d1400)
:         github.com/minio/minio/cmd/generic-handlers.go:624 +0xbf
: github.com/minio/minio/cmd.requestValidityHandler.ServeHTTP(0x20e0f20, 0xc01814d280, 0x21083e0, 0xc01814d2e0, 0xc0121d1400)
:         github.com/minio/minio/cmd/generic-handlers.go:608 +0x42a
: github.com/minio/minio/cmd.httpStatsHandler.ServeHTTP(0x20e10c0, 0xc0141a2300, 0x210cda0, 0xc0141940e0, 0xc0121d1400)
:         github.com/minio/minio/cmd/generic-handlers.go:536 +0xe4
: github.com/minio/minio/cmd.requestSizeLimitHandler.ServeHTTP(0x20e0fe0, 0xc0141a2310, 0x50004000000, 0x210cda0, 0xc0141940e0, 0xc0121d1400)
:         github.com/minio/minio/cmd/generic-handlers.go:68 +0xd4
: github.com/minio/minio/cmd.requestHeaderSizeLimitHandler.ServeHTTP(0x20e10a0, 0xc01814d2a0, 0x210cda0, 0xc0141940e0, 0xc0121d1400)
:         github.com/minio/minio/cmd/generic-handlers.go:93 +0x1b7
: github.com/minio/minio/cmd.crossDomainPolicy.ServeHTTP(0x20e1080, 0xc0141a2320, 0x210cda0, 0xc0141940e0, 0xc0121d1400)
:         github.com/minio/minio/cmd/crossdomain-xml-handler.go:51 +0x82
: github.com/minio/minio/cmd.browserRedirectHandler.ServeHTTP(0x20e0fa0, 0xc0141a2330, 0x210cda0, 0xc0141940e0, 0xc0121d1400)
:         github.com/minio/minio/cmd/generic-handlers.go:276 +0x68
: github.com/minio/minio/cmd.minioReservedBucketHandler.ServeHTTP(0x20e0f00, 0xc0141a2340, 0x210cda0, 0xc0141940e0, 0xc0121d1400)
:         github.com/minio/minio/cmd/generic-handlers.go:344 +0xb8
: github.com/minio/minio/cmd.cacheControlHandler.ServeHTTP(0x20e1020, 0xc0141a2350, 0x210cda0, 0xc0141940e0, 0xc0121d1400)
:         github.com/minio/minio/cmd/generic-handlers.go:303 +0x1ce
: github.com/minio/minio/cmd.timeValidityHandler.ServeHTTP(0x20e0f40, 0xc0141a2360, 0x210cda0, 0xc0141940e0, 0xc0121d1400)
:         github.com/minio/minio/cmd/generic-handlers.go:414 +0x3ca
: github.com/minio/minio/cmd.resourceHandler.ServeHTTP(0x20e1160, 0xc0141a2370, 0x210cda0, 0xc0141940e0, 0xc0121d1400)
:         github.com/minio/minio/cmd/generic-handlers.go:516 +0xab
: github.com/minio/minio/cmd.authHandler.ServeHTTP(0x20e1100, 0xc0141a2380, 0x210cda0, 0xc0141940e0, 0xc0121d1400)
:         github.com/minio/minio/cmd/auth-handler.go:502 +0x2e7
: github.com/minio/minio/cmd.sseTLSHandler.ServeHTTP(0x20e0ee0, 0xc0141a2390, 0x210cda0, 0xc0141940e0, 0xc0121d1400)
:         github.com/minio/minio/cmd/generic-handlers.go:802 +0x79
: github.com/minio/minio/cmd.reservedMetadataHandler.ServeHTTP(0x20e1140, 0xc0141a23a0, 0x210cda0, 0xc0141940e0, 0xc0121d1400)
:         github.com/minio/minio/cmd/generic-handlers.go:139 +0x1b7
: github.com/gorilla/mux.(*Router).ServeHTTP(0xc00073fb00, 0x210cda0, 0xc0141940e0, 0xc0121d1200)
:         github.com/gorilla/mux@v1.8.0/mux.go:210 +0xd3
: github.com/rs/cors.(*Cors).Handler.func1(0x210cda0, 0xc0141940e0, 0xc0121d1200)
:         github.com/rs/cors@v1.7.0/cors.go:219 +0x1b9
: net/http.HandlerFunc.ServeHTTP(0xc0009aece0, 0x210cda0, 0xc0141940e0, 0xc0121d1200)
:         net/http/server.go:2042 +0x44
: github.com/minio/minio/cmd.criticalErrorHandler.ServeHTTP(0x20e2180, 0xc0009aece0, 0x210cda0, 0xc0141940e0, 0xc0121d1200)
:         github.com/minio/minio/cmd/generic-handlers.go:784 +0x85
: github.com/minio/minio/cmd/http.(*Server).Start.func1(0x210cda0, 0xc0141940e0, 0xc0121d1200)
:         github.com/minio/minio/cmd/http/server.go:101 +0x258
: net/http.HandlerFunc.ServeHTTP(0xc000dc4080, 0x210cda0, 0xc0141940e0, 0xc0121d1200)
:         net/http/server.go:2042 +0x44
: net/http.serverHandler.ServeHTTP(0xc000764c60, 0x210cda0, 0xc0141940e0, 0xc0121d1200)
:         net/http/server.go:2843 +0xa3
: net/http.(*conn).serve(0xc019232780, 0x2114720, 0xc03381f6c0)
:         net/http/server.go:1925 +0x8ad
: created by net/http.(*Server).Serve
:         net/http/server.go:2969 +0x36c
```
</details>
2020-11-11 09:14:32 -08:00
Anton Melser 2c1e37197b fix: bad example json for policy in replication docs (#10869) 2020-11-10 17:49:49 -08:00
Poorna Krishnamoorthy 50c10a5087 Add delete marker replication support (#10396)
Delete marker replication is implemented for V2
configuration specified in AWS spec (though AWS
allows it only in the V1 configuration).

This PR also brings in a MinIO only extension of
replicating permanent deletes, i.e. deletes specifying
version id are replicated to target cluster.
2020-11-10 15:24:14 -08:00
Minio Trusted 9f4ad873bc Update yaml files to latest version RELEASE.2020-11-10T21-02-24Z 2020-11-10 21:18:35 +00:00
Steven Reitsma 4683a623dc fix: negative STS IAM token TTL value (#10866) 2020-11-10 12:24:01 -08:00
Klaus Post 06899210a7 Reduce health check output (#10859)
This will make the health check clients 'silent'.
Use `IsNetworkOrHostDown` determine if network is ok so it mimics the functionality in the actual client.
2020-11-10 09:28:23 -08:00
Harshavardhana cbdab62c1e fix: heal user/metadata right away upon server startup (#10863)
this is needed such that we make sure to heal the
users, policies and bucket metadata right away as
we do listing based on list cache which only lists
'3' sufficiently good drives, to avoid possibly
losing access to these users upon upgrade make
sure to heal them.
2020-11-10 09:02:06 -08:00
Harshavardhana 8df6112204 fix: avoid divide by zero error single node distributed setup (#10862) 2020-11-09 20:40:39 -08:00
Anis Elleuch 8e8ddf7233 doc: Add definition of 1KB and 1MB in prometheus (#10857) 2020-11-09 10:05:01 -08:00
Klaus Post 311ab43d4c Upgrade msgp to official release (#10858)
Code identical, but official release.
2020-11-09 07:52:24 -08:00
Harshavardhana 97692bc772 re-route requests if IAM is not initialized (#10850) 2020-11-07 21:03:06 -08:00
Minio Trusted 21016265e5 Update yaml files to latest version RELEASE.2020-11-06T23-17-07Z 2020-11-06 23:34:19 +00:00
Steven Reitsma 54120107ce fix: infinite loop in cleanupStaleUploads of encrypted MPUs (#10845)
fixes #10588
2020-11-06 11:53:42 -08:00
Klaus Post 9bf5990ea9 metadata: Invalidate cache if unreadable and not updating (#10844)
If a scanning server shuts down unexpectedly we may have "successful" caches that are incomplete on a set.

In this case mark the cache with an error so it will no longer be handed out.
2020-11-06 08:54:09 -08:00
Steven Reitsma 74f7cf24ae fix: s3 gateway SSE pagination (#10840)
Fixes #10838
2020-11-05 15:04:03 -08:00
Harshavardhana fb28aa847b fix: add missing deleted key element in multiObjectDelete (#10839)
fixes #10832
2020-11-05 12:47:46 -08:00
Klaus Post 0724205f35 metacache: Add option for life extension (#10837)
Add `MINIO_API_EXTEND_LIST_CACHE_LIFE` that will extend 
the life of generated caches for a while.

This changes caches to remain valid until no updates have been 
received for the specified time plus a fixed margin.

This also changes the caches from being invalidated when the *first* 
set finishes until the *last* set has finished plus the specified time 
has passed.
2020-11-05 11:49:56 -08:00
Harshavardhana b72cac4cf3 fix: dangling objects on actual namespace (#10822) 2020-11-05 11:48:55 -08:00
Klaus Post 47d715f642 Update msgp to main branch (#10835)
The main branch has error unwrapping to clean up logs on canceled contexts, etc.

Asked for a new release: https://github.com/tinylib/msgp/issues/282
2020-11-05 08:13:03 -08:00
Klaus Post bd77f29fc4 Don't replace caches that are receiving updates (#10834)
Keep caches while they are receiving updates.
Move update code to separate function.
2020-11-05 07:34:08 -08:00
Klaus Post d1e1205036 metacache: Always close the s2 writer (#10836)
The s2 writer could be leaked if there was an error.

Make sure it is always closed.
2020-11-05 07:30:14 -08:00
Harshavardhana 71753e21e0 add missing TTL for STS credentials on etcd (#10828) 2020-11-04 13:06:05 -08:00
Harshavardhana fde3299bf3 re-use optimized readdir for isDirEmpty() (#10829)
reduces effective memory usage by an order
of magnitude, also increases performance for
small objects
2020-11-04 13:05:21 -08:00
Harshavardhana 1a1f00fa15 fix: use internode data for DisksInfo, VolsInfo in message pack (#10821)
Similar to #10775 for fewer memory allocations, since we use
getOnlineDisks() extensively for listing we should optimize it
further.

Additionally, remove all unused walkers from the storage layer
2020-11-04 10:10:54 -08:00
Bill Thorp 4a1efabda4 Context based AccessKey passing (#10615)
A new field called AccessKey is added to the ReqInfo struct and populated.
Because ReqInfo is added to the context, this allows the AccessKey to be
accessed from 3rd-party code, such as a custom ObjectLayer.

Co-authored-by: Harshavardhana <harsha@minio.io>
Co-authored-by: Kaloyan Raev <kaloyan@storj.io>
2020-11-04 09:13:34 -08:00
Klaus Post 3b88a646ec Add remote online/offline information (#10825)
Log information about remote clients being marked offline.

This will help to identify root causes of failures.
2020-11-04 08:27:32 -08:00
Klaus Post 2294e53a0b Don't retain context in locker (#10515)
Use the context for internal timeouts, but disconnect it from outgoing 
calls so we always receive the results and cancel it remotely.
2020-11-04 08:25:42 -08:00
Klaus Post f0819cce75 Keep transient lists while they are updating (#10826)
On extremely long running listings keep the transient list 15 minutes after last update instead of using start time.

Also don't do overlap checks on transient lists.
2020-11-04 08:01:33 -08:00
Klaus Post 1e11b4629f Add remote Diskinfo caching (#10824)
Add 1 second remote disk info cache.

Should decrease need for remote calls a great deal due to how actively it is used now.
2020-11-04 08:00:18 -08:00
Harshavardhana 5c72a34fa8 fix: honor delimiter as per AWS S3 spec (#10823) 2020-11-04 07:56:58 -08:00
Klaus Post b9277c8030 metacache: Add trashcan (#10820)
Add trashcan that keeps recently updated lists after bucket deletion.
All caches were deleted once a bucket was deleted, so caches still running would report errors. Now they are canceled.
Fix `.minio.sys` not being transient.
2020-11-03 12:47:52 -08:00
Harshavardhana 8c76e1353e initialize IAM after etcd has initialized (#10819) 2020-11-03 12:12:30 -08:00
Harshavardhana ad382799b1 use list cache for Walk() with webUI and quota (#10814)
bring list cache optimizations for web UI
object listing, also FIFO quota enforcement
through list cache as well.
2020-11-03 08:53:48 -08:00
Harshavardhana 68de5a6f6a fix: IAM store fallback to list users and policies from disk (#10787)
Bonus fixes, remove package retry it is harder to get it
right, also manage context remove it such that we don't have
to rely on it anymore instead use a simple Jitter retry.
2020-11-02 17:52:13 -08:00
Harshavardhana 4ea31da889 fix: move list quorum ENV to config (#10804) 2020-11-02 17:21:56 -08:00
Klaus Post 0a796505c1 metacache: Check only one disk for updates (#10809)
Check only one disk for updates.

This will reduce IO while waiting for lists to finish.
2020-11-02 17:20:27 -08:00
Klaus Post 37749f4623 Optimize FileInfo(Version) transfer (#10775)
File Info decoding, in particular, is showing up as a major 
allocator and time consumer for internode data transfers

Switch to message pack for cross-server transfers:

```
MSGP:

Size: 945 bytes

BenchmarkEncodeFileInfoMsgp-32    	 1558444	       866 ns/op	   1.16 MB/s	       0 B/op	       0 allocs/op
BenchmarkDecodeFileInfoMsgp-32    	  479968	      2487 ns/op	   0.40 MB/s	     848 B/op	      18 allocs/op

GOB:

Size: 1409 bytes

BenchmarkEncodeFileInfoGOB-32    	  333339	      3237 ns/op	   0.31 MB/s	     576 B/op	      19 allocs/op
BenchmarkDecodeFileInfoGOB-32    	   20869	     57837 ns/op	   0.02 MB/s	   16439 B/op	     428 allocs/op
```
2020-11-02 17:07:52 -08:00
Klaus Post 86e0d272f3 Reduce WriteAll allocs (#10810)
WriteAll saw 127GB allocs in a 5 minute timeframe for 4MiB buffers 
used by `io.CopyBuffer` even if they are pooled.

Since all writers appear to write byte buffers, just send those 
instead and write directly. The files are opened through the `os` 
package so they have no special properties anyway.

This removes the alloc and copy for each operation.

REST sends content length so a precise alloc can be made.
2020-11-02 16:14:31 -08:00
Harshavardhana 8527f22df1 optimize request URL encoding for internode (#10811)
this reduces allocations in order of magnitude

Also, revert "erasure: delete dangling objects automatically (#10765)" 
affects list caching should be investigated.
2020-11-02 15:15:12 -08:00
Anis Elleuch b456292295 erasure: delete dangling objects automatically (#10765) 2020-11-02 10:49:30 -08:00
Poorna Krishnamoorthy 03fdbc3ec2 Add async caching commit option in diskcache (#10742)
Add store and a forward option for a single part
uploads when an async mode is enabled with env
MINIO_CACHE_COMMIT=writeback 

It defaults to `writethrough` if unspecified.
2020-11-02 10:00:45 -08:00
Harshavardhana 4c773f7068 re-use remote transports in Peer,Storage,Locker clients (#10788)
use one transport for internode communication
2020-11-02 07:43:11 -08:00
Venkata K Sadineni d8e07f2c41 go mod update golang.org/x/text to address a CVE (#10805) 2020-11-01 19:42:48 -08:00
Harshavardhana 5412d730c1 simplify monitoring doesn't need to be canceled (#10803)
connect disks monitoring doesn't need to be canceled
upon drive replacement, since we only need to replace
the newly replaced drive.
2020-10-31 14:10:12 -07:00
Klaus Post fe9f23e632 Recreate bucket metacache if corrupted (#10800)
If bucket metadata cannot be read, clean up existing and create a new.
2020-10-31 10:26:16 -07:00
Klaus Post 422898d9b3 Clean up metadata cache when deleting bucket (#10802)
Metadata caches were left behind when deleting a bucket.
2020-10-31 09:46:18 -07:00
Harshavardhana b686bb9c83 fix: replaced drive properly by healing the entire drive (#10799)
Bonus fixes, we do not need reload format anymore
as the replaced drive is healed locally we only need
to ensure that drive heal reloads the drive properly.

We preserve the UUID of the original order, this means
that the replacement in `format.json` doesn't mean that
the drive needs to be reloaded into memory anymore.

fixes #10791
2020-10-31 01:34:48 -07:00
Harshavardhana 5e5cdc581d remove unnecessary logging and move to log once (#10798)
the current master logs way too much when a node
is down, instead log once and move on.
2020-10-30 14:55:50 -07:00
Harshavardhana 02cfa774be allow requests to be proxied when server is booting up (#10790)
when server is booting up there is a possibility
that users might see '503' because object layer
when not initialized, then the request is proxied
to neighboring peers first one which is online.
2020-10-30 12:20:28 -07:00
Krishna Srinivas 3a2f89b3c0 fix: add support for O_DIRECT reads for erasure backends (#10718) 2020-10-30 11:04:29 -07:00
Klaus Post 6135f072d2 Fix invalidated metacaches (#10784)
* Fix caches having EOF marked as a failure.
* Simplify cache updates.
* Provide context for checkMetacacheState failures.
* Log 499 when the client disconnects.
2020-10-30 09:33:16 -07:00
kannappanr 7331659d3d obd: Remove unused log constants (#10778) 2020-10-29 13:00:30 -07:00
Klaus Post e63a44b734 rest client: Expect context timeouts for locks (#10782)
Add option for rest clients to not mark a remote offline for context timeouts.

This can be used if context timeouts are expected on the call.
2020-10-29 09:52:11 -07:00
Klaus Post 6b14c4ab1e Optimize decryptObjectInfo (#10726)
`decryptObjectInfo` is a significant bottleneck when listing objects.

Reduce the allocations for a significant speedup.

https://github.com/minio/sio/pull/40

```
λ benchcmp before.txt after.txt
benchmark                          old ns/op     new ns/op     delta
Benchmark_decryptObjectInfo-32     24260928      808656        -96.67%

benchmark                          old MB/s     new MB/s     speedup
Benchmark_decryptObjectInfo-32     0.04         1.24         31.00x

benchmark                          old allocs     new allocs     delta
Benchmark_decryptObjectInfo-32     75112          48996          -34.77%

benchmark                          old bytes     new bytes     delta
Benchmark_decryptObjectInfo-32     287694772     4228076       -98.53%
```
2020-10-29 09:34:20 -07:00
Harshavardhana 4bf90ca67f fix: handle a crash when AskDisks is set to -1 (#10777) 2020-10-29 09:25:43 -07:00
Harshavardhana e0655e24f2 fix: A possible crash when fi.Erasure.Distribution is empty (#10779) 2020-10-28 19:24:01 -07:00
Klaus Post bfc36aed89 Add update retry limit and compare error by string instead (#10776) 2020-10-28 13:19:53 -07:00
Kaloyan Raev be7f67268d fix: Do not cleanup range files in cache SaveMetadata when total hits are false (#10728) 2020-10-28 09:23:17 -07:00
Klaus Post a982baff27 ListObjects Metadata Caching (#10648)
Design: https://gist.github.com/klauspost/025c09b48ed4a1293c917cecfabdf21c

Gist of improvements:

* Cross-server caching and listing will use the same data across servers and requests.
* Lists can be arbitrarily resumed at a constant speed.
* Metadata for all files scanned is stored for streaming retrieval.
* The existing bloom filters controlled by the crawler is used for validating caches.
* Concurrent requests for the same data (or parts of it) will not spawn additional walkers.
* Listing a subdirectory of an existing recursive cache will use the cache.
* All listing operations are fully streamable so the number of objects in a bucket no 
  longer dictates the amount of memory.
* Listings can be handled by any server within the cluster.
* Caches are cleaned up when out of date or superseded by a more recent one.
2020-10-28 09:18:35 -07:00
Minio Trusted 51222cc664 Update yaml files to latest version RELEASE.2020-10-28T08-16-50Z 2020-10-28 08:33:22 +00:00
Krishna Srinivas f53c5a020e fix: heal object shards with ec.index and ec.distribution mismatches (#10773)
Co-authored-by: Harshavardhana <harsha@minio.io>
2020-10-28 00:10:20 -07:00
Harshavardhana 5b30bbda92 fix: add more protection distribution to match EcIndex (#10772)
allows for more stricter validation in picking up the right
set of disks for reconstruction.
2020-10-28 00:09:15 -07:00
Shireesh Anjal 858e2a43df Remove logging info from OBDInfoHandler (#10727)
A lot of logging data is counterproductive. A better implementation with
precise useful log data can be introduced later.
2020-10-27 17:41:48 -07:00
Kaloyan Raev df9894e275 avoid caching http ranges in background goroutine (#10724) 2020-10-26 23:04:48 -07:00
Minio Trusted ca77ee1c0e Update yaml files to latest version RELEASE.2020-10-27T04-03-55Z 2020-10-27 04:20:10 +00:00
Krishna Srinivas 592f2f23a3 fix: heal rejects objects with disk re-ordering issue (#10766) 2020-10-26 18:48:47 -07:00
Krishna Srinivas c49a80db41 fix: use meta.Erasure.Index for GetObject() to reconstruct object (#10764) 2020-10-26 16:19:42 -07:00
Poorna Krishnamoorthy 46275c6547 cache: rename function declarations (#10763) 2020-10-26 15:41:24 -07:00
Poorna Krishnamoorthy 0994ed9783 cache: fix call in GetObjectNInfo (#10762)
Fixes: #10751
2020-10-26 12:30:40 -07:00
Anis Elleuch eb95353cb1 fix: Get/HeadObject return 404 on non quorum objects (#10753) 2020-10-26 10:30:46 -07:00
Harshavardhana 029758cb20 fix: retain the previous UUID for newly replaced drives (#10759)
only newly replaced drives get the new `format.json`,
this avoids disks reloading their in-memory reference
format, ensures that drives are online without
reloading the in-memory reference format.

keeping reference format in-tact means UUIDs
never change once they are formatted.
2020-10-26 10:29:29 -07:00
Harshavardhana 649035677f fix: arm64 Dockerfile with missing \ for multiple ENVs
fixes #10758
2020-10-25 13:52:55 -07:00
Harshavardhana 646d6917ed turn-off checking for updates completely if MINIO_UPDATE=off (#10752) 2020-10-24 22:39:44 -07:00
Harshavardhana d9db7f3308 expire lockers if lockers are offline (#10749)
lockers currently might leave stale lockers,
in unknown ways waiting for downed lockers.

locker check interval is high enough to safely
cleanup stale locks.
2020-10-24 13:23:16 -07:00
Harshavardhana 6a8c62f9fd make sure to preserve UUID from reference format (#10748)
reference format should be source of truth
for inconsistent drives which reconnect,
add them back to their original position

remove automatic fix for existing offline
disk uuids
2020-10-24 13:23:08 -07:00
Harshavardhana 4442382c16 update STS examples to use latest v7 APIs 2020-10-24 11:16:59 -07:00
Anis Elleuch 00124c56d9 erasure: Commit data before xl.meta in RenameData() (#10734)
This will reduce the chance to have updated xl.meta without data.
2020-10-23 21:54:58 -07:00
Anis Elleuch 2c32c2149e tests: Avoid running TestNSRace in short test mode (#10735) 2020-10-23 21:23:12 -07:00
Harshavardhana 734f258878 fix: slow down auto healing more aggressively (#10730)
Bonus fixes

- logging improvements to ensure that we don't use
  `go logger.LogIf` to avoid runtime.Caller missing
  the function name. log where necessary.
- remove unused code at erasure sets
2020-10-22 13:36:24 -07:00
Anis Elleuch 0e0c53bba4 tests: Lower expectation in addr selection in rand cache dialer (#10739)
Test TestDialContextWithDNSCacheRand was failing sometimes because it depends
on a random selection of addresses when testing random DNS resolution from cache.

Lower addr selection exception to 10%
2020-10-22 09:35:32 -07:00
Poorna Krishnamoorthy 5cc23ae052 validate if iam store is initialized (#10719)
Fixes panic - regression from d6d770c1b1
2020-10-20 21:28:24 -07:00
Anis Elleuch 6fd088f448 docs: Update ILM doc with versioning features (#10714) 2020-10-20 09:23:27 -07:00
Harshavardhana d6d770c1b1 initialize object layer right after config has loaded 2020-10-19 22:04:59 -07:00
Harshavardhana b07df5cae1 initialize IAM as soon as object layer is initialized (#10700)
Allow requests to come in for users as soon as object
layer and config are initialized, this allows users
to be authenticated sooner and would succeed automatically
on servers which are yet to fully initialize.
2020-10-19 09:54:40 -07:00
Harshavardhana c107728676 fix: s3 gateway DNS cache initialization (#10706)
fixes #10705
2020-10-19 01:34:23 -07:00
Minio Trusted ba5215561f Update yaml files to latest version RELEASE.2020-10-18T21-54-12Z 2020-10-18 22:15:30 +00:00
Márk Sági-Kazár 4eb45c9a0f fix: dex getting started guide URL (#10701) 2020-10-17 16:47:28 -07:00
Harshavardhana 187129a907 fix comment in bucket bandwidth package 2020-10-17 00:38:54 -07:00
Anis Elleuch 284a2b9021 ilm: Send delete marker creation event when appropriate (#10696)
Before this commit, the crawler ILM will always send object delete event
notification though this is wrong.
2020-10-16 21:22:12 -07:00
Ritesh H Shukla 0b53e30ecb Clean up monitor on delete bucket (#10698) 2020-10-16 17:59:31 -07:00
Harshavardhana bd2131ba34 add DNS cache support to avoid DNS flooding (#10693)
Go stdlib resolver doesn't support caching DNS
resolutions, since we compile with CGO disabled
we are more probe to DNS flooding for all network
calls to resolve for DNS from the DNS server.

Under various containerized environments such as
VMWare this becomes a problem because there are
no DNS caches available and we may end up overloading
the kube-dns resolver under concurrent I/O.

To circumvent this issue implement a DNSCache resolver
which resolves DNS and caches them for around 10secs
with every 3sec invalidation attempted.
2020-10-16 14:49:05 -07:00
Ritesh H Shukla 73a41a725a Always close response body (#10697) 2020-10-16 12:40:36 -07:00
ebozduman 1aec168c84 fix: azure gateway should reject bucket names with "." (#10635) 2020-10-16 09:30:18 -07:00
Klaus Post 21a549a83b fix: keep MRF channel open to avoid random CI crash (#10686)
There doesn't seem to be any benefit to closing the channel, so just keep 
it open and let it die with the server.
2020-10-16 09:08:51 -07:00
Ritesh H Shukla 8a16a1a1a9 fix: misc fixes for bandwidth reporting amd monitoring (#10683)
* Set peer for fetch bandwidth
* Fix the limit for bandwidth that is reported.
* Reduce CPU burn from bandwidth management.
2020-10-16 09:07:50 -07:00
Harshavardhana ad726b49b4 rename zones to serverSets to avoid terminology conflict (#10679)
we are bringing in availability zones, we should avoid
zones as per server expansion concept.
2020-10-15 14:28:50 -07:00
Anis Elleuch db2241066b heal: Enable removing dangling delete markers (#10688) 2020-10-15 13:06:40 -07:00
Harshavardhana f1cc16e788 fix: background heal rely on getOnlineDisks() (#10687) 2020-10-15 13:06:23 -07:00
Klaus Post 3820a905e0 in getOnlineDisks wait for disks to be populated (#10685) 2020-10-15 06:37:10 -07:00
Harshavardhana 2042d4873c rename crawler config option to heal (#10678) 2020-10-14 13:51:51 -07:00
Harshavardhana f9be783f3e fix: allow crawler to crawl on disks without usage constraints (#10677)
additionally also change the resolution usage wise
return of disks, allows to small byte level differences
to be masked.
2020-10-14 12:12:10 -07:00
Harshavardhana 23773bb32b update NTP package for accurate time resolution fixes (#10670) 2020-10-14 12:29:20 +05:30
Harshavardhana 2fd7545b6c fix: move to go1.15 (#10660) 2020-10-13 18:33:18 -07:00
Harshavardhana 71b97fd3ac fix: connect disks pre-emptively during startup (#10669)
connect disks pre-emptively upon startup, to ensure we have
enough disks are connected at startup rather than wait
for them.

we need to do this to avoid long wait times for server to
be online when we have servers come up in rolling upgrade
fashion
2020-10-13 18:28:42 -07:00
Klaus Post 03991c5d41 crawler: Remove waitForLowActiveIO (#10667)
Only use dynamic delays for the crawler. Even though the max wait was 1 second the number 
of waits could severely impact crawler speed.

Instead of relying on a global metric, we use the stateless local delays to keep the crawler 
running at a speed more adjusted to current conditions.

The only case we keep it is before bitrot checks when enabled.
2020-10-13 13:45:08 -07:00
Harshavardhana 9c042a503b remove deprecate readiness from healthcheck docs (#10659) 2020-10-12 18:56:03 -07:00
飞雪无情 614060764d fix: use the correct Action type for policy.Args and iampolicy.Args (#10650) 2020-10-12 15:18:22 -07:00
Minio Trusted 4a678ad70f Update yaml files to latest version RELEASE.2020-10-12T21-53-21Z 2020-10-12 22:15:06 +00:00
Harshavardhana a3ba8188d7 fix: allow locker to be niladic 2020-10-12 14:23:44 -07:00
Harshavardhana 2760fc86af Bump default idleConnsPerHost to control conns in time_wait (#10653)
This PR fixes a hang which occurs quite commonly at higher concurrency
by allowing following changes

- allowing lower connections in time_wait allows faster socket open's
- lower idle connection timeout to ensure that we let kernel
  reclaim the time_wait connections quickly
- increase somaxconn to 4096 instead of 2048 to allow larger tcp
  syn backlogs.

fixes #10413
2020-10-12 14:19:46 -07:00
P R abb14aeec1 Header row seperator for console Table (#10651) 2020-10-12 11:32:43 -07:00
Ritesh H Shukla 8ceb2a93fd fix: peer replication bandwidth monitoring in distributed setup (#10652) 2020-10-12 09:04:55 -07:00
Ritesh H Shukla c2f16ee846 Add basic bandwidth monitoring for replication. (#10501)
This change tracks bandwidth for a bucket and object

- [x] Add Admin API
- [x] Add Peer API
- [x] Add BW throttling
- [x] Admin APIs to set replication limit
- [x] Admin APIs for fetch bandwidth
2020-10-09 20:36:00 -07:00
Minio Trusted 071c004f8b Update yaml files to latest version RELEASE.2020-10-09T22-55-05Z 2020-10-09 23:16:30 +00:00
Harshavardhana 6484453fc6 optionally allow strict quorum listing (#10649)
```
export MINIO_API_LIST_STRICT_QUORUM=on
```

would enable listing in quorum if necessary
2020-10-09 15:40:46 -07:00
Harshavardhana a0d0645128 remove safeMode behavior in startup (#10645)
In almost all scenarios MinIO now is
mostly ready for all sub-systems
independently, safe-mode is not useful
anymore and do not serve its original
intended purpose.

allow server to be fully functional
even with config partially configured,
this is to cater for availability of actual
I/O v/s manually fixing the server.

In k8s like environments it will never make
sense to take pod into safe-mode state,
because there is no real access to perform
any remote operation on them.
2020-10-09 09:59:52 -07:00
miraculli 1738eb24b1 fix: caching doc README.md missing high watermark (#10646) 2020-10-09 08:32:51 -07:00
Harshavardhana 253194e491 do not hold write locks - if objects don't exist (#10644) 2020-10-08 17:47:21 -07:00
Harshavardhana 736e58dd68 fix: handle concurrent lockers with multiple optimizations (#10640)
- select lockers which are non-local and online to have
  affinity towards remote servers for lock contention

- optimize lock retry interval to avoid sending too many
  messages during lock contention, reduces average CPU
  usage as well

- if bucket is not set, when deleteObject fails make sure
  setPutObjHeaders() honors lifecycle only if bucket name
  is set.

- fix top locks to list out always the oldest lockers always,
  avoid getting bogged down into map's unordered nature.
2020-10-08 12:32:32 -07:00
Poorna Krishnamoorthy 907a171edd Generalize error messages for remote targets (#10638)
This is to allow remote targets to be generalized
for replication/ILM transition

Also adding a field in BucketTarget to identify
a remote target with a label.
2020-10-08 10:54:11 -07:00
Andreas Auernhammer ed6d2a100f logger: avoid writing audit log response header twice (#10642)
This commit fixes a misuse of the `http.ResponseWriter.WriteHeader`.
A caller should **either** call `WriteHeader` exactly once **or**
write to the response writer and causing an implicit 200 OK.

Writing the response headers more than once causes a `http: superfluous
response.WriteHeader call` log message. This commit fixes this
by preventing a 2nd `WriteHeader` call being forwarded to the underlying
`ResponseWriter`.

Updates #10587
2020-10-08 09:29:10 -07:00
Harshavardhana effe131090 fix: allow read unlocks to be defensive about split brains (#10637) 2020-10-07 09:15:01 -07:00
Poorna Krishnamoorthy 01498a3e34 fix: add docs for new event types in notification (#10636) 2020-10-06 13:33:23 -07:00
Harshavardhana 18063bf25c fix: cleanup old directory handling code (#10633)
we don't need them anymore, remove legacy code.
2020-10-06 12:03:57 -07:00
Ravind Kumar 57f0176759 Update KES table to include additional supported KMS providers (#10631) 2020-10-06 11:09:43 -07:00
Poorna Krishnamoorthy dbbed6f7f0 update minio-go dependency (#10634) 2020-10-06 08:37:09 -07:00
Poorna Krishnamoorthy 7fbfdceba3 Fix replication slowness (#10632)
- Increase channel buffer length
- Avoid blocking wait on replicaCh
2020-10-05 14:45:42 -07:00
Mark Clarkson 9dda9fb903 fix: https healthcheck mint test (#10622) 2020-10-05 08:21:41 -07:00
Shireesh Anjal f1418a50f0 add NVMe drive info [model num, serial num, drive temp. etc.] (#10613)
* add NVMe drive info [model num, serial num, drive temp. etc.]
* Ignore fuse partitions
* Add the nvme logic only for linux
* Move smart/nvme structs to a separate file

Co-authored-by: wlan0 <sidharthamn@gmail.com>
2020-10-04 10:18:46 -07:00
Minio Trusted 017954e7ea Update yaml files to latest version RELEASE.2020-10-03T02-19-42Z 2020-10-03 02:36:54 +00:00
Harshavardhana 806625cbff turn-off go mod 2020-10-02 18:57:01 -07:00
Krishna Srinivas 045e30f2c1 Set LastModified time from source for bucket replication (#10627) 2020-10-02 18:32:22 -07:00
Harshavardhana c6a9a94f94 fix: optimize ServerInfo() handler to avoid reading config (#10626)
fixes #10620
2020-10-02 16:19:44 -07:00
Harshavardhana 8e7c00f3d4 add missing request-id from DeleteObject events (#10623)
fixes #10621
2020-10-02 13:36:13 -07:00
Anis Elleuch d1ed1da8c6 build: check-gen should exit with some changes in generated code (#10590) 2020-10-02 11:10:39 -07:00
Harshavardhana 23e8390997 fix: Allow Walk to honor load balanced drives (#10610) 2020-10-01 20:24:34 -07:00
Anis Elleuch 71403be912 fix: consider partNumber in GET/HEAD requests (#10618) 2020-10-01 15:41:12 -07:00
Harshavardhana f28d02b7f2 fix: simplify obd how we calculate transferred bytes (#10617) 2020-10-01 14:34:51 -07:00
Harshavardhana e0cb814f3f fail if port is not accessible (#10616)
throw proper error when port is not accessible
for the regular user, this is possibly a regression.

```
ERROR Unable to start the server: Insufficient permissions to use specified port
   > Please ensure MinIO binary has 'cap_net_bind_service=+ep' permissions
   HINT:
     Use 'sudo setcap cap_net_bind_service=+ep /path/to/minio' to provide sufficient permissions
```
2020-10-01 13:23:31 -07:00
Harshavardhana 98a08e1644 fix: protect updating latencies/throughput slices in obd (#10611)
Additionally close the transferChan upon function exit.
2020-10-01 09:50:08 -07:00
Klaus Post 3047121255 dataupdate: Bump to force rescan (#10609)
After #10594 let's invalidate the bloom filters to force the next cycles to go through all data.

There is a small chance that the linked PR could have caused missing bloom filter data.

This will invalidate the current bloom filters and make the crawler go through everything.
2020-09-30 16:10:40 -07:00
Ritesh H Shukla 5a7f92481e fix: client errors for DNS service creation errors (#10584) 2020-09-30 14:09:41 -07:00
Anis Elleuch 0d45c38782 List v1/versions routes based on source IP if found (#10603)
Routing using on source IP if found. This should distribute
the listing load for V1 and versioning on multiple nodes
evenly between different clients.

If source IP is not found from the http request header, then falls back
to bucket name instead.
2020-09-30 13:38:27 -07:00
Poorna Krishnamoorthy 56d1b227cf Handle changes to versioning config for replication (#10598)
Disallow versioning suspension on a bucket with
pre-existing replication configuration

If versioning is suspended on the target,replication
should fail.
2020-09-30 13:36:37 -07:00
Harshavardhana 061fa0635c remove ignoring incorrect CVE (#10597) 2020-09-30 13:10:24 -07:00
Shireesh Anjal 6e138f955e Fix a couple of typos in json config (#10605)
Vault.Encrypt: encryp -> encrypt
SysOBDProcess.Uids: uidsomitempty -> uids,omitempty
2020-09-30 13:08:11 -07:00
Lenin Alevski bea87a5a20 fix: reading multiple TLS certificates when deployed in K8S (#10601)
Ignore all regular files, CAs directory and any 
directory that starts with `..` inside the
`.minio/certs` folder
2020-09-30 08:21:30 -07:00
Harshavardhana 2b4eb87d77 pick disks which are common maximally used (#10600)
further optimization to ensure that good disks
are always used for listing, other than healing
we only use disks that are maximally used.
2020-09-29 22:54:02 -07:00
sadegh 799758e54f fix: dropzone height to fill all screen height (#10547) 2020-09-29 22:27:41 -07:00
Harshavardhana 1f9abbee4d make sure to release locks upon timeout (#10596)
fixes #10418
2020-09-29 15:18:34 -07:00
Klaus Post fdf0ae9167 exit data update tracker only upon context completion (#10594)
The data update tracker saver would exit if data wasn't updated for between cycles.
2020-09-29 13:23:53 -07:00
Harshavardhana 00eb6f6bc9 cache DiskInfo at storage layer for performance (#10586)
`mc admin info` on busy setups will not move HDD
heads unnecessarily for repeated calls, provides
a better responsiveness for the call overall.

Bonus change allow listTolerancePerSet be N-1
for good entries, to avoid skipping entries
for some reason one of the disk went offline.
2020-09-29 09:54:41 -07:00
Harshavardhana 66174692a2 add '.healing.bin' for tracking currently healing disk (#10573)
add a hint on the disk to allow for tracking fresh disk
being healed, to allow for restartable heals, and also
use this as a way to track and remove disks.

There are more pending changes where we should move
all the disk formatting logic to backend drives, this
PR doesn't deal with this refactor instead makes it
easier to track healing in the future.
2020-09-28 19:39:32 -07:00
Harshavardhana 849fcf0127 block unlocks if there are quorum failures (#10582)
fixes #10418
2020-09-28 15:39:52 -07:00
飞雪无情 209680e89f Remove redundant http.HandlerFunc type conversion. (#10576) 2020-09-28 13:33:49 -07:00
Anis Elleuch e0c04a2da0 Makefile: Check for any non committed auto-generated code (#10579)
Always check if the auto-generated code is still compatible with the
existing written code to avoid a possible forgetting or sometimes a non
intentional change.
2020-09-28 13:33:34 -07:00
飞雪无情 27d9bd04e5 Handling unhandled errors in the InfoCannedPolicy method. (#10575) 2020-09-27 10:24:04 -07:00
Minio Trusted 511424a287 Update yaml files to latest version RELEASE.2020-09-26T03-44-56Z 2020-09-26 04:02:42 +00:00
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
346 changed files with 19241 additions and 8140 deletions
-51
View File
@@ -1,51 +0,0 @@
name: "Code scanning - action"
on:
push:
pull_request:
schedule:
- cron: '0 19 * * 0'
jobs:
CodeQL-Build:
# CodeQL runs on ubuntu-latest and windows-latest
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: go, javascript
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
+2
View File
@@ -37,6 +37,8 @@ jobs:
GO111MODULE: on GO111MODULE: on
MINIO_CI_CD: 1 MINIO_CI_CD: 1
run: | run: |
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
sudo apt-get install devscripts shellcheck sudo apt-get install devscripts shellcheck
nancy_version=$(curl --retry 10 -Ls -o /dev/null -w "%{url_effective}" https://github.com/sonatype-nexus-community/nancy/releases/latest | sed "s/https:\/\/github.com\/sonatype-nexus-community\/nancy\/releases\/tag\///") nancy_version=$(curl --retry 10 -Ls -o /dev/null -w "%{url_effective}" https://github.com/sonatype-nexus-community/nancy/releases/latest | sed "s/https:\/\/github.com\/sonatype-nexus-community\/nancy\/releases\/tag\///")
curl -L -o nancy https://github.com/sonatype-nexus-community/nancy/releases/download/${nancy_version}/nancy-linux.amd64-${nancy_version} && chmod +x nancy curl -L -o nancy https://github.com/sonatype-nexus-community/nancy/releases/download/${nancy_version}/nancy-linux.amd64-${nancy_version} && chmod +x nancy
+3 -3
View File
@@ -1,4 +1,4 @@
FROM golang:1.14-alpine as builder FROM golang:1.15-alpine as builder
LABEL maintainer="MinIO Inc <dev@min.io>" LABEL maintainer="MinIO Inc <dev@min.io>"
@@ -13,11 +13,11 @@ RUN \
FROM alpine:3.12 FROM alpine:3.12
ENV MINIO_UPDATE off
ENV MINIO_ACCESS_KEY_FILE=access_key \ ENV MINIO_ACCESS_KEY_FILE=access_key \
MINIO_SECRET_KEY_FILE=secret_key \ MINIO_SECRET_KEY_FILE=secret_key \
MINIO_KMS_MASTER_KEY_FILE=kms_master_key \ MINIO_KMS_MASTER_KEY_FILE=kms_master_key \
MINIO_SSE_MASTER_KEY_FILE=sse_master_key MINIO_SSE_MASTER_KEY_FILE=sse_master_key \
MINIO_UPDATE_MINISIGN_PUBKEY="RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav"
EXPOSE 9000 EXPOSE 9000
+2 -2
View File
@@ -5,11 +5,11 @@ LABEL maintainer="MinIO Inc <dev@min.io>"
COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
ENV MINIO_UPDATE off
ENV MINIO_ACCESS_KEY_FILE=access_key \ ENV MINIO_ACCESS_KEY_FILE=access_key \
MINIO_SECRET_KEY_FILE=secret_key \ MINIO_SECRET_KEY_FILE=secret_key \
MINIO_KMS_MASTER_KEY_FILE=kms_master_key \ MINIO_KMS_MASTER_KEY_FILE=kms_master_key \
MINIO_SSE_MASTER_KEY_FILE=sse_master_key MINIO_SSE_MASTER_KEY_FILE=sse_master_key \
MINIO_UPDATE_MINISIGN_PUBKEY="RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav"
RUN \ RUN \
echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \ echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \
+2 -2
View File
@@ -5,11 +5,11 @@ LABEL maintainer="MinIO Inc <dev@min.io>"
COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
ENV MINIO_UPDATE off
ENV MINIO_ACCESS_KEY_FILE=access_key \ ENV MINIO_ACCESS_KEY_FILE=access_key \
MINIO_SECRET_KEY_FILE=secret_key \ MINIO_SECRET_KEY_FILE=secret_key \
MINIO_KMS_MASTER_KEY_FILE=kms_master_key \ MINIO_KMS_MASTER_KEY_FILE=kms_master_key \
MINIO_SSE_MASTER_KEY_FILE=sse_master_key MINIO_SSE_MASTER_KEY_FILE=sse_master_key \
MINIO_UPDATE_MINISIGN_PUBKEY="RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav"
RUN \ RUN \
echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \ echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \
+2 -2
View File
@@ -5,11 +5,11 @@ LABEL maintainer="MinIO Inc <dev@min.io>"
COPY --from=qemu /usr/bin/qemu-ppc64le-static /usr/bin COPY --from=qemu /usr/bin/qemu-ppc64le-static /usr/bin
ENV MINIO_UPDATE off
ENV MINIO_ACCESS_KEY_FILE=access_key \ ENV MINIO_ACCESS_KEY_FILE=access_key \
MINIO_SECRET_KEY_FILE=secret_key \ MINIO_SECRET_KEY_FILE=secret_key \
MINIO_KMS_MASTER_KEY_FILE=kms_master_key \ MINIO_KMS_MASTER_KEY_FILE=kms_master_key \
MINIO_SSE_MASTER_KEY_FILE=sse_master_key MINIO_SSE_MASTER_KEY_FILE=sse_master_key \
MINIO_UPDATE_MINISIGN_PUBKEY="RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav"
RUN \ RUN \
echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \ echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \
+2 -2
View File
@@ -2,11 +2,11 @@ FROM alpine:3.12
LABEL maintainer="MinIO Inc <dev@min.io>" LABEL maintainer="MinIO Inc <dev@min.io>"
ENV MINIO_UPDATE off
ENV MINIO_ACCESS_KEY_FILE=access_key \ ENV MINIO_ACCESS_KEY_FILE=access_key \
MINIO_SECRET_KEY_FILE=secret_key \ MINIO_SECRET_KEY_FILE=secret_key \
MINIO_KMS_MASTER_KEY_FILE=kms_master_key \ MINIO_KMS_MASTER_KEY_FILE=kms_master_key \
MINIO_SSE_MASTER_KEY_FILE=sse_master_key MINIO_SSE_MASTER_KEY_FILE=sse_master_key \
MINIO_UPDATE_MINISIGN_PUBKEY="RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav"
RUN \ RUN \
echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \ echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \
+2 -2
View File
@@ -5,11 +5,11 @@ LABEL maintainer="MinIO Inc <dev@min.io>"
COPY --from=qemu /usr/bin/qemu-s390x-static /usr/bin COPY --from=qemu /usr/bin/qemu-s390x-static /usr/bin
ENV MINIO_UPDATE off
ENV MINIO_ACCESS_KEY_FILE=access_key \ ENV MINIO_ACCESS_KEY_FILE=access_key \
MINIO_SECRET_KEY_FILE=secret_key \ MINIO_SECRET_KEY_FILE=secret_key \
MINIO_KMS_MASTER_KEY_FILE=kms_master_key \ MINIO_KMS_MASTER_KEY_FILE=kms_master_key \
MINIO_SSE_MASTER_KEY_FILE=sse_master_key MINIO_SSE_MASTER_KEY_FILE=sse_master_key \
MINIO_UPDATE_MINISIGN_PUBKEY="RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav"
RUN \ RUN \
echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \ echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \
+7 -1
View File
@@ -18,11 +18,17 @@ getdeps:
@mkdir -p ${GOPATH}/bin @mkdir -p ${GOPATH}/bin
@which golangci-lint 1>/dev/null || (echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin v1.27.0) @which golangci-lint 1>/dev/null || (echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin v1.27.0)
@which ruleguard 1>/dev/null || (echo "Installing ruleguard" && GO111MODULE=off go get github.com/quasilyte/go-ruleguard/...) @which ruleguard 1>/dev/null || (echo "Installing ruleguard" && GO111MODULE=off go get github.com/quasilyte/go-ruleguard/...)
@which msgp 1>/dev/null || (echo "Installing msgp" && GO111MODULE=off go get github.com/tinylib/msgp)
@which stringer 1>/dev/null || (echo "Installing stringer" && GO111MODULE=off go get golang.org/x/tools/cmd/stringer)
crosscompile: crosscompile:
@(env bash $(PWD)/buildscripts/cross-compile.sh) @(env bash $(PWD)/buildscripts/cross-compile.sh)
verifiers: getdeps fmt lint ruleguard verifiers: getdeps fmt lint ruleguard check-gen
check-gen:
@go generate ./... >/dev/null
@(! git diff --name-only | grep '_gen.go$$') || (echo "Non-committed changes in auto-generated code is detected, please commit them to proceed." && false)
fmt: fmt:
@echo "Running $@ check" @echo "Running $@ check"
+2 -2
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.14](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.15](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
+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()))
)
+1 -1
View File
@@ -36,7 +36,7 @@ export class Dropzone extends React.Component {
// Overwrite the default styling from react-dropzone; otherwise it // Overwrite the default styling from react-dropzone; otherwise it
// won't handle child elements correctly. // won't handle child elements correctly.
const style = { const style = {
height: "100%", flex: "1",
borderWidth: "0", borderWidth: "0",
borderStyle: "dashed", borderStyle: "dashed",
borderColor: "#fff" borderColor: "#fff"
+2 -1
View File
@@ -20,7 +20,8 @@
@media(max-width: @screen-sm-max) { @media(max-width: @screen-sm-max) {
padding: 75px 0 80px; padding: 75px 0 80px;
} }
display: flex;
flex-direction: column;
min-height:100vh; min-height:100vh;
overflow: auto; overflow: auto;
} }
+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
+14 -45
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-ipv6-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-ipv6-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
+1 -1
View File
@@ -42,7 +42,7 @@ import (
func validateAdminReqConfigKV(ctx context.Context, w http.ResponseWriter, r *http.Request) (auth.Credentials, ObjectLayer) { func validateAdminReqConfigKV(ctx context.Context, w http.ResponseWriter, r *http.Request) (auth.Credentials, ObjectLayer) {
// Get current object layer instance. // Get current object layer instance.
objectAPI := newObjectLayerWithoutSafeModeFn() objectAPI := newObjectLayerFn()
if objectAPI == nil { if objectAPI == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return auth.Credentials{}, nil return auth.Credentials{}, nil
+14 -11
View File
@@ -35,8 +35,8 @@ func validateAdminUsersReq(ctx context.Context, w http.ResponseWriter, r *http.R
var adminAPIErr APIErrorCode var adminAPIErr APIErrorCode
// Get current object layer instance. // Get current object layer instance.
objectAPI := newObjectLayerWithoutSafeModeFn() objectAPI := newObjectLayerFn()
if objectAPI == nil || globalNotificationSys == nil || globalIAMSys == nil { if objectAPI == nil || globalNotificationSys == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return nil, cred return nil, cred
} }
@@ -386,8 +386,8 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
defer logger.AuditLog(w, r, "AddServiceAccount", mustGetClaimsFromToken(r)) defer logger.AuditLog(w, r, "AddServiceAccount", mustGetClaimsFromToken(r))
// Get current object layer instance. // Get current object layer instance.
objectAPI := newObjectLayerWithoutSafeModeFn() objectAPI := newObjectLayerFn()
if objectAPI == nil || globalNotificationSys == nil || globalIAMSys == nil { if objectAPI == nil || globalNotificationSys == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return return
} }
@@ -465,8 +465,8 @@ func (a adminAPIHandlers) ListServiceAccounts(w http.ResponseWriter, r *http.Req
defer logger.AuditLog(w, r, "ListServiceAccounts", mustGetClaimsFromToken(r)) defer logger.AuditLog(w, r, "ListServiceAccounts", mustGetClaimsFromToken(r))
// Get current object layer instance. // Get current object layer instance.
objectAPI := newObjectLayerWithoutSafeModeFn() objectAPI := newObjectLayerFn()
if objectAPI == nil || globalNotificationSys == nil || globalIAMSys == nil { if objectAPI == nil || globalNotificationSys == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return return
} }
@@ -520,8 +520,8 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re
defer logger.AuditLog(w, r, "DeleteServiceAccount", mustGetClaimsFromToken(r)) defer logger.AuditLog(w, r, "DeleteServiceAccount", mustGetClaimsFromToken(r))
// Get current object layer instance. // Get current object layer instance.
objectAPI := newObjectLayerWithoutSafeModeFn() objectAPI := newObjectLayerFn()
if objectAPI == nil || globalNotificationSys == nil || globalIAMSys == nil { if objectAPI == nil || globalNotificationSys == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return return
} }
@@ -579,8 +579,8 @@ func (a adminAPIHandlers) AccountUsageInfoHandler(w http.ResponseWriter, r *http
defer logger.AuditLog(w, r, "AccountUsageInfo", mustGetClaimsFromToken(r)) defer logger.AuditLog(w, r, "AccountUsageInfo", mustGetClaimsFromToken(r))
// Get current object layer instance. // Get current object layer instance.
objectAPI := newObjectLayerWithoutSafeModeFn() objectAPI := newObjectLayerFn()
if objectAPI == nil || globalNotificationSys == nil || globalIAMSys == nil { if objectAPI == nil || globalNotificationSys == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return return
} }
@@ -722,7 +722,10 @@ func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Reques
return return
} }
json.NewEncoder(w).Encode(policy) if err = json.NewEncoder(w).Encode(policy); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
w.(http.Flusher).Flush() w.(http.Flusher).Flush()
} }
+132 -117
View File
@@ -35,9 +35,7 @@ import (
"time" "time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/minio/minio/cmd/config" "github.com/minio/minio/cmd/config"
"github.com/minio/minio/cmd/config/notify"
"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"
@@ -54,16 +52,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) {
@@ -90,8 +85,7 @@ func (a adminAPIHandlers) ServerUpdateHandler(w http.ResponseWriter, r *http.Req
} }
if globalInplaceUpdateDisabled { if globalInplaceUpdateDisabled {
// if MINIO_UPDATE=off - inplace update is disabled, mostly // if MINIO_UPDATE=off - inplace update is disabled, mostly in containers.
// in containers.
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
return return
} }
@@ -132,7 +126,7 @@ func (a adminAPIHandlers) ServerUpdateHandler(w http.ResponseWriter, r *http.Req
return return
} }
if lrTime.Sub(crTime) < 0 { if lrTime.Sub(crTime) <= 0 {
updateStatus := madmin.ServerUpdateStatus{ updateStatus := madmin.ServerUpdateStatus{
CurrentVersion: Version, CurrentVersion: Version,
UpdatedVersion: Version, UpdatedVersion: Version,
@@ -298,6 +292,20 @@ func (a adminAPIHandlers) StorageInfoHandler(w http.ResponseWriter, r *http.Requ
// ignores any errors here. // ignores any errors here.
storageInfo, _ := objectAPI.StorageInfo(ctx, false) storageInfo, _ := objectAPI.StorageInfo(ctx, false)
// 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
for i, disk := range storageInfo.Disks {
if _, ok := healDisks[disk.Endpoint]; ok {
storageInfo.Disks[i].Healing = true
}
}
// Marshal API response // Marshal API response
jsonBytes, err := json.Marshal(storageInfo) jsonBytes, err := json.Marshal(storageInfo)
if err != nil { if err != nil {
@@ -345,7 +353,9 @@ func lriToLockEntry(l lockRequesterInfo, resource, server string) *madmin.LockEn
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,
Quorum: l.Quorum,
} }
if l.Writer { if l.Writer {
entry.Type = "WRITE" entry.Type = "WRITE"
@@ -355,7 +365,7 @@ func lriToLockEntry(l lockRequesterInfo, resource, server string) *madmin.LockEn
return entry return entry
} }
func topLockEntries(peerLocks []*PeerLocks, count int) madmin.LockEntries { func topLockEntries(peerLocks []*PeerLocks, 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 {
@@ -373,14 +383,17 @@ func topLockEntries(peerLocks []*PeerLocks, count int) madmin.LockEntries {
} }
} }
} }
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 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,23 +423,17 @@ 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. topLocks := topLockEntries(peerLocks, stale)
var getRespLocks GetLocksResp
for _, llocker := range globalLockServers { // Marshal API response upto requested count.
getRespLocks = append(getRespLocks, llocker.DupLockMap()) if len(topLocks) > count && count > 0 {
topLocks = topLocks[:count]
} }
peerLocks = append(peerLocks, &PeerLocks{
Addr: getHostName(r),
Locks: getRespLocks,
})
topLocks := topLockEntries(peerLocks, count)
// Marshal API response
jsonBytes, err := json.Marshal(topLocks) jsonBytes, err := json.Marshal(topLocks)
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
@@ -572,8 +579,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 +599,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
} }
@@ -787,7 +794,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
case hip.clientToken == "": case hip.clientToken == "":
nh := newHealSequence(GlobalContext, hip.bucket, hip.objPrefix, handlers.GetSourceIP(r), hip.hs, hip.forceStart) nh := newHealSequence(GlobalContext, hip.bucket, hip.objPrefix, handlers.GetSourceIP(r), hip.hs, hip.forceStart)
go func() { go func() {
respBytes, apiErr, errMsg := globalAllHealState.LaunchNewHealSequence(nh) respBytes, apiErr, errMsg := globalAllHealState.LaunchNewHealSequence(nh, objectAPI)
hr := healResp{respBytes, apiErr, errMsg} hr := healResp{respBytes, apiErr, errMsg}
respCh <- hr respCh <- hr
}() }()
@@ -886,7 +893,7 @@ func validateAdminReq(ctx context.Context, w http.ResponseWriter, r *http.Reques
var cred auth.Credentials var cred auth.Credentials
var adminAPIErr APIErrorCode var adminAPIErr APIErrorCode
// Get current object layer instance. // Get current object layer instance.
objectAPI := newObjectLayerWithoutSafeModeFn() objectAPI := newObjectLayerFn()
if objectAPI == nil || globalNotificationSys == nil { if objectAPI == nil || globalNotificationSys == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return nil, cred return nil, cred
@@ -1028,7 +1035,7 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
// Use buffered channel to take care of burst sends or slow w.Write() // Use buffered channel to take care of burst sends or slow w.Write()
traceCh := make(chan interface{}, 4000) traceCh := make(chan interface{}, 4000)
peers := newPeerRestClients(globalEndpoints) peers, _ := newPeerRestClients(globalEndpoints)
globalHTTPTrace.Subscribe(traceCh, ctx.Done(), func(entry interface{}) bool { globalHTTPTrace.Subscribe(traceCh, ctx.Done(), func(entry interface{}) bool {
return mustTrace(entry, trcAll, trcErr) return mustTrace(entry, trcAll, trcErr)
@@ -1096,7 +1103,7 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque
logCh := make(chan interface{}, 4000) logCh := make(chan interface{}, 4000)
peers := newPeerRestClients(globalEndpoints) peers, _ := newPeerRestClients(globalEndpoints)
globalConsoleSys.Subscribe(logCh, ctx.Done(), node, limitLines, logKind, nil) globalConsoleSys.Subscribe(logCh, ctx.Done(), node, limitLines, logKind, nil)
@@ -1241,7 +1248,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)
@@ -1277,8 +1284,8 @@ func (a adminAPIHandlers) OBDInfoHandler(w http.ResponseWriter, r *http.Request)
deadlinedCtx, cancel := context.WithTimeout(ctx, deadline) deadlinedCtx, cancel := context.WithTimeout(ctx, deadline)
defer cancel() defer cancel()
nsLock := objectAPI.NewNSLock(ctx, minioMetaBucket, "obd-in-progress") nsLock := objectAPI.NewNSLock(minioMetaBucket, "obd-in-progress")
if err := nsLock.GetLock(newDynamicTimeout(deadline, deadline)); err != nil { // returns a locked lock if err := nsLock.GetLock(ctx, newDynamicTimeout(deadline, deadline)); err != nil { // returns a locked lock
errResp(err) errResp(err)
return return
} }
@@ -1287,7 +1294,7 @@ 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 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)
@@ -1295,7 +1302,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)
@@ -1303,7 +1310,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)
@@ -1311,7 +1318,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)
@@ -1319,7 +1326,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)
@@ -1327,14 +1334,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)
@@ -1365,7 +1372,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)
@@ -1379,6 +1386,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)
@@ -1405,6 +1413,31 @@ func (a adminAPIHandlers) OBDInfoHandler(w http.ResponseWriter, r *http.Request)
} }
// BandwidthMonitorHandler - GET /minio/admin/v3/bandwidth
// ----------
// Get bandwidth consumption information
func (a adminAPIHandlers) BandwidthMonitorHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "BandwidthMonitor")
// Validate request signature.
_, adminAPIErr := checkAdminRequestAuthType(ctx, r, iampolicy.BandwidthMonitorAction, "")
if adminAPIErr != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
return
}
setEventStreamHeaders(w)
bucketsRequestedString := r.URL.Query().Get("buckets")
bucketsRequested := strings.Split(bucketsRequestedString, ",")
consolidatedReport := globalNotificationSys.GetBandwidthReports(ctx, bucketsRequested...)
enc := json.NewEncoder(w)
err := enc.Encode(consolidatedReport)
if err != nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL)
}
w.(http.Flusher).Flush()
}
// ServerInfoHandler - GET /minio/admin/v3/info // ServerInfoHandler - GET /minio/admin/v3/info
// ---------- // ----------
// Get server information // Get server information
@@ -1418,12 +1451,6 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
return return
} }
cfg, err := readServerConfig(ctx, objectAPI)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
buckets := madmin.Buckets{} buckets := madmin.Buckets{}
objects := madmin.Objects{} objects := madmin.Objects{}
usage := madmin.Usage{} usage := madmin.Usage{}
@@ -1435,7 +1462,7 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
usage = madmin.Usage{Size: dataUsageInfo.ObjectsTotalSize} usage = madmin.Usage{Size: dataUsageInfo.ObjectsTotalSize}
} }
vault := fetchVaultStatus(cfg) vault := fetchVaultStatus()
ldap := madmin.LDAP{} ldap := madmin.LDAP{}
if globalLDAPConfig.Enabled { if globalLDAPConfig.Enabled {
@@ -1451,10 +1478,10 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
} }
} }
log, audit := fetchLoggerInfo(cfg) log, audit := fetchLoggerInfo()
// Get the notification target info // Get the notification target info
notifyTarget := fetchLambdaInfo(cfg) notifyTarget := fetchLambdaInfo()
// Fetching the Storage information, ignore any errors. // Fetching the Storage information, ignore any errors.
storageInfo, _ := objectAPI.StorageInfo(ctx, false) storageInfo, _ := objectAPI.StorageInfo(ctx, false)
@@ -1476,11 +1503,7 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
} }
} }
mode := "safemode" mode := "online"
if newObjectLayerFn() != nil {
mode = "online"
}
server := getLocalServerProperty(globalEndpoints, r) server := getLocalServerProperty(globalEndpoints, r)
servers := globalNotificationSys.ServerInfo() servers := globalNotificationSys.ServerInfo()
servers = append(servers, server) servers = append(servers, server)
@@ -1494,20 +1517,10 @@ 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)
} }
} }
@@ -1519,9 +1532,6 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
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)
} }
} }
@@ -1547,27 +1557,33 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
return return
} }
//Reply with storage information (across nodes in a // Reply with storage information (across nodes in a
// distributed setup) as json. // distributed setup) as json.
writeSuccessResponseJSON(w, jsonBytes) writeSuccessResponseJSON(w, jsonBytes)
} }
func fetchLambdaInfo(cfg config.Config) []map[string][]madmin.TargetIDStatus { func fetchLambdaInfo() []map[string][]madmin.TargetIDStatus {
// Fetch the configured targets
tr := NewGatewayHTTPTransport()
defer tr.CloseIdleConnections()
targetList, err := notify.FetchRegisteredTargets(GlobalContext, cfg, tr, true, false)
if err != nil && err != notify.ErrTargetsOffline {
logger.LogIf(GlobalContext, err)
return nil
}
lambdaMap := make(map[string][]madmin.TargetIDStatus) lambdaMap := make(map[string][]madmin.TargetIDStatus)
for targetID, target := range targetList.TargetMap() { for _, tgt := range globalConfigTargetList.Targets() {
targetIDStatus := make(map[string]madmin.Status) targetIDStatus := make(map[string]madmin.Status)
active, _ := target.IsActive() active, _ := tgt.IsActive()
targetID := tgt.ID()
if active {
targetIDStatus[targetID.ID] = madmin.Status{Status: "Online"}
} else {
targetIDStatus[targetID.ID] = madmin.Status{Status: "Offline"}
}
list := lambdaMap[targetID.Name]
list = append(list, targetIDStatus)
lambdaMap[targetID.Name] = list
}
for _, tgt := range globalEnvTargetList.Targets() {
targetIDStatus := make(map[string]madmin.Status)
active, _ := tgt.IsActive()
targetID := tgt.ID()
if active { if active {
targetIDStatus[targetID.ID] = madmin.Status{Status: "Online"} targetIDStatus[targetID.ID] = madmin.Status{Status: "Online"}
} else { } else {
@@ -1576,8 +1592,6 @@ func fetchLambdaInfo(cfg config.Config) []map[string][]madmin.TargetIDStatus {
list := lambdaMap[targetID.Name] list := lambdaMap[targetID.Name]
list = append(list, targetIDStatus) list = append(list, targetIDStatus)
lambdaMap[targetID.Name] = list lambdaMap[targetID.Name] = list
// Close any leaking connections
_ = target.Close()
} }
notify := make([]map[string][]madmin.TargetIDStatus, len(lambdaMap)) notify := make([]map[string][]madmin.TargetIDStatus, len(lambdaMap))
@@ -1592,7 +1606,7 @@ func fetchLambdaInfo(cfg config.Config) []map[string][]madmin.TargetIDStatus {
} }
// fetchVaultStatus fetches Vault Info // fetchVaultStatus fetches Vault Info
func fetchVaultStatus(cfg config.Config) madmin.Vault { func fetchVaultStatus() madmin.Vault {
vault := madmin.Vault{} vault := madmin.Vault{}
if GlobalKMS == nil { if GlobalKMS == nil {
vault.Status = "disabled" vault.Status = "disabled"
@@ -1635,41 +1649,42 @@ func fetchVaultStatus(cfg config.Config) madmin.Vault {
} }
// fetchLoggerDetails return log info // fetchLoggerDetails return log info
func fetchLoggerInfo(cfg config.Config) ([]madmin.Logger, []madmin.Audit) { func fetchLoggerInfo() ([]madmin.Logger, []madmin.Audit) {
loggerCfg, _ := logger.LookupConfig(cfg) var loggerInfo []madmin.Logger
var auditloggerInfo []madmin.Audit
var logger []madmin.Logger for _, target := range logger.Targets {
var auditlogger []madmin.Audit if target.Endpoint() != "" {
for log, l := range loggerCfg.HTTP { tgt := target.String()
if l.Enabled { err := checkConnection(target.Endpoint(), 15*time.Second)
err := checkConnection(l.Endpoint, 15*time.Second)
if err == nil { if err == nil {
mapLog := make(map[string]madmin.Status) mapLog := make(map[string]madmin.Status)
mapLog[log] = madmin.Status{Status: "Online"} mapLog[tgt] = madmin.Status{Status: "Online"}
logger = append(logger, mapLog) loggerInfo = append(loggerInfo, mapLog)
} else { } else {
mapLog := make(map[string]madmin.Status) mapLog := make(map[string]madmin.Status)
mapLog[log] = madmin.Status{Status: "offline"} mapLog[tgt] = madmin.Status{Status: "offline"}
logger = append(logger, mapLog) loggerInfo = append(loggerInfo, mapLog)
} }
} }
} }
for audit, l := range loggerCfg.Audit { for _, target := range logger.AuditTargets {
if l.Enabled { if target.Endpoint() != "" {
err := checkConnection(l.Endpoint, 15*time.Second) tgt := target.String()
err := checkConnection(target.Endpoint(), 15*time.Second)
if err == nil { if err == nil {
mapAudit := make(map[string]madmin.Status) mapAudit := make(map[string]madmin.Status)
mapAudit[audit] = madmin.Status{Status: "Online"} mapAudit[tgt] = madmin.Status{Status: "Online"}
auditlogger = append(auditlogger, mapAudit) auditloggerInfo = append(auditloggerInfo, mapAudit)
} else { } else {
mapAudit := make(map[string]madmin.Status) mapAudit := make(map[string]madmin.Status)
mapAudit[audit] = madmin.Status{Status: "Offline"} mapAudit[tgt] = madmin.Status{Status: "Offline"}
auditlogger = append(auditlogger, mapAudit) auditloggerInfo = append(auditloggerInfo, mapAudit)
} }
} }
} }
return logger, auditlogger
return loggerInfo, auditloggerInfo
} }
// checkConnection - ping an endpoint , return err in case of no connection // checkConnection - ping an endpoint , return err in case of no connection
+8 -8
View File
@@ -105,8 +105,8 @@ func initTestErasureObjLayer(ctx context.Context) (ObjectLayer, []string, error)
} }
globalPolicySys = NewPolicySys() globalPolicySys = NewPolicySys()
objLayer := &erasureZones{zones: make([]*erasureSets, 1)} objLayer := &erasureServerSets{serverSets: make([]*erasureSets, 1)}
objLayer.zones[0], err = newErasureSets(ctx, endpoints, storageDisks, format) objLayer.serverSets[0], err = newErasureSets(ctx, endpoints, storageDisks, format)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -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.
+30 -65
View File
@@ -85,7 +85,7 @@ type healSequenceStatus struct {
// structure to hold state of all heal sequences in server memory // structure to hold state of all heal sequences in server memory
type allHealState struct { type allHealState struct {
sync.Mutex sync.RWMutex
// map of heal path to heal sequence // map of heal path to heal sequence
healSeqMap map[string]*healSequence healSeqMap map[string]*healSequence
@@ -105,21 +105,21 @@ func newHealState() *allHealState {
} }
func (ahs *allHealState) healDriveCount() int { func (ahs *allHealState) healDriveCount() int {
ahs.Lock() ahs.RLock()
defer ahs.Unlock() defer ahs.RUnlock()
return len(ahs.healLocalDisks) return len(ahs.healLocalDisks)
} }
func (ahs *allHealState) getHealLocalDisks() Endpoints { func (ahs *allHealState) getHealLocalDisks() Endpoints {
ahs.Lock() ahs.RLock()
defer ahs.Unlock() defer ahs.RUnlock()
var healLocalDisks Endpoints var endpoints Endpoints
for ep := range ahs.healLocalDisks { for ep := range ahs.healLocalDisks {
healLocalDisks = append(healLocalDisks, ep) endpoints = append(endpoints, ep)
} }
return healLocalDisks return endpoints
} }
func (ahs *allHealState) popHealLocalDisks(healLocalDisks ...Endpoint) { func (ahs *allHealState) popHealLocalDisks(healLocalDisks ...Endpoint) {
@@ -228,7 +228,7 @@ func (ahs *allHealState) stopHealSequence(path string) ([]byte, APIError) {
// `keepHealSeqStateDuration`. This function also launches a // `keepHealSeqStateDuration`. This function also launches a
// background routine to clean up heal results after the // background routine to clean up heal results after the
// aforementioned duration. // aforementioned duration.
func (ahs *allHealState) LaunchNewHealSequence(h *healSequence) ( func (ahs *allHealState) LaunchNewHealSequence(h *healSequence, objAPI ObjectLayer) (
respBytes []byte, apiErr APIError, errMsg string) { respBytes []byte, apiErr APIError, errMsg string) {
if h.forceStarted { if h.forceStarted {
@@ -265,7 +265,7 @@ func (ahs *allHealState) LaunchNewHealSequence(h *healSequence) (
ahs.healSeqMap[hpath] = h ahs.healSeqMap[hpath] = h
// Launch top-level background heal go-routine // Launch top-level background heal go-routine
go h.healSequenceStart() go h.healSequenceStart(objAPI)
clientToken := h.clientToken clientToken := h.clientToken
if globalIsDistErasure { if globalIsDistErasure {
@@ -528,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.
@@ -609,7 +609,7 @@ func (h *healSequence) pushHealResultItem(r madmin.HealResultItem) error {
// routine for completion, and (2) listens for external stop // routine for completion, and (2) listens for external stop
// signals. When either event happens, it sets the finish status for // signals. When either event happens, it sets the finish status for
// the heal-sequence. // the heal-sequence.
func (h *healSequence) healSequenceStart() { func (h *healSequence) healSequenceStart(objAPI ObjectLayer) {
// Set status as running // Set status as running
h.mutex.Lock() h.mutex.Lock()
h.currentStatus.Summary = healRunningStatus h.currentStatus.Summary = healRunningStatus
@@ -617,7 +617,7 @@ func (h *healSequence) healSequenceStart() {
h.mutex.Unlock() h.mutex.Unlock()
if h.sourceCh == nil { if h.sourceCh == nil {
go h.traverseAndHeal() go h.traverseAndHeal(objAPI)
} else { } else {
go h.healFromSourceCh() go h.healFromSourceCh()
} }
@@ -766,28 +766,23 @@ func (h *healSequence) healFromSourceCh() {
h.healItemsFromSourceCh() h.healItemsFromSourceCh()
} }
func (h *healSequence) healDiskMeta() error { func (h *healSequence) healDiskMeta(objAPI ObjectLayer) error {
// Start with format healing
if err := h.healDiskFormat(); err != nil {
return err
}
// Start healing the config prefix. // Start healing the config prefix.
if err := h.healMinioSysMeta(minioConfigPrefix)(); err != nil { if err := h.healMinioSysMeta(objAPI, minioConfigPrefix)(); err != nil {
return err return err
} }
// Start healing the bucket config prefix. // Start healing the bucket config prefix.
return h.healMinioSysMeta(bucketConfigPrefix)() return h.healMinioSysMeta(objAPI, bucketConfigPrefix)()
} }
func (h *healSequence) healItems(bucketsOnly bool) error { func (h *healSequence) healItems(objAPI ObjectLayer, bucketsOnly bool) error {
if err := h.healDiskMeta(); err != nil { if err := h.healDiskMeta(objAPI); err != nil {
return err return err
} }
// Heal buckets and objects // Heal buckets and objects
return h.healBuckets(bucketsOnly) return h.healBuckets(objAPI, bucketsOnly)
} }
// traverseAndHeal - traverses on-disk data and performs healing // traverseAndHeal - traverses on-disk data and performs healing
@@ -797,26 +792,20 @@ func (h *healSequence) healItems(bucketsOnly bool) error {
// quit signal is received, this routine cannot quit immediately and // quit signal is received, this routine cannot quit immediately and
// has to wait until a safe point is reached, such as between scanning // has to wait until a safe point is reached, such as between scanning
// two objects. // two objects.
func (h *healSequence) traverseAndHeal() { func (h *healSequence) traverseAndHeal(objAPI ObjectLayer) {
bucketsOnly := false // Heals buckets and objects also. bucketsOnly := false // Heals buckets and objects also.
h.traverseAndHealDoneCh <- h.healItems(bucketsOnly) h.traverseAndHealDoneCh <- h.healItems(objAPI, bucketsOnly)
close(h.traverseAndHealDoneCh) close(h.traverseAndHealDoneCh)
} }
// healMinioSysMeta - heals all files under a given meta prefix, returns a function // healMinioSysMeta - heals all files under a given meta prefix, returns a function
// which in-turn heals the respective meta directory path and any files in int. // which in-turn heals the respective meta directory path and any files in int.
func (h *healSequence) healMinioSysMeta(metaPrefix string) func() error { func (h *healSequence) healMinioSysMeta(objAPI ObjectLayer, metaPrefix string) func() error {
return func() error { return func() error {
// Get current object layer instance.
objectAPI := newObjectLayerWithoutSafeModeFn()
if objectAPI == nil {
return errServerNotInitialized
}
// NOTE: Healing on meta is run regardless // NOTE: Healing on meta is run regardless
// of any bucket being selected, this is to ensure that // of any bucket being selected, this is to ensure that
// meta are always upto date and correct. // meta are always upto date and correct.
return objectAPI.HealObjects(h.ctx, minioMetaBucket, metaPrefix, h.settings, func(bucket, object, versionID string) error { return objAPI.HealObjects(h.ctx, minioMetaBucket, metaPrefix, h.settings, func(bucket, object, versionID string) error {
if h.isQuitting() { if h.isQuitting() {
return errHealStopSignalled return errHealStopSignalled
} }
@@ -843,39 +832,27 @@ func (h *healSequence) healDiskFormat() error {
return errHealStopSignalled return errHealStopSignalled
} }
// Get current object layer instance.
objectAPI := newObjectLayerWithoutSafeModeFn()
if objectAPI == nil {
return errServerNotInitialized
}
return h.queueHealTask(healSource{bucket: SlashSeparator}, madmin.HealItemMetadata) return h.queueHealTask(healSource{bucket: SlashSeparator}, madmin.HealItemMetadata)
} }
// healBuckets - check for all buckets heal or just particular bucket. // healBuckets - check for all buckets heal or just particular bucket.
func (h *healSequence) healBuckets(bucketsOnly bool) error { func (h *healSequence) healBuckets(objAPI ObjectLayer, bucketsOnly bool) error {
if h.isQuitting() { if h.isQuitting() {
return errHealStopSignalled return errHealStopSignalled
} }
// 1. If a bucket was specified, heal only the bucket. // 1. If a bucket was specified, heal only the bucket.
if h.bucket != "" { if h.bucket != "" {
return h.healBucket(h.bucket, bucketsOnly) return h.healBucket(objAPI, h.bucket, bucketsOnly)
} }
// Get current object layer instance. buckets, err := objAPI.ListBucketsHeal(h.ctx)
objectAPI := newObjectLayerWithoutSafeModeFn()
if objectAPI == nil {
return errServerNotInitialized
}
buckets, err := objectAPI.ListBucketsHeal(h.ctx)
if err != nil { if err != nil {
return errFnHealFromAPIErr(h.ctx, err) return errFnHealFromAPIErr(h.ctx, err)
} }
for _, bucket := range buckets { for _, bucket := range buckets {
if err = h.healBucket(bucket.Name, bucketsOnly); err != nil { if err = h.healBucket(objAPI, bucket.Name, bucketsOnly); err != nil {
return err return err
} }
} }
@@ -884,13 +861,7 @@ func (h *healSequence) healBuckets(bucketsOnly bool) error {
} }
// healBucket - traverses and heals given bucket // healBucket - traverses and heals given bucket
func (h *healSequence) healBucket(bucket string, bucketsOnly bool) error { func (h *healSequence) healBucket(objAPI ObjectLayer, bucket string, bucketsOnly bool) error {
// Get current object layer instance.
objectAPI := newObjectLayerWithoutSafeModeFn()
if objectAPI == nil {
return errServerNotInitialized
}
if err := h.queueHealTask(healSource{bucket: bucket}, madmin.HealItemBucket); err != nil { if err := h.queueHealTask(healSource{bucket: bucket}, madmin.HealItemBucket); err != nil {
if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) { if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
return err return err
@@ -905,7 +876,7 @@ func (h *healSequence) healBucket(bucket string, bucketsOnly bool) error {
if h.object != "" { if h.object != "" {
// Check if an object named as the objPrefix exists, // Check if an object named as the objPrefix exists,
// and if so heal it. // and if so heal it.
oi, err := objectAPI.GetObjectInfo(h.ctx, bucket, h.object, ObjectOptions{}) oi, err := objAPI.GetObjectInfo(h.ctx, bucket, h.object, ObjectOptions{})
if err == nil { if err == nil {
if err = h.healObject(bucket, h.object, oi.VersionID); err != nil { if err = h.healObject(bucket, h.object, oi.VersionID); err != nil {
if isErrObjectNotFound(err) || isErrVersionNotFound(err) { if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
@@ -919,7 +890,7 @@ func (h *healSequence) healBucket(bucket string, bucketsOnly bool) error {
return nil return nil
} }
if err := objectAPI.HealObjects(h.ctx, bucket, h.object, h.settings, h.healObject); err != nil { if err := objAPI.HealObjects(h.ctx, bucket, h.object, h.settings, h.healObject); err != nil {
// Object might have been deleted, by the time heal // Object might have been deleted, by the time heal
// was attempted we ignore this object an move on. // was attempted we ignore this object an move on.
if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) { if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
@@ -931,12 +902,6 @@ func (h *healSequence) healBucket(bucket string, bucketsOnly bool) error {
// healObject - heal the given object and record result // healObject - heal the given object and record result
func (h *healSequence) healObject(bucket, object, versionID string) error { func (h *healSequence) healObject(bucket, object, versionID string) error {
// Get current object layer instance.
objectAPI := newObjectLayerWithoutSafeModeFn()
if objectAPI == nil {
return errServerNotInitialized
}
if h.isQuitting() { if h.isQuitting() {
return errHealStopSignalled return errHealStopSignalled
} }
+18 -26
View File
@@ -180,19 +180,19 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
// PutBucketQuotaConfig // PutBucketQuotaConfig
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-bucket-quota").HandlerFunc( adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-bucket-quota").HandlerFunc(
httpTraceHdrs(adminAPI.PutBucketQuotaConfigHandler)).Queries("bucket", "{bucket:.*}") httpTraceHdrs(adminAPI.PutBucketQuotaConfigHandler)).Queries("bucket", "{bucket:.*}")
}
// Bucket replication operations
// GetBucketTargetHandler
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-remote-targets").HandlerFunc(
httpTraceHdrs(adminAPI.ListRemoteTargetsHandler)).Queries("bucket", "{bucket:.*}", "type", "{type:.*}")
// SetRemoteTargetHandler
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-remote-target").HandlerFunc(
httpTraceHdrs(adminAPI.SetRemoteTargetHandler)).Queries("bucket", "{bucket:.*}")
// SetRemoteTargetHandler
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-remote-target").HandlerFunc(
httpTraceHdrs(adminAPI.RemoveRemoteTargetHandler)).Queries("bucket", "{bucket:.*}", "arn", "{arn:.*}")
}
// Bucket replication operations
// GetBucketTargetHandler
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-remote-targets").HandlerFunc(
httpTraceHdrs(adminAPI.ListRemoteTargetsHandler)).Queries("bucket", "{bucket:.*}", "type", "{type:.*}")
// SetRemoteTargetHandler
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-remote-target").HandlerFunc(
httpTraceHdrs(adminAPI.SetRemoteTargetHandler)).Queries("bucket", "{bucket:.*}")
// SetRemoteTargetHandler
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-remote-target").HandlerFunc(
httpTraceHdrs(adminAPI.RemoveRemoteTargetHandler)).Queries("bucket", "{bucket:.*}", "arn", "{arn:.*}")
}
}
// -- Top APIs -- // -- Top APIs --
// Top locks // Top locks
if globalIsDistErasure { if globalIsDistErasure {
@@ -212,22 +212,14 @@ 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}", adminRouter.Methods(http.MethodGet).Path(adminVersion + "/bandwidth").
"perfnet", "{perfnet:true|false}", HandlerFunc(httpTraceHdrs(adminAPI.BandwidthMonitorHandler))
"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}",
)
} }
} }
// If none of the routes match add default error handler routes // If none of the routes match add default error handler routes
adminRouter.NotFoundHandler = http.HandlerFunc(httpTraceAll(errorResponseHandler)) adminRouter.NotFoundHandler = httpTraceAll(errorResponseHandler)
adminRouter.MethodNotAllowedHandler = http.HandlerFunc(httpTraceAll(errorResponseHandler)) adminRouter.MethodNotAllowedHandler = httpTraceAll(methodNotAllowedHandler("Admin"))
} }
+3 -3
View File
@@ -24,13 +24,13 @@ import (
// getLocalServerProperty - returns madmin.ServerProperties for only the // getLocalServerProperty - returns madmin.ServerProperties for only the
// local endpoints from given list of endpoints // local endpoints from given list of endpoints
func getLocalServerProperty(endpointZones EndpointZones, r *http.Request) madmin.ServerProperties { func getLocalServerProperty(endpointServerSets EndpointServerSets, r *http.Request) madmin.ServerProperties {
addr := r.Host addr := r.Host
if globalIsDistErasure { if globalIsDistErasure {
addr = GetLocalPeer(endpointZones) addr = GetLocalPeer(endpointServerSets)
} }
network := make(map[string]string) network := make(map[string]string)
for _, ep := range endpointZones { for _, ep := range endpointServerSets {
for _, endpoint := range ep.Endpoints { for _, endpoint := range ep.Endpoints {
nodeName := endpoint.Host nodeName := endpoint.Host
if nodeName == "" { if nodeName == "" {
+54 -22
View File
@@ -106,16 +106,17 @@ const (
ErrNoSuchCORSConfiguration ErrNoSuchCORSConfiguration
ErrNoSuchWebsiteConfiguration ErrNoSuchWebsiteConfiguration
ErrReplicationConfigurationNotFoundError ErrReplicationConfigurationNotFoundError
ErrReplicationDestinationNotFoundError ErrRemoteDestinationNotFoundError
ErrReplicationDestinationMissingLock ErrReplicationDestinationMissingLock
ErrReplicationTargetNotFoundError ErrRemoteTargetNotFoundError
ErrReplicationRemoteConnectionError ErrReplicationRemoteConnectionError
ErrBucketRemoteIdenticalToSource ErrBucketRemoteIdenticalToSource
ErrBucketRemoteAlreadyExists ErrBucketRemoteAlreadyExists
ErrBucketRemoteLabelInUse
ErrBucketRemoteArnTypeInvalid ErrBucketRemoteArnTypeInvalid
ErrBucketRemoteArnInvalid ErrBucketRemoteArnInvalid
ErrBucketRemoteRemoveDisallowed ErrBucketRemoteRemoveDisallowed
ErrReplicationTargetNotVersionedError ErrRemoteTargetNotVersionedError
ErrReplicationSourceNotVersionedError ErrReplicationSourceNotVersionedError
ErrReplicationNeedsVersioningError ErrReplicationNeedsVersioningError
ErrReplicationBucketNeedsVersioningError ErrReplicationBucketNeedsVersioningError
@@ -232,6 +233,7 @@ const (
ErrInvalidResourceName ErrInvalidResourceName
ErrServerNotInitialized ErrServerNotInitialized
ErrOperationTimedOut ErrOperationTimedOut
ErrClientDisconnected
ErrOperationMaxedOut ErrOperationMaxedOut
ErrInvalidRequest ErrInvalidRequest
// MinIO storage class error codes // MinIO storage class error codes
@@ -809,9 +811,9 @@ var errorCodes = errorCodeMap{
Description: "The replication configuration was not found", Description: "The replication configuration was not found",
HTTPStatusCode: http.StatusNotFound, HTTPStatusCode: http.StatusNotFound,
}, },
ErrReplicationDestinationNotFoundError: { ErrRemoteDestinationNotFoundError: {
Code: "ReplicationDestinationNotFoundError", Code: "RemoteDestinationNotFoundError",
Description: "The replication destination bucket does not exist", Description: "The remote destination bucket does not exist",
HTTPStatusCode: http.StatusNotFound, HTTPStatusCode: http.StatusNotFound,
}, },
ErrReplicationDestinationMissingLock: { ErrReplicationDestinationMissingLock: {
@@ -819,29 +821,34 @@ var errorCodes = errorCodeMap{
Description: "The replication destination bucket does not have object locking enabled", Description: "The replication destination bucket does not have object locking enabled",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrReplicationTargetNotFoundError: { ErrRemoteTargetNotFoundError: {
Code: "XminioAdminReplicationTargetNotFoundError", Code: "XMinioAdminRemoteTargetNotFoundError",
Description: "The replication target does not exist", Description: "The remote target does not exist",
HTTPStatusCode: http.StatusNotFound, HTTPStatusCode: http.StatusNotFound,
}, },
ErrReplicationRemoteConnectionError: { ErrReplicationRemoteConnectionError: {
Code: "XminioAdminReplicationRemoteConnectionError", Code: "XMinioAdminReplicationRemoteConnectionError",
Description: "Remote service endpoint or target bucket not available", Description: "Remote service endpoint or target bucket not available",
HTTPStatusCode: http.StatusNotFound, 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",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrBucketRemoteAlreadyExists: { ErrBucketRemoteAlreadyExists: {
Code: "XminioAdminBucketRemoteAlreadyExists", Code: "XMinioAdminBucketRemoteAlreadyExists",
Description: "The remote target already exists", Description: "The remote target already exists",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrBucketRemoteLabelInUse: {
Code: "XMinioAdminBucketRemoteLabelInUse",
Description: "The remote target with this label already exists",
HTTPStatusCode: http.StatusBadRequest,
},
ErrBucketRemoteRemoveDisallowed: { ErrBucketRemoteRemoveDisallowed: {
Code: "XMinioAdminRemoteRemoveDisallowed", Code: "XMinioAdminRemoteRemoveDisallowed",
Description: "Replication configuration exists with this ARN.", Description: "This ARN is in use by an existing configuration",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrBucketRemoteArnTypeInvalid: { ErrBucketRemoteArnTypeInvalid: {
@@ -854,9 +861,9 @@ var errorCodes = errorCodeMap{
Description: "The bucket remote ARN does not have correct format", Description: "The bucket remote ARN does not have correct format",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrReplicationTargetNotVersionedError: { ErrRemoteTargetNotVersionedError: {
Code: "ReplicationTargetNotVersionedError", Code: "RemoteTargetNotVersionedError",
Description: "The replication target does not have versioning enabled", Description: "The remote target does not have versioning enabled",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrReplicationSourceNotVersionedError: { ErrReplicationSourceNotVersionedError: {
@@ -971,7 +978,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,
}, },
@@ -1210,6 +1217,11 @@ var errorCodes = errorCodeMap{
Description: "A timeout occurred while trying to lock a resource, please reduce your request rate", Description: "A timeout occurred while trying to lock a resource, please reduce your request rate",
HTTPStatusCode: http.StatusServiceUnavailable, HTTPStatusCode: http.StatusServiceUnavailable,
}, },
ErrClientDisconnected: {
Code: "ClientDisconnected",
Description: "Client disconnected before response was ready",
HTTPStatusCode: 499, // No official code, use nginx value.
},
ErrOperationMaxedOut: { ErrOperationMaxedOut: {
Code: "SlowDown", Code: "SlowDown",
Description: "A timeout exceeded while waiting to proceed with the request, please reduce your request rate", Description: "A timeout exceeded while waiting to proceed with the request, please reduce your request rate",
@@ -1736,6 +1748,16 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
return ErrNone return ErrNone
} }
// Only return ErrClientDisconnected if the provided context is actually canceled.
// This way downstream context.Canceled will still report ErrOperationTimedOut
select {
case <-ctx.Done():
if ctx.Err() == context.Canceled {
return ErrClientDisconnected
}
default:
}
switch err { switch err {
case errInvalidArgument: case errInvalidArgument:
apiErr = ErrAdminInvalidArgument apiErr = ErrAdminInvalidArgument
@@ -1906,24 +1928,26 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
apiErr = ErrAdminNoSuchQuotaConfiguration apiErr = ErrAdminNoSuchQuotaConfiguration
case BucketReplicationConfigNotFound: case BucketReplicationConfigNotFound:
apiErr = ErrReplicationConfigurationNotFoundError apiErr = ErrReplicationConfigurationNotFoundError
case BucketReplicationDestinationNotFound: case BucketRemoteDestinationNotFound:
apiErr = ErrReplicationDestinationNotFoundError apiErr = ErrRemoteDestinationNotFoundError
case BucketReplicationDestinationMissingLock: case BucketReplicationDestinationMissingLock:
apiErr = ErrReplicationDestinationMissingLock apiErr = ErrReplicationDestinationMissingLock
case BucketRemoteTargetNotFound: case BucketRemoteTargetNotFound:
apiErr = ErrReplicationTargetNotFoundError apiErr = ErrRemoteTargetNotFoundError
case BucketRemoteConnectionErr: case BucketRemoteConnectionErr:
apiErr = ErrReplicationRemoteConnectionError apiErr = ErrReplicationRemoteConnectionError
case BucketRemoteAlreadyExists: case BucketRemoteAlreadyExists:
apiErr = ErrBucketRemoteAlreadyExists apiErr = ErrBucketRemoteAlreadyExists
case BucketRemoteLabelInUse:
apiErr = ErrBucketRemoteLabelInUse
case BucketRemoteArnTypeInvalid: case BucketRemoteArnTypeInvalid:
apiErr = ErrBucketRemoteArnTypeInvalid apiErr = ErrBucketRemoteArnTypeInvalid
case BucketRemoteArnInvalid: case BucketRemoteArnInvalid:
apiErr = ErrBucketRemoteArnInvalid apiErr = ErrBucketRemoteArnInvalid
case BucketRemoteRemoveDisallowed: case BucketRemoteRemoveDisallowed:
apiErr = ErrBucketRemoteRemoveDisallowed apiErr = ErrBucketRemoteRemoveDisallowed
case BucketReplicationTargetNotVersioned: case BucketRemoteTargetNotVersioned:
apiErr = ErrReplicationTargetNotVersionedError apiErr = ErrRemoteTargetNotVersionedError
case BucketReplicationSourceNotVersioned: case BucketReplicationSourceNotVersioned:
apiErr = ErrReplicationSourceNotVersionedError apiErr = ErrReplicationSourceNotVersionedError
case BucketQuotaExceeded: case BucketQuotaExceeded:
@@ -1956,6 +1980,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
apiErr = ErrBackendDown apiErr = ErrBackendDown
case ObjectNameTooLong: case ObjectNameTooLong:
apiErr = ErrKeyTooLongError apiErr = ErrKeyTooLongError
case dns.ErrInvalidBucketName:
apiErr = ErrInvalidBucketName
default: default:
var ie, iw int var ie, iw int
// This work-around is to handle the issue golang/go#30648 // This work-around is to handle the issue golang/go#30648
@@ -1992,6 +2018,12 @@ func toAPIError(ctx context.Context, err error) APIError {
} }
var apiErr = errorCodes.ToAPIErr(toAPIErrorCode(ctx, err)) var apiErr = errorCodes.ToAPIErr(toAPIErrorCode(ctx, err))
e, ok := err.(dns.ErrInvalidBucketName)
if ok {
code := toAPIErrorCode(ctx, e)
apiErr = errorCodes.ToAPIErrWithErr(code, e)
}
if apiErr.Code == "InternalError" { if apiErr.Code == "InternalError" {
// If we see an internal error try to interpret // If we see an internal error try to interpret
// any underlying errors if possible depending on // any underlying errors if possible depending on
+21 -9
View File
@@ -84,7 +84,7 @@ func setPartsCountHeaders(w http.ResponseWriter, objInfo ObjectInfo) {
} }
// Write object header // Write object header
func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSpec) (err error) { func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSpec, opts ObjectOptions) (err error) {
// set common headers // set common headers
setCommonHeaders(w) setCommonHeaders(w)
@@ -115,10 +115,11 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSp
} }
// Set tag count if object has tags // Set tag count if object has tags
tags, _ := url.ParseQuery(objInfo.UserTags) if len(objInfo.UserTags) > 0 {
tagCount := len(tags) tags, _ := url.ParseQuery(objInfo.UserTags)
if tagCount > 0 { if len(tags) > 0 {
w.Header()[xhttp.AmzTagCount] = []string{strconv.Itoa(tagCount)} w.Header()[xhttp.AmzTagCount] = []string{strconv.Itoa(len(tags))}
}
} }
// Set all other user defined metadata. // Set all other user defined metadata.
@@ -147,15 +148,26 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSp
} }
} }
var start, rangeLen int64
totalObjectSize, err := objInfo.GetActualSize() totalObjectSize, err := objInfo.GetActualSize()
if err != nil { if err != nil {
return err return err
} }
// for providing ranged content if opts.PartNumber > 0 {
start, rangeLen, err := rs.GetOffsetLength(totalObjectSize) var start, end int64
if err != nil { for i := 0; i < len(objInfo.Parts) && i < opts.PartNumber; i++ {
return err start = end
end = start + objInfo.Parts[i].ActualSize - 1
}
rs = &HTTPRangeSpec{Start: start, End: end}
rangeLen = end - start + 1
} else {
// for providing ranged content
start, rangeLen, err = rs.GetOffsetLength(totalObjectSize)
if err != nil {
return err
}
} }
// Set content length. // Set content length.
+14 -21
View File
@@ -35,11 +35,11 @@ import (
const ( const (
// RFC3339 a subset of the ISO8601 timestamp format. e.g 2014-04-29T18:30:38Z // RFC3339 a subset of the ISO8601 timestamp format. e.g 2014-04-29T18:30:38Z
iso8601TimeFormat = "2006-01-02T15:04:05.000Z" // Reply date format with nanosecond precision. iso8601TimeFormat = "2006-01-02T15:04:05.000Z" // Reply date format with nanosecond precision.
maxObjectList = 1000 // Limit number of objects in a listObjectsResponse/listObjectsVersionsResponse. maxObjectList = metacacheBlockSize - (metacacheBlockSize / 10) // Limit number of objects in a listObjectsResponse/listObjectsVersionsResponse.
maxDeleteList = 10000 // Limit number of objects deleted in a delete call. maxDeleteList = 10000 // Limit number of objects deleted in a delete call.
maxUploadsList = 10000 // Limit number of uploads in a listUploadsResponse. maxUploadsList = 10000 // Limit number of uploads in a listUploadsResponse.
maxPartsList = 10000 // Limit number of parts in a listPartsResponse. maxPartsList = 10000 // Limit number of parts in a listPartsResponse.
) )
// LocationResponse - format for location response. // LocationResponse - format for location response.
@@ -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)
@@ -702,10 +703,6 @@ func generateMultiDeleteResponse(quiet bool, deletedObjects []DeletedObject, err
} }
func writeResponse(w http.ResponseWriter, statusCode int, response []byte, mType mimeType) { func writeResponse(w http.ResponseWriter, statusCode int, response []byte, mType mimeType) {
if newObjectLayerFn() == nil {
// Server still in safe mode.
w.Header().Set(xhttp.MinIOServerStatus, "safemode")
}
setCommonHeaders(w) setCommonHeaders(w)
if mType != mimeNone { if mType != mimeNone {
w.Header().Set(xhttp.ContentType, string(mType)) w.Header().Set(xhttp.ContentType, string(mType))
@@ -772,10 +769,6 @@ func writeErrorResponse(ctx context.Context, w http.ResponseWriter, err APIError
// The request is from browser and also if browser // The request is from browser and also if browser
// is enabled we need to redirect. // is enabled we need to redirect.
if browser && globalBrowserEnabled { if browser && globalBrowserEnabled {
if newObjectLayerFn() == nil {
// server still in safe mode.
w.Header().Set(xhttp.MinIOServerStatus, "safemode")
}
w.Header().Set(xhttp.Location, minioReservedBucketPath+reqURL.Path) w.Header().Set(xhttp.Location, minioReservedBucketPath+reqURL.Path)
w.WriteHeader(http.StatusTemporaryRedirect) w.WriteHeader(http.StatusTemporaryRedirect)
return return
+13 -27
View File
@@ -32,39 +32,28 @@ func newHTTPServerFn() *xhttp.Server {
return globalHTTPServer return globalHTTPServer
} }
func newObjectLayerWithoutSafeModeFn() ObjectLayer {
globalObjLayerMutex.Lock()
defer globalObjLayerMutex.Unlock()
return globalObjectAPI
}
func newObjectLayerFn() ObjectLayer { func newObjectLayerFn() ObjectLayer {
globalObjLayerMutex.Lock() globalObjLayerMutex.Lock()
defer globalObjLayerMutex.Unlock() defer globalObjLayerMutex.Unlock()
if globalSafeMode {
return nil
}
return globalObjectAPI return globalObjectAPI
} }
func newCachedObjectLayerFn() CacheObjectLayer { func newCachedObjectLayerFn() CacheObjectLayer {
globalObjLayerMutex.Lock() globalObjLayerMutex.Lock()
defer globalObjLayerMutex.Unlock() defer globalObjLayerMutex.Unlock()
if globalSafeMode {
return nil
}
return globalCacheObjectAPI return globalCacheObjectAPI
} }
func setObjectLayer(o ObjectLayer) {
globalObjLayerMutex.Lock()
globalObjectAPI = o
globalObjLayerMutex.Unlock()
}
// objectAPIHandler implements and provides http handlers for S3 API. // objectAPIHandler implements and provides http handlers for S3 API.
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.
AllowSSEKMS func() bool
} }
// getHost tries its best to return the request host. // getHost tries its best to return the request host.
@@ -78,17 +67,11 @@ func getHost(r *http.Request) string {
} }
// 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
@@ -98,7 +81,10 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool)
for _, domainName := range globalDomainNames { for _, domainName := range globalDomainNames {
if IsKubernetes() { if IsKubernetes() {
routers = append(routers, apiRouter.MatcherFunc(func(r *http.Request, match *mux.RouteMatch) bool { routers = append(routers, apiRouter.MatcherFunc(func(r *http.Request, match *mux.RouteMatch) bool {
host, _, _ := net.SplitHostPort(getHost(r)) host, _, err := net.SplitHostPort(getHost(r))
if err != nil {
host = r.Host
}
// Make sure to skip matching minio.<domain>` this is // Make sure to skip matching minio.<domain>` this is
// specifically meant for operator/k8s deployment // specifically meant for operator/k8s deployment
// The reason we need to skip this is for a special // The reason we need to skip this is for a special
@@ -339,8 +325,8 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool)
maxClients(collectAPIStats("listbuckets", httpTraceAll(api.ListBucketsHandler)))) maxClients(collectAPIStats("listbuckets", httpTraceAll(api.ListBucketsHandler))))
// If none of the routes match add default error handler routes // If none of the routes match add default error handler routes
apiRouter.NotFoundHandler = http.HandlerFunc(collectAPIStats("notfound", httpTraceAll(errorResponseHandler))) apiRouter.NotFoundHandler = collectAPIStats("notfound", httpTraceAll(errorResponseHandler))
apiRouter.MethodNotAllowedHandler = http.HandlerFunc(collectAPIStats("methodnotallowed", httpTraceAll(errorResponseHandler))) apiRouter.MethodNotAllowedHandler = collectAPIStats("methodnotallowed", httpTraceAll(methodNotAllowedHandler("S3")))
} }
+16 -7
View File
@@ -333,8 +333,12 @@ func checkRequestAuthTypeToAccessKey(ctx context.Context, r *http.Request, actio
// Populate payload again to handle it in HTTP handler. // Populate payload again to handle it in HTTP handler.
r.Body = ioutil.NopCloser(bytes.NewReader(payload)) r.Body = ioutil.NopCloser(bytes.NewReader(payload))
} }
if cred.AccessKey != "" {
logger.GetReqInfo(ctx).AccessKey = cred.AccessKey
}
if cred.AccessKey == "" { if action != policy.ListAllMyBucketsAction && cred.AccessKey == "" {
// Anonymous checks are not meant for ListBuckets action
if globalPolicySys.IsAllowed(policy.Args{ if globalPolicySys.IsAllowed(policy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
Action: action, Action: action,
@@ -378,12 +382,13 @@ 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 { if action == policy.ListBucketVersionsAction {
// In AWS S3 s3:ListBucket permission is same as s3:ListBucketVersions permission // In AWS S3 s3:ListBucket permission is same as s3:ListBucketVersions permission
// verify as a fallback. // verify as a fallback.
if globalIAMSys.IsAllowed(iampolicy.Args{ if globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
Action: iampolicy.Action(policy.ListBucketAction), Action: iampolicy.ListBucketAction,
BucketName: bucketName, BucketName: bucketName,
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims), ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
ObjectName: objectName, ObjectName: objectName,
@@ -554,7 +559,7 @@ func isPutRetentionAllowed(bucketName, objectName string, retDays int, retDate t
if retMode == objectlock.RetGovernance && byPassSet { if retMode == objectlock.RetGovernance && byPassSet {
byPassSet = globalPolicySys.IsAllowed(policy.Args{ byPassSet = globalPolicySys.IsAllowed(policy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
Action: policy.Action(policy.BypassGovernanceRetentionAction), Action: policy.BypassGovernanceRetentionAction,
BucketName: bucketName, BucketName: bucketName,
ConditionValues: conditions, ConditionValues: conditions,
IsOwner: false, IsOwner: false,
@@ -563,7 +568,7 @@ func isPutRetentionAllowed(bucketName, objectName string, retDays int, retDate t
} }
if globalPolicySys.IsAllowed(policy.Args{ if globalPolicySys.IsAllowed(policy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
Action: policy.Action(policy.PutObjectRetentionAction), Action: policy.PutObjectRetentionAction,
BucketName: bucketName, BucketName: bucketName,
ConditionValues: conditions, ConditionValues: conditions,
IsOwner: false, IsOwner: false,
@@ -586,7 +591,7 @@ func isPutRetentionAllowed(bucketName, objectName string, retDays int, retDate t
if retMode == objectlock.RetGovernance && byPassSet { if retMode == objectlock.RetGovernance && byPassSet {
byPassSet = globalIAMSys.IsAllowed(iampolicy.Args{ byPassSet = globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
Action: policy.BypassGovernanceRetentionAction, Action: iampolicy.BypassGovernanceRetentionAction,
BucketName: bucketName, BucketName: bucketName,
ObjectName: objectName, ObjectName: objectName,
ConditionValues: conditions, ConditionValues: conditions,
@@ -596,7 +601,7 @@ func isPutRetentionAllowed(bucketName, objectName string, retDays int, retDate t
} }
if globalIAMSys.IsAllowed(iampolicy.Args{ if globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
Action: policy.PutObjectRetentionAction, Action: iampolicy.PutObjectRetentionAction,
BucketName: bucketName, BucketName: bucketName,
ConditionValues: conditions, ConditionValues: conditions,
ObjectName: objectName, ObjectName: objectName,
@@ -614,7 +619,7 @@ func isPutRetentionAllowed(bucketName, objectName string, retDays int, retDate t
// isPutActionAllowed - check if PUT operation is allowed on the resource, this // isPutActionAllowed - check if PUT operation is allowed on the resource, this
// call verifies bucket policies and IAM policies, supports multi user // call verifies bucket policies and IAM policies, supports multi user
// checks etc. // checks etc.
func isPutActionAllowed(atype authType, bucketName, objectName string, r *http.Request, action iampolicy.Action) (s3Err APIErrorCode) { func isPutActionAllowed(ctx context.Context, atype authType, bucketName, objectName string, r *http.Request, action iampolicy.Action) (s3Err APIErrorCode) {
var cred auth.Credentials var cred auth.Credentials
var owner bool var owner bool
switch atype { switch atype {
@@ -635,6 +640,10 @@ func isPutActionAllowed(atype authType, bucketName, objectName string, r *http.R
return s3Err return s3Err
} }
if cred.AccessKey != "" {
logger.GetReqInfo(ctx).AccessKey = cred.AccessKey
}
// Do not check for PutObjectRetentionAction permission, // Do not check for PutObjectRetentionAction permission,
// if mode and retain until date are not set. // if mode and retain until date are not set.
// Can happen when bucket has default lock config set // Can happen when bucket has default lock config set
-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
} }
+69 -32
View File
@@ -26,10 +26,20 @@ import (
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
) )
const defaultMonitorNewDiskInterval = time.Second * 10 const (
defaultMonitorNewDiskInterval = time.Second * 10
healingTrackerFilename = ".healing.bin"
)
//go:generate msgp -file $GOFILE -unexported
type healingTracker struct {
ID string
// future add more tracking capabilities
}
func initAutoHeal(ctx context.Context, objAPI ObjectLayer) { func initAutoHeal(ctx context.Context, objAPI ObjectLayer) {
z, ok := objAPI.(*erasureZones) z, ok := objAPI.(*erasureServerSets)
if !ok { if !ok {
return return
} }
@@ -47,16 +57,15 @@ func initAutoHeal(ctx context.Context, objAPI ObjectLayer) {
time.Sleep(time.Second) time.Sleep(time.Second)
} }
for _, ep := range getLocalDisksToHeal() { globalBackgroundHealState.pushHealLocalDisks(getLocalDisksToHeal()...)
globalBackgroundHealState.pushHealLocalDisks(ep)
}
if drivesToHeal := globalBackgroundHealState.healDriveCount(); drivesToHeal > 0 { if drivesToHeal := globalBackgroundHealState.healDriveCount(); drivesToHeal > 0 {
logger.Info(fmt.Sprintf("Found drives to heal %d, waiting until %s to heal the content...", logger.Info(fmt.Sprintf("Found drives to heal %d, waiting until %s to heal the content...",
drivesToHeal, defaultMonitorNewDiskInterval)) 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 { // Start with format healing
if err := bgSeq.healDiskFormat(); err != nil {
if newObjectLayerFn() != nil { if newObjectLayerFn() != nil {
// log only in situations, when object layer // log only in situations, when object layer
// has fully initialized. // has fully initialized.
@@ -65,6 +74,14 @@ func initAutoHeal(ctx context.Context, objAPI ObjectLayer) {
} }
} }
if err := bgSeq.healDiskMeta(objAPI); err != nil {
if newObjectLayerFn() != nil {
// log only in situations, when object layer
// has fully initialized.
logger.LogIf(bgSeq.ctx, err)
}
}
go monitorLocalDisksAndHeal(ctx, z, bgSeq) go monitorLocalDisksAndHeal(ctx, z, bgSeq)
} }
@@ -76,9 +93,11 @@ func getLocalDisksToHeal() (disksToHeal Endpoints) {
} }
// Try to connect to the current endpoint // Try to connect to the current endpoint
// and reformat if the current disk is not formatted // and reformat if the current disk is not formatted
_, _, err := connectEndpoint(endpoint) disk, _, err := connectEndpoint(endpoint)
if errors.Is(err, errUnformattedDisk) { if errors.Is(err, errUnformattedDisk) {
disksToHeal = append(disksToHeal, endpoint) disksToHeal = append(disksToHeal, endpoint)
} else if err == nil && disk != nil && disk.Healing() {
disksToHeal = append(disksToHeal, disk.Endpoint())
} }
} }
} }
@@ -91,13 +110,13 @@ func initBackgroundHealing(ctx context.Context, objAPI ObjectLayer) {
globalBackgroundHealRoutine = newHealRoutine() globalBackgroundHealRoutine = newHealRoutine()
go globalBackgroundHealRoutine.run(ctx, objAPI) go globalBackgroundHealRoutine.run(ctx, objAPI)
globalBackgroundHealState.LaunchNewHealSequence(newBgHealSequence()) globalBackgroundHealState.LaunchNewHealSequence(newBgHealSequence(), objAPI)
} }
// 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, bgSeq *healSequence) { func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerSets, 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 {
@@ -106,56 +125,74 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureZones, bgSeq *healS
case <-time.After(defaultMonitorNewDiskInterval): case <-time.After(defaultMonitorNewDiskInterval):
waitForLowHTTPReq(int32(globalEndpoints.NEndpoints()), time.Second) waitForLowHTTPReq(int32(globalEndpoints.NEndpoints()), time.Second)
var erasureSetInZoneEndpointToHeal = make([]map[int]Endpoint, len(z.zones)) var erasureSetInZoneDisksToHeal []map[int][]StorageAPI
for i := range z.zones {
erasureSetInZoneEndpointToHeal[i] = map[int]Endpoint{}
}
healDisks := globalBackgroundHealState.getHealLocalDisks() healDisks := globalBackgroundHealState.getHealLocalDisks()
// heal only if new disks found. if len(healDisks) > 0 {
for _, endpoint := range healDisks {
logger.Info(fmt.Sprintf("Found drives to heal %d, proceeding to heal content...",
len(healDisks)))
// 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}
// Load the new format of this passed endpoint logger.Info(fmt.Sprintf("Found drives to heal %d, proceeding to heal content...",
_, format, err := connectEndpoint(endpoint) len(healDisks)))
erasureSetInZoneDisksToHeal = make([]map[int][]StorageAPI, len(z.serverSets))
for i := range z.serverSets {
erasureSetInZoneDisksToHeal[i] = map[int][]StorageAPI{}
}
}
// heal only if new disks found.
for _, endpoint := range healDisks {
disk, format, err := connectEndpoint(endpoint)
if err != nil { if err != nil {
printEndpointError(endpoint, err, true) printEndpointError(endpoint, err, true)
continue continue
} }
zoneIdx := globalEndpoints.GetLocalZoneIdx(endpoint) zoneIdx := globalEndpoints.GetLocalZoneIdx(disk.Endpoint())
if zoneIdx < 0 { if zoneIdx < 0 {
continue continue
} }
// Calculate the set index where the current endpoint belongs // Calculate the set index where the current endpoint belongs
setIndex, _, err := findDiskIndex(z.zones[zoneIdx].format, format) z.serverSets[zoneIdx].erasureDisksMu.RLock()
// Protect reading reference format.
setIndex, _, err := findDiskIndex(z.serverSets[zoneIdx].format, format)
z.serverSets[zoneIdx].erasureDisksMu.RUnlock()
if err != nil { if err != nil {
printEndpointError(endpoint, err, false) printEndpointError(endpoint, err, false)
continue continue
} }
erasureSetInZoneEndpointToHeal[zoneIdx][setIndex] = endpoint erasureSetInZoneDisksToHeal[zoneIdx][setIndex] = append(erasureSetInZoneDisksToHeal[zoneIdx][setIndex], disk)
} }
for i, setMap := range erasureSetInZoneEndpointToHeal { buckets, _ := z.ListBucketsHeal(ctx)
for setIndex, endpoint := range setMap { for i, setMap := range erasureSetInZoneDisksToHeal {
logger.Info("Healing disk '%s' on %s zone", endpoint, humanize.Ordinal(i+1)) for setIndex, disks := range setMap {
for _, disk := range disks {
logger.Info("Healing disk '%s' on %s zone", disk, humanize.Ordinal(i+1))
if err := healErasureSet(ctx, setIndex, z.zones[i].sets[setIndex], z.zones[i].setDriveCount); err != nil { lbDisks := z.serverSets[i].sets[setIndex].getOnlineDisks()
logger.LogIf(ctx, err) if err := healErasureSet(ctx, setIndex, buckets, lbDisks); err != nil {
continue logger.LogIf(ctx, err)
continue
}
logger.Info("Healing disk '%s' on %s zone complete", disk, humanize.Ordinal(i+1))
if err := disk.Delete(ctx, pathJoin(minioMetaBucket, bucketMetaPrefix),
healingTrackerFilename, false); err != nil && !errors.Is(err, errFileNotFound) {
logger.LogIf(ctx, err)
continue
}
// Only upon success pop the healed disk.
globalBackgroundHealState.popHealLocalDisks(disk.Endpoint())
} }
// Only upon success pop the healed disk.
globalBackgroundHealState.popHealLocalDisks(endpoint)
} }
} }
} }
+110
View File
@@ -0,0 +1,110 @@
package cmd
// Code generated by github.com/tinylib/msgp DO NOT EDIT.
import (
"github.com/tinylib/msgp/msgp"
)
// DecodeMsg implements msgp.Decodable
func (z *healingTracker) DecodeMsg(dc *msgp.Reader) (err error) {
var field []byte
_ = field
var zb0001 uint32
zb0001, err = dc.ReadMapHeader()
if err != nil {
err = msgp.WrapError(err)
return
}
for zb0001 > 0 {
zb0001--
field, err = dc.ReadMapKeyPtr()
if err != nil {
err = msgp.WrapError(err)
return
}
switch msgp.UnsafeString(field) {
case "ID":
z.ID, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "ID")
return
}
default:
err = dc.Skip()
if err != nil {
err = msgp.WrapError(err)
return
}
}
}
return
}
// EncodeMsg implements msgp.Encodable
func (z healingTracker) EncodeMsg(en *msgp.Writer) (err error) {
// map header, size 1
// write "ID"
err = en.Append(0x81, 0xa2, 0x49, 0x44)
if err != nil {
return
}
err = en.WriteString(z.ID)
if err != nil {
err = msgp.WrapError(err, "ID")
return
}
return
}
// MarshalMsg implements msgp.Marshaler
func (z healingTracker) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize())
// map header, size 1
// string "ID"
o = append(o, 0x81, 0xa2, 0x49, 0x44)
o = msgp.AppendString(o, z.ID)
return
}
// UnmarshalMsg implements msgp.Unmarshaler
func (z *healingTracker) UnmarshalMsg(bts []byte) (o []byte, err error) {
var field []byte
_ = field
var zb0001 uint32
zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
for zb0001 > 0 {
zb0001--
field, bts, err = msgp.ReadMapKeyZC(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
switch msgp.UnsafeString(field) {
case "ID":
z.ID, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "ID")
return
}
default:
bts, err = msgp.Skip(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
}
}
o = bts
return
}
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z healingTracker) Msgsize() (s int) {
s = 1 + 3 + msgp.StringPrefixSize + len(z.ID)
return
}
@@ -0,0 +1,123 @@
package cmd
// Code generated by github.com/tinylib/msgp DO NOT EDIT.
import (
"bytes"
"testing"
"github.com/tinylib/msgp/msgp"
)
func TestMarshalUnmarshalhealingTracker(t *testing.T) {
v := healingTracker{}
bts, err := v.MarshalMsg(nil)
if err != nil {
t.Fatal(err)
}
left, err := v.UnmarshalMsg(bts)
if err != nil {
t.Fatal(err)
}
if len(left) > 0 {
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
}
left, err = msgp.Skip(bts)
if err != nil {
t.Fatal(err)
}
if len(left) > 0 {
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
}
}
func BenchmarkMarshalMsghealingTracker(b *testing.B) {
v := healingTracker{}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v.MarshalMsg(nil)
}
}
func BenchmarkAppendMsghealingTracker(b *testing.B) {
v := healingTracker{}
bts := make([]byte, 0, v.Msgsize())
bts, _ = v.MarshalMsg(bts[0:0])
b.SetBytes(int64(len(bts)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
bts, _ = v.MarshalMsg(bts[0:0])
}
}
func BenchmarkUnmarshalhealingTracker(b *testing.B) {
v := healingTracker{}
bts, _ := v.MarshalMsg(nil)
b.ReportAllocs()
b.SetBytes(int64(len(bts)))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := v.UnmarshalMsg(bts)
if err != nil {
b.Fatal(err)
}
}
}
func TestEncodeDecodehealingTracker(t *testing.T) {
v := healingTracker{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
m := v.Msgsize()
if buf.Len() > m {
t.Log("WARNING: TestEncodeDecodehealingTracker Msgsize() is inaccurate")
}
vn := healingTracker{}
err := msgp.Decode(&buf, &vn)
if err != nil {
t.Error(err)
}
buf.Reset()
msgp.Encode(&buf, &v)
err = msgp.NewReader(&buf).Skip()
if err != nil {
t.Error(err)
}
}
func BenchmarkEncodehealingTracker(b *testing.B) {
v := healingTracker{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
b.SetBytes(int64(buf.Len()))
en := msgp.NewWriter(msgp.Nowhere)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v.EncodeMsg(en)
}
en.Flush()
}
func BenchmarkDecodehealingTracker(b *testing.B) {
v := healingTracker{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
b.SetBytes(int64(buf.Len()))
rd := msgp.NewEndlessReader(buf.Bytes(), b)
dc := msgp.NewReader(rd)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
err := v.DecodeMsg(dc)
if err != nil {
b.Fatal(err)
}
}
}
+2 -2
View File
@@ -140,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
} }
+2 -2
View File
@@ -71,12 +71,12 @@ 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(context.TODO(), 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)
+8 -28
View File
@@ -18,9 +18,7 @@ package cmd
import ( import (
"context" "context"
"crypto/tls"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@@ -54,7 +52,7 @@ type bootstrapRESTServer struct{}
type ServerSystemConfig struct { type ServerSystemConfig struct {
MinioPlatform string MinioPlatform string
MinioRuntime string MinioRuntime string
MinioEndpoints EndpointZones MinioEndpoints EndpointServerSets
} }
// Diff - returns error on first difference found in two configs. // Diff - returns error on first difference found in two configs.
@@ -161,9 +159,9 @@ func (client *bootstrapRESTClient) Verify(ctx context.Context, srcCfg ServerSyst
return srcCfg.Diff(recvCfg) return srcCfg.Diff(recvCfg)
} }
func verifyServerSystemConfig(ctx context.Context, endpointZones EndpointZones) error { func verifyServerSystemConfig(ctx context.Context, endpointServerSets EndpointServerSets) error {
srcCfg := getServerSystemCfg() srcCfg := getServerSystemCfg()
clnts := newBootstrapRESTClients(endpointZones) clnts := newBootstrapRESTClients(endpointServerSets)
var onlineServers int var onlineServers int
var offlineEndpoints []string var offlineEndpoints []string
var retries int var retries int
@@ -188,7 +186,7 @@ func verifyServerSystemConfig(ctx context.Context, endpointZones EndpointZones)
retries++ retries++
// after 5 retries start logging that servers are not reachable yet // after 5 retries start logging that servers are not reachable yet
if retries >= 5 { if retries >= 5 {
logger.Info(fmt.Sprintf("Waiting for atleast %d servers to be online for bootstrap check", len(clnts)/2)) 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)) logger.Info(fmt.Sprintf("Following servers are currently offline or unreachable %s", offlineEndpoints))
retries = 0 // reset to log again after 5 retries. retries = 0 // reset to log again after 5 retries.
} }
@@ -198,10 +196,10 @@ func verifyServerSystemConfig(ctx context.Context, endpointZones EndpointZones)
return nil return nil
} }
func newBootstrapRESTClients(endpointZones EndpointZones) []*bootstrapRESTClient { func newBootstrapRESTClients(endpointServerSets EndpointServerSets) []*bootstrapRESTClient {
seenHosts := set.NewStringSet() seenHosts := set.NewStringSet()
var clnts []*bootstrapRESTClient var clnts []*bootstrapRESTClient
for _, ep := range endpointZones { for _, ep := range endpointServerSets {
for _, endpoint := range ep.Endpoints { for _, endpoint := range ep.Endpoints {
if seenHosts.Contains(endpoint.Host) { if seenHosts.Contains(endpoint.Host) {
continue continue
@@ -225,26 +223,8 @@ func newBootstrapRESTClient(endpoint Endpoint) *bootstrapRESTClient {
Path: bootstrapRESTPath, Path: bootstrapRESTPath,
} }
var tlsConfig *tls.Config restClient := rest.NewClient(serverURL, globalInternodeTransport, newAuthToken)
if globalIsSSL { restClient.HealthCheckFn = nil
tlsConfig = &tls.Config{
ServerName: endpoint.Hostname(),
RootCAs: globalRootCAs,
}
}
trFn := newInternodeHTTPTransport(tlsConfig, rest.DefaultRESTTimeout)
restClient := rest.NewClient(serverURL, trFn, newAuthToken)
restClient.HealthCheckFn = func() bool {
ctx, cancel := context.WithTimeout(GlobalContext, restClient.HealthCheckTimeout)
// Instantiate a new rest client for healthcheck
// to avoid recursive healthCheckFn()
respBody, err := rest.NewClient(serverURL, trFn, newAuthToken).Call(ctx, bootstrapRESTMethodHealth, nil, nil, -1)
xhttp.DrainBody(respBody)
cancel()
var ne *rest.NetworkError
return !errors.Is(err, context.DeadlineExceeded) && !errors.As(err, &ne)
}
return &bootstrapRESTClient{endpoint: endpoint, restClient: restClient} return &bootstrapRESTClient{endpoint: endpoint, restClient: restClient}
} }
+1 -1
View File
@@ -34,7 +34,7 @@ func NewBucketSSEConfigSys() *BucketSSEConfigSys {
// Get - gets bucket encryption config for the given bucket. // Get - gets bucket encryption config for the given bucket.
func (sys *BucketSSEConfigSys) Get(bucket string) (*bucketsse.BucketSSEConfig, error) { func (sys *BucketSSEConfigSys) Get(bucket string) (*bucketsse.BucketSSEConfig, error) {
if globalIsGateway { if globalIsGateway {
objAPI := newObjectLayerWithoutSafeModeFn() objAPI := newObjectLayerFn()
if objAPI == nil { if objAPI == nil {
return nil, errServerNotInitialized return nil, errServerNotInitialized
} }
+18 -11
View File
@@ -383,6 +383,10 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
return return
} }
// Call checkRequestAuthType to populate ReqInfo.AccessKey before GetBucketInfo()
// Ignore errors here to preserve the S3 error behavior of GetBucketInfo()
checkRequestAuthType(ctx, r, policy.DeleteObjectAction, bucket, "")
// Before proceeding validate if bucket exists. // Before proceeding validate if bucket exists.
_, err := objectAPI.GetBucketInfo(ctx, bucket) _, err := objectAPI.GetBucketInfo(ctx, bucket)
if err != nil { if err != nil {
@@ -457,7 +461,10 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
deletedObjects := make([]DeletedObject, len(deleteObjects.Objects)) deletedObjects := make([]DeletedObject, len(deleteObjects.Objects))
for i := range errs { for i := range errs {
dindex := objectsToDelete[deleteList[i]] dindex := objectsToDelete[ObjectToDelete{
ObjectName: dObjects[i].ObjectName,
VersionID: dObjects[i].VersionID,
}]
apiErr := toAPIError(ctx, errs[i]) apiErr := toAPIError(ctx, errs[i])
if apiErr.Code == "" || apiErr.Code == "NoSuchKey" || apiErr.Code == "InvalidArgument" { if apiErr.Code == "" || apiErr.Code == "NoSuchKey" || apiErr.Code == "InvalidArgument" {
deletedObjects[dindex] = dObjects[i] deletedObjects[dindex] = dObjects[i]
@@ -487,19 +494,21 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
// Notify deleted event for objects. // Notify deleted event for objects.
for _, dobj := range deletedObjects { for _, dobj := range deletedObjects {
eventName := event.ObjectRemovedDelete
objInfo := ObjectInfo{ objInfo := ObjectInfo{
Name: dobj.ObjectName, Name: dobj.ObjectName,
VersionID: dobj.VersionID, VersionID: dobj.VersionID,
} }
if dobj.DeleteMarker { if dobj.DeleteMarker {
objInfo = ObjectInfo{ objInfo.DeleteMarker = dobj.DeleteMarker
Name: dobj.ObjectName, objInfo.VersionID = dobj.DeleteMarkerVersionID
DeleteMarker: dobj.DeleteMarker, eventName = event.ObjectRemovedDeleteMarkerCreated
VersionID: dobj.DeleteMarkerVersionID,
}
} }
sendEvent(eventArgs{ sendEvent(eventArgs{
EventName: event.ObjectRemovedDelete, EventName: eventName,
BucketName: bucket, BucketName: bucket,
Object: objInfo, Object: objInfo,
ReqParams: extractReqParams(r), ReqParams: extractReqParams(r),
@@ -659,16 +668,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 {
+1 -2
View File
@@ -19,7 +19,6 @@ package cmd
import ( import (
"bytes" "bytes"
"encoding/xml" "encoding/xml"
"fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@@ -837,7 +836,7 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
// Verify whether the bucket obtained object is same as the one created. // Verify whether the bucket obtained object is same as the one created.
if testCase.expectedContent != nil && !bytes.Equal(testCase.expectedContent, actualContent) { if testCase.expectedContent != nil && !bytes.Equal(testCase.expectedContent, actualContent) {
fmt.Println(string(testCase.expectedContent), string(actualContent)) t.Log(string(testCase.expectedContent), string(actualContent))
t.Errorf("Test %d : MinIO %s: Object content differs from expected value.", i+1, instanceType) t.Errorf("Test %d : MinIO %s: Object content differs from expected value.", i+1, instanceType)
} }
} }
+1 -1
View File
@@ -31,7 +31,7 @@ type LifecycleSys struct{}
// Get - gets lifecycle config associated to a given bucket name. // Get - gets lifecycle config associated to a given bucket name.
func (sys *LifecycleSys) Get(bucketName string) (lc *lifecycle.Lifecycle, err error) { func (sys *LifecycleSys) Get(bucketName string) (lc *lifecycle.Lifecycle, err error) {
if globalIsGateway { if globalIsGateway {
objAPI := newObjectLayerWithoutSafeModeFn() objAPI := newObjectLayerFn()
if objAPI == nil { if objAPI == nil {
return nil, errServerNotInitialized return nil, errServerNotInitialized
} }
+11 -45
View File
@@ -18,7 +18,6 @@ package cmd
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
@@ -27,6 +26,7 @@ import (
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/bucket/policy" "github.com/minio/minio/pkg/bucket/policy"
"github.com/minio/minio/pkg/handlers"
"github.com/minio/minio/pkg/sync/errgroup" "github.com/minio/minio/pkg/sync/errgroup"
) )
@@ -113,10 +113,6 @@ func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r
return return
} }
if proxyRequestByBucket(ctx, w, r, bucket) {
return
}
listObjectVersions := objectAPI.ListObjectVersions listObjectVersions := objectAPI.ListObjectVersions
// Inititate a list object versions operation based on the input params. // Inititate a list object versions operation based on the input params.
@@ -139,7 +135,7 @@ func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r
// ListObjectsV2MHandler - GET Bucket (List Objects) Version 2 with metadata. // ListObjectsV2MHandler - GET Bucket (List Objects) Version 2 with metadata.
// -------------------------- // --------------------------
// This implementation of the GET operation returns some or all (up to 10000) // This implementation of the GET operation returns some or all (up to 10000)
// of the objects in a bucket. You can use the request parame<ters as selection // of the objects in a bucket. You can use the request parameters as selection
// criteria to return a subset of the objects in a bucket. // criteria to return a subset of the objects in a bucket.
// //
// NOTE: It is recommended that this API to be used for application development. // NOTE: It is recommended that this API to be used for application development.
@@ -179,13 +175,6 @@ func (api objectAPIHandlers) ListObjectsV2MHandler(w http.ResponseWriter, r *htt
return return
} }
// Analyze continuation token and route the request accordingly
var success bool
token, success = proxyRequestByToken(ctx, w, r, token)
if success {
return
}
listObjectsV2 := objectAPI.ListObjectsV2 listObjectsV2 := objectAPI.ListObjectsV2
// Inititate a list objects operation based on the input params. // Inititate a list objects operation based on the input params.
@@ -201,9 +190,6 @@ func (api objectAPIHandlers) ListObjectsV2MHandler(w http.ResponseWriter, r *htt
// The next continuation token has id@node_index format to optimize paginated listing // The next continuation token has id@node_index format to optimize paginated listing
nextContinuationToken := listObjectsV2Info.NextContinuationToken nextContinuationToken := listObjectsV2Info.NextContinuationToken
if nextContinuationToken != "" && listObjectsV2Info.IsTruncated {
nextContinuationToken = fmt.Sprintf("%s@%d", listObjectsV2Info.NextContinuationToken, getLocalNodeIndex())
}
response := generateListObjectsV2Response(bucket, prefix, token, nextContinuationToken, startAfter, response := generateListObjectsV2Response(bucket, prefix, token, nextContinuationToken, startAfter,
delimiter, encodingType, fetchOwner, listObjectsV2Info.IsTruncated, delimiter, encodingType, fetchOwner, listObjectsV2Info.IsTruncated,
@@ -256,13 +242,6 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
return return
} }
// Analyze continuation token and route the request accordingly
var success bool
token, success = proxyRequestByToken(ctx, w, r, token)
if success {
return
}
listObjectsV2 := objectAPI.ListObjectsV2 listObjectsV2 := objectAPI.ListObjectsV2
// Inititate a list objects operation based on the input params. // Inititate a list objects operation based on the input params.
@@ -276,13 +255,7 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
concurrentDecryptETag(ctx, listObjectsV2Info.Objects) concurrentDecryptETag(ctx, listObjectsV2Info.Objects)
// The next continuation token has id@node_index format to optimize paginated listing response := generateListObjectsV2Response(bucket, prefix, token, listObjectsV2Info.NextContinuationToken, startAfter,
nextContinuationToken := listObjectsV2Info.NextContinuationToken
if nextContinuationToken != "" && listObjectsV2Info.IsTruncated {
nextContinuationToken = fmt.Sprintf("%s@%d", listObjectsV2Info.NextContinuationToken, getLocalNodeIndex())
}
response := generateListObjectsV2Response(bucket, prefix, token, nextContinuationToken, startAfter,
delimiter, encodingType, fetchOwner, listObjectsV2Info.IsTruncated, delimiter, encodingType, fetchOwner, listObjectsV2Info.IsTruncated,
maxKeys, listObjectsV2Info.Objects, listObjectsV2Info.Prefixes, false) maxKeys, listObjectsV2Info.Objects, listObjectsV2Info.Prefixes, false)
@@ -290,18 +263,6 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
writeSuccessResponseXML(w, encodeResponse(response)) writeSuccessResponseXML(w, encodeResponse(response))
} }
func getLocalNodeIndex() int {
if len(globalProxyEndpoints) == 0 {
return -1
}
for i, ep := range globalProxyEndpoints {
if ep.IsLocal {
return i
}
}
return -1
}
func parseRequestToken(token string) (subToken string, nodeIndex int) { func parseRequestToken(token string) (subToken string, nodeIndex int) {
if token == "" { if token == "" {
return token, -1 return token, -1
@@ -340,8 +301,8 @@ func proxyRequestByNodeIndex(ctx context.Context, w http.ResponseWriter, r *http
return proxyRequest(ctx, w, r, ep) return proxyRequest(ctx, w, r, ep)
} }
func proxyRequestByBucket(ctx context.Context, w http.ResponseWriter, r *http.Request, bucket string) (success bool) { func proxyRequestByStringHash(ctx context.Context, w http.ResponseWriter, r *http.Request, str string) (success bool) {
return proxyRequestByNodeIndex(ctx, w, r, crcHashMod(bucket, len(globalProxyEndpoints))) return proxyRequestByNodeIndex(ctx, w, r, crcHashMod(str, len(globalProxyEndpoints)))
} }
// ListObjectsV1Handler - GET Bucket (List Objects) Version 1. // ListObjectsV1Handler - GET Bucket (List Objects) Version 1.
@@ -382,7 +343,12 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
return return
} }
if proxyRequestByBucket(ctx, w, r, bucket) { // Forward the request using Source IP or bucket
forwardStr := handlers.GetSourceIPFromHeaders(r)
if forwardStr == "" {
forwardStr = bucket
}
if proxyRequestByStringHash(ctx, w, r, forwardStr) {
return return
} }
+26 -13
View File
@@ -49,6 +49,7 @@ func (sys *BucketMetadataSys) Remove(bucket string) {
} }
sys.Lock() sys.Lock()
delete(sys.metadataMap, bucket) delete(sys.metadataMap, bucket)
globalBucketMonitor.DeleteBucket(bucket)
sys.Unlock() sys.Unlock()
} }
@@ -72,7 +73,7 @@ func (sys *BucketMetadataSys) Set(bucket string, meta BucketMetadata) {
// Update update bucket metadata for the specified config file. // Update update bucket metadata for the specified config file.
// The configData data should not be modified after being sent here. // The configData data should not be modified after being sent here.
func (sys *BucketMetadataSys) Update(bucket string, configFile string, configData []byte) error { func (sys *BucketMetadataSys) Update(bucket string, configFile string, configData []byte) error {
objAPI := newObjectLayerWithoutSafeModeFn() objAPI := newObjectLayerFn()
if objAPI == nil { if objAPI == nil {
return errServerNotInitialized return errServerNotInitialized
} }
@@ -275,7 +276,7 @@ func (sys *BucketMetadataSys) GetLifecycleConfig(bucket string) (*lifecycle.Life
func (sys *BucketMetadataSys) GetNotificationConfig(bucket string) (*event.Config, error) { func (sys *BucketMetadataSys) GetNotificationConfig(bucket string) (*event.Config, error) {
if globalIsGateway && globalGatewayName == NASBackendGateway { if globalIsGateway && globalGatewayName == NASBackendGateway {
// Only needed in case of NAS gateway. // Only needed in case of NAS gateway.
objAPI := newObjectLayerWithoutSafeModeFn() objAPI := newObjectLayerFn()
if objAPI == nil { if objAPI == nil {
return nil, errServerNotInitialized return nil, errServerNotInitialized
} }
@@ -313,7 +314,7 @@ func (sys *BucketMetadataSys) GetSSEConfig(bucket string) (*bucketsse.BucketSSEC
// The returned object may not be modified. // The returned object may not be modified.
func (sys *BucketMetadataSys) GetPolicyConfig(bucket string) (*policy.Policy, error) { func (sys *BucketMetadataSys) GetPolicyConfig(bucket string) (*policy.Policy, error) {
if globalIsGateway { if globalIsGateway {
objAPI := newObjectLayerWithoutSafeModeFn() objAPI := newObjectLayerFn()
if objAPI == nil { if objAPI == nil {
return nil, errServerNotInitialized return nil, errServerNotInitialized
} }
@@ -373,10 +374,24 @@ func (sys *BucketMetadataSys) GetBucketTargetsConfig(bucket string) (*madmin.Buc
return meta.bucketTargetConfig, nil return meta.bucketTargetConfig, nil
} }
// GetBucketTarget returns the target for the bucket and arn.
func (sys *BucketMetadataSys) GetBucketTarget(bucket string, arn string) (madmin.BucketTarget, error) {
targets, err := sys.GetBucketTargetsConfig(bucket)
if err != nil {
return madmin.BucketTarget{}, err
}
for _, t := range targets.Targets {
if t.Arn == arn {
return t, nil
}
}
return madmin.BucketTarget{}, errConfigNotFound
}
// GetConfig returns a specific configuration from the bucket metadata. // GetConfig returns a specific configuration from the bucket metadata.
// The returned object may not be modified. // The returned object may not be modified.
func (sys *BucketMetadataSys) GetConfig(bucket string) (BucketMetadata, error) { func (sys *BucketMetadataSys) GetConfig(bucket string) (BucketMetadata, error) {
objAPI := newObjectLayerWithoutSafeModeFn() objAPI := newObjectLayerFn()
if objAPI == nil { if objAPI == nil {
return newBucketMetadata(bucket), errServerNotInitialized return newBucketMetadata(bucket), errServerNotInitialized
} }
@@ -418,12 +433,12 @@ func (sys *BucketMetadataSys) Init(ctx context.Context, buckets []BucketInfo, ob
} }
// Load bucket metadata sys in background // Load bucket metadata sys in background
go logger.LogIf(ctx, sys.load(ctx, buckets, objAPI)) go sys.load(ctx, buckets, objAPI)
return nil return nil
} }
// concurrently load bucket metadata to speed up loading bucket metadata. // concurrently load bucket metadata to speed up loading bucket metadata.
func (sys *BucketMetadataSys) concurrentLoad(ctx context.Context, buckets []BucketInfo, objAPI ObjectLayer) error { func (sys *BucketMetadataSys) concurrentLoad(ctx context.Context, buckets []BucketInfo, objAPI ObjectLayer) {
g := errgroup.WithNErrs(len(buckets)) g := errgroup.WithNErrs(len(buckets))
for index := range buckets { for index := range buckets {
index := index index := index
@@ -440,22 +455,20 @@ func (sys *BucketMetadataSys) concurrentLoad(ctx context.Context, buckets []Buck
} }
for _, err := range g.Wait() { for _, err := range g.Wait() {
if err != nil { if err != nil {
return err logger.LogIf(ctx, err)
} }
} }
return nil
} }
// Loads bucket metadata for all buckets into BucketMetadataSys. // Loads bucket metadata for all buckets into BucketMetadataSys.
func (sys *BucketMetadataSys) load(ctx context.Context, buckets []BucketInfo, objAPI ObjectLayer) error { func (sys *BucketMetadataSys) load(ctx context.Context, buckets []BucketInfo, objAPI ObjectLayer) {
count := 100 // load 100 bucket metadata at a time. count := 100 // load 100 bucket metadata at a time.
for { for {
if len(buckets) < count { if len(buckets) < count {
return sys.concurrentLoad(ctx, buckets, objAPI) sys.concurrentLoad(ctx, buckets, objAPI)
} return
if err := sys.concurrentLoad(ctx, buckets[:count], objAPI); err != nil {
return err
} }
sys.concurrentLoad(ctx, buckets[:count], objAPI)
buckets = buckets[count:] buckets = buckets[count:]
} }
} }
+8 -2
View File
@@ -107,6 +107,10 @@ func newBucketMetadata(name string) BucketMetadata {
// Load - loads the metadata of bucket by name from ObjectLayer api. // Load - loads the metadata of bucket by name from ObjectLayer api.
// If an error is returned the returned metadata will be default initialized. // If an error is returned the returned metadata will be default initialized.
func (b *BucketMetadata) Load(ctx context.Context, api ObjectLayer, name string) error { func (b *BucketMetadata) Load(ctx context.Context, api ObjectLayer, name string) error {
if name == "" {
logger.LogIf(ctx, errors.New("bucket name cannot be empty"))
return errors.New("bucket name cannot be empty")
}
configFile := path.Join(bucketConfigPrefix, name, bucketMetadataFile) configFile := path.Join(bucketConfigPrefix, name, bucketMetadataFile)
data, err := readConfig(ctx, api, configFile) data, err := readConfig(ctx, api, configFile)
if err != nil { if err != nil {
@@ -128,20 +132,22 @@ func (b *BucketMetadata) Load(ctx context.Context, api ObjectLayer, name string)
} }
// OK, parse data. // OK, parse data.
_, err = b.UnmarshalMsg(data[4:]) _, err = b.UnmarshalMsg(data[4:])
b.Name = name // in-case parsing failed for some reason, make sure bucket name is not empty.
return err return err
} }
// loadBucketMetadata loads and migrates to bucket metadata. // loadBucketMetadata loads and migrates to bucket metadata.
func loadBucketMetadata(ctx context.Context, objectAPI ObjectLayer, bucket string) (BucketMetadata, error) { func loadBucketMetadata(ctx context.Context, objectAPI ObjectLayer, bucket string) (BucketMetadata, error) {
b := newBucketMetadata(bucket) b := newBucketMetadata(bucket)
err := b.Load(ctx, objectAPI, bucket) err := b.Load(ctx, objectAPI, b.Name)
if err == nil { if err == nil {
return b, b.convertLegacyConfigs(ctx, objectAPI) return b, b.convertLegacyConfigs(ctx, objectAPI)
} }
if err != errConfigNotFound { if !errors.Is(err, errConfigNotFound) {
return b, err return b, err
} }
// Old bucket without bucket metadata. Hence we migrate existing settings. // Old bucket without bucket metadata. Hence we migrate existing settings.
return b, b.convertLegacyConfigs(ctx, objectAPI) return b, b.convertLegacyConfigs(ctx, objectAPI)
} }
+1 -1
View File
@@ -33,7 +33,7 @@ type BucketObjectLockSys struct{}
// Get - Get retention configuration. // Get - Get retention configuration.
func (sys *BucketObjectLockSys) Get(bucketName string) (r objectlock.Retention, err error) { func (sys *BucketObjectLockSys) Get(bucketName string) (r objectlock.Retention, err error) {
if globalIsGateway { if globalIsGateway {
objAPI := newObjectLayerWithoutSafeModeFn() objAPI := newObjectLayerFn()
if objAPI == nil { if objAPI == nil {
return r, errServerNotInitialized return r, errServerNotInitialized
} }
+3 -1
View File
@@ -63,7 +63,7 @@ func parseBucketQuota(bucket string, data []byte) (quotaCfg *madmin.BucketQuota,
} }
func (sys *BucketQuotaSys) check(ctx context.Context, bucket string, size int64) error { func (sys *BucketQuotaSys) check(ctx context.Context, bucket string, size int64) error {
objAPI := newObjectLayerWithoutSafeModeFn() objAPI := newObjectLayerFn()
if objAPI == nil { if objAPI == nil {
return errServerNotInitialized return errServerNotInitialized
} }
@@ -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)
} }
}) })
+144 -34
View File
@@ -18,7 +18,9 @@ package cmd
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"runtime"
"strings" "strings"
"time" "time"
@@ -28,6 +30,7 @@ import (
"github.com/minio/minio/cmd/crypto" "github.com/minio/minio/cmd/crypto"
xhttp "github.com/minio/minio/cmd/http" xhttp "github.com/minio/minio/cmd/http"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/bucket/bandwidth"
"github.com/minio/minio/pkg/bucket/replication" "github.com/minio/minio/pkg/bucket/replication"
"github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/event"
iampolicy "github.com/minio/minio/pkg/iam/policy" iampolicy "github.com/minio/minio/pkg/iam/policy"
@@ -36,7 +39,7 @@ import (
// gets replication config associated to a given bucket name. // gets replication config associated to a given bucket name.
func getReplicationConfig(ctx context.Context, bucketName string) (rc *replication.Config, err error) { func getReplicationConfig(ctx context.Context, bucketName string) (rc *replication.Config, err error) {
if globalIsGateway { if globalIsGateway {
objAPI := newObjectLayerWithoutSafeModeFn() objAPI := newObjectLayerFn()
if objAPI == nil { if objAPI == nil {
return nil, errServerNotInitialized return nil, errServerNotInitialized
} }
@@ -50,12 +53,12 @@ func getReplicationConfig(ctx context.Context, bucketName string) (rc *replicati
// validateReplicationDestination returns error if replication destination bucket missing or not configured // validateReplicationDestination returns error if replication destination bucket missing or not configured
// It also returns true if replication destination is same as this server. // It also returns true if replication destination is same as this server.
func validateReplicationDestination(ctx context.Context, bucket string, rCfg *replication.Config) (bool, error) { func validateReplicationDestination(ctx context.Context, bucket string, rCfg *replication.Config) (bool, error) {
clnt := globalBucketTargetSys.GetReplicationTargetClient(ctx, rCfg.RoleArn) clnt := globalBucketTargetSys.GetRemoteTargetClient(ctx, rCfg.RoleArn)
if clnt == nil { if clnt == nil {
return false, BucketRemoteTargetNotFound{Bucket: bucket} return false, BucketRemoteTargetNotFound{Bucket: bucket}
} }
if found, _ := clnt.BucketExists(ctx, rCfg.GetDestination().Bucket); !found { if found, _ := clnt.BucketExists(ctx, rCfg.GetDestination().Bucket); !found {
return false, BucketReplicationDestinationNotFound{Bucket: rCfg.GetDestination().Bucket} return false, BucketRemoteDestinationNotFound{Bucket: rCfg.GetDestination().Bucket}
} }
if ret, err := globalBucketObjectLockSys.Get(bucket); err == nil { if ret, err := globalBucketObjectLockSys.Get(bucket); err == nil {
if ret.LockEnabled { if ret.LockEnabled {
@@ -85,7 +88,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(ctx, 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)
@@ -117,7 +120,7 @@ func mustReplicater(ctx context.Context, r *http.Request, bucket, object string,
return cfg.Replicate(opts) return cfg.Replicate(opts)
} }
func putReplicationOpts(dest replication.Destination, objInfo ObjectInfo) (putOpts miniogo.PutObjectOptions) { func putReplicationOpts(ctx context.Context, dest replication.Destination, objInfo ObjectInfo) (putOpts miniogo.PutObjectOptions) {
meta := make(map[string]string) meta := make(map[string]string)
for k, v := range objInfo.UserDefined { for k, v := range objInfo.UserDefined {
if k == xhttp.AmzBucketReplicationStatus { if k == xhttp.AmzBucketReplicationStatus {
@@ -137,15 +140,17 @@ func putReplicationOpts(dest replication.Destination, objInfo ObjectInfo) (putOp
sc = objInfo.StorageClass sc = objInfo.StorageClass
} }
putOpts = miniogo.PutObjectOptions{ putOpts = miniogo.PutObjectOptions{
UserMetadata: meta, UserMetadata: meta,
UserTags: tag.ToMap(), UserTags: tag.ToMap(),
ContentType: objInfo.ContentType, ContentType: objInfo.ContentType,
ContentEncoding: objInfo.ContentEncoding, ContentEncoding: objInfo.ContentEncoding,
StorageClass: sc, StorageClass: sc,
ReplicationVersionID: objInfo.VersionID, Internal: miniogo.AdvancedPutOptions{
ReplicationStatus: miniogo.ReplicationStatusReplica, SourceVersionID: objInfo.VersionID,
ReplicationMTime: objInfo.ModTime, ReplicationStatus: miniogo.ReplicationStatusReplica,
ReplicationETag: objInfo.ETag, SourceMTime: objInfo.ModTime,
SourceETag: objInfo.ETag,
},
} }
if mode, ok := objInfo.UserDefined[xhttp.AmzObjectLockMode]; ok { if mode, ok := objInfo.UserDefined[xhttp.AmzObjectLockMode]; ok {
rmode := miniogo.RetentionMode(mode) rmode := miniogo.RetentionMode(mode)
@@ -164,75 +169,114 @@ func putReplicationOpts(dest replication.Destination, objInfo ObjectInfo) (putOp
if crypto.S3.IsEncrypted(objInfo.UserDefined) { if crypto.S3.IsEncrypted(objInfo.UserDefined) {
putOpts.ServerSideEncryption = encrypt.NewSSE() putOpts.ServerSideEncryption = encrypt.NewSSE()
} }
return return
} }
// 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.GetRemoteTargetClient(ctx, cfg.RoleArn)
if tgt == nil { if tgt == nil {
logger.LogIf(ctx, fmt.Errorf("failed to get target for bucket:%s arn:%s", bucket, cfg.RoleArn))
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
} }
} }
putOpts := putReplicationOpts(dest, objInfo)
target, err := globalBucketMetadataSys.GetBucketTarget(bucket, cfg.RoleArn)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("failed to get target for replication bucket:%s cfg:%s err:%s", bucket, cfg.RoleArn, err))
return
}
putOpts := putReplicationOpts(ctx, dest, objInfo)
replicationStatus := replication.Complete replicationStatus := replication.Complete
_, err = tgt.PutObject(ctx, dest.Bucket, object, gr, size, "", "", putOpts)
// Setup bandwidth throttling
peers, _ := globalEndpoints.peers()
totalNodesCount := len(peers)
if totalNodesCount == 0 {
totalNodesCount = 1 // For standalone erasure coding
}
b := target.BandwidthLimit / int64(totalNodesCount)
var headerSize int
for k, v := range putOpts.Header() {
headerSize += len(k) + len(v)
}
r := bandwidth.NewMonitoredReader(ctx, globalBucketMonitor, objInfo.Bucket, objInfo.Name, gr, headerSize, b, target.BandwidthLimit)
_, err = tgt.PutObject(ctx, dest.Bucket, object, r, size, "", "", putOpts)
r.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 +302,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, 10000),
}
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)
}
}
+16 -12
View File
@@ -20,6 +20,7 @@ import (
"context" "context"
"net/http" "net/http"
"sync" "sync"
"time"
minio "github.com/minio/minio-go/v7" minio "github.com/minio/minio-go/v7"
miniogo "github.com/minio/minio-go/v7" miniogo "github.com/minio/minio-go/v7"
@@ -98,10 +99,9 @@ func (sys *BucketTargetSys) SetTarget(ctx context.Context, bucket string, tgt *m
return BucketRemoteConnectionErr{Bucket: tgt.TargetBucket} return BucketRemoteConnectionErr{Bucket: tgt.TargetBucket}
} }
if vcfg.Status != string(versioning.Enabled) { if vcfg.Status != string(versioning.Enabled) {
return BucketReplicationTargetNotVersioned{Bucket: tgt.TargetBucket} return BucketRemoteTargetNotVersioned{Bucket: tgt.TargetBucket}
} }
} }
sys.Lock() sys.Lock()
defer sys.Unlock() defer sys.Unlock()
@@ -113,6 +113,9 @@ func (sys *BucketTargetSys) SetTarget(ctx context.Context, bucket string, tgt *m
if t.Arn == tgt.Arn { if t.Arn == tgt.Arn {
return BucketRemoteAlreadyExists{Bucket: t.TargetBucket} return BucketRemoteAlreadyExists{Bucket: t.TargetBucket}
} }
if t.Label == tgt.Label {
return BucketRemoteLabelInUse{Bucket: t.TargetBucket}
}
newtgts[idx] = *tgt newtgts[idx] = *tgt
found = true found = true
continue continue
@@ -173,8 +176,8 @@ func (sys *BucketTargetSys) RemoveTarget(ctx context.Context, bucket, arnStr str
return nil return nil
} }
// GetReplicationTargetClient returns minio-go client for replication target instance // GetRemoteTargetClient returns minio-go client for replication target instance
func (sys *BucketTargetSys) GetReplicationTargetClient(ctx context.Context, arn string) *miniogo.Core { func (sys *BucketTargetSys) GetRemoteTargetClient(ctx context.Context, arn string) *miniogo.Core {
sys.RLock() sys.RLock()
defer sys.RUnlock() defer sys.RUnlock()
return sys.arnRemotesMap[arn] return sys.arnRemotesMap[arn]
@@ -205,14 +208,14 @@ func (sys *BucketTargetSys) Init(ctx context.Context, buckets []BucketInfo, objA
return nil return nil
} }
// UpdateTarget updates target to reflect metadata updates // UpdateAllTargets updates target to reflect metadata updates
func (sys *BucketTargetSys) UpdateTarget(bucket string, cfg *madmin.BucketTargets) { func (sys *BucketTargetSys) UpdateAllTargets(bucket string, tgts *madmin.BucketTargets) {
if sys == nil { if sys == nil {
return return
} }
sys.Lock() sys.Lock()
defer sys.Unlock() defer sys.Unlock()
if cfg == nil || cfg.Empty() { if tgts == nil || tgts.Empty() {
// remove target and arn association // remove target and arn association
if tgts, ok := sys.targetsMap[bucket]; ok { if tgts, ok := sys.targetsMap[bucket]; ok {
for _, t := range tgts { for _, t := range tgts {
@@ -223,10 +226,10 @@ func (sys *BucketTargetSys) UpdateTarget(bucket string, cfg *madmin.BucketTarget
return return
} }
if len(cfg.Targets) > 0 { if len(tgts.Targets) > 0 {
sys.targetsMap[bucket] = cfg.Targets sys.targetsMap[bucket] = tgts.Targets
} }
for _, tgt := range cfg.Targets { for _, tgt := range tgts.Targets {
tgtClient, err := sys.getRemoteTargetClient(&tgt) tgtClient, err := sys.getRemoteTargetClient(&tgt)
if err != nil { if err != nil {
continue continue
@@ -236,7 +239,7 @@ func (sys *BucketTargetSys) UpdateTarget(bucket string, cfg *madmin.BucketTarget
sys.clientsCache[tgtClient.EndpointURL().String()] = tgtClient sys.clientsCache[tgtClient.EndpointURL().String()] = tgtClient
} }
} }
sys.targetsMap[bucket] = cfg.Targets sys.targetsMap[bucket] = tgts.Targets
} }
// create minio-go clients for buckets having remote targets // create minio-go clients for buckets having remote targets
@@ -279,8 +282,9 @@ func (sys *BucketTargetSys) getRemoteTargetClient(tcfg *madmin.BucketTarget) (*m
creds := credentials.NewStaticV4(config.AccessKey, config.SecretKey, "") creds := credentials.NewStaticV4(config.AccessKey, config.SecretKey, "")
getRemoteTargetInstanceTransportOnce.Do(func() { getRemoteTargetInstanceTransportOnce.Do(func() {
getRemoteTargetInstanceTransport = NewGatewayHTTPTransport() getRemoteTargetInstanceTransport = newGatewayHTTPTransport(1 * time.Hour)
}) })
core, err := miniogo.NewCore(tcfg.Endpoint, &miniogo.Options{ core, err := miniogo.NewCore(tcfg.Endpoint, &miniogo.Options{
Creds: creds, Creds: creds,
Secure: tcfg.Secure, Secure: tcfg.Secure,
+8
View File
@@ -70,6 +70,14 @@ func (api objectAPIHandlers) PutBucketVersioningHandler(w http.ResponseWriter, r
}, r.URL, guessIsBrowserReq(r)) }, r.URL, guessIsBrowserReq(r))
return return
} }
if _, err := getReplicationConfig(ctx, bucket); err == nil && v.Suspended() {
writeErrorResponse(ctx, w, APIError{
Code: "InvalidBucketState",
Description: "A replication configuration is present on this bucket, so the versioning state cannot be changed.",
HTTPStatusCode: http.StatusConflict,
}, r.URL, guessIsBrowserReq(r))
return
}
configData, err := xml.Marshal(v) configData, err := xml.Marshal(v)
if err != nil { if err != nil {
+22 -16
View File
@@ -21,6 +21,7 @@ import (
"encoding/gob" "encoding/gob"
"errors" "errors"
"fmt" "fmt"
"math/rand"
"net" "net"
"net/url" "net/url"
"os" "os"
@@ -33,6 +34,7 @@ import (
"github.com/minio/cli" "github.com/minio/cli"
"github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/minio/cmd/config" "github.com/minio/minio/cmd/config"
xhttp "github.com/minio/minio/cmd/http"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/certs" "github.com/minio/minio/pkg/certs"
@@ -43,9 +45,8 @@ func init() {
logger.Init(GOPATH, GOROOT) logger.Init(GOPATH, GOROOT)
logger.RegisterError(config.FmtError) logger.RegisterError(config.FmtError)
// Initialize globalConsoleSys system rand.Seed(time.Now().UTC().UnixNano())
globalConsoleSys = NewConsoleLogger(GlobalContext) globalDNSCache = xhttp.NewDNSCache(3*time.Second, 10*time.Second)
logger.AddTarget(globalConsoleSys)
gob.Register(StorageErr("")) gob.Register(StorageErr(""))
} }
@@ -104,11 +105,7 @@ func checkUpdate(mode string) {
return return
} }
if globalInplaceUpdateDisabled { logStartupMessage(prepareUpdateMessage("Run `mc admin update`", lrTime.Sub(crTime)))
logStartupMessage(updateMsg)
} else {
logStartupMessage(prepareUpdateMessage("Run `mc admin update`", lrTime.Sub(crTime)))
}
} }
func newConfigDirFromCtx(ctx *cli.Context, option string, getDefaultDir func() string) (*ConfigDir, bool) { func newConfigDirFromCtx(ctx *cli.Context, option string, getDefaultDir func() string) (*ConfigDir, bool) {
@@ -338,14 +335,23 @@ func getTLSConfig() (x509Certs []*x509.Certificate, manager *certs.Manager, secu
return nil, nil, false, err return nil, nil, false, err
} }
for _, file := range files { for _, file := range files {
// We exclude any regular file and the "CAs/" directory. // Ignore all
// The "CAs/" directory contains (root) CA certificates // - regular files
// that MinIO adds to its list of trusted roots (tls.Config.RootCAs). // - "CAs" directory
// Therefore, "CAs/" does not contain X.509 certificates that // - any directory which starts with ".."
// are meant to be served by MinIO. if file.Mode().IsRegular() || file.Name() == "CAs" || strings.HasPrefix(file.Name(), "..") {
if !file.IsDir() || file.Name() == "CAs" {
continue continue
} }
if file.Mode()&os.ModeSymlink == os.ModeSymlink {
file, err = os.Stat(filepath.Join(root.Name(), file.Name()))
if err != nil {
// not accessible ignore
continue
}
if !file.IsDir() {
continue
}
}
var ( var (
certFile = filepath.Join(root.Name(), file.Name(), publicCertFile) certFile = filepath.Join(root.Name(), file.Name(), publicCertFile)
@@ -354,8 +360,8 @@ func getTLSConfig() (x509Certs []*x509.Certificate, manager *certs.Manager, secu
if !isFile(certFile) || !isFile(keyFile) { if !isFile(certFile) || !isFile(keyFile) {
continue continue
} }
if err := manager.AddCertificate(certFile, keyFile); err != nil { if err = manager.AddCertificate(certFile, keyFile); err != nil {
err = fmt.Errorf("Failed to load TLS certificate '%s': %v", certFile, err) err = fmt.Errorf("Unable to load TLS certificate '%s,%s': %w", certFile, keyFile, err)
logger.LogIf(GlobalContext, err, logger.Minio) logger.LogIf(GlobalContext, err, logger.Minio)
} }
} }
+29 -5
View File
@@ -27,6 +27,7 @@ import (
"github.com/minio/minio/cmd/config/compress" "github.com/minio/minio/cmd/config/compress"
"github.com/minio/minio/cmd/config/dns" "github.com/minio/minio/cmd/config/dns"
"github.com/minio/minio/cmd/config/etcd" "github.com/minio/minio/cmd/config/etcd"
"github.com/minio/minio/cmd/config/heal"
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.HealSubSys: heal.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.HealSubSys,
Description: "manage object healing frequency and bitrot verification 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.HealSubSys: heal.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 := heal.LookupConfig(s[config.HealSubSys][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 {
@@ -398,6 +409,11 @@ func lookupConfigs(s config.Config, setDriveCount int) {
globalAPIConfig.init(apiConfig, setDriveCount) 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)
if err != nil { if err != nil {
@@ -422,6 +438,10 @@ func lookupConfigs(s config.Config, setDriveCount int) {
} }
} }
} }
globalHealConfig, err = heal.LookupConfig(s[config.HealSubSys][config.Default])
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to read heal 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 {
@@ -435,7 +455,7 @@ 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 { if globalAutoEncryption && !globalIsGateway {
logger.LogIf(ctx, fmt.Errorf("%s env is deprecated please migrate to using `mc encrypt` at bucket level", crypto.EnvKMSAutoEncryption)) logger.LogIf(ctx, fmt.Errorf("%s env is deprecated please migrate to using `mc encrypt` at bucket level", crypto.EnvKMSAutoEncryption))
} }
@@ -473,11 +493,13 @@ func lookupConfigs(s config.Config, setDriveCount int) {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize logger: %w", err)) logger.LogIf(ctx, fmt.Errorf("Unable to initialize logger: %w", err))
} }
for _, l := range loggerCfg.HTTP { for k, l := range loggerCfg.HTTP {
if l.Enabled { if l.Enabled {
// Enable http logging // Enable http logging
if err = logger.AddTarget( if err = logger.AddTarget(
http.New(http.WithEndpoint(l.Endpoint), http.New(
http.WithTargetName(k),
http.WithEndpoint(l.Endpoint),
http.WithAuthToken(l.AuthToken), http.WithAuthToken(l.AuthToken),
http.WithUserAgent(loggerUserAgent), http.WithUserAgent(loggerUserAgent),
http.WithLogKind(string(logger.All)), http.WithLogKind(string(logger.All)),
@@ -489,11 +511,13 @@ func lookupConfigs(s config.Config, setDriveCount int) {
} }
} }
for _, l := range loggerCfg.Audit { for k, l := range loggerCfg.Audit {
if l.Enabled { if l.Enabled {
// Enable http audit logging // Enable http audit logging
if err = logger.AddAuditTarget( if err = logger.AddAuditTarget(
http.New(http.WithEndpoint(l.Endpoint), http.New(
http.WithTargetName(k),
http.WithEndpoint(l.Endpoint),
http.WithAuthToken(l.AuthToken), http.WithAuthToken(l.AuthToken),
http.WithUserAgent(loggerUserAgent), http.WithUserAgent(loggerUserAgent),
http.WithLogKind(string(logger.All)), http.WithLogKind(string(logger.All)),
+2 -3
View File
@@ -19,7 +19,6 @@ package cmd
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"time" "time"
"unicode/utf8" "unicode/utf8"
@@ -194,7 +193,7 @@ func migrateIAMConfigsEtcdToEncrypted(ctx context.Context, client *etcd.Client)
// Config is already encrypted with right keys // Config is already encrypted with right keys
continue continue
} }
return errors.New("config data not in plain-text form or encrypted") return fmt.Errorf("Decrypting config failed %w, possibly credentials are incorrect", err)
} }
cencdata, err = madmin.EncryptData(globalActiveCred.String(), data) cencdata, err = madmin.EncryptData(globalActiveCred.String(), data)
@@ -274,7 +273,7 @@ func migrateConfigPrefixToEncrypted(objAPI ObjectLayer, activeCredOld auth.Crede
// Config is already encrypted with right keys // Config is already encrypted with right keys
continue continue
} }
return errors.New("config data not in plain-text form or encrypted") return fmt.Errorf("Decrypting config failed %w, possibly credentials are incorrect", err)
} }
cencdata, err = madmin.EncryptData(globalActiveCred.String(), data) cencdata, err = madmin.EncryptData(globalActiveCred.String(), data)
+88 -18
View File
@@ -29,15 +29,28 @@ 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"
apiListQuorum = "list_quorum"
apiExtendListCacheLife = "extend_list_cache_life"
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"
EnvAPIListQuorum = "MINIO_API_LIST_QUORUM"
EnvAPIExtendListCacheLife = "MINIO_API_EXTEND_LIST_CACHE_LIFE"
EnvAPISecureCiphers = "MINIO_API_SECURE_CIPHERS"
)
// 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 +65,37 @@ 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.KV{
Key: apiListQuorum,
Value: "optimal",
},
config.KV{
Key: apiExtendListCacheLife,
Value: "0s",
},
} }
) )
// 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"`
ListQuorum string `json:"list_strict_quorum"`
ExtendListLife time.Duration `json:"extend_list_cache_life"`
} }
// UnmarshalJSON - Validate SS and RRS parity when unmarshalling JSON. // UnmarshalJSON - Validate SS and RRS parity when unmarshalling JSON.
@@ -81,8 +109,29 @@ func (sCfg *Config) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &aux) return json.Unmarshal(data, &aux)
} }
// GetListQuorum interprets list quorum values and returns appropriate
// acceptable quorum expected for list operations
func (sCfg Config) GetListQuorum() int {
switch sCfg.ListQuorum {
case "optimal":
return 3
case "reduced":
return 2
case "disk":
// smallest possible value, generally meant for testing.
return 1
case "strict":
return -1
}
// Defaults to 3 drives per set.
return 3
}
// 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 +151,37 @@ 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
}
listQuorum := env.Get(EnvAPIListQuorum, kvs.Get(apiListQuorum))
switch listQuorum {
case "strict", "optimal", "reduced", "disk":
default:
return cfg, errors.New("invalid value for list strict quorum")
}
listLife, err := time.ParseDuration(env.Get(EnvAPIExtendListCacheLife, kvs.Get(apiExtendListCacheLife)))
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,
ListQuorum: listQuorum,
ExtendListLife: listLife,
}, 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",
},
} }
) )
+21 -10
View File
@@ -28,16 +28,17 @@ import (
// Config represents cache config settings // Config represents cache config settings
type Config struct { type Config struct {
Enabled bool `json:"-"` Enabled bool `json:"-"`
Drives []string `json:"drives"` Drives []string `json:"drives"`
Expiry int `json:"expiry"` Expiry int `json:"expiry"`
MaxUse int `json:"maxuse"` MaxUse int `json:"maxuse"`
Quota int `json:"quota"` Quota int `json:"quota"`
Exclude []string `json:"exclude"` Exclude []string `json:"exclude"`
After int `json:"after"` After int `json:"after"`
WatermarkLow int `json:"watermark_low"` WatermarkLow int `json:"watermark_low"`
WatermarkHigh int `json:"watermark_high"` WatermarkHigh int `json:"watermark_high"`
Range bool `json:"range"` Range bool `json:"range"`
CommitWriteback bool `json:"-"`
} }
// UnmarshalJSON - implements JSON unmarshal interface for unmarshalling // UnmarshalJSON - implements JSON unmarshal interface for unmarshalling
@@ -152,3 +153,13 @@ func parseCacheExcludes(excludes string) ([]string, error) {
return excludesSlice, nil return excludesSlice, nil
} }
func parseCacheCommitMode(commitStr string) (bool, error) {
switch strings.ToLower(commitStr) {
case "writeback":
return true, nil
case "writethrough":
return false, nil
}
return false, config.ErrInvalidCacheCommitValue(nil).Msg("cache commit value must be `writeback` or `writethrough`")
}
+6
View File
@@ -74,5 +74,11 @@ var (
Optional: true, Optional: true,
Type: "string", Type: "string",
}, },
config.HelpKV{
Key: Commit,
Description: `set to control cache commit behavior, defaults to "writethrough"`,
Optional: true,
Type: "string",
},
} }
) )
+13
View File
@@ -35,6 +35,7 @@ const (
WatermarkLow = "watermark_low" WatermarkLow = "watermark_low"
WatermarkHigh = "watermark_high" WatermarkHigh = "watermark_high"
Range = "range" Range = "range"
Commit = "commit"
EnvCacheDrives = "MINIO_CACHE_DRIVES" EnvCacheDrives = "MINIO_CACHE_DRIVES"
EnvCacheExclude = "MINIO_CACHE_EXCLUDE" EnvCacheExclude = "MINIO_CACHE_EXCLUDE"
@@ -45,6 +46,7 @@ const (
EnvCacheWatermarkLow = "MINIO_CACHE_WATERMARK_LOW" EnvCacheWatermarkLow = "MINIO_CACHE_WATERMARK_LOW"
EnvCacheWatermarkHigh = "MINIO_CACHE_WATERMARK_HIGH" EnvCacheWatermarkHigh = "MINIO_CACHE_WATERMARK_HIGH"
EnvCacheRange = "MINIO_CACHE_RANGE" EnvCacheRange = "MINIO_CACHE_RANGE"
EnvCacheCommit = "MINIO_CACHE_COMMIT"
EnvCacheEncryptionMasterKey = "MINIO_CACHE_ENCRYPTION_MASTER_KEY" EnvCacheEncryptionMasterKey = "MINIO_CACHE_ENCRYPTION_MASTER_KEY"
@@ -53,6 +55,7 @@ const (
DefaultAfter = "0" DefaultAfter = "0"
DefaultWaterMarkLow = "70" DefaultWaterMarkLow = "70"
DefaultWaterMarkHigh = "80" DefaultWaterMarkHigh = "80"
DefaultCacheCommit = "writethrough"
) )
// DefaultKVS - default KV settings for caching. // DefaultKVS - default KV settings for caching.
@@ -90,6 +93,10 @@ var (
Key: Range, Key: Range,
Value: config.EnableOn, Value: config.EnableOn,
}, },
config.KV{
Key: Commit,
Value: DefaultCacheCommit,
},
} }
) )
@@ -210,6 +217,12 @@ func LookupConfig(kvs config.KVS) (Config, error) {
} }
cfg.Range = rng cfg.Range = rng
} }
if commit := env.Get(EnvCacheCommit, kvs.Get(Commit)); commit != "" {
cfg.CommitWriteback, err = parseCacheCommitMode(commit)
if err != nil {
return cfg, err
}
}
return cfg, nil return cfg, nil
} }
+13
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"
HealSubSys = "heal"
// 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,
HealSubSys,
NotifyAMQPSubSys, NotifyAMQPSubSys,
NotifyESSubSys, NotifyESSubSys,
NotifyKafkaSubSys, NotifyKafkaSubSys,
@@ -138,6 +140,7 @@ var SubSystemsSingleTargets = set.CreateStringSet([]string{
PolicyOPASubSys, PolicyOPASubSys,
IdentityLDAPSubSys, IdentityLDAPSubSys,
IdentityOpenIDSubSys, IdentityOpenIDSubSys,
HealSubSys,
}...) }...)
// 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 {
+18 -5
View File
@@ -22,10 +22,12 @@ import (
"crypto/x509" "crypto/x509"
"errors" "errors"
"fmt" "fmt"
"io"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go"
@@ -78,28 +80,39 @@ func (c *OperatorDNS) Put(bucket string) error {
defer cancel() defer cancel()
e, err := c.endpoint(bucket, false) e, err := c.endpoint(bucket, false)
if err != nil { if err != nil {
return err return newError(bucket, err)
} }
req, err := http.NewRequestWithContext(ctx, http.MethodPost, e, nil) req, err := http.NewRequestWithContext(ctx, http.MethodPost, e, nil)
if err != nil { if err != nil {
return err return newError(bucket, err)
} }
if err = c.addAuthHeader(req); err != nil { if err = c.addAuthHeader(req); err != nil {
return err return newError(bucket, err)
} }
resp, err := c.httpClient.Do(req) resp, err := c.httpClient.Do(req)
if err != nil { if err != nil {
if derr := c.Delete(bucket); derr != nil { if derr := c.Delete(bucket); derr != nil {
return derr return newError(bucket, derr)
} }
} }
var errorStringBuilder strings.Builder
io.Copy(&errorStringBuilder, io.LimitReader(resp.Body, resp.ContentLength))
xhttp.DrainBody(resp.Body) xhttp.DrainBody(resp.Body)
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return fmt.Errorf("request to create the service for bucket %s, failed with status %s", bucket, resp.Status) errorString := errorStringBuilder.String()
return newError(bucket, fmt.Errorf("service create for bucket %s, failed with status %s, error %s", bucket, resp.Status, errorString))
} }
return nil return nil
} }
func newError(bucket string, err error) error {
e := Error{bucket, err}
if strings.Contains(err.Error(), "invalid bucket name") {
return ErrInvalidBucketName(e)
}
return e
}
// Delete - Removes DNS entries added in Put(). // Delete - Removes DNS entries added in Put().
func (c *OperatorDNS) Delete(bucket string) error { func (c *OperatorDNS) Delete(bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultOperatorContextTimeout) ctx, cancel := context.WithTimeout(context.Background(), defaultOperatorContextTimeout)
+16
View File
@@ -16,6 +16,22 @@
package dns package dns
// Error - DNS related errors error.
type Error struct {
Bucket string
Err error
}
// ErrInvalidBucketName for buckets with invalid name
type ErrInvalidBucketName Error
func (e ErrInvalidBucketName) Error() string {
return "invalid bucket name error: " + e.Err.Error()
}
func (e Error) Error() string {
return "dns related error: " + e.Err.Error()
}
// Store dns record store // Store dns record store
type Store interface { type Store interface {
Put(bucket string) error Put(bucket string) error
+4 -7
View File
@@ -19,7 +19,6 @@ package config
import ( import (
"errors" "errors"
"fmt" "fmt"
"io"
"net" "net"
"syscall" "syscall"
@@ -111,18 +110,16 @@ func ErrorToErr(err error) Err {
case *net.OpError: case *net.OpError:
return ErrPortAccess(err).Msg("Insufficient permissions to use specified port") return ErrPortAccess(err).Msg("Insufficient permissions to use specified port")
} }
return ErrNoPermissionsToAccessDirFiles(err).Msg("Insufficient permissions to access path")
} else if errors.Is(err, io.ErrUnexpectedEOF) {
return ErrUnexpectedDataContent(err)
} else {
// Failed to identify what type of error this, return a simple UI error
return Err{msg: err.Error()}
} }
// Failed to identify what type of error this, return a simple UI error
return Err{msg: err.Error()}
} }
// FmtError converts a fatal error message to a more clear error // FmtError converts a fatal error message to a more clear error
// using some colors // using some colors
func FmtError(introMsg string, err error, jsonFlag bool) string { func FmtError(introMsg string, err error, jsonFlag bool) string {
renderedTxt := "" renderedTxt := ""
uiErr := ErrorToErr(err) uiErr := ErrorToErr(err)
// JSON print // JSON print
+6 -12
View File
@@ -102,6 +102,12 @@ var (
"MINIO_CACHE_RANGE: Valid expected value is `on` or `off`", "MINIO_CACHE_RANGE: Valid expected value is `on` or `off`",
) )
ErrInvalidCacheCommitValue = newErrFn(
"Invalid cache commit value",
"Please check the passed value",
"MINIO_CACHE_COMMIT: Valid expected value is `writeback` or `writethrough`",
)
ErrInvalidRotatingCredentialsBackendEncrypted = newErrFn( ErrInvalidRotatingCredentialsBackendEncrypted = newErrFn(
"Invalid rotating credentials", "Invalid rotating credentials",
"Please set correct rotating credentials in the environment for decryption", "Please set correct rotating credentials in the environment for decryption",
@@ -205,12 +211,6 @@ Example 1:
`Use 'sudo setcap cap_net_bind_service=+ep /path/to/minio' to provide sufficient permissions`, `Use 'sudo setcap cap_net_bind_service=+ep /path/to/minio' to provide sufficient permissions`,
) )
ErrNoPermissionsToAccessDirFiles = newErrFn(
"Missing permissions to access the specified path",
"Please ensure the specified path can be accessed",
"",
)
ErrSSLUnexpectedError = newErrFn( ErrSSLUnexpectedError = newErrFn(
"Invalid TLS certificate", "Invalid TLS certificate",
"Please check the content of your certificate data", "Please check the content of your certificate data",
@@ -247,12 +247,6 @@ Example 1:
"", "",
) )
ErrUnexpectedDataContent = newErrFn(
"Unexpected data content",
"Please contact MinIO at https://slack.min.io",
"",
)
ErrUnexpectedError = newErrFn( ErrUnexpectedError = newErrFn(
"Unexpected error", "Unexpected error",
"Please contact MinIO at https://slack.min.io", "Please contact MinIO at https://slack.min.io",
+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 heal
import (
"errors"
"github.com/minio/minio/cmd/config"
)
// Compression environment variables
const (
Bitrot = "bitrotscan"
)
// Config represents the heal 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 heal settings
DefaultKVS = config.KVS{
config.KV{
Key: Bitrot,
Value: config.EnableOff,
},
}
// Help provides help for config values
Help = config.HelpKVS{
config.HelpKV{
Key: Bitrot,
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.HealSubSys, kvs, DefaultKVS); err != nil {
return cfg, err
}
bitrot := kvs.Get(Bitrot)
if bitrot != config.EnableOn && bitrot != config.EnableOff {
return cfg, errors.New(Bitrot + ": must be 'on' or 'off'")
}
cfg.Bitrot = bitrot == config.EnableOn
return cfg, nil
}
+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
+43 -12
View File
@@ -807,6 +807,10 @@ var (
Key: target.MySQLQueueLimit, Key: target.MySQLQueueLimit,
Value: "0", Value: "0",
}, },
config.KV{
Key: target.MySQLMaxOpenConnections,
Value: "2",
},
} }
) )
@@ -855,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
@@ -1235,6 +1251,10 @@ var (
Key: target.PostgresQueueLimit, Key: target.PostgresQueueLimit,
Value: "0", Value: "0",
}, },
config.KV{
Key: target.PostgresMaxOpenConnections,
Value: "2",
},
} }
) )
@@ -1285,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
+6
View File
@@ -33,6 +33,12 @@ var (
Optional: true, Optional: true,
Type: "string", Type: "string",
}, },
config.HelpKV{
Key: ClassDMA,
Description: `enable O_DIRECT for both read and write, defaults to "write" e.g. "read+write"`,
Optional: true,
Type: "string",
},
config.HelpKV{ config.HelpKV{
Key: config.Comment, Key: config.Comment,
Description: config.DefaultComment, Description: config.DefaultComment,
+44 -3
View File
@@ -18,6 +18,7 @@ package storageclass
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@@ -32,17 +33,26 @@ const (
RRS = "REDUCED_REDUNDANCY" RRS = "REDUCED_REDUNDANCY"
// Standard storage class // Standard storage class
STANDARD = "STANDARD" STANDARD = "STANDARD"
// DMA storage class
DMA = "DMA"
// Valid values are "write" and "read+write"
DMAWrite = "write"
DMAReadWrite = "read+write"
) )
// Standard constats for config info storage class // Standard constats for config info storage class
const ( const (
ClassStandard = "standard" ClassStandard = "standard"
ClassRRS = "rrs" ClassRRS = "rrs"
ClassDMA = "dma"
// Reduced redundancy storage class environment variable // Reduced redundancy storage class environment variable
RRSEnv = "MINIO_STORAGE_CLASS_RRS" RRSEnv = "MINIO_STORAGE_CLASS_RRS"
// Standard storage class environment variable // Standard storage class environment variable
StandardEnv = "MINIO_STORAGE_CLASS_STANDARD" StandardEnv = "MINIO_STORAGE_CLASS_STANDARD"
// DMA storage class environment variable
DMAEnv = "MINIO_STORAGE_CLASS_DMA"
// Supported storage class scheme is EC // Supported storage class scheme is EC
schemePrefix = "EC" schemePrefix = "EC"
@@ -52,6 +62,9 @@ const (
// Default RRS parity is always minimum parity. // Default RRS parity is always minimum parity.
defaultRRSParity = minParityDisks defaultRRSParity = minParityDisks
// Default DMA value
defaultDMA = DMAWrite
) )
// DefaultKVS - default storage class config // DefaultKVS - default storage class config
@@ -65,18 +78,24 @@ var (
Key: ClassRRS, Key: ClassRRS,
Value: "EC:2", Value: "EC:2",
}, },
config.KV{
Key: ClassDMA,
Value: defaultDMA,
},
} }
) )
// StorageClass - holds storage class information // StorageClass - holds storage class information
type StorageClass struct { type StorageClass struct {
Parity int Parity int
DMA string
} }
// Config storage class configuration // Config storage class configuration
type Config struct { type Config struct {
Standard StorageClass `json:"standard"` Standard StorageClass `json:"standard"`
RRS StorageClass `json:"rrs"` RRS StorageClass `json:"rrs"`
DMA StorageClass `json:"dma"`
} }
// UnmarshalJSON - Validate SS and RRS parity when unmarshalling JSON. // UnmarshalJSON - Validate SS and RRS parity when unmarshalling JSON.
@@ -93,7 +112,7 @@ func (sCfg *Config) UnmarshalJSON(data []byte) error {
// IsValid - returns true if input string is a valid // IsValid - returns true if input string is a valid
// storage class kind supported. // storage class kind supported.
func IsValid(sc string) bool { func IsValid(sc string) bool {
return sc == RRS || sc == STANDARD return sc == RRS || sc == STANDARD || sc == DMA
} }
// UnmarshalText unmarshals storage class from its textual form into // UnmarshalText unmarshals storage class from its textual form into
@@ -103,6 +122,14 @@ func (sc *StorageClass) UnmarshalText(b []byte) error {
if scStr == "" { if scStr == "" {
return nil return nil
} }
if scStr == DMAWrite {
sc.DMA = DMAWrite
return nil
}
if scStr == DMAReadWrite {
sc.DMA = DMAReadWrite
return nil
}
s, err := parseStorageClass(scStr) s, err := parseStorageClass(scStr)
if err != nil { if err != nil {
return err return err
@@ -116,14 +143,14 @@ func (sc *StorageClass) MarshalText() ([]byte, error) {
if sc.Parity != 0 { if sc.Parity != 0 {
return []byte(fmt.Sprintf("%s:%d", schemePrefix, sc.Parity)), nil return []byte(fmt.Sprintf("%s:%d", schemePrefix, sc.Parity)), nil
} }
return []byte(""), nil return []byte(sc.DMA), nil
} }
func (sc *StorageClass) String() string { func (sc *StorageClass) String() string {
if sc.Parity != 0 { if sc.Parity != 0 {
return fmt.Sprintf("%s:%d", schemePrefix, sc.Parity) return fmt.Sprintf("%s:%d", schemePrefix, sc.Parity)
} }
return "" return sc.DMA
} }
// Parses given storageClassEnv and returns a storageClass structure. // Parses given storageClassEnv and returns a storageClass structure.
@@ -212,6 +239,11 @@ func (sCfg Config) GetParityForSC(sc string) (parity int) {
} }
} }
// GetDMA - returns DMA configuration.
func (sCfg Config) GetDMA() string {
return sCfg.DMA.DMA
}
// Enabled returns if etcd is enabled. // Enabled returns if etcd is enabled.
func Enabled(kvs config.KVS) bool { func Enabled(kvs config.KVS) bool {
ssc := kvs.Get(ClassStandard) ssc := kvs.Get(ClassStandard)
@@ -231,6 +263,7 @@ func LookupConfig(kvs config.KVS, setDriveCount int) (cfg Config, err error) {
ssc := env.Get(StandardEnv, kvs.Get(ClassStandard)) ssc := env.Get(StandardEnv, kvs.Get(ClassStandard))
rrsc := env.Get(RRSEnv, kvs.Get(ClassRRS)) rrsc := env.Get(RRSEnv, kvs.Get(ClassRRS))
dma := env.Get(DMAEnv, kvs.Get(ClassDMA))
// Check for environment variables and parse into storageClass struct // Check for environment variables and parse into storageClass struct
if ssc != "" { if ssc != "" {
cfg.Standard, err = parseStorageClass(ssc) cfg.Standard, err = parseStorageClass(ssc)
@@ -252,6 +285,14 @@ func LookupConfig(kvs config.KVS, setDriveCount int) (cfg Config, err error) {
cfg.RRS.Parity = defaultRRSParity cfg.RRS.Parity = defaultRRSParity
} }
if dma == "" {
dma = defaultDMA
}
if dma != DMAReadWrite && dma != DMAWrite {
return Config{}, errors.New(`valid dma values are "read-write" and "write"`)
}
cfg.DMA.DMA = dma
// Validation is done after parsing both the storage classes. This is needed because we need one // Validation is done after parsing both the storage classes. This is needed because we need one
// storage class value to deduce the correct value of the other storage class. // storage class value to deduce the correct value of the other storage class.
if err = validateParity(cfg.Standard.Parity, cfg.RRS.Parity, setDriveCount); err != nil { if err = validateParity(cfg.Standard.Parity, cfg.RRS.Parity, setDriveCount); err != nil {
+32 -4
View File
@@ -40,8 +40,8 @@ type HTTPConsoleLoggerSys struct {
logBuf *ring.Ring logBuf *ring.Ring
} }
func mustGetNodeName(endpointZones EndpointZones) (nodeName string) { func mustGetNodeName(endpointServerSets EndpointServerSets) (nodeName string) {
host, err := xnet.ParseHost(GetLocalPeer(endpointZones)) host, err := xnet.ParseHost(GetLocalPeer(endpointServerSets))
if err != nil { if err != nil {
logger.FatalIf(err, "Unable to start console logging subsystem") logger.FatalIf(err, "Unable to start console logging subsystem")
} }
@@ -63,8 +63,8 @@ func NewConsoleLogger(ctx context.Context) *HTTPConsoleLoggerSys {
} }
// SetNodeName - sets the node name if any after distributed setup has initialized // SetNodeName - sets the node name if any after distributed setup has initialized
func (sys *HTTPConsoleLoggerSys) SetNodeName(endpointZones EndpointZones) { func (sys *HTTPConsoleLoggerSys) SetNodeName(endpointServerSets EndpointServerSets) {
sys.nodeName = mustGetNodeName(endpointZones) sys.nodeName = mustGetNodeName(endpointServerSets)
} }
// HasLogListeners returns true if console log listeners are registered // HasLogListeners returns true if console log listeners are registered
@@ -122,6 +122,34 @@ func (sys *HTTPConsoleLoggerSys) Validate() error {
return nil return nil
} }
// Endpoint - dummy function for interface compatibility
func (sys *HTTPConsoleLoggerSys) Endpoint() string {
return sys.console.Endpoint()
}
// String - stringer function for interface compatibility
func (sys *HTTPConsoleLoggerSys) String() string {
return "console+http"
}
// 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 {
+5 -7
View File
@@ -177,11 +177,10 @@ func (kes *kesService) CreateKey(keyID string) error { return kes.client.CreateK
// named key referenced by keyID. It also binds the generated key // named key referenced by keyID. It also binds the generated key
// cryptographically to the provided context. // cryptographically to the provided context.
func (kes *kesService) GenerateKey(keyID string, ctx Context) (key [32]byte, sealedKey []byte, err error) { func (kes *kesService) GenerateKey(keyID string, ctx Context) (key [32]byte, sealedKey []byte, err error) {
var context bytes.Buffer context := ctx.AppendTo(make([]byte, 0, 128))
ctx.WriteTo(&context)
var plainKey []byte var plainKey []byte
plainKey, sealedKey, err = kes.client.GenerateDataKey(keyID, context.Bytes()) plainKey, sealedKey, err = kes.client.GenerateDataKey(keyID, context)
if err != nil { if err != nil {
return key, nil, err return key, nil, err
} }
@@ -200,11 +199,10 @@ func (kes *kesService) GenerateKey(keyID string, ctx Context) (key [32]byte, sea
// The context must be same context as the one provided while // The context must be same context as the one provided while
// generating the plaintext key / sealedKey. // generating the plaintext key / sealedKey.
func (kes *kesService) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key [32]byte, err error) { func (kes *kesService) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key [32]byte, err error) {
var context bytes.Buffer context := ctx.AppendTo(make([]byte, 0, 128))
ctx.WriteTo(&context)
var plainKey []byte var plainKey []byte
plainKey, err = kes.client.DecryptDataKey(keyID, sealedKey, context.Bytes()) plainKey, err = kes.client.DecryptDataKey(keyID, sealedKey, context)
if err != nil { if err != nil {
return key, err return key, err
} }
@@ -415,7 +413,7 @@ func (c *kesClient) postRetry(path string, body io.ReadSeeker, limit int64) (io.
} }
// If the error is not temp. / retryable => fail the request immediately. // If the error is not temp. / retryable => fail the request immediately.
if !xnet.IsNetworkOrHostDown(err) && if !xnet.IsNetworkOrHostDown(err, false) &&
!errors.Is(err, io.EOF) && !errors.Is(err, io.EOF) &&
!errors.Is(err, io.ErrUnexpectedEOF) && !errors.Is(err, io.ErrUnexpectedEOF) &&
!errors.Is(err, context.DeadlineExceeded) { !errors.Is(err, context.DeadlineExceeded) {
+2 -8
View File
@@ -103,7 +103,6 @@ func (key ObjectKey) Seal(extKey, iv [32]byte, domain, bucket, object string) Se
func (key *ObjectKey) Unseal(extKey [32]byte, sealedKey SealedKey, domain, bucket, object string) error { func (key *ObjectKey) Unseal(extKey [32]byte, sealedKey SealedKey, domain, bucket, object string) error {
var ( var (
unsealConfig sio.Config unsealConfig sio.Config
decryptedKey bytes.Buffer
) )
switch sealedKey.Algorithm { switch sealedKey.Algorithm {
default: default:
@@ -122,10 +121,9 @@ func (key *ObjectKey) Unseal(extKey [32]byte, sealedKey SealedKey, domain, bucke
unsealConfig = sio.Config{MinVersion: sio.Version10, Key: sha.Sum(nil)} unsealConfig = sio.Config{MinVersion: sio.Version10, Key: sha.Sum(nil)}
} }
if n, err := sio.Decrypt(&decryptedKey, bytes.NewReader(sealedKey.Key[:]), unsealConfig); n != 32 || err != nil { if out, err := sio.DecryptBuffer(key[:0], sealedKey.Key[:], unsealConfig); len(out) != 32 || err != nil {
return ErrSecretKeyMismatch return ErrSecretKeyMismatch
} }
copy(key[:], decryptedKey.Bytes())
return nil return nil
} }
@@ -165,11 +163,7 @@ func (key ObjectKey) UnsealETag(etag []byte) ([]byte, error) {
if !IsETagSealed(etag) { if !IsETagSealed(etag) {
return etag, nil return etag, nil
} }
var buffer bytes.Buffer
mac := hmac.New(sha256.New, key[:]) mac := hmac.New(sha256.New, key[:])
mac.Write([]byte("SSE-etag")) mac.Write([]byte("SSE-etag"))
if _, err := sio.Decrypt(&buffer, bytes.NewReader(etag), sio.Config{Key: mac.Sum(nil)}); err != nil { return sio.DecryptBuffer(make([]byte, 0, len(etag)), etag, sio.Config{Key: mac.Sum(nil)})
return nil, err
}
return buffer.Bytes(), nil
} }
+52 -4
View File
@@ -39,6 +39,8 @@ type Context map[string]string
// //
// WriteTo sorts the context keys and writes the sorted // WriteTo sorts the context keys and writes the sorted
// key-value pairs as canonical JSON object to w. // key-value pairs as canonical JSON object to w.
//
// Note that neither keys nor values are escaped for JSON.
func (c Context) WriteTo(w io.Writer) (n int64, err error) { func (c Context) WriteTo(w io.Writer) (n int64, err error) {
sortedKeys := make(sort.StringSlice, 0, len(c)) sortedKeys := make(sort.StringSlice, 0, len(c))
for k := range c { for k := range c {
@@ -67,6 +69,53 @@ func (c Context) WriteTo(w io.Writer) (n int64, err error) {
return n + int64(nn), err return n + int64(nn), err
} }
// AppendTo appends the context in a canonical from to dst.
//
// AppendTo sorts the context keys and writes the sorted
// key-value pairs as canonical JSON object to w.
//
// Note that neither keys nor values are escaped for JSON.
func (c Context) AppendTo(dst []byte) (output []byte) {
if len(c) == 0 {
return append(dst, '{', '}')
}
// out should not escape.
out := bytes.NewBuffer(dst)
// No need to copy+sort
if len(c) == 1 {
for k, v := range c {
out.WriteString(`{"`)
out.WriteString(k)
out.WriteString(`":"`)
out.WriteString(v)
out.WriteString(`"}`)
}
return out.Bytes()
}
sortedKeys := make([]string, 0, len(c))
for k := range c {
sortedKeys = append(sortedKeys, k)
}
sort.Strings(sortedKeys)
out.WriteByte('{')
for i, k := range sortedKeys {
out.WriteByte('"')
out.WriteString(k)
out.WriteString(`":"`)
out.WriteString(c[k])
out.WriteByte('"')
if i < len(sortedKeys)-1 {
out.WriteByte(',')
}
}
out.WriteByte('}')
return out.Bytes()
}
// KMS represents an active and authenticted connection // KMS represents an active and authenticted connection
// to a Key-Management-Service. It supports generating // to a Key-Management-Service. It supports generating
// data key generation and unsealing of KMS-generated // data key generation and unsealing of KMS-generated
@@ -155,13 +204,12 @@ func (kms *masterKeyKMS) Info() (info KMSInfo) {
func (kms *masterKeyKMS) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key [32]byte, err error) { func (kms *masterKeyKMS) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key [32]byte, err error) {
var ( var (
buffer bytes.Buffer
derivedKey = kms.deriveKey(keyID, ctx) derivedKey = kms.deriveKey(keyID, ctx)
) )
if n, err := sio.Decrypt(&buffer, bytes.NewReader(sealedKey), sio.Config{Key: derivedKey[:]}); err != nil || n != 32 { out, err := sio.DecryptBuffer(key[:0], sealedKey, sio.Config{Key: derivedKey[:]})
if err != nil || len(out) != 32 {
return key, err // TODO(aead): upgrade sio to use sio.Error return key, err // TODO(aead): upgrade sio to use sio.Error
} }
copy(key[:], buffer.Bytes())
return key, nil return key, nil
} }
@@ -171,7 +219,7 @@ func (kms *masterKeyKMS) deriveKey(keyID string, context Context) (key [32]byte)
} }
mac := hmac.New(sha256.New, kms.masterKey[:]) mac := hmac.New(sha256.New, kms.masterKey[:])
mac.Write([]byte(keyID)) mac.Write([]byte(keyID))
context.WriteTo(mac) mac.Write(context.AppendTo(make([]byte, 0, 128)))
mac.Sum(key[:0]) mac.Sum(key[:0])
return key return key
} }
+30
View File
@@ -16,6 +16,7 @@ package crypto
import ( import (
"bytes" "bytes"
"fmt"
"path" "path"
"strings" "strings"
"testing" "testing"
@@ -83,3 +84,32 @@ func TestContextWriteTo(t *testing.T) {
} }
} }
} }
func TestContextAppendTo(t *testing.T) {
for i, test := range contextWriteToTests {
dst := make([]byte, 0, 1024)
dst = test.Context.AppendTo(dst)
if s := string(dst); s != test.ExpectedJSON {
t.Errorf("Test %d: JSON representation differ - got: '%s' want: '%s'", i, s, test.ExpectedJSON)
}
// Append one more
dst = test.Context.AppendTo(dst)
if s := string(dst); s != test.ExpectedJSON+test.ExpectedJSON {
t.Errorf("Test %d: JSON representation differ - got: '%s' want: '%s'", i, s, test.ExpectedJSON+test.ExpectedJSON)
}
}
}
func BenchmarkContext_AppendTo(b *testing.B) {
tests := []Context{{}, {"bucket": "warp-benchmark-bucket"}, {"0": "1", "-": "2", ".": "#"}, {"34trg": "dfioutr89", "ikjfdghkjf": "jkedfhgfjkhg", "sdfhsdjkh": "if88889", "asddsirfh804": "kjfdshgdfuhgfg78-45604586#$%"}}
for _, test := range tests {
b.Run(fmt.Sprintf("%d-elems", len(test)), func(b *testing.B) {
dst := make([]byte, 0, 1024)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
dst = test.AppendTo(dst[:0])
}
})
}
}
+8 -6
View File
@@ -204,15 +204,17 @@ func (s3) ParseMetadata(metadata map[string]string) (keyID string, kmsKey []byte
} }
// Check whether all extracted values are well-formed // Check whether all extracted values are well-formed
iv, err := base64.StdEncoding.DecodeString(b64IV) var iv [32]byte
if err != nil || len(iv) != 32 { n, err := base64.StdEncoding.Decode(iv[:], []byte(b64IV))
if err != nil || n != 32 {
return keyID, kmsKey, sealedKey, errInvalidInternalIV return keyID, kmsKey, sealedKey, errInvalidInternalIV
} }
if algorithm != SealAlgorithm { if algorithm != SealAlgorithm {
return keyID, kmsKey, sealedKey, errInvalidInternalSealAlgorithm return keyID, kmsKey, sealedKey, errInvalidInternalSealAlgorithm
} }
encryptedKey, err := base64.StdEncoding.DecodeString(b64SealedKey) var encryptedKey [64]byte
if err != nil || len(encryptedKey) != 64 { n, err = base64.StdEncoding.Decode(encryptedKey[:], []byte(b64SealedKey))
if err != nil || n != 64 {
return keyID, kmsKey, sealedKey, Errorf("The internal sealed key for SSE-S3 is invalid") return keyID, kmsKey, sealedKey, Errorf("The internal sealed key for SSE-S3 is invalid")
} }
if idPresent && kmsKeyPresent { // We are using a KMS -> parse the sealed KMS data key. if idPresent && kmsKeyPresent { // We are using a KMS -> parse the sealed KMS data key.
@@ -223,8 +225,8 @@ func (s3) ParseMetadata(metadata map[string]string) (keyID string, kmsKey []byte
} }
sealedKey.Algorithm = algorithm sealedKey.Algorithm = algorithm
copy(sealedKey.IV[:], iv) sealedKey.IV = iv
copy(sealedKey.Key[:], encryptedKey) sealedKey.Key = encryptedKey
return keyID, kmsKey, sealedKey, nil return keyID, kmsKey, sealedKey, nil
} }
+6 -10
View File
@@ -15,7 +15,6 @@
package crypto package crypto
import ( import (
"bytes"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
@@ -224,11 +223,10 @@ func (v *vaultService) CreateKey(keyID string) error {
// named key referenced by keyID. It also binds the generated key // named key referenced by keyID. It also binds the generated key
// cryptographically to the provided context. // cryptographically to the provided context.
func (v *vaultService) GenerateKey(keyID string, ctx Context) (key [32]byte, sealedKey []byte, err error) { func (v *vaultService) GenerateKey(keyID string, ctx Context) (key [32]byte, sealedKey []byte, err error) {
var contextStream bytes.Buffer context := ctx.AppendTo(make([]byte, 0, 128))
ctx.WriteTo(&contextStream)
payload := map[string]interface{}{ payload := map[string]interface{}{
"context": base64.StdEncoding.EncodeToString(contextStream.Bytes()), "context": base64.StdEncoding.EncodeToString(context),
} }
s, err := v.client.Logical().Write(fmt.Sprintf("/transit/datakey/plaintext/%s", keyID), payload) s, err := v.client.Logical().Write(fmt.Sprintf("/transit/datakey/plaintext/%s", keyID), payload)
if err != nil { if err != nil {
@@ -260,12 +258,11 @@ func (v *vaultService) GenerateKey(keyID string, ctx Context) (key [32]byte, sea
// The context must be same context as the one provided while // The context must be same context as the one provided while
// generating the plaintext key / sealedKey. // generating the plaintext key / sealedKey.
func (v *vaultService) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key [32]byte, err error) { func (v *vaultService) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key [32]byte, err error) {
var contextStream bytes.Buffer context := ctx.AppendTo(make([]byte, 0, 128))
ctx.WriteTo(&contextStream)
payload := map[string]interface{}{ payload := map[string]interface{}{
"ciphertext": string(sealedKey), "ciphertext": string(sealedKey),
"context": base64.StdEncoding.EncodeToString(contextStream.Bytes()), "context": base64.StdEncoding.EncodeToString(context),
} }
s, err := v.client.Logical().Write(fmt.Sprintf("/transit/decrypt/%s", keyID), payload) s, err := v.client.Logical().Write(fmt.Sprintf("/transit/decrypt/%s", keyID), payload)
@@ -294,12 +291,11 @@ func (v *vaultService) UnsealKey(keyID string, sealedKey []byte, ctx Context) (k
// The context must be same context as the one provided while // The context must be same context as the one provided while
// generating the plaintext key / sealedKey. // generating the plaintext key / sealedKey.
func (v *vaultService) UpdateKey(keyID string, sealedKey []byte, ctx Context) (rotatedKey []byte, err error) { func (v *vaultService) UpdateKey(keyID string, sealedKey []byte, ctx Context) (rotatedKey []byte, err error) {
var contextStream bytes.Buffer context := ctx.AppendTo(make([]byte, 0, 128))
ctx.WriteTo(&contextStream)
payload := map[string]interface{}{ payload := map[string]interface{}{
"ciphertext": string(sealedKey), "ciphertext": string(sealedKey),
"context": base64.StdEncoding.EncodeToString(contextStream.Bytes()), "context": base64.StdEncoding.EncodeToString(context),
} }
s, err := v.client.Logical().Write(fmt.Sprintf("/transit/rewrap/%s", keyID), payload) s, err := v.client.Logical().Write(fmt.Sprintf("/transit/rewrap/%s", keyID), payload)
if err != nil { if err != nil {
+62 -30
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/heal"
"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 (
globalHealConfig heal.Config
dataCrawlerLeaderLockTimeout = newDynamicTimeout(30*time.Second, 10*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(minioMetaBucket, "runDataCrawler.lock")
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for {
err := locker.GetLock(ctx, 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
@@ -114,12 +135,11 @@ type cachedFolder struct {
} }
type folderScanner struct { type folderScanner struct {
root string root string
getSize getSizeFn getSize getSizeFn
oldCache dataUsageCache oldCache dataUsageCache
newCache dataUsageCache newCache dataUsageCache
withFilter *bloomFilter withFilter *bloomFilter
waitForLowActiveIO func()
dataUsageCrawlMult float64 dataUsageCrawlMult float64
dataUsageCrawlDebug bool dataUsageCrawlDebug bool
@@ -134,7 +154,7 @@ type folderScanner struct {
// The returned cache will always be valid, but may not be updated from the existing. // The returned cache will always be valid, but may not be updated from the existing.
// Before each operation waitForLowActiveIO is called which can be used to temporarily halt the crawler. // Before each operation waitForLowActiveIO is called which can be used to temporarily halt the crawler.
// If the supplied context is canceled the function will return at the first chance. // If the supplied context is canceled the function will return at the first chance.
func crawlDataFolder(ctx context.Context, basePath string, cache dataUsageCache, waitForLowActiveIO func(), getSize getSizeFn) (dataUsageCache, error) { func crawlDataFolder(ctx context.Context, basePath string, cache dataUsageCache, getSize getSizeFn) (dataUsageCache, error) {
t := UTCNow() t := UTCNow()
logPrefix := color.Green("data-usage: ") logPrefix := color.Green("data-usage: ")
@@ -162,7 +182,6 @@ func crawlDataFolder(ctx context.Context, basePath string, cache dataUsageCache,
getSize: getSize, getSize: getSize,
oldCache: cache, oldCache: cache,
newCache: dataUsageCache{Info: cache.Info}, newCache: dataUsageCache{Info: cache.Info},
waitForLowActiveIO: waitForLowActiveIO,
newFolders: nil, newFolders: nil,
existingFolders: nil, existingFolders: nil,
dataUsageCrawlMult: delayMult, dataUsageCrawlMult: delayMult,
@@ -174,9 +193,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 +346,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 {
@@ -355,7 +374,6 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
} }
} }
} }
f.waitForLowActiveIO()
sleepDuration(dataCrawlSleepPerFolder, f.dataUsageCrawlMult) sleepDuration(dataCrawlSleepPerFolder, f.dataUsageCrawlMult)
cache := dataUsageEntry{} cache := dataUsageEntry{}
@@ -403,7 +421,7 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
} }
return nil return nil
} }
f.waitForLowActiveIO()
// Dynamic time delay. // Dynamic time delay.
t := UTCNow() t := UTCNow()
@@ -441,7 +459,7 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
continue continue
} }
objAPI := newObjectLayerWithoutSafeModeFn() objAPI := newObjectLayerFn()
if objAPI == nil { if objAPI == nil {
continue continue
} }
@@ -463,14 +481,22 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
// If that doesn't bring it back we remove the folder and assume it was deleted. // If that doesn't bring it back we remove the folder and assume it was deleted.
// This means that the next run will not look for it. // This means that the next run will not look for it.
for k := range existing { for k := range existing {
f.waitForLowActiveIO()
bucket, prefix := path2BucketObject(k) bucket, prefix := path2BucketObject(k)
if f.dataUsageCrawlDebug { if f.dataUsageCrawlDebug {
logger.Info(color.Green("folder-scanner:")+" checking disappeared folder: %v/%v", bucket, prefix) logger.Info(color.Green("folder-scanner:")+" checking disappeared folder: %v/%v", bucket, prefix)
} }
// Dynamic time delay.
t := UTCNow()
err = objAPI.HealObjects(ctx, bucket, prefix, madmin.HealOpts{Recursive: true, Remove: healDeleteDangling}, err = objAPI.HealObjects(ctx, bucket, prefix, madmin.HealOpts{Recursive: true, Remove: healDeleteDangling},
func(bucket, object, versionID string) error { func(bucket, object, versionID string) error {
// Wait for each heal as per crawler frequency.
sleepDuration(time.Since(t), f.dataUsageCrawlMult)
defer func() {
t = UTCNow()
}()
return bgSeq.queueHealTask(healSource{ return bgSeq.queueHealTask(healSource{
bucket: bucket, bucket: bucket,
object: object, object: object,
@@ -478,6 +504,8 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
}, madmin.HealItemObject) }, madmin.HealItemObject)
}) })
sleepDuration(time.Since(t), f.dataUsageCrawlMult)
if f.dataUsageCrawlDebug && err != nil { if f.dataUsageCrawlDebug && err != nil {
logger.Info(color.Green("healObjects:")+" checking returned value %v", err) logger.Info(color.Green("healObjects:")+" checking returned value %v", err)
} }
@@ -514,7 +542,6 @@ func (f *folderScanner) deepScanFolder(ctx context.Context, folder cachedFolder)
default: default:
} }
f.waitForLowActiveIO()
if typ&os.ModeDir != 0 { if typ&os.ModeDir != 0 {
dirStack = append(dirStack, entName) dirStack = append(dirStack, entName)
err := readDirFn(path.Join(dirStack...), addDir) err := readDirFn(path.Join(dirStack...), addDir)
@@ -624,7 +651,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
} }
@@ -728,9 +755,14 @@ func (i *crawlItem) applyActions(ctx context.Context, o ObjectLayer, meta action
return size return size
} }
eventName := event.ObjectRemovedDelete
if obj.DeleteMarker {
eventName = event.ObjectRemovedDeleteMarkerCreated
}
// Notify object deleted event. // Notify object deleted event.
sendEvent(eventArgs{ sendEvent(eventArgs{
EventName: event.ObjectRemovedDelete, EventName: eventName,
BucketName: i.bucket, BucketName: i.bucket,
Object: obj, Object: obj,
Host: "Internal: [ILM-EXPIRY]", Host: "Internal: [ILM-EXPIRY]",
@@ -743,13 +775,16 @@ func (i *crawlItem) objectPath() string {
return path.Join(i.prefix, i.objectName) return path.Join(i.prefix, i.objectName)
} }
// sleepDuration multiplies the duration d by x and sleeps if is more than 100 micro seconds. // sleepDuration multiplies the duration d by x
// sleep is limited to max 1 second. // and sleeps if is more than 100 micro seconds.
// Sleep is limited to max 15 seconds.
func sleepDuration(d time.Duration, x float64) { func sleepDuration(d time.Duration, x float64) {
const maxWait = 15 * time.Second
const minWait = 100 * time.Microsecond
// Don't sleep for really small amount of time // Don't sleep for really small amount of time
if d := time.Duration(float64(d) * x); d > time.Microsecond*100 { if d := time.Duration(float64(d) * x); d > minWait {
if d > time.Second { if d > maxWait {
d = time.Second d = maxWait
} }
time.Sleep(d) time.Sleep(d)
} }
@@ -759,9 +794,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)
} }
} }
+120 -47
View File
@@ -40,13 +40,13 @@ import (
const ( const (
// Estimate bloom filter size. With this many items // Estimate bloom filter size. With this many items
dataUpdateTrackerEstItems = 1000000 dataUpdateTrackerEstItems = 10000000
// ... we want this false positive rate: // ... we want this false positive rate:
dataUpdateTrackerFP = 0.99 dataUpdateTrackerFP = 0.99
dataUpdateTrackerQueueSize = 10000 dataUpdateTrackerQueueSize = 0
dataUpdateTrackerFilename = dataUsageBucket + SlashSeparator + ".tracker.bin" dataUpdateTrackerFilename = dataUsageBucket + SlashSeparator + ".tracker.bin"
dataUpdateTrackerVersion = 2 dataUpdateTrackerVersion = 4
dataUpdateTrackerSaveInterval = 5 * time.Minute dataUpdateTrackerSaveInterval = 5 * time.Minute
) )
@@ -168,6 +168,43 @@ func (d *dataUpdateTracker) current() uint64 {
return d.Current.idx return d.Current.idx
} }
// latestWithDir returns the highest index that contains the directory.
// This means that any cycle higher than this does NOT contain the entry.
func (d *dataUpdateTracker) latestWithDir(dir string) uint64 {
bucket, _ := path2BucketObjectWithBasePath("", dir)
if bucket == "" {
if d.debug && len(dir) > 0 {
logger.Info(color.Green("dataUpdateTracker:")+" no bucket (%s)", dir)
}
return d.current()
}
if isReservedOrInvalidBucket(bucket, false) {
if d.debug {
logger.Info(color.Green("dataUpdateTracker:")+" isReservedOrInvalidBucket: %v, entry: %v", bucket, dir)
}
return d.current()
}
d.mu.Lock()
defer d.mu.Unlock()
if d.Current.bf.containsDir(dir) || d.Current.idx == 0 {
return d.Current.idx
}
if d.debug {
logger.Info("current bloom does NOT contains dir %s", dir)
}
idx := d.Current.idx - 1
for {
f := d.History.find(idx)
if f == nil || f.bf.containsDir(dir) || idx == 0 {
break
}
idx--
}
return idx
}
// start will load the current data from the drives start collecting information and // start will load the current data from the drives start collecting information and
// start a saver goroutine. // start a saver goroutine.
// All of these will exit when the context is canceled. // All of these will exit when the context is canceled.
@@ -236,7 +273,10 @@ func (d *dataUpdateTracker) startSaver(ctx context.Context, interval time.Durati
d.mu.Lock() d.mu.Lock()
if !d.dirty { if !d.dirty {
d.mu.Unlock() d.mu.Unlock()
return if exit {
return
}
continue
} }
d.Saved = UTCNow() d.Saved = UTCNow()
err := d.serialize(&buf) err := d.serialize(&buf)
@@ -363,7 +403,7 @@ func (d *dataUpdateTracker) deserialize(src io.Reader, newerThan time.Time) erro
return err return err
} }
switch tmp[0] { switch tmp[0] {
case 1: case 1, 2, 3:
logger.Info(color.Green("dataUpdateTracker: ") + "deprecated data version, updating.") logger.Info(color.Green("dataUpdateTracker: ") + "deprecated data version, updating.")
return nil return nil
case dataUpdateTrackerVersion: case dataUpdateTrackerVersion:
@@ -426,6 +466,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
@@ -435,41 +477,66 @@ func (d *dataUpdateTracker) deserialize(src io.Reader, newerThan time.Time) erro
// start a collector that picks up entries from objectUpdatedCh // start a collector that picks up entries from objectUpdatedCh
// and adds them to the current bloom filter. // and adds them to the current bloom filter.
func (d *dataUpdateTracker) startCollector(ctx context.Context) { func (d *dataUpdateTracker) startCollector(ctx context.Context) {
for { for in := range d.input {
select { bucket, _ := path2BucketObjectWithBasePath("", in)
case <-ctx.Done(): if bucket == "" {
return if d.debug && len(in) > 0 {
case in := <-d.input: logger.Info(color.Green("dataUpdateTracker:")+" no bucket (%s)", in)
bucket, _ := path2BucketObjectWithBasePath("", in)
if bucket == "" {
if d.debug && len(in) > 0 {
logger.Info(color.Green("data-usage:")+" no bucket (%s)", in)
}
continue
} }
continue
if isReservedOrInvalidBucket(bucket, false) {
if false && d.debug {
logger.Info(color.Green("data-usage:")+" isReservedOrInvalidBucket: %v, entry: %v", bucket, in)
}
continue
}
split := splitPathDeterministic(in)
// Add all paths until level 3.
d.mu.Lock()
for i := range split {
if d.debug && false {
logger.Info(color.Green("dataUpdateTracker:") + " Marking path dirty: " + color.Blue(path.Join(split[:i+1]...)))
}
d.Current.bf.AddString(hashPath(path.Join(split[:i+1]...)).String())
}
d.dirty = d.dirty || len(split) > 0
d.mu.Unlock()
} }
if isReservedOrInvalidBucket(bucket, false) {
if d.debug {
logger.Info(color.Green("dataUpdateTracker:")+" isReservedOrInvalidBucket: %v, entry: %v", bucket, in)
}
continue
}
split := splitPathDeterministic(in)
// Add all paths until done.
d.mu.Lock()
for i := range split {
if d.debug {
logger.Info(color.Green("dataUpdateTracker:") + " Marking path dirty: " + color.Blue(path.Join(split[:i+1]...)))
}
d.Current.bf.AddString(hashPath(path.Join(split[:i+1]...)).String())
}
d.dirty = d.dirty || len(split) > 0
d.mu.Unlock()
} }
} }
// markDirty adds the supplied path to the current bloom filter.
func (d *dataUpdateTracker) markDirty(in string) {
bucket, _ := path2BucketObjectWithBasePath("", in)
if bucket == "" {
if d.debug && len(in) > 0 {
logger.Info(color.Green("dataUpdateTracker:")+" no bucket (%s)", in)
}
return
}
if isReservedOrInvalidBucket(bucket, false) {
if d.debug && false {
logger.Info(color.Green("dataUpdateTracker:")+" isReservedOrInvalidBucket: %v, entry: %v", bucket, in)
}
return
}
split := splitPathDeterministic(in)
// Add all paths until done.
d.mu.Lock()
for i := range split {
if d.debug {
logger.Info(color.Green("dataUpdateTracker:") + " Marking path dirty: " + color.Blue(path.Join(split[:i+1]...)))
}
d.Current.bf.AddString(hashPath(path.Join(split[:i+1]...)).String())
}
d.dirty = d.dirty || len(split) > 0
d.mu.Unlock()
}
// find entry with specified index. // find entry with specified index.
// Returns nil if not found. // Returns nil if not found.
func (d dataUpdateTrackerHistory) find(idx uint64) *dataUpdateFilter { func (d dataUpdateTrackerHistory) find(idx uint64) *dataUpdateFilter {
@@ -529,8 +596,13 @@ func (d *dataUpdateTracker) filterFrom(ctx context.Context, oldest, newest uint6
// cycleFilter will cycle the bloom filter to start recording to index y if not already. // cycleFilter will cycle the bloom filter to start recording to index y if not already.
// The response will contain a bloom filter starting at index x up to, but not including index y. // The response will contain a bloom filter starting at index x up to, but not including index y.
// If y is 0, the response will not update y, but return the currently recorded information // If y is 0, the response will not update y, but return the currently recorded information
// from the up until and including current y. // from the oldest (unless 0, then it will be all) until and including current y.
func (d *dataUpdateTracker) cycleFilter(ctx context.Context, oldest, current uint64) (*bloomFilterResponse, error) { func (d *dataUpdateTracker) cycleFilter(ctx context.Context, req bloomFilterRequest) (*bloomFilterResponse, error) {
if req.OldestClean != "" {
return &bloomFilterResponse{OldestIdx: d.latestWithDir(req.OldestClean)}, nil
}
current := req.Current
oldest := req.Oldest
d.mu.Lock() d.mu.Lock()
defer d.mu.Unlock() defer d.mu.Unlock()
if current == 0 { if current == 0 {
@@ -538,7 +610,10 @@ func (d *dataUpdateTracker) cycleFilter(ctx context.Context, oldest, current uin
return d.filterFrom(ctx, d.Current.idx, d.Current.idx), nil return d.filterFrom(ctx, d.Current.idx, d.Current.idx), nil
} }
d.History.sort() d.History.sort()
return d.filterFrom(ctx, d.History[len(d.History)-1].idx, d.Current.idx), nil if oldest == 0 {
oldest = d.History[len(d.History)-1].idx
}
return d.filterFrom(ctx, oldest, d.Current.idx), nil
} }
// Move current to history if new one requested // Move current to history if new one requested
@@ -566,7 +641,7 @@ func (d *dataUpdateTracker) cycleFilter(ctx context.Context, oldest, current uin
// Trailing slashes are removed. // Trailing slashes are removed.
// Returns 0 length if no parts are found after trimming. // Returns 0 length if no parts are found after trimming.
func splitPathDeterministic(in string) []string { func splitPathDeterministic(in string) []string {
split := strings.Split(in, SlashSeparator) split := strings.Split(decodeDirObject(in), SlashSeparator)
// Trim empty start/end // Trim empty start/end
for len(split) > 0 { for len(split) > 0 {
@@ -582,10 +657,6 @@ func splitPathDeterministic(in string) []string {
split = split[:len(split)-1] split = split[:len(split)-1]
} }
// Return up to 3 parts.
if len(split) > 3 {
split = split[:3]
}
return split return split
} }
@@ -594,6 +665,9 @@ func splitPathDeterministic(in string) []string {
type bloomFilterRequest struct { type bloomFilterRequest struct {
Oldest uint64 Oldest uint64
Current uint64 Current uint64
// If set the oldest clean version will be returned in OldestIdx
// and the rest of the request will be ignored.
OldestClean string
} }
type bloomFilterResponse struct { type bloomFilterResponse struct {
@@ -610,10 +684,9 @@ type bloomFilterResponse struct {
} }
// ObjectPathUpdated indicates a path has been updated. // ObjectPathUpdated indicates a path has been updated.
// The function will never block. // The function will block until the entry has been picked up.
func ObjectPathUpdated(s string) { func ObjectPathUpdated(s string) {
select { if intDataUpdateTracker != nil {
case objectUpdatedCh <- s: intDataUpdateTracker.markDirty(s)
default:
} }
} }
+19 -2
View File
@@ -42,6 +42,14 @@ type testingLogger struct {
t testLoggerI t testLoggerI
} }
func (t *testingLogger) Endpoint() string {
return ""
}
func (t *testingLogger) String() string {
return ""
}
func (t *testingLogger) Validate() error { func (t *testingLogger) Validate() error {
return nil return nil
} }
@@ -161,7 +169,12 @@ func TestDataUpdateTracker(t *testing.T) {
}) })
} }
// Cycle to history // Cycle to history
_, err = dut.cycleFilter(ctx, 1, 2) req := bloomFilterRequest{
Oldest: 1,
Current: 2,
}
_, err = dut.cycleFilter(ctx, req)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -192,7 +205,11 @@ func TestDataUpdateTracker(t *testing.T) {
if dut.current() != 2 { if dut.current() != 2 {
t.Fatal("current idx after load not preserved. want 2, got:", dut.current()) t.Fatal("current idx after load not preserved. want 2, got:", dut.current())
} }
bfr2, err := dut.cycleFilter(ctx, 1, 3) req = bloomFilterRequest{
Oldest: 1,
Current: 3,
}
bfr2, err := dut.cycleFilter(ctx, req)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+5 -1
View File
@@ -440,7 +440,11 @@ func (d *dataUsageCache) load(ctx context.Context, store objectIO, name string)
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 {
if !isErrObjectNotFound(err) && !isErrBucketNotFound(err) && !errors.Is(err, InsufficientReadQuorum{}) { switch err.(type) {
case ObjectNotFound:
case BucketNotFound:
case InsufficientReadQuorum:
default:
return toObjectErr(err, dataUsageBucket, name) return toObjectErr(err, dataUsageBucket, name)
} }
*d = dataUsageCache{} *d = dataUsageCache{}
+7 -7
View File
@@ -62,7 +62,7 @@ func TestDataUsageUpdate(t *testing.T) {
return 0, nil return 0, nil
} }
got, err := crawlDataFolder(context.Background(), base, dataUsageCache{Info: dataUsageCacheInfo{Name: bucket}}, func() {}, getSize) got, err := crawlDataFolder(context.Background(), base, dataUsageCache{Info: dataUsageCacheInfo{Name: bucket}}, getSize)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -183,7 +183,7 @@ func TestDataUsageUpdate(t *testing.T) {
}, },
} }
createUsageTestFiles(t, base, bucket, files) createUsageTestFiles(t, base, bucket, files)
got, err = crawlDataFolder(context.Background(), base, got, func() {}, getSize) got, err = crawlDataFolder(context.Background(), base, got, getSize)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -268,7 +268,7 @@ func TestDataUsageUpdate(t *testing.T) {
} }
// Changed dir must be picked up in this many cycles. // Changed dir must be picked up in this many cycles.
for i := 0; i < dataUsageUpdateDirCycles; i++ { for i := 0; i < dataUsageUpdateDirCycles; i++ {
got, err = crawlDataFolder(context.Background(), base, got, func() {}, getSize) got, err = crawlDataFolder(context.Background(), base, got, getSize)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -355,7 +355,7 @@ func TestDataUsageUpdatePrefix(t *testing.T) {
} }
return 0, nil return 0, nil
} }
got, err := crawlDataFolder(context.Background(), base, dataUsageCache{Info: dataUsageCacheInfo{Name: "bucket"}}, func() {}, getSize) got, err := crawlDataFolder(context.Background(), base, dataUsageCache{Info: dataUsageCacheInfo{Name: "bucket"}}, getSize)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -465,7 +465,7 @@ func TestDataUsageUpdatePrefix(t *testing.T) {
}, },
} }
createUsageTestFiles(t, base, "", files) createUsageTestFiles(t, base, "", files)
got, err = crawlDataFolder(context.Background(), base, got, func() {}, getSize) got, err = crawlDataFolder(context.Background(), base, got, getSize)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -548,7 +548,7 @@ func TestDataUsageUpdatePrefix(t *testing.T) {
} }
// Changed dir must be picked up in this many cycles. // Changed dir must be picked up in this many cycles.
for i := 0; i < dataUsageUpdateDirCycles; i++ { for i := 0; i < dataUsageUpdateDirCycles; i++ {
got, err = crawlDataFolder(context.Background(), base, got, func() {}, getSize) got, err = crawlDataFolder(context.Background(), base, got, getSize)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -652,7 +652,7 @@ func TestDataUsageCacheSerialize(t *testing.T) {
} }
return 0, nil return 0, nil
} }
want, err := crawlDataFolder(context.Background(), base, dataUsageCache{Info: dataUsageCacheInfo{Name: bucket}}, func() {}, getSize) want, err := crawlDataFolder(context.Background(), base, dataUsageCache{Info: dataUsageCacheInfo{Name: bucket}}, getSize)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+143 -66
View File
@@ -19,7 +19,9 @@ package cmd
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/md5"
"crypto/rand" "crypto/rand"
"encoding/base64"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
@@ -70,7 +72,9 @@ type cacheMeta struct {
// Ranges maps cached range to associated filename. // Ranges maps cached range to associated filename.
Ranges map[string]string `json:"ranges,omitempty"` Ranges map[string]string `json:"ranges,omitempty"`
// Hits is a counter on the number of times this object has been accessed so far. // Hits is a counter on the number of times this object has been accessed so far.
Hits int `json:"hits,omitempty"` Hits int `json:"hits,omitempty"`
Bucket string `json:"bucket,omitempty"`
Object string `json:"object,omitempty"`
} }
// RangeInfo has the range, file and range length information for a cached range. // RangeInfo has the range, file and range length information for a cached range.
@@ -130,19 +134,21 @@ type diskCache struct {
online uint32 // ref: https://golang.org/pkg/sync/atomic/#pkg-note-BUG online uint32 // ref: https://golang.org/pkg/sync/atomic/#pkg-note-BUG
purgeRunning int32 purgeRunning int32
triggerGC chan struct{} triggerGC chan struct{}
dir string // caching directory dir string // caching directory
stats CacheDiskStats // disk cache stats for prometheus stats CacheDiskStats // disk cache stats for prometheus
quotaPct int // max usage in % quotaPct int // max usage in %
pool sync.Pool pool sync.Pool
after int // minimum accesses before an object is cached. after int // minimum accesses before an object is cached.
lowWatermark int lowWatermark int
highWatermark int highWatermark int
enableRange bool enableRange bool
commitWriteback bool
retryWritebackCh chan ObjectInfo
// nsMutex namespace lock // nsMutex namespace lock
nsMutex *nsLockMap nsMutex *nsLockMap
// Object functions pointing to the corresponding functions of backend implementation. // Object functions pointing to the corresponding functions of backend implementation.
NewNSLockFn func(ctx context.Context, cachePath string) RWLocker NewNSLockFn func(cachePath string) RWLocker
} }
// Inits the disk cache dir if it is not initialized already. // Inits the disk cache dir if it is not initialized already.
@@ -156,15 +162,17 @@ func newDiskCache(ctx context.Context, dir string, config cache.Config) (*diskCa
return nil, fmt.Errorf("Unable to initialize '%s' dir, %w", dir, err) return nil, fmt.Errorf("Unable to initialize '%s' dir, %w", dir, err)
} }
cache := diskCache{ cache := diskCache{
dir: dir, dir: dir,
triggerGC: make(chan struct{}, 1), triggerGC: make(chan struct{}, 1),
stats: CacheDiskStats{Dir: dir}, stats: CacheDiskStats{Dir: dir},
quotaPct: quotaPct, quotaPct: quotaPct,
after: config.After, after: config.After,
lowWatermark: config.WatermarkLow, lowWatermark: config.WatermarkLow,
highWatermark: config.WatermarkHigh, highWatermark: config.WatermarkHigh,
enableRange: config.Range, enableRange: config.Range,
online: 1, commitWriteback: config.CommitWriteback,
retryWritebackCh: make(chan ObjectInfo, 10000),
online: 1,
pool: sync.Pool{ pool: sync.Pool{
New: func() interface{} { New: func() interface{} {
b := disk.AlignedBlock(int(cacheBlkSize)) b := disk.AlignedBlock(int(cacheBlkSize))
@@ -174,9 +182,12 @@ 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)
if cache.commitWriteback {
go cache.scanCacheWritebackFailures(ctx)
}
cache.diskSpaceAvailable(0) // 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(cachePath string) RWLocker {
return cache.nsMutex.NewNSLock(ctx, nil, cachePath, "") return cache.nsMutex.NewNSLock(nil, cachePath, "")
} }
return &cache, nil return &cache, nil
} }
@@ -194,7 +205,7 @@ func (c *diskCache) diskUsageLow() bool {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return false return false
} }
usedPercent := (di.Total - di.Free) * 100 / di.Total usedPercent := (di.Used / di.Total) * 100
low := int(usedPercent) < gcStopPct low := int(usedPercent) < gcStopPct
atomic.StoreUint64(&c.stats.UsagePercent, usedPercent) atomic.StoreUint64(&c.stats.UsagePercent, usedPercent)
if low { if low {
@@ -218,7 +229,7 @@ func (c *diskCache) diskSpaceAvailable(size int64) bool {
logger.Info("diskCache: Received 0 total disk size") logger.Info("diskCache: Received 0 total disk size")
return false return false
} }
usedPercent := float64(di.Total-di.Free) * 100 / float64(di.Total) usedPercent := float64(di.Used) * 100 / float64(di.Total)
if usedPercent >= float64(gcTriggerPct) { if usedPercent >= float64(gcTriggerPct) {
atomic.StoreInt32(&c.stats.UsageState, 1) atomic.StoreInt32(&c.stats.UsageState, 1)
c.queueGC() c.queueGC()
@@ -226,7 +237,7 @@ func (c *diskCache) diskSpaceAvailable(size int64) bool {
atomic.StoreUint64(&c.stats.UsagePercent, uint64(usedPercent)) atomic.StoreUint64(&c.stats.UsagePercent, uint64(usedPercent))
// Recalculate percentage with provided size added. // Recalculate percentage with provided size added.
usedPercent = float64(di.Total-di.Free+uint64(size)) * 100 / float64(di.Total) usedPercent = float64(di.Used+uint64(size)) * 100 / float64(di.Total)
return usedPercent < float64(c.quotaPct) return usedPercent < float64(c.quotaPct)
} }
@@ -323,6 +334,12 @@ func (c *diskCache) purge(ctx context.Context) {
// stat all cached file ranges and cacheDataFile. // stat all cached file ranges and cacheDataFile.
cachedFiles := fiStatFn(meta.Ranges, cacheDataFile, pathJoin(c.dir, name)) cachedFiles := fiStatFn(meta.Ranges, cacheDataFile, pathJoin(c.dir, name))
objInfo := meta.ToObjectInfo("", "") objInfo := meta.ToObjectInfo("", "")
// prevent gc from clearing un-synced commits. This metadata is present when
// cache writeback commit setting is enabled.
status, ok := objInfo.UserDefined[writeBackStatusHeader]
if ok && status != CommitComplete.String() {
return nil
}
cc := cacheControlOpts(objInfo) cc := cacheControlOpts(objInfo)
for fname, fi := range cachedFiles { for fname, fi := range cachedFiles {
if cc != nil { if cc != nil {
@@ -419,8 +436,8 @@ func (c *diskCache) Stat(ctx context.Context, bucket, object string) (oi ObjectI
// if partial object is cached. // if partial object is cached.
func (c *diskCache) statCachedMeta(ctx context.Context, cacheObjPath string) (meta *cacheMeta, partial bool, numHits int, err error) { func (c *diskCache) statCachedMeta(ctx context.Context, cacheObjPath string) (meta *cacheMeta, partial bool, numHits int, err error) {
cLock := c.NewNSLockFn(ctx, cacheObjPath) cLock := c.NewNSLockFn(cacheObjPath)
if err = cLock.GetRLock(globalOperationTimeout); err != nil { if err = cLock.GetRLock(ctx, globalOperationTimeout); err != nil {
return return
} }
@@ -501,8 +518,8 @@ func (c *diskCache) statCache(ctx context.Context, cacheObjPath string) (meta *c
// incHitsOnly is true if metadata update is incrementing only the hit counter // incHitsOnly is true if metadata update is incrementing only the hit counter
func (c *diskCache) SaveMetadata(ctx context.Context, bucket, object string, meta map[string]string, actualSize int64, rs *HTTPRangeSpec, rsFileName string, incHitsOnly bool) error { func (c *diskCache) SaveMetadata(ctx context.Context, bucket, object string, meta map[string]string, actualSize int64, rs *HTTPRangeSpec, rsFileName string, incHitsOnly bool) error {
cachedPath := getCacheSHADir(c.dir, bucket, object) cachedPath := getCacheSHADir(c.dir, bucket, object)
cLock := c.NewNSLockFn(ctx, cachedPath) cLock := c.NewNSLockFn(cachedPath)
if err := cLock.GetLock(globalOperationTimeout); err != nil { if err := cLock.GetLock(ctx, globalOperationTimeout); err != nil {
return err return err
} }
defer cLock.Unlock() defer cLock.Unlock()
@@ -524,7 +541,11 @@ func (c *diskCache) saveMetadata(ctx context.Context, bucket, object string, met
} }
defer f.Close() defer f.Close()
m := &cacheMeta{Version: cacheMetaVersion} m := &cacheMeta{
Version: cacheMetaVersion,
Bucket: bucket,
Object: object,
}
if err := jsonLoad(f, m); err != nil && err != io.EOF { if err := jsonLoad(f, m); err != nil && err != io.EOF {
return err return err
} }
@@ -538,12 +559,15 @@ func (c *diskCache) saveMetadata(ctx context.Context, bucket, object string, met
} }
m.Ranges[rs.String(actualSize)] = rsFileName m.Ranges[rs.String(actualSize)] = rsFileName
} }
} else { }
if rs == nil && !incHitsOnly {
// this is necessary cleanup of range files if entire object is cached. // this is necessary cleanup of range files if entire object is cached.
for _, f := range m.Ranges { if _, err := os.Stat(pathJoin(cachedPath, cacheDataFile)); err == nil {
removeAll(pathJoin(cachedPath, f)) for _, f := range m.Ranges {
removeAll(pathJoin(cachedPath, f))
}
m.Ranges = nil
} }
m.Ranges = nil
} }
m.Stat.Size = actualSize m.Stat.Size = actualSize
m.Stat.ModTime = UTCNow() m.Stat.ModTime = UTCNow()
@@ -558,7 +582,6 @@ func (c *diskCache) saveMetadata(ctx context.Context, bucket, object string, met
m.Meta["etag"] = etag m.Meta["etag"] = etag
} }
} }
m.Hits++ m.Hits++
m.Checksum = CacheChecksumInfoV1{Algorithm: HighwayHash256S.String(), Blocksize: cacheBlkSize} m.Checksum = CacheChecksumInfoV1{Algorithm: HighwayHash256S.String(), Blocksize: cacheBlkSize}
@@ -570,22 +593,22 @@ func getCacheSHADir(dir, bucket, object string) string {
} }
// Cache data to disk with bitrot checksum added for each block of 1MB // Cache data to disk with bitrot checksum added for each block of 1MB
func (c *diskCache) bitrotWriteToCache(cachePath, fileName string, reader io.Reader, size uint64) (int64, error) { func (c *diskCache) bitrotWriteToCache(cachePath, fileName string, reader io.Reader, size uint64) (int64, string, error) {
if err := os.MkdirAll(cachePath, 0777); err != nil { if err := os.MkdirAll(cachePath, 0777); err != nil {
return 0, err return 0, "", err
} }
filePath := pathJoin(cachePath, fileName) filePath := pathJoin(cachePath, fileName)
if filePath == "" || reader == nil { if filePath == "" || reader == nil {
return 0, errInvalidArgument return 0, "", errInvalidArgument
} }
if err := checkPathLength(filePath); err != nil { if err := checkPathLength(filePath); err != nil {
return 0, err return 0, "", err
} }
f, err := os.Create(filePath) f, err := os.Create(filePath)
if err != nil { if err != nil {
return 0, osErrToFileErr(err) return 0, "", osErrToFileErr(err)
} }
defer f.Close() defer f.Close()
@@ -595,12 +618,12 @@ func (c *diskCache) bitrotWriteToCache(cachePath, fileName string, reader io.Rea
bufp := c.pool.Get().(*[]byte) bufp := c.pool.Get().(*[]byte)
defer c.pool.Put(bufp) defer c.pool.Put(bufp)
md5Hash := md5.New()
var n, n2 int var n, n2 int
for { for {
n, err = io.ReadFull(reader, *bufp) n, err = io.ReadFull(reader, *bufp)
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF { if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
return 0, err return 0, "", err
} }
eof := err == io.EOF || err == io.ErrUnexpectedEOF eof := err == io.EOF || err == io.ErrUnexpectedEOF
if n == 0 && size != 0 { if n == 0 && size != 0 {
@@ -609,21 +632,28 @@ func (c *diskCache) bitrotWriteToCache(cachePath, fileName string, reader io.Rea
} }
h.Reset() h.Reset()
if _, err = h.Write((*bufp)[:n]); err != nil { if _, err = h.Write((*bufp)[:n]); err != nil {
return 0, err return 0, "", err
} }
hashBytes := h.Sum(nil) hashBytes := h.Sum(nil)
// compute md5Hash of original data stream if writeback commit to cache
if c.commitWriteback {
if _, err = md5Hash.Write((*bufp)[:n]); err != nil {
return 0, "", err
}
}
if _, err = f.Write(hashBytes); err != nil { if _, err = f.Write(hashBytes); err != nil {
return 0, err return 0, "", err
} }
if n2, err = f.Write((*bufp)[:n]); err != nil { if n2, err = f.Write((*bufp)[:n]); err != nil {
return 0, err return 0, "", err
} }
bytesWritten += int64(n2) bytesWritten += int64(n2)
if eof { if eof {
break break
} }
} }
return bytesWritten, nil
return bytesWritten, base64.StdEncoding.EncodeToString(md5Hash.Sum(nil)), nil
} }
func newCacheEncryptReader(content io.Reader, bucket, object string, metadata map[string]string) (r io.Reader, err error) { func newCacheEncryptReader(content io.Reader, bucket, object string, metadata map[string]string) (r io.Reader, err error) {
@@ -660,41 +690,41 @@ 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) (oi ObjectInfo, err error) {
if !c.diskSpaceAvailable(size) { if !c.diskSpaceAvailable(size) {
io.Copy(ioutil.Discard, data) io.Copy(ioutil.Discard, data)
return errDiskFull return oi, errDiskFull
} }
cachePath := getCacheSHADir(c.dir, bucket, object) cachePath := getCacheSHADir(c.dir, bucket, object)
cLock := c.NewNSLockFn(ctx, cachePath) cLock := c.NewNSLockFn(cachePath)
if err := cLock.GetLock(globalOperationTimeout); err != nil { if err := cLock.GetLock(ctx, globalOperationTimeout); err != nil {
return err return oi, err
} }
defer cLock.Unlock() defer cLock.Unlock()
meta, _, numHits, err := c.statCache(ctx, cachePath) meta, _, numHits, err := c.statCache(ctx, cachePath)
// Case where object not yet cached // Case where object not yet cached
if os.IsNotExist(err) && c.after >= 1 { if os.IsNotExist(err) && c.after >= 1 {
return c.saveMetadata(ctx, bucket, object, opts.UserDefined, size, nil, "", false) return oi, c.saveMetadata(ctx, bucket, object, opts.UserDefined, size, nil, "", false)
} }
// Case where object already has a cache metadata entry but not yet cached // Case where object already has a cache metadata entry but not yet cached
if err == nil && numHits < c.after { if err == nil && numHits < c.after {
cETag := extractETag(meta.Meta) cETag := extractETag(meta.Meta)
bETag := extractETag(opts.UserDefined) bETag := extractETag(opts.UserDefined)
if cETag == bETag { if cETag == bETag {
return c.saveMetadata(ctx, bucket, object, opts.UserDefined, size, nil, "", false) return oi, c.saveMetadata(ctx, bucket, object, opts.UserDefined, size, nil, "", false)
} }
incHitsOnly = true incHitsOnly = true
} }
if rs != nil { if rs != nil {
return c.putRange(ctx, bucket, object, data, size, rs, opts) return oi, c.putRange(ctx, bucket, object, data, size, rs, opts)
} }
if !c.diskSpaceAvailable(size) { if !c.diskSpaceAvailable(size) {
return errDiskFull return oi, errDiskFull
} }
if err := os.MkdirAll(cachePath, 0777); err != nil { if err := os.MkdirAll(cachePath, 0777); err != nil {
return err return oi, err
} }
var metadata = cloneMSS(opts.UserDefined) var metadata = cloneMSS(opts.UserDefined)
var reader = data var reader = data
@@ -702,25 +732,39 @@ func (c *diskCache) Put(ctx context.Context, bucket, object string, data io.Read
if globalCacheKMS != nil { if globalCacheKMS != nil {
reader, err = newCacheEncryptReader(data, bucket, object, metadata) reader, err = newCacheEncryptReader(data, bucket, object, metadata)
if err != nil { if err != nil {
return err return oi, err
} }
actualSize, _ = sio.EncryptedSize(uint64(size)) actualSize, _ = sio.EncryptedSize(uint64(size))
} }
n, err := c.bitrotWriteToCache(cachePath, cacheDataFile, reader, actualSize) n, md5sum, err := c.bitrotWriteToCache(cachePath, cacheDataFile, reader, actualSize)
if IsErr(err, baseErrs...) { if IsErr(err, baseErrs...) {
// take the cache drive offline // take the cache drive offline
c.setOffline() c.setOffline()
} }
if err != nil { if err != nil {
removeAll(cachePath) removeAll(cachePath)
return err return oi, err
} }
if actualSize != uint64(n) { if actualSize != uint64(n) {
removeAll(cachePath) removeAll(cachePath)
return IncompleteBody{Bucket: bucket, Object: object} return oi, IncompleteBody{Bucket: bucket, Object: object}
} }
return c.saveMetadata(ctx, bucket, object, metadata, n, nil, "", incHitsOnly) if c.commitWriteback {
metadata["content-md5"] = md5sum
if md5bytes, err := base64.StdEncoding.DecodeString(md5sum); err == nil {
metadata["etag"] = hex.EncodeToString(md5bytes)
}
metadata[writeBackStatusHeader] = CommitPending.String()
}
return ObjectInfo{
Bucket: bucket,
Name: object,
ETag: metadata["etag"],
Size: n,
UserDefined: metadata,
},
c.saveMetadata(ctx, bucket, object, metadata, n, nil, "", incHitsOnly)
} }
// Caches the range to disk // Caches the range to disk
@@ -751,7 +795,7 @@ func (c *diskCache) putRange(ctx context.Context, bucket, object string, data io
} }
cacheFile := MustGetUUID() cacheFile := MustGetUUID()
n, err := c.bitrotWriteToCache(cachePath, cacheFile, reader, actualSize) n, _, err := c.bitrotWriteToCache(cachePath, cacheFile, reader, actualSize)
if IsErr(err, baseErrs...) { if IsErr(err, baseErrs...) {
// take the cache drive offline // take the cache drive offline
c.setOffline() c.setOffline()
@@ -866,8 +910,8 @@ func (c *diskCache) bitrotReadFromCache(ctx context.Context, filePath string, of
// Get returns ObjectInfo and reader for object from disk cache // Get returns ObjectInfo and reader for object from disk cache
func (c *diskCache) Get(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions) (gr *GetObjectReader, numHits int, err error) { func (c *diskCache) Get(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions) (gr *GetObjectReader, numHits int, err error) {
cacheObjPath := getCacheSHADir(c.dir, bucket, object) cacheObjPath := getCacheSHADir(c.dir, bucket, object)
cLock := c.NewNSLockFn(ctx, cacheObjPath) cLock := c.NewNSLockFn(cacheObjPath)
if err := cLock.GetRLock(globalOperationTimeout); err != nil { if err := cLock.GetRLock(ctx, globalOperationTimeout); err != nil {
return nil, numHits, err return nil, numHits, err
} }
@@ -930,8 +974,8 @@ func (c *diskCache) Get(ctx context.Context, bucket, object string, rs *HTTPRang
// Deletes the cached object // Deletes the cached object
func (c *diskCache) delete(ctx context.Context, cacheObjPath string) (err error) { func (c *diskCache) delete(ctx context.Context, cacheObjPath string) (err error) {
cLock := c.NewNSLockFn(ctx, cacheObjPath) cLock := c.NewNSLockFn(cacheObjPath)
if err := cLock.GetLock(globalOperationTimeout); err != nil { if err := cLock.GetLock(ctx, globalOperationTimeout); err != nil {
return err return err
} }
defer cLock.Unlock() defer cLock.Unlock()
@@ -951,3 +995,36 @@ func (c *diskCache) Exists(ctx context.Context, bucket, object string) bool {
} }
return true return true
} }
// queues writeback upload failures on server startup
func (c *diskCache) scanCacheWritebackFailures(ctx context.Context) {
defer close(c.retryWritebackCh)
filterFn := func(name string, typ os.FileMode) error {
if name == minioMetaBucket {
// Proceed to next file.
return nil
}
cacheDir := pathJoin(c.dir, name)
meta, _, _, err := c.statCachedMeta(ctx, cacheDir)
if err != nil {
return nil
}
objInfo := meta.ToObjectInfo("", "")
status, ok := objInfo.UserDefined[writeBackStatusHeader]
if !ok || status == CommitComplete.String() {
return nil
}
select {
case c.retryWritebackCh <- objInfo:
default:
}
return nil
}
if err := readDirFilterFn(c.dir, filterFn); err != nil {
logger.LogIf(ctx, err)
return
}
}
+162 -40
View File
@@ -22,24 +22,47 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/minio/minio/cmd/config/cache" "github.com/minio/minio/cmd/config/cache"
xhttp "github.com/minio/minio/cmd/http"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
objectlock "github.com/minio/minio/pkg/bucket/object/lock" objectlock "github.com/minio/minio/pkg/bucket/object/lock"
"github.com/minio/minio/pkg/color" "github.com/minio/minio/pkg/color"
"github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/sync/errgroup" "github.com/minio/minio/pkg/sync/errgroup"
"github.com/minio/minio/pkg/wildcard" "github.com/minio/minio/pkg/wildcard"
) )
const ( const (
cacheBlkSize = int64(1 * 1024 * 1024) cacheBlkSize = 1 << 20
cacheGCInterval = time.Minute * 30 cacheGCInterval = time.Minute * 30
writeBackStatusHeader = ReservedMetadataPrefixLower + "write-back-status"
writeBackRetryHeader = ReservedMetadataPrefixLower + "write-back-retry"
) )
type cacheCommitStatus string
const (
// CommitPending - cache writeback with backend is pending.
CommitPending cacheCommitStatus = "pending"
// CommitComplete - cache writeback completed ok.
CommitComplete cacheCommitStatus = "complete"
// CommitFailed - cache writeback needs a retry.
CommitFailed cacheCommitStatus = "failed"
)
// String returns string representation of status
func (s cacheCommitStatus) String() string {
return string(s)
}
// CacheStorageInfo - represents total, free capacity of // CacheStorageInfo - represents total, free capacity of
// underlying cache storage. // underlying cache storage.
type CacheStorageInfo struct { type CacheStorageInfo struct {
@@ -69,19 +92,22 @@ type cacheObjects struct {
exclude []string exclude []string
// number of accesses after which to cache an object // number of accesses after which to cache an object
after int after int
// commit objects in async manner
commitWriteback bool
// if true migration is in progress from v1 to v2 // if true migration is in progress from v1 to v2
migrating bool migrating bool
// mutex to protect migration bool // mutex to protect migration bool
migMutex sync.Mutex migMutex sync.Mutex
// retry queue for writeback cache mode to reattempt upload to backend
wbRetryCh chan ObjectInfo
// Cache stats // Cache stats
cacheStats *CacheStats cacheStats *CacheStats
GetObjectNInfoFn func(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) InnerGetObjectNInfoFn func(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error)
GetObjectInfoFn func(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) InnerGetObjectInfoFn func(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
DeleteObjectFn func(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) InnerDeleteObjectFn func(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
PutObjectFn func(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) InnerPutObjectFn func(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error)
CopyObjectFn func(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error) InnerCopyObjectFn func(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error)
} }
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 {
@@ -120,7 +146,7 @@ func (c *cacheObjects) updateMetadataIfChanged(ctx context.Context, dcache *disk
// DeleteObject clears cache entry if backend delete operation succeeds // DeleteObject clears cache entry if backend delete operation succeeds
func (c *cacheObjects) DeleteObject(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) { func (c *cacheObjects) DeleteObject(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
if objInfo, err = c.DeleteObjectFn(ctx, bucket, object, opts); err != nil { if objInfo, err = c.InnerDeleteObjectFn(ctx, bucket, object, opts); err != nil {
return return
} }
if c.isCacheExclude(bucket, object) || c.skipCache() { if c.isCacheExclude(bucket, object) || c.skipCache() {
@@ -188,14 +214,14 @@ func (c *cacheObjects) incCacheStats(size int64) {
func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) { func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) {
if c.isCacheExclude(bucket, object) || c.skipCache() { if c.isCacheExclude(bucket, object) || c.skipCache() {
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts) return c.InnerGetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
} }
var cc *cacheControl var cc *cacheControl
var cacheObjSize int64 var cacheObjSize int64
// fetch diskCache if object is currently cached or nearest available cache drive // fetch diskCache if object is currently cached or nearest available cache drive
dcache, err := c.getCacheToLoc(ctx, bucket, object) dcache, err := c.getCacheToLoc(ctx, bucket, object)
if err != nil { if err != nil {
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts) return c.InnerGetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
} }
cacheReader, numCacheHits, cacheErr := dcache.Get(ctx, bucket, object, rs, h, opts) cacheReader, numCacheHits, cacheErr := dcache.Get(ctx, bucket, object, rs, h, opts)
@@ -224,14 +250,14 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
if cc != nil && cc.noStore { if cc != nil && cc.noStore {
cacheReader.Close() cacheReader.Close()
c.cacheStats.incMiss() c.cacheStats.incMiss()
bReader, err := c.GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts) bReader, err := c.InnerGetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
bReader.ObjInfo.CacheLookupStatus = CacheHit bReader.ObjInfo.CacheLookupStatus = CacheHit
bReader.ObjInfo.CacheStatus = CacheMiss bReader.ObjInfo.CacheStatus = CacheMiss
return bReader, err return bReader, err
} }
} }
objInfo, err := c.GetObjectInfoFn(ctx, bucket, object, opts) objInfo, err := c.InnerGetObjectInfoFn(ctx, bucket, object, opts)
if backendDownError(err) && cacheErr == nil { if backendDownError(err) && cacheErr == nil {
c.incCacheStats(cacheObjSize) c.incCacheStats(cacheObjSize)
return cacheReader, nil return cacheReader, nil
@@ -255,7 +281,7 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
cacheReader.Close() cacheReader.Close()
} }
c.cacheStats.incMiss() c.cacheStats.incMiss()
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts) return c.InnerGetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
} }
// skip cache for objects with locks // skip cache for objects with locks
objRetention := objectlock.GetObjectRetentionMeta(objInfo.UserDefined) objRetention := objectlock.GetObjectRetentionMeta(objInfo.UserDefined)
@@ -265,7 +291,7 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
cacheReader.Close() cacheReader.Close()
} }
c.cacheStats.incMiss() c.cacheStats.incMiss()
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts) return c.InnerGetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
} }
if cacheErr == nil { if cacheErr == nil {
// if ETag matches for stale cache entry, serve from cache // if ETag matches for stale cache entry, serve from cache
@@ -283,7 +309,7 @@ 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()
bkReader, bkErr := c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts) bkReader, bkErr := c.InnerGetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
if bkErr != nil { if bkErr != nil {
return bkReader, bkErr return bkReader, bkErr
@@ -305,14 +331,12 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
return bkReader, bkErr return bkReader, bkErr
} }
if rs != nil { if rs != nil && !dcache.enableRange {
go func() { go func() {
// if range caching is disabled, download entire object. // if range caching is disabled, download entire object.
if !dcache.enableRange { rs = nil
rs = nil
}
// fill cache in the background for range GET requests // fill cache in the background for range GET requests
bReader, bErr := c.GetObjectNInfoFn(GlobalContext, bucket, object, rs, h, lockType, opts) bReader, bErr := c.InnerGetObjectNInfoFn(GlobalContext, bucket, object, rs, h, lockType, opts)
if bErr != nil { if bErr != nil {
return return
} }
@@ -335,9 +359,9 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
teeReader := io.TeeReader(bkReader, pipeWriter) teeReader := io.TeeReader(bkReader, pipeWriter)
userDefined := getMetadata(bkReader.ObjInfo) 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, rs, ObjectOptions{
UserDefined: userDefined, 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
@@ -351,7 +375,7 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
// Returns ObjectInfo from cache if available. // Returns ObjectInfo from cache if available.
func (c *cacheObjects) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) { func (c *cacheObjects) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
getObjectInfoFn := c.GetObjectInfoFn getObjectInfoFn := c.InnerGetObjectInfoFn
if c.isCacheExclude(bucket, object) || c.skipCache() { if c.isCacheExclude(bucket, object) || c.skipCache() {
return getObjectInfoFn(ctx, bucket, object, opts) return getObjectInfoFn(ctx, bucket, object, opts)
@@ -409,7 +433,7 @@ func (c *cacheObjects) GetObjectInfo(ctx context.Context, bucket, object string,
// CopyObject reverts to backend after evicting any stale cache entries // CopyObject reverts to backend after evicting any stale cache entries
func (c *cacheObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error) { func (c *cacheObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error) {
copyObjectFn := c.CopyObjectFn copyObjectFn := c.InnerCopyObjectFn
if c.isCacheExclude(srcBucket, srcObject) || c.skipCache() { if c.isCacheExclude(srcBucket, srcObject) || c.skipCache() {
return copyObjectFn(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts) return copyObjectFn(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts)
} }
@@ -596,7 +620,7 @@ func (c *cacheObjects) migrateCacheFromV1toV2(ctx context.Context) {
// PutObject - caches the uploaded object for single Put operations // PutObject - caches the uploaded object for single Put operations
func (c *cacheObjects) PutObject(ctx context.Context, bucket, object string, r *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) { func (c *cacheObjects) PutObject(ctx context.Context, bucket, object string, r *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) {
putObjectFn := c.PutObjectFn putObjectFn := c.InnerPutObjectFn
dcache, err := c.getCacheToLoc(ctx, bucket, object) dcache, err := c.getCacheToLoc(ctx, bucket, object)
if err != nil { if err != nil {
// disk cache could not be located,execute backend call. // disk cache could not be located,execute backend call.
@@ -631,13 +655,20 @@ func (c *cacheObjects) PutObject(ctx context.Context, bucket, object string, r *
dcache.Delete(ctx, bucket, object) dcache.Delete(ctx, bucket, object)
return putObjectFn(ctx, bucket, object, r, opts) return putObjectFn(ctx, bucket, object, r, opts)
} }
if c.commitWriteback {
oi, err := dcache.Put(ctx, bucket, object, r, r.Size(), nil, opts, false)
if err != nil {
return ObjectInfo{}, err
}
go c.uploadObject(GlobalContext, oi)
return oi, nil
}
objInfo, err = putObjectFn(ctx, bucket, object, r, opts) objInfo, err = putObjectFn(ctx, bucket, object, r, opts)
if err == nil { if err == nil {
go func() { go func() {
// fill cache in the background // fill cache in the background
bReader, bErr := c.GetObjectNInfoFn(GlobalContext, bucket, object, nil, http.Header{}, readLock, ObjectOptions{}) bReader, bErr := c.InnerGetObjectNInfoFn(GlobalContext, bucket, object, nil, http.Header{}, readLock, ObjectOptions{})
if bErr != nil { if bErr != nil {
return return
} }
@@ -649,9 +680,68 @@ func (c *cacheObjects) PutObject(ctx context.Context, bucket, object string, r *
} }
}() }()
} }
return objInfo, err return objInfo, err
}
// upload cached object to backend in async commit mode.
func (c *cacheObjects) uploadObject(ctx context.Context, oi ObjectInfo) {
dcache, err := c.getCacheToLoc(ctx, oi.Bucket, oi.Name)
if err != nil {
// disk cache could not be located.
logger.LogIf(ctx, fmt.Errorf("Could not upload %s/%s to backend: %w", oi.Bucket, oi.Name, err))
return
}
cReader, _, bErr := dcache.Get(ctx, oi.Bucket, oi.Name, nil, http.Header{}, ObjectOptions{})
if bErr != nil {
return
}
defer cReader.Close()
if cReader.ObjInfo.ETag != oi.ETag {
return
}
st := cacheCommitStatus(oi.UserDefined[writeBackStatusHeader])
if st == CommitComplete || st.String() == "" {
return
}
hashReader, err := hash.NewReader(cReader, oi.Size, "", "", oi.Size, globalCLIContext.StrictS3Compat)
if err != nil {
return
}
var opts ObjectOptions
opts.UserDefined = make(map[string]string)
opts.UserDefined[xhttp.ContentMD5] = oi.UserDefined["content-md5"]
objInfo, err := c.InnerPutObjectFn(ctx, oi.Bucket, oi.Name, NewPutObjReader(hashReader, nil, nil), opts)
wbCommitStatus := CommitComplete
if err != nil {
wbCommitStatus = CommitFailed
}
meta := cloneMSS(cReader.ObjInfo.UserDefined)
retryCnt := 0
if wbCommitStatus == CommitFailed {
retryCnt, _ = strconv.Atoi(meta[writeBackRetryHeader])
retryCnt++
meta[writeBackRetryHeader] = strconv.Itoa(retryCnt)
} else {
delete(meta, writeBackRetryHeader)
}
meta[writeBackStatusHeader] = wbCommitStatus.String()
meta["etag"] = oi.ETag
dcache.SaveMetadata(ctx, oi.Bucket, oi.Name, meta, objInfo.Size, nil, "", false)
if retryCnt > 0 {
// slow down retries
time.Sleep(time.Second * time.Duration(retryCnt%10+1))
c.queueWritebackRetry(oi)
}
}
func (c *cacheObjects) queueWritebackRetry(oi ObjectInfo) {
select {
case c.wbRetryCh <- oi:
c.uploadObject(GlobalContext, oi)
default:
}
} }
// Returns cacheObjects for use by Server. // Returns cacheObjects for use by Server.
@@ -662,25 +752,26 @@ func newServerCacheObjects(ctx context.Context, config cache.Config) (CacheObjec
return nil, err return nil, err
} }
c := &cacheObjects{ c := &cacheObjects{
cache: cache, cache: cache,
exclude: config.Exclude, exclude: config.Exclude,
after: config.After, after: config.After,
migrating: migrateSw, migrating: migrateSw,
migMutex: sync.Mutex{}, migMutex: sync.Mutex{},
cacheStats: newCacheStats(), commitWriteback: config.CommitWriteback,
GetObjectInfoFn: func(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) { cacheStats: newCacheStats(),
InnerGetObjectInfoFn: func(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
return newObjectLayerFn().GetObjectInfo(ctx, bucket, object, opts) return newObjectLayerFn().GetObjectInfo(ctx, bucket, object, opts)
}, },
GetObjectNInfoFn: func(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) { InnerGetObjectNInfoFn: func(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) {
return newObjectLayerFn().GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts) return newObjectLayerFn().GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts)
}, },
DeleteObjectFn: func(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) { InnerDeleteObjectFn: func(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
return newObjectLayerFn().DeleteObject(ctx, bucket, object, opts) return newObjectLayerFn().DeleteObject(ctx, bucket, object, opts)
}, },
PutObjectFn: func(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) { InnerPutObjectFn: func(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) {
return newObjectLayerFn().PutObject(ctx, bucket, object, data, opts) return newObjectLayerFn().PutObject(ctx, bucket, object, data, opts)
}, },
CopyObjectFn: func(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error) { InnerCopyObjectFn: func(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error) {
return newObjectLayerFn().CopyObject(ctx, srcBucket, srcObject, destBucket, destObject, srcInfo, srcOpts, dstOpts) return newObjectLayerFn().CopyObject(ctx, srcBucket, srcObject, destBucket, destObject, srcInfo, srcOpts, dstOpts)
}, },
} }
@@ -701,6 +792,15 @@ func newServerCacheObjects(ctx context.Context, config cache.Config) (CacheObjec
go c.migrateCacheFromV1toV2(ctx) go c.migrateCacheFromV1toV2(ctx)
} }
go c.gc(ctx) go c.gc(ctx)
if c.commitWriteback {
c.wbRetryCh = make(chan ObjectInfo, 10000)
go func() {
<-GlobalContext.Done()
close(c.wbRetryCh)
}()
go c.queuePendingWriteback(ctx)
}
return c, nil return c, nil
} }
@@ -726,3 +826,25 @@ func (c *cacheObjects) gc(ctx context.Context) {
} }
} }
} }
// queues any pending or failed async commits when server restarts
func (c *cacheObjects) queuePendingWriteback(ctx context.Context) {
for _, dcache := range c.cache {
if dcache != nil {
for {
select {
case <-ctx.Done():
return
case oi, ok := <-dcache.retryWritebackCh:
if !ok {
goto next
}
c.queueWritebackRetry(oi)
default:
time.Sleep(time.Second * 1)
}
}
next:
}
}
}
+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()
+90
View File
@@ -19,10 +19,14 @@ package cmd
import ( import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"encoding/json"
"fmt"
"net/http" "net/http"
"os"
"testing" "testing"
humanize "github.com/dustin/go-humanize" humanize "github.com/dustin/go-humanize"
"github.com/klauspost/compress/zstd"
"github.com/minio/minio-go/v7/pkg/encrypt" "github.com/minio/minio-go/v7/pkg/encrypt"
"github.com/minio/minio/cmd/crypto" "github.com/minio/minio/cmd/crypto"
"github.com/minio/sio" "github.com/minio/sio"
@@ -622,3 +626,89 @@ func TestGetDefaultOpts(t *testing.T) {
} }
} }
} }
func Test_decryptObjectInfo(t *testing.T) {
var testSet []struct {
Bucket string
Name string
UserDef map[string]string
}
file, err := os.Open("testdata/decryptObjectInfo.json.zst")
if err != nil {
t.Fatal(err)
}
defer file.Close()
dec, err := zstd.NewReader(file)
if err != nil {
t.Fatal(err)
}
defer dec.Close()
js := json.NewDecoder(dec)
err = js.Decode(&testSet)
if err != nil {
t.Fatal(err)
}
os.Setenv("MINIO_KMS_MASTER_KEY", "my-minio-key:6368616e676520746869732070617373776f726420746f206120736563726574")
defer os.Setenv("MINIO_KMS_MASTER_KEY", "")
GlobalKMS, err = crypto.NewKMS(crypto.KMSConfig{})
if err != nil {
t.Fatal(err)
}
var dst [32]byte
for i := range testSet {
t.Run(fmt.Sprint("case-", i), func(t *testing.T) {
test := &testSet[i]
_, err := decryptObjectInfo(dst[:], test.Bucket, test.Name, test.UserDef)
if err != nil {
t.Fatal(err)
}
})
}
}
func Benchmark_decryptObjectInfo(b *testing.B) {
var testSet []struct {
Bucket string
Name string
UserDef map[string]string
}
file, err := os.Open("testdata/decryptObjectInfo.json.zst")
if err != nil {
b.Fatal(err)
}
defer file.Close()
dec, err := zstd.NewReader(file)
if err != nil {
b.Fatal(err)
}
defer dec.Close()
js := json.NewDecoder(dec)
err = js.Decode(&testSet)
if err != nil {
b.Fatal(err)
}
os.Setenv("MINIO_KMS_MASTER_KEY", "my-minio-key:6368616e676520746869732070617373776f726420746f206120736563726574")
defer os.Setenv("MINIO_KMS_MASTER_KEY", "")
GlobalKMS, err = crypto.NewKMS(crypto.KMSConfig{})
if err != nil {
b.Fatal(err)
}
b.ReportAllocs()
b.ResetTimer()
b.SetBytes(int64(len(testSet)))
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
var dst [32]byte
for i := range testSet {
test := &testSet[i]
_, err := decryptObjectInfo(dst[:], test.Bucket, test.Name, test.UserDef)
if err != nil {
b.Fatal(err)
}
}
}
})
}
+7 -7
View File
@@ -329,7 +329,7 @@ var (
// CreateServerEndpoints - validates and creates new endpoints from input args, supports // CreateServerEndpoints - validates and creates new endpoints from input args, supports
// both ellipses and without ellipses transparently. // both ellipses and without ellipses transparently.
func createServerEndpoints(serverAddr string, args ...string) ( func createServerEndpoints(serverAddr string, args ...string) (
endpointZones EndpointZones, setupType SetupType, err error) { endpointServerSets EndpointServerSets, setupType SetupType, err error) {
if len(args) == 0 { if len(args) == 0 {
return nil, -1, errInvalidArgument return nil, -1, errInvalidArgument
@@ -352,13 +352,13 @@ func createServerEndpoints(serverAddr string, args ...string) (
if err != nil { if err != nil {
return nil, -1, err return nil, -1, err
} }
endpointZones = append(endpointZones, ZoneEndpoints{ endpointServerSets = append(endpointServerSets, ZoneEndpoints{
SetCount: len(setArgs), SetCount: len(setArgs),
DrivesPerSet: len(setArgs[0]), DrivesPerSet: len(setArgs[0]),
Endpoints: endpointList, Endpoints: endpointList,
}) })
setupType = newSetupType setupType = newSetupType
return endpointZones, setupType, nil return endpointServerSets, setupType, nil
} }
var prevSetupType SetupType var prevSetupType SetupType
@@ -374,12 +374,12 @@ func createServerEndpoints(serverAddr string, args ...string) (
return nil, -1, err return nil, -1, err
} }
if setDriveCount != 0 && setDriveCount != len(setArgs[0]) { if setDriveCount != 0 && setDriveCount != len(setArgs[0]) {
return nil, -1, fmt.Errorf("All zones should have same drive per set ratio - expected %d, got %d", setDriveCount, len(setArgs[0])) return nil, -1, fmt.Errorf("All serverSets should have same drive per set ratio - expected %d, got %d", setDriveCount, len(setArgs[0]))
} }
if prevSetupType != UnknownSetupType && prevSetupType != setupType { if prevSetupType != UnknownSetupType && prevSetupType != setupType {
return nil, -1, fmt.Errorf("All zones should be of the same setup-type to maintain the original SLA expectations - expected %s, got %s", prevSetupType, setupType) return nil, -1, fmt.Errorf("All serverSets should be of the same setup-type to maintain the original SLA expectations - expected %s, got %s", prevSetupType, setupType)
} }
if err = endpointZones.Add(ZoneEndpoints{ if err = endpointServerSets.Add(ZoneEndpoints{
SetCount: len(setArgs), SetCount: len(setArgs),
DrivesPerSet: len(setArgs[0]), DrivesPerSet: len(setArgs[0]),
Endpoints: endpointList, Endpoints: endpointList,
@@ -393,5 +393,5 @@ func createServerEndpoints(serverAddr string, args ...string) (
prevSetupType = setupType prevSetupType = setupType
} }
return endpointZones, setupType, nil return endpointServerSets, setupType, nil
} }
+134 -42
View File
@@ -17,7 +17,9 @@
package cmd package cmd
import ( import (
"context"
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
@@ -26,17 +28,20 @@ import (
"path/filepath" "path/filepath"
"reflect" "reflect"
"runtime" "runtime"
"sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
humanize "github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/minio/cmd/config" "github.com/minio/minio/cmd/config"
xhttp "github.com/minio/minio/cmd/http"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/cmd/rest" "github.com/minio/minio/cmd/rest"
"github.com/minio/minio/pkg/env" "github.com/minio/minio/pkg/env"
"github.com/minio/minio/pkg/mountinfo" "github.com/minio/minio/pkg/mountinfo"
xnet "github.com/minio/minio/pkg/net"
) )
// EndpointType - enum for endpoint type. // EndpointType - enum for endpoint type.
@@ -201,12 +206,12 @@ type ZoneEndpoints struct {
Endpoints Endpoints Endpoints Endpoints
} }
// EndpointZones - list of list of endpoints // EndpointServerSets - list of list of endpoints
type EndpointZones []ZoneEndpoints type EndpointServerSets []ZoneEndpoints
// GetLocalZoneIdx returns the zone which endpoint belongs to locally. // GetLocalZoneIdx returns the zone which endpoint belongs to locally.
// if ep is remote this code will return -1 zoneIndex // if ep is remote this code will return -1 zoneIndex
func (l EndpointZones) GetLocalZoneIdx(ep Endpoint) int { func (l EndpointServerSets) GetLocalZoneIdx(ep Endpoint) int {
for i, zep := range l { for i, zep := range l {
for _, cep := range zep.Endpoints { for _, cep := range zep.Endpoints {
if cep.IsLocal && ep.IsLocal { if cep.IsLocal && ep.IsLocal {
@@ -220,14 +225,14 @@ func (l EndpointZones) GetLocalZoneIdx(ep Endpoint) int {
} }
// Add add zone endpoints // Add add zone endpoints
func (l *EndpointZones) Add(zeps ZoneEndpoints) error { func (l *EndpointServerSets) Add(zeps ZoneEndpoints) error {
existSet := set.NewStringSet() existSet := set.NewStringSet()
for _, zep := range *l { for _, zep := range *l {
for _, ep := range zep.Endpoints { for _, ep := range zep.Endpoints {
existSet.Add(ep.String()) existSet.Add(ep.String())
} }
} }
// Validate if there are duplicate endpoints across zones // Validate if there are duplicate endpoints across serverSets
for _, ep := range zeps.Endpoints { for _, ep := range zeps.Endpoints {
if existSet.Contains(ep.String()) { if existSet.Contains(ep.String()) {
return fmt.Errorf("duplicate endpoints found") return fmt.Errorf("duplicate endpoints found")
@@ -238,17 +243,17 @@ func (l *EndpointZones) Add(zeps ZoneEndpoints) error {
} }
// FirstLocal returns true if the first endpoint is local. // FirstLocal returns true if the first endpoint is local.
func (l EndpointZones) FirstLocal() bool { func (l EndpointServerSets) FirstLocal() bool {
return l[0].Endpoints[0].IsLocal return l[0].Endpoints[0].IsLocal
} }
// HTTPS - returns true if secure for URLEndpointType. // HTTPS - returns true if secure for URLEndpointType.
func (l EndpointZones) HTTPS() bool { func (l EndpointServerSets) HTTPS() bool {
return l[0].Endpoints.HTTPS() return l[0].Endpoints.HTTPS()
} }
// NEndpoints - returns all nodes count // NEndpoints - returns all nodes count
func (l EndpointZones) NEndpoints() (count int) { func (l EndpointServerSets) NEndpoints() (count int) {
for _, ep := range l { for _, ep := range l {
count += len(ep.Endpoints) count += len(ep.Endpoints)
} }
@@ -256,7 +261,7 @@ func (l EndpointZones) NEndpoints() (count int) {
} }
// Hostnames - returns list of unique hostnames // Hostnames - returns list of unique hostnames
func (l EndpointZones) Hostnames() []string { func (l EndpointServerSets) Hostnames() []string {
foundSet := set.NewStringSet() foundSet := set.NewStringSet()
for _, ep := range l { for _, ep := range l {
for _, endpoint := range ep.Endpoints { for _, endpoint := range ep.Endpoints {
@@ -269,6 +274,52 @@ func (l EndpointZones) Hostnames() []string {
return foundSet.ToSlice() return foundSet.ToSlice()
} }
// hostsSorted will return all hosts found.
// The LOCAL host will be nil, but the indexes of all hosts should
// remain consistent across the cluster.
func (l EndpointServerSets) hostsSorted() []*xnet.Host {
peers, localPeer := l.peers()
sort.Strings(peers)
hosts := make([]*xnet.Host, len(peers))
for i, hostStr := range peers {
if hostStr == localPeer {
continue
}
host, err := xnet.ParseHost(hostStr)
if err != nil {
logger.LogIf(GlobalContext, err)
continue
}
hosts[i] = host
}
return hosts
}
// peers will return all peers, including local.
// The local peer is returned as a separate string.
func (l EndpointServerSets) peers() (peers []string, local string) {
allSet := set.NewStringSet()
for _, ep := range l {
for _, endpoint := range ep.Endpoints {
if endpoint.Type() != URLEndpointType {
continue
}
peer := endpoint.Host
if endpoint.IsLocal {
if _, port := mustSplitHostPort(peer); port == globalMinioPort {
local = peer
}
}
allSet.Add(peer)
}
}
return allSet.ToSlice(), local
}
// Endpoints - list of same type of endpoint. // Endpoints - list of same type of endpoint.
type Endpoints []Endpoint type Endpoints []Endpoint
@@ -688,9 +739,9 @@ func CreateEndpoints(serverAddr string, foundLocal bool, args ...[]string) (Endp
// the first element from the set of peers which indicate that // the first element from the set of peers which indicate that
// they are local. There is always one entry that is local // they are local. There is always one entry that is local
// even with repeated server endpoints. // even with repeated server endpoints.
func GetLocalPeer(endpointZones EndpointZones) (localPeer string) { func GetLocalPeer(endpointServerSets EndpointServerSets) (localPeer string) {
peerSet := set.NewStringSet() peerSet := set.NewStringSet()
for _, ep := range endpointZones { for _, ep := range endpointServerSets {
for _, endpoint := range ep.Endpoints { for _, endpoint := range ep.Endpoints {
if endpoint.Type() != URLEndpointType { if endpoint.Type() != URLEndpointType {
continue continue
@@ -712,28 +763,6 @@ func GetLocalPeer(endpointZones EndpointZones) (localPeer string) {
return peerSet.ToSlice()[0] return peerSet.ToSlice()[0]
} }
// GetRemotePeers - get hosts information other than this minio service.
func GetRemotePeers(endpointZones EndpointZones) []string {
peerSet := set.NewStringSet()
for _, ep := range endpointZones {
for _, endpoint := range ep.Endpoints {
if endpoint.Type() != URLEndpointType {
continue
}
peer := endpoint.Host
if endpoint.IsLocal {
if _, port := mustSplitHostPort(peer); port == globalMinioPort {
continue
}
}
peerSet.Add(peer)
}
}
return peerSet.ToSlice()
}
// GetProxyEndpointLocalIndex returns index of the local proxy endpoint // GetProxyEndpointLocalIndex returns index of the local proxy endpoint
func GetProxyEndpointLocalIndex(proxyEps []ProxyEndpoint) int { func GetProxyEndpointLocalIndex(proxyEps []ProxyEndpoint) int {
for i, pep := range proxyEps { for i, pep := range proxyEps {
@@ -744,13 +773,79 @@ func GetProxyEndpointLocalIndex(proxyEps []ProxyEndpoint) int {
return -1 return -1
} }
func httpDo(clnt *http.Client, req *http.Request, f func(*http.Response, error) error) error {
ctx, cancel := context.WithTimeout(GlobalContext, 200*time.Millisecond)
defer cancel()
// Run the HTTP request in a goroutine and pass the response to f.
c := make(chan error, 1)
req = req.WithContext(ctx)
go func() { c <- f(clnt.Do(req)) }()
select {
case <-ctx.Done():
<-c // Wait for f to return.
return ctx.Err()
case err := <-c:
return err
}
}
func getOnlineProxyEndpointIdx() int {
type reqIndex struct {
Request *http.Request
Idx int
}
proxyRequests := make(map[*http.Client]reqIndex, len(globalProxyEndpoints))
for i, proxyEp := range globalProxyEndpoints {
proxyEp := proxyEp
serverURL := &url.URL{
Scheme: proxyEp.Scheme,
Host: proxyEp.Host,
Path: pathJoin(healthCheckPathPrefix, healthCheckLivenessPath),
}
req, err := http.NewRequest(http.MethodGet, serverURL.String(), nil)
if err != nil {
continue
}
proxyRequests[&http.Client{
Transport: proxyEp.Transport,
}] = reqIndex{
Request: req,
Idx: i,
}
}
for c, r := range proxyRequests {
if err := httpDo(c, r.Request, func(resp *http.Response, err error) error {
if err != nil {
return err
}
xhttp.DrainBody(resp.Body)
if resp.StatusCode != http.StatusOK {
return errors.New(resp.Status)
}
if v := resp.Header.Get(xhttp.MinIOServerStatus); v == unavailable {
return errors.New(v)
}
return nil
}); err != nil {
continue
}
return r.Idx
}
return -1
}
// GetProxyEndpoints - get all endpoints that can be used to proxy list request. // GetProxyEndpoints - get all endpoints that can be used to proxy list request.
func GetProxyEndpoints(endpointZones EndpointZones) ([]ProxyEndpoint, error) { func GetProxyEndpoints(endpointServerSets EndpointServerSets) []ProxyEndpoint {
var proxyEps []ProxyEndpoint var proxyEps []ProxyEndpoint
proxyEpSet := set.NewStringSet() proxyEpSet := set.NewStringSet()
for _, ep := range endpointZones { for _, ep := range endpointServerSets {
for _, endpoint := range ep.Endpoints { for _, endpoint := range ep.Endpoints {
if endpoint.Type() != URLEndpointType { if endpoint.Type() != URLEndpointType {
continue continue
@@ -770,11 +865,8 @@ func GetProxyEndpoints(endpointZones EndpointZones) ([]ProxyEndpoint, error) {
} }
} }
tr := newCustomHTTPTransport(tlsConfig, rest.DefaultRESTTimeout)() // allow transport to be HTTP/1.1 for proxying.
// Allow more requests to be in flight with higher response header timeout. tr := newCustomHTTPProxyTransport(tlsConfig, rest.DefaultTimeout)()
tr.ResponseHeaderTimeout = 30 * time.Minute
tr.MaxIdleConns = 64
tr.MaxIdleConnsPerHost = 64
proxyEps = append(proxyEps, ProxyEndpoint{ proxyEps = append(proxyEps, ProxyEndpoint{
Endpoint: endpoint, Endpoint: endpoint,
@@ -782,7 +874,7 @@ func GetProxyEndpoints(endpointZones EndpointZones) ([]ProxyEndpoint, error) {
}) })
} }
} }
return proxyEps, nil return proxyEps
} }
func updateDomainIPs(endPoints set.StringSet) { func updateDomainIPs(endPoints set.StringSet) {
+12 -8
View File
@@ -380,24 +380,28 @@ func TestGetRemotePeers(t *testing.T) {
testCases := []struct { testCases := []struct {
endpointArgs []string endpointArgs []string
expectedResult []string expectedResult []string
expectedLocal string
}{ }{
{[]string{"/d1", "/d2", "d3", "d4"}, []string{}}, {[]string{"/d1", "/d2", "d3", "d4"}, []string{}, ""},
{[]string{"http://localhost:9000/d1", "http://localhost:9000/d2", "http://example.org:9000/d3", "http://example.com:9000/d4"}, []string{"example.com:9000", "example.org:9000"}}, {[]string{"http://localhost:9000/d1", "http://localhost:9000/d2", "http://example.org:9000/d3", "http://example.com:9000/d4"}, []string{"example.com:9000", "example.org:9000", "localhost:9000"}, "localhost:9000"},
{[]string{"http://localhost:9000/d1", "http://localhost:10000/d2", "http://example.org:9000/d3", "http://example.com:9000/d4"}, []string{"example.com:9000", "example.org:9000", "localhost:10000"}}, {[]string{"http://localhost:9000/d1", "http://localhost:10000/d2", "http://example.org:9000/d3", "http://example.com:9000/d4"}, []string{"example.com:9000", "example.org:9000", "localhost:10000", "localhost:9000"}, "localhost:9000"},
{[]string{"http://localhost:9000/d1", "http://example.org:9000/d2", "http://example.com:9000/d3", "http://example.net:9000/d4"}, []string{"example.com:9000", "example.net:9000", "example.org:9000"}}, {[]string{"http://localhost:9000/d1", "http://example.org:9000/d2", "http://example.com:9000/d3", "http://example.net:9000/d4"}, []string{"example.com:9000", "example.net:9000", "example.org:9000", "localhost:9000"}, "localhost:9000"},
{[]string{"http://localhost:9000/d1", "http://localhost:9001/d2", "http://localhost:9002/d3", "http://localhost:9003/d4"}, []string{"localhost:9001", "localhost:9002", "localhost:9003"}}, {[]string{"http://localhost:9000/d1", "http://localhost:9001/d2", "http://localhost:9002/d3", "http://localhost:9003/d4"}, []string{"localhost:9000", "localhost:9001", "localhost:9002", "localhost:9003"}, "localhost:9000"},
} }
for _, testCase := range testCases { for _, testCase := range testCases {
zendpoints := mustGetZoneEndpoints(testCase.endpointArgs...) zendpoints := mustGetZoneEndpoints(testCase.endpointArgs...)
if !zendpoints[0].Endpoints[0].IsLocal { if !zendpoints[0].Endpoints[0].IsLocal {
if err := zendpoints[0].Endpoints.UpdateIsLocal(false); err != nil { if err := zendpoints[0].Endpoints.UpdateIsLocal(false); err != nil {
t.Fatalf("error: expected = <nil>, got = %v", err) t.Errorf("error: expected = <nil>, got = %v", err)
} }
} }
remotePeers := GetRemotePeers(zendpoints) remotePeers, local := zendpoints.peers()
if !reflect.DeepEqual(remotePeers, testCase.expectedResult) { if !reflect.DeepEqual(remotePeers, testCase.expectedResult) {
t.Fatalf("expected: %v, got: %v", testCase.expectedResult, remotePeers) t.Errorf("expected: %v, got: %v", testCase.expectedResult, remotePeers)
}
if local != testCase.expectedLocal {
t.Errorf("expected: %v, got: %v", testCase.expectedLocal, local)
} }
} }
} }
+36 -70
View File
@@ -18,7 +18,7 @@ package cmd
import ( import (
"context" "context"
"sort" "errors"
"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"
@@ -50,7 +50,7 @@ func (er erasureObjects) MakeBucketWithLocation(ctx context.Context, bucket stri
g.Go(func() error { g.Go(func() error {
if storageDisks[index] != nil { if storageDisks[index] != nil {
if err := storageDisks[index].MakeVol(ctx, bucket); err != nil { if err := storageDisks[index].MakeVol(ctx, bucket); err != nil {
if err != errVolumeExists { if !errors.Is(err, errVolumeExists) {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
} }
return err return err
@@ -86,31 +86,40 @@ func undoDeleteBucket(storageDisks []StorageAPI, bucket string) {
// getBucketInfo - returns the BucketInfo from one of the load balanced disks. // getBucketInfo - returns the BucketInfo from one of the load balanced disks.
func (er erasureObjects) getBucketInfo(ctx context.Context, bucketName string) (bucketInfo BucketInfo, err error) { func (er erasureObjects) getBucketInfo(ctx context.Context, bucketName string) (bucketInfo BucketInfo, err error) {
var bucketErrs []error storageDisks := er.getDisks()
for _, disk := range er.getLoadBalancedDisks() {
if disk == nil { g := errgroup.WithNErrs(len(storageDisks))
bucketErrs = append(bucketErrs, errDiskNotFound) var bucketsInfo = make([]BucketInfo, len(storageDisks))
continue // Undo previous make bucket entry on all underlying storage disks.
} for index := range storageDisks {
volInfo, serr := disk.StatVol(ctx, bucketName) index := index
if serr == nil { g.Go(func() error {
return BucketInfo(volInfo), nil if storageDisks[index] == nil {
} return errDiskNotFound
err = serr }
// For any reason disk went offline continue and pick the next one. volInfo, err := storageDisks[index].StatVol(ctx, bucketName)
if IsErrIgnored(err, bucketMetadataOpIgnoredErrs...) { if err != nil {
bucketErrs = append(bucketErrs, err) return err
continue }
} bucketsInfo[index] = BucketInfo(volInfo)
// Any error which cannot be ignored, we return quickly. return nil
return BucketInfo{}, err }, index)
} }
errs := g.Wait()
for i, err := range errs {
if err == nil {
return bucketsInfo[i], nil
}
}
// If all our errors were ignored, then we try to // If all our errors were ignored, then we try to
// reduce to one error based on read quorum. // reduce to one error based on read quorum.
// `nil` is deliberately passed for ignoredErrs // `nil` is deliberately passed for ignoredErrs
// because these errors were already ignored. // because these errors were already ignored.
readQuorum := getReadQuorum(len(er.getDisks())) readQuorum := getReadQuorum(len(storageDisks))
return BucketInfo{}, reduceReadQuorumErrs(ctx, bucketErrs, nil, readQuorum) return BucketInfo{}, reduceReadQuorumErrs(ctx, errs, nil, readQuorum)
} }
// GetBucketInfo - returns BucketInfo for a bucket. // GetBucketInfo - returns BucketInfo for a bucket.
@@ -122,53 +131,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(ctx)
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
@@ -196,6 +158,7 @@ func deleteDanglingBucket(ctx context.Context, storageDisks []StorageAPI, dErrs
// DeleteBucket - deletes a bucket. // DeleteBucket - deletes a bucket.
func (er erasureObjects) DeleteBucket(ctx context.Context, bucket string, forceDelete bool) error { func (er erasureObjects) DeleteBucket(ctx context.Context, bucket string, forceDelete bool) error {
// Collect if all disks report volume not found. // Collect if all disks report volume not found.
defer ObjectPathUpdated(bucket + slashSeparator)
storageDisks := er.getDisks() storageDisks := er.getDisks()
g := errgroup.WithNErrs(len(storageDisks)) g := errgroup.WithNErrs(len(storageDisks))
@@ -243,7 +206,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
} }
+86 -5
View File
@@ -19,6 +19,7 @@ package cmd
import ( import (
"context" "context"
"path" "path"
"sync"
"github.com/minio/minio/pkg/sync/errgroup" "github.com/minio/minio/pkg/sync/errgroup"
) )
@@ -28,17 +29,52 @@ func (er erasureObjects) getLoadBalancedLocalDisks() (newDisks []StorageAPI) {
// Based on the random shuffling return back randomized disks. // Based on the random shuffling return back randomized disks.
for _, i := range hashOrder(UTCNow().String(), len(disks)) { for _, i := range hashOrder(UTCNow().String(), len(disks)) {
if disks[i-1] != nil && disks[i-1].IsLocal() { if disks[i-1] != nil && disks[i-1].IsLocal() {
newDisks = append(newDisks, disks[i-1]) if !disks[i-1].Healing() && disks[i-1].IsOnline() {
newDisks = append(newDisks, disks[i-1])
}
} }
} }
return newDisks return newDisks
} }
func (er erasureObjects) getOnlineDisks() (newDisks []StorageAPI) {
disks := er.getDisks()
var wg sync.WaitGroup
var mu sync.Mutex
for _, i := range hashOrder(UTCNow().String(), len(disks)) {
i := i
wg.Add(1)
go func() {
defer wg.Done()
if disks[i-1] == nil {
return
}
di, err := disks[i-1].DiskInfo(context.Background())
if err != nil || di.Healing {
// - Do not consume disks which are not reachable
// unformatted or simply not accessible for some reason.
//
// - Do not consume disks which are being healed
//
// - Future: skip busy disks
return
}
mu.Lock()
newDisks = append(newDisks, disks[i-1])
mu.Unlock()
}()
}
wg.Wait()
return newDisks
}
// getLoadBalancedNDisks - fetches load balanced (sufficiently randomized) disk slice // getLoadBalancedNDisks - fetches load balanced (sufficiently randomized) disk slice
// with N disks online. If ndisks is zero or negative, then it will returns all disks, // 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. // same if ndisks is greater than the number of all disks.
func (er erasureObjects) getLoadBalancedNDisks(ndisks int) (newDisks []StorageAPI) { func (er erasureObjects) getLoadBalancedNDisks(ndisks int) (newDisks []StorageAPI) {
disks := er.getLoadBalancedDisks() disks := er.getLoadBalancedDisks(ndisks != -1)
for _, disk := range disks { for _, disk := range disks {
if disk == nil { if disk == nil {
continue continue
@@ -53,13 +89,58 @@ func (er erasureObjects) getLoadBalancedNDisks(ndisks int) (newDisks []StorageAP
} }
// getLoadBalancedDisks - fetches load balanced (sufficiently randomized) disk slice. // getLoadBalancedDisks - fetches load balanced (sufficiently randomized) disk slice.
func (er erasureObjects) getLoadBalancedDisks() (newDisks []StorageAPI) { // ensures to skip disks if they are not healing and online.
func (er erasureObjects) getLoadBalancedDisks(optimized bool) []StorageAPI {
disks := er.getDisks() disks := er.getDisks()
if !optimized {
var newDisks []StorageAPI
for _, i := range hashOrder(UTCNow().String(), len(disks)) {
newDisks = append(newDisks, disks[i-1])
}
return newDisks
}
var wg sync.WaitGroup
var mu sync.Mutex
var newDisks = map[uint64][]StorageAPI{}
// Based on the random shuffling return back randomized disks. // Based on the random shuffling return back randomized disks.
for _, i := range hashOrder(UTCNow().String(), len(disks)) { for _, i := range hashOrder(UTCNow().String(), len(disks)) {
newDisks = append(newDisks, disks[i-1]) i := i
wg.Add(1)
go func() {
defer wg.Done()
if disks[i-1] == nil {
return
}
di, err := disks[i-1].DiskInfo(context.Background())
if err != nil || di.Healing {
// - Do not consume disks which are not reachable
// unformatted or simply not accessible for some reason.
//
// - Do not consume disks which are being healed
//
// - Future: skip busy disks
return
}
mu.Lock()
// Capture disks usage wise upto resolution of MiB
newDisks[di.Used/1024/1024] = append(newDisks[di.Used/1024/1024], disks[i-1])
mu.Unlock()
}()
} }
return newDisks wg.Wait()
var max uint64
for k := range newDisks {
if k > max {
max = k
}
}
// Return disks which have maximum disk usage common.
return newDisks[max]
} }
// This function does the following check, suppose // This function does the following check, suppose
+2 -2
View File
@@ -55,8 +55,8 @@ func TestErasureParentDirIsObject(t *testing.T) {
t.Fatalf("Unexpected object name returned got %s, expected %s", objInfo.Name, objectName) t.Fatalf("Unexpected object name returned got %s, expected %s", objInfo.Name, objectName)
} }
z := obj.(*erasureZones) z := obj.(*erasureServerSets)
xl := z.zones[0].sets[0] xl := z.serverSets[0].sets[0]
testCases := []struct { testCases := []struct {
parentIsObject bool parentIsObject bool
objectName string objectName string

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