Compare commits

..

103 Commits

Author SHA1 Message Date
Praveen raj Mani c27d0583d4 Send kafka notification messages in batches when queue_dir is enabled (#18164)
Fixes #18124
2023-10-07 08:07:38 -07:00
Klaus Post 0de2b9a1b2 Fix panic on double unfreezeServices (#18177)
Calling unfreezeServices twice results in panic:

```
panic: "POST /minio/peer/v32/signalservice?signal=4&sub-sys=": close of nil channel
goroutine 14703 [running]:
runtime/debug.Stack()
	runtime/debug/stack.go:24 +0x65
github.com/minio/minio/cmd.setCriticalErrorHandler.func1.1()
	github.com/minio/minio/cmd/generic-handlers.go:549 +0x8e
panic({0x27c3020, 0x4c9b370})
	runtime/panic.go:884 +0x212
github.com/minio/minio/cmd.unfreezeServices()
	github.com/minio/minio/cmd/service.go:112 +0xc7
github.com/minio/minio/cmd.(*peerRESTServer).SignalServiceHandler(0x0?, {0x4cb6af0, 0xc010b96420}, 0xc01affab00)
	github.com/minio/minio/cmd/peer-rest-server.go:837 +0x13a
net/http.HandlerFunc.ServeHTTP(...)
```

If the function was called a second time `val` would not be nil, but the returned channel `ch` would be, causing the panic.

Check the channel isn't nil and also use Swap for an atomic swap instead of 2 separate operations (though we are in a mutex).
2023-10-06 07:51:50 -06:00
Poorna 9dc29d7687 Avoid ILM expiry on deleted versions that are yet to replicate (#18175)
Fixes #18167
2023-10-06 06:55:15 -06:00
Poorna 72871dbb9a delete replication: avoid overwriting replication decision (#18174)
from ObjectInfo unless version purge status is present. Otherwise
there is potential to make incorrect replication decision if Stat
returned an error
2023-10-05 21:09:45 -06:00
Aditya Manthramurthy 4bda4e4e2b fix: check for disk-level O_DIRECT support (#18173)
Disk level O_DIRECT support checking at xl storage initialization was
conditional on a config setting being enabled. (This never took effect
because config initialization happens after ObjectLayer is ready.) This
is not necessary as the config setting is dynamic - O_DIRECT should be
enabled via runtime config. So we need to do the disk level support
check regardless of the config setting.
2023-10-05 20:54:49 -06:00
Harshavardhana 1971c54a50 update buffer channels for both trace and listen events (#18171)
- Trace needs higher buffered channels than 4000 to ensure
  when we run `mc admin trace -a` it captures all information
  sufficiently.

- Listen event notification needs the event channel to be
  `apiRequestsMaxPerNode` * number of nodes
2023-10-05 18:16:04 -06:00
Cesar N bb77b89da0 Update MinIO Console version (#18168)
Co-authored-by: cesnietor <>
2023-10-04 16:25:59 -07:00
Anis Eleuch b336e9a79f fix: loading usage cache to not fail early when reading the backup fails (#18158)
Currently, the retry is not fully used when there is no backup copy of
the data usage; use 5 retry attempts when we don't have any valid data, 
new or backup, unless we have seen an un-recognized error.
2023-10-02 19:22:35 -07:00
Harshavardhana a2ab21e91c add max-keys=2 optimization for spark workloads (#18154)
comment in the code provides more detailed explanation
on what this PR entails and its assumptions.

this PR reduces the amount of listing() by an order
of magnitude, however there are other such calls that
still needs further optimization that shall be done
in subsequent PRs.
2023-10-02 07:52:59 -06:00
Sveinn 603437e70f Fix startup formatting (#18156)
Percentages in root user names are used for formatting.

Before:
```
S3-API: http://192.168.50.21:9000  http://172.31.96.1:9000  http://127.0.0.1:9000
RootUser: "U4B6Zi!b75DXSPm%!!(MISSING)a(MISSING)vZb"
RootPass: "Q4#Q6y8G%!P(MISSING)x#npP4dudUobU#NBcGB7RMKV4ajYb"

Console: http://192.168.50.21:51915 http://172.31.96.1:51915 http://127.0.0.1:51915
RootUser: "U4B6Zi!b75DXSPm%!!(MISSING)a(MISSING)vZb"
RootPass: "Q4#Q6y8G%!P(MISSING)x#npP4dudUobU#NBcGB7RMKV4ajYb"

Command-line: https://min.io/docs/minio/linux/reference/minio-mc.html#quickstart
FORMAT: %117s MESSAGE: $ mc alias set myminio http://192.168.50.21:9000 "U4B6Zi!b75DXSPm%avZb" "Q4#Q6y8G%%Px#npP4dudUobU#NBcGB7RMKV4ajYb"
   $ mc alias set myminio http://192.168.50.21:9000 "U4B6Zi!b75DXSPm%!a(MISSING)vZb" "Q4#Q6y8G%Px#npP4dudUobU#NBcGB7RMKV4ajYb"
```

After:

```
Status:         1 Online, 0 Offline.
S3-API: http://192.168.50.21:9000  http://172.31.96.1:9000  http://127.0.0.1:9000
RootUser: "U4B6Zi!b75DXSPm%avZb"
RootPass: "Q4#Q6y8G%%Px#npP4dudUobU#NBcGB7RMKV4ajYb"

Console: http://192.168.50.21:52421 http://172.31.96.1:52421 http://127.0.0.1:52421
RootUser: "U4B6Zi!b75DXSPm%avZb"
RootPass: "Q4#Q6y8G%%Px#npP4dudUobU#NBcGB7RMKV4ajYb"

Command-line: https://min.io/docs/minio/linux/reference/minio-mc.html#quickstart
   $ mc alias set myminio http://192.168.50.21:9000 "U4B6Zi!b75DXSPm%avZb" "Q4#Q6y8G%%Px#npP4dudUobU#NBcGB7RMKV4ajYb"
```

No need for special Windows case. `mc` works just fine.
2023-10-02 07:39:47 -06:00
Harshavardhana db3a9a5990 update missing mc command on multipart-tests 2023-09-30 20:29:45 -07:00
Harshavardhana 24c7e73b4e update helm chart images and release v5.0.14
Signed-off-by: Harshavardhana <harsha@minio.io>
2023-09-30 13:46:10 -07:00
Alik c053e57068 Add paramaters in Helm chart to load OIDC clientSecret from Secret Resource (#17784) 2023-09-30 13:44:38 -07:00
Shireesh Anjal 6d20ec3bea Add support for resource metrics (#18057)
Add a new endpoint for "resource" metrics `/v2/metrics/resource`

This should return system metrics related to drives, network, CPU and
memory. Except for drives, other metrics should have corresponding "avg"
and "max" values also.

Reuse the real-time feature to capture the required data,
introducing CPU and memory metrics in it.

Collect the data every minute and keep updating the average and max values
accordingly, returning the latest values when the API is called.
2023-09-30 13:40:20 -07:00
Harshavardhana c50627ee3e Add tests for multipart upload overwrites on versioned buckets (#18142) 2023-09-30 03:13:56 -07:00
Minio Trusted b3cd893f93 Update yaml files to latest version RELEASE.2023-09-30T07-02-29Z 2023-09-30 07:51:58 +00:00
Anis Eleuch 22d2dbc4e6 decom: Fix infinite retry when the decom is canceled (#18143)
Also, use rand.Float64() since it is thread-safe; otherwise go race
will complain.
2023-09-30 00:02:29 -07:00
Shireesh Anjal 2b5d9428b1 Use latest madmin-go (v3.0.21) (#18138)
This ensures that drive model is included in the partition data inside
the health diagnostics report.
2023-09-29 11:25:34 -07:00
Harshavardhana d6446cb096 do not return an error in AbortMultipartUpload() (#18135)
returning an error is a bit undefined in AWS S3
as it may return an error or not depending on the
time from AbortMultipartUpload().
2023-09-29 10:28:19 -07:00
Harshavardhana c34bdc33fb make sure to set Versioned field to ensure rename2 is not called (#18141)
without this the rename2() can rename the previous dataDir
causing issues for different versions of the object, only
latest version is preserved due to this bug.

Added healing code to ensure recovery of such content.
2023-09-29 09:08:24 -07:00
ferhat elmas dd8547e51c chore: drop unnecessary linter (#18133) 2023-09-29 03:11:31 -07:00
Anis Eleuch aec023f537 Avoid showing buckets without quorum in each pool (#18125) 2023-09-29 00:58:54 -07:00
Poorna e101eeeda9 fix: tier addition validation (#18136) 2023-09-28 22:33:24 -07:00
Minio Trusted f29522269d Update yaml files to latest version RELEASE.2023-09-27T15-22-50Z 2023-09-28 17:49:57 +00:00
Harshavardhana 3c470a6b8b fix: the inspect script to use scheme per deployment (#18118) 2023-09-27 08:22:50 -07:00
Poorna 6bc7d711b3 delete of a missing versionId return 204 (#18117) 2023-09-26 14:02:56 -07:00
Shubhendu 10d5dd3a67 fix: a regression with audit log sending (#18112)
Signed-off-by: Shubhendu Ram Tripathi <shubhendu@minio.io>
2023-09-26 12:23:02 -07:00
Harshavardhana d9f1df01eb return an error in CopyAligned upon premature EOF (#18110)
add a unit-test to capture this corner case
2023-09-26 11:20:06 -07:00
Harshavardhana cdeab19673 fix: always check error upon w.Close() in Write() (#18111)
not checking w.Close() can prematurely make us
think that the w.Write() actually succeeded, apparently
Write() may or may not return an error but sometimes
only during a Close() call to the fd we may see the
error from Write() propagate.

Fdatasync(w) on the FD would return an error requiring
Close() error handling is less of a concern, however it may
happen such that fdatasync() did not return an error, where
as Close() would.
2023-09-26 11:04:00 -07:00
Anis Eleuch 22ee678136 tier: Avoid doing versioned operations since not required anymore (#18108)
Currently, setting a new tiering target returns an error when a bucket
is versioned and the tiering credentials does not have authorization to
specify a version-id when reading or removing a specific version;

Since tiering does not require versioning anymore; avoid doing versioned
operations when performing checklist ops while adding a new tiering
configuration.
2023-09-26 00:14:56 -07:00
Poorna 50a8f13e85 site replication: allow setting bandwidth default for bucket (#18062)
This can still be overridden at the bucket level
2023-09-25 15:50:52 -07:00
jiuker 6dec60b6e6 fix: check post policy like AWS S3 (#18074) 2023-09-25 12:35:25 -07:00
Harshavardhana ac3a19138a fix: set scanning details locally to avoid cached values (#18092)
atomic variable results such as scanning must not use
cached values, instead rely on real-time information.
2023-09-25 08:26:29 -07:00
Klaus Post 21e8e071d7 Improve ListObject Compatibility (#18099)
Do not error out when a provided marker is before or after the prefix, but instead just ignore it if before and return an empty list when after.

Fixes #18093
2023-09-25 08:13:08 -07:00
Klaus Post 57f84a8b4c Add abandoned folder scanning to metrics (#18076)
Include object and versions heal scan times when checking non-empty abandoned folders.

Furthermore don't add delay between healing versions, instead do one per object wait.
2023-09-24 22:15:31 -07:00
Minio Trusted 8a672e70a7 Update yaml files to latest version RELEASE.2023-09-23T03-47-50Z 2023-09-25 01:55:31 +00:00
Aditya Manthramurthy 22041bbcc4 fix: Update policy mapping properly in notification (#18088)
This is fixing a regression from an earlier change where STS account
loading was made lazy.
2023-09-22 20:47:50 -07:00
Harshavardhana 5afb459113 upgrade all dependencies (#18085) 2023-09-22 14:45:19 -07:00
Harshavardhana 91ebac0a00 fix: move abandoned parts check after healing not in ILM path (#18087) 2023-09-22 12:07:52 -07:00
mundry 5fcb1cfd31 fix: broken bucket versioning support in community helm chart (#18003) 2023-09-22 11:32:24 -07:00
Harshavardhana 3a90fb108c only look for metadata if batch replication asks for metadata filters (#18082)
This PR changes the StatObject() to be must have for non-minio source
to being a conditional API call.

- Calls StatObject() when needed
- Calls GetObjectTagging() when needed

These calls if we do without these conditionals can cause a lot of
delays, so we avoid them if not needed in more common scenario.
2023-09-22 11:31:57 -07:00
Anis Eleuch 4eeb48f8e0 Return cached online/offline status for audit/http loggers (#18083)
To avoid having delays in prometheus scrape and in 'mc admin info' command.
2023-09-21 16:58:24 -07:00
Harshavardhana 373d48c8a3 allow admin actions to have proper condition map (#18080)
upgrade minio/pkg to v2.0.2

fixes #18078
2023-09-21 13:22:09 -07:00
Harshavardhana 1472875670 fix: failed messages counting in audit_http metrics (#18075)
all retries must not be counted as failed messages,
a failed message is a single counter not for all
retries, this PR fixes this.

Also we do not need to retry 10-times, instead we should
retry at max 3 times with some jitter to deliver the
messages.
2023-09-21 11:24:56 -07:00
Shubhendu 74cfb207c1 Added check for mandatory MINIO_KMS_KES_KEY_NAME env var (#18077)
If MinIO started with KMS enabled, MINIO_KMS_KES_KEY_NAME should
be set for server to start.

Signed-off-by: Shubhendu Ram Tripathi <shubhendu@minio.io>
2023-09-21 10:37:37 -07:00
Minio Trusted 6a096e7dc7 Update yaml files to latest version RELEASE.2023-09-20T22-49-55Z 2023-09-21 00:42:16 +00:00
Harshavardhana 9788d85ea3 remove logging for invalid metadata values (#18068) 2023-09-20 15:49:55 -07:00
Anis Eleuch 69c0e18685 perf net: Add the endpoint name related to the perf net error (#18063)
In a perf test, one node will run speed test with all nodes. If there is
an error with a peer node, the peer node name is not included in the
error hence confusing the user.

This commit will add the peer endpoint string to the netperf error.
2023-09-19 22:41:06 -07:00
Aditya Manthramurthy 3cac927348 Load STS policy mappings periodically (#18061)
To ensure that policy mappings are current for service accounts
belonging to (non-derived) STS accounts (like an LDAP user's service
account) we periodically reload such mappings.

This is primarily to handle a case where a policy mapping update
notification is missed by a minio node. Such a node would continue to
have the stale mapping in memory because STS creds/mappings were never
periodically scanned from storage.
2023-09-19 17:57:42 -07:00
Harshavardhana 9081346c40 fix: more regressions listing policy mappings (#18060)
also relax ListServiceAccounts() returning error if
no service accounts exist.
2023-09-19 15:23:18 -07:00
Harshavardhana fcfadb0e51 fix: regression in loading LDAP users policy mappings (#18055)
LDAP users are stored as STS users, we need to load
their policy mappings appropriately.

Fixes a regression caused by #17994
2023-09-19 10:31:56 -07:00
Harshavardhana 2add57cfed apply healing per object at 1024 cycles (#18050)
- we already have MRF for most recent failures
- we trigger healing during HEAD/GET operation

These are enough, also change the default max wait
from 5sec to 1sec for default scanner speed.
2023-09-19 09:24:22 -07:00
Anis Eleuch c5279ec630 fix: building reorder-disks under darwin (#18053)
Also build debugging tools only in tests or with a specific target
2023-09-19 03:19:26 -07:00
Poorna b73699fad8 replication: pass user tags while queueing (#18052)
Continues from #18032 - otherwise replication will fail on tag based rules.
2023-09-19 03:18:28 -07:00
Harshavardhana b8ebe54e53 Revert "skip tiered objects to GLACIER in batch replication (#18044)"
This reverts commit fd421ddd6f.

MinIO already provides `filter` based on metadata that would work
in this scenario already.
2023-09-19 00:05:40 -07:00
Harshavardhana c3d70e0795 cache usage, prefix-usage, and buckets for AccountInfo up to 10 secs (#18051)
AccountInfo is quite frequently called by the Console UI 
login attempts, when many users are logging in it is important
that we provide them with better responsiveness.

- ListBuckets information is cached every second
- Bucket usage info is cached for up to 10 seconds
- Prefix usage (optional) info is cached for up to 10 secs

Failure to update after cache expiration, would still
allow login which would end up providing information
previously cached.

This allows for seamless responsiveness for the Console UI
logins, and overall responsiveness on a heavily loaded
system.
2023-09-18 22:13:03 -07:00
Harshavardhana 8c4561b8da add all missing go.mod for debugging tools (#18049) 2023-09-18 13:47:03 -07:00
Harshavardhana fd421ddd6f skip tiered objects to GLACIER in batch replication (#18044)
tiered objects to GLACIER are not readable until
they are restored, we skip these as unreadable
2023-09-18 10:25:31 -07:00
jiuker 9947c01c8e feat: SSE-KMS use uuid instead of read all data to md5. (#17958) 2023-09-18 10:00:54 -07:00
Eng Zer Jun a00db4267c data-usage-cache: remove redundant nil check (#17970)
From the Go specification:

  "3. If the map is nil, the number of iterations is 0." [1]

Therefore, an additional nil check for before the loop is unnecessary.

[1]: https://go.dev/ref/spec#For_range

Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>
2023-09-16 19:09:29 -07:00
Harshavardhana 36385010f5 use optimized pathJoin instead of path.Join (#18042)
this avoids allocations in scanner routine, they are tiny but 
they allocate a lot over many cycles of the scanner.
2023-09-16 19:08:59 -07:00
Harshavardhana fa6d082bfd reduce all major allocations in replication path (#18032)
- remove targetClient for passing around via replicationObjectInfo{}
- remove cloing to object info unnecessarily
- remove objectInfo from replicationObjectInfo{} (only require necessary fields)
2023-09-16 02:28:06 -07:00
Minio Trusted 9fab91852a Update yaml files to latest version RELEASE.2023-09-16T01-01-47Z 2023-09-16 07:38:18 +00:00
Poorna b733e6e83c site replication turn off retry login for admin API calls (#18039)
additionally also mark site offline if n/w is down
2023-09-15 18:01:47 -07:00
Harshavardhana ce05bb69dc update console v0.39.0 (#18038)
Signed-off-by: Harshavardhana <harsha@minio.io>
2023-09-15 14:01:52 -07:00
Anis Eleuch 37aa5934a1 scanner: Fix loading data usage cache structure (#18037)
Return an empty data usage cache structure when the data usage cache
file does not exist, otherwise, the scanner won't work.
2023-09-15 13:11:08 -07:00
Harshavardhana 1647fc7edc fix: optimize listMultipartUploads to serve via local disks (#18034)
and remove unused getLoadBalancedDisks()
2023-09-15 08:34:03 -07:00
Harshavardhana 7b92687397 remove generating presignedURLs with range header for lambda (#18033) 2023-09-14 21:58:17 -07:00
Anis Eleuch 419e5baf16 fix: webhook notify endpoint with standard ports (#18016) 2023-09-14 20:10:44 -07:00
Alex dc48cd841a Added MINIO_PROMETHEUS_AUTH_TOKEN env support (#18028)
Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
2023-09-14 17:28:21 -07:00
Anis Eleuch b0e1776d6d Do not use a chain for S3 tiering to return better error messages (#18030)
When using a chain provider all providers do not return a valid
access and secret key, an anonymous request is sent, which makes it hard
for users to figure out what is going on

In the case of S3 tiering, when AWS IAM temporary account generation returns
an error, an anonymous login will be used because of the chain provider.
Avoid this and use the AWS IAM provider directly to get a good error
message.
2023-09-14 15:28:20 -07:00
Aditya Manthramurthy 7a7068ee47 Move IAM periodic ops to a single go routine (#18026)
This helps reduce disk operations as these periodic routines would not
run concurrently any more.

Also add expired STS purging periodic operation: Since we do not scan
the on-disk STS credentials (and instead only load them on-demand) a
separate routine is needed to purge expired credentials from storage.
Currently this runs about a quarter as often as IAM refresh.

Also fix a bug where with etcd, STS accounts could get loaded into the
iamUsersMap instead of the iamSTSAccountsMap.
2023-09-14 15:25:17 -07:00
Aditya Manthramurthy cbc0ef459b Fix policy package import name (#18031)
We do not need to rename the import of minio/pkg/v2/policy as iampolicy
any more.
2023-09-14 14:50:16 -07:00
Harshavardhana a2aabfabd9 add backups for usage-caches to rely on upon error (#18029)
This allows scanner to avoid lengthy scans, skip
things appropriately and also not lose metrics in
any manner.

reduce longer deadlines for usage-cache loads/saves
to match the disk timeout which is 2minutes now per
IOP.
2023-09-14 11:53:52 -07:00
Harshavardhana 822cbd4b43 add couple of missing things from #18027 2023-09-13 23:26:48 -07:00
Ravind Kumar 3c19a9308d DOCS-987: Reorganizing list.md for better RST compatibility (#18027) 2023-09-13 23:23:37 -07:00
Harshavardhana 32890342ce introduce MINIO_BROWSER_REDIRECT env to enable/disable auto-redirect (#18025) 2023-09-13 18:43:57 -07:00
Aditya Manthramurthy ed2c2a285f Load STS accounts into IAM cache lazily (#17994)
In situations with large number of STS credentials on disk, IAM load
time is high. To mitigate this, STS accounts will now be loaded into
memory only on demand - i.e. when the credential is used.

In each IAM cache (re)load we skip loading STS credentials and STS
policy mappings into memory. Since STS accounts only expire and cannot
be deleted, there is no risk of invalid credentials being reused,
because credential validity is checked when it is used.
2023-09-13 12:43:46 -07:00
Poorna 18e23bafd9 replication resync: report only the on-disk status (#18017)
Avoid reporting in-memory status since results can vary if different
nodes are queried, resync always runs at a single node.
2023-09-13 10:58:38 -07:00
Harshavardhana 8b8be2695f optimize mkdir calls to avoid base-dir Mkdir attempts (#18021)
Currently we have IOPs of these patterns

```
[OS] os.Mkdir play.min.io:9000 /disk1 2.718µs
[OS] os.Mkdir play.min.io:9000 /disk1/data 2.406µs
[OS] os.Mkdir play.min.io:9000 /disk1/data/.minio.sys 4.068µs
[OS] os.Mkdir play.min.io:9000 /disk1/data/.minio.sys/tmp 2.843µs
[OS] os.Mkdir play.min.io:9000 /disk1/data/.minio.sys/tmp/d89c8ceb-f8d1-4cc6-b483-280f87c4719f 20.152µs
```

It can be seen that we can save quite Nx levels such as
if your drive is mounted at `/disk1/minio` you can simply
skip sending an `Mkdir /disk1/` and `Mkdir /disk1/minio`.

Since they are expected to exist already, this PR adds a way
for us to ignore all paths upto the mount or a directory which
ever has been provided to MinIO setup.
2023-09-13 08:14:36 -07:00
Poorna 96fbf18201 replication: queue existing objects to same workers as incoming (#18020)
Previously existing objects were queued to single worker and MRF re-queues
are also handled by same worker - this does not fully use the available
bandwidth in case there is no incoming workload.
2023-09-12 21:59:15 -07:00
Harshavardhana c8a57a8fa2 fix: send content-md5 for AWS S3 proactively (#18018)
fixes #17977
2023-09-12 19:11:13 -07:00
Harshavardhana b1c2dacab3 fix: allow dynamic ports for API only in non-distributed setups (#18019)
fixes #17998
2023-09-12 19:10:49 -07:00
Harshavardhana 65939913b4 update all dependencies (#18012) 2023-09-12 13:16:46 -07:00
Harshavardhana 08b3a466e8 fix: allow concurrent SFTP connections (#18013)
current implementation did not fully implement
the concurrent SFTP connection implementation,
this PR properly handles this.

fixes #17914
2023-09-12 12:41:52 -07:00
Harshavardhana 5aa7c38035 update pkg to v2.0.1 to extend admin actions (#18008) 2023-09-12 01:11:52 -07:00
Harshavardhana 1df5e31706 optimize MRF replication queue to avoid memory leaks (#18007) 2023-09-11 20:59:11 -07:00
Harshavardhana 9f7044aed0 fix: ignore transient errors in read path (#18006)
Errors such as

```
returned an error (context deadline exceeded) (*fmt.wrapError)
```

```
(msgp: too few bytes left to read object) (*fmt.wrapError)
```
2023-09-11 15:29:59 -07:00
Anis Eleuch 41de53996b heal: calculate the number of workers based on NRRequests (#17945) 2023-09-11 14:48:54 -07:00
Harshavardhana 9878031cfd fix: change DISK_ to DRIVE_ for some drive related envs (#18005) 2023-09-11 12:19:22 -07:00
Harshavardhana e3fbcaeb72 allow scanner key cycle to be empty (#18001)
configs from 2020 server throws an
error due to deprecation of the keys
however an attempt is made to parse
them, we should have chosen existing
defaults - this PR fixes that.
2023-09-09 08:53:32 -07:00
Harshavardhana ca6dd8be5e use go1.21.1 for vulncheck 2023-09-07 16:15:31 -07:00
Minio Trusted fba0924b1d Update yaml files to latest version RELEASE.2023-09-07T02-05-02Z 2023-09-07 23:10:40 +00:00
Poorna 703ed46d79 fix: replication of tags while removing (#17989)
A tag removal was not being replicated prior to this change
2023-09-06 19:05:02 -07:00
Harshavardhana f7ca6c63c2 fix: bucket quota clear and honor existing quota config (#17988) 2023-09-06 19:03:58 -07:00
Harshavardhana ad69b9907f fix: report bucket metrics for only existing buckets (#17987) 2023-09-06 12:50:46 -07:00
Anis Eleuch b9269151a4 fix: drive rotational calculation status for partitions (#17986)
Fix drive rotational calculation status

If a MinIO drive path is mounted to a partition and not a real disk,
getting the rotational status would fail because Linux does not expose
that status to partition; In other words,
/sys/block/drive-partition-name/queue/rotational does not exist;

To fix the issue, the code will search for the rotational status of the
disk that hosts the partition, and this can be calculated from the
real path of /sys/class/block/<drive-partition-name>
2023-09-06 12:37:57 -07:00
Shubhendu bfddbb8b40 Embed file in ZIP with custom permissions (#17954)
This change enables embedding files in ZIP with custom permissions.
Also uses default creds for starting MinIO based on inspect data.

Signed-off-by: Shubhendu Ram Tripathi <shubhendu@minio.io>
2023-09-06 09:24:01 -07:00
Poorna 13a2dc8485 replication resync: avoid blocking on results channel. (#17981)
continues fix in #17775
2023-09-05 20:22:39 -07:00
Harshavardhana 1e51424e8a use syscall.Rename() directly instead of os.Rename() (#17982) 2023-09-05 20:22:23 -07:00
Harshavardhana 5b114b43f7 refactor bandwidth throttling for replication target (#17980)
This refactor is to allow using the bandwidth throttling
for other purposes.
2023-09-05 20:21:59 -07:00
Poorna 812f5a02d7 metrics: fix panic in replication stats reporting (#17979) 2023-09-05 10:26:18 -07:00
Minio Trusted 19f70dbfbf Update yaml files to latest version RELEASE.2023-09-04T19-57-37Z 2023-09-04 21:20:49 +00:00
186 changed files with 5856 additions and 2557 deletions
+9 -2
View File
@@ -1,10 +1,17 @@
.git .git
.github .github
docs
default.etcd default.etcd
*.gz *.gz
*.tar.gz *.tar.gz
*.bzip2 *.bzip2
*.zip *.zip
browser/node_modules browser/node_modules
node_modules node_modules
docs/debugging/s3-verify/s3-verify
docs/debugging/xl-meta/xl-meta
docs/debugging/s3-check-md5/s3-check-md5
docs/debugging/hash-set/hash-set
docs/debugging/healing-bin/healing-bin
docs/debugging/inspect/inspect
docs/debugging/pprofgoparser/pprofgoparser
docs/debugging/reorder-disks/reorder-disks
+13 -2
View File
@@ -37,7 +37,11 @@ jobs:
- name: build-minio - name: build-minio
run: | run: |
TAG="minio/minio:${{ steps.vars.outputs.sha_short }}" make docker TAG="quay.io/minio/minio:${{ steps.vars.outputs.sha_short }}" make docker
- name: multipart uploads test
run: |
${GITHUB_WORKSPACE}/.github/workflows/multipart/migrate.sh "${{ steps.vars.outputs.sha_short }}"
- name: compress and encrypt - name: compress and encrypt
run: | run: |
@@ -59,7 +63,14 @@ jobs:
docker-compose -f ${GITHUB_WORKSPACE}/.github/workflows/mint/minio-${mode}.yaml down || true docker-compose -f ${GITHUB_WORKSPACE}/.github/workflows/mint/minio-${mode}.yaml down || true
docker-compose -f ${GITHUB_WORKSPACE}/.github/workflows/mint/minio-${mode}.yaml rm || true docker-compose -f ${GITHUB_WORKSPACE}/.github/workflows/mint/minio-${mode}.yaml rm || true
done done
docker rmi -f minio/minio:${{ steps.vars.outputs.sha_short }}
docker-compose -f ${GITHUB_WORKSPACE}/.github/workflows/multipart/docker-compose-site1.yaml rm -s -f || true
docker-compose -f ${GITHUB_WORKSPACE}/.github/workflows/multipart/docker-compose-site2.yaml rm -s -f || true
for volume in $(docker volume ls -q | grep minio); do
docker volume rm ${volume} || true
done
docker rmi -f quay.io/minio/minio:${{ steps.vars.outputs.sha_short }}
docker system prune -f || true docker system prune -f || true
docker volume prune -f || true docker volume prune -f || true
docker volume rm $(docker volume ls -q -f dangling=true) || true docker volume rm $(docker volume ls -q -f dangling=true) || true
@@ -2,7 +2,7 @@ version: '3.7'
# Settings and configurations that are common for all containers # Settings and configurations that are common for all containers
x-minio-common: &minio-common x-minio-common: &minio-common
image: minio/minio:${JOB_NAME} image: quay.io/minio/minio:${JOB_NAME}
command: server --console-address ":9001" http://minio{1...4}/cdata{1...2} command: server --console-address ":9001" http://minio{1...4}/cdata{1...2}
expose: expose:
- "9000" - "9000"
+1 -1
View File
@@ -2,7 +2,7 @@ version: '3.7'
# Settings and configurations that are common for all containers # Settings and configurations that are common for all containers
x-minio-common: &minio-common x-minio-common: &minio-common
image: minio/minio:${JOB_NAME} image: quay.io/minio/minio:${JOB_NAME}
command: server --console-address ":9001" edata{1...4} command: server --console-address ":9001" edata{1...4}
expose: expose:
- "9000" - "9000"
+1 -1
View File
@@ -2,7 +2,7 @@ version: '3.7'
# Settings and configurations that are common for all containers # Settings and configurations that are common for all containers
x-minio-common: &minio-common x-minio-common: &minio-common
image: minio/minio:${JOB_NAME} image: quay.io/minio/minio:${JOB_NAME}
command: server --console-address ":9001" http://minio{1...4}/pdata{1...2} http://minio{5...8}/pdata{1...2} command: server --console-address ":9001" http://minio{1...4}/pdata{1...2} http://minio{5...8}/pdata{1...2}
expose: expose:
- "9000" - "9000"
@@ -0,0 +1,66 @@
version: '3.7'
# Settings and configurations that are common for all containers
x-minio-common: &minio-common
image: quay.io/minio/minio:${RELEASE}
command: server http://site1-minio{1...4}/data{1...2}
environment:
- MINIO_PROMETHEUS_AUTH_TYPE=public
- CI=true
# starts 4 docker containers running minio server instances.
# using nginx reverse proxy, load balancing, you can access
# it through port 9000.
services:
site1-minio1:
<<: *minio-common
hostname: site1-minio1
volumes:
- site1-data1-1:/data1
- site1-data1-2:/data2
site1-minio2:
<<: *minio-common
hostname: site1-minio2
volumes:
- site1-data2-1:/data1
- site1-data2-2:/data2
site1-minio3:
<<: *minio-common
hostname: site1-minio3
volumes:
- site1-data3-1:/data1
- site1-data3-2:/data2
site1-minio4:
<<: *minio-common
hostname: site1-minio4
volumes:
- site1-data4-1:/data1
- site1-data4-2:/data2
site1-nginx:
image: nginx:1.19.2-alpine
hostname: site1-nginx
volumes:
- ./nginx-site1.conf:/etc/nginx/nginx.conf:ro
ports:
- "9001:9001"
depends_on:
- site1-minio1
- site1-minio2
- site1-minio3
- site1-minio4
## By default this config uses default local driver,
## For custom volumes replace with volume driver configuration.
volumes:
site1-data1-1:
site1-data1-2:
site1-data2-1:
site1-data2-2:
site1-data3-1:
site1-data3-2:
site1-data4-1:
site1-data4-2:
@@ -0,0 +1,66 @@
version: '3.7'
# Settings and configurations that are common for all containers
x-minio-common: &minio-common
image: quay.io/minio/minio:${RELEASE}
command: server http://site2-minio{1...4}/data{1...2}
environment:
- MINIO_PROMETHEUS_AUTH_TYPE=public
- CI=true
# starts 4 docker containers running minio server instances.
# using nginx reverse proxy, load balancing, you can access
# it through port 9000.
services:
site2-minio1:
<<: *minio-common
hostname: site2-minio1
volumes:
- site2-data1-1:/data1
- site2-data1-2:/data2
site2-minio2:
<<: *minio-common
hostname: site2-minio2
volumes:
- site2-data2-1:/data1
- site2-data2-2:/data2
site2-minio3:
<<: *minio-common
hostname: site2-minio3
volumes:
- site2-data3-1:/data1
- site2-data3-2:/data2
site2-minio4:
<<: *minio-common
hostname: site2-minio4
volumes:
- site2-data4-1:/data1
- site2-data4-2:/data2
site2-nginx:
image: nginx:1.19.2-alpine
hostname: site2-nginx
volumes:
- ./nginx-site2.conf:/etc/nginx/nginx.conf:ro
ports:
- "9002:9002"
depends_on:
- site2-minio1
- site2-minio2
- site2-minio3
- site2-minio4
## By default this config uses default local driver,
## For custom volumes replace with volume driver configuration.
volumes:
site2-data1-1:
site2-data1-2:
site2-data2-1:
site2-data2-2:
site2-data3-1:
site2-data3-2:
site2-data4-1:
site2-data4-2:
+115
View File
@@ -0,0 +1,115 @@
#!/bin/bash
set -x
## change working directory
cd .github/workflows/multipart/
docker-compose -f docker-compose-site1.yaml rm -s -f
docker-compose -f docker-compose-site2.yaml rm -s -f
for volume in $(docker volume ls -q | grep minio); do
docker volume rm ${volume}
done
if [ ! -f ./mc ]; then
wget --quiet -O mc https://dl.minio.io/client/mc/release/linux-amd64/mc &&
chmod +x mc
fi
(
cd /tmp
go install github.com/minio/minio/docs/debugging/s3-check-md5@latest
)
export RELEASE=RELEASE.2023-08-29T23-07-35Z
docker-compose -f docker-compose-site1.yaml up -d
docker-compose -f docker-compose-site2.yaml up -d
sleep 30s
./mc alias set site1 http://site1-nginx:9001 minioadmin minioadmin --api s3v4
./mc alias set site2 http://site2-nginx:9002 minioadmin minioadmin --api s3v4
./mc ready site1/
./mc ready site2/
./mc admin replicate add site1 site2
./mc mb site1/testbucket/
./mc cp -r --quiet /usr/bin site1/testbucket/
sleep 5
s3-check-md5 -h
failed_count_site1=$(s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site1-nginx:9001 -bucket testbucket 2>&1 | grep FAILED | wc -l)
failed_count_site2=$(s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site2-nginx:9002 -bucket testbucket 2>&1 | grep FAILED | wc -l)
if [ $failed_count_site1 -ne 0 ]; then
echo "failed with multipart on site1 uploads"
exit 1
fi
if [ $failed_count_site2 -ne 0 ]; then
echo "failed with multipart on site2 uploads"
exit 1
fi
./mc cp -r --quiet /usr/bin site1/testbucket/
sleep 5
failed_count_site1=$(s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site1-nginx:9001 -bucket testbucket 2>&1 | grep FAILED | wc -l)
failed_count_site2=$(s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site2-nginx:9002 -bucket testbucket 2>&1 | grep FAILED | wc -l)
## we do not need to fail here, since we are going to test
## upgrading to master, healing and being able to recover
## the last version.
if [ $failed_count_site1 -ne 0 ]; then
echo "failed with multipart on site1 uploads ${failed_count_site1}"
fi
if [ $failed_count_site2 -ne 0 ]; then
echo "failed with multipart on site2 uploads ${failed_count_site2}"
fi
export RELEASE=${1}
docker-compose -f docker-compose-site1.yaml up -d
docker-compose -f docker-compose-site2.yaml up -d
./mc ready site1/
./mc ready site2/
for i in $(seq 1 10); do
# mc admin heal -r --remove when used against a LB endpoint
# behaves flaky, let this run 10 times before giving up
./mc admin heal -r --remove --json site1/ 2>&1 >/dev/null
./mc admin heal -r --remove --json site2/ 2>&1 >/dev/null
done
failed_count_site1=$(s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site1-nginx:9001 -bucket testbucket 2>&1 | grep FAILED | wc -l)
failed_count_site2=$(s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site2-nginx:9002 -bucket testbucket 2>&1 | grep FAILED | wc -l)
if [ $failed_count_site1 -ne 0 ]; then
echo "failed with multipart on site1 uploads"
exit 1
fi
if [ $failed_count_site2 -ne 0 ]; then
echo "failed with multipart on site2 uploads"
exit 1
fi
docker-compose -f docker-compose-site1.yaml rm -s -f
docker-compose -f docker-compose-site2.yaml rm -s -f
for volume in $(docker volume ls -q | grep minio); do
docker volume rm ${volume}
done
docker system prune -f || true
docker volume prune -f || true
docker volume rm $(docker volume ls -q -f dangling=true) || true
## change working directory
cd ../../../
@@ -0,0 +1,61 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 4096;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
# include /etc/nginx/conf.d/*.conf;
upstream minio {
server site1-minio1:9000;
server site1-minio2:9000;
server site1-minio3:9000;
server site1-minio4:9000;
}
server {
listen 9001;
listen [::]:9001;
server_name localhost;
# To allow special characters in headers
ignore_invalid_headers off;
# Allow any size file to be uploaded.
# Set to a value such as 1000m; to restrict file size to a specific value
client_max_body_size 0;
# To disable buffering
proxy_buffering off;
proxy_request_buffering off;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 300;
# Default is HTTP/1, keepalive is only enabled in HTTP/1.1
proxy_http_version 1.1;
proxy_set_header Connection "";
chunked_transfer_encoding off;
proxy_pass http://minio;
}
}
}
@@ -0,0 +1,61 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 4096;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
# include /etc/nginx/conf.d/*.conf;
upstream minio {
server site2-minio1:9000;
server site2-minio2:9000;
server site2-minio3:9000;
server site2-minio4:9000;
}
server {
listen 9002;
listen [::]:9002;
server_name localhost;
# To allow special characters in headers
ignore_invalid_headers off;
# Allow any size file to be uploaded.
# Set to a value such as 1000m; to restrict file size to a specific value
client_max_body_size 0;
# To disable buffering
proxy_buffering off;
proxy_request_buffering off;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 300;
# Default is HTTP/1, keepalive is only enabled in HTTP/1.1
proxy_http_version 1.1;
proxy_set_header Connection "";
chunked_transfer_encoding off;
proxy_pass http://minio;
}
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ docker volume rm $(docker volume ls -f dangling=true) || true
cd .github/workflows/mint cd .github/workflows/mint
docker-compose -f minio-${MODE}.yaml up -d docker-compose -f minio-${MODE}.yaml up -d
sleep 5m sleep 30s
docker system prune -f || true docker system prune -f || true
docker volume prune -f || true docker volume prune -f || true
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v3
with: with:
go-version: 1.21.0 go-version: 1.21.1
check-latest: true check-latest: true
- name: Get official govulncheck - name: Get official govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest run: go install golang.org/x/vuln/cmd/govulncheck@latest
+3 -2
View File
@@ -32,6 +32,8 @@ minio.RELEASE*
mc mc
nancy nancy
inspects/* inspects/*
.bin/
*.gz
docs/debugging/s3-verify/s3-verify docs/debugging/s3-verify/s3-verify
docs/debugging/xl-meta/xl-meta docs/debugging/xl-meta/xl-meta
docs/debugging/s3-check-md5/s3-check-md5 docs/debugging/s3-check-md5/s3-check-md5
@@ -39,5 +41,4 @@ docs/debugging/hash-set/hash-set
docs/debugging/healing-bin/healing-bin docs/debugging/healing-bin/healing-bin
docs/debugging/inspect/inspect docs/debugging/inspect/inspect
docs/debugging/pprofgoparser/pprofgoparser docs/debugging/pprofgoparser/pprofgoparser
.bin/ docs/debugging/reorder-disks/reorder-disks
*.gz
-1
View File
@@ -13,7 +13,6 @@ linters:
enable: enable:
- durationcheck - durationcheck
- gocritic - gocritic
- gofmt
- gofumpt - gofumpt
- goimports - goimports
- gomodguard - gomodguard
+671 -28
View File
@@ -2392,6 +2392,7 @@ https://github.com/IBM/sarama
# MIT License # MIT License
Copyright (c) 2013 Shopify Copyright (c) 2013 Shopify
Copyright (c) 2023 IBM Corporation Copyright (c) 2023 IBM Corporation
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
@@ -2922,33 +2923,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================ ================================================================
github.com/benbjohnson/clock
https://github.com/benbjohnson/clock
----------------------------------------------------------------
The MIT License (MIT)
Copyright (c) 2014 Ben Johnson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================
github.com/beorn7/perks github.com/beorn7/perks
https://github.com/beorn7/perks https://github.com/beorn7/perks
---------------------------------------------------------------- ----------------------------------------------------------------
@@ -3089,7 +3063,7 @@ https://github.com/charmbracelet/lipgloss
---------------------------------------------------------------- ----------------------------------------------------------------
MIT License MIT License
Copyright (c) 2021 Charmbracelet, Inc Copyright (c) 2021-2023 Charmbracelet, Inc
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -11869,6 +11843,8 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice
github.com/hashicorp/golang-lru github.com/hashicorp/golang-lru
https://github.com/hashicorp/golang-lru https://github.com/hashicorp/golang-lru
---------------------------------------------------------------- ----------------------------------------------------------------
Copyright (c) 2014 HashiCorp, Inc.
Mozilla Public License, version 2.0 Mozilla Public License, version 2.0
1. Definitions 1. Definitions
@@ -20017,6 +19993,673 @@ For more information on this, and how to apply and follow the GNU AGPL, see
================================================================ ================================================================
github.com/minio/pkg/v2
https://github.com/minio/pkg/v2
----------------------------------------------------------------
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
================================================================
github.com/minio/selfupdate github.com/minio/selfupdate
https://github.com/minio/selfupdate https://github.com/minio/selfupdate
---------------------------------------------------------------- ----------------------------------------------------------------
+18 -9
View File
@@ -6,7 +6,7 @@ GOARCH := $(shell go env GOARCH)
GOOS := $(shell go env GOOS) GOOS := $(shell go env GOOS)
VERSION ?= $(shell git describe --tags) VERSION ?= $(shell git describe --tags)
TAG ?= "minio/minio:$(VERSION)" TAG ?= "quay.io/minio/minio:$(VERSION)"
GOLANGCI_VERSION = v1.51.2 GOLANGCI_VERSION = v1.51.2
GOLANGCI_DIR = .bin/golangci/$(GOLANGCI_VERSION) GOLANGCI_DIR = .bin/golangci/$(GOLANGCI_VERSION)
@@ -45,22 +45,22 @@ lint-fix: getdeps ## runs golangci-lint suite of linters with automatic fixes
@$(GOLANGCI) run --build-tags kqueue --timeout=10m --config ./.golangci.yml --fix @$(GOLANGCI) run --build-tags kqueue --timeout=10m --config ./.golangci.yml --fix
check: test check: test
test: verifiers build ## builds minio, runs linters, tests test: verifiers build build-debugging ## builds minio, runs linters, tests
@echo "Running unit tests" @echo "Running unit tests"
@MINIO_API_REQUESTS_MAX=10000 CGO_ENABLED=0 go test -tags kqueue ./... @MINIO_API_REQUESTS_MAX=10000 CGO_ENABLED=0 go test -tags kqueue ./...
test-root-disable: install test-root-disable: install-race
@echo "Running minio root lockdown tests" @echo "Running minio root lockdown tests"
@env bash $(PWD)/buildscripts/disable-root.sh @env bash $(PWD)/buildscripts/disable-root.sh
test-decom: install test-decom: install-race
@echo "Running minio decom tests" @echo "Running minio decom tests"
@env bash $(PWD)/docs/distributed/decom.sh @env bash $(PWD)/docs/distributed/decom.sh
@env bash $(PWD)/docs/distributed/decom-encrypted.sh @env bash $(PWD)/docs/distributed/decom-encrypted.sh
@env bash $(PWD)/docs/distributed/decom-encrypted-sse-s3.sh @env bash $(PWD)/docs/distributed/decom-encrypted-sse-s3.sh
@env bash $(PWD)/docs/distributed/decom-compressed-sse-s3.sh @env bash $(PWD)/docs/distributed/decom-compressed-sse-s3.sh
test-upgrade: build test-upgrade: install-race
@echo "Running minio upgrade tests" @echo "Running minio upgrade tests"
@(env bash $(PWD)/buildscripts/minio-upgrade.sh) @(env bash $(PWD)/buildscripts/minio-upgrade.sh)
@@ -86,18 +86,18 @@ test-replication-3site:
test-delete-replication: test-delete-replication:
@(env bash $(PWD)/docs/bucket/replication/delete-replication.sh) @(env bash $(PWD)/docs/bucket/replication/delete-replication.sh)
test-replication: install test-replication-2site test-replication-3site test-delete-replication test-sio-error ## verify multi site replication test-replication: install-race test-replication-2site test-replication-3site test-delete-replication test-sio-error ## verify multi site replication
@echo "Running tests for replicating three sites" @echo "Running tests for replicating three sites"
test-site-replication-ldap: install ## verify automatic site replication test-site-replication-ldap: install-race ## verify automatic site replication
@echo "Running tests for automatic site replication of IAM (with LDAP)" @echo "Running tests for automatic site replication of IAM (with LDAP)"
@(env bash $(PWD)/docs/site-replication/run-multi-site-ldap.sh) @(env bash $(PWD)/docs/site-replication/run-multi-site-ldap.sh)
test-site-replication-oidc: install ## verify automatic site replication test-site-replication-oidc: install-race ## verify automatic site replication
@echo "Running tests for automatic site replication of IAM (with OIDC)" @echo "Running tests for automatic site replication of IAM (with OIDC)"
@(env bash $(PWD)/docs/site-replication/run-multi-site-oidc.sh) @(env bash $(PWD)/docs/site-replication/run-multi-site-oidc.sh)
test-site-replication-minio: install ## verify automatic site replication test-site-replication-minio: install-race ## verify automatic site replication
@echo "Running tests for automatic site replication of IAM (with MinIO IDP)" @echo "Running tests for automatic site replication of IAM (with MinIO IDP)"
@(env bash $(PWD)/docs/site-replication/run-multi-site-minio-idp.sh) @(env bash $(PWD)/docs/site-replication/run-multi-site-minio-idp.sh)
@@ -128,6 +128,9 @@ verify-healing-inconsistent-versions: ## verify resolving inconsistent versions
@GORACE=history_size=7 CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null @GORACE=history_size=7 CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
@(env bash $(PWD)/buildscripts/resolve-right-versions.sh) @(env bash $(PWD)/buildscripts/resolve-right-versions.sh)
build-debugging:
@(env bash $(PWD)/docs/debugging/build.sh)
build: checks ## builds minio to $(PWD) build: checks ## builds minio to $(PWD)
@echo "Building minio binary to './minio'" @echo "Building minio binary to './minio'"
@CGO_ENABLED=0 go build -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null @CGO_ENABLED=0 go build -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
@@ -159,6 +162,12 @@ docker: build ## builds minio docker container
@echo "Building minio docker image '$(TAG)'" @echo "Building minio docker image '$(TAG)'"
@docker build -q --no-cache -t $(TAG) . -f Dockerfile @docker build -q --no-cache -t $(TAG) . -f Dockerfile
install-race: checks ## builds minio to $(PWD)
@echo "Building minio binary to './minio'"
@GORACE=history_size=7 CGO_ENABLED=1 go build -tags kqueue -race -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
@echo "Installing minio binary to '$(GOPATH)/bin/minio'"
@mkdir -p $(GOPATH)/bin && cp -f $(PWD)/minio $(GOPATH)/bin/minio
install: build ## builds minio and installs it to $GOPATH/bin. install: build ## builds minio and installs it to $GOPATH/bin.
@echo "Installing minio binary to '$(GOPATH)/bin/minio'" @echo "Installing minio binary to '$(GOPATH)/bin/minio'"
@mkdir -p $(GOPATH)/bin && cp -f $(PWD)/minio $(GOPATH)/bin/minio @mkdir -p $(GOPATH)/bin && cp -f $(PWD)/minio $(GOPATH)/bin/minio
+2
View File
@@ -56,6 +56,8 @@ done
set +e set +e
sleep 10
./mc ls minioadm/ ./mc ls minioadm/
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "listing failed, 'minioadmin' should be enabled" echo "listing failed, 'minioadmin' should be enabled"
+1 -1
View File
@@ -94,7 +94,7 @@ func (a adminAPIHandlers) PutBucketQuotaConfigHandler(w http.ResponseWriter, r *
Quota: data, Quota: data,
UpdatedAt: updatedAt, UpdatedAt: updatedAt,
} }
if quotaConfig.Quota == 0 { if quotaConfig.Size == 0 || quotaConfig.Quota == 0 {
bucketMeta.Quota = nil bucketMeta.Quota = nil
} }
+3 -3
View File
@@ -27,14 +27,14 @@ import (
"github.com/minio/madmin-go/v3" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/config" "github.com/minio/minio/internal/config"
iampolicy "github.com/minio/pkg/v2/policy" "github.com/minio/pkg/v2/policy"
) )
// validateAdminReq will validate request against and return whether it is allowed. // validateAdminReq will validate request against and return whether it is allowed.
// If any of the supplied actions are allowed it will be successful. // If any of the supplied actions are allowed it will be successful.
// If nil ObjectLayer is returned, the operation is not permitted. // If nil ObjectLayer is returned, the operation is not permitted.
// When nil ObjectLayer has been returned an error has always been sent to w. // When nil ObjectLayer has been returned an error has always been sent to w.
func validateAdminReq(ctx context.Context, w http.ResponseWriter, r *http.Request, actions ...iampolicy.AdminAction) (ObjectLayer, auth.Credentials) { func validateAdminReq(ctx context.Context, w http.ResponseWriter, r *http.Request, actions ...policy.AdminAction) (ObjectLayer, auth.Credentials) {
// Get current object layer instance. // Get current object layer instance.
objectAPI := newObjectLayerFn() objectAPI := newObjectLayerFn()
if objectAPI == nil || globalNotificationSys == nil { if objectAPI == nil || globalNotificationSys == nil {
@@ -78,7 +78,7 @@ func toAdminAPIErr(ctx context.Context, err error) APIError {
var apiErr APIError var apiErr APIError
switch e := err.(type) { switch e := err.(type) {
case iampolicy.Error: case policy.Error:
apiErr = APIError{ apiErr = APIError{
Code: "XMinioMalformedIAMPolicy", Code: "XMinioMalformedIAMPolicy",
Description: e.Error(), Description: e.Error(),
+10 -10
View File
@@ -38,14 +38,14 @@ import (
"github.com/minio/minio/internal/config/subnet" "github.com/minio/minio/internal/config/subnet"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/minio/mux" "github.com/minio/mux"
iampolicy "github.com/minio/pkg/v2/policy" "github.com/minio/pkg/v2/policy"
) )
// DelConfigKVHandler - DELETE /minio/admin/v3/del-config-kv // DelConfigKVHandler - DELETE /minio/admin/v3/del-config-kv
func (a adminAPIHandlers) DelConfigKVHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) DelConfigKVHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction) objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -149,7 +149,7 @@ type setConfigResult struct {
func (a adminAPIHandlers) SetConfigKVHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) SetConfigKVHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction) objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -242,7 +242,7 @@ func setConfigKV(ctx context.Context, objectAPI ObjectLayer, kvBytes []byte) (re
func (a adminAPIHandlers) GetConfigKVHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) GetConfigKVHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction) objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -288,7 +288,7 @@ func (a adminAPIHandlers) GetConfigKVHandler(w http.ResponseWriter, r *http.Requ
func (a adminAPIHandlers) ClearConfigHistoryKVHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) ClearConfigHistoryKVHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -321,7 +321,7 @@ func (a adminAPIHandlers) ClearConfigHistoryKVHandler(w http.ResponseWriter, r *
func (a adminAPIHandlers) RestoreConfigHistoryKVHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) RestoreConfigHistoryKVHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -367,7 +367,7 @@ func (a adminAPIHandlers) RestoreConfigHistoryKVHandler(w http.ResponseWriter, r
func (a adminAPIHandlers) ListConfigHistoryKVHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) ListConfigHistoryKVHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction) objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -405,7 +405,7 @@ func (a adminAPIHandlers) ListConfigHistoryKVHandler(w http.ResponseWriter, r *h
func (a adminAPIHandlers) HelpConfigKVHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) HelpConfigKVHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -430,7 +430,7 @@ func (a adminAPIHandlers) HelpConfigKVHandler(w http.ResponseWriter, r *http.Req
func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction) objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -482,7 +482,7 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction) objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
+5 -5
View File
@@ -34,11 +34,11 @@ import (
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/minio/mux" "github.com/minio/mux"
"github.com/minio/pkg/v2/ldap" "github.com/minio/pkg/v2/ldap"
iampolicy "github.com/minio/pkg/v2/policy" "github.com/minio/pkg/v2/policy"
) )
func addOrUpdateIDPHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, isUpdate bool) { func addOrUpdateIDPHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, isUpdate bool) {
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction) objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -220,7 +220,7 @@ func (a adminAPIHandlers) UpdateIdentityProviderCfg(w http.ResponseWriter, r *ht
func (a adminAPIHandlers) ListIdentityProviderCfg(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) ListIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction) objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -273,7 +273,7 @@ func (a adminAPIHandlers) ListIdentityProviderCfg(w http.ResponseWriter, r *http
func (a adminAPIHandlers) GetIdentityProviderCfg(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) GetIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction) objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -332,7 +332,7 @@ func (a adminAPIHandlers) GetIdentityProviderCfg(w http.ResponseWriter, r *http.
func (a adminAPIHandlers) DeleteIdentityProviderCfg(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) DeleteIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
+3 -3
View File
@@ -25,7 +25,7 @@ import (
"github.com/minio/madmin-go/v3" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/minio/mux" "github.com/minio/mux"
iampolicy "github.com/minio/pkg/v2/policy" "github.com/minio/pkg/v2/policy"
) )
// ListLDAPPolicyMappingEntities lists users/groups mapped to given/all policies. // ListLDAPPolicyMappingEntities lists users/groups mapped to given/all policies.
@@ -50,7 +50,7 @@ func (a adminAPIHandlers) ListLDAPPolicyMappingEntities(w http.ResponseWriter, r
// Check authorization. // Check authorization.
objectAPI, cred := validateAdminReq(ctx, w, r, objectAPI, cred := validateAdminReq(ctx, w, r,
iampolicy.ListGroupsAdminAction, iampolicy.ListUsersAdminAction, iampolicy.ListUserPoliciesAdminAction) policy.ListGroupsAdminAction, policy.ListUsersAdminAction, policy.ListUserPoliciesAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -96,7 +96,7 @@ func (a adminAPIHandlers) AttachDetachPolicyLDAP(w http.ResponseWriter, r *http.
// Check authorization. // Check authorization.
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.UpdatePolicyAssociationAction) objectAPI, cred := validateAdminReq(ctx, w, r, policy.UpdatePolicyAssociationAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
+8 -8
View File
@@ -26,7 +26,7 @@ import (
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/minio/mux" "github.com/minio/mux"
iampolicy "github.com/minio/pkg/v2/policy" "github.com/minio/pkg/v2/policy"
) )
var ( var (
@@ -37,7 +37,7 @@ var (
func (a adminAPIHandlers) StartDecommission(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) StartDecommission(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.DecommissionAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.DecommissionAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -113,7 +113,7 @@ func (a adminAPIHandlers) StartDecommission(w http.ResponseWriter, r *http.Reque
func (a adminAPIHandlers) CancelDecommission(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) CancelDecommission(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.DecommissionAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.DecommissionAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -159,7 +159,7 @@ func (a adminAPIHandlers) CancelDecommission(w http.ResponseWriter, r *http.Requ
func (a adminAPIHandlers) StatusPool(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) StatusPool(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ServerInfoAdminAction, iampolicy.DecommissionAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.ServerInfoAdminAction, policy.DecommissionAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -200,7 +200,7 @@ func (a adminAPIHandlers) StatusPool(w http.ResponseWriter, r *http.Request) {
func (a adminAPIHandlers) ListPools(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) ListPools(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ServerInfoAdminAction, iampolicy.DecommissionAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.ServerInfoAdminAction, policy.DecommissionAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -233,7 +233,7 @@ func (a adminAPIHandlers) ListPools(w http.ResponseWriter, r *http.Request) {
func (a adminAPIHandlers) RebalanceStart(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) RebalanceStart(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.RebalanceAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.RebalanceAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -304,7 +304,7 @@ func (a adminAPIHandlers) RebalanceStart(w http.ResponseWriter, r *http.Request)
func (a adminAPIHandlers) RebalanceStatus(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) RebalanceStatus(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.RebalanceAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.RebalanceAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -344,7 +344,7 @@ func (a adminAPIHandlers) RebalanceStatus(w http.ResponseWriter, r *http.Request
func (a adminAPIHandlers) RebalanceStop(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) RebalanceStop(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.RebalanceAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.RebalanceAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
+96 -77
View File
@@ -1,4 +1,4 @@
// Copyright (c) 2015-2021 MinIO, Inc. // Copyright (c) 2015-2023 MinIO, Inc.
// //
// This file is part of MinIO Object Storage stack // This file is part of MinIO Object Storage stack
// //
@@ -19,6 +19,7 @@ package cmd
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@@ -34,14 +35,14 @@ import (
"github.com/minio/minio/internal/config/dns" "github.com/minio/minio/internal/config/dns"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/minio/mux" "github.com/minio/mux"
iampolicy "github.com/minio/pkg/v2/policy" "github.com/minio/pkg/v2/policy"
) )
// RemoveUser - DELETE /minio/admin/v3/remove-user?accessKey=<access_key> // RemoveUser - DELETE /minio/admin/v3/remove-user?accessKey=<access_key>
func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.DeleteUserAdminAction) objectAPI, cred := validateAdminReq(ctx, w, r, policy.DeleteUserAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -85,7 +86,7 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
func (a adminAPIHandlers) ListBucketUsers(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) ListBucketUsers(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ListUsersAdminAction) objectAPI, cred := validateAdminReq(ctx, w, r, policy.ListUsersAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -119,7 +120,7 @@ func (a adminAPIHandlers) ListBucketUsers(w http.ResponseWriter, r *http.Request
func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ListUsersAdminAction) objectAPI, cred := validateAdminReq(ctx, w, r, policy.ListUsersAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -186,10 +187,10 @@ func (a adminAPIHandlers) GetUserInfo(w http.ResponseWriter, r *http.Request) {
checkDenyOnly = true checkDenyOnly = true
} }
if !globalIAMSys.IsAllowed(iampolicy.Args{ if !globalIAMSys.IsAllowed(policy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
Groups: cred.Groups, Groups: cred.Groups,
Action: iampolicy.GetUserAdminAction, Action: policy.GetUserAdminAction,
ConditionValues: getConditionValues(r, "", cred), ConditionValues: getConditionValues(r, "", cred),
IsOwner: owner, IsOwner: owner,
Claims: cred.Claims, Claims: cred.Claims,
@@ -218,7 +219,7 @@ func (a adminAPIHandlers) GetUserInfo(w http.ResponseWriter, r *http.Request) {
func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.AddUserToGroupAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.AddUserToGroupAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -288,7 +289,7 @@ func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Requ
func (a adminAPIHandlers) GetGroup(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) GetGroup(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.GetGroupAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.GetGroupAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -315,7 +316,7 @@ func (a adminAPIHandlers) GetGroup(w http.ResponseWriter, r *http.Request) {
func (a adminAPIHandlers) ListGroups(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) ListGroups(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ListGroupsAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.ListGroupsAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -339,7 +340,7 @@ func (a adminAPIHandlers) ListGroups(w http.ResponseWriter, r *http.Request) {
func (a adminAPIHandlers) SetGroupStatus(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) SetGroupStatus(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.EnableGroupAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.EnableGroupAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -382,7 +383,7 @@ func (a adminAPIHandlers) SetGroupStatus(w http.ResponseWriter, r *http.Request)
func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, creds := validateAdminReq(ctx, w, r, iampolicy.EnableUserAdminAction) objectAPI, creds := validateAdminReq(ctx, w, r, policy.EnableUserAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -470,10 +471,10 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
checkDenyOnly = true checkDenyOnly = true
} }
if !globalIAMSys.IsAllowed(iampolicy.Args{ if !globalIAMSys.IsAllowed(policy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
Groups: cred.Groups, Groups: cred.Groups,
Action: iampolicy.CreateUserAdminAction, Action: policy.CreateUserAdminAction,
ConditionValues: getConditionValues(r, "", cred), ConditionValues: getConditionValues(r, "", cred),
IsOwner: owner, IsOwner: owner,
Claims: cred.Claims, Claims: cred.Claims,
@@ -544,10 +545,10 @@ func (a adminAPIHandlers) TemporaryAccountInfo(w http.ResponseWriter, r *http.Re
return return
} }
if !globalIAMSys.IsAllowed(iampolicy.Args{ if !globalIAMSys.IsAllowed(policy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
Groups: cred.Groups, Groups: cred.Groups,
Action: iampolicy.ListTemporaryAccountsAdminAction, Action: policy.ListTemporaryAccountsAdminAction,
ConditionValues: getConditionValues(r, "", cred), ConditionValues: getConditionValues(r, "", cred),
IsOwner: owner, IsOwner: owner,
Claims: cred.Claims, Claims: cred.Claims,
@@ -556,16 +557,16 @@ func (a adminAPIHandlers) TemporaryAccountInfo(w http.ResponseWriter, r *http.Re
return return
} }
stsAccount, policy, err := globalIAMSys.GetTemporaryAccount(ctx, accessKey) stsAccount, sessionPolicy, err := globalIAMSys.GetTemporaryAccount(ctx, accessKey)
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return return
} }
var stsAccountPolicy iampolicy.Policy var stsAccountPolicy policy.Policy
if policy != nil { if sessionPolicy != nil {
stsAccountPolicy = *policy stsAccountPolicy = *sessionPolicy
} else { } else {
policiesNames, err := globalIAMSys.PolicyDBGet(stsAccount.ParentUser, false) policiesNames, err := globalIAMSys.PolicyDBGet(stsAccount.ParentUser, false)
if err != nil { if err != nil {
@@ -584,7 +585,7 @@ func (a adminAPIHandlers) TemporaryAccountInfo(w http.ResponseWriter, r *http.Re
infoResp := madmin.TemporaryAccountInfoResp{ infoResp := madmin.TemporaryAccountInfoResp{
ParentUser: stsAccount.ParentUser, ParentUser: stsAccount.ParentUser,
AccountStatus: stsAccount.Status, AccountStatus: stsAccount.Status,
ImpliedPolicy: policy == nil, ImpliedPolicy: sessionPolicy == nil,
Policy: string(policyJSON), Policy: string(policyJSON),
Expiration: &stsAccount.Expiration, Expiration: &stsAccount.Expiration,
} }
@@ -709,10 +710,10 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
// //
// This allows turning off service accounts for request sender, // This allows turning off service accounts for request sender,
// if there is no deny statement this call is implicitly enabled. // if there is no deny statement this call is implicitly enabled.
if !globalIAMSys.IsAllowed(iampolicy.Args{ if !globalIAMSys.IsAllowed(policy.Args{
AccountName: requestorUser, AccountName: requestorUser,
Groups: requestorGroups, Groups: requestorGroups,
Action: iampolicy.CreateServiceAccountAdminAction, Action: policy.CreateServiceAccountAdminAction,
ConditionValues: getConditionValues(r, "", cred), ConditionValues: getConditionValues(r, "", cred),
IsOwner: owner, IsOwner: owner,
Claims: cred.Claims, Claims: cred.Claims,
@@ -743,10 +744,10 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
} else { } else {
// Need permission if we are creating a service account for a // Need permission if we are creating a service account for a
// user <> to the request sender // user <> to the request sender
if !globalIAMSys.IsAllowed(iampolicy.Args{ if !globalIAMSys.IsAllowed(policy.Args{
AccountName: requestorUser, AccountName: requestorUser,
Groups: requestorGroups, Groups: requestorGroups,
Action: iampolicy.CreateServiceAccountAdminAction, Action: policy.CreateServiceAccountAdminAction,
ConditionValues: getConditionValues(r, "", cred), ConditionValues: getConditionValues(r, "", cred),
IsOwner: owner, IsOwner: owner,
Claims: cred.Claims, Claims: cred.Claims,
@@ -773,9 +774,9 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
// latter, a group notion is not supported. // latter, a group notion is not supported.
} }
var sp *iampolicy.Policy var sp *policy.Policy
if len(createReq.Policy) > 0 { if len(createReq.Policy) > 0 {
sp, err = iampolicy.ParseConfig(bytes.NewReader(createReq.Policy)) sp, err = policy.ParseConfig(bytes.NewReader(createReq.Policy))
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return return
@@ -864,10 +865,10 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re
return return
} }
if !globalIAMSys.IsAllowed(iampolicy.Args{ if !globalIAMSys.IsAllowed(policy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
Groups: cred.Groups, Groups: cred.Groups,
Action: iampolicy.UpdateServiceAccountAdminAction, Action: policy.UpdateServiceAccountAdminAction,
ConditionValues: getConditionValues(r, "", cred), ConditionValues: getConditionValues(r, "", cred),
IsOwner: owner, IsOwner: owner,
Claims: cred.Claims, Claims: cred.Claims,
@@ -903,9 +904,9 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re
return return
} }
var sp *iampolicy.Policy var sp *policy.Policy
if len(updateReq.NewPolicy) > 0 { if len(updateReq.NewPolicy) > 0 {
sp, err = iampolicy.ParseConfig(bytes.NewReader(updateReq.NewPolicy)) sp, err = policy.ParseConfig(bytes.NewReader(updateReq.NewPolicy))
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return return
@@ -970,16 +971,16 @@ func (a adminAPIHandlers) InfoServiceAccount(w http.ResponseWriter, r *http.Requ
return return
} }
svcAccount, policy, err := globalIAMSys.GetServiceAccount(ctx, accessKey) svcAccount, sessionPolicy, err := globalIAMSys.GetServiceAccount(ctx, accessKey)
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return return
} }
if !globalIAMSys.IsAllowed(iampolicy.Args{ if !globalIAMSys.IsAllowed(policy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
Groups: cred.Groups, Groups: cred.Groups,
Action: iampolicy.ListServiceAccountsAdminAction, Action: policy.ListServiceAccountsAdminAction,
ConditionValues: getConditionValues(r, "", cred), ConditionValues: getConditionValues(r, "", cred),
IsOwner: owner, IsOwner: owner,
Claims: cred.Claims, Claims: cred.Claims,
@@ -995,10 +996,10 @@ func (a adminAPIHandlers) InfoServiceAccount(w http.ResponseWriter, r *http.Requ
} }
} }
var svcAccountPolicy iampolicy.Policy var svcAccountPolicy policy.Policy
if policy != nil { if sessionPolicy != nil {
svcAccountPolicy = *policy svcAccountPolicy = *sessionPolicy
} else { } else {
policiesNames, err := globalIAMSys.PolicyDBGet(svcAccount.ParentUser, false) policiesNames, err := globalIAMSys.PolicyDBGet(svcAccount.ParentUser, false)
if err != nil { if err != nil {
@@ -1024,7 +1025,7 @@ func (a adminAPIHandlers) InfoServiceAccount(w http.ResponseWriter, r *http.Requ
Name: svcAccount.Name, Name: svcAccount.Name,
Description: svcAccount.Description, Description: svcAccount.Description,
AccountStatus: svcAccount.Status, AccountStatus: svcAccount.Status,
ImpliedPolicy: policy == nil, ImpliedPolicy: sessionPolicy == nil,
Policy: string(policyJSON), Policy: string(policyJSON),
Expiration: expiration, Expiration: expiration,
} }
@@ -1067,10 +1068,10 @@ func (a adminAPIHandlers) ListServiceAccounts(w http.ResponseWriter, r *http.Req
// sender), check that the user has permissions. // sender), check that the user has permissions.
user := r.Form.Get("user") user := r.Form.Get("user")
if user != "" && user != cred.AccessKey { if user != "" && user != cred.AccessKey {
if !globalIAMSys.IsAllowed(iampolicy.Args{ if !globalIAMSys.IsAllowed(policy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
Groups: cred.Groups, Groups: cred.Groups,
Action: iampolicy.ListServiceAccountsAdminAction, Action: policy.ListServiceAccountsAdminAction,
ConditionValues: getConditionValues(r, "", cred), ConditionValues: getConditionValues(r, "", cred),
IsOwner: owner, IsOwner: owner,
Claims: cred.Claims, Claims: cred.Claims,
@@ -1148,10 +1149,10 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re
// since this is a delete call we shall allow it to be deleted if possible. // since this is a delete call we shall allow it to be deleted if possible.
svcAccount, _, _ := globalIAMSys.GetServiceAccount(ctx, serviceAccount) svcAccount, _, _ := globalIAMSys.GetServiceAccount(ctx, serviceAccount)
adminPrivilege := globalIAMSys.IsAllowed(iampolicy.Args{ adminPrivilege := globalIAMSys.IsAllowed(policy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
Groups: cred.Groups, Groups: cred.Groups,
Action: iampolicy.RemoveServiceAccountAdminAction, Action: policy.RemoveServiceAccountAdminAction,
ConditionValues: getConditionValues(r, "", cred), ConditionValues: getConditionValues(r, "", cred),
IsOwner: owner, IsOwner: owner,
Claims: cred.Claims, Claims: cred.Claims,
@@ -1192,7 +1193,7 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re
writeSuccessNoContent(w) writeSuccessNoContent(w)
} }
// AccountInfoHandler returns usage // AccountInfoHandler returns usage, permissions and other bucket metadata for incoming us
func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@@ -1219,10 +1220,10 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
enablePrefixUsage := r.Form.Get("prefix-usage") == "true" enablePrefixUsage := r.Form.Get("prefix-usage") == "true"
isAllowedAccess := func(bucketName string) (rd, wr bool) { isAllowedAccess := func(bucketName string) (rd, wr bool) {
if globalIAMSys.IsAllowed(iampolicy.Args{ if globalIAMSys.IsAllowed(policy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
Groups: cred.Groups, Groups: cred.Groups,
Action: iampolicy.ListBucketAction, Action: policy.ListBucketAction,
BucketName: bucketName, BucketName: bucketName,
ConditionValues: getConditionValues(r, "", cred), ConditionValues: getConditionValues(r, "", cred),
IsOwner: owner, IsOwner: owner,
@@ -1232,10 +1233,10 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
rd = true rd = true
} }
if globalIAMSys.IsAllowed(iampolicy.Args{ if globalIAMSys.IsAllowed(policy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
Groups: cred.Groups, Groups: cred.Groups,
Action: iampolicy.GetBucketLocationAction, Action: policy.GetBucketLocationAction,
BucketName: bucketName, BucketName: bucketName,
ConditionValues: getConditionValues(r, "", cred), ConditionValues: getConditionValues(r, "", cred),
IsOwner: owner, IsOwner: owner,
@@ -1245,10 +1246,10 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
rd = true rd = true
} }
if globalIAMSys.IsAllowed(iampolicy.Args{ if globalIAMSys.IsAllowed(policy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
Groups: cred.Groups, Groups: cred.Groups,
Action: iampolicy.PutObjectAction, Action: policy.PutObjectAction,
BucketName: bucketName, BucketName: bucketName,
ConditionValues: getConditionValues(r, "", cred), ConditionValues: getConditionValues(r, "", cred),
IsOwner: owner, IsOwner: owner,
@@ -1261,12 +1262,30 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
return rd, wr return rd, wr
} }
// Load the latest calculated data usage bucketStorageCache.Once.Do(func() {
dataUsageInfo, _ := loadDataUsageFromBackend(ctx, objectAPI) // Set this to 10 secs since its enough, as scanner
// does not update the bucket usage values frequently.
bucketStorageCache.TTL = 10 * time.Second
// Rely on older value if usage loading fails from disk.
bucketStorageCache.Relax = true
bucketStorageCache.Update = func() (interface{}, error) {
ctx, done := context.WithTimeout(context.Background(), 2*time.Second)
defer done()
return loadDataUsageFromBackend(ctx, objectAPI)
}
})
var dataUsageInfo DataUsageInfo
v, _ := bucketStorageCache.Get()
if v != nil {
dataUsageInfo, _ = v.(DataUsageInfo)
}
// If etcd, dns federation configured list buckets from etcd. // If etcd, dns federation configured list buckets from etcd.
var buckets []BucketInfo
var err error var err error
var buckets []BucketInfo
if globalDNSConfig != nil && globalBucketFederation { if globalDNSConfig != nil && globalBucketFederation {
dnsBuckets, err := globalDNSConfig.List() dnsBuckets, err := globalDNSConfig.List()
if err != nil && !IsErrIgnored(err, if err != nil && !IsErrIgnored(err,
@@ -1285,7 +1304,7 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
return buckets[i].Name < buckets[j].Name return buckets[i].Name < buckets[j].Name
}) })
} else { } else {
buckets, err = objectAPI.ListBuckets(ctx, BucketOptions{}) buckets, err = objectAPI.ListBuckets(ctx, BucketOptions{Cached: true})
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return return
@@ -1298,14 +1317,14 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
accountName = cred.ParentUser accountName = cred.ParentUser
} }
roleArn := iampolicy.Args{Claims: cred.Claims}.GetRoleArn() roleArn := policy.Args{Claims: cred.Claims}.GetRoleArn()
policySetFromClaims, hasPolicyClaim := iampolicy.GetPoliciesFromClaims(cred.Claims, iamPolicyClaimNameOpenID()) policySetFromClaims, hasPolicyClaim := policy.GetPoliciesFromClaims(cred.Claims, iamPolicyClaimNameOpenID())
var effectivePolicy iampolicy.Policy var effectivePolicy policy.Policy
var buf []byte var buf []byte
switch { switch {
case accountName == globalActiveCred.AccessKey: case accountName == globalActiveCred.AccessKey:
for _, policy := range iampolicy.DefaultPolicies { for _, policy := range policy.DefaultPolicies {
if policy.Name == "consoleAdmin" { if policy.Name == "consoleAdmin" {
effectivePolicy = policy.Definition effectivePolicy = policy.Definition
break break
@@ -1417,7 +1436,7 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.GetPolicyAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.GetPolicyAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -1463,7 +1482,7 @@ func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Reques
func (a adminAPIHandlers) ListBucketPolicies(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) ListBucketPolicies(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ListUserPoliciesAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.ListUserPoliciesAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -1475,7 +1494,7 @@ func (a adminAPIHandlers) ListBucketPolicies(w http.ResponseWriter, r *http.Requ
return return
} }
newPolicies := make(map[string]iampolicy.Policy) newPolicies := make(map[string]policy.Policy)
for name, p := range policies { for name, p := range policies {
_, err = json.Marshal(p) _, err = json.Marshal(p)
if err != nil { if err != nil {
@@ -1494,7 +1513,7 @@ func (a adminAPIHandlers) ListBucketPolicies(w http.ResponseWriter, r *http.Requ
func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ListUserPoliciesAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.ListUserPoliciesAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -1505,7 +1524,7 @@ func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Requ
return return
} }
newPolicies := make(map[string]iampolicy.Policy) newPolicies := make(map[string]policy.Policy)
for name, p := range policies { for name, p := range policies {
_, err = json.Marshal(p) _, err = json.Marshal(p)
if err != nil { if err != nil {
@@ -1524,7 +1543,7 @@ func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Requ
func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.DeletePolicyAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.DeletePolicyAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -1550,7 +1569,7 @@ func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Requ
func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.CreatePolicyAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.CreatePolicyAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -1582,7 +1601,7 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request
return return
} }
iamPolicy, err := iampolicy.ParseConfig(bytes.NewReader(iamPolicyBytes)) iamPolicy, err := policy.ParseConfig(bytes.NewReader(iamPolicyBytes))
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return return
@@ -1614,7 +1633,7 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request
func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.AttachPolicyAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.AttachPolicyAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -1688,7 +1707,7 @@ func (a adminAPIHandlers) ListPolicyMappingEntities(w http.ResponseWriter, r *ht
// Check authorization. // Check authorization.
objectAPI, cred := validateAdminReq(ctx, w, r, objectAPI, cred := validateAdminReq(ctx, w, r,
iampolicy.ListGroupsAdminAction, iampolicy.ListUsersAdminAction, iampolicy.ListUserPoliciesAdminAction) policy.ListGroupsAdminAction, policy.ListUsersAdminAction, policy.ListUserPoliciesAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -1726,8 +1745,8 @@ func (a adminAPIHandlers) ListPolicyMappingEntities(w http.ResponseWriter, r *ht
func (a adminAPIHandlers) AttachDetachPolicyBuiltin(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) AttachDetachPolicyBuiltin(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.UpdatePolicyAssociationAction, objectAPI, cred := validateAdminReq(ctx, w, r, policy.UpdatePolicyAssociationAction,
iampolicy.AttachPolicyAdminAction) policy.AttachPolicyAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -1826,7 +1845,7 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
// Get current object layer instance. // Get current object layer instance.
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ExportIAMAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.ExportIAMAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -2096,7 +2115,7 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
return return
default: default:
defer f.Close() defer f.Close()
var allPolicies map[string]iampolicy.Policy var allPolicies map[string]policy.Policy
data, err = io.ReadAll(f) data, err = io.ReadAll(f)
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allPoliciesFile, ""), r.URL) writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allPoliciesFile, ""), r.URL)
@@ -2177,10 +2196,10 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
checkDenyOnly = true checkDenyOnly = true
} }
if !globalIAMSys.IsAllowed(iampolicy.Args{ if !globalIAMSys.IsAllowed(policy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
Groups: cred.Groups, Groups: cred.Groups,
Action: iampolicy.CreateUserAdminAction, Action: policy.CreateUserAdminAction,
ConditionValues: getConditionValues(r, "", cred), ConditionValues: getConditionValues(r, "", cred),
IsOwner: owner, IsOwner: owner,
Claims: cred.Claims, Claims: cred.Claims,
@@ -2257,10 +2276,10 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
return return
} }
for user, svcAcctReq := range serviceAcctReqs { for user, svcAcctReq := range serviceAcctReqs {
var sp *iampolicy.Policy var sp *policy.Policy
var err error var err error
if len(svcAcctReq.SessionPolicy) > 0 { if len(svcAcctReq.SessionPolicy) > 0 {
sp, err = iampolicy.ParseConfig(bytes.NewReader(svcAcctReq.SessionPolicy)) sp, err = policy.ParseConfig(bytes.NewReader(svcAcctReq.SessionPolicy))
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, user), r.URL) writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, user), r.URL)
return return
@@ -2271,10 +2290,10 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
return return
} }
if !globalIAMSys.IsAllowed(iampolicy.Args{ if !globalIAMSys.IsAllowed(policy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
Groups: cred.Groups, Groups: cred.Groups,
Action: iampolicy.CreateServiceAccountAdminAction, Action: policy.CreateServiceAccountAdminAction,
ConditionValues: getConditionValues(r, "", cred), ConditionValues: getConditionValues(r, "", cred),
IsOwner: owner, IsOwner: owner,
Claims: cred.Claims, Claims: cred.Claims,
+47 -41
View File
@@ -57,7 +57,7 @@ import (
"github.com/minio/mux" "github.com/minio/mux"
"github.com/minio/pkg/v2/logger/message/log" "github.com/minio/pkg/v2/logger/message/log"
xnet "github.com/minio/pkg/v2/net" xnet "github.com/minio/pkg/v2/net"
iampolicy "github.com/minio/pkg/v2/policy" "github.com/minio/pkg/v2/policy"
"github.com/secure-io/sio-go" "github.com/secure-io/sio-go"
) )
@@ -81,7 +81,7 @@ const (
func (a adminAPIHandlers) ServerUpdateHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) ServerUpdateHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ServerUpdateAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.ServerUpdateAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -252,11 +252,11 @@ func (a adminAPIHandlers) ServiceHandler(w http.ResponseWriter, r *http.Request)
var objectAPI ObjectLayer var objectAPI ObjectLayer
switch serviceSig { switch serviceSig {
case serviceRestart: case serviceRestart:
objectAPI, _ = validateAdminReq(ctx, w, r, iampolicy.ServiceRestartAdminAction) objectAPI, _ = validateAdminReq(ctx, w, r, policy.ServiceRestartAdminAction)
case serviceStop: case serviceStop:
objectAPI, _ = validateAdminReq(ctx, w, r, iampolicy.ServiceStopAdminAction) objectAPI, _ = validateAdminReq(ctx, w, r, policy.ServiceStopAdminAction)
case serviceFreeze, serviceUnFreeze: case serviceFreeze, serviceUnFreeze:
objectAPI, _ = validateAdminReq(ctx, w, r, iampolicy.ServiceFreezeAdminAction) objectAPI, _ = validateAdminReq(ctx, w, r, policy.ServiceFreezeAdminAction)
} }
if objectAPI == nil { if objectAPI == nil {
return return
@@ -331,7 +331,7 @@ type ServerHTTPStats struct {
func (a adminAPIHandlers) StorageInfoHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) StorageInfoHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.StorageInfoAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.StorageInfoAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -370,7 +370,7 @@ func (a adminAPIHandlers) StorageInfoHandler(w http.ResponseWriter, r *http.Requ
func (a adminAPIHandlers) MetricsHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) MetricsHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ServerInfoAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.ServerInfoAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -479,7 +479,7 @@ func (a adminAPIHandlers) MetricsHandler(w http.ResponseWriter, r *http.Request)
func (a adminAPIHandlers) DataUsageInfoHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) DataUsageInfoHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.DataUsageInfoAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.DataUsageInfoAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -562,7 +562,7 @@ type PeerLocks struct {
func (a adminAPIHandlers) ForceUnlockHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) ForceUnlockHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ForceUnlockAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.ForceUnlockAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -597,7 +597,7 @@ func (a adminAPIHandlers) ForceUnlockHandler(w http.ResponseWriter, r *http.Requ
func (a adminAPIHandlers) TopLocksHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) TopLocksHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.TopLocksAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.TopLocksAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -648,7 +648,7 @@ func (a adminAPIHandlers) StartProfilingHandler(w http.ResponseWriter, r *http.R
ctx := r.Context() ctx := r.Context()
// Validate request signature. // Validate request signature.
_, adminAPIErr := checkAdminRequestAuth(ctx, r, iampolicy.ProfilingAdminAction, "") _, adminAPIErr := checkAdminRequestAuth(ctx, r, policy.ProfilingAdminAction, "")
if adminAPIErr != ErrNone { if adminAPIErr != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
return return
@@ -733,7 +733,7 @@ func (a adminAPIHandlers) ProfileHandler(w http.ResponseWriter, r *http.Request)
ctx := r.Context() ctx := r.Context()
// Validate request signature. // Validate request signature.
_, adminAPIErr := checkAdminRequestAuth(ctx, r, iampolicy.ProfilingAdminAction, "") _, adminAPIErr := checkAdminRequestAuth(ctx, r, policy.ProfilingAdminAction, "")
if adminAPIErr != ErrNone { if adminAPIErr != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
return return
@@ -828,7 +828,7 @@ func (a adminAPIHandlers) DownloadProfilingHandler(w http.ResponseWriter, r *htt
ctx := r.Context() ctx := r.Context()
// Validate request signature. // Validate request signature.
_, adminAPIErr := checkAdminRequestAuth(ctx, r, iampolicy.ProfilingAdminAction, "") _, adminAPIErr := checkAdminRequestAuth(ctx, r, policy.ProfilingAdminAction, "")
if adminAPIErr != ErrNone { if adminAPIErr != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
return return
@@ -926,7 +926,7 @@ func extractHealInitParams(vars map[string]string, qParms url.Values, r io.Reade
func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.HealAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -1110,7 +1110,7 @@ func getAggregatedBackgroundHealState(ctx context.Context, o ObjectLayer) (madmi
func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.HealAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -1131,7 +1131,7 @@ func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r *
func (a adminAPIHandlers) SitePerfHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) SitePerfHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealthInfoAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.HealthInfoAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -1179,7 +1179,7 @@ func (a adminAPIHandlers) SitePerfHandler(w http.ResponseWriter, r *http.Request
func (a adminAPIHandlers) ClientDevNullExtraTime(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) ClientDevNullExtraTime(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.BandwidthMonitorAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.BandwidthMonitorAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -1196,7 +1196,7 @@ func (a adminAPIHandlers) ClientDevNull(w http.ResponseWriter, r *http.Request)
ctx := r.Context() ctx := r.Context()
timeStart := time.Now() timeStart := time.Now()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.BandwidthMonitorAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.BandwidthMonitorAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -1238,7 +1238,7 @@ func (a adminAPIHandlers) ClientDevNull(w http.ResponseWriter, r *http.Request)
func (a adminAPIHandlers) NetperfHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) NetperfHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealthInfoAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.HealthInfoAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -1284,7 +1284,7 @@ func (a adminAPIHandlers) NetperfHandler(w http.ResponseWriter, r *http.Request)
func (a adminAPIHandlers) ObjectSpeedTestHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) ObjectSpeedTestHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealthInfoAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.HealthInfoAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -1445,7 +1445,7 @@ func validateObjPerfOptions(ctx context.Context, storageInfo madmin.StorageInfo,
func (a adminAPIHandlers) DriveSpeedtestHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) DriveSpeedtestHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealthInfoAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.HealthInfoAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -1566,7 +1566,7 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
// Validate request signature. // Validate request signature.
_, adminAPIErr := checkAdminRequestAuth(ctx, r, iampolicy.TraceAdminAction, "") _, adminAPIErr := checkAdminRequestAuth(ctx, r, policy.TraceAdminAction, "")
if adminAPIErr != ErrNone { if adminAPIErr != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
return return
@@ -1581,7 +1581,9 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
// Trace Publisher and peer-trace-client uses nonblocking send and hence does not wait for slow receivers. // Trace Publisher and peer-trace-client uses nonblocking send and hence does not wait for slow receivers.
// Use buffered channel to take care of burst sends or slow w.Write() // Use buffered channel to take care of burst sends or slow w.Write()
traceCh := make(chan madmin.TraceInfo, 4000)
// Keep 100k buffered channel, should be sufficient to ensure we do not lose any events.
traceCh := make(chan madmin.TraceInfo, 100000)
peers, _ := newPeerRestClients(globalEndpoints) peers, _ := newPeerRestClients(globalEndpoints)
@@ -1605,7 +1607,7 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
peer.Trace(traceCh, ctx.Done(), traceOpts) peer.Trace(traceCh, ctx.Done(), traceOpts)
} }
keepAliveTicker := time.NewTicker(500 * time.Millisecond) keepAliveTicker := time.NewTicker(time.Second)
defer keepAliveTicker.Stop() defer keepAliveTicker.Stop()
enc := json.NewEncoder(w) enc := json.NewEncoder(w)
@@ -1637,7 +1639,7 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ConsoleLogAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.ConsoleLogAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -1718,7 +1720,7 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque
func (a adminAPIHandlers) KMSCreateKeyHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) KMSCreateKeyHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSCreateKeyAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSCreateKeyAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -1739,7 +1741,7 @@ func (a adminAPIHandlers) KMSCreateKeyHandler(w http.ResponseWriter, r *http.Req
func (a adminAPIHandlers) KMSStatusHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) KMSStatusHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSKeyStatusAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSKeyStatusAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -1776,7 +1778,7 @@ func (a adminAPIHandlers) KMSStatusHandler(w http.ResponseWriter, r *http.Reques
func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSKeyStatusAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSKeyStatusAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -2376,7 +2378,7 @@ func fetchHealthInfo(healthCtx context.Context, objectAPI ObjectLayer, query *ur
func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealthInfoAdminAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.HealthInfoAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -2483,7 +2485,7 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
ctx := r.Context() ctx := r.Context()
// Validate request signature. // Validate request signature.
_, adminAPIErr := checkAdminRequestAuth(ctx, r, iampolicy.ServerInfoAdminAction, "") _, adminAPIErr := checkAdminRequestAuth(ctx, r, policy.ServerInfoAdminAction, "")
if adminAPIErr != ErrNone { if adminAPIErr != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
return return
@@ -2627,12 +2629,12 @@ func fetchLoggerInfo() ([]madmin.Logger, []madmin.Audit) {
return loggerInfo, auditloggerInfo return loggerInfo, auditloggerInfo
} }
func embedFileInZip(zipWriter *zip.Writer, name string, data []byte) error { func embedFileInZip(zipWriter *zip.Writer, name string, data []byte, fileMode os.FileMode) error {
// Send profiling data to zip as file // Send profiling data to zip as file
header, zerr := zip.FileInfoHeader(dummyFileInfo{ header, zerr := zip.FileInfoHeader(dummyFileInfo{
name: name, name: name,
size: int64(len(data)), size: int64(len(data)),
mode: 0o600, mode: fileMode,
modTime: UTCNow(), modTime: UTCNow(),
isDir: false, isDir: false,
sys: nil, sys: nil,
@@ -2735,7 +2737,7 @@ func (a adminAPIHandlers) InspectDataHandler(w http.ResponseWriter, r *http.Requ
ctx := r.Context() ctx := r.Context()
// Validate request signature. // Validate request signature.
_, adminAPIErr := checkAdminRequestAuth(ctx, r, iampolicy.InspectDataAction, "") _, adminAPIErr := checkAdminRequestAuth(ctx, r, policy.InspectDataAction, "")
if adminAPIErr != ErrNone { if adminAPIErr != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
return return
@@ -2863,7 +2865,7 @@ func (a adminAPIHandlers) InspectDataHandler(w http.ResponseWriter, r *http.Requ
defer inspectZipW.Close() defer inspectZipW.Close()
if b := getClusterMetaInfo(ctx); len(b) > 0 { if b := getClusterMetaInfo(ctx); len(b) > 0 {
logger.LogIf(ctx, embedFileInZip(inspectZipW, "cluster.info", b)) logger.LogIf(ctx, embedFileInZip(inspectZipW, "cluster.info", b, 0o600))
} }
} }
@@ -2927,11 +2929,16 @@ func (a adminAPIHandlers) InspectDataHandler(w http.ResponseWriter, r *http.Requ
sb.WriteString(pool.CmdLine) sb.WriteString(pool.CmdLine)
} }
sb.WriteString("\n") sb.WriteString("\n")
logger.LogIf(ctx, embedFileInZip(inspectZipW, "inspect-input.txt", sb.Bytes())) logger.LogIf(ctx, embedFileInZip(inspectZipW, "inspect-input.txt", sb.Bytes(), 0o600))
scheme := "https"
if !globalIsTLS {
scheme = "http"
}
// save MinIO start script to inspect command // save MinIO start script to inspect command
var scrb bytes.Buffer var scrb bytes.Buffer
scrb.WriteString(`#!/usr/bin/env bash fmt.Fprintf(&scrb, `#!/usr/bin/env bash
function main() { function main() {
for file in $(ls -1); do for file in $(ls -1); do
@@ -2940,10 +2947,10 @@ function main() {
done done
# Read content of inspect-input.txt # Read content of inspect-input.txt
MINIO_OPTS=$(grep "Server command line args" <./inspect-input.txt | sed "s/Server command line args: //g" | sed -r "s#https:\/\/#\.\/#g") MINIO_OPTS=$(grep "Server command line args" <./inspect-input.txt | sed "s/Server command line args: //g" | sed -r "s#%s:\/\/#\.\/#g")
# Start MinIO instance using the options # Start MinIO instance using the options
START_CMD="CI=on MINIO_ROOT_USER=minio MINIO_ROOT_PASSWORD=minio123 minio server ${MINIO_OPTS} &" START_CMD="CI=on _MINIO_AUTO_DRIVE_HEALING=off minio server ${MINIO_OPTS} &"
echo echo
echo "Starting MinIO instance: ${START_CMD}" echo "Starting MinIO instance: ${START_CMD}"
echo echo
@@ -2955,9 +2962,8 @@ function main() {
sleep 10 sleep 10
} }
main "$@"`, main "$@"`, scheme)
) logger.LogIf(ctx, embedFileInZip(inspectZipW, "start-minio.sh", scrb.Bytes(), 0o755))
logger.LogIf(ctx, embedFileInZip(inspectZipW, "start-minio.sh", scrb.Bytes()))
} }
func getSubnetAdminPublicKey() []byte { func getSubnetAdminPublicKey() []byte {
-7
View File
@@ -683,13 +683,6 @@ func (h *healSequence) healSequenceStart(objAPI ObjectLayer) {
} }
} }
func (h *healSequence) logHeal(healType madmin.HealItemType) {
h.mutex.Lock()
h.scannedItemsMap[healType]++
h.lastHealActivity = UTCNow()
h.mutex.Unlock()
}
func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItemType) error { func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItemType) error {
// Send heal request // Send heal request
task := healTask{ task := healTask{
-2
View File
@@ -2236,8 +2236,6 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
apiErr = ErrSlowDownWrite apiErr = ErrSlowDownWrite
case InsufficientReadQuorum: case InsufficientReadQuorum:
apiErr = ErrSlowDownRead apiErr = ErrSlowDownRead
case InvalidMarkerPrefixCombination:
apiErr = ErrNotImplemented
case InvalidUploadIDKeyCombination: case InvalidUploadIDKeyCombination:
apiErr = ErrNotImplemented apiErr = ErrNotImplemented
case MalformedUploadID: case MalformedUploadID:
-1
View File
@@ -42,7 +42,6 @@ var toAPIErrorTests = []struct {
{err: InvalidPart{}, errCode: ErrInvalidPart}, {err: InvalidPart{}, errCode: ErrInvalidPart},
{err: InsufficientReadQuorum{}, errCode: ErrSlowDownRead}, {err: InsufficientReadQuorum{}, errCode: ErrSlowDownRead},
{err: InsufficientWriteQuorum{}, errCode: ErrSlowDownWrite}, {err: InsufficientWriteQuorum{}, errCode: ErrSlowDownWrite},
{err: InvalidMarkerPrefixCombination{}, errCode: ErrNotImplemented},
{err: InvalidUploadIDKeyCombination{}, errCode: ErrNotImplemented}, {err: InvalidUploadIDKeyCombination{}, errCode: ErrNotImplemented},
{err: MalformedUploadID{}, errCode: ErrNoSuchUpload}, {err: MalformedUploadID{}, errCode: ErrNoSuchUpload},
{err: PartTooSmall{}, errCode: ErrEntityTooSmall}, {err: PartTooSmall{}, errCode: ErrEntityTooSmall},
+1 -1
View File
@@ -567,7 +567,7 @@ func isReqAuthenticated(ctx context.Context, r *http.Request, region string, sty
// Verify 'Content-Md5' and/or 'X-Amz-Content-Sha256' if present. // Verify 'Content-Md5' and/or 'X-Amz-Content-Sha256' if present.
// The verification happens implicit during reading. // The verification happens implicit during reading.
reader, err := hash.NewReader(r.Body, -1, clientETag.String(), hex.EncodeToString(contentSHA256), -1) reader, err := hash.NewReader(ctx, r.Body, -1, clientETag.String(), hex.EncodeToString(contentSHA256), -1)
if err != nil { if err != nil {
return toAPIErrorCode(ctx, err) return toAPIErrorCode(ctx, err)
} }
+2 -2
View File
@@ -28,7 +28,7 @@ import (
"time" "time"
"github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/auth"
iampolicy "github.com/minio/pkg/v2/policy" "github.com/minio/pkg/v2/policy"
) )
type nullReader struct{} type nullReader struct{}
@@ -443,7 +443,7 @@ func TestCheckAdminRequestAuthType(t *testing.T) {
{Request: mustNewPresignedRequest(http.MethodGet, "http://127.0.0.1:9000", 0, nil, t), ErrCode: ErrAccessDenied}, {Request: mustNewPresignedRequest(http.MethodGet, "http://127.0.0.1:9000", 0, nil, t), ErrCode: ErrAccessDenied},
} }
for i, testCase := range testCases { for i, testCase := range testCases {
if _, s3Error := checkAdminRequestAuth(ctx, testCase.Request, iampolicy.AllAdminActions, globalSite.Region); s3Error != testCase.ErrCode { if _, s3Error := checkAdminRequestAuth(ctx, testCase.Request, policy.AllAdminActions, globalSite.Region); s3Error != testCase.ErrCode {
t.Errorf("Test %d: Unexpected s3error returned wanted %d, got %d", i, testCase.ErrCode, s3Error) t.Errorf("Test %d: Unexpected s3error returned wanted %d, got %d", i, testCase.ErrCode, s3Error)
} }
} }
+1 -1
View File
@@ -347,7 +347,7 @@ func initAutoHeal(ctx context.Context, objAPI ObjectLayer) {
globalBackgroundHealState.pushHealLocalDisks(getLocalDisksToHeal()...) globalBackgroundHealState.pushHealLocalDisks(getLocalDisksToHeal()...)
if env.Get("_MINIO_AUTO_DISK_HEALING", config.EnableOn) == config.EnableOn { if env.Get("_MINIO_AUTO_DRIVE_HEALING", config.EnableOn) == config.EnableOn || env.Get("_MINIO_AUTO_DISK_HEALING", config.EnableOn) == config.EnableOn {
go monitorLocalDisksAndHeal(ctx, z) go monitorLocalDisksAndHeal(ctx, z)
} }
} }
+59 -35
View File
@@ -28,7 +28,6 @@ import (
"math/rand" "math/rand"
"net/http" "net/http"
"net/url" "net/url"
"path"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
@@ -38,6 +37,7 @@ import (
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/lithammer/shortuuid/v4" "github.com/lithammer/shortuuid/v4"
"github.com/minio/madmin-go/v3" "github.com/minio/madmin-go/v3"
"github.com/minio/minio-go/v7"
miniogo "github.com/minio/minio-go/v7" miniogo "github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials" "github.com/minio/minio-go/v7/pkg/credentials"
"github.com/minio/minio-go/v7/pkg/encrypt" "github.com/minio/minio-go/v7/pkg/encrypt"
@@ -49,7 +49,7 @@ import (
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/minio/pkg/v2/console" "github.com/minio/pkg/v2/console"
"github.com/minio/pkg/v2/env" "github.com/minio/pkg/v2/env"
iampolicy "github.com/minio/pkg/v2/policy" "github.com/minio/pkg/v2/policy"
"github.com/minio/pkg/v2/workers" "github.com/minio/pkg/v2/workers"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@@ -114,7 +114,7 @@ func (r *BatchJobReplicateV1) ReplicateFromSource(ctx context.Context, api Objec
srcObject := srcObjInfo.Name srcObject := srcObjInfo.Name
tgtObject := srcObjInfo.Name tgtObject := srcObjInfo.Name
if r.Target.Prefix != "" { if r.Target.Prefix != "" {
tgtObject = path.Join(r.Target.Prefix, srcObjInfo.Name) tgtObject = pathJoin(r.Target.Prefix, srcObjInfo.Name)
} }
versionID := srcObjInfo.VersionID versionID := srcObjInfo.VersionID
@@ -167,7 +167,7 @@ func (r *BatchJobReplicateV1) ReplicateFromSource(ctx context.Context, api Objec
} }
defer rd.Close() defer rd.Close()
hr, err := hash.NewReader(rd, objInfo.Size, "", "", objInfo.Size) hr, err := hash.NewReader(ctx, rd, objInfo.Size, "", "", objInfo.Size)
if err != nil { if err != nil {
return err return err
} }
@@ -182,7 +182,7 @@ func (r *BatchJobReplicateV1) copyWithMultipartfromSource(ctx context.Context, a
srcObject := srcObjInfo.Name srcObject := srcObjInfo.Name
tgtObject := srcObjInfo.Name tgtObject := srcObjInfo.Name
if r.Target.Prefix != "" { if r.Target.Prefix != "" {
tgtObject = path.Join(r.Target.Prefix, srcObjInfo.Name) tgtObject = pathJoin(r.Target.Prefix, srcObjInfo.Name)
} }
if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 { if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 {
opts.VersionID = "" opts.VersionID = ""
@@ -230,7 +230,7 @@ func (r *BatchJobReplicateV1) copyWithMultipartfromSource(ctx context.Context, a
} }
defer rd.Close() defer rd.Close()
hr, err = hash.NewReader(io.LimitReader(rd, objInfo.Size), objInfo.Size, "", "", objInfo.Size) hr, err = hash.NewReader(ctx, io.LimitReader(rd, objInfo.Size), objInfo.Size, "", "", objInfo.Size)
if err != nil { if err != nil {
return err return err
} }
@@ -270,6 +270,10 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
} }
rnd := rand.New(rand.NewSource(time.Now().UnixNano())) rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
isTags := len(r.Flags.Filter.Tags) != 0
isMetadata := len(r.Flags.Filter.Metadata) != 0
isStorageClassOnly := len(r.Flags.Filter.Metadata) == 1 && strings.EqualFold(r.Flags.Filter.Metadata[0].Key, xhttp.AmzStorageClass)
skip := func(oi ObjectInfo) (ok bool) { skip := func(oi ObjectInfo) (ok bool) {
if r.Flags.Filter.OlderThan > 0 && time.Since(oi.ModTime) < r.Flags.Filter.OlderThan { if r.Flags.Filter.OlderThan > 0 && time.Since(oi.ModTime) < r.Flags.Filter.OlderThan {
// skip all objects that are newer than specified older duration // skip all objects that are newer than specified older duration
@@ -290,7 +294,8 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
// skip all objects that are created after the specified time. // skip all objects that are created after the specified time.
return true return true
} }
if len(r.Flags.Filter.Tags) > 0 {
if isTags {
// Only parse object tags if tags filter is specified. // Only parse object tags if tags filter is specified.
tagMap := map[string]string{} tagMap := map[string]string{}
tagStr := oi.UserTags tagStr := oi.UserTags
@@ -313,23 +318,19 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
return false return false
} }
if len(r.Flags.Filter.Metadata) > 0 { for _, kv := range r.Flags.Filter.Metadata {
for _, kv := range r.Flags.Filter.Metadata { for k, v := range oi.UserDefined {
for k, v := range oi.UserDefined { if !stringsHasPrefixFold(k, "x-amz-meta-") && !isStandardHeader(k) {
if !stringsHasPrefixFold(k, "x-amz-meta-") && !isStandardHeader(k) { continue
continue }
} // We only need to match x-amz-meta or standardHeaders
// We only need to match x-amz-meta or standardHeaders if kv.Match(BatchJobKV{Key: k, Value: v}) {
if kv.Match(BatchJobKV{Key: k, Value: v}) { return true
return true
}
} }
} }
// None of the provided metadata filters match skip the object.
return false
} }
// None of the provided filters match
return false return false
} }
@@ -384,17 +385,32 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
for obj := range objInfoCh { for obj := range objInfoCh {
oi := toObjectInfo(r.Source.Bucket, obj.Key, obj) oi := toObjectInfo(r.Source.Bucket, obj.Key, obj)
if !minioSrc { if !minioSrc {
oi2, err := c.StatObject(ctx, r.Source.Bucket, obj.Key, miniogo.StatObjectOptions{}) // Check if metadata filter was requested and it is expected to have
if err == nil { // all user metadata or just storageClass. If its only storageClass
oi = toObjectInfo(r.Source.Bucket, obj.Key, oi2) // List() already returns relevant information for filter to be applied.
} else { if isMetadata && !isStorageClassOnly {
if isErrMethodNotAllowed(ErrorRespToObjectError(err, r.Source.Bucket, obj.Key)) || oi2, err := c.StatObject(ctx, r.Source.Bucket, obj.Key, miniogo.StatObjectOptions{})
isErrObjectNotFound(ErrorRespToObjectError(err, r.Source.Bucket, obj.Key)) { if err == nil {
oi = toObjectInfo(r.Source.Bucket, obj.Key, oi2)
} else {
if !isErrMethodNotAllowed(ErrorRespToObjectError(err, r.Source.Bucket, obj.Key)) &&
!isErrObjectNotFound(ErrorRespToObjectError(err, r.Source.Bucket, obj.Key)) {
logger.LogIf(ctx, err)
}
continue
}
}
if isTags {
tags, err := c.GetObjectTagging(ctx, r.Source.Bucket, obj.Key, minio.GetObjectTaggingOptions{})
if err == nil {
oi.UserTags = tags.String()
} else {
if !isErrMethodNotAllowed(ErrorRespToObjectError(err, r.Source.Bucket, obj.Key)) &&
!isErrObjectNotFound(ErrorRespToObjectError(err, r.Source.Bucket, obj.Key)) {
logger.LogIf(ctx, err)
}
continue continue
} }
logger.LogIf(ctx, err)
cancel()
return err
} }
} }
if skip(oi) { if skip(oi) {
@@ -482,10 +498,12 @@ func toObjectInfo(bucket, object string, objInfo miniogo.ObjectInfo) ObjectInfo
ReplicationStatusInternal: objInfo.ReplicationStatus, ReplicationStatusInternal: objInfo.ReplicationStatus,
UserTags: tags.String(), UserTags: tags.String(),
} }
oi.UserDefined = make(map[string]string, len(objInfo.Metadata)) oi.UserDefined = make(map[string]string, len(objInfo.Metadata))
for k, v := range objInfo.Metadata { for k, v := range objInfo.Metadata {
oi.UserDefined[k] = v[0] oi.UserDefined[k] = v[0]
} }
ce, ok := oi.UserDefined[xhttp.ContentEncoding] ce, ok := oi.UserDefined[xhttp.ContentEncoding]
if !ok { if !ok {
ce, ok = oi.UserDefined[strings.ToLower(xhttp.ContentEncoding)] ce, ok = oi.UserDefined[strings.ToLower(xhttp.ContentEncoding)]
@@ -493,6 +511,12 @@ func toObjectInfo(bucket, object string, objInfo miniogo.ObjectInfo) ObjectInfo
if ok { if ok {
oi.ContentEncoding = ce oi.ContentEncoding = ce
} }
_, ok = oi.UserDefined[xhttp.AmzStorageClass]
if !ok {
oi.UserDefined[xhttp.AmzStorageClass] = objInfo.StorageClass
}
return oi return oi
} }
@@ -822,7 +846,7 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
} }
rnd := rand.New(rand.NewSource(time.Now().UnixNano())) rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
skip := func(info FileInfo) (ok bool) { selectObj := func(info FileInfo) (ok bool) {
if r.Flags.Filter.OlderThan > 0 && time.Since(info.ModTime) < r.Flags.Filter.OlderThan { if r.Flags.Filter.OlderThan > 0 && time.Since(info.ModTime) < r.Flags.Filter.OlderThan {
// skip all objects that are newer than specified older duration // skip all objects that are newer than specified older duration
return false return false
@@ -932,7 +956,7 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
results := make(chan ObjectInfo, 100) results := make(chan ObjectInfo, 100)
if err := api.Walk(ctx, r.Source.Bucket, r.Source.Prefix, results, ObjectOptions{ if err := api.Walk(ctx, r.Source.Bucket, r.Source.Prefix, results, ObjectOptions{
WalkMarker: lastObject, WalkMarker: lastObject,
WalkFilter: skip, WalkFilter: selectObj,
}); err != nil { }); err != nil {
cancel() cancel()
// Do not need to retry if we can't list objects on source. // Do not need to retry if we can't list objects on source.
@@ -1258,7 +1282,7 @@ func batchReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (p
func (a adminAPIHandlers) ListBatchJobs(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) ListBatchJobs(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ListBatchJobsAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.ListBatchJobsAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -1308,7 +1332,7 @@ var errNoSuchJob = errors.New("no such job")
func (a adminAPIHandlers) DescribeBatchJob(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) DescribeBatchJob(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.DescribeBatchJobAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.DescribeBatchJobAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -1343,7 +1367,7 @@ func (a adminAPIHandlers) DescribeBatchJob(w http.ResponseWriter, r *http.Reques
func (a adminAPIHandlers) StartBatchJob(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) StartBatchJob(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, creds := validateAdminReq(ctx, w, r, iampolicy.StartBatchJobAction) objectAPI, creds := validateAdminReq(ctx, w, r, policy.StartBatchJobAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -1397,7 +1421,7 @@ func (a adminAPIHandlers) StartBatchJob(w http.ResponseWriter, r *http.Request)
func (a adminAPIHandlers) CancelBatchJob(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) CancelBatchJob(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.CancelBatchJobAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.CancelBatchJobAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
+2 -2
View File
@@ -1139,7 +1139,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
return return
} }
hashReader, err := hash.NewReader(reader, fileSize, "", "", fileSize) hashReader, err := hash.NewReader(ctx, reader, fileSize, "", "", fileSize)
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
@@ -1254,7 +1254,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
return return
} }
// do not try to verify encrypted content/ // do not try to verify encrypted content/
hashReader, err = hash.NewReader(reader, -1, "", "", -1) hashReader, err = hash.NewReader(ctx, reader, -1, "", "", -1)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
+29 -20
View File
@@ -29,14 +29,12 @@ import (
) )
// BucketQuotaSys - map of bucket and quota configuration. // BucketQuotaSys - map of bucket and quota configuration.
type BucketQuotaSys struct { type BucketQuotaSys struct{}
bucketStorageCache timedValue
}
// Get - Get quota configuration. // Get - Get quota configuration.
func (sys *BucketQuotaSys) Get(ctx context.Context, bucketName string) (*madmin.BucketQuota, error) { func (sys *BucketQuotaSys) Get(ctx context.Context, bucketName string) (*madmin.BucketQuota, error) {
qCfg, _, err := globalBucketMetadataSys.GetQuotaConfig(ctx, bucketName) cfg, _, err := globalBucketMetadataSys.GetQuotaConfig(ctx, bucketName)
return qCfg, err return cfg, err
} }
// NewBucketQuotaSys returns initialized BucketQuotaSys // NewBucketQuotaSys returns initialized BucketQuotaSys
@@ -44,16 +42,18 @@ func NewBucketQuotaSys() *BucketQuotaSys {
return &BucketQuotaSys{} return &BucketQuotaSys{}
} }
var bucketStorageCache timedValue
// Init initialize bucket quota. // Init initialize bucket quota.
func (sys *BucketQuotaSys) Init(objAPI ObjectLayer) { func (sys *BucketQuotaSys) Init(objAPI ObjectLayer) {
sys.bucketStorageCache.Once.Do(func() { bucketStorageCache.Once.Do(func() {
// Set this to 10 secs since its enough, as scanner // Set this to 10 secs since its enough, as scanner
// does not update the bucket usage values frequently. // does not update the bucket usage values frequently.
sys.bucketStorageCache.TTL = 10 * time.Second bucketStorageCache.TTL = 10 * time.Second
// Rely on older value if usage loading fails from disk. // Rely on older value if usage loading fails from disk.
sys.bucketStorageCache.Relax = true bucketStorageCache.Relax = true
sys.bucketStorageCache.Update = func() (interface{}, error) { bucketStorageCache.Update = func() (interface{}, error) {
ctx, done := context.WithTimeout(context.Background(), 1*time.Second) ctx, done := context.WithTimeout(context.Background(), 2*time.Second)
defer done() defer done()
return loadDataUsageFromBackend(ctx, objAPI) return loadDataUsageFromBackend(ctx, objAPI)
@@ -63,16 +63,17 @@ func (sys *BucketQuotaSys) Init(objAPI ObjectLayer) {
// GetBucketUsageInfo return bucket usage info for a given bucket // GetBucketUsageInfo return bucket usage info for a given bucket
func (sys *BucketQuotaSys) GetBucketUsageInfo(bucket string) (BucketUsageInfo, error) { func (sys *BucketQuotaSys) GetBucketUsageInfo(bucket string) (BucketUsageInfo, error) {
v, err := sys.bucketStorageCache.Get() v, err := bucketStorageCache.Get()
if err != nil && v != nil { timedout := OperationTimedOut{}
logger.LogOnceIf(GlobalContext, fmt.Errorf("unable to retrieve usage information for bucket: %s, relying on older value cached in-memory: err(%v)", bucket, err), "bucket-usage-cache-"+bucket) if err != nil && !errors.Is(err, context.DeadlineExceeded) && !errors.As(err, &timedout) {
} if v != nil {
if v == nil { logger.LogOnceIf(GlobalContext, fmt.Errorf("unable to retrieve usage information for bucket: %s, relying on older value cached in-memory: err(%v)", bucket, err), "bucket-usage-cache-"+bucket)
logger.LogOnceIf(GlobalContext, errors.New("unable to retrieve usage information for bucket: %s, no reliable usage value available - quota will not be enforced"), "bucket-usage-empty-"+bucket) } else {
logger.LogOnceIf(GlobalContext, errors.New("unable to retrieve usage information for bucket: %s, no reliable usage value available - quota will not be enforced"), "bucket-usage-empty-"+bucket)
}
} }
var bui BucketUsageInfo var bui BucketUsageInfo
dui, ok := v.(DataUsageInfo) dui, ok := v.(DataUsageInfo)
if ok { if ok {
bui = dui.BucketsUsage[bucket] bui = dui.BucketsUsage[bucket]
@@ -107,8 +108,16 @@ func (sys *BucketQuotaSys) enforceQuotaHard(ctx context.Context, bucket string,
return err return err
} }
if q != nil && q.Type == madmin.HardQuota && q.Quota > 0 { var quotaSize uint64
if uint64(size) >= q.Quota { // check if file size already exceeds the quota if q != nil && q.Type == madmin.HardQuota {
if q.Size > 0 {
quotaSize = q.Size
} else if q.Quota > 0 {
quotaSize = q.Quota
}
}
if quotaSize > 0 {
if uint64(size) >= quotaSize { // check if file size already exceeds the quota
return BucketQuotaExceeded{Bucket: bucket} return BucketQuotaExceeded{Bucket: bucket}
} }
@@ -117,7 +126,7 @@ func (sys *BucketQuotaSys) enforceQuotaHard(ctx context.Context, bucket string,
return err return err
} }
if bui.Size > 0 && ((bui.Size + uint64(size)) >= q.Quota) { if bui.Size > 0 && ((bui.Size + uint64(size)) >= quotaSize) {
return BucketQuotaExceeded{Bucket: bucket} return BucketQuotaExceeded{Bucket: bucket}
} }
} }
+14 -24
View File
@@ -232,10 +232,10 @@ func (api objectAPIHandlers) GetBucketReplicationMetricsHandler(w http.ResponseW
enc := json.NewEncoder(w) enc := json.NewEncoder(w)
stats := globalReplicationStats.getLatestReplicationStats(bucket) stats := globalReplicationStats.getLatestReplicationStats(bucket)
bwRpt := globalNotificationSys.GetBandwidthReports(ctx, bucket) bwRpt := globalNotificationSys.GetBandwidthReports(ctx, bucket)
bwMap := bwRpt.BucketStats[bucket] bwMap := bwRpt.BucketStats
for arn, st := range stats.ReplicationStats.Stats { for arn, st := range stats.ReplicationStats.Stats {
if bwMap != nil { for opts, bw := range bwMap {
if bw, ok := bwMap[arn]; ok { if opts.ReplicationARN != "" && opts.ReplicationARN == arn {
st.BandWidthLimitInBytesPerSecond = bw.LimitInBytesPerSecond st.BandWidthLimitInBytesPerSecond = bw.LimitInBytesPerSecond
st.CurrentBandwidthInBytesPerSecond = bw.CurrentBandwidthInBytesPerSecond st.CurrentBandwidthInBytesPerSecond = bw.CurrentBandwidthInBytesPerSecond
stats.ReplicationStats.Stats[arn] = st stats.ReplicationStats.Stats[arn] = st
@@ -288,10 +288,10 @@ func (api objectAPIHandlers) GetBucketReplicationMetricsV2Handler(w http.Respons
enc := json.NewEncoder(w) enc := json.NewEncoder(w)
stats := globalReplicationStats.getLatestReplicationStats(bucket) stats := globalReplicationStats.getLatestReplicationStats(bucket)
bwRpt := globalNotificationSys.GetBandwidthReports(ctx, bucket) bwRpt := globalNotificationSys.GetBandwidthReports(ctx, bucket)
bwMap := bwRpt.BucketStats[bucket] bwMap := bwRpt.BucketStats
for arn, st := range stats.ReplicationStats.Stats { for arn, st := range stats.ReplicationStats.Stats {
if bwMap != nil { for opts, bw := range bwMap {
if bw, ok := bwMap[arn]; ok { if opts.ReplicationARN != "" && opts.ReplicationARN == arn {
st.BandWidthLimitInBytesPerSecond = bw.LimitInBytesPerSecond st.BandWidthLimitInBytesPerSecond = bw.LimitInBytesPerSecond
st.CurrentBandwidthInBytesPerSecond = bw.CurrentBandwidthInBytesPerSecond st.CurrentBandwidthInBytesPerSecond = bw.CurrentBandwidthInBytesPerSecond
stats.ReplicationStats.Stats[arn] = st stats.ReplicationStats.Stats[arn] = st
@@ -476,27 +476,17 @@ func (api objectAPIHandlers) ResetBucketReplicationStatusHandler(w http.Response
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
} }
var tgtStats map[string]TargetReplicationResyncStatus brs, err := loadBucketResyncMetadata(ctx, bucket, objectAPI)
globalReplicationPool.resyncer.RLock() if err != nil {
brs, ok := globalReplicationPool.resyncer.statusMap[bucket] writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
if ok { Bucket: bucket,
tgtStats = brs.cloneTgtStats() Err: fmt.Errorf("replication resync status not available for %s (%s)", arn, err.Error()),
} }), r.URL)
globalReplicationPool.resyncer.RUnlock() return
if !ok {
brs, err = loadBucketResyncMetadata(ctx, bucket, objectAPI)
if err != nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
Bucket: bucket,
Err: fmt.Errorf("No replication resync status available for %s", arn),
}), r.URL)
return
}
tgtStats = brs.cloneTgtStats()
} }
var rinfo ResyncTargetsInfo var rinfo ResyncTargetsInfo
for tarn, st := range tgtStats { for tarn, st := range brs.TargetsMap {
if arn != "" && tarn != arn { if arn != "" && tarn != arn {
continue continue
} }
+65 -60
View File
@@ -31,8 +31,8 @@ import (
"github.com/minio/madmin-go/v3" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/bucket/replication" "github.com/minio/minio/internal/bucket/replication"
"github.com/minio/minio/internal/crypto"
xhttp "github.com/minio/minio/internal/http" xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/logger"
) )
//go:generate msgp -file=$GOFILE //go:generate msgp -file=$GOFILE
@@ -167,7 +167,21 @@ func (ri replicatedInfos) Action() replicationAction {
var replStatusRegex = regexp.MustCompile(`([^=].*?)=([^,].*?);`) var replStatusRegex = regexp.MustCompile(`([^=].*?)=([^,].*?);`)
// TargetReplicationStatus - returns replication status of a target // TargetReplicationStatus - returns replication status of a target
func (o *ObjectInfo) TargetReplicationStatus(arn string) (status replication.StatusType) { func (ri ReplicateObjectInfo) TargetReplicationStatus(arn string) (status replication.StatusType) {
repStatMatches := replStatusRegex.FindAllStringSubmatch(ri.ReplicationStatusInternal, -1)
for _, repStatMatch := range repStatMatches {
if len(repStatMatch) != 3 {
return
}
if repStatMatch[1] == arn {
return replication.StatusType(repStatMatch[2])
}
}
return
}
// TargetReplicationStatus - returns replication status of a target
func (o ObjectInfo) TargetReplicationStatus(arn string) (status replication.StatusType) {
repStatMatches := replStatusRegex.FindAllStringSubmatch(o.ReplicationStatusInternal, -1) repStatMatches := replStatusRegex.FindAllStringSubmatch(o.ReplicationStatusInternal, -1)
for _, repStatMatch := range repStatMatches { for _, repStatMatch := range repStatMatches {
if len(repStatMatch) != 3 { if len(repStatMatch) != 3 {
@@ -185,7 +199,6 @@ type replicateTargetDecision struct {
Synchronous bool // Synchronous replication configured. Synchronous bool // Synchronous replication configured.
Arn string // ARN of replication target Arn string // ARN of replication target
ID string ID string
Tgt *TargetClient
} }
func (t *replicateTargetDecision) String() string { func (t *replicateTargetDecision) String() string {
@@ -207,7 +220,7 @@ type ReplicateDecision struct {
} }
// ReplicateAny returns true if atleast one target qualifies for replication // ReplicateAny returns true if atleast one target qualifies for replication
func (d *ReplicateDecision) ReplicateAny() bool { func (d ReplicateDecision) ReplicateAny() bool {
for _, t := range d.targetsMap { for _, t := range d.targetsMap {
if t.Replicate { if t.Replicate {
return true return true
@@ -217,7 +230,7 @@ func (d *ReplicateDecision) ReplicateAny() bool {
} }
// Synchronous returns true if atleast one target qualifies for synchronous replication // Synchronous returns true if atleast one target qualifies for synchronous replication
func (d *ReplicateDecision) Synchronous() bool { func (d ReplicateDecision) Synchronous() bool {
for _, t := range d.targetsMap { for _, t := range d.targetsMap {
if t.Synchronous { if t.Synchronous {
return true return true
@@ -226,7 +239,7 @@ func (d *ReplicateDecision) Synchronous() bool {
return false return false
} }
func (d *ReplicateDecision) String() string { func (d ReplicateDecision) String() string {
b := new(bytes.Buffer) b := new(bytes.Buffer)
for key, value := range d.targetsMap { for key, value := range d.targetsMap {
fmt.Fprintf(b, "%s=%s,", key, value.String()) fmt.Fprintf(b, "%s=%s,", key, value.String())
@@ -243,7 +256,7 @@ func (d *ReplicateDecision) Set(t replicateTargetDecision) {
} }
// PendingStatus returns a stringified representation of internal replication status with all targets marked as `PENDING` // PendingStatus returns a stringified representation of internal replication status with all targets marked as `PENDING`
func (d *ReplicateDecision) PendingStatus() string { func (d ReplicateDecision) PendingStatus() string {
b := new(bytes.Buffer) b := new(bytes.Buffer)
for _, k := range d.targetsMap { for _, k := range d.targetsMap {
if k.Replicate { if k.Replicate {
@@ -259,11 +272,11 @@ type ResyncDecision struct {
} }
// Empty returns true if no targets with resync decision present // Empty returns true if no targets with resync decision present
func (r *ResyncDecision) Empty() bool { func (r ResyncDecision) Empty() bool {
return r.targets == nil return r.targets == nil
} }
func (r *ResyncDecision) mustResync() bool { func (r ResyncDecision) mustResync() bool {
for _, v := range r.targets { for _, v := range r.targets {
if v.Replicate { if v.Replicate {
return true return true
@@ -272,15 +285,12 @@ func (r *ResyncDecision) mustResync() bool {
return false return false
} }
func (r *ResyncDecision) mustResyncTarget(tgtArn string) bool { func (r ResyncDecision) mustResyncTarget(tgtArn string) bool {
if r.targets == nil { if r.targets == nil {
return false return false
} }
v, ok := r.targets[tgtArn] v, ok := r.targets[tgtArn]
if ok && v.Replicate { return ok && v.Replicate
return true
}
return false
} }
// ResyncTargetDecision is struct that represents resync decision for this target // ResyncTargetDecision is struct that represents resync decision for this target
@@ -301,35 +311,20 @@ func parseReplicateDecision(ctx context.Context, bucket, s string) (r ReplicateD
if len(s) == 0 { if len(s) == 0 {
return return
} }
pairs := strings.Split(s, ",") for _, p := range strings.Split(s, ",") {
for _, p := range pairs { if p == "" {
continue
}
slc := strings.Split(p, "=") slc := strings.Split(p, "=")
if len(slc) != 2 { if len(slc) != 2 {
return r, errInvalidReplicateDecisionFormat return r, errInvalidReplicateDecisionFormat
} }
tgtStr := strings.TrimPrefix(slc[1], "\"") tgtStr := strings.TrimSuffix(strings.TrimPrefix(slc[1], `"`), `"`)
tgtStr = strings.TrimSuffix(tgtStr, "\"")
tgt := strings.Split(tgtStr, ";") tgt := strings.Split(tgtStr, ";")
if len(tgt) != 4 { if len(tgt) != 4 {
return r, errInvalidReplicateDecisionFormat return r, errInvalidReplicateDecisionFormat
} }
var replicate, sync bool r.targetsMap[slc[0]] = replicateTargetDecision{Replicate: tgt[0] == "true", Synchronous: tgt[1] == "true", Arn: tgt[2], ID: tgt[3]}
var err error
replicate, err = strconv.ParseBool(tgt[0])
if err != nil {
return r, err
}
sync, err = strconv.ParseBool(tgt[1])
if err != nil {
return r, err
}
tgtClnt := globalBucketTargetSys.GetRemoteTargetClient(slc[0])
if tgtClnt == nil {
// Skip stale targets if any and log them to be missing atleast once.
logger.LogOnceIf(ctx, fmt.Errorf("failed to get target for bucket:%s arn:%s", bucket, slc[0]), slc[0])
// We save the targetDecision even when its not configured or stale.
}
r.targetsMap[slc[0]] = replicateTargetDecision{Replicate: replicate, Synchronous: sync, Arn: tgt[2], ID: tgt[3], Tgt: tgtClnt}
} }
return return
} }
@@ -496,8 +491,8 @@ func getCompositeVersionPurgeStatus(m map[string]VersionPurgeStatusType) Version
} }
// getHealReplicateObjectInfo returns info needed by heal replication in ReplicateObjectInfo // getHealReplicateObjectInfo returns info needed by heal replication in ReplicateObjectInfo
func getHealReplicateObjectInfo(objInfo ObjectInfo, rcfg replicationConfig) ReplicateObjectInfo { func getHealReplicateObjectInfo(oi ObjectInfo, rcfg replicationConfig) ReplicateObjectInfo {
oi := objInfo.Clone() userDefined := cloneMSS(oi.UserDefined)
if rcfg.Config != nil && rcfg.Config.RoleArn != "" { if rcfg.Config != nil && rcfg.Config.RoleArn != "" {
// For backward compatibility of objects pending/failed replication. // For backward compatibility of objects pending/failed replication.
// Save replication related statuses in the new internal representation for // Save replication related statuses in the new internal representation for
@@ -508,17 +503,15 @@ func getHealReplicateObjectInfo(objInfo ObjectInfo, rcfg replicationConfig) Repl
if !oi.VersionPurgeStatus.Empty() { if !oi.VersionPurgeStatus.Empty() {
oi.VersionPurgeStatusInternal = fmt.Sprintf("%s=%s;", rcfg.Config.RoleArn, oi.VersionPurgeStatus) oi.VersionPurgeStatusInternal = fmt.Sprintf("%s=%s;", rcfg.Config.RoleArn, oi.VersionPurgeStatus)
} }
for k, v := range oi.UserDefined { for k, v := range userDefined {
if strings.EqualFold(k, ReservedMetadataPrefixLower+ReplicationReset) { if strings.EqualFold(k, ReservedMetadataPrefixLower+ReplicationReset) {
delete(oi.UserDefined, k) delete(userDefined, k)
oi.UserDefined[targetResetHeader(rcfg.Config.RoleArn)] = v userDefined[targetResetHeader(rcfg.Config.RoleArn)] = v
} }
} }
} }
var dsc ReplicateDecision
var tgtStatuses map[string]replication.StatusType
var purgeStatuses map[string]VersionPurgeStatusType
var dsc ReplicateDecision
if oi.DeleteMarker || !oi.VersionPurgeStatus.Empty() { if oi.DeleteMarker || !oi.VersionPurgeStatus.Empty() {
dsc = checkReplicateDelete(GlobalContext, oi.Bucket, ObjectToDelete{ dsc = checkReplicateDelete(GlobalContext, oi.Bucket, ObjectToDelete{
ObjectV: ObjectV{ ObjectV: ObjectV{
@@ -530,33 +523,45 @@ func getHealReplicateObjectInfo(objInfo ObjectInfo, rcfg replicationConfig) Repl
VersionSuspended: globalBucketVersioningSys.PrefixSuspended(oi.Bucket, oi.Name), VersionSuspended: globalBucketVersioningSys.PrefixSuspended(oi.Bucket, oi.Name),
}, nil) }, nil)
} else { } else {
dsc = mustReplicate(GlobalContext, oi.Bucket, oi.Name, getMustReplicateOptions(ObjectInfo{ dsc = mustReplicate(GlobalContext, oi.Bucket, oi.Name, getMustReplicateOptions(userDefined, oi.UserTags, "", replication.HealReplicationType, ObjectOptions{}))
UserDefined: oi.UserDefined,
}, replication.HealReplicationType, ObjectOptions{}))
} }
tgtStatuses = replicationStatusesMap(oi.ReplicationStatusInternal)
purgeStatuses = versionPurgeStatusesMap(oi.VersionPurgeStatusInternal) tgtStatuses := replicationStatusesMap(oi.ReplicationStatusInternal)
existingObjResync := rcfg.Resync(GlobalContext, oi, &dsc, tgtStatuses) purgeStatuses := versionPurgeStatusesMap(oi.VersionPurgeStatusInternal)
tm, _ := time.Parse(time.RFC3339Nano, oi.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp]) existingObjResync := rcfg.Resync(GlobalContext, oi, dsc, tgtStatuses)
tm, _ := time.Parse(time.RFC3339Nano, userDefined[ReservedMetadataPrefixLower+ReplicationTimestamp])
rstate := oi.ReplicationState()
rstate.ReplicateDecisionStr = dsc.String()
asz, _ := oi.GetActualSize()
return ReplicateObjectInfo{ return ReplicateObjectInfo{
ObjectInfo: oi, Name: oi.Name,
Size: oi.Size,
ActualSize: asz,
Bucket: oi.Bucket,
VersionID: oi.VersionID,
ETag: oi.ETag,
ModTime: oi.ModTime,
ReplicationStatus: oi.ReplicationStatus,
ReplicationStatusInternal: oi.ReplicationStatusInternal,
DeleteMarker: oi.DeleteMarker,
VersionPurgeStatusInternal: oi.VersionPurgeStatusInternal,
VersionPurgeStatus: oi.VersionPurgeStatus,
ReplicationState: rstate,
OpType: replication.HealReplicationType, OpType: replication.HealReplicationType,
Dsc: dsc, Dsc: dsc,
ExistingObjResync: existingObjResync, ExistingObjResync: existingObjResync,
TargetStatuses: tgtStatuses, TargetStatuses: tgtStatuses,
TargetPurgeStatuses: purgeStatuses, TargetPurgeStatuses: purgeStatuses,
ReplicationTimestamp: tm, ReplicationTimestamp: tm,
SSEC: crypto.SSEC.IsEncrypted(oi.UserDefined),
UserTags: oi.UserTags,
} }
} }
func (ri *ReplicateObjectInfo) getReplicationState() ReplicationState { // ReplicationState - returns replication state using other internal replication metadata in ObjectInfo
rs := ri.ObjectInfo.getReplicationState() func (o ObjectInfo) ReplicationState() ReplicationState {
rs.ReplicateDecisionStr = ri.Dsc.String()
return rs
}
// vID here represents the versionID client specified in request - need to distinguish between delete marker and delete marker deletion
func (o *ObjectInfo) getReplicationState() ReplicationState {
rs := ReplicationState{ rs := ReplicationState{
ReplicationStatusInternal: o.ReplicationStatusInternal, ReplicationStatusInternal: o.ReplicationStatusInternal,
VersionPurgeStatusInternal: o.VersionPurgeStatusInternal, VersionPurgeStatusInternal: o.VersionPurgeStatusInternal,
@@ -577,7 +582,7 @@ func (o *ObjectInfo) getReplicationState() ReplicationState {
} }
// ReplicationState returns replication state using other internal replication metadata in ObjectToDelete // ReplicationState returns replication state using other internal replication metadata in ObjectToDelete
func (o *ObjectToDelete) ReplicationState() ReplicationState { func (o ObjectToDelete) ReplicationState() ReplicationState {
r := ReplicationState{ r := ReplicationState{
ReplicationStatusInternal: o.DeleteMarkerReplicationStatus, ReplicationStatusInternal: o.DeleteMarkerReplicationStatus,
VersionPurgeStatusInternal: o.VersionPurgeStatuses, VersionPurgeStatusInternal: o.VersionPurgeStatuses,
+324 -253
View File
@@ -49,6 +49,7 @@ import (
"github.com/minio/minio/internal/hash" "github.com/minio/minio/internal/hash"
xhttp "github.com/minio/minio/internal/http" xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/tinylib/msgp/msgp"
"github.com/zeebo/xxh3" "github.com/zeebo/xxh3"
) )
@@ -223,21 +224,19 @@ func (o mustReplicateOptions) isMetadataReplication() bool {
return o.opType == replication.MetadataReplicationType return o.opType == replication.MetadataReplicationType
} }
func getMustReplicateOptions(o ObjectInfo, op replication.Type, opts ObjectOptions) mustReplicateOptions { func (o ObjectInfo) getMustReplicateOptions(op replication.Type, opts ObjectOptions) mustReplicateOptions {
if !op.Valid() { return getMustReplicateOptions(o.UserDefined, o.UserTags, o.ReplicationStatus, op, opts)
op = replication.ObjectReplicationType }
if o.metadataOnly {
op = replication.MetadataReplicationType func getMustReplicateOptions(userDefined map[string]string, userTags string, status replication.StatusType, op replication.Type, opts ObjectOptions) mustReplicateOptions {
} meta := cloneMSS(userDefined)
} if userTags != "" {
meta := cloneMSS(o.UserDefined) meta[xhttp.AmzObjectTagging] = userTags
if o.UserTags != "" {
meta[xhttp.AmzObjectTagging] = o.UserTags
} }
return mustReplicateOptions{ return mustReplicateOptions{
meta: meta, meta: meta,
status: o.ReplicationStatus, status: status,
opType: op, opType: op,
replicationRequest: opts.ReplicationRequest, replicationRequest: opts.ReplicationRequest,
} }
@@ -355,40 +354,43 @@ func checkReplicateDelete(ctx context.Context, bucket string, dobj ObjectToDelet
OpType: replication.DeleteReplicationType, OpType: replication.DeleteReplicationType,
} }
tgtArns := rcfg.FilterTargetArns(opts) tgtArns := rcfg.FilterTargetArns(opts)
if len(tgtArns) > 0 { dsc.targetsMap = make(map[string]replicateTargetDecision, len(tgtArns))
dsc.targetsMap = make(map[string]replicateTargetDecision, len(tgtArns)) if len(tgtArns) == 0 {
var sync, replicate bool return dsc
for _, tgtArn := range tgtArns { }
opts.TargetArn = tgtArn var sync, replicate bool
replicate = rcfg.Replicate(opts) for _, tgtArn := range tgtArns {
// when incoming delete is removal of a delete marker(a.k.a versioned delete), opts.TargetArn = tgtArn
// GetObjectInfo returns extra information even though it returns errFileNotFound replicate = rcfg.Replicate(opts)
if gerr != nil { // when incoming delete is removal of a delete marker(a.k.a versioned delete),
validReplStatus := false // GetObjectInfo returns extra information even though it returns errFileNotFound
switch oi.TargetReplicationStatus(tgtArn) { if gerr != nil {
case replication.Pending, replication.Completed, replication.Failed: validReplStatus := false
validReplStatus = true switch oi.TargetReplicationStatus(tgtArn) {
} case replication.Pending, replication.Completed, replication.Failed:
if oi.DeleteMarker && (validReplStatus || replicate) { validReplStatus = true
dsc.Set(newReplicateTargetDecision(tgtArn, replicate, sync)) }
continue if oi.DeleteMarker && (validReplStatus || replicate) {
} else { dsc.Set(newReplicateTargetDecision(tgtArn, replicate, sync))
// can be the case that other cluster is down and duplicate `mc rm --vid` continue
// is issued - this still needs to be replicated back to the other target } else {
// can be the case that other cluster is down and duplicate `mc rm --vid`
// is issued - this still needs to be replicated back to the other target
if !oi.VersionPurgeStatus.Empty() {
replicate = oi.VersionPurgeStatus == Pending || oi.VersionPurgeStatus == Failed replicate = oi.VersionPurgeStatus == Pending || oi.VersionPurgeStatus == Failed
dsc.Set(newReplicateTargetDecision(tgtArn, replicate, sync)) dsc.Set(newReplicateTargetDecision(tgtArn, replicate, sync))
continue
} }
continue
} }
tgt := globalBucketTargetSys.GetRemoteTargetClient(tgtArn)
// the target online status should not be used here while deciding
// whether to replicate deletes as the target could be temporarily down
tgtDsc := newReplicateTargetDecision(tgtArn, false, false)
if tgt != nil {
tgtDsc = newReplicateTargetDecision(tgtArn, replicate, tgt.replicateSync)
}
dsc.Set(tgtDsc)
} }
tgt := globalBucketTargetSys.GetRemoteTargetClient(tgtArn)
// the target online status should not be used here while deciding
// whether to replicate deletes as the target could be temporarily down
tgtDsc := newReplicateTargetDecision(tgtArn, false, false)
if tgt != nil {
tgtDsc = newReplicateTargetDecision(tgtArn, replicate, tgt.replicateSync)
}
dsc.Set(tgtDsc)
} }
return dsc return dsc
} }
@@ -482,15 +484,10 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj
ctx = lkctx.Context() ctx = lkctx.Context()
defer lk.Unlock(lkctx) defer lk.Unlock(lkctx)
rinfos := replicatedInfos{Targets: make([]replicatedTargetInfo, 0, len(dsc.targetsMap))}
var wg sync.WaitGroup var wg sync.WaitGroup
var rinfos replicatedInfos var mu sync.Mutex
rinfos.Targets = make([]replicatedTargetInfo, len(dsc.targetsMap))
idx := -1
for _, tgtEntry := range dsc.targetsMap { for _, tgtEntry := range dsc.targetsMap {
idx++
if tgtEntry.Tgt == nil {
continue
}
if !tgtEntry.Replicate { if !tgtEntry.Replicate {
continue continue
} }
@@ -498,11 +495,33 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj
if dobj.TargetArn != "" && dobj.TargetArn != tgtEntry.Arn { if dobj.TargetArn != "" && dobj.TargetArn != tgtEntry.Arn {
continue continue
} }
tgtClnt := globalBucketTargetSys.GetRemoteTargetClient(tgtEntry.Arn)
if tgtClnt == nil {
// Skip stale targets if any and log them to be missing atleast once.
logger.LogOnceIf(ctx, fmt.Errorf("failed to get target for bucket:%s arn:%s", bucket, tgtEntry.Arn), tgtEntry.Arn)
sendEvent(eventArgs{
EventName: event.ObjectReplicationNotTracked,
BucketName: bucket,
Object: ObjectInfo{
Bucket: bucket,
Name: dobj.ObjectName,
VersionID: versionID,
DeleteMarker: dobj.DeleteMarker,
},
UserAgent: "Internal: [Replication]",
Host: globalLocalNodeName,
})
continue
}
wg.Add(1) wg.Add(1)
go func(index int, tgt *TargetClient) { go func(tgt *TargetClient) {
defer wg.Done() defer wg.Done()
rinfos.Targets[index] = replicateDeleteToTarget(ctx, dobj, tgt) tgtInfo := replicateDeleteToTarget(ctx, dobj, tgt)
}(idx, tgtEntry.Tgt)
mu.Lock()
rinfos.Targets = append(rinfos.Targets, tgtInfo)
mu.Unlock()
}(tgtClnt)
} }
wg.Wait() wg.Wait()
@@ -702,10 +721,8 @@ func getCopyObjMetadata(oi ObjectInfo, sc string) map[string]string {
meta[xhttp.ContentType] = oi.ContentType meta[xhttp.ContentType] = oi.ContentType
} }
if oi.UserTags != "" { meta[xhttp.AmzObjectTagging] = oi.UserTags
meta[xhttp.AmzObjectTagging] = oi.UserTags meta[xhttp.AmzTagDirective] = "REPLACE"
meta[xhttp.AmzTagDirective] = "REPLACE"
}
if sc == "" { if sc == "" {
sc = oi.StorageClass sc = oi.StorageClass
@@ -887,7 +904,7 @@ func getReplicationAction(oi1 ObjectInfo, oi2 minio.ObjectInfo, opType replicati
} }
t, _ := tags.ParseObjectTags(oi1.UserTags) t, _ := tags.ParseObjectTags(oi1.UserTags)
if !reflect.DeepEqual(oi2.UserTags, t.ToMap()) || (oi2.UserTagCount != len(t.ToMap())) { if (oi2.UserTagCount > 0 && !reflect.DeepEqual(oi2.UserTags, t.ToMap())) || (oi2.UserTagCount != len(t.ToMap())) {
return replicateMetadata return replicateMetadata
} }
@@ -964,9 +981,8 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
}) })
}() }()
objInfo := ri.ObjectInfo bucket := ri.Bucket
bucket := objInfo.Bucket object := ri.Name
object := objInfo.Name
cfg, err := getReplicationConfig(ctx, bucket) cfg, err := getReplicationConfig(ctx, bucket)
if err != nil { if err != nil {
@@ -974,7 +990,7 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
sendEvent(eventArgs{ sendEvent(eventArgs{
EventName: event.ObjectReplicationNotTracked, EventName: event.ObjectReplicationNotTracked,
BucketName: bucket, BucketName: bucket,
Object: objInfo, Object: ri.ToObjectInfo(),
UserAgent: "Internal: [Replication]", UserAgent: "Internal: [Replication]",
Host: globalLocalNodeName, Host: globalLocalNodeName,
}) })
@@ -982,8 +998,8 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
} }
tgtArns := cfg.FilterTargetArns(replication.ObjectOpts{ tgtArns := cfg.FilterTargetArns(replication.ObjectOpts{
Name: object, Name: object,
SSEC: crypto.SSEC.IsEncrypted(objInfo.UserDefined), SSEC: ri.SSEC,
UserTags: objInfo.UserTags, UserTags: ri.UserTags,
}) })
// Lock the object name before starting replication. // Lock the object name before starting replication.
// Use separate lock that doesn't collide with regular objects. // Use separate lock that doesn't collide with regular objects.
@@ -993,7 +1009,7 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
sendEvent(eventArgs{ sendEvent(eventArgs{
EventName: event.ObjectReplicationNotTracked, EventName: event.ObjectReplicationNotTracked,
BucketName: bucket, BucketName: bucket,
Object: objInfo, Object: ri.ToObjectInfo(),
UserAgent: "Internal: [Replication]", UserAgent: "Internal: [Replication]",
Host: globalLocalNodeName, Host: globalLocalNodeName,
}) })
@@ -1003,32 +1019,38 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
ctx = lkctx.Context() ctx = lkctx.Context()
defer lk.Unlock(lkctx) defer lk.Unlock(lkctx)
rinfos := replicatedInfos{Targets: make([]replicatedTargetInfo, 0, len(tgtArns))}
var wg sync.WaitGroup var wg sync.WaitGroup
var rinfos replicatedInfos var mu sync.Mutex
rinfos.Targets = make([]replicatedTargetInfo, len(tgtArns)) for _, tgtArn := range tgtArns {
for i, tgtArn := range tgtArns {
tgt := globalBucketTargetSys.GetRemoteTargetClient(tgtArn) tgt := globalBucketTargetSys.GetRemoteTargetClient(tgtArn)
if tgt == nil { if tgt == nil {
logger.LogOnceIf(ctx, fmt.Errorf("failed to get target for bucket:%s arn:%s", bucket, tgtArn), tgtArn) logger.LogOnceIf(ctx, fmt.Errorf("failed to get target for bucket:%s arn:%s", bucket, tgtArn), tgtArn)
sendEvent(eventArgs{ sendEvent(eventArgs{
EventName: event.ObjectReplicationNotTracked, EventName: event.ObjectReplicationNotTracked,
BucketName: bucket, BucketName: bucket,
Object: objInfo, Object: ri.ToObjectInfo(),
UserAgent: "Internal: [Replication]", UserAgent: "Internal: [Replication]",
Host: globalLocalNodeName, Host: globalLocalNodeName,
}) })
continue continue
} }
wg.Add(1) wg.Add(1)
go func(index int, tgt *TargetClient) { go func(tgt *TargetClient) {
defer wg.Done() defer wg.Done()
var tgtInfo replicatedTargetInfo
if ri.OpType == replication.ObjectReplicationType { if ri.OpType == replication.ObjectReplicationType {
// all incoming calls go through optimized path. // all incoming calls go through optimized path.
rinfos.Targets[index] = ri.replicateObject(ctx, objectAPI, tgt) tgtInfo = ri.replicateObject(ctx, objectAPI, tgt)
} else { } else {
rinfos.Targets[index] = ri.replicateAll(ctx, objectAPI, tgt) tgtInfo = ri.replicateAll(ctx, objectAPI, tgt)
} }
}(i, tgt)
mu.Lock()
rinfos.Targets = append(rinfos.Targets, tgtInfo)
mu.Unlock()
}(tgt)
} }
wg.Wait() wg.Wait()
@@ -1043,10 +1065,11 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
newReplStatusInternal := rinfos.ReplicationStatusInternal() newReplStatusInternal := rinfos.ReplicationStatusInternal()
// Note that internal replication status(es) may match for previously replicated objects - in such cases // Note that internal replication status(es) may match for previously replicated objects - in such cases
// metadata should be updated with last resync timestamp. // metadata should be updated with last resync timestamp.
if objInfo.ReplicationStatusInternal != newReplStatusInternal || rinfos.ReplicationResynced() { objInfo := ri.ToObjectInfo()
if ri.ReplicationStatusInternal != newReplStatusInternal || rinfos.ReplicationResynced() {
popts := ObjectOptions{ popts := ObjectOptions{
MTime: objInfo.ModTime, MTime: ri.ModTime,
VersionID: objInfo.VersionID, VersionID: ri.VersionID,
EvalMetadataFn: func(oi *ObjectInfo, gerr error) (dsc ReplicateDecision, err error) { EvalMetadataFn: func(oi *ObjectInfo, gerr error) (dsc ReplicateDecision, err error) {
oi.UserDefined[ReservedMetadataPrefixLower+ReplicationStatus] = newReplStatusInternal oi.UserDefined[ReservedMetadataPrefixLower+ReplicationStatus] = newReplStatusInternal
oi.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano) oi.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano)
@@ -1056,14 +1079,18 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
oi.UserDefined[targetResetHeader(rinfo.Arn)] = rinfo.ResyncTimestamp oi.UserDefined[targetResetHeader(rinfo.Arn)] = rinfo.ResyncTimestamp
} }
} }
if objInfo.UserTags != "" { if ri.UserTags != "" {
oi.UserDefined[xhttp.AmzObjectTagging] = objInfo.UserTags oi.UserDefined[xhttp.AmzObjectTagging] = ri.UserTags
} }
return dsc, nil return dsc, nil
}, },
} }
_, _ = objectAPI.PutObjectMetadata(ctx, bucket, object, popts) uobjInfo, _ := objectAPI.PutObjectMetadata(ctx, bucket, object, popts)
if uobjInfo.Name != "" {
objInfo = uobjInfo
}
opType := replication.MetadataReplicationType opType := replication.MetadataReplicationType
if rinfos.Action() == replicateAll { if rinfos.Action() == replicateAll {
opType = replication.ObjectReplicationType opType = replication.ObjectReplicationType
@@ -1099,23 +1126,21 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
// The source object is then updated to reflect the replication status. // The source object is then updated to reflect the replication status.
func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI ObjectLayer, tgt *TargetClient) (rinfo replicatedTargetInfo) { func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI ObjectLayer, tgt *TargetClient) (rinfo replicatedTargetInfo) {
startTime := time.Now() startTime := time.Now()
objInfo := ri.ObjectInfo.Clone() bucket := ri.Bucket
bucket := objInfo.Bucket object := ri.Name
object := objInfo.Name
sz, _ := objInfo.GetActualSize()
rAction := replicateAll rAction := replicateAll
rinfo = replicatedTargetInfo{ rinfo = replicatedTargetInfo{
Size: sz, Size: ri.ActualSize,
Arn: tgt.ARN, Arn: tgt.ARN,
PrevReplicationStatus: objInfo.TargetReplicationStatus(tgt.ARN), PrevReplicationStatus: ri.TargetReplicationStatus(tgt.ARN),
ReplicationStatus: replication.Failed, ReplicationStatus: replication.Failed,
OpType: ri.OpType, OpType: ri.OpType,
ReplicationAction: rAction, ReplicationAction: rAction,
endpoint: tgt.EndpointURL().Host, endpoint: tgt.EndpointURL().Host,
secure: tgt.EndpointURL().Scheme == "https", secure: tgt.EndpointURL().Scheme == "https",
} }
if ri.ObjectInfo.TargetReplicationStatus(tgt.ARN) == replication.Completed && !ri.ExistingObjResync.Empty() && !ri.ExistingObjResync.mustResyncTarget(tgt.ARN) { if ri.TargetReplicationStatus(tgt.ARN) == replication.Completed && !ri.ExistingObjResync.Empty() && !ri.ExistingObjResync.mustResyncTarget(tgt.ARN) {
rinfo.ReplicationStatus = replication.Completed rinfo.ReplicationStatus = replication.Completed
rinfo.ReplicationResynced = true rinfo.ReplicationResynced = true
return return
@@ -1126,7 +1151,7 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
sendEvent(eventArgs{ sendEvent(eventArgs{
EventName: event.ObjectReplicationNotTracked, EventName: event.ObjectReplicationNotTracked,
BucketName: bucket, BucketName: bucket,
Object: objInfo, Object: ri.ToObjectInfo(),
UserAgent: "Internal: [Replication]", UserAgent: "Internal: [Replication]",
Host: globalLocalNodeName, Host: globalLocalNodeName,
}) })
@@ -1137,12 +1162,13 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
versionSuspended := globalBucketVersioningSys.PrefixSuspended(bucket, object) versionSuspended := globalBucketVersioningSys.PrefixSuspended(bucket, object)
gr, err := objectAPI.GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, ObjectOptions{ gr, err := objectAPI.GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, ObjectOptions{
VersionID: objInfo.VersionID, VersionID: ri.VersionID,
Versioned: versioned, Versioned: versioned,
VersionSuspended: versionSuspended, VersionSuspended: versionSuspended,
}) })
if err != nil { if err != nil {
if !isErrVersionNotFound(err) && !isErrObjectNotFound(err) { if !isErrVersionNotFound(err) && !isErrObjectNotFound(err) {
objInfo := ri.ToObjectInfo()
sendEvent(eventArgs{ sendEvent(eventArgs{
EventName: event.ObjectReplicationNotTracked, EventName: event.ObjectReplicationNotTracked,
BucketName: bucket, BucketName: bucket,
@@ -1156,7 +1182,8 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
} }
defer gr.Close() defer gr.Close()
objInfo = gr.ObjInfo objInfo := gr.ObjInfo
// make sure we have the latest metadata for metrics calculation // make sure we have the latest metadata for metrics calculation
rinfo.PrevReplicationStatus = objInfo.TargetReplicationStatus(tgt.ARN) rinfo.PrevReplicationStatus = objInfo.TargetReplicationStatus(tgt.ARN)
@@ -1217,8 +1244,10 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
} }
opts := &bandwidth.MonitorReaderOptions{ opts := &bandwidth.MonitorReaderOptions{
Bucket: objInfo.Bucket, BucketOptions: bandwidth.BucketOptions{
TargetARN: tgt.ARN, Name: ri.Bucket,
ReplicationARN: tgt.ARN,
},
HeaderSize: headerSize, HeaderSize: headerSize,
} }
newCtx := ctx newCtx := ctx
@@ -1255,10 +1284,8 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
// The source object is then updated to reflect the replication status. // The source object is then updated to reflect the replication status.
func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI ObjectLayer, tgt *TargetClient) (rinfo replicatedTargetInfo) { func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI ObjectLayer, tgt *TargetClient) (rinfo replicatedTargetInfo) {
startTime := time.Now() startTime := time.Now()
objInfo := ri.ObjectInfo.Clone() bucket := ri.Bucket
bucket := objInfo.Bucket object := ri.Name
object := objInfo.Name
sz, _ := objInfo.GetActualSize()
// set defaults for replication action based on operation being performed - actual // set defaults for replication action based on operation being performed - actual
// replication action can only be determined after stat on remote. This default is // replication action can only be determined after stat on remote. This default is
@@ -1266,9 +1293,9 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
rAction := replicateMetadata rAction := replicateMetadata
rinfo = replicatedTargetInfo{ rinfo = replicatedTargetInfo{
Size: sz, Size: ri.ActualSize,
Arn: tgt.ARN, Arn: tgt.ARN,
PrevReplicationStatus: objInfo.TargetReplicationStatus(tgt.ARN), PrevReplicationStatus: ri.TargetReplicationStatus(tgt.ARN),
ReplicationStatus: replication.Failed, ReplicationStatus: replication.Failed,
OpType: ri.OpType, OpType: ri.OpType,
ReplicationAction: rAction, ReplicationAction: rAction,
@@ -1281,7 +1308,7 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
sendEvent(eventArgs{ sendEvent(eventArgs{
EventName: event.ObjectReplicationNotTracked, EventName: event.ObjectReplicationNotTracked,
BucketName: bucket, BucketName: bucket,
Object: objInfo, Object: ri.ToObjectInfo(),
UserAgent: "Internal: [Replication]", UserAgent: "Internal: [Replication]",
Host: globalLocalNodeName, Host: globalLocalNodeName,
}) })
@@ -1292,12 +1319,13 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
versionSuspended := globalBucketVersioningSys.PrefixSuspended(bucket, object) versionSuspended := globalBucketVersioningSys.PrefixSuspended(bucket, object)
gr, err := objectAPI.GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, ObjectOptions{ gr, err := objectAPI.GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, ObjectOptions{
VersionID: objInfo.VersionID, VersionID: ri.VersionID,
Versioned: versioned, Versioned: versioned,
VersionSuspended: versionSuspended, VersionSuspended: versionSuspended,
}) })
if err != nil { if err != nil {
if !isErrVersionNotFound(err) && !isErrObjectNotFound(err) { if !isErrVersionNotFound(err) && !isErrObjectNotFound(err) {
objInfo := ri.ToObjectInfo()
sendEvent(eventArgs{ sendEvent(eventArgs{
EventName: event.ObjectReplicationNotTracked, EventName: event.ObjectReplicationNotTracked,
BucketName: bucket, BucketName: bucket,
@@ -1311,7 +1339,7 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
} }
defer gr.Close() defer gr.Close()
objInfo = gr.ObjInfo objInfo := gr.ObjInfo
// make sure we have the latest metadata for metrics calculation // make sure we have the latest metadata for metrics calculation
rinfo.PrevReplicationStatus = objInfo.TargetReplicationStatus(tgt.ARN) rinfo.PrevReplicationStatus = objInfo.TargetReplicationStatus(tgt.ARN)
@@ -1378,7 +1406,9 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
} }
// 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
if objInfo.TargetReplicationStatus(tgt.ARN) == replication.Pending || objInfo.TargetReplicationStatus(tgt.ARN) == replication.Failed || ri.OpType == replication.ExistingObjectReplicationType { if objInfo.TargetReplicationStatus(tgt.ARN) == replication.Pending ||
objInfo.TargetReplicationStatus(tgt.ARN) == replication.Failed ||
ri.OpType == replication.ExistingObjectReplicationType {
// if metadata is not updated for some reason after replication, such as // if metadata is not updated for some reason after replication, such as
// 503 encountered while updating metadata - make sure to set ReplicationStatus // 503 encountered while updating metadata - make sure to set ReplicationStatus
// as Completed. // as Completed.
@@ -1456,8 +1486,10 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
} }
opts := &bandwidth.MonitorReaderOptions{ opts := &bandwidth.MonitorReaderOptions{
Bucket: objInfo.Bucket, BucketOptions: bandwidth.BucketOptions{
TargetARN: tgt.ARN, Name: objInfo.Bucket,
ReplicationARN: tgt.ARN,
},
HeaderSize: headerSize, HeaderSize: headerSize,
} }
newCtx := ctx newCtx := ctx
@@ -1498,9 +1530,18 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob
var uploadedParts []minio.CompletePart var uploadedParts []minio.CompletePart
// new multipart must not set mtime as it may lead to erroneous cleanups at various intervals. // new multipart must not set mtime as it may lead to erroneous cleanups at various intervals.
opts.Internal.SourceMTime = time.Time{} // this value is saved properly in CompleteMultipartUpload() opts.Internal.SourceMTime = time.Time{} // this value is saved properly in CompleteMultipartUpload()
nctx, cancel := context.WithTimeout(ctx, 5*time.Minute) var uploadID string
defer cancel() attempts := 1
uploadID, err := c.NewMultipartUpload(nctx, bucket, object, opts) for attempts <= 3 {
nctx, cancel := context.WithTimeout(ctx, time.Minute)
uploadID, err = c.NewMultipartUpload(nctx, bucket, object, opts)
cancel()
if err == nil {
break
}
attempts++
time.Sleep(time.Duration(rand.Int63n(int64(time.Second))))
}
if err != nil { if err != nil {
return err return err
} }
@@ -1521,7 +1562,7 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob
fmt.Errorf("trying %s: Unable to cleanup failed multipart replication %s on remote %s/%s: %w - this may consume space on remote cluster", fmt.Errorf("trying %s: Unable to cleanup failed multipart replication %s on remote %s/%s: %w - this may consume space on remote cluster",
humanize.Ordinal(attempts), uploadID, bucket, object, aerr)) humanize.Ordinal(attempts), uploadID, bucket, object, aerr))
attempts++ attempts++
time.Sleep(time.Second) time.Sleep(time.Duration(rand.Int63n(int64(time.Second))))
} }
} }
}() }()
@@ -1532,7 +1573,7 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob
) )
for _, partInfo := range objInfo.Parts { for _, partInfo := range objInfo.Parts {
hr, err = hash.NewReader(io.LimitReader(r, partInfo.ActualSize), partInfo.ActualSize, "", "", partInfo.ActualSize) hr, err = hash.NewReader(ctx, io.LimitReader(r, partInfo.ActualSize), partInfo.ActualSize, "", "", partInfo.ActualSize)
if err != nil { if err != nil {
return err return err
} }
@@ -1658,9 +1699,8 @@ type ReplicationPool struct {
resyncer *replicationResyncer resyncer *replicationResyncer
// workers: // workers:
workers []chan ReplicationWorkerOperation workers []chan ReplicationWorkerOperation
lrgworkers []chan ReplicationWorkerOperation lrgworkers []chan ReplicationWorkerOperation
existingWorkers chan ReplicationWorkerOperation
// mrf: // mrf:
mrfWorkerKillCh chan struct{} mrfWorkerKillCh chan struct{}
@@ -1720,8 +1760,6 @@ func NewReplicationPool(ctx context.Context, o ObjectLayer, opts replicationPool
pool := &ReplicationPool{ pool := &ReplicationPool{
workers: make([]chan ReplicationWorkerOperation, 0, workers), workers: make([]chan ReplicationWorkerOperation, 0, workers),
lrgworkers: make([]chan ReplicationWorkerOperation, 0, LargeWorkerCount), lrgworkers: make([]chan ReplicationWorkerOperation, 0, LargeWorkerCount),
existingWorkers: make(chan ReplicationWorkerOperation, 100000),
mrfReplicaCh: make(chan ReplicationWorkerOperation, 100000), mrfReplicaCh: make(chan ReplicationWorkerOperation, 100000),
mrfWorkerKillCh: make(chan struct{}, failedWorkers), mrfWorkerKillCh: make(chan struct{}, failedWorkers),
resyncer: newresyncer(), resyncer: newresyncer(),
@@ -1735,7 +1773,6 @@ func NewReplicationPool(ctx context.Context, o ObjectLayer, opts replicationPool
pool.AddLargeWorkers() pool.AddLargeWorkers()
pool.ResizeWorkers(workers, 0) pool.ResizeWorkers(workers, 0)
pool.ResizeFailedWorkers(failedWorkers) pool.ResizeFailedWorkers(failedWorkers)
go pool.AddWorker(pool.existingWorkers, nil)
go pool.resyncer.PersistToDisk(ctx, o) go pool.resyncer.PersistToDisk(ctx, o)
go pool.processMRF() go pool.processMRF()
go pool.persistMRF() go pool.persistMRF()
@@ -1962,9 +1999,7 @@ func (p *ReplicationPool) queueReplicaTask(ri ReplicateObjectInfo) {
var ch, healCh chan<- ReplicationWorkerOperation var ch, healCh chan<- ReplicationWorkerOperation
switch ri.OpType { switch ri.OpType {
case replication.ExistingObjectReplicationType: case replication.HealReplicationType, replication.ExistingObjectReplicationType:
ch = p.existingWorkers
case replication.HealReplicationType:
ch = p.mrfReplicaCh ch = p.mrfReplicaCh
healCh = p.getWorkerCh(ri.Name, ri.Bucket, ri.Size) healCh = p.getWorkerCh(ri.Name, ri.Bucket, ri.Size)
default: default:
@@ -2023,9 +2058,7 @@ func (p *ReplicationPool) queueReplicaDeleteTask(doi DeletedObjectReplicationInf
} }
var ch chan<- ReplicationWorkerOperation var ch chan<- ReplicationWorkerOperation
switch doi.OpType { switch doi.OpType {
case replication.ExistingObjectReplicationType: case replication.HealReplicationType, replication.ExistingObjectReplicationType:
ch = p.existingWorkers
case replication.HealReplicationType:
fallthrough fallthrough
default: default:
ch = p.getWorkerCh(doi.Bucket, doi.ObjectName, 0) ch = p.getWorkerCh(doi.Bucket, doi.ObjectName, 0)
@@ -2229,8 +2262,38 @@ func proxyHeadToReplicationTarget(ctx context.Context, bucket, object string, rs
return oi, proxy return oi, proxy
} }
func scheduleReplication(ctx context.Context, objInfo ObjectInfo, o ObjectLayer, dsc ReplicateDecision, opType replication.Type) { func scheduleReplication(ctx context.Context, oi ObjectInfo, o ObjectLayer, dsc ReplicateDecision, opType replication.Type) {
ri := ReplicateObjectInfo{ObjectInfo: objInfo, OpType: opType, Dsc: dsc, EventType: ReplicateIncoming} tgtStatuses := replicationStatusesMap(oi.ReplicationStatusInternal)
purgeStatuses := versionPurgeStatusesMap(oi.VersionPurgeStatusInternal)
tm, _ := time.Parse(time.RFC3339Nano, oi.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp])
rstate := oi.ReplicationState()
rstate.ReplicateDecisionStr = dsc.String()
asz, _ := oi.GetActualSize()
ri := ReplicateObjectInfo{
Name: oi.Name,
Size: oi.Size,
ActualSize: asz,
Bucket: oi.Bucket,
VersionID: oi.VersionID,
ETag: oi.ETag,
ModTime: oi.ModTime,
ReplicationStatus: oi.ReplicationStatus,
ReplicationStatusInternal: oi.ReplicationStatusInternal,
DeleteMarker: oi.DeleteMarker,
VersionPurgeStatusInternal: oi.VersionPurgeStatusInternal,
VersionPurgeStatus: oi.VersionPurgeStatus,
ReplicationState: rstate,
OpType: opType,
Dsc: dsc,
TargetStatuses: tgtStatuses,
TargetPurgeStatuses: purgeStatuses,
ReplicationTimestamp: tm,
SSEC: crypto.SSEC.IsEncrypted(oi.UserDefined),
UserTags: oi.UserTags,
}
if dsc.Synchronous() { if dsc.Synchronous() {
replicateObject(ctx, ri, o) replicateObject(ctx, ri, o)
} else { } else {
@@ -2259,7 +2322,7 @@ func (c replicationConfig) Replicate(opts replication.ObjectOpts) bool {
} }
// Resync returns true if replication reset is requested // Resync returns true if replication reset is requested
func (c replicationConfig) Resync(ctx context.Context, oi ObjectInfo, dsc *ReplicateDecision, tgtStatuses map[string]replication.StatusType) (r ResyncDecision) { func (c replicationConfig) Resync(ctx context.Context, oi ObjectInfo, dsc ReplicateDecision, tgtStatuses map[string]replication.StatusType) (r ResyncDecision) {
if c.Empty() { if c.Empty() {
return return
} }
@@ -2268,8 +2331,6 @@ func (c replicationConfig) Resync(ctx context.Context, oi ObjectInfo, dsc *Repli
if oi.DeleteMarker { if oi.DeleteMarker {
opts := replication.ObjectOpts{ opts := replication.ObjectOpts{
Name: oi.Name, Name: oi.Name,
SSEC: crypto.SSEC.IsEncrypted(oi.UserDefined),
UserTags: oi.UserTags,
DeleteMarker: oi.DeleteMarker, DeleteMarker: oi.DeleteMarker,
VersionID: oi.VersionID, VersionID: oi.VersionID,
OpType: replication.DeleteReplicationType, OpType: replication.DeleteReplicationType,
@@ -2290,23 +2351,19 @@ func (c replicationConfig) Resync(ctx context.Context, oi ObjectInfo, dsc *Repli
} }
// Ignore previous replication status when deciding if object can be re-replicated // Ignore previous replication status when deciding if object can be re-replicated
objInfo := oi.Clone() userDefined := cloneMSS(oi.UserDefined)
objInfo.ReplicationStatusInternal = "" delete(userDefined, xhttp.AmzBucketReplicationStatus)
objInfo.VersionPurgeStatusInternal = ""
objInfo.ReplicationStatus = "" rdsc := mustReplicate(ctx, oi.Bucket, oi.Name, getMustReplicateOptions(userDefined, oi.UserTags, "", replication.ExistingObjectReplicationType, ObjectOptions{}))
objInfo.VersionPurgeStatus = "" return c.resync(oi, rdsc, tgtStatuses)
delete(objInfo.UserDefined, xhttp.AmzBucketReplicationStatus)
resyncdsc := mustReplicate(ctx, oi.Bucket, oi.Name, getMustReplicateOptions(objInfo, replication.ExistingObjectReplicationType, ObjectOptions{}))
dsc = &resyncdsc
return c.resync(oi, dsc, tgtStatuses)
} }
// wrapper function for testability. Returns true if a new reset is requested on // wrapper function for testability. Returns true if a new reset is requested on
// already replicated objects OR object qualifies for existing object replication // already replicated objects OR object qualifies for existing object replication
// and no reset requested. // and no reset requested.
func (c replicationConfig) resync(oi ObjectInfo, dsc *ReplicateDecision, tgtStatuses map[string]replication.StatusType) (r ResyncDecision) { func (c replicationConfig) resync(oi ObjectInfo, dsc ReplicateDecision, tgtStatuses map[string]replication.StatusType) (r ResyncDecision) {
r = ResyncDecision{ r = ResyncDecision{
targets: make(map[string]ResyncTargetDecision), targets: make(map[string]ResyncTargetDecision, len(dsc.targetsMap)),
} }
if c.remotes == nil { if c.remotes == nil {
return return
@@ -2527,6 +2584,13 @@ func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI Object
workers := make([]chan ReplicateObjectInfo, resyncParallelRoutines) workers := make([]chan ReplicateObjectInfo, resyncParallelRoutines)
resultCh := make(chan TargetReplicationResyncStatus, 1) resultCh := make(chan TargetReplicationResyncStatus, 1)
defer close(resultCh) defer close(resultCh)
go func() {
for r := range resultCh {
s.incStats(r, opts)
globalSiteResyncMetrics.updateMetric(r, opts.resyncID)
}
}()
var wg sync.WaitGroup var wg sync.WaitGroup
for i := 0; i < resyncParallelRoutines; i++ { for i := 0; i < resyncParallelRoutines; i++ {
wg.Add(1) wg.Add(1)
@@ -2556,7 +2620,7 @@ func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI Object
ObjectName: roi.Name, ObjectName: roi.Name,
DeleteMarkerVersionID: dmVersionID, DeleteMarkerVersionID: dmVersionID,
VersionID: versionID, VersionID: versionID,
ReplicationState: roi.getReplicationState(), ReplicationState: roi.ReplicationState,
DeleteMarkerMTime: DeleteMarkerMTime{roi.ModTime}, DeleteMarkerMTime: DeleteMarkerMTime{roi.ModTime},
DeleteMarker: roi.DeleteMarker, DeleteMarker: roi.DeleteMarker,
}, },
@@ -2631,12 +2695,6 @@ func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI Object
for i := 0; i < resyncParallelRoutines; i++ { for i := 0; i < resyncParallelRoutines; i++ {
close(workers[i]) close(workers[i])
} }
go func() {
for r := range resultCh {
s.incStats(r, opts)
globalSiteResyncMetrics.updateMetric(r, opts.resyncID)
}
}()
wg.Wait() wg.Wait()
resyncStatus = ResyncCompleted resyncStatus = ResyncCompleted
} }
@@ -3008,7 +3066,7 @@ func queueReplicationHeal(ctx context.Context, bucket string, oi ObjectInfo, rcf
ObjectName: roi.Name, ObjectName: roi.Name,
DeleteMarkerVersionID: dmVersionID, DeleteMarkerVersionID: dmVersionID,
VersionID: versionID, VersionID: versionID,
ReplicationState: roi.getReplicationState(), ReplicationState: roi.ReplicationState,
DeleteMarkerMTime: DeleteMarkerMTime{roi.ModTime}, DeleteMarkerMTime: DeleteMarkerMTime{roi.ModTime},
DeleteMarker: roi.DeleteMarker, DeleteMarker: roi.DeleteMarker,
}, },
@@ -3048,7 +3106,7 @@ func queueReplicationHeal(ctx context.Context, bucket string, oi ObjectInfo, rcf
const ( const (
mrfSaveInterval = 5 * time.Minute mrfSaveInterval = 5 * time.Minute
mrfQueueInterval = 6 * time.Minute mrfQueueInterval = mrfSaveInterval + time.Minute // A minute higher than save interval
mrfRetryLimit = 3 // max number of retries before letting scanner catch up on this object version mrfRetryLimit = 3 // max number of retries before letting scanner catch up on this object version
mrfMaxEntries = 1000000 mrfMaxEntries = 1000000
@@ -3063,46 +3121,37 @@ func (p *ReplicationPool) persistMRF() {
mTimer := time.NewTimer(mrfSaveInterval) mTimer := time.NewTimer(mrfSaveInterval)
defer mTimer.Stop() defer mTimer.Stop()
saveMRFToDisk := func(drain bool) { saveMRFToDisk := func() {
if len(entries) == 0 { if len(entries) == 0 {
return return
} }
cctx := p.ctx
if drain {
cctx = context.Background()
// drain all mrf entries and save to disk
for e := range p.mrfSaveCh {
entries[e.versionID] = e
}
}
// queue all entries for healing before overwriting the node mrf file // queue all entries for healing before overwriting the node mrf file
if !contextCanceled(p.ctx) { if !contextCanceled(p.ctx) {
p.queueMRFHeal() p.queueMRFHeal()
} }
if err := p.saveMRFEntries(cctx, entries); err != nil { p.saveMRFEntries(p.ctx, entries)
logger.LogOnceIf(p.ctx, fmt.Errorf("unable to persist replication failures to disk:%w", err), string(replicationSubsystem))
}
entries = make(map[string]MRFReplicateEntry) entries = make(map[string]MRFReplicateEntry)
} }
for { for {
select { select {
case <-mTimer.C: case <-mTimer.C:
saveMRFToDisk(false) saveMRFToDisk()
mTimer.Reset(mrfSaveInterval) mTimer.Reset(mrfSaveInterval)
case <-p.ctx.Done(): case <-p.ctx.Done():
p.mrfStopCh <- struct{}{} p.mrfStopCh <- struct{}{}
close(p.mrfSaveCh) close(p.mrfSaveCh)
saveMRFToDisk(true) // We try to save if possible, but we don't care beyond that.
saveMRFToDisk()
return return
case e, ok := <-p.mrfSaveCh: case e, ok := <-p.mrfSaveCh:
if !ok { if !ok {
return return
} }
if len(entries) >= mrfMaxEntries { if len(entries) >= mrfMaxEntries {
saveMRFToDisk(true) saveMRFToDisk()
} }
entries[e.versionID] = e entries[e.versionID] = e
} }
@@ -3132,14 +3181,45 @@ func (p *ReplicationPool) queueMRFSave(entry MRFReplicateEntry) {
} }
} }
func (p *ReplicationPool) persistToDrive(ctx context.Context, v MRFReplicateEntries, data []byte) {
newReader := func() io.ReadCloser {
r, w := io.Pipe()
go func() {
mw := msgp.NewWriter(w)
n, err := mw.Write(data)
if err != nil {
w.CloseWithError(err)
return
}
if n != len(data) {
w.CloseWithError(io.ErrShortWrite)
return
}
err = v.EncodeMsg(mw)
mw.Flush()
w.CloseWithError(err)
}()
return r
}
for _, localDrive := range globalLocalDrives {
r := newReader()
err := localDrive.CreateFile(ctx, minioMetaBucket, pathJoin(replicationMRFDir, globalLocalNodeNameHex+".bin"), -1, r)
r.Close()
if err == nil {
break
}
}
}
// save mrf entries to nodenamehex.bin // save mrf entries to nodenamehex.bin
func (p *ReplicationPool) saveMRFEntries(ctx context.Context, entries map[string]MRFReplicateEntry) error { func (p *ReplicationPool) saveMRFEntries(ctx context.Context, entries map[string]MRFReplicateEntry) {
if !p.initialized() { if !p.initialized() {
return nil return
} }
atomic.StoreUint64(&globalReplicationStats.mrfStats.LastFailedCount, uint64(len(entries))) atomic.StoreUint64(&globalReplicationStats.mrfStats.LastFailedCount, uint64(len(entries)))
if len(entries) == 0 { if len(entries) == 0 {
return nil return
} }
v := MRFReplicateEntries{ v := MRFReplicateEntries{
@@ -3152,53 +3232,60 @@ func (p *ReplicationPool) saveMRFEntries(ctx context.Context, entries map[string
binary.LittleEndian.PutUint16(data[0:2], mrfMetaFormat) binary.LittleEndian.PutUint16(data[0:2], mrfMetaFormat)
binary.LittleEndian.PutUint16(data[2:4], mrfMetaVersion) binary.LittleEndian.PutUint16(data[2:4], mrfMetaVersion)
buf, err := v.MarshalMsg(data) p.persistToDrive(ctx, v, data)
if err != nil {
return err
}
for _, localDrive := range globalLocalDrives {
if err := localDrive.WriteAll(ctx, minioMetaBucket, pathJoin(replicationMRFDir, globalLocalNodeNameHex+".bin"), buf); err == nil {
break
}
}
return nil
} }
// load mrf entries from disk // load mrf entries from disk
func (p *ReplicationPool) loadMRF(data []byte) (re MRFReplicateEntries, e error) { func (p *ReplicationPool) loadMRF() (mrfRec MRFReplicateEntries, err error) {
if !p.initialized() { loadMRF := func(rc io.ReadCloser) (re MRFReplicateEntries, err error) {
defer rc.Close()
if !p.initialized() {
return re, nil
}
data := make([]byte, 4)
n, err := rc.Read(data)
if err != nil {
return re, err
}
if n != len(data) {
return re, errors.New("replication mrf: no data")
}
// Read resync meta header
switch binary.LittleEndian.Uint16(data[0:2]) {
case mrfMetaFormat:
default:
return re, fmt.Errorf("replication mrf: unknown format: %d", binary.LittleEndian.Uint16(data[0:2]))
}
switch binary.LittleEndian.Uint16(data[2:4]) {
case mrfMetaVersion:
default:
return re, fmt.Errorf("replication mrf: unknown version: %d", binary.LittleEndian.Uint16(data[2:4]))
}
// OK, parse data.
// ignore any parsing errors, we do not care this file is generated again anyways.
re.DecodeMsg(msgp.NewReader(rc))
return re, nil return re, nil
} }
if len(data) == 0 { for _, localDrive := range globalLocalDrives {
// Seems to be empty. rc, err := localDrive.ReadFileStream(p.ctx, minioMetaBucket, pathJoin(replicationMRFDir, globalLocalNodeNameHex+".bin"), 0, -1)
return re, nil if err != nil {
} continue
if len(data) <= 4 { }
return re, fmt.Errorf("replication mrf: no data")
} mrfRec, err = loadMRF(rc)
// Read resync meta header if err != nil {
switch binary.LittleEndian.Uint16(data[0:2]) { continue
case mrfMetaFormat: }
default:
return re, fmt.Errorf("replication mrf: unknown format: %d", binary.LittleEndian.Uint16(data[0:2])) // finally delete the file after processing mrf entries
} localDrive.Delete(p.ctx, minioMetaBucket, pathJoin(replicationMRFDir, globalLocalNodeNameHex+".bin"), DeleteOptions{})
switch binary.LittleEndian.Uint16(data[2:4]) { break
case mrfMetaVersion:
default:
return re, fmt.Errorf("replication mrf: unknown version: %d", binary.LittleEndian.Uint16(data[2:4]))
}
// OK, parse data.
if _, err := re.UnmarshalMsg(data[4:]); err != nil {
return re, err
} }
switch re.Version { return mrfRec, nil
case mrfMetaVersionV1:
default:
return re, fmt.Errorf("unexpected mrf meta version: %d", re.Version)
}
return re, nil
} }
func (p *ReplicationPool) processMRF() { func (p *ReplicationPool) processMRF() {
@@ -3241,39 +3328,28 @@ func (p *ReplicationPool) queueMRFHeal() error {
return errServerNotInitialized return errServerNotInitialized
} }
for _, localDrive := range globalLocalDrives { mrfRec, err := p.loadMRF()
buf, err := localDrive.ReadAll(p.ctx, minioMetaBucket, pathJoin(replicationMRFDir, globalLocalNodeNameHex+".bin")) if err != nil {
if err != nil { return err
continue
}
mrfRec, err := p.loadMRF(buf)
if err != nil {
continue
}
// finally delete the file after processing mrf entries
localDrive.Delete(p.ctx, minioMetaBucket, pathJoin(replicationMRFDir, globalLocalNodeNameHex+".bin"), DeleteOptions{})
// queue replication heal in a goroutine to avoid holding up mrf save routine
go func(mrfRec MRFReplicateEntries) {
for vID, e := range mrfRec.Entries {
ctx, cancel := context.WithTimeout(p.ctx, time.Second) // Do not waste more than a second on this.
oi, err := p.objLayer.GetObjectInfo(ctx, e.Bucket, e.Object, ObjectOptions{
VersionID: vID,
})
cancel()
if err != nil {
continue
}
QueueReplicationHeal(p.ctx, e.Bucket, oi, e.RetryCount)
}
}(mrfRec)
break
} }
// queue replication heal in a goroutine to avoid holding up mrf save routine
go func() {
for vID, e := range mrfRec.Entries {
ctx, cancel := context.WithTimeout(p.ctx, time.Second) // Do not waste more than a second on this.
oi, err := p.objLayer.GetObjectInfo(ctx, e.Bucket, e.Object, ObjectOptions{
VersionID: vID,
})
cancel()
if err != nil {
continue
}
QueueReplicationHeal(p.ctx, e.Bucket, oi, e.RetryCount)
}
}()
return nil return nil
} }
@@ -3283,33 +3359,28 @@ func (p *ReplicationPool) initialized() bool {
// getMRF returns MRF entries for this node. // getMRF returns MRF entries for this node.
func (p *ReplicationPool) getMRF(ctx context.Context, bucket string) (ch chan madmin.ReplicationMRF, err error) { func (p *ReplicationPool) getMRF(ctx context.Context, bucket string) (ch chan madmin.ReplicationMRF, err error) {
mrfRec, err := p.loadMRF()
if err != nil {
return nil, err
}
mrfCh := make(chan madmin.ReplicationMRF, 100) mrfCh := make(chan madmin.ReplicationMRF, 100)
go func() { go func() {
defer close(mrfCh) defer close(mrfCh)
for _, localDrive := range globalLocalDrives { for vID, e := range mrfRec.Entries {
buf, err := localDrive.ReadAll(ctx, minioMetaBucket, pathJoin(replicationMRFDir, globalLocalNodeNameHex+".bin")) if e.Bucket != bucket && bucket != "" {
if err != nil {
continue continue
} }
mrfRec, err := p.loadMRF(buf) select {
if err != nil { case mrfCh <- madmin.ReplicationMRF{
continue NodeName: globalLocalNodeName,
} Object: e.Object,
for vID, e := range mrfRec.Entries { VersionID: vID,
if e.Bucket != bucket && bucket != "" { Bucket: e.Bucket,
continue RetryCount: e.RetryCount,
} }:
select { case <-ctx.Done():
case mrfCh <- madmin.ReplicationMRF{ return
NodeName: globalLocalNodeName,
Object: e.Object,
VersionID: vID,
Bucket: e.Bucket,
RetryCount: e.RetryCount,
}:
case <-ctx.Done():
return
}
} }
} }
}() }()
+2 -2
View File
@@ -88,7 +88,7 @@ var replicationConfigTests = []struct {
func TestReplicationResync(t *testing.T) { func TestReplicationResync(t *testing.T) {
ctx := context.Background() ctx := context.Background()
for i, test := range replicationConfigTests { for i, test := range replicationConfigTests {
if sync := test.rcfg.Resync(ctx, test.info, &test.dsc, test.tgtStatuses); sync.mustResync() != test.expectedSync { if sync := test.rcfg.Resync(ctx, test.info, test.dsc, test.tgtStatuses); sync.mustResync() != test.expectedSync {
t.Errorf("Test%d (%s): Resync got %t , want %t", i+1, test.name, sync.mustResync(), test.expectedSync) t.Errorf("Test%d (%s): Resync got %t , want %t", i+1, test.name, sync.mustResync(), test.expectedSync)
} }
} }
@@ -283,7 +283,7 @@ var (
func TestReplicationResyncwrapper(t *testing.T) { func TestReplicationResyncwrapper(t *testing.T) {
for i, test := range replicationConfigTests2 { for i, test := range replicationConfigTests2 {
if sync := test.rcfg.resync(test.info, &test.dsc, test.tgtStatuses); sync.mustResync() != test.expectedSync { if sync := test.rcfg.resync(test.info, test.dsc, test.tgtStatuses); sync.mustResync() != test.expectedSync {
t.Errorf("%s (%s): Replicationresync got %t , want %t", fmt.Sprintf("Test%d - %s", i+1, time.Now().Format(http.TimeFormat)), test.name, sync.mustResync(), test.expectedSync) t.Errorf("%s (%s): Replicationresync got %t , want %t", fmt.Sprintf("Test%d - %s", i+1, time.Now().Format(http.TimeFormat)), test.name, sync.mustResync(), test.expectedSync)
} }
} }
+14 -21
View File
@@ -489,31 +489,27 @@ func (sys *BucketTargetSys) UpdateAllTargets(bucket string, tgts *madmin.BucketT
defer sys.Unlock() defer sys.Unlock()
// Remove existingtarget and arn association // Remove existingtarget and arn association
if tgts, ok := sys.targetsMap[bucket]; ok { if stgts, ok := sys.targetsMap[bucket]; ok {
for _, t := range tgts { for _, t := range stgts {
delete(sys.arnRemotesMap, t.Arn) delete(sys.arnRemotesMap, t.Arn)
} }
delete(sys.targetsMap, bucket) delete(sys.targetsMap, bucket)
} }
// No need for more if not adding anything if tgts != nil {
if tgts == nil || tgts.Empty() { for _, tgt := range tgts.Targets {
globalBucketMonitor.DeleteBucket(bucket) tgtClient, err := sys.getRemoteTargetClient(&tgt)
return if err != nil {
} continue
}
if len(tgts.Targets) > 0 { sys.arnRemotesMap[tgt.Arn] = tgtClient
sys.targetsMap[bucket] = tgts.Targets sys.updateBandwidthLimit(bucket, tgt.Arn, tgt.BandwidthLimit)
} }
for _, tgt := range tgts.Targets {
tgtClient, err := sys.getRemoteTargetClient(&tgt) if !tgts.Empty() {
if err != nil { sys.targetsMap[bucket] = tgts.Targets
continue
} }
sys.arnRemotesMap[tgt.Arn] = tgtClient
sys.updateBandwidthLimit(bucket, tgt.Arn, tgt.BandwidthLimit)
} }
sys.targetsMap[bucket] = tgts.Targets
} }
// create minio-go clients for buckets having remote targets // create minio-go clients for buckets having remote targets
@@ -524,9 +520,6 @@ func (sys *BucketTargetSys) set(bucket BucketInfo, meta BucketMetadata) {
} }
sys.Lock() sys.Lock()
defer sys.Unlock() defer sys.Unlock()
if len(cfg.Targets) > 0 {
sys.targetsMap[bucket.Name] = cfg.Targets
}
for _, tgt := range cfg.Targets { for _, tgt := range cfg.Targets {
tgtClient, err := sys.getRemoteTargetClient(&tgt) tgtClient, err := sys.getRemoteTargetClient(&tgt)
if err != nil { if err != nil {
+33 -24
View File
@@ -29,7 +29,6 @@ import (
"fmt" "fmt"
"math/rand" "math/rand"
"net" "net"
"net/http"
"net/url" "net/url"
"os" "os"
"path" "path"
@@ -54,7 +53,6 @@ import (
"github.com/minio/kes-go" "github.com/minio/kes-go"
"github.com/minio/madmin-go/v3" "github.com/minio/madmin-go/v3"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/color" "github.com/minio/minio/internal/color"
@@ -71,10 +69,7 @@ import (
// serverDebugLog will enable debug printing // serverDebugLog will enable debug printing
var serverDebugLog = env.Get("_MINIO_SERVER_DEBUG", config.EnableOff) == config.EnableOn var serverDebugLog = env.Get("_MINIO_SERVER_DEBUG", config.EnableOff) == config.EnableOn
var ( var shardDiskTimeDelta time.Duration
shardDiskTimeDelta time.Duration
defaultAWSCredProvider []credentials.Provider
)
func init() { func init() {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
@@ -112,23 +107,16 @@ func init() {
gob.Register(madmin.XFSErrorConfigs{}) gob.Register(madmin.XFSErrorConfigs{})
gob.Register(map[string]interface{}{}) gob.Register(map[string]interface{}{})
defaultAWSCredProvider = []credentials.Provider{
&credentials.IAM{
Client: &http.Client{
Transport: NewHTTPTransport(),
},
},
}
var err error var err error
shardDiskTimeDelta, err = time.ParseDuration(env.Get("_MINIO_SHARD_DISKTIME_DELTA", "1m")) shardDiskTimeDelta, err = time.ParseDuration(env.Get("_MINIO_SHARD_DISKTIME_DELTA", "1m"))
if err != nil { if err != nil {
shardDiskTimeDelta = 1 * time.Minute shardDiskTimeDelta = 1 * time.Minute
} }
// All minio-go API operations shall be performed only once, // All minio-go and madmin-go API operations shall be performed only once,
// another way to look at this is we are turning off retries. // another way to look at this is we are turning off retries.
minio.MaxRetry = 1 minio.MaxRetry = 1
madmin.MaxRetry = 1
} }
const consolePrefix = "CONSOLE_" const consolePrefix = "CONSOLE_"
@@ -143,29 +131,33 @@ func minioConfigToConsoleFeatures() {
// This will save users from providing a certificate with IP or FQDN SAN that points to the local host. // This will save users from providing a certificate with IP or FQDN SAN that points to the local host.
os.Setenv("CONSOLE_MINIO_SERVER", fmt.Sprintf("%s://127.0.0.1:%s", getURLScheme(globalIsTLS), globalMinioPort)) os.Setenv("CONSOLE_MINIO_SERVER", fmt.Sprintf("%s://127.0.0.1:%s", getURLScheme(globalIsTLS), globalMinioPort))
} }
if value := env.Get("MINIO_LOG_QUERY_URL", ""); value != "" { if value := env.Get(config.EnvMinIOLogQueryURL, ""); value != "" {
os.Setenv("CONSOLE_LOG_QUERY_URL", value) os.Setenv("CONSOLE_LOG_QUERY_URL", value)
if value := env.Get("MINIO_LOG_QUERY_AUTH_TOKEN", ""); value != "" { if value := env.Get(config.EnvMinIOLogQueryAuthToken, ""); value != "" {
os.Setenv("CONSOLE_LOG_QUERY_AUTH_TOKEN", value) os.Setenv("CONSOLE_LOG_QUERY_AUTH_TOKEN", value)
} }
} }
// pass the console subpath configuration // pass the console subpath configuration
if value := env.Get(config.EnvBrowserRedirectURL, ""); value != "" { if globalBrowserRedirectURL != nil {
subPath := path.Clean(pathJoin(strings.TrimSpace(globalBrowserRedirectURL.Path), SlashSeparator)) subPath := path.Clean(pathJoin(strings.TrimSpace(globalBrowserRedirectURL.Path), SlashSeparator))
if subPath != SlashSeparator { if subPath != SlashSeparator {
os.Setenv("CONSOLE_SUBPATH", subPath) os.Setenv("CONSOLE_SUBPATH", subPath)
} }
} }
// Enable if prometheus URL is set. // Enable if prometheus URL is set.
if value := env.Get("MINIO_PROMETHEUS_URL", ""); value != "" { if value := env.Get(config.EnvMinIOPrometheusURL, ""); value != "" {
os.Setenv("CONSOLE_PROMETHEUS_URL", value) os.Setenv("CONSOLE_PROMETHEUS_URL", value)
if value := env.Get("MINIO_PROMETHEUS_JOB_ID", "minio-job"); value != "" { if value := env.Get(config.EnvMinIOPrometheusJobID, "minio-job"); value != "" {
os.Setenv("CONSOLE_PROMETHEUS_JOB_ID", value) os.Setenv("CONSOLE_PROMETHEUS_JOB_ID", value)
// Support additional labels for more granular filtering. // Support additional labels for more granular filtering.
if value := env.Get("MINIO_PROMETHEUS_EXTRA_LABELS", ""); value != "" { if value := env.Get(config.EnvMinIOPrometheusExtraLabels, ""); value != "" {
os.Setenv("CONSOLE_PROMETHEUS_EXTRA_LABELS", value) os.Setenv("CONSOLE_PROMETHEUS_EXTRA_LABELS", value)
} }
} }
// Support Prometheus Auth Token
if value := env.Get(config.EnvMinIOPrometheusAuthToken, ""); value != "" {
os.Setenv("CONSOLE_PROMETHEUS_AUTH_TOKEN", value)
}
} }
// Enable if LDAP is enabled. // Enable if LDAP is enabled.
if globalIAMSys.LDAPConfig.Enabled() { if globalIAMSys.LDAPConfig.Enabled() {
@@ -391,7 +383,7 @@ func handleCommonCmdArgs(ctx *cli.Context) {
if consoleAddr == "" { if consoleAddr == "" {
p, err := xnet.GetFreePort() p, err := xnet.GetFreePort()
if err != nil { if err != nil {
logger.FatalIf(err, "Unable to get free port for console on the host") logger.FatalIf(err, "Unable to get free port for Console UI on the host")
} }
consoleAddr = net.JoinHostPort("", p.String()) consoleAddr = net.JoinHostPort("", p.String())
} }
@@ -405,6 +397,15 @@ func handleCommonCmdArgs(ctx *cli.Context) {
} }
globalMinioHost, globalMinioPort = mustSplitHostPort(addr) globalMinioHost, globalMinioPort = mustSplitHostPort(addr)
if globalMinioPort == "0" {
p, err := xnet.GetFreePort()
if err != nil {
logger.FatalIf(err, "Unable to get free port for S3 API on the host")
}
globalMinioPort = p.String()
globalDynamicAPIPort = true
}
globalMinioConsoleHost, globalMinioConsolePort = mustSplitHostPort(consoleAddr) globalMinioConsoleHost, globalMinioConsolePort = mustSplitHostPort(consoleAddr)
if globalMinioPort == globalMinioConsolePort { if globalMinioPort == globalMinioConsolePort {
@@ -640,6 +641,7 @@ func handleCommonEnvVars() {
} }
globalBrowserRedirectURL = u globalBrowserRedirectURL = u
} }
globalBrowserRedirect = env.Get(config.EnvBrowserRedirect, config.EnableOn) == config.EnableOn
} }
if serverURL := env.Get(config.EnvMinIOServerURL, ""); serverURL != "" { if serverURL := env.Get(config.EnvMinIOServerURL, ""); serverURL != "" {
@@ -664,10 +666,14 @@ func handleCommonEnvVars() {
logger.Fatal(config.ErrInvalidFSOSyncValue(err), "Invalid MINIO_FS_OSYNC value in environment variable") logger.Fatal(config.ErrInvalidFSOSyncValue(err), "Invalid MINIO_FS_OSYNC value in environment variable")
} }
if rootDiskSize := env.Get(config.EnvRootDiskThresholdSize, ""); rootDiskSize != "" { rootDiskSize := env.Get(config.EnvRootDriveThresholdSize, "")
if rootDiskSize == "" {
rootDiskSize = env.Get(config.EnvRootDiskThresholdSize, "")
}
if rootDiskSize != "" {
size, err := humanize.ParseBytes(rootDiskSize) size, err := humanize.ParseBytes(rootDiskSize)
if err != nil { if err != nil {
logger.Fatal(err, fmt.Sprintf("Invalid %s value in environment variable", config.EnvRootDiskThresholdSize)) logger.Fatal(err, fmt.Sprintf("Invalid %s value in root drive threshold environment variable", rootDiskSize))
} }
globalRootDiskThreshold = size globalRootDiskThreshold = size
} }
@@ -802,6 +808,9 @@ func handleKMSConfig() {
logger.Fatal(errors.New("ambigious KMS configuration"), fmt.Sprintf("The environment contains %q as well as %q", kms.EnvKESAPIKey, kms.EnvKESClientCert)) logger.Fatal(errors.New("ambigious KMS configuration"), fmt.Sprintf("The environment contains %q as well as %q", kms.EnvKESAPIKey, kms.EnvKESClientCert))
} }
} }
if !env.IsSet(kms.EnvKESKeyName) {
logger.Fatal(errors.New("Invalid KES configuration"), fmt.Sprintf("The mandatory environment variable %q not set", kms.EnvKESKeyName))
}
var endpoints []string var endpoints []string
for _, endpoint := range strings.Split(env.Get(kms.EnvKESEndpoint, ""), ",") { for _, endpoint := range strings.Split(env.Get(kms.EnvKESEndpoint, ""), ",") {
+1 -1
View File
@@ -71,7 +71,7 @@ func deleteConfig(ctx context.Context, objAPI objectDeleter, configFile string)
} }
func saveConfigWithOpts(ctx context.Context, store objectIO, configFile string, data []byte, opts ObjectOptions) error { func saveConfigWithOpts(ctx context.Context, store objectIO, configFile string, data []byte, opts ObjectOptions) error {
hashReader, err := hash.NewReader(bytes.NewReader(data), int64(len(data)), "", getSHA256Hash(data), int64(len(data))) hashReader, err := hash.NewReader(ctx, bytes.NewReader(data), int64(len(data)), "", getSHA256Hash(data), int64(len(data)))
if err != nil { if err != nil {
return err return err
} }
+2
View File
@@ -66,10 +66,12 @@ const (
scannerMetricYield scannerMetricYield
scannerMetricCleanAbandoned scannerMetricCleanAbandoned
scannerMetricApplyNonCurrent scannerMetricApplyNonCurrent
scannerMetricHealAbandonedVersion
// START Trace metrics: // START Trace metrics:
scannerMetricStartTrace scannerMetricStartTrace
scannerMetricScanObject // Scan object. All operations included. scannerMetricScanObject // Scan object. All operations included.
scannerMetricHealAbandonedObject
// END realtime metrics: // END realtime metrics:
scannerMetricLastRealtime scannerMetricLastRealtime
+45 -33
View File
@@ -54,11 +54,10 @@ const (
dataScannerForceCompactAtFolders = 1_000_000 // Compact when this many subfolders in a single folder (even top level). dataScannerForceCompactAtFolders = 1_000_000 // Compact when this many subfolders in a single folder (even top level).
dataScannerStartDelay = 1 * time.Minute // Time to wait on startup and between cycles. dataScannerStartDelay = 1 * time.Minute // Time to wait on startup and between cycles.
healDeleteDangling = true healDeleteDangling = true
healFolderIncludeProb = 32 // Include a clean folder one in n cycles. healObjectSelectProb = 1024 // Overall probability of a file being scanned; one in n.
healObjectSelectProb = 512 // Overall probability of a file being scanned; one in n.
dataScannerExcessiveVersionsThreshold = 1000 // Issue a warning when a single object has more versions than this dataScannerExcessiveVersionsThreshold = 100 // Issue a warning when a single object has more versions than this
dataScannerExcessiveFoldersThreshold = 50000 // Issue a warning when a folder has more subfolders than this in a *set* dataScannerExcessiveFoldersThreshold = 50000 // Issue a warning when a folder has more subfolders than this in a *set*
) )
@@ -66,7 +65,7 @@ var (
globalHealConfig heal.Config globalHealConfig heal.Config
// Sleeper values are updated when config is loaded. // Sleeper values are updated when config is loaded.
scannerSleeper = newDynamicSleeper(10, 10*time.Second, true) scannerSleeper = newDynamicSleeper(2, time.Second, true) // Keep defaults same as config defaults
scannerCycle = uatomic.NewDuration(dataScannerStartDelay) scannerCycle = uatomic.NewDuration(dataScannerStartDelay)
) )
@@ -274,7 +273,6 @@ type folderScanner struct {
// rarer if the bloom filter for the path is clean and no lifecycles are applied. // rarer if the bloom filter for the path is clean and no lifecycles are applied.
// Skipped leaves have their totals transferred from the previous cycle. // Skipped leaves have their totals transferred from the previous cycle.
// //
// A clean leaf will be included once every healFolderIncludeProb for partial heal scans.
// When selected there is a one in healObjectSelectProb that any object will be chosen for heal scan. // When selected there is a one in healObjectSelectProb that any object will be chosen for heal scan.
// //
// Compaction happens when either: // Compaction happens when either:
@@ -409,9 +407,9 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
var existingFolders, newFolders []cachedFolder var existingFolders, newFolders []cachedFolder
var foundObjects bool var foundObjects bool
err := readDirFn(path.Join(f.root, folder.name), func(entName string, typ os.FileMode) error { err := readDirFn(pathJoin(f.root, folder.name), func(entName string, typ os.FileMode) error {
// Parse // Parse
entName = pathClean(path.Join(folder.name, entName)) entName = pathClean(pathJoin(folder.name, entName))
if entName == "" || entName == folder.name { if entName == "" || entName == folder.name {
if f.dataUsageScannerDebug { if f.dataUsageScannerDebug {
console.Debugf(scannerLogPrefix+" no entity (%s,%s)\n", f.root, entName) console.Debugf(scannerLogPrefix+" no entity (%s,%s)\n", f.root, entName)
@@ -461,7 +459,7 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
// Get file size, ignore errors. // Get file size, ignore errors.
item := scannerItem{ item := scannerItem{
Path: path.Join(f.root, entName), Path: pathJoin(f.root, entName),
Typ: typ, Typ: typ,
bucket: bucket, bucket: bucket,
prefix: path.Dir(prefix), prefix: path.Dir(prefix),
@@ -496,7 +494,7 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
// Object already accounted for, remove from heal map, // Object already accounted for, remove from heal map,
// simply because getSize() function already heals the // simply because getSize() function already heals the
// object. // object.
delete(abandonedChildren, path.Join(item.bucket, item.objectPath())) delete(abandonedChildren, pathJoin(item.bucket, item.objectPath()))
into.addSizes(sz) into.addSizes(sz)
into.Objects++ into.Objects++
@@ -691,6 +689,7 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
reportNotFound: true, reportNotFound: true,
minDisks: f.disksQuorum, minDisks: f.disksQuorum,
agreed: func(entry metaCacheEntry) { agreed: func(entry metaCacheEntry) {
f.updateCurrentPath(entry.name)
if f.dataUsageScannerDebug { if f.dataUsageScannerDebug {
console.Debugf(healObjectsPrefix+" got agreement: %v\n", entry.name) console.Debugf(healObjectsPrefix+" got agreement: %v\n", entry.name)
} }
@@ -704,6 +703,13 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
// this object might be dangling. // this object might be dangling.
entry, _ = entries.firstFound() entry, _ = entries.firstFound()
} }
// wait timer per object.
wait := scannerSleeper.Timer(ctx)
defer wait()
f.updateCurrentPath(entry.name)
stopFn := globalScannerMetrics.log(scannerMetricHealAbandonedObject, f.root, entry.name)
custom := make(map[string]string)
defer stopFn(custom)
if f.dataUsageScannerDebug { if f.dataUsageScannerDebug {
console.Debugf(healObjectsPrefix+" resolved to: %v, dir: %v\n", entry.name, entry.isDir()) console.Debugf(healObjectsPrefix+" resolved to: %v, dir: %v\n", entry.name, entry.isDir())
@@ -713,13 +719,9 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
return return
} }
// wait on timer per object.
wait := scannerSleeper.Timer(ctx)
// We got an entry which we should be able to heal. // We got an entry which we should be able to heal.
fiv, err := entry.fileInfoVersions(bucket) fiv, err := entry.fileInfoVersions(bucket)
if err != nil { if err != nil {
wait()
err := bgSeq.queueHealTask(healSource{ err := bgSeq.queueHealTask(healSource{
bucket: bucket, bucket: bucket,
object: entry.name, object: entry.name,
@@ -732,21 +734,28 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
return return
} }
custom["versions"] = fmt.Sprint(len(fiv.Versions))
var successVersions, failVersions int
for _, ver := range fiv.Versions { for _, ver := range fiv.Versions {
// Sleep and reset. stopFn := globalScannerMetrics.timeSize(scannerMetricHealAbandonedVersion)
wait()
wait = scannerSleeper.Timer(ctx)
err := bgSeq.queueHealTask(healSource{ err := bgSeq.queueHealTask(healSource{
bucket: bucket, bucket: bucket,
object: fiv.Name, object: fiv.Name,
versionID: ver.VersionID, versionID: ver.VersionID,
}, madmin.HealItemObject) }, madmin.HealItemObject)
stopFn(int(ver.Size))
if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) { if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
} }
if err == nil {
successVersions++
} else {
failVersions++
}
foundObjs = foundObjs || err == nil foundObjs = foundObjs || err == nil
} }
custom["success_versions"] = fmt.Sprint(successVersions)
custom["failed_versions"] = fmt.Sprint(failVersions)
}, },
// Too many disks failed. // Too many disks failed.
finished: func(errs []error) { finished: func(errs []error) {
@@ -873,7 +882,7 @@ type getSizeFn func(item scannerItem) (sizeSummary, error)
func (i *scannerItem) transformMetaDir() { func (i *scannerItem) transformMetaDir() {
split := strings.Split(i.prefix, SlashSeparator) split := strings.Split(i.prefix, SlashSeparator)
if len(split) > 1 { if len(split) > 1 {
i.prefix = path.Join(split[:len(split)-1]...) i.prefix = pathJoin(split[:len(split)-1]...)
} else { } else {
i.prefix = "" i.prefix = ""
} }
@@ -920,7 +929,8 @@ func (i *scannerItem) applyLifecycle(ctx context.Context, o ObjectLayer, oi Obje
versionID := oi.VersionID versionID := oi.VersionID
rCfg, _ := globalBucketObjectLockSys.Get(i.bucket) rCfg, _ := globalBucketObjectLockSys.Get(i.bucket)
lcEvt := evalActionFromLifecycle(ctx, *i.lifeCycle, rCfg, oi) replcfg, _ := getReplicationConfig(ctx, i.bucket)
lcEvt := evalActionFromLifecycle(ctx, *i.lifeCycle, rCfg, replcfg, oi)
if i.debug { if i.debug {
if versionID != "" { if versionID != "" {
console.Debugf(applyActionsLogPrefix+" lifecycle: %q (version-id=%s), Initial scan: %v\n", i.objectPath(), versionID, lcEvt.Action) console.Debugf(applyActionsLogPrefix+" lifecycle: %q (version-id=%s), Initial scan: %v\n", i.objectPath(), versionID, lcEvt.Action)
@@ -1061,17 +1071,6 @@ func (i *scannerItem) applyNewerNoncurrentVersionLimit(ctx context.Context, _ Ob
// applyVersionActions will apply lifecycle checks on all versions of a scanned item. Returns versions that remain // applyVersionActions will apply lifecycle checks on all versions of a scanned item. Returns versions that remain
// after applying lifecycle checks configured. // after applying lifecycle checks configured.
func (i *scannerItem) applyVersionActions(ctx context.Context, o ObjectLayer, fivs []FileInfo) ([]ObjectInfo, error) { func (i *scannerItem) applyVersionActions(ctx context.Context, o ObjectLayer, fivs []FileInfo) ([]ObjectInfo, error) {
if i.heal.enabled {
if healDeleteDangling {
done := globalScannerMetrics.time(scannerMetricCleanAbandoned)
err := o.CheckAbandonedParts(ctx, i.bucket, i.objectPath(), madmin.HealOpts{Remove: healDeleteDangling})
done()
if err != nil {
logger.LogIf(ctx, fmt.Errorf("unable to check object %s/%s for abandoned data: %w", i.bucket, i.objectPath(), err))
}
}
}
objInfos, err := i.applyNewerNoncurrentVersionLimit(ctx, o, fivs) objInfos, err := i.applyNewerNoncurrentVersionLimit(ctx, o, fivs)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1112,7 +1111,17 @@ func (i *scannerItem) applyActions(ctx context.Context, o ObjectLayer, oi Object
done := globalScannerMetrics.time(scannerMetricHealCheck) done := globalScannerMetrics.time(scannerMetricHealCheck)
size = i.applyHealing(ctx, o, oi) size = i.applyHealing(ctx, o, oi)
done() done()
if healDeleteDangling {
done := globalScannerMetrics.time(scannerMetricCleanAbandoned)
err := o.CheckAbandonedParts(ctx, i.bucket, i.objectPath(), madmin.HealOpts{Remove: healDeleteDangling})
done()
if err != nil {
logger.LogIf(ctx, fmt.Errorf("unable to check object %s/%s for abandoned data: %w", i.bucket, i.objectPath(), err))
}
}
} }
// replicate only if lifecycle rules are not applied. // replicate only if lifecycle rules are not applied.
done := globalScannerMetrics.time(scannerMetricCheckReplication) done := globalScannerMetrics.time(scannerMetricCheckReplication)
i.healReplication(ctx, o, oi.Clone(), sizeS) i.healReplication(ctx, o, oi.Clone(), sizeS)
@@ -1121,7 +1130,7 @@ func (i *scannerItem) applyActions(ctx context.Context, o ObjectLayer, oi Object
return size return size
} }
func evalActionFromLifecycle(ctx context.Context, lc lifecycle.Lifecycle, lr lock.Retention, obj ObjectInfo) lifecycle.Event { func evalActionFromLifecycle(ctx context.Context, lc lifecycle.Lifecycle, lr lock.Retention, rcfg *replication.Config, obj ObjectInfo) lifecycle.Event {
event := lc.Eval(obj.ToLifecycleOpts()) event := lc.Eval(obj.ToLifecycleOpts())
if serverDebugLog { if serverDebugLog {
console.Debugf(applyActionsLogPrefix+" lifecycle: Secondary scan: %v\n", event.Action) console.Debugf(applyActionsLogPrefix+" lifecycle: Secondary scan: %v\n", event.Action)
@@ -1153,6 +1162,9 @@ func evalActionFromLifecycle(ctx context.Context, lc lifecycle.Lifecycle, lr loc
} }
return lifecycle.Event{Action: lifecycle.NoneAction} return lifecycle.Event{Action: lifecycle.NoneAction}
} }
if rcfg != nil && !obj.VersionPurgeStatus.Empty() && rcfg.HasActiveRules(obj.Name, true) {
return lifecycle.Event{Action: lifecycle.NoneAction}
}
} }
return event return event
@@ -1247,7 +1259,7 @@ func applyLifecycleAction(event lifecycle.Event, src lcEventSrc, obj ObjectInfo)
// objectPath returns the prefix and object name. // objectPath returns the prefix and object name.
func (i *scannerItem) objectPath() string { func (i *scannerItem) objectPath() string {
return path.Join(i.prefix, i.objectName) return pathJoin(i.prefix, i.objectName)
} }
// healReplication will heal a scanned item that has failed replication. // healReplication will heal a scanned item that has failed replication.
+87 -67
View File
@@ -38,6 +38,7 @@ import (
"github.com/minio/minio/internal/hash" "github.com/minio/minio/internal/hash"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/tinylib/msgp/msgp" "github.com/tinylib/msgp/msgp"
"github.com/valyala/bytebufferpool"
) )
//go:generate msgp -file $GOFILE -unexported //go:generate msgp -file $GOFILE -unexported
@@ -346,20 +347,18 @@ func (e *dataUsageEntry) addSizes(summary sizeSummary) {
e.ReplicationStats.ReplicaSize += uint64(summary.replicaSize) e.ReplicationStats.ReplicaSize += uint64(summary.replicaSize)
e.ReplicationStats.ReplicaCount += uint64(summary.replicaCount) e.ReplicationStats.ReplicaCount += uint64(summary.replicaCount)
if summary.replTargetStats != nil { for arn, st := range summary.replTargetStats {
for arn, st := range summary.replTargetStats { tgtStat, ok := e.ReplicationStats.Targets[arn]
tgtStat, ok := e.ReplicationStats.Targets[arn] if !ok {
if !ok { tgtStat = replicationStats{}
tgtStat = replicationStats{}
}
tgtStat.PendingSize += uint64(st.pendingSize)
tgtStat.FailedSize += uint64(st.failedSize)
tgtStat.ReplicatedSize += uint64(st.replicatedSize)
tgtStat.ReplicatedCount += uint64(st.replicatedCount)
tgtStat.FailedCount += st.failedCount
tgtStat.PendingCount += st.pendingCount
e.ReplicationStats.Targets[arn] = tgtStat
} }
tgtStat.PendingSize += uint64(st.pendingSize)
tgtStat.FailedSize += uint64(st.failedSize)
tgtStat.ReplicatedSize += uint64(st.replicatedSize)
tgtStat.ReplicatedCount += uint64(st.replicatedCount)
tgtStat.FailedCount += st.failedCount
tgtStat.PendingCount += st.pendingCount
e.ReplicationStats.Targets[arn] = tgtStat
} }
if summary.tiers != nil { if summary.tiers != nil {
if e.AllTierStats == nil { if e.AllTierStats == nil {
@@ -925,39 +924,55 @@ type objectIO interface {
// load the cache content with name from minioMetaBackgroundOpsBucket. // load the cache content with name from minioMetaBackgroundOpsBucket.
// Only backend errors are returned as errors. // Only backend errors are returned as errors.
// The loader is optimistic and has no locking, but tries 5 times before giving up. // The loader is optimistic and has no locking, but tries 5 times before giving up.
// If the object is not found or unable to deserialize d is cleared and nil error is returned. // If the object is not found, a nil error with empty data usage cache is returned.
func (d *dataUsageCache) load(ctx context.Context, store objectIO, name string) error { func (d *dataUsageCache) load(ctx context.Context, store objectIO, name string) error {
// Abandon if more than 5 minutes, so we don't hold up scanner. // By defaut, empty data usage cache
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute) *d = dataUsageCache{}
defer cancel()
load := func(name string, timeout time.Duration) (bool, error) {
// Abandon if more than time.Minute, so we don't hold up scanner.
// drive timeout by default is 2 minutes, we do not need to wait longer.
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
// Caches are read+written without locks,
retries := 0
for retries < 5 {
r, err := store.GetObjectNInfo(ctx, dataUsageBucket, name, nil, http.Header{}, ObjectOptions{NoLock: true}) r, err := store.GetObjectNInfo(ctx, dataUsageBucket, name, nil, http.Header{}, ObjectOptions{NoLock: true})
if err != nil { if err != nil {
switch err.(type) { switch err.(type) {
case ObjectNotFound, BucketNotFound: case ObjectNotFound, BucketNotFound:
return false, nil
case InsufficientReadQuorum, StorageErr: case InsufficientReadQuorum, StorageErr:
retries++ return true, nil
time.Sleep(time.Duration(rand.Int63n(int64(time.Second))))
continue
default:
return toObjectErr(err, dataUsageBucket, name)
} }
*d = dataUsageCache{} return false, err
return nil
}
if err := d.deserialize(r); err != nil {
r.Close()
retries++
time.Sleep(time.Duration(rand.Int63n(int64(time.Second))))
continue
} }
err = d.deserialize(r)
r.Close() r.Close()
return nil return err != nil, nil
} }
*d = dataUsageCache{}
// Caches are read+written without locks,
retries := 0
for retries < 5 {
retry, err := load(name, time.Minute)
if err != nil {
return toObjectErr(err, dataUsageBucket, name)
}
if !retry {
break
}
retry, err = load(name+".bkp", 30*time.Second)
if err == nil && !retry {
// Only return when we have valid data from the backup
break
}
retries++
time.Sleep(time.Duration(rand.Int63n(int64(time.Second))))
}
if retries == 5 {
logger.LogOnceIf(ctx, fmt.Errorf("maximum retry reached to load the data usage cache `%s`", name), "retry-loading-data-usage-cache")
}
return nil return nil
} }
@@ -967,47 +982,52 @@ var maxConcurrentScannerSaves = make(chan struct{}, 4)
// save the content of the cache to minioMetaBackgroundOpsBucket with the provided name. // save the content of the cache to minioMetaBackgroundOpsBucket with the provided name.
// Note that no locking is done when saving. // Note that no locking is done when saving.
func (d *dataUsageCache) save(ctx context.Context, store objectIO, name string) error { func (d *dataUsageCache) save(ctx context.Context, store objectIO, name string) error {
var r io.Reader select {
maxConcurrentScannerSaves <- struct{}{} case <-ctx.Done():
return ctx.Err()
case maxConcurrentScannerSaves <- struct{}{}:
}
defer func() { defer func() {
<-maxConcurrentScannerSaves select {
}() case <-ctx.Done():
// If big, do streaming... case <-maxConcurrentScannerSaves:
size := int64(-1)
if len(d.Cache) > 10000 {
pr, pw := io.Pipe()
go func() {
pw.CloseWithError(d.serializeTo(pw))
}()
defer pr.Close()
r = pr
} else {
var buf bytes.Buffer
err := d.serializeTo(&buf)
if err != nil {
return err
} }
r = &buf }()
size = int64(buf.Len())
buf := bytebufferpool.Get()
defer func() {
buf.Reset()
bytebufferpool.Put(buf)
}()
if err := d.serializeTo(buf); err != nil {
return err
} }
hr, err := hash.NewReader(r, size, "", "", size) hr, err := hash.NewReader(ctx, bytes.NewReader(buf.Bytes()), int64(buf.Len()), "", "", int64(buf.Len()))
if err != nil { if err != nil {
return err return err
} }
// Abandon if more than 5 minutes, so we don't hold up scanner. save := func(name string, timeout time.Duration) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute) // Abandon if more than a minute, so we don't hold up scanner.
defer cancel() ctx, cancel := context.WithTimeout(ctx, timeout)
_, err = store.PutObject(ctx, defer cancel()
dataUsageBucket,
name, _, err = store.PutObject(ctx,
NewPutObjReader(hr), dataUsageBucket,
ObjectOptions{NoLock: true}) name,
if isErrBucketNotFound(err) { NewPutObjReader(hr),
return nil ObjectOptions{NoLock: true})
if isErrBucketNotFound(err) {
return nil
}
return err
} }
return err defer save(name+".bkp", 30*time.Second) // Keep a backup as well
// drive timeout by default is 2 minutes, we do not need to wait longer.
return save(name, time.Minute)
} }
// dataUsageCacheVer indicates the cache version. // dataUsageCacheVer indicates the cache version.
+48 -19
View File
@@ -21,6 +21,7 @@ import (
"context" "context"
"errors" "errors"
"strings" "strings"
"time"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
@@ -42,6 +43,7 @@ const (
// storeDataUsageInBackend will store all objects sent on the gui channel until closed. // storeDataUsageInBackend will store all objects sent on the gui channel until closed.
func storeDataUsageInBackend(ctx context.Context, objAPI ObjectLayer, dui <-chan DataUsageInfo) { func storeDataUsageInBackend(ctx context.Context, objAPI ObjectLayer, dui <-chan DataUsageInfo) {
attempts := 1
for dataUsageInfo := range dui { for dataUsageInfo := range dui {
json := jsoniter.ConfigCompatibleWithStandardLibrary json := jsoniter.ConfigCompatibleWithStandardLibrary
dataUsageJSON, err := json.Marshal(dataUsageInfo) dataUsageJSON, err := json.Marshal(dataUsageInfo)
@@ -49,12 +51,19 @@ func storeDataUsageInBackend(ctx context.Context, objAPI ObjectLayer, dui <-chan
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
continue continue
} }
if attempts > 10 {
saveConfig(ctx, objAPI, dataUsageObjNamePath+".bkp", dataUsageJSON) // Save a backup every 10th update.
attempts = 1
}
if err = saveConfig(ctx, objAPI, dataUsageObjNamePath, dataUsageJSON); err != nil { if err = saveConfig(ctx, objAPI, dataUsageObjNamePath, dataUsageJSON); err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
} }
attempts++
} }
} }
var prefixUsageCache timedValue
// loadPrefixUsageFromBackend returns prefix usages found in passed buckets // loadPrefixUsageFromBackend returns prefix usages found in passed buckets
// //
// e.g.: /testbucket/prefix => 355601334 // e.g.: /testbucket/prefix => 355601334
@@ -67,37 +76,57 @@ func loadPrefixUsageFromBackend(ctx context.Context, objAPI ObjectLayer, bucket
cache := dataUsageCache{} cache := dataUsageCache{}
m := make(map[string]uint64) prefixUsageCache.Once.Do(func() {
for _, pool := range z.serverPools { prefixUsageCache.TTL = 30 * time.Second
for _, er := range pool.sets {
// Load bucket usage prefixes
if err := cache.load(ctx, er, bucket+slashSeparator+dataUsageCacheName); err == nil {
root := cache.find(bucket)
if root == nil {
// We dont have usage information for this bucket in this
// set, go to the next set
continue
}
for id, usageInfo := range cache.flattenChildrens(*root) { // No need to fail upon Update() error, fallback to old value.
prefix := decodeDirObject(strings.TrimPrefix(id, bucket+slashSeparator)) prefixUsageCache.Relax = true
// decodeDirObject to avoid any __XLDIR__ objects prefixUsageCache.Update = func() (interface{}, error) {
m[prefix] += uint64(usageInfo.Size) m := make(map[string]uint64)
for _, pool := range z.serverPools {
for _, er := range pool.sets {
// Load bucket usage prefixes
ctx, done := context.WithTimeout(context.Background(), 2*time.Second)
ok := cache.load(ctx, er, bucket+slashSeparator+dataUsageCacheName) == nil
done()
if ok {
root := cache.find(bucket)
if root == nil {
// We dont have usage information for this bucket in this
// set, go to the next set
continue
}
for id, usageInfo := range cache.flattenChildrens(*root) {
prefix := decodeDirObject(strings.TrimPrefix(id, bucket+slashSeparator))
// decodeDirObject to avoid any __XLDIR__ objects
m[prefix] += uint64(usageInfo.Size)
}
}
} }
} }
return m, nil
} }
})
v, _ := prefixUsageCache.Get()
if v != nil {
return v.(map[string]uint64), nil
} }
return m, nil return map[string]uint64{}, nil
} }
func loadDataUsageFromBackend(ctx context.Context, objAPI ObjectLayer) (DataUsageInfo, error) { func loadDataUsageFromBackend(ctx context.Context, objAPI ObjectLayer) (DataUsageInfo, error) {
buf, err := readConfig(ctx, objAPI, dataUsageObjNamePath) buf, err := readConfig(ctx, objAPI, dataUsageObjNamePath)
if err != nil { if err != nil {
if errors.Is(err, errConfigNotFound) { buf, err = readConfig(ctx, objAPI, dataUsageObjNamePath+".bkp")
return DataUsageInfo{}, nil if err != nil {
if errors.Is(err, errConfigNotFound) {
return DataUsageInfo{}, nil
}
return DataUsageInfo{}, toObjectErr(err, minioMetaBucket, dataUsageObjNamePath)
} }
return DataUsageInfo{}, toObjectErr(err, minioMetaBucket, dataUsageObjNamePath)
} }
var dataUsageInfo DataUsageInfo var dataUsageInfo DataUsageInfo
+5 -5
View File
@@ -604,7 +604,7 @@ func (c *diskCache) SaveMetadata(ctx context.Context, bucket, object string, met
// move part saved in writeback directory and cache.json atomically // move part saved in writeback directory and cache.json atomically
if finalizeWB { if finalizeWB {
wbdir := getCacheWriteBackSHADir(c.dir, bucket, object) wbdir := getCacheWriteBackSHADir(c.dir, bucket, object)
if err = renameAll(pathJoin(wbdir, cacheDataFile), pathJoin(cachedPath, cacheDataFile)); err != nil { if err = renameAll(pathJoin(wbdir, cacheDataFile), pathJoin(cachedPath, cacheDataFile), c.dir); err != nil {
return err return err
} }
removeAll(wbdir) // cleanup writeback/shadir removeAll(wbdir) // cleanup writeback/shadir
@@ -1273,7 +1273,7 @@ func (c *diskCache) NewMultipartUpload(ctx context.Context, bucket, object, uID
cachePath := getMultipartCacheSHADir(c.dir, bucket, object) cachePath := getMultipartCacheSHADir(c.dir, bucket, object)
uploadIDDir := path.Join(cachePath, uploadID) uploadIDDir := path.Join(cachePath, uploadID)
if err := mkdirAll(uploadIDDir, 0o777); err != nil { if err := mkdirAll(uploadIDDir, 0o777, c.dir); err != nil {
return uploadID, err return uploadID, err
} }
metaPath := pathJoin(uploadIDDir, cacheMetaJSONFile) metaPath := pathJoin(uploadIDDir, cacheMetaJSONFile)
@@ -1441,7 +1441,7 @@ func newCachePartEncryptReader(ctx context.Context, bucket, object string, partI
info := ObjectInfo{Size: size} info := ObjectInfo{Size: size}
wantSize = info.EncryptedSize() wantSize = info.EncryptedSize()
} }
hReader, err := hash.NewReader(content, wantSize, "", "", size) hReader, err := hash.NewReader(ctx, content, wantSize, "", "", size)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1570,9 +1570,9 @@ func (c *diskCache) CompleteMultipartUpload(ctx context.Context, bucket, object,
jsonSave(f, uploadMeta) jsonSave(f, uploadMeta)
for _, pi := range uploadedParts { for _, pi := range uploadedParts {
part := fmt.Sprintf("part.%d", pi.PartNumber) part := fmt.Sprintf("part.%d", pi.PartNumber)
renameAll(pathJoin(uploadIDDir, part), pathJoin(cachePath, part)) renameAll(pathJoin(uploadIDDir, part), pathJoin(cachePath, part), c.dir)
} }
renameAll(pathJoin(uploadIDDir, cacheMetaJSONFile), pathJoin(cachePath, cacheMetaJSONFile)) renameAll(pathJoin(uploadIDDir, cacheMetaJSONFile), pathJoin(cachePath, cacheMetaJSONFile), c.dir)
removeAll(uploadIDDir) // clean up any unused parts in the uploadIDDir removeAll(uploadIDDir) // clean up any unused parts in the uploadIDDir
return uploadMeta.ToObjectInfo(), nil return uploadMeta.ToObjectInfo(), nil
} }
+3 -3
View File
@@ -734,7 +734,7 @@ func (c *cacheObjects) PutObject(ctx context.Context, bucket, object string, r *
defer cLock.Unlock(lkctx) defer cLock.Unlock(lkctx)
// Initialize pipe to stream data to backend // Initialize pipe to stream data to backend
pipeReader, pipeWriter := io.Pipe() pipeReader, pipeWriter := io.Pipe()
hashReader, err := hash.NewReader(pipeReader, size, "", "", r.ActualSize()) hashReader, err := hash.NewReader(ctx, pipeReader, size, "", "", r.ActualSize())
if err != nil { if err != nil {
return return
} }
@@ -795,7 +795,7 @@ func (c *cacheObjects) uploadObject(ctx context.Context, oi ObjectInfo) {
if st == CommitComplete || st.String() == "" { if st == CommitComplete || st.String() == "" {
return return
} }
hashReader, err := hash.NewReader(cReader, oi.Size, "", "", oi.Size) hashReader, err := hash.NewReader(ctx, cReader, oi.Size, "", "", oi.Size)
if err != nil { if err != nil {
return return
} }
@@ -1059,7 +1059,7 @@ func (c *cacheObjects) PutObjectPart(ctx context.Context, bucket, object, upload
info = PartInfo{} info = PartInfo{}
// Initialize pipe to stream data to backend // Initialize pipe to stream data to backend
pipeReader, pipeWriter := io.Pipe() pipeReader, pipeWriter := io.Pipe()
hashReader, err := hash.NewReader(pipeReader, size, "", "", data.ActualSize()) hashReader, err := hash.NewReader(ctx, pipeReader, size, "", "", data.ActualSize())
if err != nil { if err != nil {
return return
} }
+1 -1
View File
@@ -728,7 +728,7 @@ func (d *DecryptBlocksReader) Read(p []byte) (int, error) {
// DecryptedSize returns the size of the object after decryption in bytes. // DecryptedSize returns the size of the object after decryption in bytes.
// It returns an error if the object is not encrypted or marked as encrypted // It returns an error if the object is not encrypted or marked as encrypted
// but has an invalid size. // but has an invalid size.
func (o *ObjectInfo) DecryptedSize() (int64, error) { func (o ObjectInfo) DecryptedSize() (int64, error) {
if _, ok := crypto.IsEncrypted(o.UserDefined); !ok { if _, ok := crypto.IsEncrypted(o.UserDefined); !ok {
return 0, errors.New("Cannot compute decrypted size of an unencrypted object") return 0, errors.New("Cannot compute decrypted size of an unencrypted object")
} }
-56
View File
@@ -74,62 +74,6 @@ func (er erasureObjects) getLoadBalancedLocalDisks() (newDisks []StorageAPI) {
return newDisks return newDisks
} }
// getLoadBalancedDisks - fetches load balanced (sufficiently randomized) disk slice.
// ensures to skip disks if they are not healing and online.
func (er erasureObjects) getLoadBalancedDisks(optimized bool) []StorageAPI {
disks := er.getDisks()
r := rand.New(rand.NewSource(time.Now().UnixNano()))
if !optimized {
var newDisks []StorageAPI
for _, i := range r.Perm(len(disks)) {
newDisks = append(newDisks, disks[i])
}
return newDisks
}
var wg sync.WaitGroup
var mu sync.Mutex
newDisks := map[uint64][]StorageAPI{}
// Based on the random shuffling return back randomized disks.
for _, i := range r.Perm(len(disks)) {
i := i
wg.Add(1)
go func() {
defer wg.Done()
if disks[i] == nil {
return
}
di, err := disks[i].DiskInfo(context.Background(), false)
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])
mu.Unlock()
}()
}
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]
}
// readMultipleFiles Reads raw data from all specified files from all disks. // readMultipleFiles Reads raw data from all specified files from all disks.
func readMultipleFiles(ctx context.Context, disks []StorageAPI, req ReadMultipleReq, readQuorum int) ([]ReadMultipleResp, error) { func readMultipleFiles(ctx context.Context, disks []StorageAPI, req ReadMultipleReq, readQuorum int) ([]ReadMultipleResp, error) {
resps := make([]chan ReadMultipleResp, len(disks)) resps := make([]chan ReadMultipleResp, len(disks))
+36 -5
View File
@@ -563,11 +563,26 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
} }
if !latestMeta.XLV1 && !latestMeta.Deleted && !recreate && disksToHealCount > latestMeta.Erasure.ParityBlocks { if !latestMeta.XLV1 && !latestMeta.Deleted && !recreate && disksToHealCount > latestMeta.Erasure.ParityBlocks {
// When disk to heal count is greater than parity blocks we should simply error out. // Allow for dangling deletes, on versions that have DataDir missing etc.
err := fmt.Errorf("(%d > %d) more drives are expected to heal than parity, returned errors: %v (dataErrs %v) -> %s/%s(%s)", disksToHealCount, latestMeta.Erasure.ParityBlocks, errs, dataErrs, bucket, object, versionID) // this would end up restoring the correct readable versions.
logger.LogOnceIf(ctx, err, "heal-object-count-gt-parity") m, err := er.deleteIfDangling(ctx, bucket, object, partsMetadata, errs, dataErrs, ObjectOptions{
return er.defaultHealResult(latestMeta, storageDisks, storageEndpoints, errs, VersionID: versionID,
bucket, object, versionID), err })
errs = make([]error, len(errs))
for i := range errs {
errs[i] = err
}
if err == nil {
// Dangling object successfully purged, size is '0'
m.Size = 0
}
// Generate file/version not found with default heal result
err = errFileNotFound
if versionID != "" {
err = errFileVersionNotFound
}
return er.defaultHealResult(m, storageDisks, storageEndpoints,
errs, bucket, object, versionID), err
} }
cleanFileInfo := func(fi FileInfo) FileInfo { cleanFileInfo := func(fi FileInfo) FileInfo {
@@ -1077,6 +1092,22 @@ func isObjectDangling(metaArr []FileInfo, errs []error, dataErrs []error) (valid
} }
if !validMeta.IsValid() { if !validMeta.IsValid() {
// validMeta is invalid because notFoundPartsErrs is
// greater than parity blocks, thus invalidating the FileInfo{}
// every dataErrs[i], metaArr[i] is an empty FileInfo{}
dataBlocks := (len(ndataErrs) + 1) / 2
if notFoundPartsErrs > dataBlocks {
// Not using parity to ensure that we do not delete
// any valid content, if any is recoverable. But if
// notFoundDataDirs are already greater than the data
// blocks all bets are off and it is safe to purge.
//
// This is purely a defensive code, ideally parityBlocks
// is sufficient, however we can't know that since we
// do have the FileInfo{}.
return validMeta, true
}
// We have no idea what this file is, leave it as is. // We have no idea what this file is, leave it as is.
return validMeta, false return validMeta, false
} }
+12 -2
View File
@@ -269,7 +269,13 @@ func (er erasureObjects) ListMultipartUploads(ctx context.Context, bucket, objec
var uploadIDs []string var uploadIDs []string
var disk StorageAPI var disk StorageAPI
for _, disk = range er.getLoadBalancedDisks(true) { disks := er.getLoadBalancedLocalDisks()
if len(disks) == 0 {
// using er.getLoadBalancedLocalDisks() has one side-affect where
// on a pooled setup all disks are remote, add a fallback
disks = er.getOnlineDisks()
}
for _, disk = range disks {
uploadIDs, err = disk.ListDir(ctx, minioMetaMultipartBucket, er.getMultipartSHADir(bucket, object), -1) uploadIDs, err = disk.ListDir(ctx, minioMetaMultipartBucket, er.getMultipartSHADir(bucket, object), -1)
if err != nil { if err != nil {
if errors.Is(err, errDiskNotFound) { if errors.Is(err, errDiskNotFound) {
@@ -1242,7 +1248,11 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
} }
} }
defer er.deleteAll(context.Background(), minioMetaMultipartBucket, uploadIDPath) defer func() {
if err == nil {
er.deleteAll(context.Background(), minioMetaMultipartBucket, uploadIDPath)
}
}()
// Rename the multipart object to final location. // Rename the multipart object to final location.
onlineDisks, versionsDisparity, err := renameData(ctx, onlineDisks, minioMetaMultipartBucket, uploadIDPath, onlineDisks, versionsDisparity, err := renameData(ctx, onlineDisks, minioMetaMultipartBucket, uploadIDPath,
+17 -4
View File
@@ -46,6 +46,7 @@ import (
"github.com/minio/pkg/v2/mimedb" "github.com/minio/pkg/v2/mimedb"
"github.com/minio/pkg/v2/sync/errgroup" "github.com/minio/pkg/v2/sync/errgroup"
"github.com/minio/pkg/v2/wildcard" "github.com/minio/pkg/v2/wildcard"
"github.com/tinylib/msgp/msgp"
uatomic "go.uber.org/atomic" uatomic "go.uber.org/atomic"
) )
@@ -497,7 +498,14 @@ func (er erasureObjects) deleteIfDangling(ctx context.Context, bucket, object st
tags := make(map[string]interface{}, 4) tags := make(map[string]interface{}, 4)
tags["set"] = er.setIndex tags["set"] = er.setIndex
tags["pool"] = er.poolIndex tags["pool"] = er.poolIndex
tags["parity"] = m.Erasure.ParityBlocks tags["merrs"] = errors.Join(errs...)
tags["derrs"] = errors.Join(dataErrs...)
if m.IsValid() {
tags["size"] = m.Size
tags["mtime"] = m.ModTime.Format(http.TimeFormat)
tags["parity"] = m.Erasure.ParityBlocks
}
if cok { if cok {
tags["caller"] = fmt.Sprintf("%s:%d", file, line) tags["caller"] = fmt.Sprintf("%s:%d", file, line)
} }
@@ -578,6 +586,9 @@ func readAllXL(ctx context.Context, disks []StorageAPI, bucket, object string, r
errFileVersionNotFound, errFileVersionNotFound,
io.ErrUnexpectedEOF, // some times we would read without locks, ignore these errors io.ErrUnexpectedEOF, // some times we would read without locks, ignore these errors
io.EOF, // some times we would read without locks, ignore these errors io.EOF, // some times we would read without locks, ignore these errors
msgp.ErrShortBytes,
context.DeadlineExceeded,
context.Canceled,
} }
ignoredErrs = append(ignoredErrs, objectOpIgnoredErrs...) ignoredErrs = append(ignoredErrs, objectOpIgnoredErrs...)
@@ -1617,10 +1628,12 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
var lc *lifecycle.Lifecycle var lc *lifecycle.Lifecycle
var rcfg lock.Retention var rcfg lock.Retention
var replcfg *replication.Config
if opts.Expiration.Expire { if opts.Expiration.Expire {
// Check if the current bucket has a configured lifecycle policy // Check if the current bucket has a configured lifecycle policy
lc, _ = globalLifecycleSys.Get(bucket) lc, _ = globalLifecycleSys.Get(bucket)
rcfg, _ = globalBucketObjectLockSys.Get(bucket) rcfg, _ = globalBucketObjectLockSys.Get(bucket)
replcfg, _ = getReplicationConfig(ctx, bucket)
} }
// expiration attempted on a bucket with no lifecycle // expiration attempted on a bucket with no lifecycle
@@ -1673,7 +1686,7 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
if opts.Expiration.Expire { if opts.Expiration.Expire {
if gerr == nil { if gerr == nil {
evt := evalActionFromLifecycle(ctx, *lc, rcfg, goi) evt := evalActionFromLifecycle(ctx, *lc, rcfg, replcfg, goi)
var isErr bool var isErr bool
switch evt.Action { switch evt.Action {
case lifecycle.NoneAction: case lifecycle.NoneAction:
@@ -2151,7 +2164,7 @@ func (er erasureObjects) restoreTransitionedObject(ctx context.Context, bucket s
return setRestoreHeaderFn(oi, toObjectErr(err, bucket, object)) return setRestoreHeaderFn(oi, toObjectErr(err, bucket, object))
} }
defer gr.Close() defer gr.Close()
hashReader, err := hash.NewReader(gr, gr.ObjInfo.Size, "", "", gr.ObjInfo.Size) hashReader, err := hash.NewReader(ctx, gr, gr.ObjInfo.Size, "", "", gr.ObjInfo.Size)
if err != nil { if err != nil {
return setRestoreHeaderFn(oi, toObjectErr(err, bucket, object)) return setRestoreHeaderFn(oi, toObjectErr(err, bucket, object))
} }
@@ -2176,7 +2189,7 @@ func (er erasureObjects) restoreTransitionedObject(ctx context.Context, bucket s
// rehydrate the parts back on disk as per the original xl.meta prior to transition // rehydrate the parts back on disk as per the original xl.meta prior to transition
for _, partInfo := range oi.Parts { for _, partInfo := range oi.Parts {
hr, err := hash.NewReader(io.LimitReader(gr, partInfo.Size), partInfo.Size, "", "", partInfo.Size) hr, err := hash.NewReader(ctx, io.LimitReader(gr, partInfo.Size), partInfo.Size, "", "", partInfo.Size)
if err != nil { if err != nil {
return setRestoreHeaderFn(oi, err) return setRestoreHeaderFn(oi, err)
} }
+60 -21
View File
@@ -68,6 +68,34 @@ type PoolDecommissionInfo struct {
BytesFailed int64 `json:"bytesDecommissionedFailed" msg:"bf"` BytesFailed int64 `json:"bytesDecommissionedFailed" msg:"bf"`
} }
// Clone make a copy of PoolDecommissionInfo
func (pd *PoolDecommissionInfo) Clone() *PoolDecommissionInfo {
if pd == nil {
return nil
}
if pd.StartTime.IsZero() {
return nil
}
return &PoolDecommissionInfo{
StartTime: pd.StartTime,
StartSize: pd.StartSize,
TotalSize: pd.TotalSize,
CurrentSize: pd.CurrentSize,
Complete: pd.Complete,
Failed: pd.Failed,
Canceled: pd.Canceled,
QueuedBuckets: pd.QueuedBuckets,
DecommissionedBuckets: pd.DecommissionedBuckets,
Bucket: pd.Bucket,
Prefix: pd.Prefix,
Object: pd.Object,
ItemsDecommissioned: pd.ItemsDecommissioned,
ItemsDecommissionFailed: pd.ItemsDecommissionFailed,
BytesDone: pd.BytesDone,
BytesFailed: pd.BytesFailed,
}
}
// bucketPop should be called when a bucket is done decommissioning. // bucketPop should be called when a bucket is done decommissioning.
// Adds the bucket to the list of decommissioned buckets and updates resume numbers. // Adds the bucket to the list of decommissioned buckets and updates resume numbers.
func (pd *PoolDecommissionInfo) bucketPop(bucket string) { func (pd *PoolDecommissionInfo) bucketPop(bucket string) {
@@ -118,6 +146,16 @@ type PoolStatus struct {
Decommission *PoolDecommissionInfo `json:"decommissionInfo,omitempty" msg:"dec"` Decommission *PoolDecommissionInfo `json:"decommissionInfo,omitempty" msg:"dec"`
} }
// Clone returns a copy of PoolStatus
func (ps PoolStatus) Clone() PoolStatus {
return PoolStatus{
ID: ps.ID,
CmdLine: ps.CmdLine,
LastUpdate: ps.LastUpdate,
Decommission: ps.Decommission.Clone(),
}
}
//go:generate msgp -file $GOFILE -unexported //go:generate msgp -file $GOFILE -unexported
type poolMeta struct { type poolMeta struct {
Version int `msg:"v"` Version int `msg:"v"`
@@ -375,16 +413,17 @@ func (p *poolMeta) load(ctx context.Context, pool *erasureSets, pools []*erasure
func (p *poolMeta) CountItem(idx int, size int64, failed bool) { func (p *poolMeta) CountItem(idx int, size int64, failed bool) {
pd := p.Pools[idx].Decommission pd := p.Pools[idx].Decommission
if pd != nil { if pd == nil {
if failed { return
pd.ItemsDecommissionFailed++
pd.BytesFailed += size
} else {
pd.ItemsDecommissioned++
pd.BytesDone += size
}
p.Pools[idx].Decommission = pd
} }
if failed {
pd.ItemsDecommissionFailed++
pd.BytesFailed += size
} else {
pd.ItemsDecommissioned++
pd.BytesDone += size
}
p.Pools[idx].Decommission = pd
} }
func (p *poolMeta) updateAfter(ctx context.Context, idx int, pools []*erasureSets, duration time.Duration) (bool, error) { func (p *poolMeta) updateAfter(ctx context.Context, idx int, pools []*erasureSets, duration time.Duration) (bool, error) {
@@ -569,7 +608,7 @@ func (z *erasureServerPools) decommissionObject(ctx context.Context, bucket stri
defer z.AbortMultipartUpload(ctx, bucket, objInfo.Name, res.UploadID, ObjectOptions{}) defer z.AbortMultipartUpload(ctx, bucket, objInfo.Name, res.UploadID, ObjectOptions{})
parts := make([]CompletePart, len(objInfo.Parts)) parts := make([]CompletePart, len(objInfo.Parts))
for i, part := range objInfo.Parts { for i, part := range objInfo.Parts {
hr, err := hash.NewReader(io.LimitReader(gr, part.Size), part.Size, "", "", part.ActualSize) hr, err := hash.NewReader(ctx, io.LimitReader(gr, part.Size), part.Size, "", "", part.ActualSize)
if err != nil { if err != nil {
return fmt.Errorf("decommissionObject: hash.NewReader() %w", err) return fmt.Errorf("decommissionObject: hash.NewReader() %w", err)
} }
@@ -603,7 +642,7 @@ func (z *erasureServerPools) decommissionObject(ctx context.Context, bucket stri
return err return err
} }
hr, err := hash.NewReader(io.LimitReader(gr, objInfo.Size), objInfo.Size, "", "", actualSize) hr, err := hash.NewReader(ctx, io.LimitReader(gr, objInfo.Size), objInfo.Size, "", "", actualSize)
if err != nil { if err != nil {
return fmt.Errorf("decommissionObject: hash.NewReader() %w", err) return fmt.Errorf("decommissionObject: hash.NewReader() %w", err)
} }
@@ -695,8 +734,7 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
// Check if bucket is object locked. // Check if bucket is object locked.
lr, _ := globalBucketObjectLockSys.Get(bi.Name) lr, _ := globalBucketObjectLockSys.Get(bi.Name)
rcfg, _ := getReplicationConfig(ctx, bi.Name)
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for setIdx, set := range pool.sets { for setIdx, set := range pool.sets {
set := set set := set
@@ -708,7 +746,7 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
versioned := vc != nil && vc.Versioned(object) versioned := vc != nil && vc.Versioned(object)
objInfo := fi.ToObjectInfo(bucket, object, versioned) objInfo := fi.ToObjectInfo(bucket, object, versioned)
evt := evalActionFromLifecycle(ctx, *lc, lr, objInfo) evt := evalActionFromLifecycle(ctx, *lc, lr, rcfg, objInfo)
switch { switch {
case evt.Action.DeleteRestored(): // if restored copy has expired,delete it synchronously case evt.Action.DeleteRestored(): // if restored copy has expired,delete it synchronously
applyExpiryOnTransitionedObject(ctx, z, objInfo, evt, lcEventSrc_Decom) applyExpiryOnTransitionedObject(ctx, z, objInfo, evt, lcEventSrc_Decom)
@@ -908,11 +946,11 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
go decommissionEntry(entry) go decommissionEntry(entry)
}, },
) )
if err == nil { if err == nil || errors.Is(err, context.Canceled) {
break break
} }
setN := humanize.Ordinal(setIdx + 1) setN := humanize.Ordinal(setIdx + 1)
retryDur := time.Duration(r.Float64() * float64(5*time.Second)) retryDur := time.Duration(rand.Float64() * float64(5*time.Second))
logger.LogOnceIf(ctx, fmt.Errorf("listing objects from %s set failed with %v, retrying in %v", setN, err, retryDur), "decom-listing-failed"+setN) logger.LogOnceIf(ctx, fmt.Errorf("listing objects from %s set failed with %v, retrying in %v", setN, err, retryDur), "decom-listing-failed"+setN)
time.Sleep(retryDur) time.Sleep(retryDur)
} }
@@ -1011,6 +1049,7 @@ func (z *erasureServerPools) checkAfterDecom(ctx context.Context, idx int) error
// Check if bucket is object locked. // Check if bucket is object locked.
lr, _ := globalBucketObjectLockSys.Get(bi.Name) lr, _ := globalBucketObjectLockSys.Get(bi.Name)
rcfg, _ := getReplicationConfig(ctx, bi.Name)
filterLifecycle := func(bucket, object string, fi FileInfo) bool { filterLifecycle := func(bucket, object string, fi FileInfo) bool {
if lc == nil { if lc == nil {
@@ -1019,7 +1058,7 @@ func (z *erasureServerPools) checkAfterDecom(ctx context.Context, idx int) error
versioned := vc != nil && vc.Versioned(object) versioned := vc != nil && vc.Versioned(object)
objInfo := fi.ToObjectInfo(bucket, object, versioned) objInfo := fi.ToObjectInfo(bucket, object, versioned)
evt := evalActionFromLifecycle(ctx, *lc, lr, objInfo) evt := evalActionFromLifecycle(ctx, *lc, lr, rcfg, objInfo)
switch { switch {
case evt.Action.DeleteRestored(): // if restored copy has expired,delete it synchronously case evt.Action.DeleteRestored(): // if restored copy has expired,delete it synchronously
applyExpiryOnTransitionedObject(ctx, z, objInfo, evt, lcEventSrc_Decom) applyExpiryOnTransitionedObject(ctx, z, objInfo, evt, lcEventSrc_Decom)
@@ -1185,15 +1224,15 @@ func (z *erasureServerPools) Status(ctx context.Context, idx int) (PoolStatus, e
return PoolStatus{}, errInvalidArgument return PoolStatus{}, errInvalidArgument
} }
z.poolMetaMutex.RLock()
defer z.poolMetaMutex.RUnlock()
pi, err := z.getDecommissionPoolSpaceInfo(idx) pi, err := z.getDecommissionPoolSpaceInfo(idx)
if err != nil { if err != nil {
return PoolStatus{}, err return PoolStatus{}, err
} }
poolInfo := z.poolMeta.Pools[idx] z.poolMetaMutex.RLock()
defer z.poolMetaMutex.RUnlock()
poolInfo := z.poolMeta.Pools[idx].Clone()
if poolInfo.Decommission != nil { if poolInfo.Decommission != nil {
poolInfo.Decommission.TotalSize = pi.Total poolInfo.Decommission.TotalSize = pi.Total
if poolInfo.Decommission.Failed || poolInfo.Decommission.Canceled { if poolInfo.Decommission.Failed || poolInfo.Decommission.Canceled {
+4 -3
View File
@@ -440,6 +440,7 @@ func (z *erasureServerPools) rebalanceBucket(ctx context.Context, bucket string,
lc, _ := globalLifecycleSys.Get(bucket) lc, _ := globalLifecycleSys.Get(bucket)
// Check if bucket is object locked. // Check if bucket is object locked.
lr, _ := globalBucketObjectLockSys.Get(bucket) lr, _ := globalBucketObjectLockSys.Get(bucket)
rcfg, _ := getReplicationConfig(ctx, bucket)
pool := z.serverPools[poolIdx] pool := z.serverPools[poolIdx]
const envRebalanceWorkers = "_MINIO_REBALANCE_WORKERS" const envRebalanceWorkers = "_MINIO_REBALANCE_WORKERS"
@@ -467,7 +468,7 @@ func (z *erasureServerPools) rebalanceBucket(ctx context.Context, bucket string,
versioned := vc != nil && vc.Versioned(object) versioned := vc != nil && vc.Versioned(object)
objInfo := fi.ToObjectInfo(bucket, object, versioned) objInfo := fi.ToObjectInfo(bucket, object, versioned)
evt := evalActionFromLifecycle(ctx, *lc, lr, objInfo) evt := evalActionFromLifecycle(ctx, *lc, lr, rcfg, objInfo)
if evt.Action.Delete() { if evt.Action.Delete() {
globalExpiryState.enqueueByDays(objInfo, evt, lcEventSrc_Rebal) globalExpiryState.enqueueByDays(objInfo, evt, lcEventSrc_Rebal)
return true return true
@@ -736,7 +737,7 @@ func (z *erasureServerPools) rebalanceObject(ctx context.Context, bucket string,
parts := make([]CompletePart, len(oi.Parts)) parts := make([]CompletePart, len(oi.Parts))
for i, part := range oi.Parts { for i, part := range oi.Parts {
hr, err := hash.NewReader(io.LimitReader(gr, part.Size), part.Size, "", "", part.ActualSize) hr, err := hash.NewReader(ctx, io.LimitReader(gr, part.Size), part.Size, "", "", part.ActualSize)
if err != nil { if err != nil {
return fmt.Errorf("rebalanceObject: hash.NewReader() %w", err) return fmt.Errorf("rebalanceObject: hash.NewReader() %w", err)
} }
@@ -766,7 +767,7 @@ func (z *erasureServerPools) rebalanceObject(ctx context.Context, bucket string,
return err return err
} }
hr, err := hash.NewReader(gr, oi.Size, "", "", actualSize) hr, err := hash.NewReader(ctx, gr, oi.Size, "", "", actualSize)
if err != nil { if err != nil {
return fmt.Errorf("rebalanceObject: hash.NewReader() %w", err) return fmt.Errorf("rebalanceObject: hash.NewReader() %w", err)
} }
+113 -1
View File
@@ -25,6 +25,7 @@ import (
"io" "io"
"math/rand" "math/rand"
"net/http" "net/http"
"path"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -1323,6 +1324,85 @@ func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, ma
} }
opts.setBucketMeta(ctx) opts.setBucketMeta(ctx)
ri := logger.GetReqInfo(ctx)
hadoop := ri != nil && strings.Contains(ri.UserAgent, `Hadoop `) && strings.Contains(ri.UserAgent, "scala/")
matches := func() bool {
if prefix == "" {
return false
}
// List of standard files supported by s3a
// that involves a List() on a directory
// where directory is actually an object on
// namespace.
for _, k := range []string{
"_SUCCESS/",
".parquet/",
".csv/",
".json/",
".avro/",
".orc/",
".txt/",
// Add any other files in future
} {
if strings.HasSuffix(prefix, k) {
return true
}
}
return false
}
if hadoop && matches() && delimiter == SlashSeparator && maxKeys == 2 && marker == "" {
// Optimization for Spark/Hadoop workload where spark sends a garbage
// request of this kind
//
// GET /testbucket/?list-type=2&delimiter=%2F&max-keys=2&prefix=parquet%2F_SUCCESS%2F&fetch-owner=false
//
// Here spark is expecting that the List() return empty instead, so from MinIO's point
// of view if we simply do a GetObjectInfo() on this prefix by treating it as an object
// We save a lot of calls over the network.
//
// This happens repeatedly for all objects that are created concurrently() avoiding this
// as a List() call is an important performance improvement.
//
// Spark based s3a committers are a big enough use-case to have this optimization.
//
// A sample code to see the improvements is as follows, this sample code is
// simply a read on JSON from MinIO and write it back as "parquet".
//
// import org.apache.spark.sql.SparkSession
// import org.apache.spark.sql.types.{IntegerType, StringType, StructField, StructType}
// object SparkJSONRead {
// def main(args: Array[String]): Unit = {
// val spark:SparkSession = SparkSession.builder()
// .appName("SparkByExample")
// .master("local[1]").getOrCreate()
//
// spark.sparkContext.setLogLevel("ERROR")
// spark.sparkContext.hadoopConfiguration.set("fs.s3a.endpoint", "http://minio-lb:9000")
// spark.sparkContext.hadoopConfiguration.set("fs.s3a.path.style.access", "true")
// spark.sparkContext.hadoopConfiguration.set("fs.s3a.access.key", "minioadmin")
// spark.sparkContext.hadoopConfiguration.set("fs.s3a.secret.key", "minioadmin")
//
// val df = spark.read.json("s3a://testbucket/s3.json")
//
// df.write.parquet("s3a://testbucket/parquet/")
// }
// }
objInfo, err := z.GetObjectInfo(ctx, bucket, path.Dir(prefix), ObjectOptions{NoLock: true})
if err == nil {
if opts.Lifecycle != nil {
evt := evalActionFromLifecycle(ctx, *opts.Lifecycle, opts.Retention, opts.Replication.Config, objInfo)
if evt.Action.Delete() {
globalExpiryState.enqueueByDays(objInfo, evt, lcEventSrc_s3ListObjects)
if !evt.Action.DeleteRestored() {
// Skip entry if ILM action was DeleteVersionAction or DeleteAction
return loi, nil
}
}
}
return loi, nil
}
}
if len(prefix) > 0 && maxKeys == 1 && marker == "" { if len(prefix) > 0 && maxKeys == 1 && marker == "" {
// Optimization for certain applications like // Optimization for certain applications like
// - Cohesity // - Cohesity
@@ -1334,7 +1414,7 @@ func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, ma
objInfo, err := z.GetObjectInfo(ctx, bucket, prefix, ObjectOptions{NoLock: true}) objInfo, err := z.GetObjectInfo(ctx, bucket, prefix, ObjectOptions{NoLock: true})
if err == nil { if err == nil {
if opts.Lifecycle != nil { if opts.Lifecycle != nil {
evt := evalActionFromLifecycle(ctx, *opts.Lifecycle, opts.Retention, objInfo) evt := evalActionFromLifecycle(ctx, *opts.Lifecycle, opts.Retention, opts.Replication.Config, objInfo)
if evt.Action.Delete() { if evt.Action.Delete() {
globalExpiryState.enqueueByDays(objInfo, evt, lcEventSrc_s3ListObjects) globalExpiryState.enqueueByDays(objInfo, evt, lcEventSrc_s3ListObjects)
if !evt.Action.DeleteRestored() { if !evt.Action.DeleteRestored() {
@@ -1716,10 +1796,42 @@ func (z *erasureServerPools) deleteAll(ctx context.Context, bucket, prefix strin
} }
} }
var listBucketsCache timedValue
// List all buckets from one of the serverPools, we are not doing merge // List all buckets from one of the serverPools, we are not doing merge
// sort here just for simplification. As per design it is assumed // sort here just for simplification. As per design it is assumed
// that all buckets are present on all serverPools. // that all buckets are present on all serverPools.
func (z *erasureServerPools) ListBuckets(ctx context.Context, opts BucketOptions) (buckets []BucketInfo, err error) { func (z *erasureServerPools) ListBuckets(ctx context.Context, opts BucketOptions) (buckets []BucketInfo, err error) {
if opts.Cached {
listBucketsCache.Once.Do(func() {
listBucketsCache.TTL = time.Second
listBucketsCache.Relax = true
listBucketsCache.Update = func() (interface{}, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
buckets, err = z.s3Peer.ListBuckets(ctx, opts)
cancel()
if err != nil {
return nil, err
}
for i := range buckets {
createdAt, err := globalBucketMetadataSys.CreatedAt(buckets[i].Name)
if err == nil {
buckets[i].Created = createdAt
}
}
return buckets, nil
}
})
v, _ := listBucketsCache.Get()
if v != nil {
return v.([]BucketInfo), nil
}
return buckets, nil
}
buckets, err = z.s3Peer.ListBuckets(ctx, opts) buckets, err = z.s3Peer.ListBuckets(ctx, opts)
if err != nil { if err != nil {
return nil, err return nil, err
-1
View File
@@ -182,7 +182,6 @@ func getDisksInfo(disks []StorageAPI, endpoints []Endpoint) (disksInfo []madmin.
DiskIndex: endpoints[index].DiskIdx, DiskIndex: endpoints[index].DiskIdx,
} }
if disks[index] == OfflineDisk { if disks[index] == OfflineDisk {
logger.LogOnceIf(GlobalContext, fmt.Errorf("%s: %s", errDiskNotFound, endpoints[index]), "get-disks-info-offline-"+di.Endpoint)
di.State = diskErrToDriveState(errDiskNotFound) di.State = diskErrToDriveState(errDiskNotFound)
disksInfo[index] = di disksInfo[index] = di
return nil return nil
+1 -1
View File
@@ -275,7 +275,7 @@ func formatErasureMigrateV2ToV3(data []byte, export, version string) ([]byte, er
tmpOld := pathJoin(export, minioMetaTmpDeletedBucket, mustGetUUID()) tmpOld := pathJoin(export, minioMetaTmpDeletedBucket, mustGetUUID())
if err := renameAll(pathJoin(export, minioMetaMultipartBucket), if err := renameAll(pathJoin(export, minioMetaMultipartBucket),
tmpOld); err != nil && err != errFileNotFound { tmpOld, export); err != nil && err != errFileNotFound {
logger.LogIf(GlobalContext, fmt.Errorf("unable to rename (%s -> %s) %w, drive may be faulty please investigate", logger.LogIf(GlobalContext, fmt.Errorf("unable to rename (%s -> %s) %w, drive may be faulty please investigate",
pathJoin(export, minioMetaMultipartBucket), pathJoin(export, minioMetaMultipartBucket),
tmpOld, tmpOld,
+82 -44
View File
@@ -21,11 +21,11 @@ import (
"context" "context"
"crypto/subtle" "crypto/subtle"
"fmt" "fmt"
"io"
"net" "net"
"os" "os"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/minio/cli" "github.com/minio/cli"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
@@ -75,6 +75,69 @@ func (log *minioLogger) PrintResponse(sessionID string, code int, message string
} }
} }
func acceptSFTPConnection(conn net.Conn, config *ssh.ServerConfig) {
// For SSH handshake keep a 2 minute deadline, as OpensSSH default.
conn.SetDeadline(time.Now().Add(2 * time.Minute))
// Before use, a handshake must be performed on the incoming net.Conn.
sconn, chans, reqs, err := ssh.NewServerConn(conn, config)
if err != nil {
return
}
// Once we are done with SSH handshake, remove deadline.
conn.SetDeadline(time.Time{})
// The incoming Request channel must be serviced.
go ssh.DiscardRequests(reqs)
// Service the incoming Channel channel.
for newChannel := range chans {
// Channels have a type, depending on the application level
// protocol intended. In the case of an SFTP session, this is "subsystem"
// with a payload string of "<length=4>sftp"
if newChannel.ChannelType() != "session" {
newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
continue
}
channel, requests, err := newChannel.Accept()
if err != nil {
logger.LogOnceIf(context.Background(), fmt.Errorf("unable to accept the connection requests channel: %w", err), "accept-channel-sftp")
continue
}
// Sessions have out-of-band requests such as "shell",
// "pty-req" and "env". Here we handle only the
// "subsystem" request.
go func(in <-chan *ssh.Request) {
for req := range in {
ok := false
if req.Type == "subsystem" {
if len(req.Payload) > 4 && string(req.Payload[4:]) == "sftp" {
ok = true
go handleSFTPConnection(channel, sconn)
}
}
if req.WantReply {
// We only reply to SSH packets that have `sftp` payload, all other
// packets are rejected
req.Reply(ok, nil)
}
}
}(requests)
}
}
func handleSFTPConnection(channel ssh.Channel, sconn *ssh.ServerConn) {
// Create the server instance for the channel using the handler we created above.
server := sftp.NewRequestServer(channel, NewSFTPDriver(sconn.Permissions), sftp.WithRSAllocator())
defer server.Close()
server.Serve()
}
func startSFTPServer(c *cli.Context) { func startSFTPServer(c *cli.Context) {
args := c.StringSlice("sftp") args := c.StringSlice("sftp")
@@ -176,54 +239,29 @@ func startSFTPServer(c *cli.Context) {
logger.Info(fmt.Sprintf("MinIO SFTP Server listening on %s", net.JoinHostPort(publicIP, strconv.Itoa(port)))) logger.Info(fmt.Sprintf("MinIO SFTP Server listening on %s", net.JoinHostPort(publicIP, strconv.Itoa(port))))
var tempDelay time.Duration // how long to sleep on accept failure
for { for {
nConn, err := listener.Accept() conn, err := listener.Accept()
if err != nil { if err != nil {
logger.LogIf(context.Background(), err) // From https://github.com/golang/go/blob/4aa1efed4853ea067d665a952eee77c52faac774/src/net/http/server.go#L3046
continue if ne, ok := err.(net.Error); ok && ne.Temporary() {
} if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
// Before use, a handshake must be performed on the incoming net.Conn. } else {
sconn, chans, reqs, err := ssh.NewServerConn(nConn, config) tempDelay *= 2
if err != nil { }
logger.LogIf(context.Background(), err) if max := 1 * time.Second; tempDelay > max {
continue tempDelay = max
} }
logger.LogOnceIf(context.Background(), fmt.Errorf("error while accepting connections: %w, retrying in %s", err, tempDelay), "accept-limit-sftp")
// The incoming Request channel must be serviced. time.Sleep(tempDelay)
go ssh.DiscardRequests(reqs)
// Service the incoming Channel channel.
for newChannel := range chans {
// Channels have a type, depending on the application level
// protocol intended. In the case of an SFTP session, this is "subsystem"
// with a payload string of "<length=4>sftp"
if newChannel.ChannelType() != "session" {
newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
continue continue
} }
channel, requests, err := newChannel.Accept() logger.Fatal(err, "unrecoverable error while accepting new connections")
if err != nil {
logger.Fatal(err, "unable to accept the connection requests channel")
}
// Sessions have out-of-band requests such as "shell",
// "pty-req" and "env". Here we handle only the
// "subsystem" request.
go func(in <-chan *ssh.Request) {
for req := range in {
// We only reply to SSH packets that have `sftp` payload.
req.Reply(req.Type == "subsystem" && string(req.Payload[4:]) == "sftp", nil)
}
}(requests)
server := sftp.NewRequestServer(channel, NewSFTPDriver(sconn.Permissions))
if err := server.Serve(); err == io.EOF {
server.Close()
} else if err != nil {
logger.Fatal(err, "unable to start SFTP server")
}
} }
go acceptSFTPConnection(conn, config)
} }
} }
+3 -2
View File
@@ -152,7 +152,7 @@ func setBrowserRedirectMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
read := r.Method == http.MethodGet || r.Method == http.MethodHead read := r.Method == http.MethodGet || r.Method == http.MethodHead
// Re-direction is handled specifically for browser requests. // Re-direction is handled specifically for browser requests.
if guessIsBrowserReq(r) && read { if guessIsBrowserReq(r) && read && globalBrowserRedirect {
// Fetch the redirect location if any. // Fetch the redirect location if any.
if u := getRedirectLocation(r); u != nil { if u := getRedirectLocation(r); u != nil {
// Employ a temporary re-direct. // Employ a temporary re-direct.
@@ -231,7 +231,8 @@ func guessIsMetricsReq(req *http.Request) bool {
req.URL.Path == minioReservedBucketPath+prometheusMetricsPathLegacy || req.URL.Path == minioReservedBucketPath+prometheusMetricsPathLegacy ||
req.URL.Path == minioReservedBucketPath+prometheusMetricsV2ClusterPath || req.URL.Path == minioReservedBucketPath+prometheusMetricsV2ClusterPath ||
req.URL.Path == minioReservedBucketPath+prometheusMetricsV2NodePath || req.URL.Path == minioReservedBucketPath+prometheusMetricsV2NodePath ||
req.URL.Path == minioReservedBucketPath+prometheusMetricsV2BucketPath req.URL.Path == minioReservedBucketPath+prometheusMetricsV2BucketPath ||
req.URL.Path == minioReservedBucketPath+prometheusMetricsV2ResourcePath
} }
// guessIsRPCReq - returns true if the request is for an RPC endpoint. // guessIsRPCReq - returns true if the request is for an RPC endpoint.
+46 -35
View File
@@ -20,6 +20,7 @@ package cmd
import ( import (
"context" "context"
"fmt" "fmt"
"runtime"
"sort" "sort"
"time" "time"
@@ -30,6 +31,7 @@ import (
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/minio/pkg/v2/console" "github.com/minio/pkg/v2/console"
"github.com/minio/pkg/v2/wildcard" "github.com/minio/pkg/v2/wildcard"
"github.com/minio/pkg/v2/workers"
) )
const ( const (
@@ -132,30 +134,8 @@ func getLocalBackgroundHealStatus(ctx context.Context, o ObjectLayer) (madmin.Bg
return status, true return status, true
} }
func mustGetHealSequence(ctx context.Context) *healSequence {
// Get background heal sequence to send elements to heal
for {
globalHealStateLK.RLock()
hstate := globalBackgroundHealState
globalHealStateLK.RUnlock()
if hstate == nil {
time.Sleep(time.Second)
continue
}
bgSeq, ok := hstate.getHealSequenceByToken(bgHealingUUID)
if !ok {
time.Sleep(time.Second)
continue
}
return bgSeq
}
}
// healErasureSet lists and heals all objects in a specific erasure set // healErasureSet lists and heals all objects in a specific erasure set
func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string, tracker *healingTracker) error { func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string, tracker *healingTracker) error {
bgSeq := mustGetHealSequence(ctx)
scanMode := madmin.HealNormalScan scanMode := madmin.HealNormalScan
// Make sure to copy since `buckets slice` // Make sure to copy since `buckets slice`
@@ -173,6 +153,30 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
} }
} }
info, err := tracker.disk.DiskInfo(ctx, false)
if err != nil {
return fmt.Errorf("unable to get disk information before healing it: %w", err)
}
var numHealers uint64
if numCores := uint64(runtime.GOMAXPROCS(0)); info.NRRequests > numCores {
numHealers = numCores / 4
} else {
numHealers = info.NRRequests / 4
}
if numHealers < 4 {
numHealers = 4
}
// allow overriding this value as well..
if v := globalHealConfig.GetWorkers(); v > 0 {
numHealers = uint64(v)
}
logger.Info(fmt.Sprintf("Healing drive '%s' - use %d parallel workers.", tracker.disk.String(), numHealers))
jt, _ := workers.New(int(numHealers))
var retErr error var retErr error
// Heal all buckets with all objects // Heal all buckets with all objects
for _, bucket := range healBuckets { for _, bucket := range healBuckets {
@@ -267,6 +271,8 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
// Note: updates from healEntry to tracker must be sent on results channel. // Note: updates from healEntry to tracker must be sent on results channel.
healEntry := func(bucket string, entry metaCacheEntry) { healEntry := func(bucket string, entry metaCacheEntry) {
defer jt.Give()
if entry.name == "" && len(entry.metadata) == 0 { if entry.name == "" && len(entry.metadata) == 0 {
// ignore entries that don't have metadata. // ignore entries that don't have metadata.
return return
@@ -291,14 +297,17 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
} }
} }
// erasureObjects layer needs object names to be encoded
encodedEntryName := encodeDirObject(entry.name)
var result healEntryResult var result healEntryResult
fivs, err := entry.fileInfoVersions(bucket) fivs, err := entry.fileInfoVersions(bucket)
if err != nil { if err != nil {
err := bgSeq.queueHealTask(healSource{ _, err := er.HealObject(ctx, bucket, encodedEntryName, "",
bucket: bucket, madmin.HealOpts{
object: entry.name, ScanMode: scanMode,
versionID: "", Remove: healDeleteDangling,
}, madmin.HealItemObject) })
if err != nil { if err != nil {
if isErrObjectNotFound(err) { if isErrObjectNotFound(err) {
// queueing happens across namespace, ignore // queueing happens across namespace, ignore
@@ -321,11 +330,11 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
if version.ModTime.After(tracker.Started) { if version.ModTime.After(tracker.Started) {
continue continue
} }
if err := bgSeq.queueHealTask(healSource{ if _, err := er.HealObject(ctx, bucket, encodedEntryName,
bucket: bucket, version.VersionID, madmin.HealOpts{
object: version.Name, ScanMode: scanMode,
versionID: version.VersionID, Remove: healDeleteDangling,
}, madmin.HealItemObject); err != nil { }); err != nil {
if isErrObjectNotFound(err) { if isErrObjectNotFound(err) {
// queueing happens across namespace, ignore // queueing happens across namespace, ignore
// objects that are not found. // objects that are not found.
@@ -344,7 +353,6 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
} else { } else {
result = healEntrySuccess(uint64(version.Size)) result = healEntrySuccess(uint64(version.Size))
} }
bgSeq.logHeal(madmin.HealItemObject)
if !send(result) { if !send(result) {
return return
@@ -382,7 +390,8 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
minDisks: 1, minDisks: 1,
reportNotFound: false, reportNotFound: false,
agreed: func(entry metaCacheEntry) { agreed: func(entry metaCacheEntry) {
healEntry(actualBucket, entry) jt.Take()
go healEntry(actualBucket, entry)
}, },
partial: func(entries metaCacheEntries, _ []error) { partial: func(entries metaCacheEntries, _ []error) {
entry, ok := entries.resolve(&resolver) entry, ok := entries.resolve(&resolver)
@@ -391,10 +400,12 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
// proceed to heal nonetheless. // proceed to heal nonetheless.
entry, _ = entries.firstFound() entry, _ = entries.firstFound()
} }
healEntry(actualBucket, *entry) jt.Take()
go healEntry(actualBucket, *entry)
}, },
finished: nil, finished: nil,
}) })
jt.Wait() // synchronize all the concurrent heal jobs
close(results) close(results)
if err != nil { if err != nil {
// Set this such that when we return this function // Set this such that when we return this function
+6
View File
@@ -152,6 +152,9 @@ var (
// and it is automatically deduced. // and it is automatically deduced.
globalBrowserRedirectURL *xnet.URL globalBrowserRedirectURL *xnet.URL
// Disable redirect, default is enabled.
globalBrowserRedirect bool
// This flag is set to 'true' when MINIO_UPDATE env is set to 'off'. Default is false. // This flag is set to 'true' when MINIO_UPDATE env is set to 'off'. Default is false.
globalInplaceUpdateDisabled = false globalInplaceUpdateDisabled = false
@@ -402,6 +405,9 @@ var (
// Captures all batch jobs metrics globally // Captures all batch jobs metrics globally
globalBatchJobsMetrics batchJobMetrics globalBatchJobsMetrics batchJobMetrics
// Indicates if server was started as `--address ":0"`
globalDynamicAPIPort bool
// Add new variable global values here. // Add new variable global values here.
) )
+3 -2
View File
@@ -300,7 +300,8 @@ func collectAPIStats(api string, f http.HandlerFunc) http.HandlerFunc {
globalHTTPStats.currentS3Requests.Inc(api) globalHTTPStats.currentS3Requests.Inc(api)
defer globalHTTPStats.currentS3Requests.Dec(api) defer globalHTTPStats.currentS3Requests.Dec(api)
if bucket != "" && bucket != minioReservedBucket { _, err = globalBucketMetadataSys.Get(bucket) // check if this bucket exists.
if bucket != "" && bucket != minioReservedBucket && err == nil {
globalBucketHTTPStats.updateHTTPStats(bucket, api, nil) globalBucketHTTPStats.updateHTTPStats(bucket, api, nil)
} }
@@ -316,7 +317,7 @@ func collectAPIStats(api string, f http.HandlerFunc) http.HandlerFunc {
globalConnStats.incS3InputBytes(int64(tc.RequestRecorder.Size())) globalConnStats.incS3InputBytes(int64(tc.RequestRecorder.Size()))
globalConnStats.incS3OutputBytes(int64(tc.ResponseRecorder.Size())) globalConnStats.incS3OutputBytes(int64(tc.ResponseRecorder.Size()))
if bucket != "" && bucket != minioReservedBucket { if bucket != "" && bucket != minioReservedBucket && err == nil {
globalBucketConnStats.incS3InputBytes(bucket, int64(tc.RequestRecorder.Size())) globalBucketConnStats.incS3InputBytes(bucket, int64(tc.RequestRecorder.Size()))
globalBucketConnStats.incS3OutputBytes(bucket, int64(tc.ResponseRecorder.Size())) globalBucketConnStats.incS3OutputBytes(bucket, int64(tc.ResponseRecorder.Size()))
globalBucketHTTPStats.updateHTTPStats(bucket, api, tc.ResponseRecorder) globalBucketHTTPStats.updateHTTPStats(bucket, api, tc.ResponseRecorder)
+67 -31
View File
@@ -29,6 +29,7 @@ import (
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"github.com/minio/madmin-go/v3" "github.com/minio/madmin-go/v3"
"github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/minio/internal/config" "github.com/minio/minio/internal/config"
"github.com/minio/minio/internal/kms" "github.com/minio/minio/internal/kms"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
@@ -343,6 +344,7 @@ var (
policyDBServiceAccountsListKey = "policydb/service-accounts/" policyDBServiceAccountsListKey = "policydb/service-accounts/"
policyDBGroupsListKey = "policydb/groups/" policyDBGroupsListKey = "policydb/groups/"
// List of directories from which to read iam data into memory.
allListKeys = []string{ allListKeys = []string{
usersListKey, usersListKey,
svcAccListKey, svcAccListKey,
@@ -354,34 +356,60 @@ var (
policyDBServiceAccountsListKey, policyDBServiceAccountsListKey,
policyDBGroupsListKey, policyDBGroupsListKey,
} }
// List of directories to skip: we do not read STS directories for better
// performance. STS credentials would be stored in memory when they are
// first used.
iamLoadSkipListKeySet = set.CreateStringSet(
stsListKey,
policyDBSTSUsersListKey,
)
) )
func (iamOS *IAMObjectStore) listAllIAMConfigItems(ctx context.Context) (map[string][]string, error) { func (iamOS *IAMObjectStore) listAllIAMConfigItems(ctx context.Context) (map[string][]string, error) {
res := make(map[string][]string) res := make(map[string][]string)
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
for item := range listIAMConfigItems(ctx, iamOS.objAPI, iamConfigPrefix+SlashSeparator) { for _, listKey := range allListKeys {
if item.Err != nil { if iamLoadSkipListKeySet.Contains(listKey) {
return nil, item.Err continue
} }
for item := range listIAMConfigItems(ctx, iamOS.objAPI, iamConfigPrefix+SlashSeparator+listKey) {
found := false if item.Err != nil {
for _, listKey := range allListKeys { return nil, item.Err
if strings.HasPrefix(item.Item, listKey) {
found = true
name := strings.TrimPrefix(item.Item, listKey)
res[listKey] = append(res[listKey], name)
break
} }
} res[listKey] = append(res[listKey], item.Item)
if !found && (item.Item != "format.json") {
logger.LogIf(ctx, fmt.Errorf("unknown type of IAM file listed: %v", item.Item))
} }
} }
return res, nil return res, nil
} }
// PurgeExpiredSTS - purge expired STS credentials from object store.
func (iamOS *IAMObjectStore) PurgeExpiredSTS(ctx context.Context) error {
if iamOS.objAPI == nil {
return errServerNotInitialized
}
bootstrapTraceMsg("purging expired STS credentials")
// Scan STS users on disk and purge expired ones. We do not need to hold a
// lock with store.lock() here.
for item := range listIAMConfigItems(ctx, iamOS.objAPI, iamConfigPrefix+SlashSeparator+stsListKey) {
if item.Err != nil {
return item.Err
}
userName := path.Dir(item.Item)
// loadUser() will delete expired user during the load - we do not need
// to keep the loaded user around in memory, so we reinitialize the map
// each time.
m := map[string]UserIdentity{}
if err := iamOS.loadUser(ctx, userName, stsUser, m); err != nil && err != errNoSuchUser {
logger.LogIf(GlobalContext, fmt.Errorf("unable to load user during STS purge: %w (%s)", err, item.Item))
}
}
return nil
}
// Assumes cache is locked by caller. // Assumes cache is locked by caller.
func (iamOS *IAMObjectStore) loadAllFromObjStore(ctx context.Context, cache *iamCache) error { func (iamOS *IAMObjectStore) loadAllFromObjStore(ctx context.Context, cache *iamCache) error {
if iamOS.objAPI == nil { if iamOS.objAPI == nil {
@@ -448,29 +476,37 @@ func (iamOS *IAMObjectStore) loadAllFromObjStore(ctx context.Context, cache *iam
bootstrapTraceMsg("loading service accounts") bootstrapTraceMsg("loading service accounts")
svcAccList := listedConfigItems[svcAccListKey] svcAccList := listedConfigItems[svcAccListKey]
svcUsersMap := make(map[string]UserIdentity, len(svcAccList))
for _, item := range svcAccList { for _, item := range svcAccList {
userName := path.Dir(item) userName := path.Dir(item)
if err := iamOS.loadUser(ctx, userName, svcUser, cache.iamUsersMap); err != nil && err != errNoSuchUser { if err := iamOS.loadUser(ctx, userName, svcUser, svcUsersMap); err != nil && err != errNoSuchUser {
return fmt.Errorf("unable to load the service account `%s`: %w", userName, err) return fmt.Errorf("unable to load the service account `%s`: %w", userName, err)
} }
} }
for _, svcAcc := range svcUsersMap {
bootstrapTraceMsg("loading STS users") svcParent := svcAcc.Credentials.ParentUser
stsUsersList := listedConfigItems[stsListKey] if _, ok := cache.iamUsersMap[svcParent]; !ok {
for _, item := range stsUsersList { // If a service account's parent user is not in iamUsersMap, the
userName := path.Dir(item) // parent is an STS account. Such accounts may have a policy mapped
if err := iamOS.loadUser(ctx, userName, stsUser, cache.iamUsersMap); err != nil && err != errNoSuchUser { // on the parent user, so we load them. This is not needed for the
return fmt.Errorf("unable to load the STS user `%s`: %w", userName, err) // initial server startup, however, it is needed for the case where
// the STS account's policy mapping (for example in LDAP mode) may
// be changed and the user's policy mapping in memory is stale
// (because the policy change notification was missed by the current
// server).
//
// The "policy not found" error is ignored because the STS account may
// not have a policy mapped via its parent (for e.g. in
// OIDC/AssumeRoleWithCustomToken/AssumeRoleWithCertificate).
err := iamOS.loadMappedPolicy(ctx, svcParent, stsUser, false, cache.iamSTSPolicyMap)
if err != nil && !errors.Is(err, errNoSuchPolicy) {
return fmt.Errorf("unable to load the policy mapping for the STS user `%s`: %w", svcParent, err)
}
} }
} }
// Copy svcUsersMap to cache.iamUsersMap
bootstrapTraceMsg("loading STS policy mapping") for k, v := range svcUsersMap {
stsPolicyMappingsList := listedConfigItems[policyDBSTSUsersListKey] cache.iamUsersMap[k] = v
for _, item := range stsPolicyMappingsList {
stsName := strings.TrimSuffix(item, ".json")
if err := iamOS.loadMappedPolicy(ctx, stsName, stsUser, false, cache.iamUserPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) {
return fmt.Errorf("unable to load the policy mapping for the STS user `%s`: %w", stsName, err)
}
} }
cache.buildUserGroupMemberships() cache.buildUserGroupMemberships()
+290 -122
View File
@@ -34,7 +34,7 @@ import (
"github.com/minio/minio/internal/config/identity/openid" "github.com/minio/minio/internal/config/identity/openid"
"github.com/minio/minio/internal/jwt" "github.com/minio/minio/internal/jwt"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
iampolicy "github.com/minio/pkg/v2/policy" "github.com/minio/pkg/v2/policy"
) )
const ( const (
@@ -212,12 +212,12 @@ func newMappedPolicy(policy string) MappedPolicy {
// PolicyDoc represents an IAM policy with some metadata. // PolicyDoc represents an IAM policy with some metadata.
type PolicyDoc struct { type PolicyDoc struct {
Version int `json:",omitempty"` Version int `json:",omitempty"`
Policy iampolicy.Policy Policy policy.Policy
CreateDate time.Time `json:",omitempty"` CreateDate time.Time `json:",omitempty"`
UpdateDate time.Time `json:",omitempty"` UpdateDate time.Time `json:",omitempty"`
} }
func newPolicyDoc(p iampolicy.Policy) PolicyDoc { func newPolicyDoc(p policy.Policy) PolicyDoc {
now := UTCNow().Round(time.Millisecond) now := UTCNow().Round(time.Millisecond)
return PolicyDoc{ return PolicyDoc{
Version: 1, Version: 1,
@@ -228,14 +228,14 @@ func newPolicyDoc(p iampolicy.Policy) PolicyDoc {
} }
// defaultPolicyDoc - used to wrap a default policy as PolicyDoc. // defaultPolicyDoc - used to wrap a default policy as PolicyDoc.
func defaultPolicyDoc(p iampolicy.Policy) PolicyDoc { func defaultPolicyDoc(p policy.Policy) PolicyDoc {
return PolicyDoc{ return PolicyDoc{
Version: 1, Version: 1,
Policy: p, Policy: p,
} }
} }
func (d *PolicyDoc) update(p iampolicy.Policy) { func (d *PolicyDoc) update(p policy.Policy) {
now := UTCNow().Round(time.Millisecond) now := UTCNow().Round(time.Millisecond)
d.UpdateDate = now d.UpdateDate = now
if d.CreateDate.IsZero() { if d.CreateDate.IsZero() {
@@ -248,7 +248,7 @@ func (d *PolicyDoc) update(p iampolicy.Policy) {
// definitions. // definitions.
// //
// The on-disk format of policy definitions has changed (around early 12/2021) // The on-disk format of policy definitions has changed (around early 12/2021)
// from iampolicy.Policy to PolicyDoc. To avoid a migration, loading supports // from policy.Policy to PolicyDoc. To avoid a migration, loading supports
// both the old and the new formats. // both the old and the new formats.
func (d *PolicyDoc) parseJSON(data []byte) error { func (d *PolicyDoc) parseJSON(data []byte) error {
json := jsoniter.ConfigCompatibleWithStandardLibrary json := jsoniter.ConfigCompatibleWithStandardLibrary
@@ -283,14 +283,22 @@ type iamCache struct {
// map of policy names to policy definitions // map of policy names to policy definitions
iamPolicyDocsMap map[string]PolicyDoc iamPolicyDocsMap map[string]PolicyDoc
// map of usernames to credentials
// map of regular username to credentials
iamUsersMap map[string]UserIdentity iamUsersMap map[string]UserIdentity
// map of regular username to policy names
iamUserPolicyMap map[string]MappedPolicy
// STS accounts are loaded on demand and not via the periodic IAM reload.
// map of STS access key to credentials
iamSTSAccountsMap map[string]UserIdentity
// map of STS access key to policy names
iamSTSPolicyMap map[string]MappedPolicy
// map of group names to group info // map of group names to group info
iamGroupsMap map[string]GroupInfo iamGroupsMap map[string]GroupInfo
// map of user names to groups they are a member of // map of user names to groups they are a member of
iamUserGroupMemberships map[string]set.StringSet iamUserGroupMemberships map[string]set.StringSet
// map of usernames/temporary access keys to policy names
iamUserPolicyMap map[string]MappedPolicy
// map of group names to policy names // map of group names to policy names
iamGroupPolicyMap map[string]MappedPolicy iamGroupPolicyMap map[string]MappedPolicy
} }
@@ -299,9 +307,11 @@ func newIamCache() *iamCache {
return &iamCache{ return &iamCache{
iamPolicyDocsMap: map[string]PolicyDoc{}, iamPolicyDocsMap: map[string]PolicyDoc{},
iamUsersMap: map[string]UserIdentity{}, iamUsersMap: map[string]UserIdentity{},
iamUserPolicyMap: map[string]MappedPolicy{},
iamSTSAccountsMap: map[string]UserIdentity{},
iamSTSPolicyMap: map[string]MappedPolicy{},
iamGroupsMap: map[string]GroupInfo{}, iamGroupsMap: map[string]GroupInfo{},
iamUserGroupMemberships: map[string]set.StringSet{}, iamUserGroupMemberships: map[string]set.StringSet{},
iamUserPolicyMap: map[string]MappedPolicy{},
iamGroupPolicyMap: map[string]MappedPolicy{}, iamGroupPolicyMap: map[string]MappedPolicy{},
} }
} }
@@ -354,9 +364,9 @@ func (c *iamCache) removeGroupFromMembershipsMap(group string) {
// information in IAM (i.e sys.iam*Map) - this info is stored only in the STS // information in IAM (i.e sys.iam*Map) - this info is stored only in the STS
// generated credentials. Thus we skip looking up group memberships, user map, // generated credentials. Thus we skip looking up group memberships, user map,
// and group map and check the appropriate policy maps directly. // and group map and check the appropriate policy maps directly.
func (c *iamCache) policyDBGet(mode UsersSysType, name string, isGroup bool) ([]string, time.Time, error) { func (c *iamCache) policyDBGet(store *IAMStoreSys, name string, isGroup bool) ([]string, time.Time, error) {
if isGroup { if isGroup {
if mode == MinIOUsersSysType { if store.getUsersSysType() == MinIOUsersSysType {
g, ok := c.iamGroupsMap[name] g, ok := c.iamGroupsMap[name]
if !ok { if !ok {
return nil, time.Time{}, errNoSuchGroup return nil, time.Time{}, errNoSuchGroup
@@ -381,7 +391,20 @@ func (c *iamCache) policyDBGet(mode UsersSysType, name string, isGroup bool) ([]
} }
} }
mp := c.iamUserPolicyMap[name] // For internal IDP regular/service account user accounts, the policy
// mapping is iamUserPolicyMap. For STS accounts, the parent user would be
// passed here and we lookup the mapping in iamSTSPolicyMap.
mp, ok := c.iamUserPolicyMap[name]
if !ok {
// Since user "name" could be a parent user of an STS account, we lookup
// mappings for those too.
mp, ok = c.iamSTSPolicyMap[name]
if !ok {
// Attempt to load parent user mapping for STS accounts
store.loadMappedPolicy(context.TODO(), name, stsUser, false, c.iamSTSPolicyMap)
mp = c.iamSTSPolicyMap[name]
}
}
// returned policy could be empty // returned policy could be empty
policies := mp.toSlice() policies := mp.toSlice()
@@ -407,7 +430,11 @@ func (c *iamCache) updateUserWithClaims(key string, u UserIdentity) error {
} }
u.Credentials.Claims = jwtClaims.Map() u.Credentials.Claims = jwtClaims.Map()
} }
c.iamUsersMap[key] = u if !u.Credentials.IsTemp() {
c.iamUsersMap[key] = u
} else {
c.iamSTSAccountsMap[key] = u
}
c.updatedAt = time.Now() c.updatedAt = time.Now()
return nil return nil
} }
@@ -452,13 +479,23 @@ type iamStorageWatcher interface {
// Set default canned policies only if not already overridden by users. // Set default canned policies only if not already overridden by users.
func setDefaultCannedPolicies(policies map[string]PolicyDoc) { func setDefaultCannedPolicies(policies map[string]PolicyDoc) {
for _, v := range iampolicy.DefaultPolicies { for _, v := range policy.DefaultPolicies {
if _, ok := policies[v.Name]; !ok { if _, ok := policies[v.Name]; !ok {
policies[v.Name] = defaultPolicyDoc(v.Definition) policies[v.Name] = defaultPolicyDoc(v.Definition)
} }
} }
} }
// PurgeExpiredSTS - purges expired STS credentials.
func (store *IAMStoreSys) PurgeExpiredSTS(ctx context.Context) error {
iamOS, ok := store.IAMStorageAPI.(*IAMObjectStore)
if !ok {
// No purging is done for non-object storage.
return nil
}
return iamOS.PurgeExpiredSTS(ctx)
}
// LoadIAMCache reads all IAM items and populates a new iamCache object and // LoadIAMCache reads all IAM items and populates a new iamCache object and
// replaces the in-memory cache object. // replaces the in-memory cache object.
func (store *IAMStoreSys) LoadIAMCache(ctx context.Context) error { func (store *IAMStoreSys) LoadIAMCache(ctx context.Context) error {
@@ -512,18 +549,6 @@ func (store *IAMStoreSys) LoadIAMCache(ctx context.Context) error {
return err return err
} }
bootstrapTraceMsg("loading STS users")
// load STS temp users
if err := store.loadUsers(ctx, stsUser, newCache.iamUsersMap); err != nil {
return err
}
bootstrapTraceMsg("loading STS policy mapping")
// load STS policy mappings
if err := store.loadMappedPolicies(ctx, stsUser, false, newCache.iamUserPolicyMap); err != nil {
return err
}
newCache.buildUserGroupMemberships() newCache.buildUserGroupMemberships()
} }
@@ -535,9 +560,9 @@ func (store *IAMStoreSys) LoadIAMCache(ctx context.Context) error {
// were changes to the in-memory cache we should wait for the next // were changes to the in-memory cache we should wait for the next
// cycle until we can safely update the in-memory cache. // cycle until we can safely update the in-memory cache.
// //
// An in-memory cache must be replaced only if we know for sure that // An in-memory cache must be replaced only if we know for sure that the
// the values loaded from disk are not stale. They might be stale // values loaded from disk are not stale. They might be stale if the
// if the cached.updatedAt is recent than the refresh cycle began. // cached.updatedAt is more recent than the refresh cycle began.
if cache.updatedAt.Before(loadedAt) { if cache.updatedAt.Before(loadedAt) {
// No one has updated anything since the config was loaded, // No one has updated anything since the config was loaded,
// so we just replace whatever is on the disk into memory. // so we just replace whatever is on the disk into memory.
@@ -547,6 +572,15 @@ func (store *IAMStoreSys) LoadIAMCache(ctx context.Context) error {
cache.iamUserGroupMemberships = newCache.iamUserGroupMemberships cache.iamUserGroupMemberships = newCache.iamUserGroupMemberships
cache.iamUserPolicyMap = newCache.iamUserPolicyMap cache.iamUserPolicyMap = newCache.iamUserPolicyMap
cache.iamUsersMap = newCache.iamUsersMap cache.iamUsersMap = newCache.iamUsersMap
// For STS policy map, we need to merge the new cache with the existing
// cache because the periodic IAM reload is partial. The periodic load
// here is to account for STS policy mapping changes that should apply
// for service accounts derived from such STS accounts (i.e. LDAP STS
// accounts).
for k, v := range newCache.iamSTSPolicyMap {
cache.iamSTSPolicyMap[k] = v
}
cache.updatedAt = time.Now() cache.updatedAt = time.Now()
} }
@@ -571,6 +605,10 @@ func (store *IAMStoreSys) GetUser(user string) (UserIdentity, bool) {
defer store.runlock() defer store.runlock()
u, ok := cache.iamUsersMap[user] u, ok := cache.iamUsersMap[user]
if !ok {
// Check the sts map
u, ok = cache.iamSTSAccountsMap[user]
}
return u, ok return u, ok
} }
@@ -635,14 +673,14 @@ func (store *IAMStoreSys) PolicyDBGet(name string, isGroup bool, groups ...strin
cache := store.rlock() cache := store.rlock()
defer store.runlock() defer store.runlock()
policies, _, err := cache.policyDBGet(store.getUsersSysType(), name, isGroup) policies, _, err := cache.policyDBGet(store, name, isGroup)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !isGroup { if !isGroup {
for _, group := range groups { for _, group := range groups {
ps, _, err := cache.policyDBGet(store.getUsersSysType(), group, true) ps, _, err := cache.policyDBGet(store, group, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -828,7 +866,7 @@ func (store *IAMStoreSys) GetGroupDescription(group string) (gd madmin.GroupDesc
cache := store.rlock() cache := store.rlock()
defer store.runlock() defer store.runlock()
ps, updatedAt, err := cache.policyDBGet(store.getUsersSysType(), group, true) ps, updatedAt, err := cache.policyDBGet(store, group, true)
if err != nil { if err != nil {
return gd, err return gd, err
} }
@@ -928,7 +966,16 @@ func (store *IAMStoreSys) PolicyDBUpdate(ctx context.Context, name string, isGro
// Load existing policy mapping // Load existing policy mapping
var mp MappedPolicy var mp MappedPolicy
if !isGroup { if !isGroup {
mp = cache.iamUserPolicyMap[name] if userType == stsUser {
stsMap := map[string]MappedPolicy{}
// Attempt to load parent user mapping for STS accounts
store.loadMappedPolicy(context.TODO(), name, stsUser, false, stsMap)
mp = stsMap[name]
} else {
mp = cache.iamUserPolicyMap[name]
}
} else { } else {
if store.getUsersSysType() == MinIOUsersSysType { if store.getUsersSysType() == MinIOUsersSysType {
g, ok := cache.iamGroupsMap[name] g, ok := cache.iamGroupsMap[name]
@@ -981,7 +1028,11 @@ func (store *IAMStoreSys) PolicyDBUpdate(ctx context.Context, name string, isGro
return return
} }
if !isGroup { if !isGroup {
cache.iamUserPolicyMap[name] = newPolicyMapping if userType == stsUser {
cache.iamSTSPolicyMap[name] = newPolicyMapping
} else {
cache.iamUserPolicyMap[name] = newPolicyMapping
}
} else { } else {
cache.iamGroupPolicyMap[name] = newPolicyMapping cache.iamGroupPolicyMap[name] = newPolicyMapping
} }
@@ -1015,7 +1066,11 @@ func (store *IAMStoreSys) PolicyDBSet(ctx context.Context, name, policy string,
return updatedAt, err return updatedAt, err
} }
if !isGroup { if !isGroup {
delete(cache.iamUserPolicyMap, name) if userType == stsUser {
delete(cache.iamSTSPolicyMap, name)
} else {
delete(cache.iamUserPolicyMap, name)
}
} else { } else {
delete(cache.iamGroupPolicyMap, name) delete(cache.iamGroupPolicyMap, name)
} }
@@ -1035,7 +1090,11 @@ func (store *IAMStoreSys) PolicyDBSet(ctx context.Context, name, policy string,
return updatedAt, err return updatedAt, err
} }
if !isGroup { if !isGroup {
cache.iamUserPolicyMap[name] = mp if userType == stsUser {
cache.iamSTSPolicyMap[name] = mp
} else {
cache.iamUserPolicyMap[name] = mp
}
} else { } else {
cache.iamGroupPolicyMap[name] = mp cache.iamGroupPolicyMap[name] = mp
} }
@@ -1104,7 +1163,9 @@ func (store *IAMStoreSys) DeletePolicy(ctx context.Context, policy string) error
cache := store.lock() cache := store.lock()
defer store.unlock() defer store.unlock()
// Check if policy is mapped to any existing user or group. // Check if policy is mapped to any existing user or group. If so, we do not
// allow deletion of the policy. If the policy is mapped to an STS account,
// we do allow deletion.
users := []string{} users := []string{}
groups := []string{} groups := []string{}
for u, mp := range cache.iamUserPolicyMap { for u, mp := range cache.iamUserPolicyMap {
@@ -1148,16 +1209,16 @@ func (store *IAMStoreSys) DeletePolicy(ctx context.Context, policy string) error
// GetPolicy - gets the policy definition. Allows specifying multiple comma // GetPolicy - gets the policy definition. Allows specifying multiple comma
// separated policies - returns a combined policy. // separated policies - returns a combined policy.
func (store *IAMStoreSys) GetPolicy(name string) (iampolicy.Policy, error) { func (store *IAMStoreSys) GetPolicy(name string) (policy.Policy, error) {
if name == "" { if name == "" {
return iampolicy.Policy{}, errInvalidArgument return policy.Policy{}, errInvalidArgument
} }
cache := store.rlock() cache := store.rlock()
defer store.runlock() defer store.runlock()
policies := newMappedPolicy(name).toSlice() policies := newMappedPolicy(name).toSlice()
var toMerge []iampolicy.Policy var toMerge []policy.Policy
for _, policy := range policies { for _, policy := range policies {
if policy == "" { if policy == "" {
continue continue
@@ -1168,7 +1229,7 @@ func (store *IAMStoreSys) GetPolicy(name string) (iampolicy.Policy, error) {
} }
toMerge = append(toMerge, v.Policy) toMerge = append(toMerge, v.Policy)
} }
return iampolicy.MergePolicies(toMerge...), nil return policy.MergePolicies(toMerge...), nil
} }
// GetPolicyDoc - gets the policy doc which has the policy and some metadata. // GetPolicyDoc - gets the policy doc which has the policy and some metadata.
@@ -1190,7 +1251,7 @@ func (store *IAMStoreSys) GetPolicyDoc(name string) (r PolicyDoc, err error) {
} }
// SetPolicy - creates a policy with name. // SetPolicy - creates a policy with name.
func (store *IAMStoreSys) SetPolicy(ctx context.Context, name string, policy iampolicy.Policy) (time.Time, error) { func (store *IAMStoreSys) SetPolicy(ctx context.Context, name string, policy policy.Policy) (time.Time, error) {
if policy.IsEmpty() || name == "" { if policy.IsEmpty() || name == "" {
return time.Time{}, errInvalidArgument return time.Time{}, errInvalidArgument
} }
@@ -1220,7 +1281,7 @@ func (store *IAMStoreSys) SetPolicy(ctx context.Context, name string, policy iam
// ListPolicies - fetches all policies from storage and updates cache as well. // ListPolicies - fetches all policies from storage and updates cache as well.
// If bucketName is non-empty, returns policies matching the bucket. // If bucketName is non-empty, returns policies matching the bucket.
func (store *IAMStoreSys) ListPolicies(ctx context.Context, bucketName string) (map[string]iampolicy.Policy, error) { func (store *IAMStoreSys) ListPolicies(ctx context.Context, bucketName string) (map[string]policy.Policy, error) {
cache := store.lock() cache := store.lock()
defer store.unlock() defer store.unlock()
@@ -1236,7 +1297,7 @@ func (store *IAMStoreSys) ListPolicies(ctx context.Context, bucketName string) (
cache.iamPolicyDocsMap = m cache.iamPolicyDocsMap = m
cache.updatedAt = time.Now() cache.updatedAt = time.Now()
ret := map[string]iampolicy.Policy{} ret := map[string]policy.Policy{}
for k, v := range m { for k, v := range m {
if bucketName == "" || v.Policy.MatchResource(bucketName) { if bucketName == "" || v.Policy.MatchResource(bucketName) {
ret[k] = v.Policy ret[k] = v.Policy
@@ -1289,10 +1350,10 @@ func (store *IAMStoreSys) listPolicyDocs(ctx context.Context, bucketName string)
} }
// helper function - does not take locks. // helper function - does not take locks.
func filterPolicies(cache *iamCache, policyName string, bucketName string) (string, iampolicy.Policy) { func filterPolicies(cache *iamCache, policyName string, bucketName string) (string, policy.Policy) {
var policies []string var policies []string
mp := newMappedPolicy(policyName) mp := newMappedPolicy(policyName)
var toMerge []iampolicy.Policy var toMerge []policy.Policy
for _, policy := range mp.toSlice() { for _, policy := range mp.toSlice() {
if policy == "" { if policy == "" {
continue continue
@@ -1306,7 +1367,7 @@ func filterPolicies(cache *iamCache, policyName string, bucketName string) (stri
toMerge = append(toMerge, p.Policy) toMerge = append(toMerge, p.Policy)
} }
} }
return strings.Join(policies, ","), iampolicy.MergePolicies(toMerge...) return strings.Join(policies, ","), policy.MergePolicies(toMerge...)
} }
// FilterPolicies - accepts a comma separated list of policy names as a string // FilterPolicies - accepts a comma separated list of policy names as a string
@@ -1314,7 +1375,7 @@ func filterPolicies(cache *iamCache, policyName string, bucketName string) (stri
// bucketName is non-empty, additionally filters policies matching the bucket. // bucketName is non-empty, additionally filters policies matching the bucket.
// The first returned value is the list of currently existing policies, and the // The first returned value is the list of currently existing policies, and the
// second is their combined policy definition. // second is their combined policy definition.
func (store *IAMStoreSys) FilterPolicies(policyName string, bucketName string) (string, iampolicy.Policy) { func (store *IAMStoreSys) FilterPolicies(policyName string, bucketName string) (string, policy.Policy) {
cache := store.rlock() cache := store.rlock()
defer store.runlock() defer store.runlock()
@@ -1403,6 +1464,9 @@ func (store *IAMStoreSys) GetUsersWithMappedPolicies() map[string]string {
for k, v := range cache.iamUserPolicyMap { for k, v := range cache.iamUserPolicyMap {
result[k] = v.Policies result[k] = v.Policies
} }
for k, v := range cache.iamSTSPolicyMap {
result[k] = v.Policies
}
return result return result
} }
@@ -1425,10 +1489,25 @@ func (store *IAMStoreSys) GetUserInfo(name string) (u madmin.UserInfo, err error
break break
} }
} }
for _, v := range cache.iamSTSAccountsMap {
if v.Credentials.ParentUser == name {
groups = v.Credentials.Groups
break
}
}
mappedPolicy, ok := cache.iamUserPolicyMap[name] mappedPolicy, ok := cache.iamUserPolicyMap[name]
if !ok { if !ok {
return u, errNoSuchUser mappedPolicy, ok = cache.iamSTSPolicyMap[name]
} }
if !ok {
// Attempt to load parent user mapping for STS accounts
store.loadMappedPolicy(context.TODO(), name, stsUser, false, cache.iamSTSPolicyMap)
mappedPolicy, ok = cache.iamSTSPolicyMap[name]
if !ok {
return u, errNoSuchUser
}
}
return madmin.UserInfo{ return madmin.UserInfo{
PolicyName: mappedPolicy.Policies, PolicyName: mappedPolicy.Policies,
MemberOf: groups, MemberOf: groups,
@@ -1467,8 +1546,11 @@ func (store *IAMStoreSys) PolicyMappingNotificationHandler(ctx context.Context,
cache := store.lock() cache := store.lock()
defer store.unlock() defer store.unlock()
m := cache.iamGroupPolicyMap var m map[string]MappedPolicy
if !isGroup { switch {
case isGroup:
m = cache.iamGroupPolicyMap
default:
m = cache.iamUserPolicyMap m = cache.iamUserPolicyMap
} }
err := store.loadMappedPolicy(ctx, userOrGroup, userType, isGroup, m) err := store.loadMappedPolicy(ctx, userOrGroup, userType, isGroup, m)
@@ -1493,10 +1575,23 @@ func (store *IAMStoreSys) UserNotificationHandler(ctx context.Context, accessKey
cache := store.lock() cache := store.lock()
defer store.unlock() defer store.unlock()
err := store.loadUser(ctx, accessKey, userType, cache.iamUsersMap) var m map[string]UserIdentity
switch userType {
case stsUser:
m = cache.iamSTSAccountsMap
default:
m = cache.iamUsersMap
}
err := store.loadUser(ctx, accessKey, userType, m)
if err == errNoSuchUser { if err == errNoSuchUser {
// User was deleted - we update the cache. // User was deleted - we update the cache.
delete(cache.iamUsersMap, accessKey) delete(m, accessKey)
// Since cache was updated, we update the timestamp.
defer func() {
cache.updatedAt = time.Now()
}()
// 1. Start with updating user-group memberships // 1. Start with updating user-group memberships
if store.getUsersSysType() == MinIOUsersSysType { if store.getUsersSysType() == MinIOUsersSysType {
@@ -1514,12 +1609,14 @@ func (store *IAMStoreSys) UserNotificationHandler(ctx context.Context, accessKey
// 2. Remove any derived credentials from memory // 2. Remove any derived credentials from memory
if userType == regUser { if userType == regUser {
for _, u := range cache.iamUsersMap { for k, u := range cache.iamUsersMap {
if u.Credentials.IsServiceAccount() && u.Credentials.ParentUser == accessKey { if u.Credentials.IsServiceAccount() && u.Credentials.ParentUser == accessKey {
delete(cache.iamUsersMap, u.Credentials.AccessKey) delete(cache.iamUsersMap, k)
} }
if u.Credentials.IsTemp() && u.Credentials.ParentUser == accessKey { }
delete(cache.iamUsersMap, u.Credentials.AccessKey) for k, u := range cache.iamSTSAccountsMap {
if u.Credentials.ParentUser == accessKey {
delete(cache.iamSTSAccountsMap, k)
} }
} }
} }
@@ -1527,7 +1624,6 @@ func (store *IAMStoreSys) UserNotificationHandler(ctx context.Context, accessKey
// 3. Delete any mapped policy // 3. Delete any mapped policy
delete(cache.iamUserPolicyMap, accessKey) delete(cache.iamUserPolicyMap, accessKey)
cache.updatedAt = time.Now()
return nil return nil
} }
@@ -1535,31 +1631,43 @@ func (store *IAMStoreSys) UserNotificationHandler(ctx context.Context, accessKey
return err return err
} }
if userType != svcUser { // Since cache was updated, we update the timestamp.
err = store.loadMappedPolicy(ctx, accessKey, userType, false, cache.iamUserPolicyMap) defer func() {
// Ignore policy not mapped error cache.updatedAt = time.Now()
if err != nil && !errors.Is(err, errNoSuchPolicy) { }()
return err
}
}
// We are on purpose not persisting the policy map for parent cred := m[accessKey].Credentials
// user, although this is a hack, it is a good enough hack switch userType {
// at this point in time - we need to overhaul our OIDC case stsUser:
// usage with service accounts with a more cleaner implementation // For STS accounts a policy is mapped to the parent user (if a mapping exists).
// err = store.loadMappedPolicy(ctx, cred.ParentUser, userType, false, cache.iamSTSPolicyMap)
// This mapping is necessary to ensure that valid credentials case svcUser:
// have necessary ParentUser present - this is mainly for only // For service accounts, the parent may be a regular (internal) IDP
// webIdentity based STS tokens. // user or a "virtual" user (parent of an STS account).
u, ok := cache.iamUsersMap[accessKey] //
if ok { // If parent is a regular user => policy mapping is done on that parent itself.
cred := u.Credentials //
if cred.IsTemp() && cred.ParentUser != "" && cred.ParentUser != globalActiveCred.AccessKey { // If parent is "virtual" => policy mapping is done on the virtual
if _, ok := cache.iamUserPolicyMap[cred.ParentUser]; !ok { // parent and that virtual parent is an stsUser.
cache.iamUserPolicyMap[cred.ParentUser] = cache.iamUserPolicyMap[accessKey] //
cache.updatedAt = time.Now() // To load the appropriate mapping, we check the parent user type.
} _, parentIsRegularUser := cache.iamUsersMap[cred.ParentUser]
if parentIsRegularUser {
err = store.loadMappedPolicy(ctx, cred.ParentUser, regUser, false, cache.iamUserPolicyMap)
} else {
err = store.loadMappedPolicy(ctx, cred.ParentUser, stsUser, false, cache.iamSTSPolicyMap)
} }
case regUser:
// For regular users, we load the mapped policy.
err = store.loadMappedPolicy(ctx, accessKey, userType, false, cache.iamUserPolicyMap)
default:
// This is just to ensure that we have covered all cases for new
// code in future.
panic("unknown user type")
}
// Ignore policy not mapped error
if err != nil && !errors.Is(err, errNoSuchPolicy) {
return err
} }
return nil return nil
@@ -1648,7 +1756,7 @@ func (store *IAMStoreSys) SetTempUser(ctx context.Context, accessKey string, cre
return time.Time{}, err return time.Time{}, err
} }
cache.iamUserPolicyMap[cred.ParentUser] = mp cache.iamSTSPolicyMap[cred.ParentUser] = mp
} }
u := newUserIdentity(cred) u := newUserIdentity(cred)
@@ -1657,8 +1765,7 @@ func (store *IAMStoreSys) SetTempUser(ctx context.Context, accessKey string, cre
return time.Time{}, err return time.Time{}, err
} }
cache.iamUsersMap[accessKey] = u cache.iamSTSAccountsMap[accessKey] = u
cache.updatedAt = time.Now() cache.updatedAt = time.Now()
return u.UpdatedAt, nil return u.UpdatedAt, nil
@@ -1800,6 +1907,25 @@ func (store *IAMStoreSys) listUserPolicyMappings(cache *iamCache, users []string
}) })
} }
stsMap := map[string]MappedPolicy{}
for _, user := range users {
// Attempt to load parent user mapping for STS accounts
store.loadMappedPolicy(context.TODO(), user, stsUser, false, stsMap)
}
for user, mappedPolicy := range stsMap {
if userPredicate != nil && !userPredicate(user) {
continue
}
ps := mappedPolicy.toSlice()
sort.Strings(ps)
r = append(r, madmin.UserPolicyEntities{
User: user,
Policies: ps,
})
}
sort.Slice(r, func(i, j int) bool { sort.Slice(r, func(i, j int) bool {
return r[i].User < r[j].User return r[i].User < r[j].User
}) })
@@ -1864,6 +1990,32 @@ func (store *IAMStoreSys) listPolicyMappings(cache *iamCache, policies []string,
} }
} }
if iamOS, ok := store.IAMStorageAPI.(*IAMObjectStore); ok {
for item := range listIAMConfigItems(context.Background(), iamOS.objAPI, iamConfigPrefix+SlashSeparator+policyDBSTSUsersListKey) {
user := strings.TrimSuffix(item.Item, ".json")
if userPredicate != nil && !userPredicate(user) {
continue
}
var mappedPolicy MappedPolicy
store.loadIAMConfig(context.Background(), &mappedPolicy, getMappedPolicyPath(user, stsUser, false))
commonPolicySet := mappedPolicy.policySet()
if !queryPolSet.IsEmpty() {
commonPolicySet = commonPolicySet.Intersection(queryPolSet)
}
for _, policy := range commonPolicySet.ToSlice() {
s, ok := policyToUsersMap[policy]
if !ok {
policyToUsersMap[policy] = set.CreateStringSet(user)
} else {
s.Add(user)
policyToUsersMap[policy] = s
}
}
}
}
policyToGroupsMap := make(map[string]set.StringSet) policyToGroupsMap := make(map[string]set.StringSet)
for group, mappedPolicy := range cache.iamGroupPolicyMap { for group, mappedPolicy := range cache.iamGroupPolicyMap {
if groupPredicate != nil && !groupPredicate(group) { if groupPredicate != nil && !groupPredicate(group) {
@@ -2076,8 +2228,8 @@ func (store *IAMStoreSys) UpdateServiceAccount(ctx context.Context, accessKey st
// sessionPolicy is nil and there is embedded policy attached we remove // sessionPolicy is nil and there is embedded policy attached we remove
// embedded policy at that point. // embedded policy at that point.
if _, ok := m[iampolicy.SessionPolicyName]; ok && opts.sessionPolicy == nil { if _, ok := m[policy.SessionPolicyName]; ok && opts.sessionPolicy == nil {
delete(m, iampolicy.SessionPolicyName) delete(m, policy.SessionPolicyName)
m[iamPolicyClaimNameSA()] = inheritedPolicyType m[iamPolicyClaimNameSA()] = inheritedPolicyType
} }
@@ -2096,7 +2248,7 @@ func (store *IAMStoreSys) UpdateServiceAccount(ctx context.Context, accessKey st
} }
// Overwrite session policy claims. // Overwrite session policy claims.
m[iampolicy.SessionPolicyName] = base64.StdEncoding.EncodeToString(policyBuf) m[policy.SessionPolicyName] = base64.StdEncoding.EncodeToString(policyBuf)
m[iamPolicyClaimNameSA()] = embeddedPolicyType m[iamPolicyClaimNameSA()] = embeddedPolicyType
} }
@@ -2155,19 +2307,10 @@ func (store *IAMStoreSys) ListServiceAccounts(ctx context.Context, accessKey str
cache := store.rlock() cache := store.rlock()
defer store.runlock() defer store.runlock()
userExists := false
var serviceAccounts []auth.Credentials var serviceAccounts []auth.Credentials
for _, u := range cache.iamUsersMap { for _, u := range cache.iamUsersMap {
isDerived := false
v := u.Credentials v := u.Credentials
if v.IsServiceAccount() || v.IsTemp() { if accessKey != "" && v.ParentUser == accessKey {
isDerived = true
}
if !isDerived && v.AccessKey == accessKey {
userExists = true
} else if isDerived && v.ParentUser == accessKey {
userExists = true
if v.IsServiceAccount() { if v.IsServiceAccount() {
// Hide secret key & session key here // Hide secret key & session key here
v.SecretKey = "" v.SecretKey = ""
@@ -2177,12 +2320,6 @@ func (store *IAMStoreSys) ListServiceAccounts(ctx context.Context, accessKey str
} }
} }
// If root user has no STS/Service Accounts, userExists would be false here,
// so we handle this exception.
if !userExists && globalActiveCred.AccessKey != accessKey {
return nil, errNoSuchUser
}
return serviceAccounts, nil return serviceAccounts, nil
} }
@@ -2251,10 +2388,20 @@ func (store *IAMStoreSys) GetSTSAndServiceAccounts() []auth.Credentials {
var res []auth.Credentials var res []auth.Credentials
for _, u := range cache.iamUsersMap { for _, u := range cache.iamUsersMap {
cred := u.Credentials cred := u.Credentials
if cred.IsTemp() || cred.IsServiceAccount() { if cred.IsTemp() {
panic("unexpected STS credential found in iamUsersMap")
}
if cred.IsServiceAccount() {
res = append(res, cred) res = append(res, cred)
} }
} }
for _, u := range cache.iamSTSAccountsMap {
if !u.Credentials.IsTemp() {
panic("unexpected non STS credential found in iamSTSAccountsMap")
}
res = append(res, u.Credentials)
}
return res return res
} }
@@ -2289,35 +2436,56 @@ func (store *IAMStoreSys) LoadUser(ctx context.Context, accessKey string) {
cache.updatedAt = time.Now() cache.updatedAt = time.Now()
_, found := cache.iamUsersMap[accessKey] _, found := cache.iamUsersMap[accessKey]
// Check for regular user access key
if !found { if !found {
store.loadUser(ctx, accessKey, regUser, cache.iamUsersMap) store.loadUser(ctx, accessKey, regUser, cache.iamUsersMap)
if _, found = cache.iamUsersMap[accessKey]; found { if _, found = cache.iamUsersMap[accessKey]; found {
// load mapped policies // load mapped policies
store.loadMappedPolicy(ctx, accessKey, regUser, false, cache.iamUserPolicyMap) store.loadMappedPolicy(ctx, accessKey, regUser, false, cache.iamUserPolicyMap)
} else { }
// check for service account }
store.loadUser(ctx, accessKey, svcUser, cache.iamUsersMap)
if svc, found := cache.iamUsersMap[accessKey]; found { // Check for service account
// Load parent user and mapped policies. if !found {
if store.getUsersSysType() == MinIOUsersSysType { store.loadUser(ctx, accessKey, svcUser, cache.iamUsersMap)
store.loadUser(ctx, svc.Credentials.ParentUser, regUser, cache.iamUsersMap) if svc, found := cache.iamUsersMap[accessKey]; found {
} // Load parent user and mapped policies.
if store.getUsersSysType() == MinIOUsersSysType {
store.loadUser(ctx, svc.Credentials.ParentUser, regUser, cache.iamUsersMap)
store.loadMappedPolicy(ctx, svc.Credentials.ParentUser, regUser, false, cache.iamUserPolicyMap) store.loadMappedPolicy(ctx, svc.Credentials.ParentUser, regUser, false, cache.iamUserPolicyMap)
} else { } else {
// check for STS account // In case of LDAP the parent user's policy mapping needs to be
store.loadUser(ctx, accessKey, stsUser, cache.iamUsersMap) // loaded into sts map
if _, found = cache.iamUsersMap[accessKey]; found { store.loadMappedPolicy(ctx, svc.Credentials.ParentUser, stsUser, false, cache.iamSTSPolicyMap)
// Load mapped policy
store.loadMappedPolicy(ctx, accessKey, stsUser, false, cache.iamUserPolicyMap)
}
} }
} }
} }
// Check for STS account
stsAccountFound := false
var stsUserCred UserIdentity
if !found {
store.loadUser(ctx, accessKey, stsUser, cache.iamSTSAccountsMap)
if stsUserCred, found = cache.iamSTSAccountsMap[accessKey]; found {
// Load mapped policy
store.loadMappedPolicy(ctx, stsUserCred.Credentials.ParentUser, stsUser, false, cache.iamSTSPolicyMap)
stsAccountFound = true
}
}
// Load any associated policy definitions // Load any associated policy definitions
for _, policy := range cache.iamUserPolicyMap[accessKey].toSlice() { if !stsAccountFound {
if _, found = cache.iamPolicyDocsMap[policy]; !found { for _, policy := range cache.iamUserPolicyMap[accessKey].toSlice() {
store.loadPolicyDoc(ctx, policy, cache.iamPolicyDocsMap) if _, found = cache.iamPolicyDocsMap[policy]; !found {
store.loadPolicyDoc(ctx, policy, cache.iamPolicyDocsMap)
}
}
} else {
for _, policy := range cache.iamSTSPolicyMap[stsUserCred.Credentials.AccessKey].toSlice() {
if _, found = cache.iamPolicyDocsMap[policy]; !found {
store.loadPolicyDoc(ctx, policy, cache.iamPolicyDocsMap)
}
} }
} }
} }
+117 -120
View File
@@ -47,7 +47,7 @@ import (
xhttp "github.com/minio/minio/internal/http" xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/jwt" "github.com/minio/minio/internal/jwt"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
iampolicy "github.com/minio/pkg/v2/policy" "github.com/minio/pkg/v2/policy"
etcd "go.etcd.io/etcd/client/v3" etcd "go.etcd.io/etcd/client/v3"
) )
@@ -319,44 +319,7 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
refreshInterval := sys.iamRefreshInterval refreshInterval := sys.iamRefreshInterval
// Set up polling for expired accounts and credentials purging. go sys.periodicRoutines(ctx, refreshInterval)
switch {
case sys.OpenIDConfig.ProviderEnabled():
go func() {
timer := time.NewTimer(refreshInterval)
defer timer.Stop()
for {
select {
case <-timer.C:
sys.purgeExpiredCredentialsForExternalSSO(ctx)
timer.Reset(refreshInterval)
case <-ctx.Done():
return
}
}
}()
case sys.LDAPConfig.Enabled():
go func() {
timer := time.NewTimer(refreshInterval)
defer timer.Stop()
for {
select {
case <-timer.C:
sys.purgeExpiredCredentialsForLDAP(ctx)
sys.updateGroupMembershipsForLDAP(ctx)
timer.Reset(refreshInterval)
case <-ctx.Done():
return
}
}
}()
}
// Start watching changes to storage.
go sys.watch(ctx)
// Load RoleARNs // Load RoleARNs
sys.rolesMap = make(map[arn.ARN]string) sys.rolesMap = make(map[arn.ARN]string)
@@ -377,6 +340,79 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
bootstrapTraceMsg("finishing IAM loading") bootstrapTraceMsg("finishing IAM loading")
} }
func (sys *IAMSys) periodicRoutines(ctx context.Context, baseInterval time.Duration) {
// Watch for IAM config changes for iamStorageWatcher.
watcher, isWatcher := sys.store.IAMStorageAPI.(iamStorageWatcher)
if isWatcher {
go func() {
ch := watcher.watch(ctx, iamConfigPrefix)
for event := range ch {
if err := sys.loadWatchedEvent(ctx, event); err != nil {
// we simply log errors
logger.LogIf(ctx, fmt.Errorf("Failure in loading watch event: %v", err))
}
}
}()
}
r := rand.New(rand.NewSource(time.Now().UnixNano()))
// Add a random interval of up to 20% of the base interval.
randInterval := func() time.Duration {
return time.Duration(r.Float64() * float64(baseInterval) * 0.2)
}
var maxDurationSecondsForLog float64 = 5
timer := time.NewTimer(baseInterval + randInterval())
defer timer.Stop()
for {
select {
case <-timer.C:
// Load all IAM items (except STS creds) periodically.
refreshStart := time.Now()
if err := sys.Load(ctx, false); err != nil {
logger.LogIf(ctx, fmt.Errorf("Failure in periodic refresh for IAM (took %.2fs): %v", time.Since(refreshStart).Seconds(), err))
} else {
took := time.Since(refreshStart).Seconds()
if took > maxDurationSecondsForLog {
// Log if we took a lot of time to load.
logger.Info("IAM refresh took %.2fs", took)
}
}
// The following actions are performed about once in 4 times that
// IAM is refreshed:
if r.Intn(4) == 0 {
// Purge expired STS credentials.
purgeStart := time.Now()
if err := sys.store.PurgeExpiredSTS(ctx); err != nil {
logger.LogIf(ctx, fmt.Errorf("Failure in periodic STS purge for IAM (took %.2fs): %v", time.Since(purgeStart).Seconds(), err))
} else {
took := time.Since(purgeStart).Seconds()
if took > maxDurationSecondsForLog {
// Log if we took a lot of time to load.
logger.Info("IAM expired STS purge took %.2fs", took)
}
}
// Poll and remove accounts for those users who were removed
// from LDAP/OpenID.
if sys.LDAPConfig.Enabled() {
sys.purgeExpiredCredentialsForLDAP(ctx)
sys.updateGroupMembershipsForLDAP(ctx)
}
if sys.OpenIDConfig.ProviderEnabled() {
sys.purgeExpiredCredentialsForExternalSSO(ctx)
}
}
timer.Reset(baseInterval + randInterval())
case <-ctx.Done():
return
}
}
}
func (sys *IAMSys) validateAndAddRolePolicyMappings(ctx context.Context, m map[arn.ARN]string) { func (sys *IAMSys) validateAndAddRolePolicyMappings(ctx context.Context, m map[arn.ARN]string) {
// Validate that policies associated with roles are defined. If // Validate that policies associated with roles are defined. If
// authZ plugin is set, role policies are just claims sent to // authZ plugin is set, role policies are just claims sent to
@@ -428,45 +464,6 @@ func (sys *IAMSys) HasWatcher() bool {
return sys.store.HasWatcher() return sys.store.HasWatcher()
} }
func (sys *IAMSys) watch(ctx context.Context) {
watcher, ok := sys.store.IAMStorageAPI.(iamStorageWatcher)
if ok {
ch := watcher.watch(ctx, iamConfigPrefix)
for event := range ch {
if err := sys.loadWatchedEvent(ctx, event); err != nil {
// we simply log errors
logger.LogIf(ctx, fmt.Errorf("Failure in loading watch event: %v", err))
}
}
return
}
var maxRefreshDurationSecondsForLog float64 = 10
// Load all items periodically
timer := time.NewTimer(sys.iamRefreshInterval)
defer timer.Stop()
for {
select {
case <-timer.C:
refreshStart := time.Now()
if err := sys.Load(ctx, false); err != nil {
logger.LogIf(ctx, fmt.Errorf("Failure in periodic refresh for IAM (took %.2fs): %v", time.Since(refreshStart).Seconds(), err))
} else {
took := time.Since(refreshStart).Seconds()
if took > maxRefreshDurationSecondsForLog {
// Log if we took a lot of time to load.
logger.Info("IAM refresh took %.2fs", took)
}
}
timer.Reset(sys.iamRefreshInterval)
case <-ctx.Done():
return
}
}
}
func (sys *IAMSys) loadWatchedEvent(ctx context.Context, event iamWatchEvent) (err error) { func (sys *IAMSys) loadWatchedEvent(ctx context.Context, event iamWatchEvent) (err error) {
usersPrefix := strings.HasPrefix(event.keyPath, iamConfigUsersPrefix) usersPrefix := strings.HasPrefix(event.keyPath, iamConfigUsersPrefix)
groupsPrefix := strings.HasPrefix(event.keyPath, iamConfigGroupsPrefix) groupsPrefix := strings.HasPrefix(event.keyPath, iamConfigGroupsPrefix)
@@ -536,7 +533,7 @@ func (sys *IAMSys) DeletePolicy(ctx context.Context, policyName string, notifyPe
return errServerNotInitialized return errServerNotInitialized
} }
for _, v := range iampolicy.DefaultPolicies { for _, v := range policy.DefaultPolicies {
if v.Name == policyName { if v.Name == policyName {
if err := checkConfig(ctx, globalObjectAPI, getPolicyDocPath(policyName)); err != nil && err == errConfigNotFound { if err := checkConfig(ctx, globalObjectAPI, getPolicyDocPath(policyName)); err != nil && err == errConfigNotFound {
return fmt.Errorf("inbuilt policy `%s` not allowed to be deleted", policyName) return fmt.Errorf("inbuilt policy `%s` not allowed to be deleted", policyName)
@@ -589,7 +586,7 @@ func (sys *IAMSys) InfoPolicy(policyName string) (*madmin.PolicyInfo, error) {
} }
// ListPolicies - lists all canned policies. // ListPolicies - lists all canned policies.
func (sys *IAMSys) ListPolicies(ctx context.Context, bucketName string) (map[string]iampolicy.Policy, error) { func (sys *IAMSys) ListPolicies(ctx context.Context, bucketName string) (map[string]policy.Policy, error) {
if !sys.Initialized() { if !sys.Initialized() {
return nil, errServerNotInitialized return nil, errServerNotInitialized
} }
@@ -607,7 +604,7 @@ func (sys *IAMSys) ListPolicyDocs(ctx context.Context, bucketName string) (map[s
} }
// SetPolicy - sets a new named policy. // SetPolicy - sets a new named policy.
func (sys *IAMSys) SetPolicy(ctx context.Context, policyName string, p iampolicy.Policy) (time.Time, error) { func (sys *IAMSys) SetPolicy(ctx context.Context, policyName string, p policy.Policy) (time.Time, error) {
if !sys.Initialized() { if !sys.Initialized() {
return time.Time{}, errServerNotInitialized return time.Time{}, errServerNotInitialized
} }
@@ -849,13 +846,20 @@ func (sys *IAMSys) GetUserInfo(ctx context.Context, name string) (u madmin.UserI
return u, errServerNotInitialized return u, errServerNotInitialized
} }
loadUserCalled := false
select { select {
case <-sys.configLoaded: case <-sys.configLoaded:
default: default:
sys.store.LoadUser(ctx, name) sys.store.LoadUser(ctx, name)
loadUserCalled = true
} }
return sys.store.GetUserInfo(name) userInfo, err := sys.store.GetUserInfo(name)
if err == errNoSuchUser && !loadUserCalled {
sys.store.LoadUser(ctx, name)
userInfo, err = sys.store.GetUserInfo(name)
}
return userInfo, err
} }
// QueryPolicyEntities - queries policy associations for builtin users/groups/policies. // QueryPolicyEntities - queries policy associations for builtin users/groups/policies.
@@ -915,7 +919,7 @@ func (sys *IAMSys) notifyForServiceAccount(ctx context.Context, accessKey string
} }
type newServiceAccountOpts struct { type newServiceAccountOpts struct {
sessionPolicy *iampolicy.Policy sessionPolicy *policy.Policy
accessKey string accessKey string
secretKey string secretKey string
name, description string name, description string
@@ -962,7 +966,7 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro
m[parentClaim] = parentUser m[parentClaim] = parentUser
if len(policyBuf) > 0 { if len(policyBuf) > 0 {
m[iampolicy.SessionPolicyName] = base64.StdEncoding.EncodeToString(policyBuf) m[policy.SessionPolicyName] = base64.StdEncoding.EncodeToString(policyBuf)
m[iamPolicyClaimNameSA()] = embeddedPolicyType m[iamPolicyClaimNameSA()] = embeddedPolicyType
} else { } else {
m[iamPolicyClaimNameSA()] = inheritedPolicyType m[iamPolicyClaimNameSA()] = inheritedPolicyType
@@ -1014,7 +1018,7 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro
} }
type updateServiceAccountOpts struct { type updateServiceAccountOpts struct {
sessionPolicy *iampolicy.Policy sessionPolicy *policy.Policy
secretKey string secretKey string
status string status string
name, description string name, description string
@@ -1065,7 +1069,7 @@ func (sys *IAMSys) ListTempAccounts(ctx context.Context, accessKey string) ([]Us
} }
// GetServiceAccount - wrapper method to get information about a service account // GetServiceAccount - wrapper method to get information about a service account
func (sys *IAMSys) GetServiceAccount(ctx context.Context, accessKey string) (auth.Credentials, *iampolicy.Policy, error) { func (sys *IAMSys) GetServiceAccount(ctx context.Context, accessKey string) (auth.Credentials, *policy.Policy, error) {
sa, embeddedPolicy, err := sys.getServiceAccount(ctx, accessKey) sa, embeddedPolicy, err := sys.getServiceAccount(ctx, accessKey)
if err != nil { if err != nil {
return auth.Credentials{}, nil, err return auth.Credentials{}, nil, err
@@ -1076,7 +1080,7 @@ func (sys *IAMSys) GetServiceAccount(ctx context.Context, accessKey string) (aut
return sa.Credentials, embeddedPolicy, nil return sa.Credentials, embeddedPolicy, nil
} }
func (sys *IAMSys) getServiceAccount(ctx context.Context, accessKey string) (UserIdentity, *iampolicy.Policy, error) { func (sys *IAMSys) getServiceAccount(ctx context.Context, accessKey string) (UserIdentity, *policy.Policy, error) {
sa, jwtClaims, err := sys.getAccountWithClaims(ctx, accessKey) sa, jwtClaims, err := sys.getAccountWithClaims(ctx, accessKey)
if err != nil { if err != nil {
if err == errNoSuchAccount { if err == errNoSuchAccount {
@@ -1088,16 +1092,16 @@ func (sys *IAMSys) getServiceAccount(ctx context.Context, accessKey string) (Use
return UserIdentity{}, nil, errNoSuchServiceAccount return UserIdentity{}, nil, errNoSuchServiceAccount
} }
var embeddedPolicy *iampolicy.Policy var embeddedPolicy *policy.Policy
pt, ptok := jwtClaims.Lookup(iamPolicyClaimNameSA()) pt, ptok := jwtClaims.Lookup(iamPolicyClaimNameSA())
sp, spok := jwtClaims.Lookup(iampolicy.SessionPolicyName) sp, spok := jwtClaims.Lookup(policy.SessionPolicyName)
if ptok && spok && pt == embeddedPolicyType { if ptok && spok && pt == embeddedPolicyType {
policyBytes, err := base64.StdEncoding.DecodeString(sp) policyBytes, err := base64.StdEncoding.DecodeString(sp)
if err != nil { if err != nil {
return UserIdentity{}, nil, err return UserIdentity{}, nil, err
} }
embeddedPolicy, err = iampolicy.ParseConfig(bytes.NewReader(policyBytes)) embeddedPolicy, err = policy.ParseConfig(bytes.NewReader(policyBytes))
if err != nil { if err != nil {
return UserIdentity{}, nil, err return UserIdentity{}, nil, err
} }
@@ -1107,7 +1111,7 @@ func (sys *IAMSys) getServiceAccount(ctx context.Context, accessKey string) (Use
} }
// GetTemporaryAccount - wrapper method to get information about a temporary account // GetTemporaryAccount - wrapper method to get information about a temporary account
func (sys *IAMSys) GetTemporaryAccount(ctx context.Context, accessKey string) (auth.Credentials, *iampolicy.Policy, error) { func (sys *IAMSys) GetTemporaryAccount(ctx context.Context, accessKey string) (auth.Credentials, *policy.Policy, error) {
tmpAcc, embeddedPolicy, err := sys.getTempAccount(ctx, accessKey) tmpAcc, embeddedPolicy, err := sys.getTempAccount(ctx, accessKey)
if err != nil { if err != nil {
return auth.Credentials{}, nil, err return auth.Credentials{}, nil, err
@@ -1118,7 +1122,7 @@ func (sys *IAMSys) GetTemporaryAccount(ctx context.Context, accessKey string) (a
return tmpAcc.Credentials, embeddedPolicy, nil return tmpAcc.Credentials, embeddedPolicy, nil
} }
func (sys *IAMSys) getTempAccount(ctx context.Context, accessKey string) (UserIdentity, *iampolicy.Policy, error) { func (sys *IAMSys) getTempAccount(ctx context.Context, accessKey string) (UserIdentity, *policy.Policy, error) {
tmpAcc, claims, err := sys.getAccountWithClaims(ctx, accessKey) tmpAcc, claims, err := sys.getAccountWithClaims(ctx, accessKey)
if err != nil { if err != nil {
if err == errNoSuchAccount { if err == errNoSuchAccount {
@@ -1130,15 +1134,15 @@ func (sys *IAMSys) getTempAccount(ctx context.Context, accessKey string) (UserId
return UserIdentity{}, nil, errNoSuchTempAccount return UserIdentity{}, nil, errNoSuchTempAccount
} }
var embeddedPolicy *iampolicy.Policy var embeddedPolicy *policy.Policy
sp, spok := claims.Lookup(iampolicy.SessionPolicyName) sp, spok := claims.Lookup(policy.SessionPolicyName)
if spok { if spok {
policyBytes, err := base64.StdEncoding.DecodeString(sp) policyBytes, err := base64.StdEncoding.DecodeString(sp)
if err != nil { if err != nil {
return UserIdentity{}, nil, err return UserIdentity{}, nil, err
} }
embeddedPolicy, err = iampolicy.ParseConfig(bytes.NewReader(policyBytes)) embeddedPolicy, err = policy.ParseConfig(bytes.NewReader(policyBytes))
if err != nil { if err != nil {
return UserIdentity{}, nil, err return UserIdentity{}, nil, err
} }
@@ -1421,31 +1425,24 @@ func (sys *IAMSys) GetUser(ctx context.Context, accessKey string) (u UserIdentit
return u, false return u, false
} }
fallback := false if accessKey == globalActiveCred.AccessKey {
return newUserIdentity(globalActiveCred), true
}
loadUserCalled := false
select { select {
case <-sys.configLoaded: case <-sys.configLoaded:
default: default:
sys.store.LoadUser(ctx, accessKey) sys.store.LoadUser(ctx, accessKey)
fallback = true loadUserCalled = true
} }
u, ok = sys.store.GetUser(accessKey) u, ok = sys.store.GetUser(accessKey)
if !ok && !fallback { if !ok && !loadUserCalled {
// accessKey not found, also
// IAM store is not in fallback mode
// we can try to reload again from
// the IAM store and see if credential
// exists now. If it doesn't proceed to
// fail.
sys.store.LoadUser(ctx, accessKey) sys.store.LoadUser(ctx, accessKey)
u, ok = sys.store.GetUser(accessKey) u, ok = sys.store.GetUser(accessKey)
} }
if !ok {
if accessKey == globalActiveCred.AccessKey {
return newUserIdentity(globalActiveCred), true
}
}
return u, ok && u.Credentials.IsValid() return u, ok && u.Credentials.IsValid()
} }
@@ -1723,11 +1720,11 @@ func (sys *IAMSys) PolicyDBGet(name string, isGroup bool, groups ...string) ([]s
return sys.store.PolicyDBGet(name, isGroup, groups...) return sys.store.PolicyDBGet(name, isGroup, groups...)
} }
const sessionPolicyNameExtracted = iampolicy.SessionPolicyName + "-extracted" const sessionPolicyNameExtracted = policy.SessionPolicyName + "-extracted"
// IsAllowedServiceAccount - checks if the given service account is allowed to perform // IsAllowedServiceAccount - checks if the given service account is allowed to perform
// actions. The permission of the parent user is checked first // actions. The permission of the parent user is checked first
func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parentUser string) bool { func (sys *IAMSys) IsAllowedServiceAccount(args policy.Args, parentUser string) bool {
// Verify if the parent claim matches the parentUser. // Verify if the parent claim matches the parentUser.
p, ok := args.Claims[parentClaim] p, ok := args.Claims[parentClaim]
if ok { if ok {
@@ -1778,7 +1775,7 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parentUser strin
// Finally, if there is no parent policy, check if a policy claim is // Finally, if there is no parent policy, check if a policy claim is
// present. // present.
if len(svcPolicies) == 0 { if len(svcPolicies) == 0 {
policySet, _ := iampolicy.GetPoliciesFromClaims(args.Claims, iamPolicyClaimNameOpenID()) policySet, _ := policy.GetPoliciesFromClaims(args.Claims, iamPolicyClaimNameOpenID())
svcPolicies = policySet.ToSlice() svcPolicies = policySet.ToSlice()
} }
} }
@@ -1788,7 +1785,7 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parentUser strin
return false return false
} }
var combinedPolicy iampolicy.Policy var combinedPolicy policy.Policy
// Policies were found, evaluate all of them. // Policies were found, evaluate all of them.
if !isOwnerDerived { if !isOwnerDerived {
availablePoliciesStr, c := sys.store.FilterPolicies(strings.Join(svcPolicies, ","), "") availablePoliciesStr, c := sys.store.FilterPolicies(strings.Join(svcPolicies, ","), "")
@@ -1831,7 +1828,7 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parentUser strin
} }
// Check if policy is parseable. // Check if policy is parseable.
subPolicy, err := iampolicy.ParseConfig(bytes.NewReader([]byte(spolicyStr))) subPolicy, err := policy.ParseConfig(bytes.NewReader([]byte(spolicyStr)))
if err != nil { if err != nil {
// Log any error in input session policy config. // Log any error in input session policy config.
logger.LogIf(GlobalContext, err) logger.LogIf(GlobalContext, err)
@@ -1853,7 +1850,7 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parentUser strin
// IsAllowedSTS is meant for STS based temporary credentials, // IsAllowedSTS is meant for STS based temporary credentials,
// which implements claims validation and verification other than // which implements claims validation and verification other than
// applying policies. // applying policies.
func (sys *IAMSys) IsAllowedSTS(args iampolicy.Args, parentUser string) bool { func (sys *IAMSys) IsAllowedSTS(args policy.Args, parentUser string) bool {
// 1. Determine mapped policies // 1. Determine mapped policies
isOwnerDerived := parentUser == globalActiveCred.AccessKey isOwnerDerived := parentUser == globalActiveCred.AccessKey
@@ -1905,7 +1902,7 @@ func (sys *IAMSys) IsAllowedSTS(args iampolicy.Args, parentUser string) bool {
// 2. Combine the mapped policies into a single combined policy. // 2. Combine the mapped policies into a single combined policy.
var combinedPolicy iampolicy.Policy var combinedPolicy policy.Policy
if !isOwnerDerived { if !isOwnerDerived {
var err error var err error
combinedPolicy, err = sys.store.GetPolicy(strings.Join(policies, ",")) combinedPolicy, err = sys.store.GetPolicy(strings.Join(policies, ","))
@@ -1937,7 +1934,7 @@ func (sys *IAMSys) IsAllowedSTS(args iampolicy.Args, parentUser string) bool {
return isOwnerDerived || combinedPolicy.IsAllowed(args) return isOwnerDerived || combinedPolicy.IsAllowed(args)
} }
func isAllowedBySessionPolicy(args iampolicy.Args) (hasSessionPolicy bool, isAllowed bool) { func isAllowedBySessionPolicy(args policy.Args) (hasSessionPolicy bool, isAllowed bool) {
hasSessionPolicy = false hasSessionPolicy = false
isAllowed = false isAllowed = false
@@ -1957,7 +1954,7 @@ func isAllowedBySessionPolicy(args iampolicy.Args) (hasSessionPolicy bool, isAll
} }
// Check if policy is parseable. // Check if policy is parseable.
subPolicy, err := iampolicy.ParseConfig(bytes.NewReader([]byte(spolicyStr))) subPolicy, err := policy.ParseConfig(bytes.NewReader([]byte(spolicyStr)))
if err != nil { if err != nil {
// Log any error in input session policy config. // Log any error in input session policy config.
logger.LogIf(GlobalContext, err) logger.LogIf(GlobalContext, err)
@@ -1974,13 +1971,13 @@ func isAllowedBySessionPolicy(args iampolicy.Args) (hasSessionPolicy bool, isAll
} }
// GetCombinedPolicy returns a combined policy combining all policies // GetCombinedPolicy returns a combined policy combining all policies
func (sys *IAMSys) GetCombinedPolicy(policies ...string) iampolicy.Policy { func (sys *IAMSys) GetCombinedPolicy(policies ...string) policy.Policy {
_, policy := sys.store.FilterPolicies(strings.Join(policies, ","), "") _, policy := sys.store.FilterPolicies(strings.Join(policies, ","), "")
return policy return policy
} }
// IsAllowed - checks given policy args is allowed to continue the Rest API. // IsAllowed - checks given policy args is allowed to continue the Rest API.
func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool { func (sys *IAMSys) IsAllowed(args policy.Args) bool {
// If opa is configured, use OPA always. // If opa is configured, use OPA always.
if authz := newGlobalAuthZPluginFn(); authz != nil { if authz := newGlobalAuthZPluginFn(); authz != nil {
ok, err := authz.IsAllowed(args) ok, err := authz.IsAllowed(args)
+2 -2
View File
@@ -28,7 +28,7 @@ import (
"github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/auth"
xjwt "github.com/minio/minio/internal/jwt" xjwt "github.com/minio/minio/internal/jwt"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
iampolicy "github.com/minio/pkg/v2/policy" "github.com/minio/pkg/v2/policy"
) )
const ( const (
@@ -148,7 +148,7 @@ func metricsRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, []string, b
} }
// Now check if we have a sessionPolicy. // Now check if we have a sessionPolicy.
if _, ok = eclaims[iampolicy.SessionPolicyName]; ok { if _, ok = eclaims[policy.SessionPolicyName]; ok {
owner = false owner = false
} else { } else {
owner = globalActiveCred.AccessKey == ucred.ParentUser owner = globalActiveCred.AccessKey == ucred.ParentUser
+19 -19
View File
@@ -29,14 +29,14 @@ import (
"github.com/minio/madmin-go/v3" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/kms" "github.com/minio/minio/internal/kms"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
iampolicy "github.com/minio/pkg/v2/policy" "github.com/minio/pkg/v2/policy"
) )
// KMSStatusHandler - GET /minio/kms/v1/status // KMSStatusHandler - GET /minio/kms/v1/status
func (a kmsAPIHandlers) KMSStatusHandler(w http.ResponseWriter, r *http.Request) { func (a kmsAPIHandlers) KMSStatusHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "KMSStatus") ctx := newContext(r, w, "KMSStatus")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSStatusAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSStatusAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -74,7 +74,7 @@ func (a kmsAPIHandlers) KMSMetricsHandler(w http.ResponseWriter, r *http.Request
ctx := newContext(r, w, "KMSMetrics") ctx := newContext(r, w, "KMSMetrics")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSMetricsAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSMetricsAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -106,7 +106,7 @@ func (a kmsAPIHandlers) KMSAPIsHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "KMSAPIs") ctx := newContext(r, w, "KMSAPIs")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSAPIAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSAPIAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -143,7 +143,7 @@ func (a kmsAPIHandlers) KMSVersionHandler(w http.ResponseWriter, r *http.Request
ctx := newContext(r, w, "KMSVersion") ctx := newContext(r, w, "KMSVersion")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSVersionAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSVersionAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -183,7 +183,7 @@ func (a kmsAPIHandlers) KMSCreateKeyHandler(w http.ResponseWriter, r *http.Reque
} }
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSCreateKeyAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSCreateKeyAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -211,7 +211,7 @@ func (a kmsAPIHandlers) KMSDeleteKeyHandler(w http.ResponseWriter, r *http.Reque
ctx := newContext(r, w, "KMSDeleteKey") ctx := newContext(r, w, "KMSDeleteKey")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSDeleteKeyAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSDeleteKeyAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -246,7 +246,7 @@ func (a kmsAPIHandlers) KMSListKeysHandler(w http.ResponseWriter, r *http.Reques
} }
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSListKeysAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSListKeysAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -297,7 +297,7 @@ func (a kmsAPIHandlers) KMSImportKeyHandler(w http.ResponseWriter, r *http.Reque
ctx := newContext(r, w, "KMSImportKey") ctx := newContext(r, w, "KMSImportKey")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSImportKeyAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSImportKeyAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -328,7 +328,7 @@ func (a kmsAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Reque
ctx := newContext(r, w, "KMSKeyStatus") ctx := newContext(r, w, "KMSKeyStatus")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSKeyStatusAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSKeyStatusAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -404,7 +404,7 @@ func (a kmsAPIHandlers) KMSDescribePolicyHandler(w http.ResponseWriter, r *http.
ctx := newContext(r, w, "KMSDescribePolicy") ctx := newContext(r, w, "KMSDescribePolicy")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSDescribePolicyAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSDescribePolicyAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -440,7 +440,7 @@ func (a kmsAPIHandlers) KMSAssignPolicyHandler(w http.ResponseWriter, r *http.Re
ctx := newContext(r, w, "KMSAssignPolicy") ctx := newContext(r, w, "KMSAssignPolicy")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSAssignPolicyAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSAssignPolicyAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -472,7 +472,7 @@ func (a kmsAPIHandlers) KMSDeletePolicyHandler(w http.ResponseWriter, r *http.Re
ctx := newContext(r, w, "KMSDeletePolicy") ctx := newContext(r, w, "KMSDeletePolicy")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSDeletePolicyAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSDeletePolicyAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -499,7 +499,7 @@ func (a kmsAPIHandlers) KMSListPoliciesHandler(w http.ResponseWriter, r *http.Re
ctx := newContext(r, w, "KMSListPolicies") ctx := newContext(r, w, "KMSListPolicies")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSListPoliciesAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSListPoliciesAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -546,7 +546,7 @@ func (a kmsAPIHandlers) KMSGetPolicyHandler(w http.ResponseWriter, r *http.Reque
ctx := newContext(r, w, "KMSGetPolicy") ctx := newContext(r, w, "KMSGetPolicy")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSGetPolicyAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSGetPolicyAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -578,7 +578,7 @@ func (a kmsAPIHandlers) KMSDescribeIdentityHandler(w http.ResponseWriter, r *htt
ctx := newContext(r, w, "KMSDescribeIdentity") ctx := newContext(r, w, "KMSDescribeIdentity")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSDescribeIdentityAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSDescribeIdentityAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -619,7 +619,7 @@ func (a kmsAPIHandlers) KMSDescribeSelfIdentityHandler(w http.ResponseWriter, r
ctx := newContext(r, w, "KMSDescribeSelfIdentity") ctx := newContext(r, w, "KMSDescribeSelfIdentity")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSDescribeSelfIdentityAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSDescribeSelfIdentityAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -659,7 +659,7 @@ func (a kmsAPIHandlers) KMSDeleteIdentityHandler(w http.ResponseWriter, r *http.
ctx := newContext(r, w, "KMSDeleteIdentity") ctx := newContext(r, w, "KMSDeleteIdentity")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSDeleteIdentityAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSDeleteIdentityAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -686,7 +686,7 @@ func (a kmsAPIHandlers) KMSListIdentitiesHandler(w http.ResponseWriter, r *http.
ctx := newContext(r, w, "KMSListIdentities") ctx := newContext(r, w, "KMSListIdentities")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSListIdentitiesAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSListIdentitiesAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
+1 -1
View File
@@ -116,7 +116,7 @@ func (api objectAPIHandlers) ListenNotificationHandler(w http.ResponseWriter, r
// Listen Publisher and peer-listen-client uses nonblocking send and hence does not wait for slow receivers. // Listen Publisher and peer-listen-client uses nonblocking send and hence does not wait for slow receivers.
// Use buffered channel to take care of burst sends or slow w.Write() // Use buffered channel to take care of burst sends or slow w.Write()
listenCh := make(chan event.Event, 4000) listenCh := make(chan event.Event, globalAPIConfig.getRequestsPoolCapacity()*len(globalEndpoints.Hostnames()))
peers, _ := newPeerRestClients(globalEndpoints) peers, _ := newPeerRestClients(globalEndpoints)
+7 -3
View File
@@ -38,7 +38,7 @@ func renameAllBucketMetacache(epPath string) error {
if typ == os.ModeDir { if typ == os.ModeDir {
tmpMetacacheOld := pathutil.Join(epPath, minioMetaTmpDeletedBucket, mustGetUUID()) tmpMetacacheOld := pathutil.Join(epPath, minioMetaTmpDeletedBucket, mustGetUUID())
if err := renameAll(pathJoin(epPath, minioMetaBucket, metacachePrefixForID(name, slashSeparator)), if err := renameAll(pathJoin(epPath, minioMetaBucket, metacachePrefixForID(name, slashSeparator)),
tmpMetacacheOld); err != nil && err != errFileNotFound { tmpMetacacheOld, epPath); err != nil && err != errFileNotFound {
return fmt.Errorf("unable to rename (%s -> %s) %w", return fmt.Errorf("unable to rename (%s -> %s) %w",
pathJoin(epPath, minioMetaBucket+metacachePrefixForID(minioMetaBucket, slashSeparator)), pathJoin(epPath, minioMetaBucket+metacachePrefixForID(minioMetaBucket, slashSeparator)),
tmpMetacacheOld, tmpMetacacheOld,
@@ -60,6 +60,10 @@ func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) (
if err := checkListObjsArgs(ctx, o.Bucket, o.Prefix, o.Marker, z); err != nil { if err := checkListObjsArgs(ctx, o.Bucket, o.Prefix, o.Marker, z); err != nil {
return entries, err return entries, err
} }
// Marker points to before the prefix, just ignore it.
if o.Marker < o.Prefix {
o.Marker = ""
}
// Marker is set validate pre-condition. // Marker is set validate pre-condition.
if o.Marker != "" && o.Prefix != "" { if o.Marker != "" && o.Prefix != "" {
@@ -366,7 +370,7 @@ func applyBucketActions(ctx context.Context, o listPathOptions, in <-chan metaCa
objInfo := fi.ToObjectInfo(o.Bucket, obj.name, versioned) objInfo := fi.ToObjectInfo(o.Bucket, obj.name, versioned)
if o.Lifecycle != nil { if o.Lifecycle != nil {
act := evalActionFromLifecycle(ctx, *o.Lifecycle, o.Retention, objInfo).Action act := evalActionFromLifecycle(ctx, *o.Lifecycle, o.Retention, o.Replication.Config, objInfo).Action
skip = act.Delete() skip = act.Delete()
if act.DeleteRestored() { if act.DeleteRestored() {
// do not skip DeleteRestored* actions // do not skip DeleteRestored* actions
@@ -394,7 +398,7 @@ func applyBucketActions(ctx context.Context, o listPathOptions, in <-chan metaCa
objInfo := version.ToObjectInfo(o.Bucket, obj.name, versioned) objInfo := version.ToObjectInfo(o.Bucket, obj.name, versioned)
if o.Lifecycle != nil { if o.Lifecycle != nil {
evt := evalActionFromLifecycle(ctx, *o.Lifecycle, o.Retention, objInfo) evt := evalActionFromLifecycle(ctx, *o.Lifecycle, o.Retention, o.Replication.Config, objInfo)
if evt.Action.Delete() { if evt.Action.Delete() {
globalExpiryState.enqueueByDays(objInfo, evt, lcEventSrc_s3ListObjects) globalExpiryState.enqueueByDays(objInfo, evt, lcEventSrc_s3ListObjects)
if !evt.Action.DeleteRestored() { if !evt.Action.DeleteRestored() {
+1 -1
View File
@@ -761,7 +761,7 @@ func (er *erasureObjects) saveMetaCacheStream(ctx context.Context, mc *metaCache
return nil return nil
} }
o.debugln(color.Green("saveMetaCacheStream:")+" saving block", b.n, "to", o.objectPath(b.n)) o.debugln(color.Green("saveMetaCacheStream:")+" saving block", b.n, "to", o.objectPath(b.n))
r, err := hash.NewReader(bytes.NewReader(b.data), int64(len(b.data)), "", "", int64(len(b.data))) r, err := hash.NewReader(ctx, bytes.NewReader(b.data), int64(len(b.data)), "", "", int64(len(b.data)))
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
custom := b.headerKV() custom := b.headerKV()
_, err = er.putMetacacheObject(ctx, o.objectPath(b.n), NewPutObjReader(r), ObjectOptions{ _, err = er.putMetacacheObject(ctx, o.objectPath(b.n), NewPutObjReader(r), ObjectOptions{
+31
View File
@@ -19,12 +19,15 @@ package cmd
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"time" "time"
"github.com/minio/madmin-go/v3" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/disk" "github.com/minio/minio/internal/disk"
"github.com/minio/minio/internal/net" "github.com/minio/minio/internal/net"
c "github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/load"
) )
type collectMetricsOpts struct { type collectMetricsOpts struct {
@@ -89,6 +92,34 @@ func collectLocalMetrics(types madmin.MetricType, opts collectMetricsOpts) (m ma
m.Aggregated.Net.NetStats = netStats m.Aggregated.Net.NetStats = netStats
} }
} }
if types.Contains(madmin.MetricsMem) {
m.Aggregated.Mem = &madmin.MemMetrics{
CollectedAt: UTCNow(),
}
m.Aggregated.Mem.Info = madmin.GetMemInfo(GlobalContext, globalMinioAddr)
}
if types.Contains(madmin.MetricsCPU) {
m.Aggregated.CPU = &madmin.CPUMetrics{
CollectedAt: UTCNow(),
}
cm, err := c.Times(false)
if err != nil {
m.Errors = append(m.Errors, err.Error())
} else {
// not collecting per-cpu stats, so there will be only one element
if len(cm) == 1 {
m.Aggregated.CPU.TimesStat = &cm[0]
} else {
m.Errors = append(m.Errors, fmt.Sprintf("Expected one CPU stat, got %d", len(cm)))
}
}
loadStat, err := load.Avg()
if err != nil {
m.Errors = append(m.Errors, err.Error())
} else {
m.Aggregated.CPU.LoadStat = loadStat
}
}
// Add types... // Add types...
// ByHost is a shallow reference, so careful about sharing. // ByHost is a shallow reference, so careful about sharing.
+453
View File
@@ -0,0 +1,453 @@
// Copyright (c) 2015-2023 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package cmd
import (
"context"
"fmt"
"math"
"net/http"
"sync"
"time"
"github.com/minio/madmin-go/v3"
"github.com/prometheus/client_golang/prometheus"
"github.com/shirou/gopsutil/v3/host"
)
const (
resourceMetricsCollectionInterval = time.Minute
resourceMetricsCacheInterval = time.Minute
// drive stats
totalInodes MetricName = "total_inodes"
readsPerSec MetricName = "reads_per_sec"
writesPerSec MetricName = "writes_per_sec"
readsKBPerSec MetricName = "reads_kb_per_sec"
writesKBPerSec MetricName = "writes_kb_per_sec"
readsAwait MetricName = "reads_await"
writesAwait MetricName = "writes_await"
percUtil MetricName = "perc_util"
usedInodes MetricName = "used_inodes"
// network stats
interfaceRxBytes MetricName = "rx_bytes"
interfaceRxErrors MetricName = "rx_errors"
interfaceTxBytes MetricName = "tx_bytes"
interfaceTxErrors MetricName = "tx_errors"
// memory stats
memUsed MetricName = "used"
memFree MetricName = "free"
memShared MetricName = "shared"
memBuffers MetricName = "buffers"
memCache MetricName = "cache"
memAvailable MetricName = "available"
// cpu stats
cpuUser MetricName = "user"
cpuSystem MetricName = "system"
cpuIOWait MetricName = "iowait"
cpuIdle MetricName = "idle"
cpuNice MetricName = "nice"
cpuSteal MetricName = "steal"
cpuLoad1 MetricName = "load1"
cpuLoad5 MetricName = "load5"
cpuLoad15 MetricName = "load15"
)
var (
resourceCollector *minioResourceCollector
// resourceMetricsMap is a map of subsystem to its metrics
resourceMetricsMap map[MetricSubsystem]ResourceMetrics
// resourceMetricsHelpMap maps metric name to its help string
resourceMetricsHelpMap map[MetricName]string
resourceMetricsGroups []*MetricsGroup
)
// PeerResourceMetrics represents the resource metrics
// retrieved from a peer, along with errors if any
type PeerResourceMetrics struct {
Metrics map[MetricSubsystem]ResourceMetrics
Errors []string
}
// ResourceMetrics is a map of unique key identifying
// a resource metric (e.g. reads_per_sec_{node}_{drive})
// to its data
type ResourceMetrics map[string]ResourceMetric
// ResourceMetric represents a single resource metric
// The metrics are collected from all servers periodically
// and stored in the resource metrics map.
// It also maintains the count of number of times this metric
// was collected since the server started, and the sum,
// average and max values across the same.
type ResourceMetric struct {
Name MetricName
Labels map[string]string
// value captured in current cycle
Current float64
// Used when system provides cumulative (since uptime) values
// helps in calculating the current value by comparing the new
// cumulative value with previous one
Cumulative float64
Max float64
Avg float64
Sum float64
Count uint64
}
func init() {
interval := fmt.Sprintf("%ds", int(resourceMetricsCollectionInterval.Seconds()))
resourceMetricsHelpMap = map[MetricName]string{
interfaceRxBytes: "Bytes received on the interface in " + interval,
interfaceRxErrors: "Receive errors in " + interval,
interfaceTxBytes: "Bytes transmitted in " + interval,
interfaceTxErrors: "Transmit errors in " + interval,
total: "Total memory on the node",
memUsed: "Used memory on the node",
memFree: "Free memory on the node",
memShared: "Shared memory on the node",
memBuffers: "Buffers memory on the node",
memCache: "Cache memory on the node",
memAvailable: "Available memory on the node",
readsPerSec: "Reads per second on a drive",
writesPerSec: "Writes per second on a drive",
readsKBPerSec: "Kilobytes read per second on a drive",
writesKBPerSec: "Kilobytes written per second on a drive",
readsAwait: "Average time for read requests to be served on a drive",
writesAwait: "Average time for write requests to be served on a drive",
percUtil: "Percentage of time the disk was busy since uptime",
usedBytes: "Used bytes on a drive",
totalBytes: "Total bytes on a drive",
usedInodes: "Total inodes used on a drive",
totalInodes: "Total inodes on a drive",
cpuUser: "CPU user time",
cpuSystem: "CPU system time",
cpuIdle: "CPU idle time",
cpuIOWait: "CPU ioWait time",
cpuSteal: "CPU steal time",
cpuNice: "CPU nice time",
cpuLoad1: "CPU load average 1min",
cpuLoad5: "CPU load average 5min",
cpuLoad15: "CPU load average 15min",
}
resourceMetricsGroups = []*MetricsGroup{
getResourceMetrics(),
}
resourceCollector = newMinioResourceCollector(resourceMetricsGroups)
}
func updateResourceMetrics(subSys MetricSubsystem, name MetricName, val float64, labels map[string]string, isCumulative bool) {
subsysMetrics, found := resourceMetricsMap[subSys]
if !found {
subsysMetrics = ResourceMetrics{}
}
// labels are used to uniquely identify a metric
// e.g. reads_per_sec_{drive} inside the map
sfx := ""
for _, v := range labels {
if len(sfx) > 0 {
sfx += "_"
}
sfx += v
}
key := string(name) + "_" + sfx
metric, found := subsysMetrics[key]
if !found {
metric = ResourceMetric{
Name: name,
Labels: labels,
}
}
if isCumulative {
metric.Current = val - metric.Cumulative
metric.Cumulative = val
} else {
metric.Current = val
}
if metric.Current > metric.Max {
metric.Max = val
}
metric.Sum += metric.Current
metric.Count++
metric.Avg = metric.Sum / float64(metric.Count)
subsysMetrics[key] = metric
resourceMetricsMap[subSys] = subsysMetrics
}
func collectDriveMetrics(m madmin.RealtimeMetrics) {
upt, _ := host.Uptime()
kib := 1 << 10
sectorSize := uint64(512)
for d, dm := range m.ByDisk {
stats := dm.IOStats
labels := map[string]string{"drive": d}
updateResourceMetrics(driveSubsystem, readsPerSec, float64(stats.ReadIOs)/float64(upt), labels, false)
readBytes := stats.ReadSectors * sectorSize
readKib := float64(readBytes) / float64(kib)
readKibPerSec := readKib / float64(upt)
updateResourceMetrics(driveSubsystem, readsKBPerSec, readKibPerSec, labels, false)
updateResourceMetrics(driveSubsystem, writesPerSec, float64(stats.WriteIOs)/float64(upt), labels, false)
writeBytes := stats.WriteSectors * sectorSize
writeKib := float64(writeBytes) / float64(kib)
writeKibPerSec := writeKib / float64(upt)
updateResourceMetrics(driveSubsystem, writesKBPerSec, writeKibPerSec, labels, false)
rdAwait := 0.0
if stats.ReadIOs > 0 {
rdAwait = float64(stats.ReadTicks) / float64(stats.ReadIOs)
}
updateResourceMetrics(driveSubsystem, readsAwait, rdAwait, labels, false)
wrAwait := 0.0
if stats.WriteIOs > 0 {
wrAwait = float64(stats.WriteTicks) / float64(stats.WriteIOs)
}
updateResourceMetrics(driveSubsystem, writesAwait, wrAwait, labels, false)
updateResourceMetrics(driveSubsystem, percUtil, float64(stats.TotalTicks)/float64(upt*10), labels, false)
}
globalLocalDrivesMu.RLock()
gld := globalLocalDrives
globalLocalDrivesMu.RUnlock()
for _, d := range gld {
labels := map[string]string{"drive": d.Endpoint().RawPath}
di, err := d.DiskInfo(GlobalContext, false)
if err == nil {
updateResourceMetrics(driveSubsystem, usedBytes, float64(di.Used), labels, false)
updateResourceMetrics(driveSubsystem, totalBytes, float64(di.Total), labels, false)
updateResourceMetrics(driveSubsystem, usedInodes, float64(di.UsedInodes), labels, false)
updateResourceMetrics(driveSubsystem, totalInodes, float64(di.FreeInodes+di.UsedInodes), labels, false)
}
}
}
func collectLocalResourceMetrics() {
var types madmin.MetricType = madmin.MetricsDisk | madmin.MetricNet | madmin.MetricsMem | madmin.MetricsCPU
m := collectLocalMetrics(types, collectMetricsOpts{
hosts: map[string]struct{}{
globalLocalNodeName: {},
},
})
for host, hm := range m.ByHost {
if len(host) > 0 {
if hm.Net != nil && len(hm.Net.NetStats.Name) > 0 {
stats := hm.Net.NetStats
labels := map[string]string{"interface": stats.Name}
updateResourceMetrics(interfaceSubsystem, interfaceRxBytes, float64(stats.RxBytes), labels, true)
updateResourceMetrics(interfaceSubsystem, interfaceRxErrors, float64(stats.RxErrors), labels, true)
updateResourceMetrics(interfaceSubsystem, interfaceTxBytes, float64(stats.TxBytes), labels, true)
updateResourceMetrics(interfaceSubsystem, interfaceTxErrors, float64(stats.TxErrors), labels, true)
}
if hm.Mem != nil && len(hm.Mem.Info.Addr) > 0 {
labels := map[string]string{}
stats := hm.Mem.Info
updateResourceMetrics(memSubsystem, total, float64(stats.Total), labels, false)
updateResourceMetrics(memSubsystem, memUsed, float64(stats.Used), labels, false)
updateResourceMetrics(memSubsystem, memFree, float64(stats.Free), labels, false)
updateResourceMetrics(memSubsystem, memShared, float64(stats.Shared), labels, false)
updateResourceMetrics(memSubsystem, memBuffers, float64(stats.Buffers), labels, false)
updateResourceMetrics(memSubsystem, memAvailable, float64(stats.Available), labels, false)
updateResourceMetrics(memSubsystem, memCache, float64(stats.Cache), labels, false)
}
if hm.CPU != nil {
labels := map[string]string{}
ts := hm.CPU.TimesStat
if ts != nil {
tot := ts.User + ts.System + ts.Idle + ts.Iowait + ts.Nice + ts.Steal
cpuUserVal := math.Round(ts.User/tot*100*100) / 100
updateResourceMetrics(cpuSubsystem, cpuUser, cpuUserVal, labels, false)
cpuSystemVal := math.Round(ts.System/tot*100*100) / 100
updateResourceMetrics(cpuSubsystem, cpuSystem, cpuSystemVal, labels, false)
cpuIdleVal := math.Round(ts.Idle/tot*100*100) / 100
updateResourceMetrics(cpuSubsystem, cpuIdle, cpuIdleVal, labels, false)
cpuIOWaitVal := math.Round(ts.Iowait/tot*100*100) / 100
updateResourceMetrics(cpuSubsystem, cpuIOWait, cpuIOWaitVal, labels, false)
cpuNiceVal := math.Round(ts.Nice/tot*100*100) / 100
updateResourceMetrics(cpuSubsystem, cpuNice, cpuNiceVal, labels, false)
cpuStealVal := math.Round(ts.Steal/tot*100*100) / 100
updateResourceMetrics(cpuSubsystem, cpuSteal, cpuStealVal, labels, false)
}
ls := hm.CPU.LoadStat
if ls != nil {
updateResourceMetrics(cpuSubsystem, cpuLoad1, ls.Load1, labels, false)
updateResourceMetrics(cpuSubsystem, cpuLoad5, ls.Load5, labels, false)
updateResourceMetrics(cpuSubsystem, cpuLoad15, ls.Load15, labels, false)
}
}
break // only one host expected
}
}
collectDriveMetrics(m)
}
// startResourceMetricsCollection - starts the job for collecting resource metrics
func startResourceMetricsCollection() {
resourceMetricsMap = map[MetricSubsystem]ResourceMetrics{}
metricsTimer := time.NewTimer(resourceMetricsCollectionInterval)
defer metricsTimer.Stop()
collectLocalResourceMetrics()
for {
select {
case <-GlobalContext.Done():
return
case <-metricsTimer.C:
collectLocalResourceMetrics()
// Reset the timer for next cycle.
metricsTimer.Reset(resourceMetricsCollectionInterval)
}
}
}
// minioResourceCollector is the Collector for resource metrics
type minioResourceCollector struct {
metricsGroups []*MetricsGroup
desc *prometheus.Desc
}
// Describe sends the super-set of all possible descriptors of metrics
func (c *minioResourceCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.desc
}
// Collect is called by the Prometheus registry when collecting metrics.
func (c *minioResourceCollector) Collect(out chan<- prometheus.Metric) {
var wg sync.WaitGroup
publish := func(in <-chan Metric) {
defer wg.Done()
for metric := range in {
labels, values := getOrderedLabelValueArrays(metric.VariableLabels)
collectMetric(metric, labels, values, "resource", out)
}
}
// Call peer api to fetch metrics
wg.Add(2)
go publish(ReportMetrics(GlobalContext, c.metricsGroups))
go publish(globalNotificationSys.GetResourceMetrics(GlobalContext))
wg.Wait()
}
// newMinioResourceCollector describes the collector
// and returns reference of minio resource Collector
// It creates the Prometheus Description which is used
// to define Metric and help string
func newMinioResourceCollector(metricsGroups []*MetricsGroup) *minioResourceCollector {
return &minioResourceCollector{
metricsGroups: metricsGroups,
desc: prometheus.NewDesc("minio_resource_stats", "Resource statistics exposed by MinIO server", nil, nil),
}
}
func prepareResourceMetrics(rm ResourceMetric, subSys MetricSubsystem, requireAvgMax bool) []Metric {
help := resourceMetricsHelpMap[rm.Name]
name := rm.Name
metrics := []Metric{}
metrics = append(metrics, Metric{
Description: getResourceMetricDescription(subSys, name, help),
Value: rm.Current,
VariableLabels: rm.Labels,
})
if requireAvgMax {
avgName := MetricName(fmt.Sprintf("%s_avg", name))
avgHelp := fmt.Sprintf("%s (avg)", help)
metrics = append(metrics, Metric{
Description: getResourceMetricDescription(subSys, avgName, avgHelp),
Value: math.Round(rm.Avg*100) / 100,
VariableLabels: rm.Labels,
})
maxName := MetricName(fmt.Sprintf("%s_max", name))
maxHelp := fmt.Sprintf("%s (max)", help)
metrics = append(metrics, Metric{
Description: getResourceMetricDescription(subSys, maxName, maxHelp),
Value: rm.Max,
VariableLabels: rm.Labels,
})
}
return metrics
}
func getResourceMetricDescription(subSys MetricSubsystem, name MetricName, help string) MetricDescription {
return MetricDescription{
Namespace: nodeMetricNamespace,
Subsystem: subSys,
Name: name,
Help: help,
Type: gaugeMetric,
}
}
func getResourceMetrics() *MetricsGroup {
mg := &MetricsGroup{
cacheInterval: resourceMetricsCacheInterval,
}
mg.RegisterRead(func(ctx context.Context) []Metric {
metrics := []Metric{}
subSystems := []MetricSubsystem{interfaceSubsystem, memSubsystem, driveSubsystem, cpuSubsystem}
for _, subSys := range subSystems {
stats, found := resourceMetricsMap[subSys]
if found {
requireAvgMax := true
if subSys == driveSubsystem {
requireAvgMax = false
}
for _, m := range stats {
metrics = append(metrics, prepareResourceMetrics(m, subSys, requireAvgMax)...)
}
}
}
return metrics
})
return mg
}
// metricsResourceHandler is the prometheus handler for resource metrics
func metricsResourceHandler() http.Handler {
return metricsHTTPHandler(resourceCollector, "handler.MetricsResource")
}
+6 -4
View File
@@ -25,10 +25,11 @@ import (
) )
const ( const (
prometheusMetricsPathLegacy = "/prometheus/metrics" prometheusMetricsPathLegacy = "/prometheus/metrics"
prometheusMetricsV2ClusterPath = "/v2/metrics/cluster" prometheusMetricsV2ClusterPath = "/v2/metrics/cluster"
prometheusMetricsV2BucketPath = "/v2/metrics/bucket" prometheusMetricsV2BucketPath = "/v2/metrics/bucket"
prometheusMetricsV2NodePath = "/v2/metrics/node" prometheusMetricsV2NodePath = "/v2/metrics/node"
prometheusMetricsV2ResourcePath = "/v2/metrics/resource"
) )
// Standard env prometheus auth type // Standard env prometheus auth type
@@ -57,4 +58,5 @@ func registerMetricsRouter(router *mux.Router) {
metricsRouter.Handle(prometheusMetricsV2ClusterPath, auth(metricsServerHandler())) metricsRouter.Handle(prometheusMetricsV2ClusterPath, auth(metricsServerHandler()))
metricsRouter.Handle(prometheusMetricsV2BucketPath, auth(metricsBucketHandler())) metricsRouter.Handle(prometheusMetricsV2BucketPath, auth(metricsBucketHandler()))
metricsRouter.Handle(prometheusMetricsV2NodePath, auth(metricsNodeHandler())) metricsRouter.Handle(prometheusMetricsV2NodePath, auth(metricsNodeHandler()))
metricsRouter.Handle(prometheusMetricsV2ResourcePath, auth(metricsResourceHandler()))
} }
+180 -206
View File
@@ -132,6 +132,9 @@ const (
capacityRawSubsystem MetricSubsystem = "capacity_raw" capacityRawSubsystem MetricSubsystem = "capacity_raw"
capacityUsableSubsystem MetricSubsystem = "capacity_usable" capacityUsableSubsystem MetricSubsystem = "capacity_usable"
driveSubsystem MetricSubsystem = "drive" driveSubsystem MetricSubsystem = "drive"
interfaceSubsystem MetricSubsystem = "if"
memSubsystem MetricSubsystem = "mem"
cpuSubsystem MetricSubsystem = "cpu_avg"
storageClassSubsystem MetricSubsystem = "storage_class" storageClassSubsystem MetricSubsystem = "storage_class"
fileDescriptorSubsystem MetricSubsystem = "file_descriptor" fileDescriptorSubsystem MetricSubsystem = "file_descriptor"
goRoutines MetricSubsystem = "go_routine" goRoutines MetricSubsystem = "go_routine"
@@ -539,12 +542,12 @@ func getNodeRRSParityMD() MetricDescription {
} }
} }
func getNodeDrivesFreeInodes() MetricDescription { func getNodeDrivesFreeInodesMD() MetricDescription {
return MetricDescription{ return MetricDescription{
Namespace: nodeMetricNamespace, Namespace: nodeMetricNamespace,
Subsystem: driveSubsystem, Subsystem: driveSubsystem,
Name: freeInodes, Name: freeInodes,
Help: "Total free inodes", Help: "Free inodes on a drive",
Type: gaugeMetric, Type: gaugeMetric,
} }
} }
@@ -2085,99 +2088,101 @@ func getReplicationClusterMetrics() *MetricsGroup {
) )
mg.RegisterRead(func(_ context.Context) []Metric { mg.RegisterRead(func(_ context.Context) []Metric {
var ml []Metric
// common operational metrics for bucket replication and site replication - published // common operational metrics for bucket replication and site replication - published
// at cluster level // at cluster level
qs := globalReplicationStats.getNodeQueueStatsSummary() if globalReplicationStats != nil {
activeWorkersCount := Metric{ qs := globalReplicationStats.getNodeQueueStatsSummary()
Description: getClusterReplActiveWorkersCountMD(), activeWorkersCount := Metric{
VariableLabels: map[string]string{serverName: qs.NodeName}, Description: getClusterReplActiveWorkersCountMD(),
} VariableLabels: map[string]string{serverName: qs.NodeName},
avgActiveWorkersCount := Metric{ }
Description: getClusterReplAvgActiveWorkersCountMD(), avgActiveWorkersCount := Metric{
VariableLabels: map[string]string{serverName: qs.NodeName}, Description: getClusterReplAvgActiveWorkersCountMD(),
} VariableLabels: map[string]string{serverName: qs.NodeName},
maxActiveWorkersCount := Metric{ }
Description: getClusterReplMaxActiveWorkersCountMD(), maxActiveWorkersCount := Metric{
VariableLabels: map[string]string{serverName: qs.NodeName}, Description: getClusterReplMaxActiveWorkersCountMD(),
} VariableLabels: map[string]string{serverName: qs.NodeName},
currInQueueCount := Metric{ }
Description: getClusterReplCurrQueuedOperationsMD(), currInQueueCount := Metric{
VariableLabels: map[string]string{serverName: qs.NodeName}, Description: getClusterReplCurrQueuedOperationsMD(),
} VariableLabels: map[string]string{serverName: qs.NodeName},
currInQueueBytes := Metric{ }
Description: getClusterReplCurrQueuedBytesMD(), currInQueueBytes := Metric{
VariableLabels: map[string]string{serverName: qs.NodeName}, Description: getClusterReplCurrQueuedBytesMD(),
} VariableLabels: map[string]string{serverName: qs.NodeName},
}
currTransferRate := Metric{ currTransferRate := Metric{
Description: getClusterReplCurrentTransferRateMD(), Description: getClusterReplCurrentTransferRateMD(),
VariableLabels: map[string]string{serverName: qs.NodeName}, VariableLabels: map[string]string{serverName: qs.NodeName},
} }
avgQueueCount := Metric{ avgQueueCount := Metric{
Description: getClusterReplAvgQueuedOperationsMD(), Description: getClusterReplAvgQueuedOperationsMD(),
VariableLabels: map[string]string{serverName: qs.NodeName}, VariableLabels: map[string]string{serverName: qs.NodeName},
} }
avgQueueBytes := Metric{ avgQueueBytes := Metric{
Description: getClusterReplAvgQueuedBytesMD(), Description: getClusterReplAvgQueuedBytesMD(),
VariableLabels: map[string]string{serverName: qs.NodeName}, VariableLabels: map[string]string{serverName: qs.NodeName},
} }
maxQueueCount := Metric{ maxQueueCount := Metric{
Description: getClusterReplMaxQueuedOperationsMD(), Description: getClusterReplMaxQueuedOperationsMD(),
VariableLabels: map[string]string{serverName: qs.NodeName}, VariableLabels: map[string]string{serverName: qs.NodeName},
} }
maxQueueBytes := Metric{ maxQueueBytes := Metric{
Description: getClusterReplMaxQueuedBytesMD(), Description: getClusterReplMaxQueuedBytesMD(),
VariableLabels: map[string]string{serverName: qs.NodeName}, VariableLabels: map[string]string{serverName: qs.NodeName},
} }
avgTransferRate := Metric{ avgTransferRate := Metric{
Description: getClusterReplAvgTransferRateMD(), Description: getClusterReplAvgTransferRateMD(),
VariableLabels: map[string]string{serverName: qs.NodeName}, VariableLabels: map[string]string{serverName: qs.NodeName},
} }
maxTransferRate := Metric{ maxTransferRate := Metric{
Description: getClusterReplMaxTransferRateMD(), Description: getClusterReplMaxTransferRateMD(),
VariableLabels: map[string]string{serverName: qs.NodeName}, VariableLabels: map[string]string{serverName: qs.NodeName},
} }
mrfCount := Metric{ mrfCount := Metric{
Description: getClusterReplMRFFailedOperationsMD(), Description: getClusterReplMRFFailedOperationsMD(),
VariableLabels: map[string]string{serverName: qs.NodeName}, VariableLabels: map[string]string{serverName: qs.NodeName},
Value: float64(qs.MRFStats.LastFailedCount), Value: float64(qs.MRFStats.LastFailedCount),
} }
if qs.QStats.Avg.Count > 0 || qs.QStats.Curr.Count > 0 { if qs.QStats.Avg.Count > 0 || qs.QStats.Curr.Count > 0 {
qt := qs.QStats qt := qs.QStats
currInQueueBytes.Value = qt.Curr.Bytes currInQueueBytes.Value = qt.Curr.Bytes
currInQueueCount.Value = qt.Curr.Count currInQueueCount.Value = qt.Curr.Count
avgQueueBytes.Value = qt.Avg.Bytes avgQueueBytes.Value = qt.Avg.Bytes
avgQueueCount.Value = qt.Avg.Count avgQueueCount.Value = qt.Avg.Count
maxQueueBytes.Value = qt.Max.Bytes maxQueueBytes.Value = qt.Max.Bytes
maxQueueCount.Value = qt.Max.Count maxQueueCount.Value = qt.Max.Count
} }
activeWorkersCount.Value = float64(qs.ActiveWorkers.Curr) activeWorkersCount.Value = float64(qs.ActiveWorkers.Curr)
avgActiveWorkersCount.Value = float64(qs.ActiveWorkers.Avg) avgActiveWorkersCount.Value = float64(qs.ActiveWorkers.Avg)
maxActiveWorkersCount.Value = float64(qs.ActiveWorkers.Max) maxActiveWorkersCount.Value = float64(qs.ActiveWorkers.Max)
if len(qs.XferStats) > 0 { if len(qs.XferStats) > 0 {
tots := qs.XferStats[Total] tots := qs.XferStats[Total]
currTransferRate.Value = tots.Curr currTransferRate.Value = tots.Curr
avgTransferRate.Value = tots.Avg avgTransferRate.Value = tots.Avg
maxTransferRate.Value = tots.Peak maxTransferRate.Value = tots.Peak
}
ml = []Metric{
activeWorkersCount,
avgActiveWorkersCount,
maxActiveWorkersCount,
currInQueueCount,
currInQueueBytes,
avgQueueCount,
avgQueueBytes,
maxQueueCount,
maxQueueBytes,
currTransferRate,
avgTransferRate,
maxTransferRate,
mrfCount,
}
} }
ml := []Metric{
activeWorkersCount,
avgActiveWorkersCount,
maxActiveWorkersCount,
currInQueueCount,
currInQueueBytes,
avgQueueCount,
avgQueueBytes,
maxQueueCount,
maxQueueBytes,
currTransferRate,
avgTransferRate,
maxTransferRate,
mrfCount,
}
for ep, health := range globalBucketTargetSys.healthStats() { for ep, health := range globalBucketTargetSys.healthStats() {
// link latency current // link latency current
m := Metric{ m := Metric{
@@ -3019,17 +3024,21 @@ func getBucketUsageMetrics() *MetricsGroup {
}) })
} }
if !globalSiteReplicationSys.isEnabled() { if !globalSiteReplicationSys.isEnabled() {
stats := bucketReplStats[bucket].ReplicationStats var stats BucketReplicationStats
metrics = append(metrics, Metric{ s, ok := bucketReplStats[bucket]
Description: getRepReceivedBytesMD(bucketMetricNamespace), if ok {
Value: float64(stats.ReplicaSize), stats = s.ReplicationStats
VariableLabels: map[string]string{"bucket": bucket}, metrics = append(metrics, Metric{
}) Description: getRepReceivedBytesMD(bucketMetricNamespace),
metrics = append(metrics, Metric{ Value: float64(stats.ReplicaSize),
Description: getRepReceivedOperationsMD(bucketMetricNamespace), VariableLabels: map[string]string{"bucket": bucket},
Value: float64(stats.ReplicaCount), })
VariableLabels: map[string]string{"bucket": bucket}, metrics = append(metrics, Metric{
}) Description: getRepReceivedOperationsMD(bucketMetricNamespace),
Value: float64(stats.ReplicaCount),
VariableLabels: map[string]string{"bucket": bucket},
})
}
if stats.hasReplicationUsage() { if stats.hasReplicationUsage() {
for arn, stat := range stats.Stats { for arn, stat := range stats.Stats {
metrics = append(metrics, Metric{ metrics = append(metrics, Metric{
@@ -3201,7 +3210,7 @@ func getLocalStorageMetrics() *MetricsGroup {
}) })
metrics = append(metrics, Metric{ metrics = append(metrics, Metric{
Description: getNodeDrivesFreeInodes(), Description: getNodeDrivesFreeInodesMD(),
Value: float64(disk.FreeInodes), Value: float64(disk.FreeInodes),
VariableLabels: map[string]string{"drive": disk.DrivePath}, VariableLabels: map[string]string{"drive": disk.DrivePath},
}) })
@@ -3540,6 +3549,61 @@ func getKMSMetrics() *MetricsGroup {
return mg return mg
} }
func collectMetric(metric Metric, labels []string, values []string, metricName string, out chan<- prometheus.Metric) {
if metric.Description.Type == histogramMetric {
if metric.Histogram == nil {
return
}
for k, v := range metric.Histogram {
pmetric, err := prometheus.NewConstMetric(
prometheus.NewDesc(
prometheus.BuildFQName(string(metric.Description.Namespace),
string(metric.Description.Subsystem),
string(metric.Description.Name)),
metric.Description.Help,
append(labels, metric.HistogramBucketLabel),
metric.StaticLabels,
),
prometheus.GaugeValue,
float64(v),
append(values, k)...)
if err != nil {
// Enable for debugging
if serverDebugLog {
logger.LogOnceIf(GlobalContext, fmt.Errorf("unable to validate prometheus metric (%w) %v+%v", err, values, metric.Histogram), metricName+"-metrics-histogram")
}
} else {
out <- pmetric
}
}
return
}
metricType := prometheus.GaugeValue
if metric.Description.Type == counterMetric {
metricType = prometheus.CounterValue
}
pmetric, err := prometheus.NewConstMetric(
prometheus.NewDesc(
prometheus.BuildFQName(string(metric.Description.Namespace),
string(metric.Description.Subsystem),
string(metric.Description.Name)),
metric.Description.Help,
labels,
metric.StaticLabels,
),
metricType,
metric.Value,
values...)
if err != nil {
// Enable for debugging
if serverDebugLog {
logger.LogOnceIf(GlobalContext, fmt.Errorf("unable to validate prometheus metric (%w) %v", err, values), metricName+"-metrics")
}
} else {
out <- pmetric
}
}
type minioBucketCollector struct { type minioBucketCollector struct {
metricsGroups []*MetricsGroup metricsGroups []*MetricsGroup
desc *prometheus.Desc desc *prometheus.Desc
@@ -3564,52 +3628,7 @@ func (c *minioBucketCollector) Collect(out chan<- prometheus.Metric) {
defer wg.Done() defer wg.Done()
for metric := range in { for metric := range in {
labels, values := getOrderedLabelValueArrays(metric.VariableLabels) labels, values := getOrderedLabelValueArrays(metric.VariableLabels)
if metric.Description.Type == histogramMetric { collectMetric(metric, labels, values, "bucket", out)
if metric.Histogram == nil {
continue
}
for k, v := range metric.Histogram {
pmetric, err := prometheus.NewConstMetric(
prometheus.NewDesc(
prometheus.BuildFQName(string(metric.Description.Namespace),
string(metric.Description.Subsystem),
string(metric.Description.Name)),
metric.Description.Help,
append(labels, metric.HistogramBucketLabel),
metric.StaticLabels,
),
prometheus.GaugeValue,
float64(v),
append(values, k)...)
if err != nil {
logger.LogOnceIf(GlobalContext, fmt.Errorf("unable to validate prometheus metric (%w) %v+%v", err, values, metric.Histogram), "bucket-metrics-histogram")
} else {
out <- pmetric
}
}
continue
}
metricType := prometheus.GaugeValue
if metric.Description.Type == counterMetric {
metricType = prometheus.CounterValue
}
pmetric, err := prometheus.NewConstMetric(
prometheus.NewDesc(
prometheus.BuildFQName(string(metric.Description.Namespace),
string(metric.Description.Subsystem),
string(metric.Description.Name)),
metric.Description.Help,
labels,
metric.StaticLabels,
),
metricType,
metric.Value,
values...)
if err != nil {
logger.LogOnceIf(GlobalContext, fmt.Errorf("unable to validate prometheus metric (%w) %v", err, values), "bucket-metrics")
} else {
out <- pmetric
}
} }
} }
@@ -3644,52 +3663,7 @@ func (c *minioClusterCollector) Collect(out chan<- prometheus.Metric) {
defer wg.Done() defer wg.Done()
for metric := range in { for metric := range in {
labels, values := getOrderedLabelValueArrays(metric.VariableLabels) labels, values := getOrderedLabelValueArrays(metric.VariableLabels)
if metric.Description.Type == histogramMetric { collectMetric(metric, labels, values, "cluster", out)
if metric.Histogram == nil {
continue
}
for k, v := range metric.Histogram {
pmetric, err := prometheus.NewConstMetric(
prometheus.NewDesc(
prometheus.BuildFQName(string(metric.Description.Namespace),
string(metric.Description.Subsystem),
string(metric.Description.Name)),
metric.Description.Help,
append(labels, metric.HistogramBucketLabel),
metric.StaticLabels,
),
prometheus.GaugeValue,
float64(v),
append(values, k)...)
if err != nil {
logger.LogOnceIf(GlobalContext, fmt.Errorf("unable to validate prometheus metric (%w) %v:%v", err, values, metric.Histogram), "cluster-metrics-histogram")
} else {
out <- pmetric
}
}
continue
}
metricType := prometheus.GaugeValue
if metric.Description.Type == counterMetric {
metricType = prometheus.CounterValue
}
pmetric, err := prometheus.NewConstMetric(
prometheus.NewDesc(
prometheus.BuildFQName(string(metric.Description.Namespace),
string(metric.Description.Subsystem),
string(metric.Description.Name)),
metric.Description.Help,
labels,
metric.StaticLabels,
),
metricType,
metric.Value,
values...)
if err != nil {
logger.LogOnceIf(GlobalContext, fmt.Errorf("unable to validate prometheus metric (%w) %v", err, values), "cluster-metrics")
} else {
out <- pmetric
}
} }
} }
@@ -3822,11 +3796,11 @@ func newMinioCollectorNode(metricsGroups []*MetricsGroup) *minioNodeCollector {
} }
} }
func metricsBucketHandler() http.Handler { func metricsHTTPHandler(c prometheus.Collector, funcName string) http.Handler {
registry := prometheus.NewRegistry() registry := prometheus.NewRegistry()
// Report all other metrics // Report all other metrics
logger.CriticalIf(GlobalContext, registry.Register(bucketCollector)) logger.CriticalIf(GlobalContext, registry.Register(c))
// DefaultGatherers include golang metrics and process metrics. // DefaultGatherers include golang metrics and process metrics.
gatherers := prometheus.Gatherers{ gatherers := prometheus.Gatherers{
@@ -3836,16 +3810,14 @@ func metricsBucketHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt) tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
if ok { if ok {
tc.FuncName = "handler.MetricsBucket" tc.FuncName = funcName
tc.ResponseRecorder.LogErrBody = true tc.ResponseRecorder.LogErrBody = true
} }
mfs, err := gatherers.Gather() mfs, err := gatherers.Gather()
if err != nil { if err != nil && len(mfs) == 0 {
if len(mfs) == 0 { writeErrorResponseJSON(r.Context(), w, toAdminAPIErr(r.Context(), err), r.URL)
writeErrorResponseJSON(r.Context(), w, toAdminAPIErr(r.Context(), err), r.URL) return
return
}
} }
contentType := expfmt.Negotiate(r.Header) contentType := expfmt.Negotiate(r.Header)
@@ -3865,6 +3837,10 @@ func metricsBucketHandler() http.Handler {
}) })
} }
func metricsBucketHandler() http.Handler {
return metricsHTTPHandler(bucketCollector, "handler.MetricsBucket")
}
func metricsServerHandler() http.Handler { func metricsServerHandler() http.Handler {
registry := prometheus.NewRegistry() registry := prometheus.NewRegistry()
@@ -3884,11 +3860,9 @@ func metricsServerHandler() http.Handler {
} }
mfs, err := gatherers.Gather() mfs, err := gatherers.Gather()
if err != nil { if err != nil && len(mfs) == 0 {
if len(mfs) == 0 { writeErrorResponseJSON(r.Context(), w, toAdminAPIErr(r.Context(), err), r.URL)
writeErrorResponseJSON(r.Context(), w, toAdminAPIErr(r.Context(), err), r.URL) return
return
}
} }
contentType := expfmt.Negotiate(r.Header) contentType := expfmt.Negotiate(r.Header)
+4 -4
View File
@@ -25,7 +25,7 @@ import (
"github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/minio/minio/internal/mcontext" "github.com/minio/minio/internal/mcontext"
iampolicy "github.com/minio/pkg/v2/policy" "github.com/minio/pkg/v2/policy"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/expfmt" "github.com/prometheus/common/expfmt"
) )
@@ -616,7 +616,7 @@ func AuthMiddleware(h http.Handler) http.Handler {
tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt) tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
claims, groups, owner, authErr := metricsRequestAuthenticate(r) claims, groups, owner, authErr := metricsRequestAuthenticate(r)
if authErr != nil || !claims.VerifyIssuer("prometheus", true) { if authErr != nil || (claims != nil && !claims.VerifyIssuer("prometheus", true)) {
if ok { if ok {
tc.FuncName = "handler.MetricsAuth" tc.FuncName = "handler.MetricsAuth"
tc.ResponseRecorder.LogErrBody = true tc.ResponseRecorder.LogErrBody = true
@@ -633,10 +633,10 @@ func AuthMiddleware(h http.Handler) http.Handler {
} }
// For authenticated users apply IAM policy. // For authenticated users apply IAM policy.
if !globalIAMSys.IsAllowed(iampolicy.Args{ if !globalIAMSys.IsAllowed(policy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
Groups: cred.Groups, Groups: cred.Groups,
Action: iampolicy.PrometheusAdminAction, Action: policy.PrometheusAdminAction,
ConditionValues: getConditionValues(r, "", cred), ConditionValues: getConditionValues(r, "", cred),
IsOwner: owner, IsOwner: owner,
Claims: cred.Claims, Claims: cred.Claims,
+76 -101
View File
@@ -297,7 +297,7 @@ func (sys *NotificationSys) DownloadProfilingData(ctx context.Context, writer io
profilingDataFound = true profilingDataFound = true
for typ, data := range data { for typ, data := range data {
err := embedFileInZip(zipWriter, fmt.Sprintf("profile-%s-%s", client.host.String(), typ), data) err := embedFileInZip(zipWriter, fmt.Sprintf("profile-%s-%s", client.host.String(), typ), data, 0o600)
if err != nil { if err != nil {
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", client.host.String()) reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", client.host.String())
ctx := logger.SetReqInfo(ctx, reqInfo) ctx := logger.SetReqInfo(ctx, reqInfo)
@@ -325,11 +325,11 @@ func (sys *NotificationSys) DownloadProfilingData(ctx context.Context, writer io
// Send profiling data to zip as file // Send profiling data to zip as file
for typ, data := range data { for typ, data := range data {
err := embedFileInZip(zipWriter, fmt.Sprintf("profile-%s-%s", thisAddr, typ), data) err := embedFileInZip(zipWriter, fmt.Sprintf("profile-%s-%s", thisAddr, typ), data, 0o600)
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
} }
if b := getClusterMetaInfo(ctx); len(b) > 0 { if b := getClusterMetaInfo(ctx); len(b) > 0 {
logger.LogIf(ctx, embedFileInZip(zipWriter, "cluster.info", b)) logger.LogIf(ctx, embedFileInZip(zipWriter, "cluster.info", b, 0o600))
} }
return return
@@ -783,6 +783,27 @@ func (sys *NotificationSys) GetMetrics(ctx context.Context, t madmin.MetricType,
return reply return reply
} }
// GetResourceMetrics - gets the resource metrics from all nodes excluding self.
func (sys *NotificationSys) GetResourceMetrics(ctx context.Context) <-chan Metric {
if sys == nil {
return nil
}
g := errgroup.WithNErrs(len(sys.peerClients))
peerChannels := make([]<-chan Metric, len(sys.peerClients))
for index := range sys.peerClients {
index := index
g.Go(func() error {
if sys.peerClients[index] == nil {
return errPeerNotReachable
}
var err error
peerChannels[index], err = sys.peerClients[index].GetResourceMetrics(ctx)
return err
}, index)
}
return sys.collectPeerMetrics(ctx, peerChannels, g)
}
// GetSysConfig - Get information about system config // GetSysConfig - Get information about system config
// (only the config that are of concern to minio) // (only the config that are of concern to minio)
func (sys *NotificationSys) GetSysConfig(ctx context.Context) []madmin.SysConfig { func (sys *NotificationSys) GetSysConfig(ctx context.Context) []madmin.SysConfig {
@@ -1099,40 +1120,70 @@ func (sys *NotificationSys) GetBandwidthReports(ctx context.Context, buckets ...
} }
reports = append(reports, globalBucketMonitor.GetReport(bandwidth.SelectBuckets(buckets...))) reports = append(reports, globalBucketMonitor.GetReport(bandwidth.SelectBuckets(buckets...)))
consolidatedReport := bandwidth.BucketBandwidthReport{ consolidatedReport := bandwidth.BucketBandwidthReport{
BucketStats: make(map[string]map[string]bandwidth.Details), BucketStats: make(map[bandwidth.BucketOptions]bandwidth.Details),
} }
for _, report := range reports { for _, report := range reports {
if report == nil || report.BucketStats == nil { if report == nil || report.BucketStats == nil {
continue continue
} }
for bucket := range report.BucketStats { for opts := range report.BucketStats {
d, ok := consolidatedReport.BucketStats[bucket] d, ok := consolidatedReport.BucketStats[opts]
if !ok { if !ok {
consolidatedReport.BucketStats[bucket] = make(map[string]bandwidth.Details) d = bandwidth.Details{
d = consolidatedReport.BucketStats[bucket] LimitInBytesPerSecond: report.BucketStats[opts].LimitInBytesPerSecond,
for arn := range d {
d[arn] = bandwidth.Details{
LimitInBytesPerSecond: report.BucketStats[bucket][arn].LimitInBytesPerSecond,
}
} }
} }
for arn, st := range report.BucketStats[bucket] { dt, ok := report.BucketStats[opts]
bwDet := bandwidth.Details{} if ok {
if bw, ok := d[arn]; ok { d.CurrentBandwidthInBytesPerSecond += dt.CurrentBandwidthInBytesPerSecond
bwDet = bw
}
if bwDet.LimitInBytesPerSecond < st.LimitInBytesPerSecond {
bwDet.LimitInBytesPerSecond = st.LimitInBytesPerSecond
}
bwDet.CurrentBandwidthInBytesPerSecond += st.CurrentBandwidthInBytesPerSecond
d[arn] = bwDet
consolidatedReport.BucketStats[bucket] = d
} }
consolidatedReport.BucketStats[opts] = d
} }
} }
return consolidatedReport return consolidatedReport
} }
func (sys *NotificationSys) collectPeerMetrics(ctx context.Context, peerChannels []<-chan Metric, g *errgroup.Group) <-chan Metric {
ch := make(chan Metric)
var wg sync.WaitGroup
for index, err := range g.Wait() {
if err != nil {
if sys.peerClients[index] != nil {
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress",
sys.peerClients[index].host.String())
logger.LogOnceIf(logger.SetReqInfo(ctx, reqInfo), err, sys.peerClients[index].host.String())
} else {
logger.LogOnceIf(ctx, err, "peer-offline")
}
continue
}
wg.Add(1)
go func(ctx context.Context, peerChannel <-chan Metric, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case m, ok := <-peerChannel:
if !ok {
return
}
select {
case ch <- m:
case <-ctx.Done():
return
}
case <-ctx.Done():
return
}
}
}(ctx, peerChannels[index], &wg)
}
go func(wg *sync.WaitGroup, ch chan Metric) {
wg.Wait()
close(ch)
}(&wg, ch)
return ch
}
// GetBucketMetrics - gets the cluster level bucket metrics from all nodes excluding self. // GetBucketMetrics - gets the cluster level bucket metrics from all nodes excluding self.
func (sys *NotificationSys) GetBucketMetrics(ctx context.Context) <-chan Metric { func (sys *NotificationSys) GetBucketMetrics(ctx context.Context) <-chan Metric {
if sys == nil { if sys == nil {
@@ -1151,45 +1202,7 @@ func (sys *NotificationSys) GetBucketMetrics(ctx context.Context) <-chan Metric
return err return err
}, index) }, index)
} }
return sys.collectPeerMetrics(ctx, peerChannels, g)
ch := make(chan Metric)
var wg sync.WaitGroup
for index, err := range g.Wait() {
if err != nil {
if sys.peerClients[index] != nil {
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress",
sys.peerClients[index].host.String())
logger.LogOnceIf(logger.SetReqInfo(ctx, reqInfo), err, sys.peerClients[index].host.String())
} else {
logger.LogOnceIf(ctx, err, "peer-offline")
}
continue
}
wg.Add(1)
go func(ctx context.Context, peerChannel <-chan Metric, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case m, ok := <-peerChannel:
if !ok {
return
}
select {
case ch <- m:
case <-ctx.Done():
return
}
case <-ctx.Done():
return
}
}
}(ctx, peerChannels[index], &wg)
}
go func(wg *sync.WaitGroup, ch chan Metric) {
wg.Wait()
close(ch)
}(&wg, ch)
return ch
} }
// GetClusterMetrics - gets the cluster metrics from all nodes excluding self. // GetClusterMetrics - gets the cluster metrics from all nodes excluding self.
@@ -1210,45 +1223,7 @@ func (sys *NotificationSys) GetClusterMetrics(ctx context.Context) <-chan Metric
return err return err
}, index) }, index)
} }
return sys.collectPeerMetrics(ctx, peerChannels, g)
ch := make(chan Metric)
var wg sync.WaitGroup
for index, err := range g.Wait() {
if err != nil {
if sys.peerClients[index] != nil {
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress",
sys.peerClients[index].host.String())
logger.LogOnceIf(logger.SetReqInfo(ctx, reqInfo), err, sys.peerClients[index].host.String())
} else {
logger.LogOnceIf(ctx, err, "peer-offline")
}
continue
}
wg.Add(1)
go func(ctx context.Context, peerChannel <-chan Metric, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case m, ok := <-peerChannel:
if !ok {
return
}
select {
case ch <- m:
case <-ctx.Done():
return
}
case <-ctx.Done():
return
}
}
}(ctx, peerChannels[index], &wg)
}
go func(wg *sync.WaitGroup, ch chan Metric) {
wg.Wait()
close(ch)
}(&wg, ch)
return ch
} }
// ServiceFreeze freezes all S3 API calls when 'freeze' is true, // ServiceFreeze freezes all S3 API calls when 'freeze' is true,
+41 -2
View File
@@ -109,6 +109,9 @@ type ObjectInfo struct {
// Total object size. // Total object size.
Size int64 Size int64
// Actual size is the real size of the object uploaded by client.
ActualSize *int64
// IsDir indicates if the object is prefix. // IsDir indicates if the object is prefix.
IsDir bool IsDir bool
@@ -282,9 +285,45 @@ func (o ObjectInfo) tierStats() tierStats {
return ts return ts
} }
// ToObjectInfo converts a replication object info to a partial ObjectInfo
// do not rely on this function to give you correct ObjectInfo, this
// function is merely and optimization.
func (ri ReplicateObjectInfo) ToObjectInfo() ObjectInfo {
return ObjectInfo{
Name: ri.Name,
Bucket: ri.Bucket,
VersionID: ri.VersionID,
ModTime: ri.ModTime,
UserTags: ri.UserTags,
Size: ri.Size,
ActualSize: &ri.ActualSize,
ReplicationStatus: ri.ReplicationStatus,
ReplicationStatusInternal: ri.ReplicationStatusInternal,
VersionPurgeStatus: ri.VersionPurgeStatus,
VersionPurgeStatusInternal: ri.VersionPurgeStatusInternal,
DeleteMarker: true,
UserDefined: map[string]string{},
}
}
// ReplicateObjectInfo represents object info to be replicated // ReplicateObjectInfo represents object info to be replicated
type ReplicateObjectInfo struct { type ReplicateObjectInfo struct {
ObjectInfo Name string
Bucket string
VersionID string
ETag string
Size int64
ActualSize int64
ModTime time.Time
UserTags string
SSEC bool
ReplicationStatus replication.StatusType
ReplicationStatusInternal string
VersionPurgeStatusInternal string
VersionPurgeStatus VersionPurgeStatusType
ReplicationState ReplicationState
DeleteMarker bool
OpType replication.Type OpType replication.Type
EventType string EventType string
RetryCount uint32 RetryCount uint32
@@ -529,7 +568,7 @@ type PartInfo struct {
// Size in bytes of the part. // Size in bytes of the part.
Size int64 Size int64
// Decompressed Size. // Real size of the object uploaded by client.
ActualSize int64 ActualSize int64
// Checksum values // Checksum values
-9
View File
@@ -348,15 +348,6 @@ func (e InvalidUploadIDKeyCombination) Error() string {
return fmt.Sprintf("Invalid combination of uploadID marker '%s' and marker '%s'", e.UploadIDMarker, e.KeyMarker) return fmt.Sprintf("Invalid combination of uploadID marker '%s' and marker '%s'", e.UploadIDMarker, e.KeyMarker)
} }
// InvalidMarkerPrefixCombination - invalid marker and prefix combination.
type InvalidMarkerPrefixCombination struct {
Marker, Prefix string
}
func (e InvalidMarkerPrefixCombination) Error() string {
return fmt.Sprintf("Invalid combination of marker '%s' and prefix '%s'", e.Marker, e.Prefix)
}
// BucketPolicyNotFound - no bucket policy found. // BucketPolicyNotFound - no bucket policy found.
type BucketPolicyNotFound GenericError type BucketPolicyNotFound GenericError
-11
View File
@@ -78,17 +78,6 @@ func checkListObjsArgs(ctx context.Context, bucket, prefix, marker string, obj g
Object: prefix, Object: prefix,
} }
} }
// Verify if marker has prefix.
if marker != "" && !HasPrefix(marker, prefix) {
logger.LogIf(ctx, InvalidMarkerPrefixCombination{
Marker: marker,
Prefix: prefix,
})
return InvalidMarkerPrefixCombination{
Marker: marker,
Prefix: prefix,
}
}
return nil return nil
} }
+1
View File
@@ -146,6 +146,7 @@ type DeleteBucketOptions struct {
// BucketOptions provides options for ListBuckets and GetBucketInfo call. // BucketOptions provides options for ListBuckets and GetBucketInfo call.
type BucketOptions struct { type BucketOptions struct {
Deleted bool // true only when site replication is enabled Deleted bool // true only when site replication is enabled
Cached bool // true only when we are requesting a cached response instead of hitting the disk for example ListBuckets() call.
} }
// SetReplicaStatus sets replica status and timestamp for delete operations in ObjectOptions // SetReplicaStatus sets replica status and timestamp for delete operations in ObjectOptions
+48 -47
View File
@@ -393,7 +393,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
resultCases := []ListObjectsInfo{ resultCases := []ListObjectsInfo{
// ListObjectsResult-0. // ListObjectsResult-0.
// Testing for listing all objects in the bucket, (testCase 20,21,22). // Testing for listing all objects in the bucket, (testCase 20,21,22).
{ 0: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "Asia-maps.png"}, {Name: "Asia-maps.png"},
@@ -409,7 +409,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
}, },
// ListObjectsResult-1. // ListObjectsResult-1.
// Used for asserting the truncated case, (testCase 23). // Used for asserting the truncated case, (testCase 23).
{ 1: {
IsTruncated: true, IsTruncated: true,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "Asia-maps.png"}, {Name: "Asia-maps.png"},
@@ -421,7 +421,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
}, },
// ListObjectsResult-2. // ListObjectsResult-2.
// (TestCase 24). // (TestCase 24).
{ 2: {
IsTruncated: true, IsTruncated: true,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "Asia-maps.png"}, {Name: "Asia-maps.png"},
@@ -432,7 +432,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
}, },
// ListObjectsResult-3. // ListObjectsResult-3.
// (TestCase 25). // (TestCase 25).
{ 3: {
IsTruncated: true, IsTruncated: true,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "Asia-maps.png"}, {Name: "Asia-maps.png"},
@@ -443,7 +443,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
// ListObjectsResult-4. // ListObjectsResult-4.
// Again used for truncated case. // Again used for truncated case.
// (TestCase 26). // (TestCase 26).
{ 4: {
IsTruncated: true, IsTruncated: true,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "Asia-maps.png"}, {Name: "Asia-maps.png"},
@@ -452,7 +452,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
// ListObjectsResult-5. // ListObjectsResult-5.
// Used for Asserting prefixes. // Used for Asserting prefixes.
// Used for test case with prefix "new", (testCase 27-29). // Used for test case with prefix "new", (testCase 27-29).
{ 5: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "newPrefix0"}, {Name: "newPrefix0"},
@@ -463,7 +463,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
// ListObjectsResult-6. // ListObjectsResult-6.
// Used for Asserting prefixes. // Used for Asserting prefixes.
// Used for test case with prefix = "obj", (testCase 30). // Used for test case with prefix = "obj", (testCase 30).
{ 6: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "obj0"}, {Name: "obj0"},
@@ -474,7 +474,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
// ListObjectsResult-7. // ListObjectsResult-7.
// Used for Asserting prefixes and truncation. // Used for Asserting prefixes and truncation.
// Used for test case with prefix = "new" and maxKeys = 1, (testCase 31). // Used for test case with prefix = "new" and maxKeys = 1, (testCase 31).
{ 7: {
IsTruncated: true, IsTruncated: true,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "newPrefix0"}, {Name: "newPrefix0"},
@@ -483,7 +483,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
// ListObjectsResult-8. // ListObjectsResult-8.
// Used for Asserting prefixes. // Used for Asserting prefixes.
// Used for test case with prefix = "obj" and maxKeys = 2, (testCase 32). // Used for test case with prefix = "obj" and maxKeys = 2, (testCase 32).
{ 8: {
IsTruncated: true, IsTruncated: true,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "obj0"}, {Name: "obj0"},
@@ -493,7 +493,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
// ListObjectsResult-9. // ListObjectsResult-9.
// Used for asserting the case with marker, but without prefix. // Used for asserting the case with marker, but without prefix.
// marker is set to "newPrefix0" in the testCase, (testCase 33). // marker is set to "newPrefix0" in the testCase, (testCase 33).
{ 9: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "newPrefix1"}, {Name: "newPrefix1"},
@@ -505,7 +505,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
}, },
// ListObjectsResult-10. // ListObjectsResult-10.
// marker is set to "newPrefix1" in the testCase, (testCase 34). // marker is set to "newPrefix1" in the testCase, (testCase 34).
{ 10: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "newzen/zen/recurse/again/again/again/pics"}, {Name: "newzen/zen/recurse/again/again/again/pics"},
@@ -516,7 +516,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
}, },
// ListObjectsResult-11. // ListObjectsResult-11.
// marker is set to "obj0" in the testCase, (testCase 35). // marker is set to "obj0" in the testCase, (testCase 35).
{ 11: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "obj1"}, {Name: "obj1"},
@@ -525,7 +525,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
}, },
// ListObjectsResult-12. // ListObjectsResult-12.
// Marker is set to "obj1" in the testCase, (testCase 36). // Marker is set to "obj1" in the testCase, (testCase 36).
{ 12: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "obj2"}, {Name: "obj2"},
@@ -533,7 +533,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
}, },
// ListObjectsResult-13. // ListObjectsResult-13.
// Marker is set to "man" in the testCase, (testCase37). // Marker is set to "man" in the testCase, (testCase37).
{ 13: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "newPrefix0"}, {Name: "newPrefix0"},
@@ -546,7 +546,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
}, },
// ListObjectsResult-14. // ListObjectsResult-14.
// Marker is set to "Abc" in the testCase, (testCase 39). // Marker is set to "Abc" in the testCase, (testCase 39).
{ 14: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "Asia-maps.png"}, {Name: "Asia-maps.png"},
@@ -562,7 +562,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
}, },
// ListObjectsResult-15. // ListObjectsResult-15.
// Marker is set to "Asia/India/India-summer-photos-1" in the testCase, (testCase 40). // Marker is set to "Asia/India/India-summer-photos-1" in the testCase, (testCase 40).
{ 15: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"}, {Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
@@ -576,7 +576,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
}, },
// ListObjectsResult-16. // ListObjectsResult-16.
// Marker is set to "Asia/India/Karnataka/Bangalore/Koramangala/pics" in the testCase, (testCase 41). // Marker is set to "Asia/India/Karnataka/Bangalore/Koramangala/pics" in the testCase, (testCase 41).
{ 16: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "newPrefix0"}, {Name: "newPrefix0"},
@@ -591,7 +591,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
// Used for asserting the case with marker, without prefix but with truncation. // Used for asserting the case with marker, without prefix but with truncation.
// Marker = "newPrefix0" & maxKeys = 3 in the testCase, (testCase42). // Marker = "newPrefix0" & maxKeys = 3 in the testCase, (testCase42).
// Output truncated to 3 values. // Output truncated to 3 values.
{ 17: {
IsTruncated: true, IsTruncated: true,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "newPrefix1"}, {Name: "newPrefix1"},
@@ -602,7 +602,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
// ListObjectsResult-18. // ListObjectsResult-18.
// Marker = "newPrefix1" & maxkeys = 1 in the testCase, (testCase43). // Marker = "newPrefix1" & maxkeys = 1 in the testCase, (testCase43).
// Output truncated to 1 value. // Output truncated to 1 value.
{ 18: {
IsTruncated: true, IsTruncated: true,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "newzen/zen/recurse/again/again/again/pics"}, {Name: "newzen/zen/recurse/again/again/again/pics"},
@@ -611,7 +611,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
// ListObjectsResult-19. // ListObjectsResult-19.
// Marker = "obj0" & maxKeys = 1 in the testCase, (testCase44). // Marker = "obj0" & maxKeys = 1 in the testCase, (testCase44).
// Output truncated to 1 value. // Output truncated to 1 value.
{ 19: {
IsTruncated: true, IsTruncated: true,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "obj1"}, {Name: "obj1"},
@@ -619,7 +619,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
}, },
// ListObjectsResult-20. // ListObjectsResult-20.
// Marker = "obj0" & prefix = "obj" in the testCase, (testCase 45). // Marker = "obj0" & prefix = "obj" in the testCase, (testCase 45).
{ 20: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "obj1"}, {Name: "obj1"},
@@ -628,7 +628,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
}, },
// ListObjectsResult-21. // ListObjectsResult-21.
// Marker = "obj1" & prefix = "obj" in the testCase, (testCase 46). // Marker = "obj1" & prefix = "obj" in the testCase, (testCase 46).
{ 21: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "obj2"}, {Name: "obj2"},
@@ -636,7 +636,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
}, },
// ListObjectsResult-22. // ListObjectsResult-22.
// Marker = "newPrefix0" & prefix = "new" in the testCase,, (testCase 47). // Marker = "newPrefix0" & prefix = "new" in the testCase,, (testCase 47).
{ 22: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "newPrefix1"}, {Name: "newPrefix1"},
@@ -645,7 +645,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
}, },
// ListObjectsResult-23. // ListObjectsResult-23.
// Prefix is set to "Asia/India/" in the testCase, and delimiter is not set (testCase 55). // Prefix is set to "Asia/India/" in the testCase, and delimiter is not set (testCase 55).
{ 23: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "Asia/India/India-summer-photos-1"}, {Name: "Asia/India/India-summer-photos-1"},
@@ -655,7 +655,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
// ListObjectsResult-24. // ListObjectsResult-24.
// Prefix is set to "Asia" in the testCase, and delimiter is not set (testCase 56). // Prefix is set to "Asia" in the testCase, and delimiter is not set (testCase 56).
{ 24: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "Asia-maps.png"}, {Name: "Asia-maps.png"},
@@ -666,7 +666,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
// ListObjectsResult-25. // ListObjectsResult-25.
// Prefix is set to "Asia" in the testCase, and delimiter is set (testCase 57). // Prefix is set to "Asia" in the testCase, and delimiter is set (testCase 57).
{ 25: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "Asia-maps.png"}, {Name: "Asia-maps.png"},
@@ -675,7 +675,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
}, },
// ListObjectsResult-26. // ListObjectsResult-26.
// prefix = "new" and delimiter is set in the testCase.(testCase 58). // prefix = "new" and delimiter is set in the testCase.(testCase 58).
{ 26: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "newPrefix0"}, {Name: "newPrefix0"},
@@ -685,7 +685,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
}, },
// ListObjectsResult-27. // ListObjectsResult-27.
// Prefix is set to "Asia/India/" in the testCase, and delimiter is set to forward slash '/' (testCase 59). // Prefix is set to "Asia/India/" in the testCase, and delimiter is set to forward slash '/' (testCase 59).
{ 27: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "Asia/India/India-summer-photos-1"}, {Name: "Asia/India/India-summer-photos-1"},
@@ -694,7 +694,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
}, },
// ListObjectsResult-28. // ListObjectsResult-28.
// Marker is set to "Asia/India/India-summer-photos-1" and delimiter set in the testCase, (testCase 60). // Marker is set to "Asia/India/India-summer-photos-1" and delimiter set in the testCase, (testCase 60).
{ 28: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "newPrefix0"}, {Name: "newPrefix0"},
@@ -707,7 +707,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
}, },
// ListObjectsResult-29. // ListObjectsResult-29.
// Marker is set to "Asia/India/Karnataka/Bangalore/Koramangala/pics" in the testCase and delimiter set, (testCase 61). // Marker is set to "Asia/India/Karnataka/Bangalore/Koramangala/pics" in the testCase and delimiter set, (testCase 61).
{ 29: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "newPrefix0"}, {Name: "newPrefix0"},
@@ -720,12 +720,12 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
}, },
// ListObjectsResult-30. // ListObjectsResult-30.
// Prefix and Delimiter is set to '/', (testCase 62). // Prefix and Delimiter is set to '/', (testCase 62).
{ 30: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{}, Objects: []ObjectInfo{},
}, },
// ListObjectsResult-31 Empty directory, recursive listing // ListObjectsResult-31 Empty directory, recursive listing
{ 31: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "obj1"}, {Name: "obj1"},
@@ -734,7 +734,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
}, },
}, },
// ListObjectsResult-32 Empty directory, non recursive listing // ListObjectsResult-32 Empty directory, non recursive listing
{ 32: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "obj1"}, {Name: "obj1"},
@@ -743,7 +743,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
Prefixes: []string{"temporary/"}, Prefixes: []string{"temporary/"},
}, },
// ListObjectsResult-33 Listing empty directory only // ListObjectsResult-33 Listing empty directory only
{ 33: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "temporary/0/"}, {Name: "temporary/0/"},
@@ -752,12 +752,12 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
// ListObjectsResult-34: // ListObjectsResult-34:
// * Listing with marker > last object should return empty // * Listing with marker > last object should return empty
// * Listing an object with a trailing slash and '/' delimiter // * Listing an object with a trailing slash and '/' delimiter
{ 34: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{}, Objects: []ObjectInfo{},
}, },
// ListObjectsResult-35 list with custom uncommon delimiter // ListObjectsResult-35 list with custom uncommon delimiter
{ 35: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "file1/receipt.json"}, {Name: "file1/receipt.json"},
@@ -765,12 +765,12 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
Prefixes: []string{"file1/guidSplunk"}, Prefixes: []string{"file1/guidSplunk"},
}, },
// ListObjectsResult-36 list with nextmarker prefix and maxKeys set to 1. // ListObjectsResult-36 list with nextmarker prefix and maxKeys set to 1.
{ 36: {
IsTruncated: true, IsTruncated: true,
Prefixes: []string{"dir/day_id=2017-10-10/"}, Prefixes: []string{"dir/day_id=2017-10-10/"},
}, },
// ListObjectsResult-37 list with prefix match 2 levels deep // ListObjectsResult-37 list with prefix match 2 levels deep
{ 37: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "foo/201910/1112"}, {Name: "foo/201910/1112"},
@@ -778,7 +778,7 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
}, },
}, },
// ListObjectsResult-38 list with prefix match 1 level deep // ListObjectsResult-38 list with prefix match 1 level deep
{ 38: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "foo/201910/1112"}, {Name: "foo/201910/1112"},
@@ -788,14 +788,14 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
}, },
}, },
// ListObjectsResult-39 list with prefix match 1 level deep // ListObjectsResult-39 list with prefix match 1 level deep
{ 39: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "201910/foo/bar/xl.meta/1.txt"}, {Name: "201910/foo/bar/xl.meta/1.txt"},
}, },
}, },
// ListObjectsResult-40 // ListObjectsResult-40
{ 40: {
IsTruncated: false, IsTruncated: false,
Objects: []ObjectInfo{ Objects: []ObjectInfo{
{Name: "aaa"}, {Name: "aaa"},
@@ -829,9 +829,10 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
{"volatile-bucket-1", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-1"}, false}, {"volatile-bucket-1", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-1"}, false},
{"volatile-bucket-2", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-2"}, false}, {"volatile-bucket-2", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-2"}, false},
{"volatile-bucket-3", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-3"}, false}, {"volatile-bucket-3", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-3"}, false},
// Testing for failure cases with both perfix and marker (11). // If marker is *after* the last possible object from the prefix it should return an empty list.
// The prefix and marker combination to be valid it should satisfy strings.HasPrefix(marker, prefix). {"test-bucket-list-object", "Asia", "europe-object", "", 0, ListObjectsInfo{}, nil, true},
{"test-bucket-list-object", "asia", "europe-object", "", 0, ListObjectsInfo{}, fmt.Errorf("Invalid combination of marker '%s' and prefix '%s'", "europe-object", "asia"), false}, // If the marker is *before* the first possible object from the prefix it should return the first object.
{"test-bucket-list-object", "Asia", "A", "", 1, resultCases[4], nil, true},
// Setting a non-existing directory to be prefix (12-13). // Setting a non-existing directory to be prefix (12-13).
{"empty-bucket", "europe/france/", "", "", 1, ListObjectsInfo{}, nil, true}, {"empty-bucket", "europe/france/", "", "", 1, ListObjectsInfo{}, nil, true},
{"empty-bucket", "africa/tunisia/", "", "", 1, ListObjectsInfo{}, nil, true}, {"empty-bucket", "africa/tunisia/", "", "", 1, ListObjectsInfo{}, nil, true},
@@ -1573,9 +1574,8 @@ func testListObjectVersions(obj ObjectLayer, instanceType string, t1 TestErrHand
{"volatile-bucket-1", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-1"}, false}, {"volatile-bucket-1", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-1"}, false},
{"volatile-bucket-2", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-2"}, false}, {"volatile-bucket-2", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-2"}, false},
{"volatile-bucket-3", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-3"}, false}, {"volatile-bucket-3", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-3"}, false},
// Testing for failure cases with both perfix and marker (9). // If marker is *after* the last possible object from the prefix it should return an empty list.
// The prefix and marker combination to be valid it should satisfy strings.HasPrefix(marker, prefix). {"test-bucket-list-object", "Asia", "europe-object", "", 0, ListObjectsInfo{}, nil, true},
{"test-bucket-list-object", "asia", "europe-object", "", 0, ListObjectsInfo{}, fmt.Errorf("Invalid combination of marker '%s' and prefix '%s'", "europe-object", "asia"), false},
// Setting a non-existing directory to be prefix (10-11). // Setting a non-existing directory to be prefix (10-11).
{"empty-bucket", "europe/france/", "", "", 1, ListObjectsInfo{}, nil, true}, {"empty-bucket", "europe/france/", "", "", 1, ListObjectsInfo{}, nil, true},
{"empty-bucket", "africa/tunisia/", "", "", 1, ListObjectsInfo{}, nil, true}, {"empty-bucket", "africa/tunisia/", "", "", 1, ListObjectsInfo{}, nil, true},
@@ -1671,6 +1671,7 @@ func testListObjectVersions(obj ObjectLayer, instanceType string, t1 TestErrHand
{testBuckets[4], "file1/", "", "guidSplunk", 1000, resultCases[35], nil, true}, {testBuckets[4], "file1/", "", "guidSplunk", 1000, resultCases[35], nil, true},
// Test listing at prefix with expected prefix markers // Test listing at prefix with expected prefix markers
{testBuckets[5], "dir/", "", SlashSeparator, 1, resultCases[36], nil, true}, {testBuckets[5], "dir/", "", SlashSeparator, 1, resultCases[36], nil, true},
{"test-bucket-list-object", "Asia", "A", "", 1, resultCases[4], nil, true},
} }
for i, testCase := range testCases { for i, testCase := range testCases {
+4 -5
View File
@@ -1051,12 +1051,11 @@ func testListMultipartUploads(obj ObjectLayer, instanceType string, t TestErrHan
// Valid, existing bucket, delimiter not supported, returns empty values (Test number 8-9). // Valid, existing bucket, delimiter not supported, returns empty values (Test number 8-9).
{bucketNames[0], "", "", "", "*", 0, ListMultipartsInfo{Delimiter: "*"}, nil, true}, {bucketNames[0], "", "", "", "*", 0, ListMultipartsInfo{Delimiter: "*"}, nil, true},
{bucketNames[0], "", "", "", "-", 0, ListMultipartsInfo{Delimiter: "-"}, nil, true}, {bucketNames[0], "", "", "", "-", 0, ListMultipartsInfo{Delimiter: "-"}, nil, true},
// Testing for failure cases with both perfix and marker (Test number 10). // If marker is *after* the last possible object from the prefix it should return an empty list.
// The prefix and marker combination to be valid it should satisfy strings.HasPrefix(marker, prefix).
{ {
bucketNames[0], "asia", "europe-object", "", "", 0, bucketNames[0], "Asia", "europe-object", "", "", 0,
ListMultipartsInfo{}, ListMultipartsInfo{KeyMarker: "europe-object", Prefix: "Asia", IsTruncated: false},
fmt.Errorf("Invalid combination of marker '%s' and prefix '%s'", "europe-object", "asia"), false, nil, true,
}, },
// Setting an invalid combination of uploadIDMarker and Marker (Test number 11-12). // Setting an invalid combination of uploadIDMarker and Marker (Test number 11-12).
{ {
+13 -1
View File
@@ -342,6 +342,15 @@ func mustGetUUID() string {
return u.String() return u.String()
} }
// mustGetUUIDBytes - get a random UUID as 16 bytes unencoded.
func mustGetUUIDBytes() []byte {
u, err := uuid.NewRandom()
if err != nil {
logger.CriticalIf(GlobalContext, err)
}
return u[:]
}
// Create an s3 compatible MD5sum for complete multipart transaction. // Create an s3 compatible MD5sum for complete multipart transaction.
func getCompleteMultipartMD5(parts []CompletePart) string { func getCompleteMultipartMD5(parts []CompletePart) string {
var finalMD5Bytes []byte var finalMD5Bytes []byte
@@ -507,7 +516,10 @@ func (o *ObjectInfo) IsCompressedOK() (bool, error) {
} }
// GetActualSize - returns the actual size of the stored object // GetActualSize - returns the actual size of the stored object
func (o *ObjectInfo) GetActualSize() (int64, error) { func (o ObjectInfo) GetActualSize() (int64, error) {
if o.ActualSize != nil {
return *o.ActualSize, nil
}
if o.IsCompressed() { if o.IsCompressed() {
sizeStr, ok := o.UserDefined[ReservedMetadataPrefix+"actual-size"] sizeStr, ok := o.UserDefined[ReservedMetadataPrefix+"actual-size"]
if !ok { if !ok {
+54 -44
View File
@@ -474,7 +474,8 @@ func (api objectAPIHandlers) getObjectHandler(ctx context.Context, objectAPI Obj
// Automatically remove the object/version if an expiry lifecycle rule can be applied // Automatically remove the object/version if an expiry lifecycle rule can be applied
if lc, err := globalLifecycleSys.Get(bucket); err == nil { if lc, err := globalLifecycleSys.Get(bucket); err == nil {
rcfg, _ := globalBucketObjectLockSys.Get(bucket) rcfg, _ := globalBucketObjectLockSys.Get(bucket)
event := evalActionFromLifecycle(ctx, *lc, rcfg, objInfo) replcfg, _ := getReplicationConfig(ctx, bucket)
event := evalActionFromLifecycle(ctx, *lc, rcfg, replcfg, objInfo)
if event.Action.Delete() { if event.Action.Delete() {
// apply whatever the expiry rule is. // apply whatever the expiry rule is.
applyExpiryRule(event, lcEventSrc_s3GetObject, objInfo) applyExpiryRule(event, lcEventSrc_s3GetObject, objInfo)
@@ -732,7 +733,8 @@ func (api objectAPIHandlers) headObjectHandler(ctx context.Context, objectAPI Ob
// Automatically remove the object/version if an expiry lifecycle rule can be applied // Automatically remove the object/version if an expiry lifecycle rule can be applied
if lc, err := globalLifecycleSys.Get(bucket); err == nil { if lc, err := globalLifecycleSys.Get(bucket); err == nil {
rcfg, _ := globalBucketObjectLockSys.Get(bucket) rcfg, _ := globalBucketObjectLockSys.Get(bucket)
event := evalActionFromLifecycle(ctx, *lc, rcfg, objInfo) replcfg, _ := getReplicationConfig(ctx, bucket)
event := evalActionFromLifecycle(ctx, *lc, rcfg, replcfg, objInfo)
if event.Action.Delete() { if event.Action.Delete() {
// apply whatever the expiry rule is. // apply whatever the expiry rule is.
applyExpiryRule(event, lcEventSrc_s3HeadObject, objInfo) applyExpiryRule(event, lcEventSrc_s3HeadObject, objInfo)
@@ -1182,7 +1184,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
compressMetadata[ReservedMetadataPrefix+"compression"] = compressionAlgorithmV2 compressMetadata[ReservedMetadataPrefix+"compression"] = compressionAlgorithmV2
compressMetadata[ReservedMetadataPrefix+"actual-size"] = strconv.FormatInt(actualSize, 10) compressMetadata[ReservedMetadataPrefix+"actual-size"] = strconv.FormatInt(actualSize, 10)
reader = etag.NewReader(reader, nil) reader = etag.NewReader(ctx, reader, nil, nil)
wantEncryption := crypto.Requested(r.Header) wantEncryption := crypto.Requested(r.Header)
s2c, cb := newS2CompressReader(reader, actualSize, wantEncryption) s2c, cb := newS2CompressReader(reader, actualSize, wantEncryption)
dstOpts.IndexCB = cb dstOpts.IndexCB = cb
@@ -1195,7 +1197,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
reader = gr reader = gr
} }
srcInfo.Reader, err = hash.NewReader(reader, length, "", "", actualSize) srcInfo.Reader, err = hash.NewReader(ctx, reader, length, "", "", actualSize)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
@@ -1316,7 +1318,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
} }
// do not try to verify encrypted content // do not try to verify encrypted content
srcInfo.Reader, err = hash.NewReader(reader, targetSize, "", "", actualSize) srcInfo.Reader, err = hash.NewReader(ctx, reader, targetSize, "", "", actualSize)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
@@ -1427,7 +1429,12 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
srcInfo.UserDefined[ReservedMetadataPrefixLower+ReplicaTimestamp] = UTCNow().Format(time.RFC3339Nano) srcInfo.UserDefined[ReservedMetadataPrefixLower+ReplicaTimestamp] = UTCNow().Format(time.RFC3339Nano)
srcInfo.UserDefined[xhttp.AmzBucketReplicationStatus] = rs srcInfo.UserDefined[xhttp.AmzBucketReplicationStatus] = rs
} }
if dsc := mustReplicate(ctx, dstBucket, dstObject, getMustReplicateOptions(srcInfo, replication.UnsetReplicationType, dstOpts)); dsc.ReplicateAny() {
op := replication.ObjectReplicationType
if srcInfo.metadataOnly {
op = replication.MetadataReplicationType
}
if dsc := mustReplicate(ctx, dstBucket, dstObject, srcInfo.getMustReplicateOptions(op, dstOpts)); dsc.ReplicateAny() {
srcInfo.UserDefined[ReservedMetadataPrefixLower+ReplicationStatus] = dsc.PendingStatus() srcInfo.UserDefined[ReservedMetadataPrefixLower+ReplicationStatus] = dsc.PendingStatus()
srcInfo.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano) srcInfo.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano)
} }
@@ -1500,6 +1507,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
writeErrorResponse(ctx, w, toAPIError(ctx, rerr), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, rerr), r.URL)
return return
} }
objInfo.UserDefined = cloneMSS(opts.UserMetadata)
objInfo.ETag = remoteObjInfo.ETag objInfo.ETag = remoteObjInfo.ETag
objInfo.ModTime = remoteObjInfo.LastModified objInfo.ModTime = remoteObjInfo.LastModified
} else { } else {
@@ -1533,8 +1541,8 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
response := generateCopyObjectResponse(objInfo.ETag, objInfo.ModTime) response := generateCopyObjectResponse(objInfo.ETag, objInfo.ModTime)
encodedSuccessResponse := encodeResponse(response) encodedSuccessResponse := encodeResponse(response)
if dsc := mustReplicate(ctx, dstBucket, dstObject, getMustReplicateOptions(objInfo, replication.UnsetReplicationType, dstOpts)); dsc.ReplicateAny() { if dsc := mustReplicate(ctx, dstBucket, dstObject, objInfo.getMustReplicateOptions(replication.ObjectReplicationType, dstOpts)); dsc.ReplicateAny() {
scheduleReplication(ctx, objInfo.Clone(), objectAPI, dsc, replication.ObjectReplicationType) scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.ObjectReplicationType)
} }
setPutObjHeaders(w, objInfo, false) setPutObjHeaders(w, objInfo, false)
@@ -1737,7 +1745,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
metadata[ReservedMetadataPrefix+"compression"] = compressionAlgorithmV2 metadata[ReservedMetadataPrefix+"compression"] = compressionAlgorithmV2
metadata[ReservedMetadataPrefix+"actual-size"] = strconv.FormatInt(size, 10) metadata[ReservedMetadataPrefix+"actual-size"] = strconv.FormatInt(size, 10)
actualReader, err := hash.NewReader(reader, size, md5hex, sha256hex, actualSize) actualReader, err := hash.NewReader(ctx, reader, size, md5hex, sha256hex, actualSize)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
@@ -1757,8 +1765,20 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
md5hex = "" // Do not try to verify the content. md5hex = "" // Do not try to verify the content.
sha256hex = "" sha256hex = ""
} }
var hashReader *hash.Reader
hashReader, err := hash.NewReader(reader, size, md5hex, sha256hex, actualSize) // Optimization: If SSE-KMS and SSE-C did not request Content-Md5. Use uuid as etag
if !etag.ContentMD5Requested(r.Header) && (crypto.S3KMS.IsRequested(r.Header) || crypto.SSEC.IsRequested(r.Header)) {
hashReader, err = hash.NewReaderWithOpts(ctx, reader, hash.Options{
Size: size,
MD5Hex: md5hex,
SHA256Hex: sha256hex,
ActualSize: actualSize,
DisableMD5: false,
ForceMD5: mustGetUUIDBytes(),
})
} else {
hashReader, err = hash.NewReader(ctx, reader, size, md5hex, sha256hex, actualSize)
}
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
@@ -1815,9 +1835,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
return return
} }
if dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(ObjectInfo{ if dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(metadata, "", "", replication.ObjectReplicationType, opts)); dsc.ReplicateAny() {
UserDefined: metadata,
}, replication.ObjectReplicationType, opts)); dsc.ReplicateAny() {
metadata[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano) metadata[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano)
metadata[ReservedMetadataPrefixLower+ReplicationStatus] = dsc.PendingStatus() metadata[ReservedMetadataPrefixLower+ReplicationStatus] = dsc.PendingStatus()
} }
@@ -1856,7 +1874,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
} }
// do not try to verify encrypted content // do not try to verify encrypted content
hashReader, err = hash.NewReader(etag.Wrap(reader, hashReader), wantSize, "", "", actualSize) hashReader, err = hash.NewReader(ctx, etag.Wrap(reader, hashReader), wantSize, "", "", actualSize)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
@@ -1923,10 +1941,8 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
} }
} }
} }
if dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(ObjectInfo{ if dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(metadata, "", "", replication.ObjectReplicationType, opts)); dsc.ReplicateAny() {
UserDefined: metadata, scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.ObjectReplicationType)
}, replication.ObjectReplicationType, opts)); dsc.ReplicateAny() {
scheduleReplication(ctx, objInfo.Clone(), objectAPI, dsc, replication.ObjectReplicationType)
} }
setPutObjHeaders(w, objInfo, false) setPutObjHeaders(w, objInfo, false)
@@ -2075,7 +2091,7 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
} }
} }
hreader, err := hash.NewReader(reader, size, md5hex, sha256hex, size) hreader, err := hash.NewReader(ctx, reader, size, md5hex, sha256hex, size)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
@@ -2126,7 +2142,7 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
metadata[ReservedMetadataPrefix+"compression"] = compressionAlgorithmV2 metadata[ReservedMetadataPrefix+"compression"] = compressionAlgorithmV2
metadata[ReservedMetadataPrefix+"actual-size"] = strconv.FormatInt(size, 10) metadata[ReservedMetadataPrefix+"actual-size"] = strconv.FormatInt(size, 10)
actualReader, err := hash.NewReader(reader, size, "", "", actualSize) actualReader, err := hash.NewReader(ctx, reader, size, "", "", actualSize)
if err != nil { if err != nil {
return err return err
} }
@@ -2140,7 +2156,7 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
size = -1 // Since compressed size is un-predictable. size = -1 // Since compressed size is un-predictable.
} }
hashReader, err := hash.NewReader(reader, size, "", "", actualSize) hashReader, err := hash.NewReader(ctx, reader, size, "", "", actualSize)
if err != nil { if err != nil {
return err return err
} }
@@ -2182,9 +2198,7 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
return ObjectLocked{} return ObjectLocked{}
} }
if dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(ObjectInfo{ if dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(metadata, "", "", replication.ObjectReplicationType, opts)); dsc.ReplicateAny() {
UserDefined: metadata,
}, replication.ObjectReplicationType, opts)); dsc.ReplicateAny() {
metadata[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano) metadata[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano)
metadata[ReservedMetadataPrefixLower+ReplicationStatus] = dsc.PendingStatus() metadata[ReservedMetadataPrefixLower+ReplicationStatus] = dsc.PendingStatus()
@@ -2212,7 +2226,7 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
} }
// do not try to verify encrypted content // do not try to verify encrypted content
hashReader, err = hash.NewReader(etag.Wrap(reader, hashReader), wantSize, "", "", actualSize) hashReader, err = hash.NewReader(ctx, etag.Wrap(reader, hashReader), wantSize, "", "", actualSize)
if err != nil { if err != nil {
return err return err
} }
@@ -2235,10 +2249,8 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
return err return err
} }
if dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(ObjectInfo{ if dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(metadata, "", "", replication.ObjectReplicationType, opts)); dsc.ReplicateAny() {
UserDefined: metadata, scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.ObjectReplicationType)
}, replication.ObjectReplicationType, opts)); dsc.ReplicateAny() {
scheduleReplication(ctx, objInfo.Clone(), objectAPI, dsc, replication.ObjectReplicationType)
} }
// Notify object created event. // Notify object created event.
@@ -2406,7 +2418,7 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http.
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
} }
if isErrObjectNotFound(err) { if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
writeSuccessNoContent(w) writeSuccessNoContent(w)
return return
} }
@@ -2453,7 +2465,7 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http.
DeleteMarkerVersionID: dmVersionID, DeleteMarkerVersionID: dmVersionID,
DeleteMarkerMTime: DeleteMarkerMTime{objInfo.ModTime}, DeleteMarkerMTime: DeleteMarkerMTime{objInfo.ModTime},
DeleteMarker: objInfo.DeleteMarker, DeleteMarker: objInfo.DeleteMarker,
ReplicationState: objInfo.getReplicationState(), ReplicationState: objInfo.ReplicationState(),
}, },
Bucket: bucket, Bucket: bucket,
EventType: ReplicateIncomingDelete, EventType: ReplicateIncomingDelete,
@@ -2528,7 +2540,7 @@ func (api objectAPIHandlers) PutObjectLegalHoldHandler(w http.ResponseWriter, r
oi.UserDefined[strings.ToLower(xhttp.AmzObjectLockLegalHold)] = strings.ToUpper(string(legalHold.Status)) oi.UserDefined[strings.ToLower(xhttp.AmzObjectLockLegalHold)] = strings.ToUpper(string(legalHold.Status))
oi.UserDefined[ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp] = UTCNow().Format(time.RFC3339Nano) oi.UserDefined[ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp] = UTCNow().Format(time.RFC3339Nano)
dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(*oi, replication.MetadataReplicationType, opts)) dsc := mustReplicate(ctx, bucket, object, oi.getMustReplicateOptions(replication.MetadataReplicationType, opts))
if dsc.ReplicateAny() { if dsc.ReplicateAny() {
oi.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano) oi.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano)
oi.UserDefined[ReservedMetadataPrefixLower+ReplicationStatus] = dsc.PendingStatus() oi.UserDefined[ReservedMetadataPrefixLower+ReplicationStatus] = dsc.PendingStatus()
@@ -2543,9 +2555,9 @@ func (api objectAPIHandlers) PutObjectLegalHoldHandler(w http.ResponseWriter, r
return return
} }
dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(objInfo, replication.MetadataReplicationType, opts)) dsc := mustReplicate(ctx, bucket, object, objInfo.getMustReplicateOptions(replication.MetadataReplicationType, opts))
if dsc.ReplicateAny() { if dsc.ReplicateAny() {
scheduleReplication(ctx, objInfo.Clone(), objectAPI, dsc, replication.MetadataReplicationType) scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.MetadataReplicationType)
} }
writeSuccessResponseHeadersOnly(w) writeSuccessResponseHeadersOnly(w)
@@ -2697,7 +2709,7 @@ func (api objectAPIHandlers) PutObjectRetentionHandler(w http.ResponseWriter, r
oi.UserDefined[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = "" oi.UserDefined[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = ""
} }
oi.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp] = UTCNow().Format(time.RFC3339Nano) oi.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp] = UTCNow().Format(time.RFC3339Nano)
dsc = mustReplicate(ctx, bucket, object, getMustReplicateOptions(*oi, replication.MetadataReplicationType, opts)) dsc = mustReplicate(ctx, bucket, object, oi.getMustReplicateOptions(replication.MetadataReplicationType, opts))
if dsc.ReplicateAny() { if dsc.ReplicateAny() {
oi.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano) oi.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano)
oi.UserDefined[ReservedMetadataPrefixLower+ReplicationStatus] = dsc.PendingStatus() oi.UserDefined[ReservedMetadataPrefixLower+ReplicationStatus] = dsc.PendingStatus()
@@ -2712,9 +2724,9 @@ func (api objectAPIHandlers) PutObjectRetentionHandler(w http.ResponseWriter, r
return return
} }
dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(objInfo, replication.MetadataReplicationType, opts)) dsc := mustReplicate(ctx, bucket, object, objInfo.getMustReplicateOptions(replication.MetadataReplicationType, opts))
if dsc.ReplicateAny() { if dsc.ReplicateAny() {
scheduleReplication(ctx, objInfo.Clone(), objectAPI, dsc, replication.MetadataReplicationType) scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.MetadataReplicationType)
} }
writeSuccessResponseHeadersOnly(w) writeSuccessResponseHeadersOnly(w)
@@ -2923,9 +2935,7 @@ func (api objectAPIHandlers) PutObjectTaggingHandler(w http.ResponseWriter, r *h
} }
tagsStr := tags.String() tagsStr := tags.String()
oi := objInfo.Clone() dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(objInfo.UserDefined, tagsStr, objInfo.ReplicationStatus, replication.MetadataReplicationType, opts))
oi.UserTags = tagsStr
dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(oi, replication.MetadataReplicationType, opts))
if dsc.ReplicateAny() { if dsc.ReplicateAny() {
opts.UserDefined = make(map[string]string) opts.UserDefined = make(map[string]string)
opts.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano) opts.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano)
@@ -2941,7 +2951,7 @@ func (api objectAPIHandlers) PutObjectTaggingHandler(w http.ResponseWriter, r *h
} }
if dsc.ReplicateAny() { if dsc.ReplicateAny() {
scheduleReplication(ctx, objInfo.Clone(), objAPI, dsc, replication.MetadataReplicationType) scheduleReplication(ctx, objInfo, objAPI, dsc, replication.MetadataReplicationType)
} }
if objInfo.VersionID != "" && objInfo.VersionID != nullVersionID { if objInfo.VersionID != "" && objInfo.VersionID != nullVersionID {
@@ -3003,7 +3013,7 @@ func (api objectAPIHandlers) DeleteObjectTaggingHandler(w http.ResponseWriter, r
return return
} }
dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(oi, replication.MetadataReplicationType, opts)) dsc := mustReplicate(ctx, bucket, object, oi.getMustReplicateOptions(replication.MetadataReplicationType, opts))
if dsc.ReplicateAny() { if dsc.ReplicateAny() {
opts.UserDefined = make(map[string]string) opts.UserDefined = make(map[string]string)
opts.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano) opts.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano)
@@ -3017,7 +3027,7 @@ func (api objectAPIHandlers) DeleteObjectTaggingHandler(w http.ResponseWriter, r
} }
if dsc.ReplicateAny() { if dsc.ReplicateAny() {
scheduleReplication(ctx, oi.Clone(), objAPI, dsc, replication.MetadataReplicationType) scheduleReplication(ctx, oi, objAPI, dsc, replication.MetadataReplicationType)
} }
if oi.VersionID != "" && oi.VersionID != nullVersionID { if oi.VersionID != "" && oi.VersionID != nullVersionID {
+1 -4
View File
@@ -70,11 +70,8 @@ func getLambdaEventData(bucket, object string, cred auth.Credentials, r *http.Re
reqParams.Set(k, v) reqParams.Set(k, v)
} }
} }
extraHeaders := http.Header{}
if rng := r.Header.Get(xhttp.Range); rng != "" {
extraHeaders.Set(xhttp.Range, r.Header.Get(xhttp.Range))
}
extraHeaders := http.Header{}
u, err := clnt.PresignHeader(r.Context(), http.MethodGet, bucket, object, duration, reqParams, extraHeaders) u, err := clnt.PresignHeader(r.Context(), http.MethodGet, bucket, object, duration, reqParams, extraHeaders)
if err != nil { if err != nil {
return levent.Event{}, err return levent.Event{}, err
+18 -13
View File
@@ -164,9 +164,7 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
return return
} }
if dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(ObjectInfo{ if dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(metadata, "", "", replication.ObjectReplicationType, ObjectOptions{})); dsc.ReplicateAny() {
UserDefined: metadata,
}, replication.ObjectReplicationType, ObjectOptions{})); dsc.ReplicateAny() {
metadata[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano) metadata[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano)
metadata[ReservedMetadataPrefixLower+ReplicationStatus] = dsc.PendingStatus() metadata[ReservedMetadataPrefixLower+ReplicationStatus] = dsc.PendingStatus()
} }
@@ -450,7 +448,7 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
} }
actualPartSize = length actualPartSize = length
var reader io.Reader = etag.NewReader(gr, nil) var reader io.Reader = etag.NewReader(ctx, gr, nil, nil)
mi, err := objectAPI.GetMultipartInfo(ctx, dstBucket, dstObject, uploadID, dstOpts) mi, err := objectAPI.GetMultipartInfo(ctx, dstBucket, dstObject, uploadID, dstOpts)
if err != nil { if err != nil {
@@ -473,7 +471,7 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
length = -1 length = -1
} }
srcInfo.Reader, err = hash.NewReader(reader, length, "", "", actualPartSize) srcInfo.Reader, err = hash.NewReader(ctx, reader, length, "", "", actualPartSize)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
@@ -528,7 +526,7 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
wantSize = info.EncryptedSize() wantSize = info.EncryptedSize()
} }
srcInfo.Reader, err = hash.NewReader(reader, wantSize, "", "", actualPartSize) srcInfo.Reader, err = hash.NewReader(ctx, reader, wantSize, "", "", actualPartSize)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
@@ -717,7 +715,7 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
_, isCompressed := mi.UserDefined[ReservedMetadataPrefix+"compression"] _, isCompressed := mi.UserDefined[ReservedMetadataPrefix+"compression"]
var idxCb func() []byte var idxCb func() []byte
if isCompressed { if isCompressed {
actualReader, err := hash.NewReader(reader, size, md5hex, sha256hex, actualSize) actualReader, err := hash.NewReader(ctx, reader, size, md5hex, sha256hex, actualSize)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
@@ -738,7 +736,7 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
sha256hex = "" sha256hex = ""
} }
hashReader, err := hash.NewReader(reader, size, md5hex, sha256hex, actualSize) hashReader, err := hash.NewReader(ctx, reader, size, md5hex, sha256hex, actualSize)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
@@ -800,7 +798,7 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
wantSize = info.EncryptedSize() wantSize = info.EncryptedSize()
} }
// do not try to verify encrypted content // do not try to verify encrypted content
hashReader, err = hash.NewReader(etag.Wrap(reader, hashReader), wantSize, "", "", actualSize) hashReader, err = hash.NewReader(ctx, etag.Wrap(reader, hashReader), wantSize, "", "", actualSize)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
@@ -957,6 +955,8 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
} }
opts.Versioned = versioned
opts.VersionSuspended = suspended
// First, we compute the ETag of the multipart object. // First, we compute the ETag of the multipart object.
// The ETag of a multi-part object is always: // The ETag of a multi-part object is always:
@@ -997,8 +997,8 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
} }
setPutObjHeaders(w, objInfo, false) setPutObjHeaders(w, objInfo, false)
if dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(objInfo, replication.ObjectReplicationType, opts)); dsc.ReplicateAny() { if dsc := mustReplicate(ctx, bucket, object, objInfo.getMustReplicateOptions(replication.ObjectReplicationType, opts)); dsc.ReplicateAny() {
scheduleReplication(ctx, objInfo.Clone(), objectAPI, dsc, replication.ObjectReplicationType) scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.ObjectReplicationType)
} }
if _, ok := r.Header[xhttp.MinIOSourceReplicationRequest]; ok { if _, ok := r.Header[xhttp.MinIOSourceReplicationRequest]; ok {
actualSize, _ := objInfo.GetActualSize() actualSize, _ := objInfo.GetActualSize()
@@ -1075,8 +1075,13 @@ func (api objectAPIHandlers) AbortMultipartUploadHandler(w http.ResponseWriter,
} }
opts := ObjectOptions{} opts := ObjectOptions{}
if err := abortMultipartUpload(ctx, bucket, object, uploadID, opts); err != nil { if err := abortMultipartUpload(ctx, bucket, object, uploadID, opts); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) switch err.(type) {
return case InvalidUploadID:
// Do not have return an error for non-existent upload-id
default:
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
} }
writeSuccessNoContent(w) writeSuccessNoContent(w)
+3 -3
View File
@@ -129,15 +129,15 @@ func Mkdir(dirPath string, mode os.FileMode) (err error) {
} }
// MkdirAll captures time taken to call os.MkdirAll // MkdirAll captures time taken to call os.MkdirAll
func MkdirAll(dirPath string, mode os.FileMode) (err error) { func MkdirAll(dirPath string, mode os.FileMode, baseDir string) (err error) {
defer updateOSMetrics(osMetricMkdirAll, dirPath)(err) defer updateOSMetrics(osMetricMkdirAll, dirPath)(err)
return osMkdirAll(dirPath, mode) return osMkdirAll(dirPath, mode, baseDir)
} }
// Rename captures time taken to call os.Rename // Rename captures time taken to call os.Rename
func Rename(src, dst string) (err error) { func Rename(src, dst string) (err error) {
defer updateOSMetrics(osMetricRename, src, dst)(err) defer updateOSMetrics(osMetricRename, src, dst)(err)
return os.Rename(src, dst) return RenameSys(src, dst)
} }
// OpenFile captures time taken to call os.OpenFile // OpenFile captures time taken to call os.OpenFile
+8 -8
View File
@@ -73,7 +73,7 @@ func reliableRemoveAll(dirPath string) (err error) {
// Wrapper functions to os.MkdirAll, which calls reliableMkdirAll // Wrapper functions to os.MkdirAll, which calls reliableMkdirAll
// this is to ensure that if there is a racy parent directory // this is to ensure that if there is a racy parent directory
// delete in between we can simply retry the operation. // delete in between we can simply retry the operation.
func mkdirAll(dirPath string, mode os.FileMode) (err error) { func mkdirAll(dirPath string, mode os.FileMode, baseDir string) (err error) {
if dirPath == "" { if dirPath == "" {
return errInvalidArgument return errInvalidArgument
} }
@@ -82,7 +82,7 @@ func mkdirAll(dirPath string, mode os.FileMode) (err error) {
return err return err
} }
if err = reliableMkdirAll(dirPath, mode); err != nil { if err = reliableMkdirAll(dirPath, mode, baseDir); err != nil {
// File path cannot be verified since one of the parents is a file. // File path cannot be verified since one of the parents is a file.
if isSysErrNotDir(err) { if isSysErrNotDir(err) {
return errFileAccessDenied return errFileAccessDenied
@@ -100,11 +100,11 @@ func mkdirAll(dirPath string, mode os.FileMode) (err error) {
// Reliably retries os.MkdirAll if for some reason os.MkdirAll returns // Reliably retries os.MkdirAll if for some reason os.MkdirAll returns
// syscall.ENOENT (parent does not exist). // syscall.ENOENT (parent does not exist).
func reliableMkdirAll(dirPath string, mode os.FileMode) (err error) { func reliableMkdirAll(dirPath string, mode os.FileMode, baseDir string) (err error) {
i := 0 i := 0
for { for {
// Creates all the parent directories, with mode 0777 mkdir honors system umask. // Creates all the parent directories, with mode 0777 mkdir honors system umask.
if err = osMkdirAll(dirPath, mode); err != nil { if err = osMkdirAll(dirPath, mode, baseDir); err != nil {
// Retry only for the first retryable error. // Retry only for the first retryable error.
if osIsNotExist(err) && i == 0 { if osIsNotExist(err) && i == 0 {
i++ i++
@@ -120,7 +120,7 @@ func reliableMkdirAll(dirPath string, mode os.FileMode) (err error) {
// and reliableRenameAll. This is to ensure that if there is a // and reliableRenameAll. This is to ensure that if there is a
// racy parent directory delete in between we can simply retry // racy parent directory delete in between we can simply retry
// the operation. // the operation.
func renameAll(srcFilePath, dstFilePath string) (err error) { func renameAll(srcFilePath, dstFilePath, baseDir string) (err error) {
if srcFilePath == "" || dstFilePath == "" { if srcFilePath == "" || dstFilePath == "" {
return errInvalidArgument return errInvalidArgument
} }
@@ -132,7 +132,7 @@ func renameAll(srcFilePath, dstFilePath string) (err error) {
return err return err
} }
if err = reliableRename(srcFilePath, dstFilePath); err != nil { if err = reliableRename(srcFilePath, dstFilePath, baseDir); err != nil {
switch { switch {
case isSysErrNotDir(err) && !osIsNotExist(err): case isSysErrNotDir(err) && !osIsNotExist(err):
// Windows can have both isSysErrNotDir(err) and osIsNotExist(err) returning // Windows can have both isSysErrNotDir(err) and osIsNotExist(err) returning
@@ -162,8 +162,8 @@ func renameAll(srcFilePath, dstFilePath string) (err error) {
// Reliably retries os.RenameAll if for some reason os.RenameAll returns // Reliably retries os.RenameAll if for some reason os.RenameAll returns
// syscall.ENOENT (parent does not exist). // syscall.ENOENT (parent does not exist).
func reliableRename(srcFilePath, dstFilePath string) (err error) { func reliableRename(srcFilePath, dstFilePath, baseDir string) (err error) {
if err = reliableMkdirAll(path.Dir(dstFilePath), 0o777); err != nil { if err = reliableMkdirAll(path.Dir(dstFilePath), 0o777, baseDir); err != nil {
return err return err
} }
+10 -10
View File
@@ -29,15 +29,15 @@ func TestOSMkdirAll(t *testing.T) {
t.Fatalf("Unable to create xlStorage test setup, %s", err) t.Fatalf("Unable to create xlStorage test setup, %s", err)
} }
if err = mkdirAll("", 0o777); err != errInvalidArgument { if err = mkdirAll("", 0o777, ""); err != errInvalidArgument {
t.Fatal("Unexpected error", err) t.Fatal("Unexpected error", err)
} }
if err = mkdirAll(pathJoin(path, "my-obj-del-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"), 0o777); err != errFileNameTooLong { if err = mkdirAll(pathJoin(path, "my-obj-del-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"), 0o777, ""); err != errFileNameTooLong {
t.Fatal("Unexpected error", err) t.Fatal("Unexpected error", err)
} }
if err = mkdirAll(pathJoin(path, "success-vol", "success-object"), 0o777); err != nil { if err = mkdirAll(pathJoin(path, "success-vol", "success-object"), 0o777, ""); err != nil {
t.Fatal("Unexpected error", err) t.Fatal("Unexpected error", err)
} }
} }
@@ -50,25 +50,25 @@ func TestOSRenameAll(t *testing.T) {
t.Fatalf("Unable to create xlStorage test setup, %s", err) t.Fatalf("Unable to create xlStorage test setup, %s", err)
} }
if err = mkdirAll(pathJoin(path, "testvolume1"), 0o777); err != nil { if err = mkdirAll(pathJoin(path, "testvolume1"), 0o777, ""); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err = renameAll("", "foo"); err != errInvalidArgument { if err = renameAll("", "foo", ""); err != errInvalidArgument {
t.Fatal(err) t.Fatal(err)
} }
if err = renameAll("foo", ""); err != errInvalidArgument { if err = renameAll("foo", "", ""); err != errInvalidArgument {
t.Fatal(err) t.Fatal(err)
} }
if err = renameAll(pathJoin(path, "testvolume1"), pathJoin(path, "testvolume2")); err != nil { if err = renameAll(pathJoin(path, "testvolume1"), pathJoin(path, "testvolume2"), ""); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err = renameAll(pathJoin(path, "testvolume1"), pathJoin(path, "testvolume2")); err != errFileNotFound { if err = renameAll(pathJoin(path, "testvolume1"), pathJoin(path, "testvolume2"), ""); err != errFileNotFound {
t.Fatal(err) t.Fatal(err)
} }
if err = renameAll(pathJoin(path, "my-obj-del-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"), pathJoin(path, "testvolume2")); err != errFileNameTooLong { if err = renameAll(pathJoin(path, "my-obj-del-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"), pathJoin(path, "testvolume2"), ""); err != errFileNameTooLong {
t.Fatal("Unexpected error", err) t.Fatal("Unexpected error", err)
} }
if err = renameAll(pathJoin(path, "testvolume1"), pathJoin(path, "my-obj-del-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001")); err != errFileNameTooLong { if err = renameAll(pathJoin(path, "testvolume1"), pathJoin(path, "my-obj-del-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"), ""); err != errFileNameTooLong {
t.Fatal("Unexpected error", err) t.Fatal("Unexpected error", err)
} }
} }
+7
View File
@@ -21,6 +21,8 @@
package cmd package cmd
import ( import (
"syscall"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
@@ -29,3 +31,8 @@ func Rename2(src, dst string) (err error) {
defer updateOSMetrics(osMetricRename2, src, dst)(err) defer updateOSMetrics(osMetricRename2, src, dst)(err)
return unix.Renameat2(unix.AT_FDCWD, src, unix.AT_FDCWD, dst, uint(2)) // RENAME_EXCHANGE from 'man renameat2' return unix.Renameat2(unix.AT_FDCWD, src, unix.AT_FDCWD, dst, uint(2)) // RENAME_EXCHANGE from 'man renameat2'
} }
// RenameSys is low level call in case of Linux this uses syscall.Rename() directly.
func RenameSys(src, dst string) (err error) {
return syscall.Rename(src, dst)
}
+9 -1
View File
@@ -20,10 +20,18 @@
package cmd package cmd
import "errors" import (
"errors"
"os"
)
// Rename2 is not implemented in a non linux environment // Rename2 is not implemented in a non linux environment
func Rename2(src, dst string) (err error) { func Rename2(src, dst string) (err error) {
defer updateOSMetrics(osMetricRename2, src, dst)(errors.New("not implemented, skipping")) defer updateOSMetrics(osMetricRename2, src, dst)(errors.New("not implemented, skipping"))
return errSkipFile return errSkipFile
} }
// RenameSys is low level call in case of non-Linux this just uses os.Rename()
func RenameSys(src, dst string) (err error) {
return os.Rename(src, dst)
}
+2 -1
View File
@@ -31,7 +31,8 @@ func access(name string) error {
return err return err
} }
func osMkdirAll(dirPath string, perm os.FileMode) error { func osMkdirAll(dirPath string, perm os.FileMode, _ string) error {
// baseDir is not honored in plan9 and solaris platforms.
return os.MkdirAll(dirPath, perm) return os.MkdirAll(dirPath, perm)
} }
+9 -2
View File
@@ -24,6 +24,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"os" "os"
"strings"
"sync" "sync"
"syscall" "syscall"
"unsafe" "unsafe"
@@ -47,7 +48,13 @@ func access(name string) error {
// directories that MkdirAll creates. // directories that MkdirAll creates.
// If path is already a directory, MkdirAll does nothing // If path is already a directory, MkdirAll does nothing
// and returns nil. // and returns nil.
func osMkdirAll(dirPath string, perm os.FileMode) error { func osMkdirAll(dirPath string, perm os.FileMode, baseDir string) error {
if baseDir != "" {
if strings.HasPrefix(baseDir, dirPath) {
return nil
}
}
// Slow path: make sure parent exists and then call Mkdir for path. // Slow path: make sure parent exists and then call Mkdir for path.
i := len(dirPath) i := len(dirPath)
for i > 0 && os.IsPathSeparator(dirPath[i-1]) { // Skip trailing path separator. for i > 0 && os.IsPathSeparator(dirPath[i-1]) { // Skip trailing path separator.
@@ -61,7 +68,7 @@ func osMkdirAll(dirPath string, perm os.FileMode) error {
if j > 1 { if j > 1 {
// Create parent. // Create parent.
if err := osMkdirAll(dirPath[:j-1], perm); err != nil { if err := osMkdirAll(dirPath[:j-1], perm, baseDir); err != nil {
return err return err
} }
} }
+2 -1
View File
@@ -31,7 +31,8 @@ func access(name string) error {
return err return err
} }
func osMkdirAll(dirPath string, perm os.FileMode) error { func osMkdirAll(dirPath string, perm os.FileMode, _ string) error {
// baseDir is not honored in windows platform
return os.MkdirAll(dirPath, perm) return os.MkdirAll(dirPath, perm)
} }
+27
View File
@@ -229,6 +229,33 @@ func (client *peerRESTClient) GetMetrics(ctx context.Context, t madmin.MetricTyp
return info, err return info, err
} }
func (client *peerRESTClient) GetResourceMetrics(ctx context.Context) (<-chan Metric, error) {
respBody, err := client.callWithContext(ctx, peerRESTMethodResourceMetrics, nil, nil, -1)
if err != nil {
return nil, err
}
dec := gob.NewDecoder(respBody)
ch := make(chan Metric)
go func(ch chan<- Metric) {
defer func() {
xhttp.DrainBody(respBody)
close(ch)
}()
for {
var metric Metric
if err := dec.Decode(&metric); err != nil {
return
}
select {
case <-ctx.Done():
return
case ch <- metric:
}
}
}(ch)
return ch, nil
}
// GetProcInfo - fetch MinIO process information for a remote node. // GetProcInfo - fetch MinIO process information for a remote node.
func (client *peerRESTClient) GetProcInfo(ctx context.Context) (info madmin.ProcInfo, err error) { func (client *peerRESTClient) GetProcInfo(ctx context.Context) (info madmin.ProcInfo, err error) {
respBody, err := client.callWithContext(ctx, peerRESTMethodProcInfo, nil, nil, -1) respBody, err := client.callWithContext(ctx, peerRESTMethodProcInfo, nil, nil, -1)
+1
View File
@@ -77,6 +77,7 @@ const (
peerRESTMethodDevNull = "/devnull" peerRESTMethodDevNull = "/devnull"
peerRESTMethodNetperf = "/netperf" peerRESTMethodNetperf = "/netperf"
peerRESTMethodMetrics = "/metrics" peerRESTMethodMetrics = "/metrics"
peerRESTMethodResourceMetrics = "/resourcemetrics"
peerRESTMethodGetReplicationMRF = "/getreplicationmrf" peerRESTMethodGetReplicationMRF = "/getreplicationmrf"
peerRESTMethodGetSRMetrics = "/getsrmetrics" peerRESTMethodGetSRMetrics = "/getsrmetrics"
) )
+21 -4
View File
@@ -474,6 +474,22 @@ func (s *peerRESTServer) GetMetricsHandler(w http.ResponseWriter, r *http.Reques
logger.LogIf(ctx, gob.NewEncoder(w).Encode(info)) logger.LogIf(ctx, gob.NewEncoder(w).Encode(info))
} }
func (s *peerRESTServer) GetResourceMetrics(w http.ResponseWriter, r *http.Request) {
if !s.IsValid(w, r) {
s.writeErrorResponse(w, errors.New("invalid request"))
return
}
enc := gob.NewEncoder(w)
for m := range ReportMetrics(r.Context(), resourceMetricsGroups) {
if err := enc.Encode(m); err != nil {
s.writeErrorResponse(w, errors.New("Encoding metric failed: "+err.Error()))
return
}
}
}
// GetSysConfigHandler - returns system config information. // GetSysConfigHandler - returns system config information.
// (only the config that are of concern to minio) // (only the config that are of concern to minio)
func (s *peerRESTServer) GetSysConfigHandler(w http.ResponseWriter, r *http.Request) { func (s *peerRESTServer) GetSysConfigHandler(w http.ResponseWriter, r *http.Request) {
@@ -939,7 +955,7 @@ func (s *peerRESTServer) ListenHandler(w http.ResponseWriter, r *http.Request) {
// Listen Publisher uses nonblocking publish and hence does not wait for slow subscribers. // Listen Publisher uses nonblocking publish and hence does not wait for slow subscribers.
// 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()
ch := make(chan event.Event, 2000) ch := make(chan event.Event, globalAPIConfig.getRequestsPoolCapacity())
err := globalHTTPListen.Subscribe(mask, ch, doneCh, func(ev event.Event) bool { err := globalHTTPListen.Subscribe(mask, ch, doneCh, func(ev event.Event) bool {
if ev.S3.Bucket.Name != "" && values.Get(peerRESTListenBucket) != "" { if ev.S3.Bucket.Name != "" && values.Get(peerRESTListenBucket) != "" {
@@ -994,7 +1010,7 @@ func (s *peerRESTServer) TraceHandler(w http.ResponseWriter, r *http.Request) {
// Trace Publisher uses nonblocking publish and hence does not wait for slow subscribers. // Trace Publisher uses nonblocking publish and hence does not wait for slow subscribers.
// 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()
ch := make(chan madmin.TraceInfo, 2000) ch := make(chan madmin.TraceInfo, 100000)
err = globalTrace.Subscribe(traceOpts.TraceTypes(), ch, r.Context().Done(), func(entry madmin.TraceInfo) bool { err = globalTrace.Subscribe(traceOpts.TraceTypes(), ch, r.Context().Done(), func(entry madmin.TraceInfo) bool {
return shouldTrace(entry, traceOpts) return shouldTrace(entry, traceOpts)
}) })
@@ -1143,13 +1159,13 @@ func (s *peerRESTServer) ConsoleLogHandler(w http.ResponseWriter, r *http.Reques
doneCh := make(chan struct{}) doneCh := make(chan struct{})
defer close(doneCh) defer close(doneCh)
ch := make(chan log.Info, 2000) ch := make(chan log.Info, 100000)
err := globalConsoleSys.Subscribe(ch, doneCh, "", 0, madmin.LogMaskAll, nil) err := globalConsoleSys.Subscribe(ch, doneCh, "", 0, madmin.LogMaskAll, nil)
if err != nil { if err != nil {
s.writeErrorResponse(w, err) s.writeErrorResponse(w, err)
return return
} }
keepAliveTicker := time.NewTicker(500 * time.Millisecond) keepAliveTicker := time.NewTicker(time.Second)
defer keepAliveTicker.Stop() defer keepAliveTicker.Stop()
enc := gob.NewEncoder(w) enc := gob.NewEncoder(w)
@@ -1438,6 +1454,7 @@ func registerPeerRESTHandlers(router *mux.Router) {
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodProcInfo).HandlerFunc(h(server.GetProcInfoHandler)) subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodProcInfo).HandlerFunc(h(server.GetProcInfoHandler))
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodMemInfo).HandlerFunc(h(server.GetMemInfoHandler)) subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodMemInfo).HandlerFunc(h(server.GetMemInfoHandler))
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodMetrics).HandlerFunc(h(server.GetMetricsHandler)).Queries(restQueries(peerRESTMetricsTypes)...) subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodMetrics).HandlerFunc(h(server.GetMetricsHandler)).Queries(restQueries(peerRESTMetricsTypes)...)
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodResourceMetrics).HandlerFunc(h(server.GetResourceMetrics))
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodSysErrors).HandlerFunc(h(server.GetSysErrorsHandler)) subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodSysErrors).HandlerFunc(h(server.GetSysErrorsHandler))
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodSysServices).HandlerFunc(h(server.GetSysServicesHandler)) subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodSysServices).HandlerFunc(h(server.GetSysServicesHandler))
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodSysConfig).HandlerFunc(h(server.GetSysConfigHandler)) subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodSysConfig).HandlerFunc(h(server.GetSysConfigHandler))
+45 -15
View File
@@ -23,6 +23,7 @@ import (
"errors" "errors"
"io" "io"
"net/url" "net/url"
"sort"
"strconv" "strconv"
xhttp "github.com/minio/minio/internal/http" xhttp "github.com/minio/minio/internal/http"
@@ -123,8 +124,10 @@ func NewS3PeerSys(endpoints EndpointServerPools) *S3PeerSys {
} }
} }
// ListBuckets lists buckets across all servers and returns a possible consistent view // ListBuckets lists buckets across all nodes and returns a consistent view:
func (sys *S3PeerSys) ListBuckets(ctx context.Context, opts BucketOptions) (result []BucketInfo, err error) { // - Return an error when a pool cannot return N/2+1 valid bucket information
// - For each pool, check if the bucket exists in N/2+1 nodes before including it in the final result
func (sys *S3PeerSys) ListBuckets(ctx context.Context, opts BucketOptions) ([]BucketInfo, error) {
g := errgroup.WithNErrs(len(sys.peerClients)) g := errgroup.WithNErrs(len(sys.peerClients))
nodeBuckets := make([][]BucketInfo, len(sys.peerClients)) nodeBuckets := make([][]BucketInfo, len(sys.peerClients))
@@ -148,25 +151,52 @@ func (sys *S3PeerSys) ListBuckets(ctx context.Context, opts BucketOptions) (resu
errs = append(errs, g.Wait()...) errs = append(errs, g.Wait()...)
quorum := len(sys.peerClients)/2 + 1 // The list of buckets in a map to avoid duplication
if err = reduceReadQuorumErrs(ctx, errs, bucketOpIgnoredErrs, quorum); err != nil { resultMap := make(map[string]BucketInfo)
return nil, err
}
bucketsMap := make(map[string]struct{}) for poolIdx := 0; poolIdx < sys.poolsCount; poolIdx++ {
for idx, buckets := range nodeBuckets { perPoolErrs := make([]error, 0, len(sys.peerClients))
if errs[idx] != nil { for i, client := range sys.peerClients {
continue if slices.Contains(client.GetPools(), poolIdx) {
perPoolErrs = append(perPoolErrs, errs[i])
}
} }
for _, bi := range buckets { quorum := len(perPoolErrs)/2 + 1
_, ok := bucketsMap[bi.Name] if poolErr := reduceWriteQuorumErrs(ctx, perPoolErrs, bucketOpIgnoredErrs, quorum); poolErr != nil {
if !ok { return nil, poolErr
bucketsMap[bi.Name] = struct{}{} }
result = append(result, bi)
bucketsMap := make(map[string]int)
for idx, buckets := range nodeBuckets {
if buckets == nil {
continue
}
if !slices.Contains(sys.peerClients[idx].GetPools(), poolIdx) {
continue
}
for _, bi := range buckets {
_, ok := resultMap[bi.Name]
if ok {
// Skip it, this bucket is found in another pool
continue
}
bucketsMap[bi.Name]++
if bucketsMap[bi.Name] == quorum {
resultMap[bi.Name] = bi
}
} }
} }
} }
result := make([]BucketInfo, 0, len(resultMap))
for _, bi := range resultMap {
result = append(result, bi)
}
sort.Slice(result, func(i, j int) bool {
return result[i].Name < result[j].Name
})
return result, nil return result, nil
} }
-5
View File
@@ -22,7 +22,6 @@ import (
"encoding/gob" "encoding/gob"
"errors" "errors"
"net/http" "net/http"
"sort"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/minio/mux" "github.com/minio/mux"
@@ -124,10 +123,6 @@ func listBucketsLocal(ctx context.Context, opts BucketOptions) (buckets []Bucket
} }
} }
sort.Slice(buckets, func(i, j int) bool {
return buckets[i].Name < buckets[j].Name
})
return buckets, nil return buckets, nil
} }
+1 -1
View File
@@ -306,7 +306,7 @@ func netperf(ctx context.Context, duration time.Duration) madmin.NetperfNodeResu
defer wg.Done() defer wg.Done()
err := globalNotificationSys.peerClients[index].DevNull(ctx, r) err := globalNotificationSys.peerClients[index].DevNull(ctx, r)
if err != nil { if err != nil {
errStr = err.Error() errStr = fmt.Sprintf("error with %s: %s", globalNotificationSys.peerClients[index].String(), err.Error())
} }
}() }()
} }

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