Compare commits

..

273 Commits

Author SHA1 Message Date
Harshavardhana aae6846413 feat: allow expiration of all versions via ILM Expiration action (#17521)
Following extension allows users to specify immediate purge of
all versions as soon as the latest version of this object has
expired.

```
<LifecycleConfiguration>
    <Rule>
        <ID>ClassADocRule</ID>
        <Filter>
           <Prefix>classA/</Prefix>
        </Filter>
        <Status>Enabled</Status>
        <Expiration>
             <Days>3650</Days>
	     <ExpiredObjectAllVersions>true</ExpiredObjectAllVersions>
        </Expiration>
    </Rule>
    ...
```
2023-06-28 22:12:28 -07:00
Harshavardhana 5317a0b755 fix: support LDAP settings properly in ftp/sftp (#17536)
Bonus this PR enhances and supports creating
buckets via ftp `mkdir`

fixes #17526
2023-06-28 13:15:21 -07:00
Harshavardhana 73de721a63 fix: handle copyObjectPart encryption properly (#17530)
- look for requested encryption while compressing
  not just via HTTP Headers, but also via multipart
  metadata

- look for SSE-S3 etag decryption not just via HTTP
  Headers, but also via multipart metadata

fixes #17519
2023-06-28 09:43:50 -07:00
Harshavardhana d2f5c3621f fix: add additional decommission traces for ILM expired content (#17522)
current decommission traces were missing for

- Skipped ILM expired versions
- Skipped single DELETE marked version
- A success or failure in decommissioning DELETE marker
- allow additional info to be shared in DecomStatus() API
2023-06-27 11:59:40 -07:00
Harshavardhana 1818764840 fix: bug in passing Versioned field set for getHealReplicationInfo() (#17498)
Bonus: rejects prefix deletes on object-locked buckets earlier
2023-06-27 09:45:50 -07:00
Harshavardhana d3e5e607a7 allow site-replication checks to work on non-distributed setups (#17524)
fixes #17523
2023-06-27 09:23:50 -07:00
Shireesh Anjal c1943ea3af Capture realtime metrics in health report (#17516) 2023-06-27 01:39:18 -07:00
Harshavardhana 2a82c15bf1 update all our deps (#17497) 2023-06-26 15:36:56 -07:00
guangwu 87b6fb37d6 chore: pkg imported more than once (#17444) 2023-06-26 09:21:29 -07:00
Kaan Kabalak 21fbe88e1f Print certain log messages once per error (#17484) 2023-06-24 20:29:13 -07:00
Harshavardhana 1f8b9b4bd5 fix: do not listAndHeal() inline with PutObject() (#17499)
there is a possibility that slow drives can actually add latency
to the overall call, leading to a large spike in latency.

this can happen if there are other parallel listObjects()
calls to the same drive, in-turn causing each other to sort
of serialize.

this potentially improves performance and makes PutObject()
also non-blocking.
2023-06-24 19:31:04 -07:00
Minio Trusted fcbed41cc3 Update yaml files to latest version RELEASE.2023-06-23T20-26-00Z 2023-06-24 07:25:49 +00:00
Klaus Post 216069d0da Remove 'null' version ID from directory object response (#17495)
Fixes #17494

Regression from #17132
2023-06-23 13:26:00 -07:00
Harshavardhana eefa047974 fix: keep decommission in a go-routine (#17496)
This was removed by mistake in #17491
2023-06-23 12:29:32 -07:00
Anis Eleuch d8dad5c9ea s3: Make/Delete buckets to use error quorum per pool (#17467) 2023-06-23 11:48:23 -07:00
Klaus Post bf8a68879c fix: Time ILM Actions for scanner info (#17493)
ILM Actions were not timed fix it.
2023-06-23 07:48:36 -07:00
Aditya Manthramurthy f3248a4b37 Redact all secrets from config viewing APIs (#17380)
This change adds a `Secret` property to `HelpKV` to identify secrets
like passwords and auth tokens that should not be revealed by the server
in its configuration fetching APIs. Configuration reporting APIs now do
not return secrets.
2023-06-23 07:45:27 -07:00
Harshavardhana d315d012a4 decom: during multiple pool decom preserve current pool status (#17491)
removal of completed pools must retain pool status of other
pools in draining, to resume any remaining draining operations.
2023-06-23 07:44:18 -07:00
Harshavardhana bd9bf3693f lambda: negative duration for presigned URL default to 1H (#17489)
fixes a bug where users created with Expiration as
timeSentinel is not rejected while generating the
presigned URL for lambda processing.
2023-06-23 00:17:24 -07:00
Klaus Post 15daa2e74a tooling: Add xlmeta --combine switch that will combine inline data (#17488)
Will combine or write partial data of each version found in the inspect data.

Example:

```
> xl-meta -export -combine inspect-data.1228fb52.zip

(... metadata json...)
}
Attempting to combine version "994f1113-da94-4be1-8551-9dbc54b204bc".
Read shard 1 Data shards 9 Parity 4 (994f1113-da94-4be1-8551-9dbc54b204bc/shard-01-of-13.data)
Read shard 2 Data shards 9 Parity 4 (994f1113-da94-4be1-8551-9dbc54b204bc/shard-02-of-13.data)
Read shard 3 Data shards 9 Parity 4 (994f1113-da94-4be1-8551-9dbc54b204bc/shard-03-of-13.data)
Read shard 4 Data shards 9 Parity 4 (994f1113-da94-4be1-8551-9dbc54b204bc/shard-04-of-13.data)
Read shard 6 Data shards 9 Parity 4 (994f1113-da94-4be1-8551-9dbc54b204bc/shard-06-of-13.data)
Read shard 7 Data shards 9 Parity 4 (994f1113-da94-4be1-8551-9dbc54b204bc/shard-07-of-13.data)
Read shard 8 Data shards 8 Parity 5 (994f1113-da94-4be1-8551-9dbc54b204bc/shard-08-of-13.data)
Read shard 9 Data shards 8 Parity 5 (994f1113-da94-4be1-8551-9dbc54b204bc/shard-09-of-13.data)
Read shard 10 Data shards 8 Parity 5 (994f1113-da94-4be1-8551-9dbc54b204bc/shard-10-of-13.data)
Read shard 11 Data shards 8 Parity 5 (994f1113-da94-4be1-8551-9dbc54b204bc/shard-11-of-13.data)
Read shard 13 Data shards 8 Parity 5 (994f1113-da94-4be1-8551-9dbc54b204bc/shard-13-of-13.data)
Attempting to reconstruct using parity sets:
* Setup: Data shards: 9 - Parity blocks: 6
Have 6 complete remapped data shards and 6 complete parity shards. Could NOT reconstruct: too few shards given
* Setup: Data shards: 8 - Parity blocks: 5
Have 5 complete remapped data shards and 5 complete parity shards. Could reconstruct completely
0 bytes missing. Truncating 0 from the end.
Wrote output to 994f1113-da94-4be1-8551-9dbc54b204bc.complete
```

So far only inline data, but no real reason that external data can't also be included with some handling of blocks.

Supports only unencrypted data.
2023-06-22 12:41:24 -07:00
Harshavardhana 74759b05a5 make sure to set relevant config entries correctly (#17485)
Bonus: also allow skipping keys properly.
2023-06-22 10:04:02 -07:00
Aditya Manthramurthy 82ce78a17c Fix locking in policy attach API (#17426)
For policy attach/detach API to work correctly the server should hold a
lock before reading existing policy mapping and until after writing the
updated policy mapping. This is fixed in this change.

A site replication bug, where LDAP policy attach/detach were not
correctly propagated is also fixed in this change.

Bonus: Additionally, the server responds with the actual (or net)
changes performed in the attach/detach API call. For e.g. if a user
already has policy A applied, and a call to attach policies A and B is
performed, the server will respond that B was attached successfully.
2023-06-21 22:44:50 -07:00
Harshavardhana 9af6c6ceef under rebalance look for expired versions v/s remaining versions (#17482)
A continuation of PR #17479 for rebalance behavior must
also match the decommission behavior.

Fixes bug where rebalance would ignore rebalancing object
versions after one of the version returned "ObjectNotFound"
2023-06-21 13:23:20 -07:00
Harshavardhana 021372cc4c update helm release v5.0.11
Signed-off-by: Harshavardhana <harsha@minio.io>
2023-06-21 12:29:09 -07:00
Praveen raj Mani b94ab07c2f Honor global root CAs for kafka audit tls (#17481)
honor global root CAs for kafka audit tls
2023-06-21 10:50:40 -07:00
Harshavardhana 7605d07bb2 add support for bucket level request count per API (#17468)
New metrics added to calculate API request count
per bucket, per API.  Captures errors, including
4xx, 5xx HTTP status codes separately.
2023-06-21 09:41:59 -07:00
Harshavardhana ccc5801112 always look for expired versions v/s remaining versions (#17479)
while decommissioning it can so happen that the non-current
versions are all expired but there is a DEL marker as the
latest version.

For such objects, we should not decommission them instead
calculate the remaining versions and if the remaining versions
is one and that version is a DEL marker consider such
an object not to be scheduled for decommissioning.
2023-06-21 08:49:28 -07:00
Praveen raj Mani 7c72b25ef0 Add an option to make bucket notifications synchronous (#17406)
With the current asynchronous behaviour in sending notification events
to the targets, we can't provide guaranteed delivery as the systems
might go for restarts.

For such event-driven use-cases, we can provide an option to enable
synchronous events where the APIs wait until the event is successfully
sent or persisted.

This commit adds 'MINIO_API_SYNC_EVENTS' env which when set to 'on'
will enable sending/persisting events to targets synchronously.
2023-06-20 17:38:59 -07:00
Harshavardhana 02c2ec3027 skip onlineDisks with parity mismatch (#17478) 2023-06-20 13:18:24 -07:00
Harshavardhana 65c31fab12 fix: do not crash rebalance code instead set the object layer (#17465)
fixes #17421
2023-06-20 09:28:23 -07:00
jiuker b6b68be052 fix: replication check for duplicate endpoints detection with wrong route (#17474) 2023-06-20 09:27:54 -07:00
Harshavardhana 15911c85f6 safely ignore out of band deletions while decommissioning (#17473) 2023-06-20 08:31:42 -07:00
Aditya Manthramurthy 5a1612fe32 Bump up madmin-go and pkg deps (#17469) 2023-06-19 17:53:08 -07:00
Minio Trusted bbb7ae156c Update yaml files to latest version RELEASE.2023-06-19T19-52-50Z 2023-06-19 22:53:03 +00:00
Harshavardhana f9b8d1c699 fix: sio-error test to fail if commands fail (#17466) 2023-06-19 12:52:50 -07:00
Harshavardhana 1443b5927a allow quorum fileInfo to pick same parityBlocks (#17454)
Bonus: allow replication to proceed for 503 errors such as
with error code SlowDownRead
2023-06-18 18:20:15 -07:00
Anis Eleuch 35ef35b5c1 fix a integer divide by zero crash during rebalance (#17455)
A state is updated with a delete marker, which does not have parity or
data blocks defined, which can cause the integer divide by zero panics.

This commit fixes to avoid panics.
2023-06-18 11:14:53 -07:00
Harshavardhana 6806537eb3 event args list for fanOut notification must be sized same (#17450)
without this fan-out API can crash if client cancels
the on-going request.
2023-06-18 07:09:20 -07:00
Harshavardhana 64de61d15d fallback on etags if they match when mtime is not same (#17424)
on "unversioned" buckets there are situations
when successive concurrent I/O can lead to
an inconsistent state() with mtime while the
etag might be the same for the object on disk.

in such a scenario it is possible for us to
allow reading of the object since etag matches
and if etag matches we are guaranteed that we
have enough copies the object will be readable
and same.

This PR allows fallback in such scenarios.
2023-06-17 19:18:20 -07:00
Harshavardhana 22b7c8cd8a upgrade pkg and dperf to latest packages (#17448)
- dperf improvements in benchmarking read and write tests
- upgrade mimedb to use latest content-types
2023-06-17 07:31:36 -07:00
Poorna c4d0c49a5f ensure metadata updates go to same pool where version exists (#17451)
This PR also returns the replication status in 
proxy calls and defers replication attempt if 
HEAD on object version returned a error different
from NoSuchKey
2023-06-17 07:30:53 -07:00
Minio Trusted 142a5b0dcd Update yaml files to latest version RELEASE.2023-06-16T02-41-06Z 2023-06-16 05:36:19 +00:00
mungo312 25db1e4eca helm: fix permission denied errors in post-job when running as non-root (#17175)
Fixes ##17174
2023-06-15 19:41:06 -07:00
Harshavardhana 47a48b6832 do not save any metadata from the headers in tar extract (#17436)
only preserve the same storage-class as incoming
request other than that rest of them must be
deduced.
2023-06-15 17:44:07 -07:00
Harshavardhana e98309eb75 update minio-go/v7 v7.0.57 (#17439) 2023-06-15 16:29:19 -07:00
Alex 87051872a7 Update console to v0.30.0 (#17438) 2023-06-15 15:30:56 -07:00
Anis Eleuch a2aed12dcd decom: Fix a typo in routing decommissioning requests (#17435)
A specific node should do the decommissioning task, however routing the
start decommissioning to that node was not working properly.

Co-authored-by: Anis Elleuch <anis@min.io>
2023-06-15 14:54:29 -07:00
Anis Eleuch d8e6e76e89 site-repl: Better error msg when setting sync in a local cluster (#17407) 2023-06-15 12:44:22 -07:00
Anis Eleuch 8c33fdf5f4 s3-check-md5: Add --modified-since flag to skip some objects (#17410) 2023-06-15 12:44:09 -07:00
Harshavardhana ad4e511026 do not save plain-text ETag when encryption is requested (#17427)
fixes an issue under bucket replication could cause
ETags for replicated SSE-S3 single part PUT objects,
to fail as we would attempt a decryption while listing,
or stat() operation.
2023-06-15 12:43:26 -07:00
Klaus Post 4a562d6732 fix: fanout error response - error must be string for marshaling (#17433)
Uses https://github.com/minio/minio-go/pull/1839
2023-06-15 09:21:53 -07:00
Poorna a9082e4f79 site replication: cancel ongoing op properly (#17428) 2023-06-15 08:05:08 -07:00
dependabot[bot] 6278679ffd build(deps): bump github.com/lestrrat-go/jwx from 1.2.25 to 1.2.26 (#17425)
Bumps [github.com/lestrrat-go/jwx](https://github.com/lestrrat-go/jwx) from 1.2.25 to 1.2.26.
- [Release notes](https://github.com/lestrrat-go/jwx/releases)
- [Changelog](https://github.com/lestrrat-go/jwx/blob/v1.2.26/Changes)
- [Commits](https://github.com/lestrrat-go/jwx/compare/v1.2.25...v1.2.26)

---
updated-dependencies:
- dependency-name: github.com/lestrrat-go/jwx
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-14 12:17:22 -07:00
jiuker 0474791cf8 fix: set time format right (#17402) 2023-06-14 07:49:13 -07:00
Harshavardhana 69f819e199 move to pkg@v1.7.3 to support aws:kms string in policy conditions (#17414) 2023-06-14 07:42:03 -07:00
Harshavardhana f32efd5429 more compliance related fixes (#17408)
- lifecycle must return InvalidArgument for rule errors
- do not return `null` versionId in HTTP header
- reject mixed SSE uploads with correct error message
2023-06-13 13:52:33 -07:00
jiuker 22c247a988 fix: preserve multiple values for query params (#17392) 2023-06-13 11:38:46 -07:00
Shubhendu 35d71682f6 fix: do not allow removal of inbuilt policies unless they are already persisted (#17264)
Dont allow removal of inbuilt policies such as `readwrite, readonly, writeonly and diagnostics`
2023-06-13 11:06:17 -07:00
drivebyer 3d6b88a60e fix: syscall to record time on non-linux (#17383) 2023-06-13 11:04:50 -07:00
Harshavardhana 26a0803388 various compliance related fixes (#17401)
- getObjectTagging to be allowed for anonymous policies
- return correct errors for invalid retention period
- return sorted list of tags for an object
- putObjectTagging must return 200 OK not 204 OK
- return 409 ErrObjectLockConfigurationNotAllowed for existing buckets
2023-06-12 13:22:07 -07:00
Anis Eleuch ae95384dd8 Revert "heal: Update object parity with the latest configured SC (#17187)" (#17404) 2023-06-12 11:54:51 -07:00
Anis Eleuch 0f0dcf0c5e tar: Avoid storing snowball extraction header in extract objects (#17389) 2023-06-12 09:42:06 -07:00
Klaus Post 6f2406b0b6 fix: protect ReplicationStats against concurrent map iteration and write crash (#17403) 2023-06-12 09:17:11 -07:00
Anis Eleuch bb24346e04 listen: Only error out if not able to bind any interface (#17353) 2023-06-12 09:09:28 -07:00
Harshavardhana be45ffd8a4 return 204 status code for DeleteBucketTagging (#17400) 2023-06-11 20:49:02 -07:00
Poorna Krishnamoorthy f986b0c493 replication: perform bucket resync in parallel (#16707)
Default number of parallel resync operations for a bucket to 10
to speed up resync.
2023-06-11 16:09:55 -07:00
Harshavardhana c9e87f0548 service accounts are allowed to have no expiration (#17397) 2023-06-11 10:34:59 -07:00
Harshavardhana 43468f4d47 return InvalidRequest when no parts are provided (#17395) 2023-06-10 21:59:51 -07:00
Minio Trusted 91987d6f7a Update yaml files to latest version RELEASE.2023-06-09T07-32-12Z 2023-06-10 05:38:08 +00:00
Harshavardhana 6b7c98bd0f make sure we pick up the right Go version in vulncheck (#17388) 2023-06-09 00:32:12 -07:00
Harshavardhana b829e80ecb do not disable root for invalid API config values (#17386) 2023-06-08 15:50:06 -07:00
Klaus Post 6e38d0f3ab Add more bootstrap info in debug mode (#17362) 2023-06-08 08:39:47 -07:00
Anis Eleuch 38342b1df5 decom: Parallelize decommissining (#17364) 2023-06-07 14:27:51 -07:00
Harshavardhana dbd4c2425e fix: kafka broker pings must not be greater than 1sec (#17376) 2023-06-07 11:47:00 -07:00
Harshavardhana 49ce85ee3d allow prefix/markers to have '/' in the beginning to throw an empty (#17373) 2023-06-07 11:25:26 -07:00
Anis Eleuch eba378e4a1 vrf: Fix testing for loopback coming from the address (#17372) 2023-06-07 09:53:05 -07:00
Harshavardhana 442c50ff00 remove delimiter if not set by client, also fetchOwner is optional (#17366) 2023-06-06 21:31:47 -07:00
Harshavardhana d1448adbda use slices package and remove some helpers (#17342) 2023-06-06 10:12:52 -07:00
jiuker 5a21b1f353 fix: Delete dir failed when .DS_Store in it (#17352) 2023-06-06 10:12:06 -07:00
Bartosz Marczyński 123a2fb3a8 helm: add /tmp mount to post jobs (#17301) 2023-06-06 06:50:33 -07:00
Harshavardhana 2f9e2147f5 allow quota enforcement to rely on older values (#17351)
PUT calls cannot afford to have large latency build-ups due
to contentious usage.json, or worse letting them fail with
some unexpected error, this can happen when this file is
concurrently being updated via scanner or it is being
healed during a disk replacement heal.

However, these are fairly quick in theory, stressed clusters
can quickly show visible latency this can add up leading to
invalid errors returned during PUT.

It is perhaps okay for us to relax this error return requirement
instead, make sure that we log that we are proceeding to take in
the requests while the quota is using an older value for the quota
enforcement. These things will reconcile themselves eventually,
via scanner making sure to overwrite the usage.json.

Bonus: make sure that storage-rest-client sets ExpectTimeouts to
be 'true', such that DiskInfo() call with contextTimeout does
not prematurely disconnect the servers leading to a longer
healthCheck, back-off routine. This can easily pile up while also
causing active callers to disconnect, leading to quorum loss.

DiskInfo is actively used in the PUT, Multipart call path for
upgrading parity when disks are down, it in-turn shouldn't cause
more disks to go down.
2023-06-05 16:56:35 -07:00
Harshavardhana 75c6fc4f02 only allow decryption of etag for only sse-s3 (#17335) 2023-06-05 13:08:51 -07:00
Anis Eleuch f9e07d6143 goroutines parser: Add --less flag to filter goroutines (#17339) 2023-06-04 14:20:46 -07:00
Anis Eleuch 1436858347 log: Add a log when saving pool.bin fails (#17338)
Co-authored-by: Anis Elleuch <anis@min.io>
2023-06-04 14:20:21 -07:00
jiuker 8030e12ba5 fix: expMovingAvg is too small when startTime is zero (#17346) 2023-06-03 13:41:51 -07:00
Minio Trusted a485b923bf Update yaml files to latest version RELEASE.2023-06-02T23-17-26Z 2023-06-03 05:07:30 +00:00
Kaan Kabalak 0649aca219 Add expiration to ListServiceAccounts function (#17249) 2023-06-02 16:17:26 -07:00
Harshavardhana b210ea79bc do not save MTime in newMultipartUpload() to avoid side-affects (#17340) 2023-06-02 14:38:09 -07:00
Poorna 68f80b5fe7 replication: ignore retention mode validation for replica (#17332) 2023-06-01 18:53:12 -07:00
Poorna e95825a42e replication: use latest object info for metrics update (#17333) 2023-06-01 18:52:55 -07:00
Anis Eleuch 931712dc46 fix: converting 'server closed idle connection' to errDiskNotFound (#17330) 2023-06-01 15:40:28 -07:00
Harshavardhana 54e544e03e allow lookup()/head() operations on Veeam SOS objects (#17331) 2023-06-01 15:26:26 -07:00
Poorna f86b9abf32 site removal: update site config and reload targets after update (#17327) 2023-06-01 10:19:56 -07:00
Anis Eleuch 9ef7eda33a heal: Avoid objects created after the heal disk start time (#17323) 2023-05-31 13:10:45 -07:00
Klaus Post c9e26401fa Fix GetObject encrypted etag (#17302)
Co-authored-by: Harshavardhana <harsha@minio.io>
2023-05-31 13:10:25 -07:00
Harshavardhana e53f49e9a9 add additional tools that help in debugging (#17325) 2023-05-31 13:06:08 -07:00
jiuker 14f6ac9222 fix: fail large content in DeleteMultipleObjects() early (#17321) 2023-05-31 10:58:14 -07:00
drivebyer b8474295af fix: time() returned function not being called as expected in globalSync() (#17319) 2023-05-31 09:40:23 -07:00
Shireesh Anjal 817e85a3e0 fix: proxy not set on subnet logger webhook sometimes (#17320) 2023-05-31 08:09:09 -07:00
jiuker fb5ce3b87a record err time when remote node is offline (#17262) 2023-05-30 10:07:26 -07:00
Klaus Post 6fe028b7c5 Revert s3 select simdjson reuse (#17310) 2023-05-30 10:02:22 -07:00
Harshavardhana 1cd7f1e38d fix: cleanup empty multipart folders upon stale upload cleanup (#17312) 2023-05-30 09:56:50 -07:00
jiuker 043fd8b536 fix: on windows use FindClose close handler (#17306) 2023-05-30 02:15:57 -07:00
Klaus Post 669acbb032 Fix Test LDAP for automatic site replication (#17305) 2023-05-29 08:13:58 -07:00
Minio Trusted 086d8f036e Update yaml files to latest version RELEASE.2023-05-27T05-56-19Z 2023-05-28 06:53:17 +00:00
Harshavardhana 394690dcfb check for upto 50%+ data disks to be offline (#17294) 2023-05-26 22:56:19 -07:00
Harshavardhana 398bca92ff update helm to v5.0.10
Signed-off-by: Harshavardhana <harsha@minio.io>
2023-05-26 17:05:49 -07:00
Harshavardhana fb328b1a64 upgrade all dependencies (#17276) 2023-05-26 16:31:28 -07:00
Anis Eleuch 563f667e30 reorder-disks: Fix UID to UUID and add better error messages (#17292) 2023-05-26 15:03:31 -07:00
Klaus Post c839b64f6a fix: compressed+encrypted block overhead (#17289) 2023-05-26 10:57:07 -07:00
Anis Eleuch 6425fec366 s3: Add x-minio-error-code header for S3 HEAD requests (#17283) 2023-05-26 10:13:18 -07:00
Harshavardhana d5059840ef fix: for delete marked objects choose appropriate parity (#17287) 2023-05-26 09:57:44 -07:00
Aditya Manthramurthy 65cba212e8 Remove older policy attach behavior for LDAP (#17240) 2023-05-26 06:31:24 -07:00
Aditya Manthramurthy 7a69c9c75a Update builtin policy entities command (#17241) 2023-05-25 22:31:05 -07:00
Harshavardhana 4a425cbac1 cleanup scripts and apply shfmt (#17284) 2023-05-25 22:07:25 -07:00
Harshavardhana 615169c4ec update docker ubi image to 8.8 (#17281) 2023-05-25 18:19:09 -07:00
Harshavardhana 5cd9dcb844 rebalance 'null' delete markers properly (#17282) 2023-05-25 16:12:53 -07:00
Anis Eleuch 54c5c88fe6 Add number of offline disks in quorum errors (#16822) 2023-05-25 09:39:06 -07:00
jiuker 443250d135 fix: for Target isActive use net.Dial instead (#17251) 2023-05-25 09:24:11 -07:00
Harshavardhana 9b5829c16e avoid decommissioning DEL markers with single versions (#17274) 2023-05-25 09:18:49 -07:00
jiuker d749aaab69 fix: ignore existing target status when adding new targets (#17250) 2023-05-24 22:57:37 -07:00
Krishnan Parthasarathi 62df731006 Add updatedAt for GetBucketLifecycleConfig (#17271) 2023-05-24 22:52:39 -07:00
Harshavardhana d0a0eb9738 support fan-out objects via PostUpload() (#17233) 2023-05-24 22:51:07 -07:00
Klaus Post 66156b8230 Stricter partNumber checks (#17270)
Fixes #17269
2023-05-24 08:00:47 -07:00
Klaus Post 5677f73794 Add PostObject Checksum (#17244) 2023-05-23 07:58:33 -07:00
Harshavardhana ef54200db7 offline drives more than 50% of total drives return error (#17252) 2023-05-23 07:57:57 -07:00
Harshavardhana 7875efbf61 update minio/dperf to latest release v0.4.4 (#17267) 2023-05-22 19:17:17 -07:00
Krishnan Parthasarathi 3e128c116e Add lifecycle event source to audit log tags (#17248) 2023-05-22 15:28:56 -07:00
Krishnan Parthasarathi 55a3310446 logger-http: Don't retry after a succesful send (#17266) 2023-05-22 14:53:18 -07:00
Harshavardhana fc03be7891 simplify bucket metadata lookups for versioning/object locking (#17253) 2023-05-22 12:05:14 -07:00
jiuker b1b00a5055 fix: Avoid Income globalStats twice upon error (#17263) 2023-05-22 07:42:27 -07:00
Poorna 2920b0fc6d allow specification of path/virtual style bucket lookup in batch replication (#17201) 2023-05-21 15:16:31 -07:00
Anis Eleuch a30a55f3b1 Add object parity in listing V2M and listing versions M (#17238) 2023-05-19 09:42:45 -07:00
Praveen raj Mani ecfb18b26a Freeze the s3 APIs until the notification sub-system initializes completely (#17182) 2023-05-19 08:44:48 -07:00
jiuker 41fa8fa2d2 fix: increment counter when entry be skipped (#17237) 2023-05-19 08:36:52 -07:00
jiuker e94e6adf91 fix: return proper error if OIDC Discoverydoc fails to respond (#17242) 2023-05-19 02:13:33 -07:00
jiuker 7d433f16c4 before return make globalScannerMetrics.incTime call (#17230) 2023-05-18 13:45:05 -07:00
Klaus Post b06d7bf834 fix: leaking connections in JSON SQL with limited return (#17239) 2023-05-18 11:26:46 -07:00
Minio Trusted b784e458cb Update yaml files to latest version RELEASE.2023-05-18T00-05-36Z 2023-05-18 17:56:05 +00:00
Aditya Manthramurthy 9d96b18df0 Add "name" and "description" params to service acc (#17172) 2023-05-17 17:05:36 -07:00
drivebyer ad2ab6eb3e fix: Give accurate cap to slice (#17224) 2023-05-17 15:14:09 -07:00
jiuker f037c9b286 Protecting the read index is not out of bounds (#17226) 2023-05-17 12:09:41 -07:00
Praveen raj Mani 85912985b6 Check for only network errors in audit webhook for reachability (#17228) 2023-05-17 11:10:33 -07:00
Harshavardhana 876f51a708 remove minio-js from mint tests until next minio-js release 2023-05-17 09:09:54 -07:00
Harshavardhana f7d29b4a53 cleanup of multipart per disk must cleanup itself only (#17223) 2023-05-17 01:45:58 -07:00
Harshavardhana 06557fe8be allow decommissioned pools to be removed while others are finishing (#17221) 2023-05-16 16:00:57 -07:00
Poorna 2131046427 replication: fix audit log reporting (#17222) 2023-05-16 15:35:08 -07:00
Klaus Post aaf1abc993 simplify HardLimitReader by using LimitReader for internal usage (#17218) 2023-05-16 13:14:37 -07:00
jiuker 413549bcf5 fix: loadStatsFromDisk() should return nil for configNotFound (#17217) 2023-05-16 12:23:38 -07:00
jiuker 9a799065b3 fix: make slice cap of right size (#17192) 2023-05-16 08:10:07 -07:00
jiuker fd2959fa3a fix: workers.New err must be returned (#17208) 2023-05-16 08:08:00 -07:00
Anis Eleuch 07927e032a Add a script to filter goroutines waiting for a given number of minutes (#17204) 2023-05-16 08:05:49 -07:00
jiuker 15bec32bb4 fix: tier handlers must write error only once (#17205) 2023-05-15 23:56:52 -07:00
Anis Eleuch e2b7a08c10 heal: Update object parity with the latest configured SC (#17187) 2023-05-15 21:32:13 -07:00
Harshavardhana ef2fc0f99e fix: reduce using memory and temporary files. (#17206) 2023-05-15 14:08:54 -07:00
Harshavardhana d063596430 fix: veeam SOS API 'system.xml' strings (#17202) 2023-05-15 12:06:42 -07:00
jiuker bd2dc6c670 fix: in healing tracker printTo when err (#17207) 2023-05-15 10:14:48 -07:00
Anis Eleuch 684399433b lock: Retry locking with an increasing random interval (#17200) 2023-05-13 08:42:21 -07:00
Harshavardhana b62791617c fix: notify systemd as soon as we wait on the OS signal (#17199) 2023-05-12 16:42:17 -07:00
Poorna e07c2ab868 Use hash.NewLimitReader for internal multipart calls (#17191) 2023-05-12 11:19:08 -07:00
jiuker 203755793c fix: in printEndpointError count error once per init() (#17193) 2023-05-12 10:41:54 -07:00
Anis Eleuch 883c98e26f fix: remove objects when there are skipped versions due to ILM in decom (#17198) 2023-05-12 10:37:38 -07:00
Harshavardhana f5a20a5d06 allow nodes offline in k8s setups when expanding pools (#17183) 2023-05-11 17:41:33 -07:00
Poorna ef7177ebbd disallow bucket replication setup with site replication (#17189) 2023-05-11 15:48:40 -07:00
Harshavardhana 3637aad36e do not count ILM expired objects and other skipped objects (#17184) 2023-05-11 13:35:16 -07:00
Aditya Manthramurthy 77db9686fb Update console to v0.27.0 (#17188) 2023-05-11 12:18:17 -07:00
Shireesh Anjal c326e5a34e Add metrics for webhook endpoint stats (#17179) 2023-05-11 11:24:37 -07:00
jiuker c23c982593 xmlDecoder err use ErrMalformedXML when PutBucketACLHandler (#17185) 2023-05-11 11:11:15 -07:00
Shireesh Anjal a3d666356c fix: error in capturing XFS error config in health report (#17176) 2023-05-10 15:20:48 -07:00
jiuker 3cdbc2f414 add validationErr to validateConfig When DeleteIdentityProviderCfg (#17173) 2023-05-10 09:37:30 -07:00
Harshavardhana b92cdea578 fix: start using pkg/workers to spawn parallel workers (#17170) 2023-05-09 16:37:31 -07:00
jiuker 5e629a99af fix: for profiling duration parsing error reply use ErrInvalidRequest (#17169) 2023-05-09 14:27:49 -07:00
Klaus Post 99c4ffa34f fix: avoid audit log race protection deadlocks (#17168) 2023-05-09 08:11:32 -07:00
Harshavardhana a7f266c907 allow JWT parsing on large session policy based tokens (#17167) 2023-05-09 00:53:08 -07:00
Praveen raj Mani 57acacd5a7 Support persistent queue store for loggers (#17121) 2023-05-08 21:20:31 -07:00
Han Cen 42fb3cd95e helm: fix pod annotations indentation (#17130) 2023-05-08 07:59:56 -07:00
mstein11 855ed642c3 helm: allow postjob to run without user 1000 (#17160) 2023-05-08 07:59:25 -07:00
jiuker 629503ff73 add Err to BucketExists when NoSuchBucket (#17155) 2023-05-08 07:51:59 -07:00
jiuker e3a070e3de put *msgp.Reader back to pool (#17156) 2023-05-08 07:51:39 -07:00
Anton Lindholm 5b364bca1f helm-chart: Use minio service account for post-deploy job if available (#17077) 2023-05-06 23:12:56 -07:00
Poorna c5c1426262 Validate if replication config being added is self referential (#17142) 2023-05-06 13:35:43 -07:00
Denis Krivenko be18d435a2 helm: declare missing properties in values.yaml (#17153) 2023-05-06 13:34:58 -07:00
mstein11 7eea6cdb12 add etc-path to post-job.yaml in helm chart (#17148) 2023-05-06 13:34:38 -07:00
Harshavardhana 824c55b3a4 fix: few typos and wordings in minio-limits.md 2023-05-05 20:04:52 -07:00
Klaus Post 76913a9fd5 Signed trailers for signature v4 (#16484) 2023-05-05 19:53:12 -07:00
Minio Trusted 2f44dac14f Update yaml files to latest version RELEASE.2023-05-04T21-44-30Z 2023-05-05 06:21:31 +00:00
Harshavardhana 5569acd95c disallow EC:0 if not set during server startup (#17141) 2023-05-04 14:44:30 -07:00
Harshavardhana 1d0211d395 allow deletes on directory objects to perform permanent deletes (#17132) 2023-05-04 14:43:52 -07:00
Anis Eleuch 06cd0a636e Avoid calling KES Status when peers ping each other (#17140) 2023-05-04 11:28:33 -07:00
Klaus Post 7f7b489a3d snowball: use latest time when mtime is missing (#17133) 2023-05-04 07:29:33 -07:00
Klaus Post bb6f4d7633 Remove redundant checkFormatJSON logging (#17134) 2023-05-04 07:28:37 -07:00
Alex 6e24dff26a Added MINIO_BROWSER_LOGIN_ANIMATION env support for WebUI console (#17123)
Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
2023-05-03 15:32:50 -07:00
Harshavardhana e372e4e592 add nodeName to the log while taking drive offline (#17124) 2023-05-03 15:05:45 -07:00
Harshavardhana 9571b0825e add configurable VRF interface and user-timeout (#17108) 2023-05-03 14:12:25 -07:00
Poorna 90e2cc3d4c Add audit logging of site replication multipart proxying (#17122) 2023-05-03 11:19:45 -07:00
Alex 0c0820caef Updated Console version to v0.26.4 (#17119)
Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
2023-05-03 11:00:23 -07:00
Harshavardhana 9112ca4e29 change ttfb_distribution metrics to histogramMetric (#17115) 2023-05-03 07:31:00 -07:00
Dylan Piergies 8203cb9990 helm: apply templating to Ingress host and environment variable values (#17073) 2023-05-02 23:27:17 -07:00
Harshavardhana d5bce978a8 upgrade helm to v5.0.9
Signed-off-by: Harshavardhana <harsha@minio.io>
2023-05-02 23:23:26 -07:00
Poorna ec84bad882 batch replication now supports arbitrary S3 targets (#17113) 2023-05-02 22:52:35 -07:00
Harshavardhana b53376a3a4 change directory objects to never create new versions (#17109) 2023-05-02 16:09:33 -07:00
Krishnan Parthasarathi 0ec722bc54 Add tags to NewerNoncurrentVersions audit event (#17110) 2023-05-02 12:56:33 -07:00
Anis Eleuch 4640b13c66 Use expontential backoff algo for internode reconnections (#17052) 2023-05-02 12:35:52 -07:00
Praveen raj Mani 1704abaf6b fix: store notification events immediately for persistent queues (#17112) 2023-05-02 07:53:13 -07:00
WGH ab34f0065c Support systemd notify protocol (#17062) 2023-05-01 23:15:08 -07:00
Klaus Post e8c0a50862 optimization use small blocks up to 64KB (#17107) 2023-05-01 09:47:49 -07:00
Denis Krivenko b963f69f34 helm: align chart properties with naming convention (#17065) 2023-04-29 23:59:40 -07:00
Harshavardhana 02d8f3cdc8 fix: remove active healing on .minio.sys/ during startup (#17072) 2023-04-29 02:05:28 -07:00
Harshavardhana 7ae69accc0 allow root user to be disabled via config settings (#17089) 2023-04-28 12:24:14 -07:00
Minio Trusted 701b89f377 Update yaml files to latest version RELEASE.2023-04-28T18-11-17Z 2023-04-28 18:48:25 +00:00
Anis Eleuch 46d45a6923 grafana: Add TCP dial errors panel (#17101) 2023-04-28 11:11:17 -07:00
Klaus Post 7fad0c8b41 Remove checksums from HTTP range request, add part checksums (#17105) 2023-04-28 08:26:32 -07:00
jiuker 6e27264c6b update cleanupRoutine comment (#17102) 2023-04-28 01:11:51 -07:00
Anis Eleuch 5c83c9724f audit: Add request path and host to audit event (#17099) 2023-04-27 22:18:24 -07:00
Anis Eleuch d5aff735be info: Add drives per set and sets count per pool information (#17100) 2023-04-27 15:24:03 -07:00
Poorna 98c26df53e fix: allow past retention headers to be copied in batch replication (#17095) 2023-04-27 13:43:18 -07:00
Anis Eleuch 2448a9e047 grafana: Remove minio_s3_requests_errors_total metric (#17094) 2023-04-27 10:55:30 -07:00
jiuker b28d391a22 fix: add correct worker count before startHTTPLogger() (#17091) 2023-04-27 10:51:16 -07:00
jiuker c8b92f6067 protect wg.Done from being called twice (#17075) 2023-04-27 07:55:36 -07:00
Krishnan Parthasarathi e7cac8acef Add tags to auditLogLifecycle (#17081) 2023-04-26 17:49:00 -07:00
Anis Eleuch 31b5acc245 tcp: Increase user timeout to 10 minutes (#17087) 2023-04-26 17:48:31 -07:00
Harshavardhana 6105997299 remove unnecessary log when listing resume fails (#17086) 2023-04-26 14:53:25 -07:00
Harshavardhana 8c874884fc fix: do not copy context in DiskInfo cache (#17085) 2023-04-26 12:13:54 -07:00
Aditya Manthramurthy ebfe81e5fd Fix put bucket policy error code (#17084) 2023-04-26 11:21:27 -07:00
Anis Eleuch 0b7ca094e4 Remove Expect 100-continue in internode communications (#17061) 2023-04-26 09:33:45 -07:00
Praveen raj Mani 72802a5972 Use 'minio/pkg/sync/errgroup' and 'minio/pkg/workers' (#17069) 2023-04-25 22:57:40 -07:00
Harshavardhana b1f3935c5b allow ListObjects() when a prefix is an object (#17074) 2023-04-25 22:41:54 -07:00
Harshavardhana dbd53af369 fix: initialize reverse proxy forwarder with right public certs (#17080) 2023-04-25 15:50:32 -07:00
Harshavardhana b09fe0e50e fix: DeleteBucket for peers() must recreate bucket upon errors (#17079) 2023-04-25 14:16:35 -07:00
Krishnan Parthasarathi fae9000304 heal: Pick maximally occuring modTime in quorum (#17071) 2023-04-25 10:13:57 -07:00
Harshavardhana 8fd07bcd51 simplify sort.Sort by using sort.Slice (#17066) 2023-04-24 13:28:18 -07:00
Anis Eleuch 6addc7a35d server-info: Return initializing state properly (#17070) 2023-04-24 09:10:02 -07:00
Harshavardhana 477230c82e avoid attempting to migrate old configs (#17004) 2023-04-21 13:56:08 -07:00
Harshavardhana d1737199ed fix: delete DNS upon success, update failure message (#17059) 2023-04-21 12:12:31 -07:00
Jiffs Maverick 61101d82d9 Rename inodes metric in grafana dashboards (#17030) 2023-04-21 11:07:30 -07:00
Minio Trusted cebb948da2 Update yaml files to latest version RELEASE.2023-04-20T17-56-55Z 2023-04-21 08:00:05 +00:00
Shireesh Anjal c61c4b71b2 Upgrade madmin-go to 2.0.20 (#17054) 2023-04-20 10:56:55 -07:00
Harshavardhana 84f31ed45d simplify MRF, converge it to regular healing (#17026) 2023-04-19 07:47:42 -07:00
jiuker 8a81e317d6 verify maxPartID in object options helpers (#17015) 2023-04-18 22:34:30 -07:00
Anis Eleuch 224d9a752f fix: the race in healing tracker code (#17048) 2023-04-18 14:49:56 -07:00
Anis Eleuch 0db34e4b85 Listen bucket events to send empty events with new line (#17037) 2023-04-18 08:11:30 -07:00
Harshavardhana 8a9b9832fd add Dial timeout for Kafka broker pings (#17044) 2023-04-17 15:45:01 -07:00
Klaus Post f66625be67 Snowball: Extract headers for metadata (#17042) 2023-04-17 12:16:54 -07:00
Harshavardhana 6825bd7e75 fix: inlined objects don't need to honor long locks (#17039) 2023-04-17 12:16:37 -07:00
Denis Krivenko 18515a4e3b Reformat helm chart templates (#16947) 2023-04-16 23:04:15 -07:00
Klaus Post 839b9c9271 Reduce allocations in Walkdir (#17036) 2023-04-15 10:25:25 -07:00
Harshavardhana dd9ed85e22 implement support for FTP/SFTP server (#16952) 2023-04-15 07:34:02 -07:00
jiuker e96c88e914 fix: DeleteBucketThrottle must delete ARN (#17034) 2023-04-15 02:14:26 -07:00
jiuker 6c1410f7f5 fix: Type of rejection for FIFO quota input (#17016) 2023-04-15 01:22:18 -07:00
Krishnan Parthasarathi f92450d8b3 commonParity should pick readable FileInfo (#17032) 2023-04-14 16:23:28 -07:00
Klaus Post c133979b8e Add part count to checksum (#17035) 2023-04-14 09:44:45 -07:00
Poorna a9269cee29 heal: avoid logging version not found (#17031) 2023-04-13 19:45:52 -07:00
Harshavardhana cf42ede92c upgrade helm to v5.0.8
Signed-off-by: Harshavardhana <harsha@minio.io>
2023-04-13 14:49:51 -07:00
Klaus Post 958a480e53 fix: lambda function expiration when cred.Expiration is set (#17029) 2023-04-13 08:10:57 -07:00
吴小白 62151a751d build: support loong64 (#17027) 2023-04-13 08:10:23 -07:00
Minio Trusted f1ab9df2ee Update yaml files to latest version RELEASE.2023-04-13T03-08-07Z 2023-04-13 07:28:46 +00:00
Anis Eleuch a42650c065 Add minio_bucket_usage_version_total metric to Prometheus (#17023) 2023-04-12 20:08:07 -07:00
Harshavardhana bdad3730f7 fix: do not error out if the local bucket is missing (#17025) 2023-04-12 15:44:16 -07:00
Harshavardhana a5835cecbf fix: regression in counting total requests (#17024) 2023-04-12 14:37:19 -07:00
Denis Krivenko b19620b324 Remove redundant namespace from the helm chart (#16968) 2023-04-11 21:09:29 -07:00
Poorna cd6dec49c0 Add trace support for ilm activity (#16993) 2023-04-11 19:22:32 -07:00
Poorna d350654aee config: fix duplication of replication priority key (#17014) 2023-04-11 19:22:10 -07:00
Krishnan Parthasarathi 6877578bbc Update minio_node_bucket_scans_finished metrics (#17006) 2023-04-11 19:21:34 -07:00
Kunal Singh 10693fddfa docs: fix repetition in wordings (#16999) 2023-04-11 13:23:14 -07:00
Harshavardhana f3682b6149 allow writes to pools with inconsistent xl.meta (#17008) 2023-04-11 11:17:46 -07:00
ferhat elmas 056ca0c68e refactor: reuse open file in storage interface (#16970) 2023-04-09 23:09:28 -07:00
Krishnan Parthasarathi 25f7a8e406 Indicate RenameData is called by healObject (#16997) 2023-04-09 10:25:37 -07:00
Anis Eleuch 1f1c267b6c Add used inodes to disk info (#16994) 2023-04-07 07:52:14 -07:00
ferhat elmas eab1dc927b chore: fix automatic issue handling from linter in ci (#16969) 2023-04-07 07:51:31 -07:00
Anis Eleuch fc94ea1ced trace: Fix <unknown> func name of requests rejected by max clients (#16977) 2023-04-07 07:51:12 -07:00
jiuker 67d2cf8f30 add Err to Get getRemoteTargetClient. (#16982) 2023-04-07 07:50:46 -07:00
Minio Trusted 2c85b84cbc Update yaml files to latest version RELEASE.2023-04-07T05-28-58Z 2023-04-07 06:07:27 +00:00
Harshavardhana 09a25ea7b7 lint: fix some lint issues on files 2023-04-06 22:42:10 -07:00
377 changed files with 14799 additions and 9352 deletions
-5
View File
@@ -1,5 +0,0 @@
# Config file for markdownlint-cli
MD033:
allowed_elements:
- details
- summary
-30
View File
@@ -1,30 +0,0 @@
name: Markdown Linter
on:
pull_request:
branches:
- master
# This ensures that previous jobs for the PR are canceled when the PR is
# updated.
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
lint:
name: Lint all docs
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Lint all docs
run: |
npm install -g markdownlint-cli
markdownlint --fix '**/*.md' \
--config /home/runner/work/minio/minio/.github/markdown-lint-cfg.yaml \
--disable MD013 MD040 MD051
@@ -11,8 +11,9 @@ x-minio-common: &minio-common
MINIO_CI_CD: "on" MINIO_CI_CD: "on"
MINIO_ROOT_USER: "minio" MINIO_ROOT_USER: "minio"
MINIO_ROOT_PASSWORD: "minio123" MINIO_ROOT_PASSWORD: "minio123"
MINIO_COMPRESS: "true" MINIO_COMPRESSION_ENABLE: "on"
MINIO_COMPRESS_MIMETYPES: "*" MINIO_COMPRESSION_MIME_TYPES: "*"
MINIO_COMPRESSION_ALLOW_ENCRYPTION: "on"
MINIO_KMS_SECRET_KEY: "my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw=" MINIO_KMS_SECRET_KEY: "my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw="
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
@@ -11,8 +11,6 @@ x-minio-common: &minio-common
MINIO_CI_CD: "on" MINIO_CI_CD: "on"
MINIO_ROOT_USER: "minio" MINIO_ROOT_USER: "minio"
MINIO_ROOT_PASSWORD: "minio123" MINIO_ROOT_PASSWORD: "minio123"
MINIO_COMPRESS: "true"
MINIO_COMPRESS_MIMETYPES: "*"
MINIO_KMS_SECRET_KEY: "my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw=" MINIO_KMS_SECRET_KEY: "my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw="
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
+34
View File
@@ -0,0 +1,34 @@
name: Root lockdown tests
on:
pull_request:
branches:
- master
# This ensures that previous jobs for the PR are canceled when the PR is
# updated.
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
build:
name: Go ${{ matrix.go-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
go-version: [1.20.x]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
check-latest: true
- name: Start root lockdown tests
run: |
make test-root-disable
+5 -18
View File
@@ -8,8 +8,9 @@ export SECRET_KEY="$3"
export JOB_NAME="$4" export JOB_NAME="$4"
export MINT_MODE="full" export MINT_MODE="full"
docker system prune -f docker system prune -f || true
docker volume prune -f docker volume prune -f || true
docker volume rm $(docker volume ls -f dangling=true) || true
## change working directory ## change working directory
cd .github/workflows/mint cd .github/workflows/mint
@@ -24,28 +25,14 @@ docker run --rm --net=host \
-e SECRET_KEY="${SECRET_KEY}" \ -e SECRET_KEY="${SECRET_KEY}" \
-e ENABLE_HTTPS=0 \ -e ENABLE_HTTPS=0 \
-e MINT_MODE="${MINT_MODE}" \ -e MINT_MODE="${MINT_MODE}" \
docker.io/minio/mint:edge \ docker.io/minio/mint:edge
aws-sdk-go \
aws-sdk-java \
aws-sdk-php \
aws-sdk-ruby \
awscli \
healthcheck \
mc \
minio-go \
minio-java \
minio-js \
minio-py \
s3cmd \
s3select \
versioning
docker-compose -f minio-${MODE}.yaml down || true docker-compose -f minio-${MODE}.yaml down || true
sleep 10s sleep 10s
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 -f dangling=true) || true
## change working directory ## change working directory
cd ../../../ cd ../../../
+22
View File
@@ -0,0 +1,22 @@
name: Shell formatting checks
on:
pull_request:
branches:
- master
permissions:
contents: read
jobs:
build:
name: runner / shfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: luizm/action-sh-checker@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SHFMT_OPTS: "-s"
with:
sh_checker_shellcheck_disable: true # disable for now
+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.20.x go-version: 1.19.10
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
+1510 -423
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -1,4 +1,4 @@
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.7 FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8
ARG RELEASE ARG RELEASE
@@ -27,9 +27,8 @@ COPY LICENSE /licenses/LICENSE
RUN \ RUN \
microdnf clean all && \ microdnf clean all && \
microdnf update --nodocs && \ microdnf update --nodocs && \
microdnf install curl ca-certificates shadow-utils util-linux --nodocs && \
rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm && \ rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm && \
microdnf install minisign --nodocs && \ microdnf install curl ca-certificates shadow-utils util-linux gzip lsof tar net-tools iproute iputils jq minisign --nodocs && \
mkdir -p /opt/bin && chmod -R 777 /opt/bin && \ mkdir -p /opt/bin && chmod -R 777 /opt/bin && \
curl -s -q https://dl.min.io/server/minio/hotfixes/linux-amd64/archive/minio.${RELEASE} -o /opt/bin/minio && \ curl -s -q https://dl.min.io/server/minio/hotfixes/linux-amd64/archive/minio.${RELEASE} -o /opt/bin/minio && \
curl -s -q https://dl.min.io/server/minio/hotfixes/linux-amd64/archive/minio.${RELEASE}.sha256sum -o /opt/bin/minio.sha256sum && \ curl -s -q https://dl.min.io/server/minio/hotfixes/linux-amd64/archive/minio.${RELEASE}.sha256sum -o /opt/bin/minio.sha256sum && \
+2 -3
View File
@@ -1,4 +1,4 @@
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.7 FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8
ARG TARGETARCH ARG TARGETARCH
@@ -29,9 +29,8 @@ COPY LICENSE /licenses/LICENSE
RUN \ RUN \
microdnf clean all && \ microdnf clean all && \
microdnf update --nodocs && \ microdnf update --nodocs && \
microdnf install curl ca-certificates shadow-utils util-linux gzip lsof tar net-tools --nodocs && \
rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm && \ rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm && \
microdnf install minisign --nodocs && \ microdnf install curl ca-certificates shadow-utils util-linux gzip lsof tar net-tools iproute iputils jq minisign --nodocs && \
mkdir -p /opt/bin && chmod -R 777 /opt/bin && \ mkdir -p /opt/bin && chmod -R 777 /opt/bin && \
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE} -o /opt/bin/minio && \ curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE} -o /opt/bin/minio && \
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.sha256sum -o /opt/bin/minio.sha256sum && \ curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.sha256sum -o /opt/bin/minio.sha256sum && \
+2 -3
View File
@@ -1,4 +1,4 @@
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.7 FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8
ARG TARGETARCH ARG TARGETARCH
@@ -29,9 +29,8 @@ COPY LICENSE /licenses/LICENSE
RUN \ RUN \
microdnf clean all && \ microdnf clean all && \
microdnf update --nodocs && \ microdnf update --nodocs && \
microdnf install curl ca-certificates shadow-utils util-linux --nodocs && \
rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm && \ rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm && \
microdnf install minisign --nodocs && \ microdnf install curl ca-certificates shadow-utils util-linux gzip lsof tar net-tools iproute iputils jq minisign --nodocs && \
mkdir -p /opt/bin && chmod -R 777 /opt/bin && \ mkdir -p /opt/bin && chmod -R 777 /opt/bin && \
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.fips -o /opt/bin/minio && \ curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.fips -o /opt/bin/minio && \
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.fips.sha256sum -o /opt/bin/minio.sha256sum && \ curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.fips.sha256sum -o /opt/bin/minio.sha256sum && \
+12 -1
View File
@@ -37,6 +37,10 @@ check-gen: ## check for updated autogenerated files
@(! git diff --name-only | grep '_gen.go$$') || (echo "Non-committed changes in auto-generated code is detected, please commit them to proceed." && false) @(! git diff --name-only | grep '_gen.go$$') || (echo "Non-committed changes in auto-generated code is detected, please commit them to proceed." && false)
lint: getdeps ## runs golangci-lint suite of linters lint: getdeps ## runs golangci-lint suite of linters
@echo "Running $@ check"
@$(GOLANGCI) run --build-tags kqueue --timeout=10m --config ./.golangci.yml
lint-fix: getdeps ## runs golangci-lint suite of linters with automatic fixes
@echo "Running $@ check" @echo "Running $@ check"
@$(GOLANGCI) run --build-tags kqueue --timeout=10m --config ./.golangci.yml --fix @$(GOLANGCI) run --build-tags kqueue --timeout=10m --config ./.golangci.yml --fix
@@ -45,6 +49,10 @@ test: verifiers build ## 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
@echo "Running minio root lockdown tests"
@env bash $(PWD)/buildscripts/disable-root.sh
test-decom: install test-decom: install
@echo "Running minio decom tests" @echo "Running minio decom tests"
@env bash $(PWD)/docs/distributed/decom.sh @env bash $(PWD)/docs/distributed/decom.sh
@@ -66,6 +74,9 @@ test-iam: build ## verify IAM (external IDP, etcd backends)
@echo "Running tests for IAM (external IDP, etcd backends) with -race" @echo "Running tests for IAM (external IDP, etcd backends) with -race"
@MINIO_API_REQUESTS_MAX=10000 GORACE=history_size=7 CGO_ENABLED=1 go test -race -tags kqueue -v -run TestIAM* ./cmd @MINIO_API_REQUESTS_MAX=10000 GORACE=history_size=7 CGO_ENABLED=1 go test -race -tags kqueue -v -run TestIAM* ./cmd
test-sio-error:
@(env bash $(PWD)/docs/bucket/replication/sio-error.sh)
test-replication-2site: test-replication-2site:
@(env bash $(PWD)/docs/bucket/replication/setup_2site_existing_replication.sh) @(env bash $(PWD)/docs/bucket/replication/setup_2site_existing_replication.sh)
@@ -75,7 +86,7 @@ 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 ## verify multi site replication test-replication: install 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 ## verify automatic site replication
-2
View File
@@ -86,8 +86,6 @@ chmod +x minio
./minio server /data ./minio server /data
``` ```
Replace ``/data`` with the path to the drive or directory in which you want MinIO to store data.
The following table lists supported architectures. Replace the `wget` URL with the architecture for your Linux host. The following table lists supported architectures. Replace the `wget` URL with the architecture for your Linux host.
| Architecture | URL | | Architecture | URL |
+10 -9
View File
@@ -30,20 +30,19 @@ _init() {
readlink() { readlink() {
TARGET_FILE=$1 TARGET_FILE=$1
cd `dirname $TARGET_FILE` cd $(dirname $TARGET_FILE)
TARGET_FILE=`basename $TARGET_FILE` TARGET_FILE=$(basename $TARGET_FILE)
# Iterate down a (possible) chain of symlinks # Iterate down a (possible) chain of symlinks
while [ -L "$TARGET_FILE" ] while [ -L "$TARGET_FILE" ]; do
do
TARGET_FILE=$(env readlink $TARGET_FILE) TARGET_FILE=$(env readlink $TARGET_FILE)
cd `dirname $TARGET_FILE` cd $(dirname $TARGET_FILE)
TARGET_FILE=`basename $TARGET_FILE` TARGET_FILE=$(basename $TARGET_FILE)
done done
# Compute the canonicalized name by finding the physical path # Compute the canonicalized name by finding the physical path
# for the directory we're in and appending the target file. # for the directory we're in and appending the target file.
PHYS_DIR=`pwd -P` PHYS_DIR=$(pwd -P)
RESULT=$PHYS_DIR/$TARGET_FILE RESULT=$PHYS_DIR/$TARGET_FILE
echo $RESULT echo $RESULT
} }
@@ -75,12 +74,13 @@ check_minimum_version() {
assert_is_supported_arch() { assert_is_supported_arch() {
case "${ARCH}" in case "${ARCH}" in
x86_64 | amd64 | aarch64 | ppc64le | arm* | s390x ) x86_64 | amd64 | aarch64 | ppc64le | arm* | s390x | loong64 | loongarch64)
return return
;; ;;
*) *)
echo "Arch '${ARCH}' is not supported. Supported Arch: [x86_64, amd64, aarch64, ppc64le, arm*, s390x]" echo "Arch '${ARCH}' is not supported. Supported Arch: [x86_64, amd64, aarch64, ppc64le, arm*, s390x, loong64, loongarch64]"
exit 1 exit 1
;;
esac esac
} }
@@ -100,6 +100,7 @@ assert_is_supported_os() {
*) *)
echo "OS '${KNAME}' is not supported. Supported OS: [Linux, FreeBSD, OpenBSD, NetBSD, Darwin, DragonFly]" echo "OS '${KNAME}' is not supported. Supported OS: [Linux, FreeBSD, OpenBSD, NetBSD, Darwin, DragonFly]"
exit 1 exit 1
;;
esac esac
} }
+1 -1
View File
@@ -9,7 +9,7 @@ function _init() {
export CGO_ENABLED=0 export CGO_ENABLED=0
## List of architectures and OS to test coss compilation. ## List of architectures and OS to test coss compilation.
SUPPORTED_OSARCH="linux/ppc64le linux/mips64 linux/amd64 linux/arm64 linux/s390x darwin/arm64 darwin/amd64 freebsd/amd64 windows/amd64 linux/arm linux/386 netbsd/amd64 linux/mips openbsd/amd64" SUPPORTED_OSARCH="linux/ppc64le linux/mips64 linux/amd64 linux/arm64 linux/s390x darwin/arm64 darwin/amd64 freebsd/amd64 windows/amd64 linux/arm linux/386 netbsd/amd64 linux/mips"
} }
function _build() { function _build() {
+119
View File
@@ -0,0 +1,119 @@
#!/bin/bash
set -x
export MINIO_CI_CD=1
killall -9 minio
rm -rf ${HOME}/tmp/dist
scheme="http"
nr_servers=4
addr="localhost"
args=""
for ((i = 0; i < $((nr_servers)); i++)); do
args="$args $scheme://$addr:$((9100 + i))/${HOME}/tmp/dist/path1/$i"
done
echo $args
for ((i = 0; i < $((nr_servers)); i++)); do
(minio server --address ":$((9100 + i))" $args 2>&1 >/tmp/log$i.txt) &
done
sleep 10s
if [ ! -f ./mc ]; then
wget --quiet -O ./mc https://dl.minio.io/client/mc/release/linux-amd64/./mc &&
chmod +x mc
fi
set +e
export MC_HOST_minioadm=http://minioadmin:minioadmin@localhost:9100/
./mc ls minioadm/
./mc admin config set minioadm/ api root_access=off
sleep 3s # let things settle a little
./mc ls minioadm/
if [ $? -eq 0 ]; then
echo "listing succeeded, 'minioadmin' was not disabled"
exit 1
fi
set -e
killall -9 minio
export MINIO_API_ROOT_ACCESS=on
for ((i = 0; i < $((nr_servers)); i++)); do
(minio server --address ":$((9100 + i))" $args 2>&1 >/tmp/log$i.txt) &
done
set +e
./mc ls minioadm/
if [ $? -ne 0 ]; then
echo "listing failed, 'minioadmin' should be enabled"
exit 1
fi
killall -9 minio
rm -rf /tmp/multisitea/
rm -rf /tmp/multisiteb/
echo "Setup site-replication and then disable root credentials"
minio server --address 127.0.0.1:9001 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \
"http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_1.log 2>&1 &
minio server --address 127.0.0.1:9002 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \
"http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_2.log 2>&1 &
minio server --address 127.0.0.1:9003 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \
"http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_1.log 2>&1 &
minio server --address 127.0.0.1:9004 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \
"http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_2.log 2>&1 &
sleep 20s
export MC_HOST_sitea=http://minioadmin:minioadmin@127.0.0.1:9001
export MC_HOST_siteb=http://minioadmin:minioadmin@127.0.0.1:9004
./mc admin replicate add sitea siteb
./mc admin user add sitea foobar foo12345
./mc admin policy attach sitea/ consoleAdmin --user=foobar
./mc admin user info siteb foobar
killall -9 minio
echo "turning off root access, however site replication must continue"
export MINIO_API_ROOT_ACCESS=off
minio server --address 127.0.0.1:9001 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \
"http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_1.log 2>&1 &
minio server --address 127.0.0.1:9002 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \
"http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_2.log 2>&1 &
minio server --address 127.0.0.1:9003 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \
"http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_1.log 2>&1 &
minio server --address 127.0.0.1:9004 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \
"http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_2.log 2>&1 &
sleep 20s
export MC_HOST_sitea=http://foobar:foo12345@127.0.0.1:9001
export MC_HOST_siteb=http://foobar:foo12345@127.0.0.1:9004
./mc admin user add sitea foobar-admin foo12345
sleep 2s
./mc admin user info siteb foobar-admin
+1 -2
View File
@@ -82,8 +82,7 @@ function main() {
start_minio_4drive ${start_port} start_minio_4drive ${start_port}
} }
function purge() function purge() {
{
rm -rf "$1" rm -rf "$1"
} }
+1 -1
View File
@@ -27,7 +27,7 @@ import (
"os" "os"
"time" "time"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
) )
func main() { func main() {
+5 -5
View File
@@ -12,14 +12,14 @@ cleanup() {
verify_checksum_after_heal() { verify_checksum_after_heal() {
local sum1 local sum1
sum1=$(curl -s "$2" | sha256sum); sum1=$(curl -s "$2" | sha256sum)
mc admin heal --json -r "$1" >/dev/null; # test after healing mc admin heal --json -r "$1" >/dev/null # test after healing
local sum1_heal local sum1_heal
sum1_heal=$(curl -s "$2" | sha256sum); sum1_heal=$(curl -s "$2" | sha256sum)
if [ "${sum1_heal}" != "${sum1}" ]; then if [ "${sum1_heal}" != "${sum1}" ]; then
echo "mismatch expected ${sum1_heal}, got ${sum1}" echo "mismatch expected ${sum1_heal}, got ${sum1}"
exit 1; exit 1
fi fi
} }
@@ -31,7 +31,7 @@ verify_checksum_mc() {
if [ "${expected}" != "${got}" ]; then if [ "${expected}" != "${got}" ]; then
echo "mismatch - expected ${expected}, got ${got}" echo "mismatch - expected ${expected}, got ${got}"
exit 1; exit 1
fi fi
echo "matches - ${expected}, got ${got}" echo "matches - ${expected}, got ${got}"
} }
+1 -2
View File
@@ -61,8 +61,7 @@ function main() {
start_minio_5drive ${start_port} start_minio_5drive ${start_port}
} }
function purge() function purge() {
{
rm -rf "$1" rm -rf "$1"
} }
+9 -4
View File
@@ -98,7 +98,10 @@ function verify_rewrite() {
cat "${WORK_DIR}/server1.log" cat "${WORK_DIR}/server1.log"
echo "FAILED" echo "FAILED"
mkdir -p inspects mkdir -p inspects
(cd inspects; "${WORK_DIR}/mc" admin inspect minio/healing-rewrite-bucket/verify-build.sh/**) (
cd inspects
"${WORK_DIR}/mc" admin inspect minio/healing-rewrite-bucket/verify-build.sh/**
)
"${WORK_DIR}/mc" mb play/inspects "${WORK_DIR}/mc" mb play/inspects
"${WORK_DIR}/mc" mirror inspects play/inspects "${WORK_DIR}/mc" mirror inspects play/inspects
@@ -120,7 +123,10 @@ function verify_rewrite() {
cat "${WORK_DIR}/server1.log" cat "${WORK_DIR}/server1.log"
echo "FAILED" echo "FAILED"
mkdir -p inspects mkdir -p inspects
(cd inspects; "${WORK_DIR}/mc" admin inspect minio/healing-rewrite-bucket/verify-build.sh/**) (
cd inspects
"${WORK_DIR}/mc" admin inspect minio/healing-rewrite-bucket/verify-build.sh/**
)
"${WORK_DIR}/mc" mb play/inspects "${WORK_DIR}/mc" mb play/inspects
"${WORK_DIR}/mc" mirror inspects play/inspects "${WORK_DIR}/mc" mirror inspects play/inspects
@@ -140,8 +146,7 @@ function main() {
verify_rewrite ${start_port} verify_rewrite ${start_port}
} }
function purge() function purge() {
{
rm -rf "$1" rm -rf "$1"
} }
+9 -4
View File
@@ -120,7 +120,10 @@ function start_minio_16drive() {
cat "${WORK_DIR}/server1.log" cat "${WORK_DIR}/server1.log"
echo "FAILED" echo "FAILED"
mkdir -p inspects mkdir -p inspects
(cd inspects; "${WORK_DIR}/mc" support inspect minio/healing-shard-bucket/unaligned/**) (
cd inspects
"${WORK_DIR}/mc" support inspect minio/healing-shard-bucket/unaligned/**
)
"${WORK_DIR}/mc" mb play/inspects "${WORK_DIR}/mc" mb play/inspects
"${WORK_DIR}/mc" mirror inspects play/inspects "${WORK_DIR}/mc" mirror inspects play/inspects
@@ -140,7 +143,10 @@ function start_minio_16drive() {
cat "${WORK_DIR}/server1.log" cat "${WORK_DIR}/server1.log"
echo "FAILED" echo "FAILED"
mkdir -p inspects mkdir -p inspects
(cd inspects; "${WORK_DIR}/mc" support inspect minio/healing-shard-bucket/unaligned/**) (
cd inspects
"${WORK_DIR}/mc" support inspect minio/healing-shard-bucket/unaligned/**
)
"${WORK_DIR}/mc" mb play/inspects "${WORK_DIR}/mc" mb play/inspects
"${WORK_DIR}/mc" mirror inspects play/inspects "${WORK_DIR}/mc" mirror inspects play/inspects
@@ -161,8 +167,7 @@ function main() {
start_minio_16drive ${start_port} start_minio_16drive ${start_port}
} }
function purge() function purge() {
{
rm -rf "$1" rm -rf "$1"
} }
+15 -30
View File
@@ -32,29 +32,25 @@ FILE_65_MB="$MINT_DATA_DIR/datafile-65-MB"
FUNCTIONAL_TESTS="$WORK_DIR/functional-tests.sh" FUNCTIONAL_TESTS="$WORK_DIR/functional-tests.sh"
function start_minio_fs() function start_minio_fs() {
{
export MINIO_ROOT_USER=$ACCESS_KEY export MINIO_ROOT_USER=$ACCESS_KEY
export MINIO_ROOT_PASSWORD=$SECRET_KEY export MINIO_ROOT_PASSWORD=$SECRET_KEY
"${MINIO[@]}" server "${WORK_DIR}/fs-disk" >"$WORK_DIR/fs-minio.log" 2>&1 & "${MINIO[@]}" server "${WORK_DIR}/fs-disk" >"$WORK_DIR/fs-minio.log" 2>&1 &
sleep 10 sleep 10
} }
function start_minio_erasure() function start_minio_erasure() {
{
"${MINIO[@]}" server "${WORK_DIR}/erasure-disk1" "${WORK_DIR}/erasure-disk2" "${WORK_DIR}/erasure-disk3" "${WORK_DIR}/erasure-disk4" >"$WORK_DIR/erasure-minio.log" 2>&1 & "${MINIO[@]}" server "${WORK_DIR}/erasure-disk1" "${WORK_DIR}/erasure-disk2" "${WORK_DIR}/erasure-disk3" "${WORK_DIR}/erasure-disk4" >"$WORK_DIR/erasure-minio.log" 2>&1 &
sleep 15 sleep 15
} }
function start_minio_erasure_sets() function start_minio_erasure_sets() {
{
export MINIO_ENDPOINTS="${WORK_DIR}/erasure-disk-sets{1...32}" export MINIO_ENDPOINTS="${WORK_DIR}/erasure-disk-sets{1...32}"
"${MINIO[@]}" server >"$WORK_DIR/erasure-minio-sets.log" 2>&1 & "${MINIO[@]}" server >"$WORK_DIR/erasure-minio-sets.log" 2>&1 &
sleep 15 sleep 15
} }
function start_minio_pool_erasure_sets() function start_minio_pool_erasure_sets() {
{
export MINIO_ROOT_USER=$ACCESS_KEY export MINIO_ROOT_USER=$ACCESS_KEY
export MINIO_ROOT_PASSWORD=$SECRET_KEY export MINIO_ROOT_PASSWORD=$SECRET_KEY
export MINIO_ENDPOINTS="http://127.0.0.1:9000${WORK_DIR}/pool-disk-sets{1...4} http://127.0.0.1:9001${WORK_DIR}/pool-disk-sets{5...8}" export MINIO_ENDPOINTS="http://127.0.0.1:9000${WORK_DIR}/pool-disk-sets{1...4} http://127.0.0.1:9001${WORK_DIR}/pool-disk-sets{5...8}"
@@ -64,8 +60,7 @@ function start_minio_pool_erasure_sets()
sleep 40 sleep 40
} }
function start_minio_pool_erasure_sets_ipv6() function start_minio_pool_erasure_sets_ipv6() {
{
export MINIO_ROOT_USER=$ACCESS_KEY export MINIO_ROOT_USER=$ACCESS_KEY
export MINIO_ROOT_PASSWORD=$SECRET_KEY export MINIO_ROOT_PASSWORD=$SECRET_KEY
export MINIO_ENDPOINTS="http://[::1]:9000${WORK_DIR}/pool-disk-sets-ipv6{1...4} http://[::1]:9001${WORK_DIR}/pool-disk-sets-ipv6{5...8}" export MINIO_ENDPOINTS="http://[::1]:9000${WORK_DIR}/pool-disk-sets-ipv6{1...4} http://[::1]:9001${WORK_DIR}/pool-disk-sets-ipv6{5...8}"
@@ -75,8 +70,7 @@ function start_minio_pool_erasure_sets_ipv6()
sleep 40 sleep 40
} }
function start_minio_dist_erasure() function start_minio_dist_erasure() {
{
export MINIO_ROOT_USER=$ACCESS_KEY export MINIO_ROOT_USER=$ACCESS_KEY
export MINIO_ROOT_PASSWORD=$SECRET_KEY export MINIO_ROOT_PASSWORD=$SECRET_KEY
export MINIO_ENDPOINTS="http://127.0.0.1:9000${WORK_DIR}/dist-disk1 http://127.0.0.1:9001${WORK_DIR}/dist-disk2 http://127.0.0.1:9002${WORK_DIR}/dist-disk3 http://127.0.0.1:9003${WORK_DIR}/dist-disk4" export MINIO_ENDPOINTS="http://127.0.0.1:9000${WORK_DIR}/dist-disk1 http://127.0.0.1:9001${WORK_DIR}/dist-disk2 http://127.0.0.1:9002${WORK_DIR}/dist-disk3 http://127.0.0.1:9003${WORK_DIR}/dist-disk4"
@@ -87,8 +81,7 @@ function start_minio_dist_erasure()
sleep 40 sleep 40
} }
function run_test_fs() function run_test_fs() {
{
start_minio_fs start_minio_fs
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS") (cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
@@ -105,8 +98,7 @@ function run_test_fs()
return "$rv" return "$rv"
} }
function run_test_erasure_sets() function run_test_erasure_sets() {
{
start_minio_erasure_sets start_minio_erasure_sets
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS") (cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
@@ -123,8 +115,7 @@ function run_test_erasure_sets()
return "$rv" return "$rv"
} }
function run_test_pool_erasure_sets() function run_test_pool_erasure_sets() {
{
start_minio_pool_erasure_sets start_minio_pool_erasure_sets
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS") (cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
@@ -147,8 +138,7 @@ function run_test_pool_erasure_sets()
return "$rv" return "$rv"
} }
function run_test_pool_erasure_sets_ipv6() function run_test_pool_erasure_sets_ipv6() {
{
start_minio_pool_erasure_sets_ipv6 start_minio_pool_erasure_sets_ipv6
export SERVER_ENDPOINT="[::1]:9000" export SERVER_ENDPOINT="[::1]:9000"
@@ -173,8 +163,7 @@ function run_test_pool_erasure_sets_ipv6()
return "$rv" return "$rv"
} }
function run_test_erasure() function run_test_erasure() {
{
start_minio_erasure start_minio_erasure
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS") (cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
@@ -191,8 +180,7 @@ function run_test_erasure()
return "$rv" return "$rv"
} }
function run_test_dist_erasure() function run_test_dist_erasure() {
{
start_minio_dist_erasure start_minio_dist_erasure
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS") (cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
@@ -217,13 +205,11 @@ function run_test_dist_erasure()
return "$rv" return "$rv"
} }
function purge() function purge() {
{
rm -rf "$1" rm -rf "$1"
} }
function __init__() function __init__() {
{
echo "Initializing environment" echo "Initializing environment"
mkdir -p "$WORK_DIR" mkdir -p "$WORK_DIR"
mkdir -p "$MINIO_CONFIG_DIR" mkdir -p "$MINIO_CONFIG_DIR"
@@ -256,8 +242,7 @@ function __init__()
chmod a+x "$FUNCTIONAL_TESTS" chmod a+x "$FUNCTIONAL_TESTS"
} }
function main() function main() {
{
echo "Testing in FS setup" echo "Testing in FS setup"
if ! run_test_fs; then if ! run_test_fs; then
echo "FAILED" echo "FAILED"
@@ -4,7 +4,6 @@ set -E
set -o pipefail set -o pipefail
set -x set -x
if [ ! -x "$PWD/minio" ]; then if [ ! -x "$PWD/minio" ]; then
echo "minio executable binary not found in current directory" echo "minio executable binary not found in current directory"
exit 1 exit 1
@@ -14,7 +13,6 @@ WORK_DIR="$(mktemp -d)"
MINIO_CONFIG_DIR="$WORK_DIR/.minio" MINIO_CONFIG_DIR="$WORK_DIR/.minio"
MINIO=("$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" server) MINIO=("$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" server)
function start_minio() { function start_minio() {
start_port=$1 start_port=$1
@@ -26,18 +24,18 @@ function start_minio() {
args=() args=()
for i in $(seq 1 4); do for i in $(seq 1 4); do
args+=("http://localhost:$[${start_port}+$i]${WORK_DIR}/mnt/disk$i/ ") args+=("http://localhost:$((start_port + i))${WORK_DIR}/mnt/disk$i/ ")
done done
for i in $(seq 1 4); do for i in $(seq 1 4); do
"${MINIO[@]}" --address ":$[$start_port+$i]" ${args[@]} 2>&1 >"${WORK_DIR}/server$i.log" & "${MINIO[@]}" --address ":$((start_port + i))" ${args[@]} 2>&1 >"${WORK_DIR}/server$i.log" &
done done
# Wait until all nodes return 403 # Wait until all nodes return 403
for i in $(seq 1 4); do for i in $(seq 1 4); do
while [ "$(curl -m 1 -s -o /dev/null -w "%{http_code}" http://localhost:$[$start_port+$i])" -ne "403" ]; do while [ "$(curl -m 1 -s -o /dev/null -w "%{http_code}" http://localhost:$((start_port + i)))" -ne "403" ]; do
echo -n "."; echo -n "."
sleep 1; sleep 1
done done
done done
@@ -96,4 +94,3 @@ rv=$?
cleanup cleanup
exit "$rv" exit "$rv"
+8 -12
View File
@@ -22,18 +22,18 @@ function start_minio_3_node() {
start_port=$2 start_port=$2
args="" args=""
for i in $(seq 1 3); do for i in $(seq 1 3); do
args="$args http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/1/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/2/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/3/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/4/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/5/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/6/" args="$args http://127.0.0.1:$((start_port + i))${WORK_DIR}/$i/1/ http://127.0.0.1:$((start_port + i))${WORK_DIR}/$i/2/ http://127.0.0.1:$((start_port + i))${WORK_DIR}/$i/3/ http://127.0.0.1:$((start_port + i))${WORK_DIR}/$i/4/ http://127.0.0.1:$((start_port + i))${WORK_DIR}/$i/5/ http://127.0.0.1:$((start_port + i))${WORK_DIR}/$i/6/"
done done
"${MINIO[@]}" --address ":$[$start_port+1]" $args > "${WORK_DIR}/dist-minio-server1.log" 2>&1 & "${MINIO[@]}" --address ":$((start_port + 1))" $args >"${WORK_DIR}/dist-minio-server1.log" 2>&1 &
pid1=$! pid1=$!
disown ${pid1} disown ${pid1}
"${MINIO[@]}" --address ":$[$start_port+2]" $args > "${WORK_DIR}/dist-minio-server2.log" 2>&1 & "${MINIO[@]}" --address ":$((start_port + 2))" $args >"${WORK_DIR}/dist-minio-server2.log" 2>&1 &
pid2=$! pid2=$!
disown $pid2 disown $pid2
"${MINIO[@]}" --address ":$[$start_port+3]" $args > "${WORK_DIR}/dist-minio-server3.log" 2>&1 & "${MINIO[@]}" --address ":$((start_port + 3))" $args >"${WORK_DIR}/dist-minio-server3.log" 2>&1 &
pid3=$! pid3=$!
disown $pid3 disown $pid3
@@ -73,7 +73,7 @@ function start_minio_3_node() {
exit 1 exit 1
fi fi
sleep 1; sleep 1
if pgrep minio; then if pgrep minio; then
# forcibly killing, to proceed further properly. # forcibly killing, to proceed further properly.
if ! pkill -9 minio; then if ! pkill -9 minio; then
@@ -82,20 +82,17 @@ function start_minio_3_node() {
fi fi
} }
function check_online() { function check_online() {
if grep -q 'Unable to initialize sub-systems' ${WORK_DIR}/dist-minio-*.log; then if grep -q 'Unable to initialize sub-systems' ${WORK_DIR}/dist-minio-*.log; then
echo "1" echo "1"
fi fi
} }
function purge() function purge() {
{
rm -rf "$1" rm -rf "$1"
} }
function __init__() function __init__() {
{
echo "Initializing environment" echo "Initializing environment"
mkdir -p "$WORK_DIR" mkdir -p "$WORK_DIR"
mkdir -p "$MINIO_CONFIG_DIR" mkdir -p "$MINIO_CONFIG_DIR"
@@ -127,8 +124,7 @@ function perform_test() {
fi fi
} }
function main() function main() {
{
# use same ports for all tests # use same ports for all tests
start_port=$(shuf -i 10000-65000 -n 1) start_port=$(shuf -i 10000-65000 -n 1)
+1 -1
View File
@@ -91,7 +91,7 @@ func (api objectAPIHandlers) PutBucketACLHandler(w http.ResponseWriter, r *http.
acl := &accessControlPolicy{} acl := &accessControlPolicy{}
if err = xmlDecoder(r.Body, acl, r.ContentLength); err != nil { if err = xmlDecoder(r.Body, acl, r.ContentLength); err != nil {
if terr, ok := err.(*xml.SyntaxError); ok && terr.Msg == io.EOF.Error() { if terr, ok := err.(*xml.SyntaxError); ok && terr.Msg == io.EOF.Error() {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingSecurityHeader), writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedXML),
r.URL) r.URL)
return return
} }
+12 -17
View File
@@ -32,7 +32,7 @@ import (
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"github.com/klauspost/compress/zip" "github.com/klauspost/compress/zip"
"github.com/minio/kes-go" "github.com/minio/kes-go"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio-go/v7/pkg/tags" "github.com/minio/minio-go/v7/pkg/tags"
"github.com/minio/minio/internal/bucket/lifecycle" "github.com/minio/minio/internal/bucket/lifecycle"
objectlock "github.com/minio/minio/internal/bucket/object/lock" objectlock "github.com/minio/minio/internal/bucket/object/lock"
@@ -85,11 +85,6 @@ func (a adminAPIHandlers) PutBucketQuotaConfigHandler(w http.ResponseWriter, r *
return return
} }
if quotaConfig.Type == "fifo" {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
return
}
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, data) updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, data)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
@@ -215,6 +210,11 @@ func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http.
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
return return
} }
if globalSiteReplicationSys.isEnabled() && !update {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrRemoteTargetDenyAddError, err), r.URL)
return
}
if update { if update {
// overlay the updates on existing target // overlay the updates on existing target
tgt := globalBucketTargetSys.GetRemoteBucketTargetByArn(ctx, bucket, target.Arn) tgt := globalBucketTargetSys.GetRemoteBucketTargetByArn(ctx, bucket, target.Arn)
@@ -225,14 +225,14 @@ func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http.
for _, op := range ops { for _, op := range ops {
switch op { switch op {
case madmin.CredentialsUpdateType: case madmin.CredentialsUpdateType:
if globalSiteReplicationSys.isEnabled() { if !globalSiteReplicationSys.isEnabled() {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrRemoteTargetDenyEditError, err), r.URL) // credentials update is possible only in bucket replication. User will never
return // know the site replicator creds.
}
tgt.Credentials = target.Credentials tgt.Credentials = target.Credentials
tgt.TargetBucket = target.TargetBucket tgt.TargetBucket = target.TargetBucket
tgt.Secure = target.Secure tgt.Secure = target.Secure
tgt.Endpoint = target.Endpoint tgt.Endpoint = target.Endpoint
}
case madmin.SyncUpdateType: case madmin.SyncUpdateType:
tgt.ReplicationSync = target.ReplicationSync tgt.ReplicationSync = target.ReplicationSync
case madmin.ProxyUpdateType: case madmin.ProxyUpdateType:
@@ -460,7 +460,7 @@ func (a adminAPIHandlers) ExportBucketMetadataHandler(w http.ResponseWriter, r *
return return
} }
case bucketLifecycleConfig: case bucketLifecycleConfig:
config, err := globalBucketMetadataSys.GetLifecycleConfig(bucket) config, _, err := globalBucketMetadataSys.GetLifecycleConfig(bucket)
if err != nil { if err != nil {
if errors.Is(err, BucketLifecycleNotFound{Bucket: bucket}) { if errors.Is(err, BucketLifecycleNotFound{Bucket: bucket}) {
continue continue
@@ -705,7 +705,7 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *
} }
if _, ok := bucketMap[bucket]; !ok { if _, ok := bucketMap[bucket]; !ok {
opts := MakeBucketOptions{ opts := MakeBucketOptions{
LockEnabled: config.ObjectLockEnabled == "Enabled", LockEnabled: config.Enabled(),
} }
err = objectAPI.MakeBucket(ctx, bucket, opts) err = objectAPI.MakeBucket(ctx, bucket, opts)
if err != nil { if err != nil {
@@ -1032,11 +1032,6 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *
continue continue
} }
if quotaConfig.Type == "fifo" {
rpt.SetStatus(bucket, fileName, fmt.Errorf("Detected older 'fifo' quota config, 'fifo' feature is removed and not supported anymore"))
continue
}
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, data) updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, data)
if err != nil { if err != nil {
rpt.SetStatus(bucket, fileName, err) rpt.SetStatus(bucket, fileName, err)
+10 -6
View File
@@ -24,7 +24,7 @@ import (
"net/http" "net/http"
"github.com/minio/kes-go" "github.com/minio/kes-go"
"github.com/minio/madmin-go/v2" "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/iam/policy" iampolicy "github.com/minio/pkg/iam/policy"
@@ -168,10 +168,16 @@ func toAdminAPIErr(ctx context.Context, err error) APIError {
} }
case errors.Is(err, errPolicyInUse): case errors.Is(err, errPolicyInUse):
apiErr = APIError{ apiErr = APIError{
Code: "XMinioAdminPolicyInUse", Code: "XMinioIAMPolicyInUse",
Description: "The policy cannot be removed, as it is in use", Description: "The policy cannot be removed, as it is in use",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
} }
case errors.Is(err, errSessionPolicyTooLarge):
apiErr = APIError{
Code: "XMinioIAMServiceAccountSessionPolicyTooLarge",
Description: err.Error(),
HTTPStatusCode: http.StatusBadRequest,
}
case errors.Is(err, kes.ErrKeyExists): case errors.Is(err, kes.ErrKeyExists):
apiErr = APIError{ apiErr = APIError{
Code: "XMinioKMSKeyExists", Code: "XMinioKMSKeyExists",
@@ -220,12 +226,10 @@ func toAdminAPIErr(ctx context.Context, err error) APIError {
// toAdminAPIErrCode - converts errErasureWriteQuorum error to admin API // toAdminAPIErrCode - converts errErasureWriteQuorum error to admin API
// specific error. // specific error.
func toAdminAPIErrCode(ctx context.Context, err error) APIErrorCode { func toAdminAPIErrCode(ctx context.Context, err error) APIErrorCode {
switch err { if errors.Is(err, errErasureWriteQuorum) {
case errErasureWriteQuorum:
return ErrAdminConfigNoQuorum return ErrAdminConfigNoQuorum
default:
return toAPIErrorCode(ctx, err)
} }
return toAPIErrorCode(ctx, err)
} }
// wraps export error for more context // wraps export error for more context
+18 -9
View File
@@ -26,7 +26,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/config" "github.com/minio/minio/internal/config"
"github.com/minio/minio/internal/config/cache" "github.com/minio/minio/internal/config/cache"
"github.com/minio/minio/internal/config/etcd" "github.com/minio/minio/internal/config/etcd"
@@ -83,7 +83,7 @@ func (a adminAPIHandlers) DelConfigKVHandler(w http.ResponseWriter, r *http.Requ
return return
} }
if err = validateConfig(cfg, subSys); err != nil { if err = validateConfig(ctx, cfg, subSys); err != nil {
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL) writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
return return
} }
@@ -211,7 +211,12 @@ func setConfigKV(ctx context.Context, objectAPI ObjectLayer, kvBytes []byte) (re
return return
} }
if verr := validateConfig(result.Cfg, result.SubSys); verr != nil { tgts, err := config.ParseConfigTargetID(bytes.NewReader(kvBytes))
if err != nil {
return
}
ctx = context.WithValue(ctx, config.ContextKeyForTargetFromConfig, tgts)
if verr := validateConfig(ctx, result.Cfg, result.SubSys); verr != nil {
err = badConfigErr{Err: verr} err = badConfigErr{Err: verr}
return return
} }
@@ -236,6 +241,8 @@ func setConfigKV(ctx context.Context, objectAPI ObjectLayer, kvBytes []byte) (re
// 1. `subsys:target` -> request for config of a single subsystem and target pair. // 1. `subsys:target` -> request for config of a single subsystem and target pair.
// 2. `subsys:` -> request for config of a single subsystem and the default target. // 2. `subsys:` -> request for config of a single subsystem and the default target.
// 3. `subsys` -> request for config of all targets for the given subsystem. // 3. `subsys` -> request for config of all targets for the given subsystem.
//
// This is a reporting API and config secrets are redacted in the response.
func (a adminAPIHandlers) GetConfigKVHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) GetConfigKVHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "GetConfigKV") ctx := newContext(r, w, "GetConfigKV")
@@ -263,7 +270,7 @@ func (a adminAPIHandlers) GetConfigKVHandler(w http.ResponseWriter, r *http.Requ
} }
} }
subSysConfigs, err := cfg.GetSubsysInfo(subSys, target) subSysConfigs, err := cfg.GetSubsysInfo(subSys, target, 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
@@ -353,7 +360,7 @@ func (a adminAPIHandlers) RestoreConfigHistoryKVHandler(w http.ResponseWriter, r
return return
} }
if err = validateConfig(cfg, ""); err != nil { if err = validateConfig(ctx, cfg, ""); err != nil {
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL) writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
return return
} }
@@ -464,7 +471,7 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
return return
} }
if err = validateConfig(cfg, ""); err != nil { if err = validateConfig(ctx, cfg, ""); err != nil {
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL) writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
return return
} }
@@ -485,7 +492,9 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
} }
// GetConfigHandler - GET /minio/admin/v3/config // GetConfigHandler - GET /minio/admin/v3/config
// Get config.json of this minio setup. //
// This endpoint is mainly for exporting and backing up the configuration.
// Secrets are not redacted.
func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "GetConfig") ctx := newContext(r, w, "GetConfig")
@@ -502,7 +511,7 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
hkvs := config.HelpSubSysMap[""] hkvs := config.HelpSubSysMap[""]
for _, hkv := range hkvs { for _, hkv := range hkvs {
// We ignore the error below, as we cannot get one. // We ignore the error below, as we cannot get one.
cfgSubsysItems, _ := cfg.GetSubsysInfo(hkv.Key, "") cfgSubsysItems, _ := cfg.GetSubsysInfo(hkv.Key, "", false)
for _, item := range cfgSubsysItems { for _, item := range cfgSubsysItems {
off := item.Config.Get(config.Enable) == config.EnableOff off := item.Config.Get(config.Enable) == config.EnableOff
@@ -541,7 +550,7 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
// setLoggerWebhookSubnetProxy - Sets the logger webhook's subnet proxy value to // setLoggerWebhookSubnetProxy - Sets the logger webhook's subnet proxy value to
// one being set for subnet proxy // one being set for subnet proxy
func setLoggerWebhookSubnetProxy(subSys string, cfg config.Config) bool { func setLoggerWebhookSubnetProxy(subSys string, cfg config.Config) bool {
if subSys == config.SubnetSubSys { if subSys == config.SubnetSubSys || subSys == config.LoggerWebhookSubSys {
subnetWebhookCfg := cfg[config.LoggerWebhookSubSys][subnet.LoggerWebhookName] subnetWebhookCfg := cfg[config.LoggerWebhookSubSys][subnet.LoggerWebhookName]
loggerWebhookSubnetProxy := subnetWebhookCfg.Get(logger.Proxy) loggerWebhookSubnetProxy := subnetWebhookCfg.Get(logger.Proxy)
subnetProxy := cfg[config.SubnetSubSys][config.Default].Get(logger.Proxy) subnetProxy := cfg[config.SubnetSubSys][config.Default].Get(logger.Proxy)
+13 -3
View File
@@ -26,7 +26,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/minio/internal/config" "github.com/minio/minio/internal/config"
cfgldap "github.com/minio/minio/internal/config/identity/ldap" cfgldap "github.com/minio/minio/internal/config/identity/ldap"
@@ -125,7 +125,7 @@ func (a adminAPIHandlers) addOrUpdateIDPHandler(ctx context.Context, w http.Resp
return return
} }
if err = validateConfig(cfg, subSys); err != nil { if err = validateConfig(ctx, cfg, subSys); err != nil {
var validationErr ldap.Validation var validationErr ldap.Validation
if errors.As(err, &validationErr) { if errors.As(err, &validationErr) {
@@ -422,7 +422,17 @@ func (a adminAPIHandlers) DeleteIdentityProviderCfg(w http.ResponseWriter, r *ht
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return return
} }
if err = validateConfig(cfg, subSys); err != nil { if err = validateConfig(ctx, cfg, subSys); err != nil {
var validationErr ldap.Validation
if errors.As(err, &validationErr) {
// If we got an LDAP validation error, we need to send appropriate
// error message back to client (likely mc).
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigLDAPValidation),
validationErr.FormatError(), r.URL)
return
}
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL) writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
return return
} }
+2 -2
View File
@@ -22,7 +22,7 @@ import (
"io" "io"
"net/http" "net/http"
"github.com/minio/madmin-go/v2" "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/iam/policy" iampolicy "github.com/minio/pkg/iam/policy"
@@ -150,7 +150,7 @@ func (a adminAPIHandlers) AttachDetachPolicyLDAP(w http.ResponseWriter, r *http.
} }
// Call IAM subsystem // Call IAM subsystem
updatedAt, addedOrRemoved, err := globalIAMSys.PolicyDBUpdateLDAP(ctx, isAttach, par) updatedAt, addedOrRemoved, _, err := globalIAMSys.PolicyDBUpdateLDAP(ctx, isAttach, par)
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return return
+1 -1
View File
@@ -95,7 +95,7 @@ func (a adminAPIHandlers) StartDecommission(w http.ResponseWriter, r *http.Reque
poolIndices = append(poolIndices, idx) poolIndices = append(poolIndices, idx)
} }
if len(poolIndices) > 0 && globalEndpoints[poolIndices[0]].Endpoints[0].IsLocal { if len(poolIndices) > 0 && !globalEndpoints[poolIndices[0]].Endpoints[0].IsLocal {
ep := globalEndpoints[poolIndices[0]].Endpoints[0] ep := globalEndpoints[poolIndices[0]].Endpoints[0]
for nodeIdx, proxyEp := range globalProxyEndpoints { for nodeIdx, proxyEp := range globalProxyEndpoints {
if proxyEp.Endpoint.Host == ep.Host { if proxyEp.Endpoint.Host == ep.Host {
+1 -1
View File
@@ -26,7 +26,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/mux" "github.com/minio/mux"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
+2 -2
View File
@@ -30,9 +30,9 @@ import (
"testing" "testing"
"time" "time"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
minio "github.com/minio/minio-go/v7" minio "github.com/minio/minio-go/v7"
"github.com/minio/minio/internal/sync/errgroup" "github.com/minio/pkg/sync/errgroup"
) )
func runAllIAMConcurrencyTests(suite *TestSuiteIAM, c *check) { func runAllIAMConcurrencyTests(suite *TestSuiteIAM, c *check) {
+126 -224
View File
@@ -26,11 +26,10 @@ import (
"net/http" "net/http"
"os" "os"
"sort" "sort"
"strings"
"time" "time"
"github.com/klauspost/compress/zip" "github.com/klauspost/compress/zip"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/auth"
"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"
@@ -44,7 +43,7 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.DeleteUserAdminAction) objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.DeleteUserAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -62,6 +61,13 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
return return
} }
// When the user is root credential you are not allowed to
// remove the root user. Also you cannot delete yourself.
if accessKey == globalActiveCred.AccessKey || accessKey == cred.AccessKey {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
return
}
if err := globalIAMSys.DeleteUser(ctx, accessKey, true); err != nil { if err := globalIAMSys.DeleteUser(ctx, accessKey, true); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return return
@@ -239,6 +245,26 @@ func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Requ
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
return return
} }
// Reject if the group add and remove are temporary credentials, or root credential.
for _, member := range updReq.Members {
ok, _, err := globalIAMSys.IsTempUser(member)
if err != nil && err != errNoSuchUser {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
if ok {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
return
}
// When the user is root credential you are not allowed to
// add policies for root user.
if member == globalActiveCred.AccessKey {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
return
}
}
var updatedAt time.Time var updatedAt time.Time
if updReq.IsRemove { if updReq.IsRemove {
updatedAt, err = globalIAMSys.RemoveUsersFromGroup(ctx, updReq.Group, updReq.Members) updatedAt, err = globalIAMSys.RemoveUsersFromGroup(ctx, updReq.Group, updReq.Members)
@@ -374,7 +400,7 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request)
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.EnableUserAdminAction) objectAPI, creds := validateAdminReq(ctx, w, r, iampolicy.EnableUserAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
@@ -383,9 +409,9 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request)
accessKey := vars["accessKey"] accessKey := vars["accessKey"]
status := vars["status"] status := vars["status"]
// This API is not allowed to lookup master access key user status // you cannot enable or disable yourself.
if accessKey == globalActiveCred.AccessKey { if accessKey == creds.AccessKey {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL)
return return
} }
@@ -638,6 +664,13 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
return return
} }
if err := createReq.Validate(); err != nil {
// Since this validation would happen client side as well, we only send
// a generic error message here.
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
return
}
var ( var (
targetUser string targetUser string
targetGroups []string targetGroups []string
@@ -650,10 +683,15 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
targetUser = cred.AccessKey targetUser = cred.AccessKey
} }
description := createReq.Description
if description == "" {
description = createReq.Comment
}
opts := newServiceAccountOpts{ opts := newServiceAccountOpts{
accessKey: createReq.AccessKey, accessKey: createReq.AccessKey,
secretKey: createReq.SecretKey, secretKey: createReq.SecretKey,
comment: createReq.Comment, name: createReq.Name,
description: description,
expiration: createReq.Expiration, expiration: createReq.Expiration,
claims: make(map[string]interface{}), claims: make(map[string]interface{}),
} }
@@ -808,7 +846,8 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
AccessKey: newCred.AccessKey, AccessKey: newCred.AccessKey,
SecretKey: newCred.SecretKey, SecretKey: newCred.SecretKey,
Groups: newCred.Groups, Groups: newCred.Groups,
Comment: newCred.Comment, Name: newCred.Name,
Description: newCred.Description,
Claims: opts.claims, Claims: opts.claims,
SessionPolicy: createReq.Policy, SessionPolicy: createReq.Policy,
Status: auth.AccountOn, Status: auth.AccountOn,
@@ -883,6 +922,13 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re
return return
} }
if err := updateReq.Validate(); err != nil {
// Since this validation would happen client side as well, we only send
// a generic error message here.
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
return
}
var sp *iampolicy.Policy var sp *iampolicy.Policy
if len(updateReq.NewPolicy) > 0 { if len(updateReq.NewPolicy) > 0 {
sp, err = iampolicy.ParseConfig(bytes.NewReader(updateReq.NewPolicy)) sp, err = iampolicy.ParseConfig(bytes.NewReader(updateReq.NewPolicy))
@@ -894,7 +940,8 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re
opts := updateServiceAccountOpts{ opts := updateServiceAccountOpts{
secretKey: updateReq.NewSecretKey, secretKey: updateReq.NewSecretKey,
status: updateReq.NewStatus, status: updateReq.NewStatus,
comment: updateReq.NewComment, name: updateReq.NewName,
description: updateReq.NewDescription,
expiration: updateReq.NewExpiration, expiration: updateReq.NewExpiration,
sessionPolicy: sp, sessionPolicy: sp,
} }
@@ -913,7 +960,8 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re
AccessKey: accessKey, AccessKey: accessKey,
SecretKey: opts.secretKey, SecretKey: opts.secretKey,
Status: opts.status, Status: opts.status,
Comment: opts.comment, Name: opts.name,
Description: opts.description,
SessionPolicy: updateReq.NewPolicy, SessionPolicy: updateReq.NewPolicy,
Expiration: updateReq.NewExpiration, Expiration: updateReq.NewExpiration,
}, },
@@ -1001,7 +1049,8 @@ func (a adminAPIHandlers) InfoServiceAccount(w http.ResponseWriter, r *http.Requ
infoResp := madmin.InfoServiceAccountResp{ infoResp := madmin.InfoServiceAccountResp{
ParentUser: svcAccount.ParentUser, ParentUser: svcAccount.ParentUser,
Comment: svcAccount.Comment, Name: svcAccount.Name,
Description: svcAccount.Description,
AccountStatus: svcAccount.Status, AccountStatus: svcAccount.Status,
ImpliedPolicy: policy == nil, ImpliedPolicy: policy == nil,
Policy: string(policyJSON), Policy: string(policyJSON),
@@ -1073,14 +1122,18 @@ func (a adminAPIHandlers) ListServiceAccounts(w http.ResponseWriter, r *http.Req
return return
} }
var serviceAccountsNames []string var serviceAccountList []madmin.ServiceAccountInfo
for _, svc := range serviceAccounts { for _, svc := range serviceAccounts {
serviceAccountsNames = append(serviceAccountsNames, svc.AccessKey) expiryTime := svc.Expiration
serviceAccountList = append(serviceAccountList, madmin.ServiceAccountInfo{
AccessKey: svc.AccessKey,
Expiration: &expiryTime,
})
} }
listResp := madmin.ListServiceAccountsResp{ listResp := madmin.ListServiceAccountsResp{
Accounts: serviceAccountsNames, Accounts: serviceAccountList,
} }
data, err := json.Marshal(listResp) data, err := json.Marshal(listResp)
@@ -1627,6 +1680,12 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
return return
} }
// When the user is root credential you are not allowed to
// add policies for root user.
if entityName == globalActiveCred.AccessKey {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
return
}
} }
// Validate that user or group exists. // Validate that user or group exists.
@@ -1711,24 +1770,40 @@ func (a adminAPIHandlers) ListPolicyMappingEntities(w http.ResponseWriter, r *ht
writeSuccessResponseJSON(w, econfigData) writeSuccessResponseJSON(w, econfigData)
} }
// AttachPolicyBuiltin - POST /minio/admin/v3/idp/builtin/attach // AttachDetachPolicyBuiltin - POST /minio/admin/v3/idp/builtin/policy/{operation}
func (a adminAPIHandlers) AttachPolicyBuiltin(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) AttachDetachPolicyBuiltin(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "AttachPolicyBuiltin") ctx := newContext(r, w, "AttachPolicyBuiltin")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.AttachPolicyAdminAction) objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.UpdatePolicyAssociationAction,
iampolicy.AttachPolicyAdminAction)
if objectAPI == nil { if objectAPI == nil {
return return
} }
cred, _, s3Err := validateAdminSignature(ctx, r, "") if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
if s3Err != ErrNone { // More than maxConfigSize bytes were available
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
return return
} }
password := cred.SecretKey
// Ensure body content type is opaque to ensure that request body has not
// been interpreted as form data.
contentType := r.Header.Get("Content-Type")
if contentType != "application/octet-stream" {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
return
}
operation := mux.Vars(r)["operation"]
if operation != "attach" && operation != "detach" {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminInvalidArgument), r.URL)
return
}
isAttach := operation == "attach"
password := cred.SecretKey
reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength)) reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
@@ -1746,224 +1821,42 @@ func (a adminAPIHandlers) AttachPolicyBuiltin(w http.ResponseWriter, r *http.Req
return return
} }
userOrGroup := par.User updatedAt, addedOrRemoved, _, err := globalIAMSys.PolicyDBUpdateBuiltin(ctx, isAttach, par)
isGroup := false
if userOrGroup == "" {
userOrGroup = par.Group
isGroup = true
}
if isGroup {
_, err := globalIAMSys.GetGroupDescription(userOrGroup)
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) if err == errNoSuchUser || err == errNoSuchGroup {
return if globalIAMSys.LDAPConfig.Enabled() {
} // When LDAP is enabled, warn user that they are using the wrong
} else { // API. FIXME: error can be no such group as well - fix errNoSuchUserLDAPWarn
ok, _, err := globalIAMSys.IsTempUser(userOrGroup) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errNoSuchUserLDAPWarn), r.URL)
if err != nil && err != errNoSuchUser {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
if ok {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
return
}
// Validate that user exists.
if globalIAMSys.GetUsersSysType() == MinIOUsersSysType {
_, ok := globalIAMSys.GetUser(ctx, userOrGroup)
if !ok {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errNoSuchUser), r.URL)
return return
} }
} }
}
userType := regUser
if globalIAMSys.GetUsersSysType() == LDAPUsersSysType {
userType = stsUser
}
var existingPolicies []string
if isGroup {
existingPolicies, err = globalIAMSys.PolicyDBGet(userOrGroup, true)
} else {
existingPolicies, err = globalIAMSys.GetUserPolicies(userOrGroup)
}
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return return
} }
policyMap := make(map[string]bool) respBody := madmin.PolicyAssociationResp{
for _, p := range existingPolicies {
policyMap[p] = true
}
policiesToAttach := par.Policies
// Check if policy is already attached to user.
for _, p := range policiesToAttach {
if _, ok := policyMap[p]; ok {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrPolicyAlreadyAttached), r.URL)
return
}
}
existingPolicies = append(existingPolicies, policiesToAttach...)
newPolicies := strings.Join(existingPolicies, ",")
updatedAt, err := globalIAMSys.PolicyDBSet(ctx, userOrGroup, newPolicies, userType, isGroup)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
Type: madmin.SRIAMItemPolicyMapping,
PolicyMapping: &madmin.SRPolicyMapping{
UserOrGroup: userOrGroup,
UserType: int(userType),
IsGroup: isGroup,
Policy: strings.Join(policiesToAttach, ","),
},
UpdatedAt: updatedAt, UpdatedAt: updatedAt,
}))
writeResponse(w, http.StatusCreated, nil, mimeNone)
}
// DetachPolicyBuiltin - POST /minio/admin/v3/idp/builtin/detach
func (a adminAPIHandlers) DetachPolicyBuiltin(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "DetachPolicyBuiltin")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.AttachPolicyAdminAction)
if objectAPI == nil {
return
}
cred, _, s3Err := validateAdminSignature(ctx, r, "")
if s3Err != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
return
}
password := cred.SecretKey
reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
var par madmin.PolicyAssociationReq
if err = json.Unmarshal(reqBytes, &par); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
if err = par.IsValid(); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
userOrGroup := par.User
isGroup := false
if userOrGroup == "" {
userOrGroup = par.Group
isGroup = true
}
if isGroup {
_, err := globalIAMSys.GetGroupDescription(userOrGroup)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
} }
if isAttach {
respBody.PoliciesAttached = addedOrRemoved
} else { } else {
ok, _, err := globalIAMSys.IsTempUser(userOrGroup) respBody.PoliciesDetached = addedOrRemoved
if err != nil && err != errNoSuchUser {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
if ok {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
return
} }
// Validate that user exists. data, err := json.Marshal(respBody)
if globalIAMSys.GetUsersSysType() == MinIOUsersSysType {
_, ok := globalIAMSys.GetUser(ctx, userOrGroup)
if !ok {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errNoSuchUser), r.URL)
return
}
}
}
userType := regUser
if globalIAMSys.GetUsersSysType() == LDAPUsersSysType {
userType = stsUser
}
var existingPolicies []string
if isGroup {
existingPolicies, err = globalIAMSys.PolicyDBGet(userOrGroup, true)
} else {
existingPolicies, err = globalIAMSys.GetUserPolicies(userOrGroup)
}
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return return
} }
policyMap := make(map[string]bool) encryptedData, err := madmin.EncryptData(password, data)
for _, p := range existingPolicies {
policyMap[p] = true
}
policiesToDetach := par.Policies
// Check if policy is already attached to user.
for _, p := range policiesToDetach {
if _, ok := policyMap[p]; ok {
delete(policyMap, p)
} else {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrPolicyNotAttached), r.URL)
return
}
}
newPoliciesSl := []string{}
for p := range policyMap {
newPoliciesSl = append(newPoliciesSl, p)
}
newPolicies := strings.Join(newPoliciesSl, ",")
updatedAt, err := globalIAMSys.PolicyDBSet(ctx, userOrGroup, newPolicies, userType, isGroup)
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return return
} }
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{ writeSuccessResponseJSON(w, encryptedData)
Type: madmin.SRIAMItemPolicyMapping,
PolicyMapping: &madmin.SRPolicyMapping{
UserOrGroup: userOrGroup,
UserType: int(userType),
IsGroup: isGroup,
Policy: newPolicies,
},
UpdatedAt: updatedAt,
}))
// Return successful JSON response
writeSuccessNoContent(w)
} }
const ( const (
@@ -2056,13 +1949,15 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
} }
userAccounts := make(map[string]madmin.AddOrUpdateUserReq) userAccounts := make(map[string]madmin.AddOrUpdateUserReq)
for u, uid := range userIdentities { for u, uid := range userIdentities {
status := madmin.AccountDisabled
if uid.Credentials.IsValid() {
status = madmin.AccountEnabled
}
userAccounts[u] = madmin.AddOrUpdateUserReq{ userAccounts[u] = madmin.AddOrUpdateUserReq{
SecretKey: uid.Credentials.SecretKey, SecretKey: uid.Credentials.SecretKey,
Status: status, Status: func() madmin.AccountStatus {
// Export current credential status
if uid.Credentials.Status == auth.AccountOff {
return madmin.AccountDisabled
}
return madmin.AccountEnabled
}(),
} }
} }
userData, err := json.Marshal(userAccounts) userData, err := json.Marshal(userAccounts)
@@ -2101,6 +1996,10 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
} }
svcAccts := make(map[string]madmin.SRSvcAccCreate) svcAccts := make(map[string]madmin.SRSvcAccCreate)
for user, acc := range serviceAccounts { for user, acc := range serviceAccounts {
if user == siteReplicatorSvcAcc {
// skip site-replication service account.
continue
}
claims, err := globalIAMSys.GetClaimsForSvcAcc(ctx, acc.Credentials.AccessKey) claims, err := globalIAMSys.GetClaimsForSvcAcc(ctx, acc.Credentials.AccessKey)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
@@ -2449,7 +2348,8 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
opts := updateServiceAccountOpts{ opts := updateServiceAccountOpts{
secretKey: svcAcctReq.SecretKey, secretKey: svcAcctReq.SecretKey,
status: svcAcctReq.Status, status: svcAcctReq.Status,
comment: svcAcctReq.Comment, name: svcAcctReq.Name,
description: svcAcctReq.Description,
expiration: svcAcctReq.Expiration, expiration: svcAcctReq.Expiration,
sessionPolicy: sp, sessionPolicy: sp,
} }
@@ -2465,8 +2365,10 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
secretKey: svcAcctReq.SecretKey, secretKey: svcAcctReq.SecretKey,
sessionPolicy: sp, sessionPolicy: sp,
claims: svcAcctReq.Claims, claims: svcAcctReq.Claims,
comment: svcAcctReq.Comment, name: svcAcctReq.Name,
description: svcAcctReq.Description,
expiration: svcAcctReq.Expiration, expiration: svcAcctReq.Expiration,
allowSiteReplicatorAccount: false,
} }
// In case of LDAP we need to resolve the targetUser to a DN and // In case of LDAP we need to resolve the targetUser to a DN and
+8 -5
View File
@@ -33,10 +33,9 @@ import (
"testing" "testing"
"time" "time"
"github.com/minio/madmin-go/v2" "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/credentials"
cr "github.com/minio/minio-go/v7/pkg/credentials"
"github.com/minio/minio-go/v7/pkg/s3utils" "github.com/minio/minio-go/v7/pkg/s3utils"
"github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/minio-go/v7/pkg/signer" "github.com/minio/minio-go/v7/pkg/signer"
@@ -877,7 +876,7 @@ func (s *TestSuiteIAM) TestServiceAccountOpsByUser(c *check) {
// Create an madmin client with user creds // Create an madmin client with user creds
userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{ userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{
Creds: cr.NewStaticV4(accessKey, secretKey, ""), Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
Secure: s.secure, Secure: s.secure,
}) })
if err != nil { if err != nil {
@@ -1072,7 +1071,7 @@ func (s *TestSuiteIAM) TestAccMgmtPlugin(c *check) {
// Create an madmin client with user creds // Create an madmin client with user creds
userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{ userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{
Creds: cr.NewStaticV4(accessKey, secretKey, ""), Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
Secure: s.secure, Secure: s.secure,
}) })
if err != nil { if err != nil {
@@ -1329,7 +1328,11 @@ func (c *check) assertSvcAccAppearsInListing(ctx context.Context, madmClient *ma
if err != nil { if err != nil {
c.Fatalf("unable to list svc accounts: %v", err) c.Fatalf("unable to list svc accounts: %v", err)
} }
if !set.CreateStringSet(listResp.Accounts...).Contains(svcAK) { var accessKeys []string
for _, item := range listResp.Accounts {
accessKeys = append(accessKeys, item.AccessKey)
}
if !set.CreateStringSet(accessKeys...).Contains(svcAK) {
c.Fatalf("service account did not appear in listing!") c.Fatalf("service account did not appear in listing!")
} }
} }
+32 -8
View File
@@ -45,8 +45,8 @@ import (
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/klauspost/compress/zip" "github.com/klauspost/compress/zip"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/madmin-go/v2/estream" "github.com/minio/madmin-go/v3/estream"
"github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/minio/internal/dsync" "github.com/minio/minio/internal/dsync"
"github.com/minio/minio/internal/handlers" "github.com/minio/minio/internal/handlers"
@@ -770,7 +770,7 @@ func (a adminAPIHandlers) ProfileHandler(w http.ResponseWriter, r *http.Request)
var err error var err error
duration, err = time.ParseDuration(dstr) duration, err = time.ParseDuration(dstr)
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
return return
} }
} }
@@ -1525,7 +1525,7 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
return shouldTrace(entry, traceOpts) return shouldTrace(entry, traceOpts)
}) })
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrSlowDown), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return return
} }
@@ -1605,7 +1605,7 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque
err = globalConsoleSys.Subscribe(logCh, ctx.Done(), node, limitLines, logKind, nil) err = globalConsoleSys.Subscribe(logCh, ctx.Done(), node, limitLines, logKind, nil)
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrSlowDown), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return return
} }
@@ -1855,7 +1855,7 @@ func getServerInfo(ctx context.Context, poolsInfoEnabled bool, r *http.Request)
assignPoolNumbers(servers) assignPoolNumbers(servers)
var poolsInfo map[int]map[int]madmin.ErasureSetInfo var poolsInfo map[int]map[int]madmin.ErasureSetInfo
var backend interface{} var backend madmin.ErasureBackend
mode := madmin.ItemInitializing mode := madmin.ItemInitializing
@@ -1896,6 +1896,8 @@ func getServerInfo(ctx context.Context, poolsInfoEnabled bool, r *http.Request)
OfflineDisks: offlineDisks.Sum(), OfflineDisks: offlineDisks.Sum(),
StandardSCParity: backendInfo.StandardSCParity, StandardSCParity: backendInfo.StandardSCParity,
RRSCParity: backendInfo.RRSCParity, RRSCParity: backendInfo.RRSCParity,
TotalSets: backendInfo.TotalSets,
DrivesPerSet: backendInfo.DrivesPerSet,
} }
if poolsInfoEnabled { if poolsInfoEnabled {
@@ -2101,6 +2103,27 @@ func fetchHealthInfo(healthCtx context.Context, objectAPI ObjectLayer, query *ur
} }
} }
// collect all realtime metrics except disk
// disk metrics are already included under drive info of each server
getRealtimeMetrics := func() *madmin.RealtimeMetrics {
var m madmin.RealtimeMetrics
var types madmin.MetricType = madmin.MetricsAll &^ madmin.MetricsDisk
mLocal := collectLocalMetrics(types, collectMetricsOpts{})
m.Merge(&mLocal)
cctx, cancel := context.WithTimeout(healthCtx, time.Second/2)
mRemote := collectRemoteMetrics(cctx, types, collectMetricsOpts{})
cancel()
m.Merge(&mRemote)
for idx, host := range m.Hosts {
m.Hosts[idx] = anonAddr(host)
}
for host, metrics := range m.ByHost {
m.ByHost[anonAddr(host)] = metrics
delete(m.ByHost, host)
}
return &m
}
anonymizeCmdLine := func(cmdLine string) string { anonymizeCmdLine := func(cmdLine string) string {
if !globalIsDistErasure { if !globalIsDistErasure {
// FS mode - single server - hard code to `server1` // FS mode - single server - hard code to `server1`
@@ -2278,6 +2301,7 @@ func fetchHealthInfo(healthCtx context.Context, objectAPI ObjectLayer, query *ur
TLS: &tls, TLS: &tls,
IsKubernetes: &isK8s, IsKubernetes: &isK8s,
IsDocker: &isDocker, IsDocker: &isDocker,
Metrics: getRealtimeMetrics(),
} }
partialWrite(healthInfo) partialWrite(healthInfo)
} }
@@ -2509,13 +2533,13 @@ func fetchLoggerInfo() ([]madmin.Logger, []madmin.Audit) {
var auditloggerInfo []madmin.Audit var auditloggerInfo []madmin.Audit
for _, tgt := range logger.SystemTargets() { for _, tgt := range logger.SystemTargets() {
if tgt.Endpoint() != "" { if tgt.Endpoint() != "" {
loggerInfo = append(loggerInfo, madmin.Logger{tgt.String(): logger.TargetStatus(tgt)}) loggerInfo = append(loggerInfo, madmin.Logger{tgt.String(): logger.TargetStatus(GlobalContext, tgt)})
} }
} }
for _, tgt := range logger.AuditTargets() { for _, tgt := range logger.AuditTargets() {
if tgt.Endpoint() != "" { if tgt.Endpoint() != "" {
auditloggerInfo = append(auditloggerInfo, madmin.Audit{tgt.String(): logger.TargetStatus(tgt)}) auditloggerInfo = append(auditloggerInfo, madmin.Audit{tgt.String(): logger.TargetStatus(GlobalContext, tgt)})
} }
} }
+3 -3
View File
@@ -31,7 +31,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/auth"
"github.com/minio/mux" "github.com/minio/mux"
) )
@@ -479,8 +479,8 @@ func TestTopLockEntries(t *testing.T) {
if len(exp) != len(got) { if len(exp) != len(got) {
return 0, false return 0, false
} }
sort.Sort(byResourceUID{exp}) sort.Slice(exp, byResourceUID{exp}.Less)
sort.Sort(byResourceUID{got}) sort.Slice(got, byResourceUID{got}.Less)
// printEntries(exp) // printEntries(exp)
// printEntries(got) // printEntries(got)
for i, e := range exp { for i, e := range exp {
+7 -7
View File
@@ -27,7 +27,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
) )
@@ -784,6 +784,11 @@ func (h *healSequence) healDiskMeta(objAPI ObjectLayer) error {
} }
func (h *healSequence) healItems(objAPI ObjectLayer, bucketsOnly bool) error { func (h *healSequence) healItems(objAPI ObjectLayer, bucketsOnly bool) error {
if h.clientToken == bgHealingUUID {
// For background heal do nothing.
return nil
}
if err := h.healDiskMeta(objAPI); err != nil { if err := h.healDiskMeta(objAPI); err != nil {
return err return err
} }
@@ -869,15 +874,10 @@ func (h *healSequence) healBucket(objAPI ObjectLayer, bucket string, bucketsOnly
if !h.settings.Recursive { if !h.settings.Recursive {
if h.object != "" { if h.object != "" {
// Check if an object named as the objPrefix exists, if err := h.healObject(bucket, h.object, ""); err != nil {
// and if so heal it.
oi, err := objAPI.GetObjectInfo(h.ctx, bucket, h.object, ObjectOptions{})
if err == nil {
if err = h.healObject(bucket, h.object, oi.VersionID); err != nil {
return err return err
} }
} }
}
return nil return nil
} }
+3 -6
View File
@@ -22,7 +22,7 @@ import (
"github.com/klauspost/compress/gzhttp" "github.com/klauspost/compress/gzhttp"
"github.com/klauspost/compress/gzip" "github.com/klauspost/compress/gzip"
"github.com/minio/madmin-go/v2" "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"
) )
@@ -162,11 +162,8 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
HandlerFunc(gz(httpTraceHdrs(adminAPI.SetPolicyForUserOrGroup))). HandlerFunc(gz(httpTraceHdrs(adminAPI.SetPolicyForUserOrGroup))).
Queries("policyName", "{policyName:.*}", "userOrGroup", "{userOrGroup:.*}", "isGroup", "{isGroup:true|false}") Queries("policyName", "{policyName:.*}", "userOrGroup", "{userOrGroup:.*}", "isGroup", "{isGroup:true|false}")
// Attach policies to user or group // Attach/Detach policies to/from user or group
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/idp/builtin/policy/attach").HandlerFunc(gz(httpTraceHdrs(adminAPI.AttachPolicyBuiltin))) adminRouter.Methods(http.MethodPost).Path(adminVersion + "/idp/builtin/policy/{operation}").HandlerFunc(gz(httpTraceHdrs(adminAPI.AttachDetachPolicyBuiltin)))
// Detach policies from user or group
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/idp/builtin/policy/detach").HandlerFunc(gz(httpTraceHdrs(adminAPI.DetachPolicyBuiltin)))
// Remove user IAM // Remove user IAM
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-user").HandlerFunc(gz(httpTraceHdrs(adminAPI.RemoveUser))).Queries("accessKey", "{accessKey:.*}") adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-user").HandlerFunc(gz(httpTraceHdrs(adminAPI.RemoveUser))).Queries("accessKey", "{accessKey:.*}")
+3 -3
View File
@@ -26,7 +26,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"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"
@@ -87,7 +87,6 @@ func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Req
} }
props := madmin.ServerProperties{ props := madmin.ServerProperties{
State: string(madmin.ItemInitializing),
Endpoint: addr, Endpoint: addr,
Uptime: UTCNow().Unix() - globalBootTime.Unix(), Uptime: UTCNow().Unix() - globalBootTime.Unix(),
Version: Version, Version: Version,
@@ -146,7 +145,8 @@ func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Req
props.State = string(madmin.ItemOnline) props.State = string(madmin.ItemOnline)
props.Disks = storageInfo.Disks props.Disks = storageInfo.Disks
} else { } else {
props.State = string(madmin.ItemOffline) props.State = string(madmin.ItemInitializing)
props.Disks = getOfflineDisks("", globalEndpoints)
} }
return props return props
+87 -35
View File
@@ -28,9 +28,10 @@ import (
"strings" "strings"
"github.com/Azure/azure-storage-blob-go/azblob" "github.com/Azure/azure-storage-blob-go/azblob"
"github.com/minio/minio/internal/ioutil"
"google.golang.org/api/googleapi" "google.golang.org/api/googleapi"
"github.com/minio/madmin-go/v2" "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/tags" "github.com/minio/minio-go/v7/pkg/tags"
"github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/auth"
@@ -86,6 +87,7 @@ const (
ErrInternalError ErrInternalError
ErrInvalidAccessKeyID ErrInvalidAccessKeyID
ErrAccessKeyDisabled ErrAccessKeyDisabled
ErrInvalidArgument
ErrInvalidBucketName ErrInvalidBucketName
ErrInvalidDigest ErrInvalidDigest
ErrInvalidRange ErrInvalidRange
@@ -134,7 +136,7 @@ const (
ErrReplicationNeedsVersioningError ErrReplicationNeedsVersioningError
ErrReplicationBucketNeedsVersioningError ErrReplicationBucketNeedsVersioningError
ErrReplicationDenyEditError ErrReplicationDenyEditError
ErrRemoteTargetDenyEditError ErrRemoteTargetDenyAddError
ErrReplicationNoExistingObjects ErrReplicationNoExistingObjects
ErrObjectRestoreAlreadyInProgress ErrObjectRestoreAlreadyInProgress
ErrNoSuchKey ErrNoSuchKey
@@ -148,6 +150,7 @@ const (
ErrMethodNotAllowed ErrMethodNotAllowed
ErrInvalidPart ErrInvalidPart
ErrInvalidPartOrder ErrInvalidPartOrder
ErrMissingPart
ErrAuthorizationHeaderMalformed ErrAuthorizationHeaderMalformed
ErrMalformedPOSTRequest ErrMalformedPOSTRequest
ErrPOSTFileRequired ErrPOSTFileRequired
@@ -181,8 +184,10 @@ const (
ErrBucketAlreadyExists ErrBucketAlreadyExists
ErrMetadataTooLarge ErrMetadataTooLarge
ErrUnsupportedMetadata ErrUnsupportedMetadata
ErrUnsupportedHostHeader
ErrMaximumExpires ErrMaximumExpires
ErrSlowDown ErrSlowDownRead
ErrSlowDownWrite
ErrInvalidPrefixMarker ErrInvalidPrefixMarker
ErrBadRequest ErrBadRequest
ErrKeyTooLongError ErrKeyTooLongError
@@ -199,6 +204,7 @@ const (
ErrInvalidTagDirective ErrInvalidTagDirective
ErrPolicyAlreadyAttached ErrPolicyAlreadyAttached
ErrPolicyNotAttached ErrPolicyNotAttached
ErrExcessData
// Add new error codes here. // Add new error codes here.
// SSE-S3/SSE-KMS related API errors // SSE-S3/SSE-KMS related API errors
@@ -251,10 +257,11 @@ const (
ErrInvalidObjectName ErrInvalidObjectName
ErrInvalidObjectNamePrefixSlash ErrInvalidObjectNamePrefixSlash
ErrInvalidResourceName ErrInvalidResourceName
ErrInvalidLifecycleQueryParameter
ErrServerNotInitialized ErrServerNotInitialized
ErrOperationTimedOut ErrRequestTimedout
ErrClientDisconnected ErrClientDisconnected
ErrOperationMaxedOut ErrTooManyRequests
ErrInvalidRequest ErrInvalidRequest
ErrTransitionStorageClassNotFoundError ErrTransitionStorageClassNotFoundError
// MinIO storage class error codes // MinIO storage class error codes
@@ -266,6 +273,7 @@ const (
ErrMalformedJSON ErrMalformedJSON
ErrAdminNoSuchUser ErrAdminNoSuchUser
ErrAdminNoSuchUserLDAPWarn
ErrAdminNoSuchGroup ErrAdminNoSuchGroup
ErrAdminGroupNotEmpty ErrAdminGroupNotEmpty
ErrAdminGroupDisabled ErrAdminGroupDisabled
@@ -527,6 +535,11 @@ var errorCodes = errorCodeMap{
Description: "Your proposed upload exceeds the maximum allowed object size.", Description: "Your proposed upload exceeds the maximum allowed object size.",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrExcessData: {
Code: "ExcessData",
Description: "More data provided than indicated content length",
HTTPStatusCode: http.StatusBadRequest,
},
ErrPolicyTooLarge: { ErrPolicyTooLarge: {
Code: "PolicyTooLarge", Code: "PolicyTooLarge",
Description: "Policy exceeds the maximum allowed document size.", Description: "Policy exceeds the maximum allowed document size.",
@@ -552,6 +565,11 @@ var errorCodes = errorCodeMap{
Description: "Your account is disabled; please contact your administrator.", Description: "Your account is disabled; please contact your administrator.",
HTTPStatusCode: http.StatusForbidden, HTTPStatusCode: http.StatusForbidden,
}, },
ErrInvalidArgument: {
Code: "InvalidArgument",
Description: "Invalid argument",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidBucketName: { ErrInvalidBucketName: {
Code: "InvalidBucketName", Code: "InvalidBucketName",
Description: "The specified bucket is not valid.", Description: "The specified bucket is not valid.",
@@ -677,6 +695,11 @@ var errorCodes = errorCodeMap{
Description: "One or more of the specified parts could not be found. The part may not have been uploaded, or the specified entity tag may not match the part's entity tag.", Description: "One or more of the specified parts could not be found. The part may not have been uploaded, or the specified entity tag may not match the part's entity tag.",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrMissingPart: {
Code: "InvalidRequest",
Description: "You must specify at least one part",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidPartOrder: { ErrInvalidPartOrder: {
Code: "InvalidPartOrder", Code: "InvalidPartOrder",
Description: "The list of parts was not in ascending order. The parts list must be specified in order by part number.", Description: "The list of parts was not in ascending order. The parts list must be specified in order by part number.",
@@ -822,11 +845,16 @@ var errorCodes = errorCodeMap{
Description: "Request is not valid yet", Description: "Request is not valid yet",
HTTPStatusCode: http.StatusForbidden, HTTPStatusCode: http.StatusForbidden,
}, },
ErrSlowDown: { ErrSlowDownRead: {
Code: "SlowDown", Code: "SlowDownRead",
Description: "Resource requested is unreadable, please reduce your request rate", Description: "Resource requested is unreadable, please reduce your request rate",
HTTPStatusCode: http.StatusServiceUnavailable, HTTPStatusCode: http.StatusServiceUnavailable,
}, },
ErrSlowDownWrite: {
Code: "SlowDownWrite",
Description: "Resource requested is unwritable, please reduce your request rate",
HTTPStatusCode: http.StatusServiceUnavailable,
},
ErrInvalidPrefixMarker: { ErrInvalidPrefixMarker: {
Code: "InvalidPrefixMarker", Code: "InvalidPrefixMarker",
Description: "Invalid marker prefix combination", Description: "Invalid marker prefix combination",
@@ -927,9 +955,9 @@ var errorCodes = errorCodeMap{
Description: "No matching ExistingsObjects rule enabled", Description: "No matching ExistingsObjects rule enabled",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrRemoteTargetDenyEditError: { ErrRemoteTargetDenyAddError: {
Code: "XMinioAdminRemoteTargetDenyEdit", Code: "XMinioAdminRemoteTargetDenyAdd",
Description: "Cannot alter remote target endpoint since this server is in a cluster replication setup. use `mc admin replicate update`", Description: "Cannot add remote target endpoint since this server is in a cluster replication setup",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrReplicationDenyEditError: { ErrReplicationDenyEditError: {
@@ -1100,8 +1128,13 @@ var errorCodes = errorCodeMap{
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrInvalidEncryptionMethod: { ErrInvalidEncryptionMethod: {
Code: "InvalidRequest", Code: "InvalidArgument",
Description: "The encryption method specified is not supported", Description: "Server Side Encryption with AWS KMS managed key requires HTTP header x-amz-server-side-encryption : aws:kms",
HTTPStatusCode: http.StatusBadRequest,
},
ErrIncompatibleEncryptionMethod: {
Code: "InvalidArgument",
Description: "Server Side Encryption with Customer provided key is incompatible with the encryption method specified",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrInvalidEncryptionKeyID: { ErrInvalidEncryptionKeyID: {
@@ -1164,11 +1197,6 @@ var errorCodes = errorCodeMap{
Description: "The provided encryption parameters did not match the ones used originally.", Description: "The provided encryption parameters did not match the ones used originally.",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrIncompatibleEncryptionMethod: {
Code: "InvalidArgument",
Description: "Server side encryption specified with both SSE-C and SSE-S3 headers",
HTTPStatusCode: http.StatusBadRequest,
},
ErrKMSNotConfigured: { ErrKMSNotConfigured: {
Code: "NotImplemented", Code: "NotImplemented",
Description: "Server side encryption specified but KMS is not configured", Description: "Server side encryption specified but KMS is not configured",
@@ -1248,11 +1276,21 @@ var errorCodes = errorCodeMap{
Description: "The JSON you provided was not well-formed or did not validate against our published format.", Description: "The JSON you provided was not well-formed or did not validate against our published format.",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrInvalidLifecycleQueryParameter: {
Code: "XMinioInvalidLifecycleParameter",
Description: "The boolean value provided for withUpdatedAt query parameter was invalid.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrAdminNoSuchUser: { ErrAdminNoSuchUser: {
Code: "XMinioAdminNoSuchUser", Code: "XMinioAdminNoSuchUser",
Description: "The specified user does not exist.", Description: "The specified user does not exist.",
HTTPStatusCode: http.StatusNotFound, HTTPStatusCode: http.StatusNotFound,
}, },
ErrAdminNoSuchUserLDAPWarn: {
Code: "XMinioAdminNoSuchUser",
Description: "The specified user does not exist. If you meant a user in LDAP, use `mc idp ldap`",
HTTPStatusCode: http.StatusNotFound,
},
ErrAdminNoSuchGroup: { ErrAdminNoSuchGroup: {
Code: "XMinioAdminNoSuchGroup", Code: "XMinioAdminNoSuchGroup",
Description: "The specified group does not exist.", Description: "The specified group does not exist.",
@@ -1385,7 +1423,7 @@ var errorCodes = errorCodeMap{
Description: "Cannot respond to plain-text request from TLS-encrypted server", Description: "Cannot respond to plain-text request from TLS-encrypted server",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrOperationTimedOut: { ErrRequestTimedout: {
Code: "RequestTimeout", Code: "RequestTimeout",
Description: "A timeout occurred while trying to lock a resource, please reduce your request rate", Description: "A timeout occurred while trying to lock a resource, please reduce your request rate",
HTTPStatusCode: http.StatusServiceUnavailable, HTTPStatusCode: http.StatusServiceUnavailable,
@@ -1395,9 +1433,9 @@ var errorCodes = errorCodeMap{
Description: "Client disconnected before response was ready", Description: "Client disconnected before response was ready",
HTTPStatusCode: 499, // No official code, use nginx value. HTTPStatusCode: 499, // No official code, use nginx value.
}, },
ErrOperationMaxedOut: { ErrTooManyRequests: {
Code: "SlowDown", Code: "TooManyRequests",
Description: "A timeout exceeded while waiting to proceed with the request, please reduce your request rate", Description: "Deadline exceeded while waiting in incoming queue, please reduce your request rate",
HTTPStatusCode: http.StatusServiceUnavailable, HTTPStatusCode: http.StatusServiceUnavailable,
}, },
ErrUnsupportedMetadata: { ErrUnsupportedMetadata: {
@@ -1405,6 +1443,11 @@ var errorCodes = errorCodeMap{
Description: "Your metadata headers are not supported.", Description: "Your metadata headers are not supported.",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrUnsupportedHostHeader: {
Code: "InvalidArgument",
Description: "Your Host header is malformed.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrObjectTampered: { ErrObjectTampered: {
Code: "XMinioObjectTampered", Code: "XMinioObjectTampered",
Description: errObjectTampered.Error(), Description: errObjectTampered.Error(),
@@ -2008,11 +2051,14 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
} }
// Only return ErrClientDisconnected if the provided context is actually canceled. // Only return ErrClientDisconnected if the provided context is actually canceled.
// This way downstream context.Canceled will still report ErrOperationTimedOut // This way downstream context.Canceled will still report ErrRequestTimedout
if contextCanceled(ctx) && errors.Is(ctx.Err(), context.Canceled) { if contextCanceled(ctx) && errors.Is(ctx.Err(), context.Canceled) {
return ErrClientDisconnected return ErrClientDisconnected
} }
// Unwrap the error first
err = unwrapAll(err)
switch err { switch err {
case errInvalidArgument: case errInvalidArgument:
apiErr = ErrAdminInvalidArgument apiErr = ErrAdminInvalidArgument
@@ -2020,6 +2066,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
apiErr = ErrAdminNoSuchPolicy apiErr = ErrAdminNoSuchPolicy
case errNoSuchUser: case errNoSuchUser:
apiErr = ErrAdminNoSuchUser apiErr = ErrAdminNoSuchUser
case errNoSuchUserLDAPWarn:
apiErr = ErrAdminNoSuchUserLDAPWarn
case errNoSuchServiceAccount: case errNoSuchServiceAccount:
apiErr = ErrAdminServiceAccountNotFound apiErr = ErrAdminServiceAccountNotFound
case errNoSuchGroup: case errNoSuchGroup:
@@ -2047,9 +2095,9 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
case errInvalidStorageClass: case errInvalidStorageClass:
apiErr = ErrInvalidStorageClass apiErr = ErrInvalidStorageClass
case errErasureReadQuorum: case errErasureReadQuorum:
apiErr = ErrSlowDown apiErr = ErrSlowDownRead
case errErasureWriteQuorum: case errErasureWriteQuorum:
apiErr = ErrSlowDown apiErr = ErrSlowDownWrite
// SSE errors // SSE errors
case errInvalidEncryptionParameters: case errInvalidEncryptionParameters:
apiErr = ErrInvalidEncryptionParameters apiErr = ErrInvalidEncryptionParameters
@@ -2083,10 +2131,10 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
apiErr = ErrKMSKeyNotFoundException apiErr = ErrKMSKeyNotFoundException
case errKMSDefaultKeyAlreadyConfigured: case errKMSDefaultKeyAlreadyConfigured:
apiErr = ErrKMSDefaultKeyAlreadyConfigured apiErr = ErrKMSDefaultKeyAlreadyConfigured
case context.Canceled, context.DeadlineExceeded: case context.Canceled:
apiErr = ErrOperationTimedOut apiErr = ErrClientDisconnected
case errDiskNotFound: case context.DeadlineExceeded:
apiErr = ErrSlowDown apiErr = ErrRequestTimedout
case objectlock.ErrInvalidRetentionDate: case objectlock.ErrInvalidRetentionDate:
apiErr = ErrInvalidRetentionDate apiErr = ErrInvalidRetentionDate
case objectlock.ErrPastObjectLockRetainDate: case objectlock.ErrPastObjectLockRetainDate:
@@ -2097,6 +2145,10 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
apiErr = ErrObjectLockInvalidHeaders apiErr = ErrObjectLockInvalidHeaders
case objectlock.ErrMalformedXML: case objectlock.ErrMalformedXML:
apiErr = ErrMalformedXML apiErr = ErrMalformedXML
case errInvalidMaxParts:
apiErr = ErrInvalidMaxParts
case ioutil.ErrOverread:
apiErr = ErrExcessData
} }
// Compression errors // Compression errors
@@ -2161,9 +2213,9 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
case InvalidPart: case InvalidPart:
apiErr = ErrInvalidPart apiErr = ErrInvalidPart
case InsufficientWriteQuorum: case InsufficientWriteQuorum:
apiErr = ErrSlowDown apiErr = ErrSlowDownWrite
case InsufficientReadQuorum: case InsufficientReadQuorum:
apiErr = ErrSlowDown apiErr = ErrSlowDownRead
case InvalidMarkerPrefixCombination: case InvalidMarkerPrefixCombination:
apiErr = ErrNotImplemented apiErr = ErrNotImplemented
case InvalidUploadIDKeyCombination: case InvalidUploadIDKeyCombination:
@@ -2178,10 +2230,10 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
apiErr = ErrContentSHA256Mismatch apiErr = ErrContentSHA256Mismatch
case hash.ChecksumMismatch: case hash.ChecksumMismatch:
apiErr = ErrContentChecksumMismatch apiErr = ErrContentChecksumMismatch
case ObjectTooLarge: case hash.SizeTooSmall:
apiErr = ErrEntityTooLarge
case ObjectTooSmall:
apiErr = ErrEntityTooSmall apiErr = ErrEntityTooSmall
case hash.SizeTooLarge:
apiErr = ErrEntityTooLarge
case NotImplemented: case NotImplemented:
apiErr = ErrNotImplemented apiErr = ErrNotImplemented
case PartTooBig: case PartTooBig:
@@ -2257,7 +2309,7 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
case *event.ErrUnsupportedConfiguration: case *event.ErrUnsupportedConfiguration:
apiErr = ErrUnsupportedNotification apiErr = ErrUnsupportedNotification
case OperationTimedOut: case OperationTimedOut:
apiErr = ErrOperationTimedOut apiErr = ErrRequestTimedout
case BackendDown: case BackendDown:
apiErr = ErrBackendDown apiErr = ErrBackendDown
case ObjectNameTooLong: case ObjectNameTooLong:
@@ -2352,7 +2404,7 @@ func toAPIError(ctx context.Context, err error) APIError {
} }
case lifecycle.Error: case lifecycle.Error:
apiErr = APIError{ apiErr = APIError{
Code: "InvalidRequest", Code: "InvalidArgument",
Description: e.Error(), Description: e.Error(),
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
} }
+2 -9
View File
@@ -20,8 +20,6 @@ package cmd
import ( import (
"context" "context"
"errors" "errors"
"os"
"path/filepath"
"testing" "testing"
"github.com/minio/minio/internal/crypto" "github.com/minio/minio/internal/crypto"
@@ -42,8 +40,8 @@ var toAPIErrorTests = []struct {
{err: ObjectNameInvalid{}, errCode: ErrInvalidObjectName}, {err: ObjectNameInvalid{}, errCode: ErrInvalidObjectName},
{err: InvalidUploadID{}, errCode: ErrNoSuchUpload}, {err: InvalidUploadID{}, errCode: ErrNoSuchUpload},
{err: InvalidPart{}, errCode: ErrInvalidPart}, {err: InvalidPart{}, errCode: ErrInvalidPart},
{err: InsufficientReadQuorum{}, errCode: ErrSlowDown}, {err: InsufficientReadQuorum{}, errCode: ErrSlowDownRead},
{err: InsufficientWriteQuorum{}, errCode: ErrSlowDown}, {err: InsufficientWriteQuorum{}, errCode: ErrSlowDownWrite},
{err: InvalidMarkerPrefixCombination{}, errCode: ErrNotImplemented}, {err: InvalidMarkerPrefixCombination{}, errCode: ErrNotImplemented},
{err: InvalidUploadIDKeyCombination{}, errCode: ErrNotImplemented}, {err: InvalidUploadIDKeyCombination{}, errCode: ErrNotImplemented},
{err: MalformedUploadID{}, errCode: ErrNoSuchUpload}, {err: MalformedUploadID{}, errCode: ErrNoSuchUpload},
@@ -67,11 +65,6 @@ var toAPIErrorTests = []struct {
} }
func TestAPIErrCode(t *testing.T) { func TestAPIErrCode(t *testing.T) {
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
defer os.RemoveAll(disk)
initFSObjects(disk, t)
ctx := context.Background() ctx := context.Background()
for i, testCase := range toAPIErrorTests { for i, testCase := range toAPIErrorTests {
errCode := toAPIErrorCode(ctx, testCase.err) errCode := toAPIErrorCode(ctx, testCase.err)
+1 -1
View File
@@ -203,7 +203,7 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSp
} }
// Set the relevant version ID as part of the response header. // Set the relevant version ID as part of the response header.
if objInfo.VersionID != "" { if objInfo.VersionID != "" && objInfo.VersionID != nullVersionID {
w.Header()[xhttp.AmzVersionID] = []string{objInfo.VersionID} w.Header()[xhttp.AmzVersionID] = []string{objInfo.VersionID}
} }
+8 -8
View File
@@ -37,8 +37,8 @@ func getListObjectsV1Args(values url.Values) (prefix, marker, delimiter string,
maxkeys = maxObjectList maxkeys = maxObjectList
} }
prefix = trimLeadingSlash(values.Get("prefix")) prefix = values.Get("prefix")
marker = trimLeadingSlash(values.Get("marker")) marker = values.Get("marker")
delimiter = values.Get("delimiter") delimiter = values.Get("delimiter")
encodingType = values.Get("encoding-type") encodingType = values.Get("encoding-type")
return return
@@ -57,8 +57,8 @@ func getListBucketObjectVersionsArgs(values url.Values) (prefix, marker, delimit
maxkeys = maxObjectList maxkeys = maxObjectList
} }
prefix = trimLeadingSlash(values.Get("prefix")) prefix = values.Get("prefix")
marker = trimLeadingSlash(values.Get("key-marker")) marker = values.Get("key-marker")
delimiter = values.Get("delimiter") delimiter = values.Get("delimiter")
encodingType = values.Get("encoding-type") encodingType = values.Get("encoding-type")
versionIDMarker = values.Get("version-id-marker") versionIDMarker = values.Get("version-id-marker")
@@ -87,8 +87,8 @@ func getListObjectsV2Args(values url.Values) (prefix, token, startAfter, delimit
maxkeys = maxObjectList maxkeys = maxObjectList
} }
prefix = trimLeadingSlash(values.Get("prefix")) prefix = values.Get("prefix")
startAfter = trimLeadingSlash(values.Get("start-after")) startAfter = values.Get("start-after")
delimiter = values.Get("delimiter") delimiter = values.Get("delimiter")
fetchOwner = values.Get("fetch-owner") == "true" fetchOwner = values.Get("fetch-owner") == "true"
encodingType = values.Get("encoding-type") encodingType = values.Get("encoding-type")
@@ -118,8 +118,8 @@ func getBucketMultipartResources(values url.Values) (prefix, keyMarker, uploadID
maxUploads = maxUploadsList maxUploads = maxUploadsList
} }
prefix = trimLeadingSlash(values.Get("prefix")) prefix = values.Get("prefix")
keyMarker = trimLeadingSlash(values.Get("key-marker")) keyMarker = values.Get("key-marker")
uploadIDMarker = values.Get("upload-id-marker") uploadIDMarker = values.Get("upload-id-marker")
delimiter = values.Get("delimiter") delimiter = values.Get("delimiter")
encodingType = values.Get("encoding-type") encodingType = values.Get("encoding-type")
+32 -9
View File
@@ -85,7 +85,7 @@ type ListVersionsResponse struct {
VersionIDMarker string `xml:"VersionIdMarker"` VersionIDMarker string `xml:"VersionIdMarker"`
MaxKeys int MaxKeys int
Delimiter string Delimiter string `xml:"Delimiter,omitempty"`
// A flag that indicates whether or not ListObjects returned all of the results // A flag that indicates whether or not ListObjects returned all of the results
// that satisfied the search criteria. // that satisfied the search criteria.
IsTruncated bool IsTruncated bool
@@ -115,7 +115,7 @@ type ListObjectsResponse struct {
NextMarker string `xml:"NextMarker,omitempty"` NextMarker string `xml:"NextMarker,omitempty"`
MaxKeys int MaxKeys int
Delimiter string Delimiter string `xml:"Delimiter,omitempty"`
// A flag that indicates whether or not ListObjects returned all of the results // A flag that indicates whether or not ListObjects returned all of the results
// that satisfied the search criteria. // that satisfied the search criteria.
IsTruncated bool IsTruncated bool
@@ -146,7 +146,7 @@ type ListObjectsV2Response struct {
KeyCount int KeyCount int
MaxKeys int MaxKeys int
Delimiter string Delimiter string `xml:"Delimiter,omitempty"`
// A flag that indicates whether or not ListObjects returned all of the results // A flag that indicates whether or not ListObjects returned all of the results
// that satisfied the search criteria. // that satisfied the search criteria.
IsTruncated bool IsTruncated bool
@@ -205,7 +205,7 @@ type ListMultipartUploadsResponse struct {
UploadIDMarker string `xml:"UploadIdMarker"` UploadIDMarker string `xml:"UploadIdMarker"`
NextKeyMarker string NextKeyMarker string
NextUploadIDMarker string `xml:"NextUploadIdMarker"` NextUploadIDMarker string `xml:"NextUploadIdMarker"`
Delimiter string Delimiter string `xml:"Delimiter,omitempty"`
Prefix string Prefix string
EncodingType string `xml:"EncodingType,omitempty"` EncodingType string `xml:"EncodingType,omitempty"`
MaxUploads int MaxUploads int
@@ -345,6 +345,13 @@ func (s *Metadata) MarshalXML(e *xxml.Encoder, start xxml.StartElement) error {
return e.EncodeToken(start.End()) return e.EncodeToken(start.End())
} }
// ObjectInternalInfo contains some internal information about a given
// object, it will printed in listing calls with enabled metadata.
type ObjectInternalInfo struct {
K int // Data blocks
M int // Parity blocks
}
// Object container for object metadata // Object container for object metadata
type Object struct { type Object struct {
Key string Key string
@@ -353,7 +360,7 @@ type Object struct {
Size int64 Size int64
// Owner of the object. // Owner of the object.
Owner Owner Owner *Owner `xml:"Owner,omitempty"`
// The class of storage used to store the object. // The class of storage used to store the object.
StorageClass string StorageClass string
@@ -361,6 +368,8 @@ type Object struct {
// UserMetadata user-defined metadata // UserMetadata user-defined metadata
UserMetadata *Metadata `xml:"UserMetadata,omitempty"` UserMetadata *Metadata `xml:"UserMetadata,omitempty"`
UserTags string `xml:"UserTags,omitempty"` UserTags string `xml:"UserTags,omitempty"`
Internal *ObjectInternalInfo `xml:"Internal,omitempty"`
} }
// CopyObjectResponse container returns ETag and LastModified of the successfully copied object // CopyObjectResponse container returns ETag and LastModified of the successfully copied object
@@ -497,7 +506,7 @@ func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delimiter, encodingType string, maxKeys int, resp ListObjectVersionsInfo, metadata metaCheckFn) ListVersionsResponse { func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delimiter, encodingType string, maxKeys int, resp ListObjectVersionsInfo, metadata metaCheckFn) ListVersionsResponse {
versions := make([]ObjectVersion, 0, len(resp.Objects)) versions := make([]ObjectVersion, 0, len(resp.Objects))
owner := Owner{ owner := &Owner{
ID: globalMinioDefaultOwnerID, ID: globalMinioDefaultOwnerID,
DisplayName: "minio", DisplayName: "minio",
} }
@@ -552,6 +561,10 @@ func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delim
} }
content.UserMetadata.Set(k, v) content.UserMetadata.Set(k, v)
} }
content.Internal = &ObjectInternalInfo{
K: object.DataBlocks,
M: object.ParityBlocks,
}
} }
content.Owner = owner content.Owner = owner
content.VersionID = object.VersionID content.VersionID = object.VersionID
@@ -589,7 +602,7 @@ func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delim
// generates an ListObjectsV1 response for the said bucket with other enumerated options. // generates an ListObjectsV1 response for the said bucket with other enumerated options.
func generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingType string, maxKeys int, resp ListObjectsInfo) ListObjectsResponse { func generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingType string, maxKeys int, resp ListObjectsInfo) ListObjectsResponse {
contents := make([]Object, 0, len(resp.Objects)) contents := make([]Object, 0, len(resp.Objects))
owner := Owner{ owner := &Owner{
ID: globalMinioDefaultOwnerID, ID: globalMinioDefaultOwnerID,
DisplayName: "minio", DisplayName: "minio",
} }
@@ -638,10 +651,14 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingTy
// generates an ListObjectsV2 response for the said bucket with other enumerated options. // generates an ListObjectsV2 response for the said bucket with other enumerated options.
func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter, delimiter, encodingType string, fetchOwner, isTruncated bool, maxKeys int, objects []ObjectInfo, prefixes []string, metadata metaCheckFn) ListObjectsV2Response { func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter, delimiter, encodingType string, fetchOwner, isTruncated bool, maxKeys int, objects []ObjectInfo, prefixes []string, metadata metaCheckFn) ListObjectsV2Response {
contents := make([]Object, 0, len(objects)) contents := make([]Object, 0, len(objects))
owner := Owner{ var owner *Owner
if fetchOwner {
owner = &Owner{
ID: globalMinioDefaultOwnerID, ID: globalMinioDefaultOwnerID,
DisplayName: "minio", DisplayName: "minio",
} }
}
data := ListObjectsV2Response{} data := ListObjectsV2Response{}
for _, object := range objects { for _, object := range objects {
@@ -687,6 +704,10 @@ func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter,
} }
content.UserMetadata.Set(k, v) content.UserMetadata.Set(k, v)
} }
content.Internal = &ObjectInternalInfo{
K: object.DataBlocks,
M: object.ParityBlocks,
}
} }
} }
contents = append(contents, content) contents = append(contents, content)
@@ -743,7 +764,7 @@ func generateInitiateMultipartUploadResponse(bucket, key, uploadID string) Initi
// generates CompleteMultipartUploadResponse for given bucket, key, location and ETag. // generates CompleteMultipartUploadResponse for given bucket, key, location and ETag.
func generateCompleteMultpartUploadResponse(bucket, key, location string, oi ObjectInfo) CompleteMultipartUploadResponse { func generateCompleteMultpartUploadResponse(bucket, key, location string, oi ObjectInfo) CompleteMultipartUploadResponse {
cs := oi.decryptChecksums() cs := oi.decryptChecksums(0)
c := CompleteMultipartUploadResponse{ c := CompleteMultipartUploadResponse{
Location: location, Location: location,
Bucket: bucket, Bucket: bucket,
@@ -924,6 +945,8 @@ func writeErrorResponse(ctx context.Context, w http.ResponseWriter, err APIError
} }
func writeErrorResponseHeadersOnly(w http.ResponseWriter, err APIError) { func writeErrorResponseHeadersOnly(w http.ResponseWriter, err APIError) {
w.Header().Set(xMinIOErrCodeHeader, err.Code)
w.Header().Set(xMinIOErrDescHeader, "\""+err.Description+"\"")
writeResponse(w, err.HTTPStatusCode, nil, mimeNone) writeResponse(w, err.HTTPStatusCode, nil, mimeNone)
} }
+2 -2
View File
@@ -245,10 +245,10 @@ func registerAPIRouter(router *mux.Router) {
router.Methods(http.MethodPut).Path("/{object:.+}"). router.Methods(http.MethodPut).Path("/{object:.+}").
HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?"). HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").
HandlerFunc(collectAPIStats("copyobjectpart", maxClients(gz(httpTraceAll(api.CopyObjectPartHandler))))). HandlerFunc(collectAPIStats("copyobjectpart", maxClients(gz(httpTraceAll(api.CopyObjectPartHandler))))).
Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}") Queries("partNumber", "{partNumber:.*}", "uploadId", "{uploadId:.*}")
// PutObjectPart // PutObjectPart
router.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc( router.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
collectAPIStats("putobjectpart", maxClients(gz(httpTraceHdrs(api.PutObjectPartHandler))))).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}") collectAPIStats("putobjectpart", maxClients(gz(httpTraceHdrs(api.PutObjectPartHandler))))).Queries("partNumber", "{partNumber:.*}", "uploadId", "{uploadId:.*}")
// ListObjectParts // ListObjectParts
router.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc( router.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
collectAPIStats("listobjectparts", maxClients(gz(httpTraceAll(api.ListObjectPartsHandler))))).Queries("uploadId", "{uploadId:.*}") collectAPIStats("listobjectparts", maxClients(gz(httpTraceAll(api.ListObjectPartsHandler))))).Queries("uploadId", "{uploadId:.*}")
+308 -301
View File
File diff suppressed because one or more lines are too long
+32 -5
View File
@@ -88,6 +88,18 @@ func isRequestSignStreamingV4(r *http.Request) bool {
r.Method == http.MethodPut r.Method == http.MethodPut
} }
// Verify if the request has AWS Streaming Signature Version '4'. This is only valid for 'PUT' operation.
func isRequestSignStreamingTrailerV4(r *http.Request) bool {
return r.Header.Get(xhttp.AmzContentSha256) == streamingContentSHA256Trailer &&
r.Method == http.MethodPut
}
// Verify if the request has AWS Streaming Signature Version '4', with unsigned content and trailer.
func isRequestUnsignedTrailerV4(r *http.Request) bool {
return r.Header.Get(xhttp.AmzContentSha256) == unsignedPayloadTrailer &&
r.Method == http.MethodPut && strings.Contains(r.Header.Get(xhttp.ContentEncoding), streamingContentEncoding)
}
// Authorization type. // Authorization type.
// //
//go:generate stringer -type=authType -trimprefix=authType $GOFILE //go:generate stringer -type=authType -trimprefix=authType $GOFILE
@@ -105,10 +117,12 @@ const (
authTypeSignedV2 authTypeSignedV2
authTypeJWT authTypeJWT
authTypeSTS authTypeSTS
authTypeStreamingSignedTrailer
authTypeStreamingUnsignedTrailer
) )
// Get request authentication type. // Get request authentication type.
func getRequestAuthType(r *http.Request) authType { func getRequestAuthType(r *http.Request) (at authType) {
if r.URL != nil { if r.URL != nil {
var err error var err error
r.Form, err = url.ParseQuery(r.URL.RawQuery) r.Form, err = url.ParseQuery(r.URL.RawQuery)
@@ -123,6 +137,10 @@ func getRequestAuthType(r *http.Request) authType {
return authTypePresignedV2 return authTypePresignedV2
} else if isRequestSignStreamingV4(r) { } else if isRequestSignStreamingV4(r) {
return authTypeStreamingSigned return authTypeStreamingSigned
} else if isRequestSignStreamingTrailerV4(r) {
return authTypeStreamingSignedTrailer
} else if isRequestUnsignedTrailerV4(r) {
return authTypeStreamingUnsignedTrailer
} else if isRequestSignatureV4(r) { } else if isRequestSignatureV4(r) {
return authTypeSigned return authTypeSigned
} else if isRequestPresignedSignatureV4(r) { } else if isRequestPresignedSignatureV4(r) {
@@ -567,6 +585,8 @@ var supportedS3AuthTypes = map[authType]struct{}{
authTypeSignedV2: {}, authTypeSignedV2: {},
authTypePostPolicy: {}, authTypePostPolicy: {},
authTypeStreamingSigned: {}, authTypeStreamingSigned: {},
authTypeStreamingSignedTrailer: {},
authTypeStreamingUnsignedTrailer: {},
} }
// Validate if the authType is valid and supported. // Validate if the authType is valid and supported.
@@ -582,7 +602,8 @@ func setAuthHandler(h http.Handler) http.Handler {
tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt) tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
aType := getRequestAuthType(r) aType := getRequestAuthType(r)
if aType == authTypeSigned || aType == authTypeSignedV2 || aType == authTypeStreamingSigned { switch aType {
case authTypeSigned, authTypeSignedV2, authTypeStreamingSigned, authTypeStreamingSignedTrailer:
// Verify if date headers are set, if not reject the request // Verify if date headers are set, if not reject the request
amzDate, errCode := parseAmzDateHeader(r) amzDate, errCode := parseAmzDateHeader(r)
if errCode != ErrNone { if errCode != ErrNone {
@@ -613,10 +634,16 @@ func setAuthHandler(h http.Handler) http.Handler {
atomic.AddUint64(&globalHTTPStats.rejectedRequestsTime, 1) atomic.AddUint64(&globalHTTPStats.rejectedRequestsTime, 1)
return return
} }
}
if isSupportedS3AuthType(aType) || aType == authTypeJWT || aType == authTypeSTS {
h.ServeHTTP(w, r) h.ServeHTTP(w, r)
return return
case authTypeJWT, authTypeSTS:
h.ServeHTTP(w, r)
return
default:
if isSupportedS3AuthType(aType) {
h.ServeHTTP(w, r)
return
}
} }
if ok { if ok {
@@ -710,7 +737,7 @@ func isPutActionAllowed(ctx context.Context, atype authType, bucketName, objectN
return ErrSignatureVersionNotSupported return ErrSignatureVersionNotSupported
case authTypeSignedV2, authTypePresignedV2: case authTypeSignedV2, authTypePresignedV2:
cred, owner, s3Err = getReqAccessKeyV2(r) cred, owner, s3Err = getReqAccessKeyV2(r)
case authTypeStreamingSigned, authTypePresigned, authTypeSigned: case authTypeStreamingSigned, authTypePresigned, authTypeSigned, authTypeStreamingSignedTrailer, authTypeStreamingUnsignedTrailer:
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3) cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
} }
if s3Err != ErrNone { if s3Err != ErrNone {
+5 -4
View File
@@ -375,8 +375,6 @@ func TestIsReqAuthenticated(t *testing.T) {
initConfigSubsystem(ctx, objLayer) initConfigSubsystem(ctx, objLayer)
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
creds, err := auth.CreateCredentials("myuser", "mypassword") creds, err := auth.CreateCredentials("myuser", "mypassword")
if err != nil { if err != nil {
t.Fatalf("unable create credential, %s", err) t.Fatalf("unable create credential, %s", err)
@@ -384,6 +382,8 @@ func TestIsReqAuthenticated(t *testing.T) {
globalActiveCred = creds globalActiveCred = creds
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
// List of test cases for validating http request authentication. // List of test cases for validating http request authentication.
testCases := []struct { testCases := []struct {
req *http.Request req *http.Request
@@ -464,9 +464,8 @@ func TestValidateAdminSignature(t *testing.T) {
} }
initAllSubsystems(ctx) initAllSubsystems(ctx)
initConfigSubsystem(ctx, objLayer)
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second) initConfigSubsystem(ctx, objLayer)
creds, err := auth.CreateCredentials("admin", "mypassword") creds, err := auth.CreateCredentials("admin", "mypassword")
if err != nil { if err != nil {
@@ -474,6 +473,8 @@ func TestValidateAdminSignature(t *testing.T) {
} }
globalActiveCred = creds globalActiveCred = creds
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
testCases := []struct { testCases := []struct {
AccessKey string AccessKey string
SecretKey string SecretKey string
+4 -2
View File
@@ -18,11 +18,13 @@ func _() {
_ = x[authTypeSignedV2-7] _ = x[authTypeSignedV2-7]
_ = x[authTypeJWT-8] _ = x[authTypeJWT-8]
_ = x[authTypeSTS-9] _ = x[authTypeSTS-9]
_ = x[authTypeStreamingSignedTrailer-10]
_ = x[authTypeStreamingUnsignedTrailer-11]
} }
const _authType_name = "UnknownAnonymousPresignedPresignedV2PostPolicyStreamingSignedSignedSignedV2JWTSTS" const _authType_name = "UnknownAnonymousPresignedPresignedV2PostPolicyStreamingSignedSignedSignedV2JWTSTSStreamingSignedTrailerStreamingUnsignedTrailer"
var _authType_index = [...]uint8{0, 7, 16, 25, 36, 46, 61, 67, 75, 78, 81} var _authType_index = [...]uint8{0, 7, 16, 25, 36, 46, 61, 67, 75, 78, 81, 103, 127}
func (i authType) String() string { func (i authType) String() string {
if i < 0 || i >= authType(len(_authType_index)-1) { if i < 0 || i >= authType(len(_authType_index)-1) {
+1 -1
View File
@@ -24,7 +24,7 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/minio/pkg/env" "github.com/minio/pkg/env"
) )
+94 -12
View File
@@ -26,12 +26,15 @@ import (
"os" "os"
"sort" "sort"
"strings" "strings"
"sync"
"time" "time"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/minio/internal/config"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/minio/pkg/env"
) )
const ( const (
@@ -44,6 +47,7 @@ const (
// healingTracker is used to persist healing information during a heal. // healingTracker is used to persist healing information during a heal.
type healingTracker struct { type healingTracker struct {
disk StorageAPI `msg:"-"` disk StorageAPI `msg:"-"`
mu *sync.RWMutex `msg:"-"`
ID string ID string
PoolIndex int PoolIndex int
@@ -112,22 +116,76 @@ func loadHealingTracker(ctx context.Context, disk StorageAPI) (*healingTracker,
} }
h.disk = disk h.disk = disk
h.ID = diskID h.ID = diskID
h.mu = &sync.RWMutex{}
return &h, nil return &h, nil
} }
// newHealingTracker will create a new healing tracker for the disk. // newHealingTracker will create a new healing tracker for the disk.
func newHealingTracker(disk StorageAPI, healID string) *healingTracker { func newHealingTracker() *healingTracker {
diskID, _ := disk.GetDiskID() return &healingTracker{
h := healingTracker{ mu: &sync.RWMutex{},
disk: disk,
ID: diskID,
HealID: healID,
Path: disk.String(),
Endpoint: disk.Endpoint().String(),
Started: time.Now().UTC(),
} }
}
func initHealingTracker(disk StorageAPI, healID string) *healingTracker {
h := newHealingTracker()
diskID, _ := disk.GetDiskID()
h.disk = disk
h.ID = diskID
h.HealID = healID
h.Path = disk.String()
h.Endpoint = disk.Endpoint().String()
h.Started = time.Now().UTC()
h.PoolIndex, h.SetIndex, h.DiskIndex = disk.GetDiskLoc() h.PoolIndex, h.SetIndex, h.DiskIndex = disk.GetDiskLoc()
return &h return h
}
func (h healingTracker) getLastUpdate() time.Time {
h.mu.RLock()
defer h.mu.RUnlock()
return h.LastUpdate
}
func (h healingTracker) getBucket() string {
h.mu.RLock()
defer h.mu.RUnlock()
return h.Bucket
}
func (h *healingTracker) setBucket(bucket string) {
h.mu.Lock()
defer h.mu.Unlock()
h.Bucket = bucket
}
func (h healingTracker) getObject() string {
h.mu.RLock()
defer h.mu.RUnlock()
return h.Object
}
func (h *healingTracker) setObject(object string) {
h.mu.Lock()
defer h.mu.Unlock()
h.Object = object
}
func (h *healingTracker) updateProgress(success bool, bytes uint64) {
h.mu.Lock()
defer h.mu.Unlock()
if success {
h.ItemsHealed++
h.BytesDone += bytes
} else {
h.ItemsFailed++
h.BytesFailed += bytes
}
} }
// update will update the tracker on the disk. // update will update the tracker on the disk.
@@ -136,15 +194,18 @@ func (h *healingTracker) update(ctx context.Context) error {
if h.disk.Healing() == nil { if h.disk.Healing() == nil {
return fmt.Errorf("healingTracker: drive %q is not marked as healing", h.ID) return fmt.Errorf("healingTracker: drive %q is not marked as healing", h.ID)
} }
h.mu.Lock()
if h.ID == "" || h.PoolIndex < 0 || h.SetIndex < 0 || h.DiskIndex < 0 { if h.ID == "" || h.PoolIndex < 0 || h.SetIndex < 0 || h.DiskIndex < 0 {
h.ID, _ = h.disk.GetDiskID() h.ID, _ = h.disk.GetDiskID()
h.PoolIndex, h.SetIndex, h.DiskIndex = h.disk.GetDiskLoc() h.PoolIndex, h.SetIndex, h.DiskIndex = h.disk.GetDiskLoc()
} }
h.mu.Unlock()
return h.save(ctx) return h.save(ctx)
} }
// save will unconditionally save the tracker and will be created if not existing. // save will unconditionally save the tracker and will be created if not existing.
func (h *healingTracker) save(ctx context.Context) error { func (h *healingTracker) save(ctx context.Context) error {
h.mu.Lock()
if h.PoolIndex < 0 || h.SetIndex < 0 || h.DiskIndex < 0 { if h.PoolIndex < 0 || h.SetIndex < 0 || h.DiskIndex < 0 {
// Attempt to get location. // Attempt to get location.
if api := newObjectLayerFn(); api != nil { if api := newObjectLayerFn(); api != nil {
@@ -155,6 +216,7 @@ func (h *healingTracker) save(ctx context.Context) error {
} }
h.LastUpdate = time.Now().UTC() h.LastUpdate = time.Now().UTC()
htrackerBytes, err := h.MarshalMsg(nil) htrackerBytes, err := h.MarshalMsg(nil)
h.mu.Unlock()
if err != nil { if err != nil {
return err return err
} }
@@ -176,6 +238,8 @@ func (h *healingTracker) delete(ctx context.Context) error {
} }
func (h *healingTracker) isHealed(bucket string) bool { func (h *healingTracker) isHealed(bucket string) bool {
h.mu.RLock()
defer h.mu.RUnlock()
for _, v := range h.HealedBuckets { for _, v := range h.HealedBuckets {
if v == bucket { if v == bucket {
return true return true
@@ -186,6 +250,9 @@ func (h *healingTracker) isHealed(bucket string) bool {
// resume will reset progress to the numbers at the start of the bucket. // resume will reset progress to the numbers at the start of the bucket.
func (h *healingTracker) resume() { func (h *healingTracker) resume() {
h.mu.Lock()
defer h.mu.Unlock()
h.ItemsHealed = h.ResumeItemsHealed h.ItemsHealed = h.ResumeItemsHealed
h.ItemsFailed = h.ResumeItemsFailed h.ItemsFailed = h.ResumeItemsFailed
h.BytesDone = h.ResumeBytesDone h.BytesDone = h.ResumeBytesDone
@@ -195,6 +262,9 @@ func (h *healingTracker) resume() {
// bucketDone should be called when a bucket is done healing. // bucketDone should be called when a bucket is done healing.
// Adds the bucket to the list of healed buckets and updates resume numbers. // Adds the bucket to the list of healed buckets and updates resume numbers.
func (h *healingTracker) bucketDone(bucket string) { func (h *healingTracker) bucketDone(bucket string) {
h.mu.Lock()
defer h.mu.Unlock()
h.ResumeItemsHealed = h.ItemsHealed h.ResumeItemsHealed = h.ItemsHealed
h.ResumeItemsFailed = h.ItemsFailed h.ResumeItemsFailed = h.ItemsFailed
h.ResumeBytesDone = h.BytesDone h.ResumeBytesDone = h.BytesDone
@@ -211,6 +281,9 @@ func (h *healingTracker) bucketDone(bucket string) {
// setQueuedBuckets will add buckets, but exclude any that is already in h.HealedBuckets. // setQueuedBuckets will add buckets, but exclude any that is already in h.HealedBuckets.
// Order is preserved. // Order is preserved.
func (h *healingTracker) setQueuedBuckets(buckets []BucketInfo) { func (h *healingTracker) setQueuedBuckets(buckets []BucketInfo) {
h.mu.Lock()
defer h.mu.Unlock()
s := set.CreateStringSet(h.HealedBuckets...) s := set.CreateStringSet(h.HealedBuckets...)
h.QueuedBuckets = make([]string, 0, len(buckets)) h.QueuedBuckets = make([]string, 0, len(buckets))
for _, b := range buckets { for _, b := range buckets {
@@ -221,15 +294,22 @@ func (h *healingTracker) setQueuedBuckets(buckets []BucketInfo) {
} }
func (h *healingTracker) printTo(writer io.Writer) { func (h *healingTracker) printTo(writer io.Writer) {
h.mu.RLock()
defer h.mu.RUnlock()
b, err := json.MarshalIndent(h, "", " ") b, err := json.MarshalIndent(h, "", " ")
if err != nil { if err != nil {
writer.Write([]byte(err.Error())) writer.Write([]byte(err.Error()))
return
} }
writer.Write(b) writer.Write(b)
} }
// toHealingDisk converts the information to madmin.HealingDisk // toHealingDisk converts the information to madmin.HealingDisk
func (h *healingTracker) toHealingDisk() madmin.HealingDisk { func (h *healingTracker) toHealingDisk() madmin.HealingDisk {
h.mu.RLock()
defer h.mu.RUnlock()
return madmin.HealingDisk{ return madmin.HealingDisk{
ID: h.ID, ID: h.ID,
HealID: h.HealID, HealID: h.HealID,
@@ -267,8 +347,10 @@ func initAutoHeal(ctx context.Context, objAPI ObjectLayer) {
globalBackgroundHealState.pushHealLocalDisks(getLocalDisksToHeal()...) globalBackgroundHealState.pushHealLocalDisks(getLocalDisksToHeal()...)
if env.Get("_MINIO_AUTO_DISK_HEALING", config.EnableOn) == config.EnableOn {
go monitorLocalDisksAndHeal(ctx, z) go monitorLocalDisksAndHeal(ctx, z)
} }
}
func getLocalDisksToHeal() (disksToHeal Endpoints) { func getLocalDisksToHeal() (disksToHeal Endpoints) {
globalLocalDrivesMu.RLock() globalLocalDrivesMu.RLock()
@@ -336,7 +418,7 @@ func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint
return nil return nil
} }
logger.LogIf(ctx, fmt.Errorf("Unable to load healing tracker on '%s': %w, re-initializing..", disk, err)) logger.LogIf(ctx, fmt.Errorf("Unable to load healing tracker on '%s': %w, re-initializing..", disk, err))
tracker = newHealingTracker(disk, mustGetUUID()) tracker = initHealingTracker(disk, mustGetUUID())
} }
logger.Info(fmt.Sprintf("Healing drive '%s' - 'mc admin heal alias/ --verbose' to check the current status.", endpoint)) logger.Info(fmt.Sprintf("Healing drive '%s' - 'mc admin heal alias/ --verbose' to check the current status.", endpoint))
+134 -22
View File
@@ -37,8 +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/v2" "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"
@@ -48,11 +47,11 @@ 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/minio/minio/internal/workers"
"github.com/minio/pkg/console" "github.com/minio/pkg/console"
"github.com/minio/pkg/env" "github.com/minio/pkg/env"
iampolicy "github.com/minio/pkg/iam/policy" iampolicy "github.com/minio/pkg/iam/policy"
"github.com/minio/pkg/wildcard" "github.com/minio/pkg/wildcard"
"github.com/minio/pkg/workers"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@@ -86,6 +85,7 @@ import (
// type: "minio" // type: "minio"
// bucket: "testbucket1" // bucket: "testbucket1"
// endpoint: "https://play.min.io" // endpoint: "https://play.min.io"
// path: "on"
// credentials: // credentials:
// accessKey: "minioadmin" // accessKey: "minioadmin"
// secretKey: "minioadmin" // secretKey: "minioadmin"
@@ -174,15 +174,22 @@ type BatchJobReplicateResourceType string
func (t BatchJobReplicateResourceType) Validate() error { func (t BatchJobReplicateResourceType) Validate() error {
switch t { switch t {
case BatchJobReplicateResourceMinIO: case BatchJobReplicateResourceMinIO:
case BatchJobReplicateResourceS3:
default: default:
return errInvalidArgument return errInvalidArgument
} }
return nil return nil
} }
func (t BatchJobReplicateResourceType) isMinio() bool {
return t == BatchJobReplicateResourceMinIO
}
// Different types of batch jobs.. // Different types of batch jobs..
const ( const (
BatchJobReplicateResourceMinIO BatchJobReplicateResourceType = "minio" BatchJobReplicateResourceMinIO BatchJobReplicateResourceType = "minio"
BatchJobReplicateResourceS3 BatchJobReplicateResourceType = "s3"
// add future targets // add future targets
) )
@@ -214,9 +221,15 @@ type BatchJobReplicateTarget struct {
Bucket string `yaml:"bucket" json:"bucket"` Bucket string `yaml:"bucket" json:"bucket"`
Prefix string `yaml:"prefix" json:"prefix"` Prefix string `yaml:"prefix" json:"prefix"`
Endpoint string `yaml:"endpoint" json:"endpoint"` Endpoint string `yaml:"endpoint" json:"endpoint"`
Path string `yaml:"path" json:"path"`
Creds BatchJobReplicateCredentials `yaml:"credentials" json:"credentials"` Creds BatchJobReplicateCredentials `yaml:"credentials" json:"credentials"`
} }
// ValidPath returns true if path is valid
func (t BatchJobReplicateTarget) ValidPath() bool {
return t.Path == "on" || t.Path == "off" || t.Path == "auto" || t.Path == ""
}
// BatchJobReplicateSource describes source element of the replication job that is // BatchJobReplicateSource describes source element of the replication job that is
// the source of the data for the target // the source of the data for the target
type BatchJobReplicateSource struct { type BatchJobReplicateSource struct {
@@ -224,9 +237,20 @@ type BatchJobReplicateSource struct {
Bucket string `yaml:"bucket" json:"bucket"` Bucket string `yaml:"bucket" json:"bucket"`
Prefix string `yaml:"prefix" json:"prefix"` Prefix string `yaml:"prefix" json:"prefix"`
Endpoint string `yaml:"endpoint" json:"endpoint"` Endpoint string `yaml:"endpoint" json:"endpoint"`
Path string `yaml:"path" json:"path"`
Creds BatchJobReplicateCredentials `yaml:"credentials" json:"credentials"` Creds BatchJobReplicateCredentials `yaml:"credentials" json:"credentials"`
} }
// ValidPath returns true if path is valid
func (s BatchJobReplicateSource) ValidPath() bool {
switch s.Path {
case "on", "off", "auto", "":
return true
default:
return false
}
}
// BatchJobReplicateV1 v1 of batch job replication // BatchJobReplicateV1 v1 of batch job replication
type BatchJobReplicateV1 struct { type BatchJobReplicateV1 struct {
APIVersion string `yaml:"apiVersion" json:"apiVersion"` APIVersion string `yaml:"apiVersion" json:"apiVersion"`
@@ -286,7 +310,7 @@ func (r BatchJobReplicateV1) Notify(ctx context.Context, body io.Reader) error {
} }
// ReplicateFromSource - this is not implemented yet where source is 'remote' and target is local. // ReplicateFromSource - this is not implemented yet where source is 'remote' and target is local.
func (r *BatchJobReplicateV1) ReplicateFromSource(ctx context.Context, api ObjectLayer, core *minio.Core, srcObjInfo ObjectInfo, retry bool) error { func (r *BatchJobReplicateV1) ReplicateFromSource(ctx context.Context, api ObjectLayer, core *miniogo.Core, srcObjInfo ObjectInfo, retry bool) error {
srcBucket := r.Source.Bucket srcBucket := r.Source.Bucket
tgtBucket := r.Target.Bucket tgtBucket := r.Target.Bucket
srcObject := srcObjInfo.Name srcObject := srcObjInfo.Name
@@ -297,10 +321,13 @@ func (r *BatchJobReplicateV1) ReplicateFromSource(ctx context.Context, api Objec
versioned := globalBucketVersioningSys.PrefixEnabled(tgtBucket, tgtObject) versioned := globalBucketVersioningSys.PrefixEnabled(tgtBucket, tgtObject)
versionSuspended := globalBucketVersioningSys.PrefixSuspended(tgtBucket, tgtObject) versionSuspended := globalBucketVersioningSys.PrefixSuspended(tgtBucket, tgtObject)
versionID := srcObjInfo.VersionID
if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 {
versionID = ""
}
if srcObjInfo.DeleteMarker { if srcObjInfo.DeleteMarker {
_, err := api.DeleteObject(ctx, tgtBucket, tgtObject, ObjectOptions{ _, err := api.DeleteObject(ctx, tgtBucket, tgtObject, ObjectOptions{
VersionID: srcObjInfo.VersionID, VersionID: versionID,
VersionSuspended: versionSuspended, VersionSuspended: versionSuspended,
Versioned: versioned, Versioned: versioned,
MTime: srcObjInfo.ModTime, MTime: srcObjInfo.ModTime,
@@ -318,6 +345,9 @@ func (r *BatchJobReplicateV1) ReplicateFromSource(ctx context.Context, api Objec
PreserveETag: srcObjInfo.ETag, PreserveETag: srcObjInfo.ETag,
UserDefined: srcObjInfo.UserDefined, UserDefined: srcObjInfo.UserDefined,
} }
if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 {
opts.VersionID = ""
}
if crypto.S3.IsEncrypted(srcObjInfo.UserDefined) { if crypto.S3.IsEncrypted(srcObjInfo.UserDefined) {
opts.ServerSideEncryption = encrypt.NewSSE() opts.ServerSideEncryption = encrypt.NewSSE()
} }
@@ -329,7 +359,7 @@ func (r *BatchJobReplicateV1) ReplicateFromSource(ctx context.Context, api Objec
} }
return r.copyWithMultipartfromSource(ctx, api, core, srcObjInfo, opts, partsCount) return r.copyWithMultipartfromSource(ctx, api, core, srcObjInfo, opts, partsCount)
} }
gopts := minio.GetObjectOptions{ gopts := miniogo.GetObjectOptions{
VersionID: srcObjInfo.VersionID, VersionID: srcObjInfo.VersionID,
} }
if err := gopts.SetMatchETag(srcObjInfo.ETag); err != nil { if err := gopts.SetMatchETag(srcObjInfo.ETag); err != nil {
@@ -350,7 +380,7 @@ func (r *BatchJobReplicateV1) ReplicateFromSource(ctx context.Context, api Objec
return err return err
} }
func (r *BatchJobReplicateV1) copyWithMultipartfromSource(ctx context.Context, api ObjectLayer, c *minio.Core, srcObjInfo ObjectInfo, opts ObjectOptions, partsCount int) (err error) { func (r *BatchJobReplicateV1) copyWithMultipartfromSource(ctx context.Context, api ObjectLayer, c *miniogo.Core, srcObjInfo ObjectInfo, opts ObjectOptions, partsCount int) (err error) {
srcBucket := r.Source.Bucket srcBucket := r.Source.Bucket
tgtBucket := r.Target.Bucket tgtBucket := r.Target.Bucket
srcObject := srcObjInfo.Name srcObject := srcObjInfo.Name
@@ -358,7 +388,9 @@ func (r *BatchJobReplicateV1) copyWithMultipartfromSource(ctx context.Context, a
if r.Target.Prefix != "" { if r.Target.Prefix != "" {
tgtObject = path.Join(r.Target.Prefix, srcObjInfo.Name) tgtObject = path.Join(r.Target.Prefix, srcObjInfo.Name)
} }
if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 {
opts.VersionID = ""
}
var uploadedParts []CompletePart var uploadedParts []CompletePart
res, err := api.NewMultipartUpload(context.Background(), tgtBucket, tgtObject, opts) res, err := api.NewMultipartUpload(context.Background(), tgtBucket, tgtObject, opts)
if err != nil { if err != nil {
@@ -389,7 +421,7 @@ func (r *BatchJobReplicateV1) copyWithMultipartfromSource(ctx context.Context, a
) )
for i := 0; i < partsCount; i++ { for i := 0; i < partsCount; i++ {
gopts := minio.GetObjectOptions{ gopts := miniogo.GetObjectOptions{
VersionID: srcObjInfo.VersionID, VersionID: srcObjInfo.VersionID,
PartNumber: i + 1, PartNumber: i + 1,
} }
@@ -402,7 +434,7 @@ func (r *BatchJobReplicateV1) copyWithMultipartfromSource(ctx context.Context, a
} }
defer rd.Close() defer rd.Close()
hr, err = hash.NewReader(rd, objInfo.Size, "", "", objInfo.Size) hr, err = hash.NewReader(io.LimitReader(rd, objInfo.Size), objInfo.Size, "", "", objInfo.Size)
if err != nil { if err != nil {
return err return err
} }
@@ -516,13 +548,14 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
Creds: credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken), Creds: credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
Secure: u.Scheme == "https", Secure: u.Scheme == "https",
Transport: getRemoteInstanceTransport, Transport: getRemoteInstanceTransport,
BucketLookup: lookupStyle(r.Source.Path),
}) })
if err != nil { if err != nil {
return err return err
} }
c.SetAppInfo("minio-"+batchJobPrefix, r.APIVersion+" "+job.ID) c.SetAppInfo("minio-"+batchJobPrefix, r.APIVersion+" "+job.ID)
core := &minio.Core{Client: c} core := &miniogo.Core{Client: c}
workerSize, err := strconv.Atoi(env.Get("_MINIO_BATCH_REPLICATION_WORKERS", strconv.Itoa(runtime.GOMAXPROCS(0)/2))) workerSize, err := strconv.Atoi(env.Get("_MINIO_BATCH_REPLICATION_WORKERS", strconv.Itoa(runtime.GOMAXPROCS(0)/2)))
if err != nil { if err != nil {
@@ -539,19 +572,47 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
retry := false retry := false
for attempts := 1; attempts <= retryAttempts; attempts++ { for attempts := 1; attempts <= retryAttempts; attempts++ {
attempts := attempts attempts := attempts
// one of source/target is s3, skip delete marker and all versions under the same object name.
s3Type := r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3
minioSrc := r.Source.Type == BatchJobReplicateResourceMinIO
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
objInfoCh := c.ListObjects(ctx, r.Source.Bucket, minio.ListObjectsOptions{ objInfoCh := c.ListObjects(ctx, r.Source.Bucket, miniogo.ListObjectsOptions{
Prefix: r.Source.Prefix, Prefix: r.Source.Prefix,
WithVersions: true, WithVersions: minioSrc,
Recursive: true, Recursive: true,
WithMetadata: true, WithMetadata: true,
}) })
prevObj := ""
skipReplicate := false
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 {
oi2, err := c.StatObject(ctx, r.Source.Bucket, obj.Key, miniogo.StatObjectOptions{})
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)) {
continue
}
logger.LogIf(ctx, err)
cancel()
return err
}
}
if skip(oi) { if skip(oi) {
continue continue
} }
if obj.Key != prevObj {
prevObj = obj.Key
// skip replication of delete marker and all versions under the same object name if one of source or target is s3.
skipReplicate = obj.IsDeleteMarker && s3Type
}
if skipReplicate {
continue
}
wk.Take() wk.Take()
go func() { go func() {
defer wk.Give() defer wk.Give()
@@ -609,7 +670,7 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
} }
// toObjectInfo converts minio.ObjectInfo to ObjectInfo // toObjectInfo converts minio.ObjectInfo to ObjectInfo
func toObjectInfo(bucket, object string, objInfo minio.ObjectInfo) ObjectInfo { func toObjectInfo(bucket, object string, objInfo miniogo.ObjectInfo) ObjectInfo {
tags, _ := tags.MapToObjectTags(objInfo.UserTags) tags, _ := tags.MapToObjectTags(objInfo.UserTags)
oi := ObjectInfo{ oi := ObjectInfo{
Bucket: bucket, Bucket: bucket,
@@ -646,9 +707,10 @@ func (r *BatchJobReplicateV1) ReplicateToTarget(ctx context.Context, api ObjectL
tgtBucket := r.Target.Bucket tgtBucket := r.Target.Bucket
tgtPrefix := r.Target.Prefix tgtPrefix := r.Target.Prefix
srcObject := srcObjInfo.Name srcObject := srcObjInfo.Name
s3Type := r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3
if srcObjInfo.DeleteMarker || !srcObjInfo.VersionPurgeStatus.Empty() { if srcObjInfo.DeleteMarker || !srcObjInfo.VersionPurgeStatus.Empty() {
if retry { if retry && !s3Type {
if _, err := c.StatObject(ctx, tgtBucket, pathJoin(tgtPrefix, srcObject), miniogo.StatObjectOptions{ if _, err := c.StatObject(ctx, tgtBucket, pathJoin(tgtPrefix, srcObject), miniogo.StatObjectOptions{
VersionID: srcObjInfo.VersionID, VersionID: srcObjInfo.VersionID,
Internal: miniogo.AdvancedGetOptions{ Internal: miniogo.AdvancedGetOptions{
@@ -664,7 +726,10 @@ func (r *BatchJobReplicateV1) ReplicateToTarget(ctx context.Context, api ObjectL
if srcObjInfo.VersionPurgeStatus.Empty() { if srcObjInfo.VersionPurgeStatus.Empty() {
dmVersionID = srcObjInfo.VersionID dmVersionID = srcObjInfo.VersionID
} }
if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 {
dmVersionID = ""
versionID = ""
}
return c.RemoveObject(ctx, tgtBucket, pathJoin(tgtPrefix, srcObject), miniogo.RemoveObjectOptions{ return c.RemoveObject(ctx, tgtBucket, pathJoin(tgtPrefix, srcObject), miniogo.RemoveObjectOptions{
VersionID: versionID, VersionID: versionID,
Internal: miniogo.AdvancedRemoveOptions{ Internal: miniogo.AdvancedRemoveOptions{
@@ -676,7 +741,7 @@ func (r *BatchJobReplicateV1) ReplicateToTarget(ctx context.Context, api ObjectL
}) })
} }
if retry { // when we are retrying avoid copying if necessary. if retry && !s3Type { // when we are retrying avoid copying if necessary.
gopts := miniogo.GetObjectOptions{} gopts := miniogo.GetObjectOptions{}
if err := gopts.SetMatchETag(srcObjInfo.ETag); err != nil { if err := gopts.SetMatchETag(srcObjInfo.ETag); err != nil {
return err return err
@@ -694,7 +759,7 @@ func (r *BatchJobReplicateV1) ReplicateToTarget(ctx context.Context, api ObjectL
Versioned: versioned, Versioned: versioned,
VersionSuspended: versionSuspended, VersionSuspended: versionSuspended,
} }
rd, err := api.GetObjectNInfo(ctx, srcBucket, srcObject, nil, http.Header{}, readLock, opts) rd, err := api.GetObjectNInfo(ctx, srcBucket, srcObject, nil, http.Header{}, opts)
if err != nil { if err != nil {
return err return err
} }
@@ -710,7 +775,9 @@ func (r *BatchJobReplicateV1) ReplicateToTarget(ctx context.Context, api ObjectL
if err != nil { if err != nil {
return err return err
} }
if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 {
putOpts.Internal = miniogo.AdvancedPutOptions{}
}
if objInfo.isMultipart() { if objInfo.isMultipart() {
if err := replicateObjectWithMultipart(ctx, c, tgtBucket, pathJoin(tgtPrefix, objInfo.Name), rd, objInfo, putOpts); err != nil { if err := replicateObjectWithMultipart(ctx, c, tgtBucket, pathJoin(tgtPrefix, objInfo.Name), rd, objInfo, putOpts); err != nil {
return err return err
@@ -1021,6 +1088,10 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
// None of the provided metadata filters match skip the object. // None of the provided metadata filters match skip the object.
return false return false
} }
// if one of source or target is non MinIO, just replicate the top most version like `mc mirror`
if (r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3) && !info.IsLatest {
return false
}
return true return true
} }
@@ -1036,6 +1107,7 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
Creds: credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken), Creds: credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
Secure: u.Scheme == "https", Secure: u.Scheme == "https",
Transport: getRemoteInstanceTransport, Transport: getRemoteInstanceTransport,
BucketLookup: lookupStyle(r.Target.Path),
}) })
if err != nil { if err != nil {
return err return err
@@ -1059,6 +1131,8 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
attempts := attempts attempts := attempts
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
// one of source/target is s3, skip delete marker and all versions under the same object name.
s3Type := r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3
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{
@@ -1070,8 +1144,18 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
return err return err
} }
prevObj := ""
skipReplicate := false
for result := range results { for result := range results {
result := result result := result
if result.Name != prevObj {
prevObj = result.Name
skipReplicate = result.DeleteMarker && s3Type
}
if skipReplicate {
continue
}
wk.Take() wk.Take()
go func() { go func() {
defer wk.Give() defer wk.Give()
@@ -1195,6 +1279,13 @@ func (r *BatchJobReplicateV1) Validate(ctx context.Context, job BatchJobRequest,
return errInvalidArgument return errInvalidArgument
} }
if r.Source.Endpoint != "" && !r.Source.Type.isMinio() && !r.Source.ValidPath() {
return errInvalidArgument
}
if r.Target.Endpoint != "" && !r.Target.Type.isMinio() && !r.Target.ValidPath() {
return errInvalidArgument
}
if r.Target.Bucket == "" { if r.Target.Bucket == "" {
return errInvalidArgument return errInvalidArgument
} }
@@ -1232,11 +1323,14 @@ func (r *BatchJobReplicateV1) Validate(ctx context.Context, job BatchJobRequest,
remoteEp := r.Target.Endpoint remoteEp := r.Target.Endpoint
remoteBkt := r.Target.Bucket remoteBkt := r.Target.Bucket
cred := r.Target.Creds cred := r.Target.Creds
pathStyle := r.Target.Path
if r.Source.Endpoint != "" { if r.Source.Endpoint != "" {
remoteEp = r.Source.Endpoint remoteEp = r.Source.Endpoint
cred = r.Source.Creds cred = r.Source.Creds
remoteBkt = r.Source.Bucket remoteBkt = r.Source.Bucket
pathStyle = r.Source.Path
} }
u, err := url.Parse(remoteEp) u, err := url.Parse(remoteEp)
@@ -1248,6 +1342,7 @@ func (r *BatchJobReplicateV1) Validate(ctx context.Context, job BatchJobRequest,
Creds: credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken), Creds: credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
Secure: u.Scheme == "https", Secure: u.Scheme == "https",
Transport: getRemoteInstanceTransport, Transport: getRemoteInstanceTransport,
BucketLookup: lookupStyle(pathStyle),
}) })
if err != nil { if err != nil {
return err return err
@@ -1265,8 +1360,10 @@ func (r *BatchJobReplicateV1) Validate(ctx context.Context, job BatchJobRequest,
} }
return err return err
} }
// if both source and target are minio instances
minioType := r.Target.Type == BatchJobReplicateResourceMinIO && r.Source.Type == BatchJobReplicateResourceMinIO
// If source has versioning enabled, target must have versioning enabled // If source has versioning enabled, target must have versioning enabled
if (info.Versioning && !vcfg.Enabled() && !isRemoteToLocal) || (!info.Versioning && vcfg.Enabled() && isRemoteToLocal) { if minioType && ((info.Versioning && !vcfg.Enabled() && !isRemoteToLocal) || (!info.Versioning && vcfg.Enabled() && isRemoteToLocal)) {
return batchReplicationJobError{ return batchReplicationJobError{
Code: "InvalidBucketState", Code: "InvalidBucketState",
Description: fmt.Sprintf("The source '%s' has versioning enabled, target '%s' must have versioning enabled", Description: fmt.Sprintf("The source '%s' has versioning enabled, target '%s' must have versioning enabled",
@@ -1358,6 +1455,7 @@ func batchReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (p
SourceVersionID: objInfo.VersionID, SourceVersionID: objInfo.VersionID,
SourceMTime: objInfo.ModTime, SourceMTime: objInfo.ModTime,
SourceETag: objInfo.ETag, SourceETag: objInfo.ETag,
ReplicationRequest: true,
} }
return putOpts, nil return putOpts, nil
} }
@@ -1810,3 +1908,17 @@ func (m *batchJobMetrics) trace(d batchJobMetric, job string, attempts int, info
} }
} }
} }
func lookupStyle(s string) miniogo.BucketLookupType {
var lookup miniogo.BucketLookupType
switch s {
case "on":
lookup = miniogo.BucketLookupPath
case "off":
lookup = miniogo.BucketLookupDNS
default:
lookup = miniogo.BucketLookupAuto
}
return lookup
}
+60 -10
View File
@@ -702,6 +702,12 @@ func (z *BatchJobReplicateSource) DecodeMsg(dc *msgp.Reader) (err error) {
err = msgp.WrapError(err, "Endpoint") err = msgp.WrapError(err, "Endpoint")
return return
} }
case "Path":
z.Path, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Path")
return
}
case "Creds": case "Creds":
var zb0003 uint32 var zb0003 uint32
zb0003, err = dc.ReadMapHeader() zb0003, err = dc.ReadMapHeader()
@@ -756,9 +762,9 @@ func (z *BatchJobReplicateSource) DecodeMsg(dc *msgp.Reader) (err error) {
// EncodeMsg implements msgp.Encodable // EncodeMsg implements msgp.Encodable
func (z *BatchJobReplicateSource) EncodeMsg(en *msgp.Writer) (err error) { func (z *BatchJobReplicateSource) EncodeMsg(en *msgp.Writer) (err error) {
// map header, size 5 // map header, size 6
// write "Type" // write "Type"
err = en.Append(0x85, 0xa4, 0x54, 0x79, 0x70, 0x65) err = en.Append(0x86, 0xa4, 0x54, 0x79, 0x70, 0x65)
if err != nil { if err != nil {
return return
} }
@@ -797,6 +803,16 @@ func (z *BatchJobReplicateSource) EncodeMsg(en *msgp.Writer) (err error) {
err = msgp.WrapError(err, "Endpoint") err = msgp.WrapError(err, "Endpoint")
return return
} }
// write "Path"
err = en.Append(0xa4, 0x50, 0x61, 0x74, 0x68)
if err != nil {
return
}
err = en.WriteString(z.Path)
if err != nil {
err = msgp.WrapError(err, "Path")
return
}
// write "Creds" // write "Creds"
err = en.Append(0xa5, 0x43, 0x72, 0x65, 0x64, 0x73) err = en.Append(0xa5, 0x43, 0x72, 0x65, 0x64, 0x73)
if err != nil { if err != nil {
@@ -839,9 +855,9 @@ func (z *BatchJobReplicateSource) EncodeMsg(en *msgp.Writer) (err error) {
// MarshalMsg implements msgp.Marshaler // MarshalMsg implements msgp.Marshaler
func (z *BatchJobReplicateSource) MarshalMsg(b []byte) (o []byte, err error) { func (z *BatchJobReplicateSource) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize()) o = msgp.Require(b, z.Msgsize())
// map header, size 5 // map header, size 6
// string "Type" // string "Type"
o = append(o, 0x85, 0xa4, 0x54, 0x79, 0x70, 0x65) o = append(o, 0x86, 0xa4, 0x54, 0x79, 0x70, 0x65)
o = msgp.AppendString(o, string(z.Type)) o = msgp.AppendString(o, string(z.Type))
// string "Bucket" // string "Bucket"
o = append(o, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74) o = append(o, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
@@ -852,6 +868,9 @@ func (z *BatchJobReplicateSource) MarshalMsg(b []byte) (o []byte, err error) {
// string "Endpoint" // string "Endpoint"
o = append(o, 0xa8, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74) o = append(o, 0xa8, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74)
o = msgp.AppendString(o, z.Endpoint) o = msgp.AppendString(o, z.Endpoint)
// string "Path"
o = append(o, 0xa4, 0x50, 0x61, 0x74, 0x68)
o = msgp.AppendString(o, z.Path)
// string "Creds" // string "Creds"
o = append(o, 0xa5, 0x43, 0x72, 0x65, 0x64, 0x73) o = append(o, 0xa5, 0x43, 0x72, 0x65, 0x64, 0x73)
// map header, size 3 // map header, size 3
@@ -913,6 +932,12 @@ func (z *BatchJobReplicateSource) UnmarshalMsg(bts []byte) (o []byte, err error)
err = msgp.WrapError(err, "Endpoint") err = msgp.WrapError(err, "Endpoint")
return return
} }
case "Path":
z.Path, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Path")
return
}
case "Creds": case "Creds":
var zb0003 uint32 var zb0003 uint32
zb0003, bts, err = msgp.ReadMapHeaderBytes(bts) zb0003, bts, err = msgp.ReadMapHeaderBytes(bts)
@@ -968,7 +993,7 @@ func (z *BatchJobReplicateSource) UnmarshalMsg(bts []byte) (o []byte, err error)
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z *BatchJobReplicateSource) Msgsize() (s int) { func (z *BatchJobReplicateSource) Msgsize() (s int) {
s = 1 + 5 + msgp.StringPrefixSize + len(string(z.Type)) + 7 + msgp.StringPrefixSize + len(z.Bucket) + 7 + msgp.StringPrefixSize + len(z.Prefix) + 9 + msgp.StringPrefixSize + len(z.Endpoint) + 6 + 1 + 10 + msgp.StringPrefixSize + len(z.Creds.AccessKey) + 10 + msgp.StringPrefixSize + len(z.Creds.SecretKey) + 13 + msgp.StringPrefixSize + len(z.Creds.SessionToken) s = 1 + 5 + msgp.StringPrefixSize + len(string(z.Type)) + 7 + msgp.StringPrefixSize + len(z.Bucket) + 7 + msgp.StringPrefixSize + len(z.Prefix) + 9 + msgp.StringPrefixSize + len(z.Endpoint) + 5 + msgp.StringPrefixSize + len(z.Path) + 6 + 1 + 10 + msgp.StringPrefixSize + len(z.Creds.AccessKey) + 10 + msgp.StringPrefixSize + len(z.Creds.SecretKey) + 13 + msgp.StringPrefixSize + len(z.Creds.SessionToken)
return return
} }
@@ -1018,6 +1043,12 @@ func (z *BatchJobReplicateTarget) DecodeMsg(dc *msgp.Reader) (err error) {
err = msgp.WrapError(err, "Endpoint") err = msgp.WrapError(err, "Endpoint")
return return
} }
case "Path":
z.Path, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Path")
return
}
case "Creds": case "Creds":
var zb0003 uint32 var zb0003 uint32
zb0003, err = dc.ReadMapHeader() zb0003, err = dc.ReadMapHeader()
@@ -1072,9 +1103,9 @@ func (z *BatchJobReplicateTarget) DecodeMsg(dc *msgp.Reader) (err error) {
// EncodeMsg implements msgp.Encodable // EncodeMsg implements msgp.Encodable
func (z *BatchJobReplicateTarget) EncodeMsg(en *msgp.Writer) (err error) { func (z *BatchJobReplicateTarget) EncodeMsg(en *msgp.Writer) (err error) {
// map header, size 5 // map header, size 6
// write "Type" // write "Type"
err = en.Append(0x85, 0xa4, 0x54, 0x79, 0x70, 0x65) err = en.Append(0x86, 0xa4, 0x54, 0x79, 0x70, 0x65)
if err != nil { if err != nil {
return return
} }
@@ -1113,6 +1144,16 @@ func (z *BatchJobReplicateTarget) EncodeMsg(en *msgp.Writer) (err error) {
err = msgp.WrapError(err, "Endpoint") err = msgp.WrapError(err, "Endpoint")
return return
} }
// write "Path"
err = en.Append(0xa4, 0x50, 0x61, 0x74, 0x68)
if err != nil {
return
}
err = en.WriteString(z.Path)
if err != nil {
err = msgp.WrapError(err, "Path")
return
}
// write "Creds" // write "Creds"
err = en.Append(0xa5, 0x43, 0x72, 0x65, 0x64, 0x73) err = en.Append(0xa5, 0x43, 0x72, 0x65, 0x64, 0x73)
if err != nil { if err != nil {
@@ -1155,9 +1196,9 @@ func (z *BatchJobReplicateTarget) EncodeMsg(en *msgp.Writer) (err error) {
// MarshalMsg implements msgp.Marshaler // MarshalMsg implements msgp.Marshaler
func (z *BatchJobReplicateTarget) MarshalMsg(b []byte) (o []byte, err error) { func (z *BatchJobReplicateTarget) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize()) o = msgp.Require(b, z.Msgsize())
// map header, size 5 // map header, size 6
// string "Type" // string "Type"
o = append(o, 0x85, 0xa4, 0x54, 0x79, 0x70, 0x65) o = append(o, 0x86, 0xa4, 0x54, 0x79, 0x70, 0x65)
o = msgp.AppendString(o, string(z.Type)) o = msgp.AppendString(o, string(z.Type))
// string "Bucket" // string "Bucket"
o = append(o, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74) o = append(o, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
@@ -1168,6 +1209,9 @@ func (z *BatchJobReplicateTarget) MarshalMsg(b []byte) (o []byte, err error) {
// string "Endpoint" // string "Endpoint"
o = append(o, 0xa8, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74) o = append(o, 0xa8, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74)
o = msgp.AppendString(o, z.Endpoint) o = msgp.AppendString(o, z.Endpoint)
// string "Path"
o = append(o, 0xa4, 0x50, 0x61, 0x74, 0x68)
o = msgp.AppendString(o, z.Path)
// string "Creds" // string "Creds"
o = append(o, 0xa5, 0x43, 0x72, 0x65, 0x64, 0x73) o = append(o, 0xa5, 0x43, 0x72, 0x65, 0x64, 0x73)
// map header, size 3 // map header, size 3
@@ -1229,6 +1273,12 @@ func (z *BatchJobReplicateTarget) UnmarshalMsg(bts []byte) (o []byte, err error)
err = msgp.WrapError(err, "Endpoint") err = msgp.WrapError(err, "Endpoint")
return return
} }
case "Path":
z.Path, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Path")
return
}
case "Creds": case "Creds":
var zb0003 uint32 var zb0003 uint32
zb0003, bts, err = msgp.ReadMapHeaderBytes(bts) zb0003, bts, err = msgp.ReadMapHeaderBytes(bts)
@@ -1284,7 +1334,7 @@ func (z *BatchJobReplicateTarget) UnmarshalMsg(bts []byte) (o []byte, err error)
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z *BatchJobReplicateTarget) Msgsize() (s int) { func (z *BatchJobReplicateTarget) Msgsize() (s int) {
s = 1 + 5 + msgp.StringPrefixSize + len(string(z.Type)) + 7 + msgp.StringPrefixSize + len(z.Bucket) + 7 + msgp.StringPrefixSize + len(z.Prefix) + 9 + msgp.StringPrefixSize + len(z.Endpoint) + 6 + 1 + 10 + msgp.StringPrefixSize + len(z.Creds.AccessKey) + 10 + msgp.StringPrefixSize + len(z.Creds.SecretKey) + 13 + msgp.StringPrefixSize + len(z.Creds.SessionToken) s = 1 + 5 + msgp.StringPrefixSize + len(string(z.Type)) + 7 + msgp.StringPrefixSize + len(z.Bucket) + 7 + msgp.StringPrefixSize + len(z.Prefix) + 9 + msgp.StringPrefixSize + len(z.Endpoint) + 5 + msgp.StringPrefixSize + len(z.Path) + 6 + 1 + 10 + msgp.StringPrefixSize + len(z.Creds.AccessKey) + 10 + msgp.StringPrefixSize + len(z.Creds.SecretKey) + 13 + msgp.StringPrefixSize + len(z.Creds.SessionToken)
return return
} }
+1 -1
View File
@@ -38,9 +38,9 @@ import (
xhttp "github.com/minio/minio/internal/http" xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/kms" "github.com/minio/minio/internal/kms"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/minio/minio/internal/workers"
"github.com/minio/pkg/env" "github.com/minio/pkg/env"
"github.com/minio/pkg/wildcard" "github.com/minio/pkg/wildcard"
"github.com/minio/pkg/workers"
) )
// keyrotate: // keyrotate:
+2 -2
View File
@@ -165,9 +165,9 @@ func (b *streamingBitrotReader) ReadAt(buf []byte, offset int64) (int, error) {
b.rc, err = b.disk.ReadFileStream(context.TODO(), b.volume, b.filePath, streamOffset, b.tillOffset-streamOffset) b.rc, err = b.disk.ReadFileStream(context.TODO(), b.volume, b.filePath, streamOffset, b.tillOffset-streamOffset)
if err != nil { if err != nil {
if !IsErr(err, ignoredErrs...) { if !IsErr(err, ignoredErrs...) {
logger.LogIf(GlobalContext, logger.LogOnceIf(GlobalContext,
fmt.Errorf("Reading erasure shards at (%s: %s/%s) returned '%w', will attempt to reconstruct if we have quorum", fmt.Errorf("Reading erasure shards at (%s: %s/%s) returned '%w', will attempt to reconstruct if we have quorum",
b.disk, b.volume, b.filePath, err)) b.disk, b.volume, b.filePath, err), "bitrot-read-file-stream"+b.volume+b.filePath)
} }
} }
} else { } else {
+1 -1
View File
@@ -23,7 +23,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/pubsub" "github.com/minio/minio/internal/pubsub"
) )
+7 -1
View File
@@ -24,6 +24,7 @@ import (
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"os"
"reflect" "reflect"
"time" "time"
@@ -216,6 +217,7 @@ func verifyServerSystemConfig(ctx context.Context, endpointServerPools EndpointS
for onlineServers < len(clnts)/2 { for onlineServers < len(clnts)/2 {
for _, clnt := range clnts { for _, clnt := range clnts {
if err := clnt.Verify(ctx, srcCfg); err != nil { if err := clnt.Verify(ctx, srcCfg); err != nil {
bootstrapTrace(fmt.Sprintf("clnt.Verify: %v, endpoint: %v", err, clnt.endpoint))
if !isNetworkError(err) { if !isNetworkError(err) {
logger.LogOnceIf(ctx, fmt.Errorf("%s has incorrect configuration: %w", clnt.String(), err), clnt.String()) logger.LogOnceIf(ctx, fmt.Errorf("%s has incorrect configuration: %w", clnt.String(), err), clnt.String())
incorrectConfigs = append(incorrectConfigs, fmt.Errorf("%s has incorrect configuration: %w", clnt.String(), err)) incorrectConfigs = append(incorrectConfigs, fmt.Errorf("%s has incorrect configuration: %w", clnt.String(), err))
@@ -264,7 +266,11 @@ func newBootstrapRESTClients(endpointServerPools EndpointServerPools) []*bootstr
// Only proceed for remote endpoints. // Only proceed for remote endpoints.
if !endpoint.IsLocal { if !endpoint.IsLocal {
clnts = append(clnts, newBootstrapRESTClient(endpoint)) cl := newBootstrapRESTClient(endpoint)
if serverDebugLog {
cl.restClient.TraceOutput = os.Stdout
}
clnts = append(clnts, cl)
} }
} }
} }
+1 -1
View File
@@ -26,7 +26,7 @@ import (
"net/http" "net/http"
"github.com/minio/kes-go" "github.com/minio/kes-go"
"github.com/minio/madmin-go/v2" "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"
"github.com/minio/mux" "github.com/minio/mux"
+351 -62
View File
@@ -21,13 +21,17 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/json"
"encoding/xml" "encoding/xml"
"errors"
"fmt" "fmt"
"io" "io"
"mime/multipart"
"net/http" "net/http"
"net/textproto" "net/textproto"
"net/url" "net/url"
"path" "path"
"runtime"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -35,8 +39,10 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/minio/mux" "github.com/minio/mux"
"github.com/valyala/bytebufferpool"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/minio-go/v7/pkg/tags" "github.com/minio/minio-go/v7/pkg/tags"
"github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/auth"
@@ -49,17 +55,21 @@ import (
"github.com/minio/minio/internal/handlers" "github.com/minio/minio/internal/handlers"
"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/ioutil"
"github.com/minio/minio/internal/kms" "github.com/minio/minio/internal/kms"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/minio/minio/internal/sync/errgroup"
"github.com/minio/pkg/bucket/policy" "github.com/minio/pkg/bucket/policy"
iampolicy "github.com/minio/pkg/iam/policy" iampolicy "github.com/minio/pkg/iam/policy"
"github.com/minio/pkg/sync/errgroup"
) )
const ( const (
objectLockConfig = "object-lock.xml" objectLockConfig = "object-lock.xml"
bucketTaggingConfig = "tagging.xml" bucketTaggingConfig = "tagging.xml"
bucketReplicationConfig = "replication.xml" bucketReplicationConfig = "replication.xml"
xMinIOErrCodeHeader = "x-minio-error-code"
xMinIOErrDescHeader = "x-minio-error-desc"
) )
// Check if there are buckets on server without corresponding entry in etcd backend and // Check if there are buckets on server without corresponding entry in etcd backend and
@@ -429,6 +439,11 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
// The max. XML contains 100000 object names (each at most 1024 bytes long) + XML overhead // The max. XML contains 100000 object names (each at most 1024 bytes long) + XML overhead
const maxBodySize = 2 * 100000 * 1024 const maxBodySize = 2 * 100000 * 1024
if r.ContentLength > maxBodySize {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrEntityTooLarge), r.URL)
return
}
// Unmarshal list of keys to be deleted. // Unmarshal list of keys to be deleted.
deleteObjectsReq := &DeleteObjectsRequest{} deleteObjectsReq := &DeleteObjectsRequest{}
if err := xmlDecoder(r.Body, deleteObjectsReq, maxBodySize); err != nil { if err := xmlDecoder(r.Body, deleteObjectsReq, maxBodySize); err != nil {
@@ -540,6 +555,11 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
oss[index].SetTransitionState(goi.TransitionedObject) oss[index].SetTransitionState(goi.TransitionedObject)
} }
// All deletes on directory objects needs to be for `nullVersionID`
if isDirObject(object.ObjectName) && object.VersionID == "" {
object.VersionID = nullVersionID
}
if replicateDeletes { if replicateDeletes {
dsc = checkReplicateDelete(ctx, bucket, ObjectToDelete{ dsc = checkReplicateDelete(ctx, bucket, ObjectToDelete{
ObjectV: ObjectV{ ObjectV: ObjectV{
@@ -634,6 +654,11 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
if deleteResult.errInfo.Code != "" { if deleteResult.errInfo.Code != "" {
deleteErrors = append(deleteErrors, deleteResult.errInfo) deleteErrors = append(deleteErrors, deleteResult.errInfo)
} else { } else {
// All deletes on directory objects was with `nullVersionID`.
// Remove it from response.
if isDirObject(deleteResult.delInfo.ObjectName) && deleteResult.delInfo.VersionID == nullVersionID {
deleteResult.delInfo.VersionID = ""
}
deletedObjects = append(deletedObjects, deleteResult.delInfo) deletedObjects = append(deletedObjects, deleteResult.delInfo)
} }
} }
@@ -649,6 +674,11 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
} }
if replicateDeletes && (dobj.DeleteMarkerReplicationStatus() == replication.Pending || dobj.VersionPurgeStatus() == Pending) { if replicateDeletes && (dobj.DeleteMarkerReplicationStatus() == replication.Pending || dobj.VersionPurgeStatus() == Pending) {
// copy so we can re-add null ID.
dobj := dobj
if isDirObject(dobj.ObjectName) && dobj.VersionID == "" {
dobj.VersionID = nullVersionID
}
dv := DeletedObjectReplicationInfo{ dv := DeletedObjectReplicationInfo{
DeletedObject: dobj, DeletedObject: dobj,
Bucket: bucket, Bucket: bucket,
@@ -882,14 +912,6 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
} }
bucket := mux.Vars(r)["bucket"] bucket := mux.Vars(r)["bucket"]
// Require Content-Length to be set in the request
size := r.ContentLength
if size < 0 {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL)
return
}
resource, err := getResource(r.URL.Path, r.Host, globalDomainNames) resource, err := getResource(r.URL.Path, r.Host, globalDomainNames)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
@@ -904,43 +926,151 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
// Here the parameter is the size of the form data that should // Here the parameter is the size of the form data that should
// be loaded in memory, the remaining being put in temporary files. // be loaded in memory, the remaining being put in temporary files.
reader, err := r.MultipartReader() mp, err := r.MultipartReader()
if err != nil { if err != nil {
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest) apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
apiErr.Description = fmt.Sprintf("%s (%s)", apiErr.Description, err) apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, err)
writeErrorResponse(ctx, w, apiErr, r.URL) writeErrorResponse(ctx, w, apiErr, r.URL)
return return
} }
// Read multipart data and save in memory and in the disk if needed const mapEntryOverhead = 200
form, err := reader.ReadForm(maxFormMemory)
var (
reader io.Reader
fileSize int64 = -1
fileName string
fanOutEntries = make([]minio.PutObjectFanOutEntry, 0, 100)
)
maxParts := 1000
// Canonicalize the form values into http.Header.
formValues := make(http.Header)
for {
part, err := mp.NextRawPart()
if errors.Is(err, io.EOF) {
break
}
if err != nil { if err != nil {
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest) apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
apiErr.Description = fmt.Sprintf("%s (%s)", apiErr.Description, err) apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, err)
writeErrorResponse(ctx, w, apiErr, r.URL)
return
}
if maxParts <= 0 {
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, multipart.ErrMessageTooLarge)
writeErrorResponse(ctx, w, apiErr, r.URL)
return
}
maxParts--
name := part.FormName()
if name == "" {
continue
}
fileName = part.FileName()
// Multiple values for the same key (one map entry, longer slice) are cheaper
// than the same number of values for different keys (many map entries), but
// using a consistent per-value cost for overhead is simpler.
maxMemoryBytes := 2 * int64(10<<20)
maxMemoryBytes -= int64(len(name))
maxMemoryBytes -= mapEntryOverhead
if maxMemoryBytes < 0 {
// We can't actually take this path, since nextPart would already have
// rejected the MIME headers for being too large. Check anyway.
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, multipart.ErrMessageTooLarge)
writeErrorResponse(ctx, w, apiErr, r.URL) writeErrorResponse(ctx, w, apiErr, r.URL)
return return
} }
// Remove all tmp files created during multipart upload var b bytes.Buffer
defer form.RemoveAll() if fileName == "" {
if http.CanonicalHeaderKey(name) == http.CanonicalHeaderKey("x-minio-fanout-list") {
dec := json.NewDecoder(part)
// Extract all form fields // while the array contains values
fileBody, fileName, fileSize, formValues, err := extractPostPolicyFormValues(ctx, form) for dec.More() {
if err != nil { var m minio.PutObjectFanOutEntry
if err := dec.Decode(&m); err != nil {
part.Close()
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest) apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
apiErr.Description = fmt.Sprintf("%s (%s)", apiErr.Description, err) apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, multipart.ErrMessageTooLarge)
writeErrorResponse(ctx, w, apiErr, r.URL)
return
}
fanOutEntries = append(fanOutEntries, m)
}
part.Close()
continue
}
// value, store as string in memory
n, err := io.CopyN(&b, part, maxMemoryBytes+1)
part.Close()
if err != nil && err != io.EOF {
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, err)
writeErrorResponse(ctx, w, apiErr, r.URL)
return
}
maxMemoryBytes -= n
if maxMemoryBytes < 0 {
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, multipart.ErrMessageTooLarge)
writeErrorResponse(ctx, w, apiErr, r.URL)
return
}
if n > maxFormFieldSize {
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, multipart.ErrMessageTooLarge)
writeErrorResponse(ctx, w, apiErr, r.URL)
return
}
formValues[http.CanonicalHeaderKey(name)] = append(formValues[http.CanonicalHeaderKey(name)], b.String())
continue
}
// In accordance with https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPOST.html
// The file or text content.
// The file or text content must be the last field in the form.
// You cannot upload more than one file at a time.
reader = part
// we have found the File part of the request we are done processing multipart-form
break
}
if _, ok := formValues["Key"]; !ok {
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, errors.New("The name of the uploaded key is missing"))
writeErrorResponse(ctx, w, apiErr, r.URL) writeErrorResponse(ctx, w, apiErr, r.URL)
return return
} }
// Check if file is provided, error out otherwise. if fileName == "" {
if fileBody == nil { apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrPOSTFileRequired), r.URL) apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, errors.New("The file or text content is missing"))
writeErrorResponse(ctx, w, apiErr, r.URL)
return
}
checksum, err := hash.GetContentChecksum(formValues)
if err != nil {
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, fmt.Errorf("Invalid checksum: %w", err))
writeErrorResponse(ctx, w, apiErr, r.URL)
return
}
if checksum != nil && checksum.Type.Trailing() {
// Not officially supported in POST requests.
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, errors.New("Trailing checksums not available for POST operations"))
writeErrorResponse(ctx, w, apiErr, r.URL)
return return
} }
// Close multipart file
defer fileBody.Close()
formValues.Set("Bucket", bucket) formValues.Set("Bucket", bucket)
if fileName != "" && strings.Contains(formValues.Get("Key"), "${filename}") { if fileName != "" && strings.Contains(formValues.Get("Key"), "${filename}") {
@@ -968,6 +1098,23 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
return return
} }
if len(fanOutEntries) > 0 {
// Once signature is validated, check if the user has
// explicit permissions for the user.
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey,
Groups: cred.Groups,
Action: iampolicy.PutObjectFanOutAction,
ConditionValues: getConditionValues(r, "", cred),
BucketName: bucket,
ObjectName: object,
IsOwner: globalActiveCred.AccessKey == cred.AccessKey,
Claims: cred.Claims,
}) {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return
}
} else {
// Once signature is validated, check if the user has // Once signature is validated, check if the user has
// explicit permissions for the user. // explicit permissions for the user.
if !globalIAMSys.IsAllowed(iampolicy.Args{ if !globalIAMSys.IsAllowed(iampolicy.Args{
@@ -983,6 +1130,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return return
} }
}
policyBytes, err := base64.StdEncoding.DecodeString(formValues.Get("Policy")) policyBytes, err := base64.StdEncoding.DecodeString(formValues.Get("Policy"))
if err != nil { if err != nil {
@@ -990,6 +1138,19 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
return return
} }
hashReader, err := hash.NewReader(reader, fileSize, "", "", fileSize)
if err != nil {
logger.LogIf(ctx, err)
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
if checksum != nil && checksum.Valid() {
if err = hashReader.AddChecksumNoTrailer(formValues, false); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
}
// Handle policy if it is set. // Handle policy if it is set.
if len(policyBytes) > 0 { if len(policyBytes) > 0 {
postPolicyForm, err := parsePostPolicyForm(bytes.NewReader(policyBytes)) postPolicyForm, err := parsePostPolicyForm(bytes.NewReader(policyBytes))
@@ -1010,15 +1171,8 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
// should not exceed the maximum single Put size (5 GiB) // should not exceed the maximum single Put size (5 GiB)
lengthRange := postPolicyForm.Conditions.ContentLengthRange lengthRange := postPolicyForm.Conditions.ContentLengthRange
if lengthRange.Valid { if lengthRange.Valid {
if fileSize < lengthRange.Min { hashReader.SetExpectedMin(lengthRange.Min)
writeErrorResponse(ctx, w, toAPIError(ctx, errDataTooSmall), r.URL) hashReader.SetExpectedMax(lengthRange.Max)
return
}
if fileSize > lengthRange.Max || isMaxObjectSize(fileSize) {
writeErrorResponse(ctx, w, toAPIError(ctx, errDataTooLarge), r.URL)
return
}
} }
} }
@@ -1030,19 +1184,13 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
return return
} }
hashReader, err := hash.NewReader(fileBody, fileSize, "", "", fileSize)
if err != nil {
logger.LogIf(ctx, err)
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
rawReader := hashReader rawReader := hashReader
pReader := NewPutObjReader(rawReader) pReader := NewPutObjReader(rawReader)
var objectEncryptionKey crypto.ObjectKey var objectEncryptionKey crypto.ObjectKey
// Check if bucket encryption is enabled // Check if bucket encryption is enabled
sseConfig, _ := globalBucketSSEConfigSys.Get(bucket) sseConfig, _ := globalBucketSSEConfigSys.Get(bucket)
sseConfig.Apply(r.Header, sse.ApplyOptions{ sseConfig.Apply(formValues, sse.ApplyOptions{
AutoEncrypt: globalAutoEncryption, AutoEncrypt: globalAutoEncryption,
}) })
@@ -1053,12 +1201,24 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
return return
} }
fanOutOpts := fanOutOptions{Checksum: checksum}
if crypto.Requested(formValues) { if crypto.Requested(formValues) {
if crypto.SSECopy.IsRequested(r.Header) { if crypto.SSECopy.IsRequested(r.Header) {
writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidEncryptionParameters), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidEncryptionParameters), r.URL)
return return
} }
if crypto.SSEC.IsRequested(r.Header) && crypto.S3.IsRequested(r.Header) {
writeErrorResponse(ctx, w, toAPIError(ctx, crypto.ErrIncompatibleEncryptionMethod), r.URL)
return
}
if crypto.SSEC.IsRequested(r.Header) && crypto.S3KMS.IsRequested(r.Header) {
writeErrorResponse(ctx, w, toAPIError(ctx, crypto.ErrIncompatibleEncryptionMethod), r.URL)
return
}
if crypto.SSEC.IsRequested(r.Header) && isReplicationEnabled(ctx, bucket) { if crypto.SSEC.IsRequested(r.Header) && isReplicationEnabled(ctx, bucket) {
writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidEncryptionParametersSSEC), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidEncryptionParametersSSEC), r.URL)
return return
@@ -1085,23 +1245,147 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
return return
} }
} }
if len(fanOutEntries) == 0 {
reader, objectEncryptionKey, err = newEncryptReader(ctx, hashReader, kind, keyID, key, bucket, object, metadata, kmsCtx) reader, objectEncryptionKey, err = newEncryptReader(ctx, hashReader, kind, keyID, key, bucket, object, metadata, kmsCtx)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
} }
info := ObjectInfo{Size: fileSize} // 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(reader, info.EncryptedSize(), "", "", fileSize)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
} }
if checksum != nil && checksum.Valid() {
if err = hashReader.AddChecksumNoTrailer(formValues, true); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
}
pReader, err = pReader.WithEncryption(hashReader, &objectEncryptionKey) pReader, err = pReader.WithEncryption(hashReader, &objectEncryptionKey)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
} }
} else {
fanOutOpts = fanOutOptions{
Key: key,
Kind: kind,
KeyID: keyID,
KmsCtx: kmsCtx,
Checksum: checksum,
}
}
}
if len(fanOutEntries) > 0 {
// Fan-out requires no copying, and must be carried from original source
// https://en.wikipedia.org/wiki/Copy_protection so the incoming stream
// is always going to be in-memory as we cannot re-read from what we
// wrote to disk - since that amounts to "copying" from a "copy"
// instead of "copying" from source, we need the stream to be seekable
// to ensure that we can make fan-out calls concurrently.
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
// Maximum allowed fan-out object size.
const maxFanOutSize = 16 << 20
n, err := io.Copy(buf, ioutil.HardLimitReader(pReader, maxFanOutSize))
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
concurrentSize := 100
if runtime.GOMAXPROCS(0) < concurrentSize {
concurrentSize = runtime.GOMAXPROCS(0)
}
fanOutResp := make([]minio.PutObjectFanOutResponse, 0, len(fanOutEntries))
eventArgsList := make([]eventArgs, 0, len(fanOutEntries))
for {
var objInfos []ObjectInfo
var errs []error
var done bool
if len(fanOutEntries) < concurrentSize {
objInfos, errs = fanOutPutObject(ctx, bucket, objectAPI, fanOutEntries, buf.Bytes()[:n], fanOutOpts)
done = true
} else {
objInfos, errs = fanOutPutObject(ctx, bucket, objectAPI, fanOutEntries[:concurrentSize], buf.Bytes()[:n], fanOutOpts)
fanOutEntries = fanOutEntries[concurrentSize:]
}
for i, objInfo := range objInfos {
if errs[i] != nil {
fanOutResp = append(fanOutResp, minio.PutObjectFanOutResponse{
Key: objInfo.Name,
Error: errs[i].Error(),
})
eventArgsList = append(eventArgsList, eventArgs{
EventName: event.ObjectCreatedPost,
BucketName: objInfo.Bucket,
Object: ObjectInfo{Name: objInfo.Name},
ReqParams: extractReqParams(r),
RespElements: extractRespElements(w),
UserAgent: fmt.Sprintf("%s MinIO-Fan-Out (failed: %v)", r.UserAgent(), errs[i]),
Host: handlers.GetSourceIP(r),
})
continue
}
fanOutResp = append(fanOutResp, minio.PutObjectFanOutResponse{
Key: objInfo.Name,
ETag: getDecryptedETag(formValues, objInfo, false),
VersionID: objInfo.VersionID,
LastModified: &objInfo.ModTime,
})
eventArgsList = append(eventArgsList, eventArgs{
EventName: event.ObjectCreatedPost,
BucketName: objInfo.Bucket,
Object: objInfo,
ReqParams: extractReqParams(r),
RespElements: extractRespElements(w),
UserAgent: r.UserAgent() + " " + "MinIO-Fan-Out",
Host: handlers.GetSourceIP(r),
})
}
if done {
break
}
}
enc := json.NewEncoder(w)
for i, fanOutResp := range fanOutResp {
if err = enc.Encode(&fanOutResp); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
// Notify object created events.
sendEvent(eventArgsList[i])
if eventArgsList[i].Object.NumVersions > dataScannerExcessiveVersionsThreshold {
// Send events for excessive versions.
sendEvent(eventArgs{
EventName: event.ObjectManyVersions,
BucketName: eventArgsList[i].Object.Bucket,
Object: eventArgsList[i].Object,
ReqParams: extractReqParams(r),
RespElements: extractRespElements(w),
UserAgent: r.UserAgent() + " " + "MinIO-Fan-Out",
Host: handlers.GetSourceIP(r),
})
}
}
return
} }
objInfo, err := objectAPI.PutObject(ctx, bucket, object, pReader, opts) objInfo, err := objectAPI.PutObject(ctx, bucket, object, pReader, opts)
@@ -1116,7 +1400,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
w.Header()[xhttp.ETag] = []string{`"` + objInfo.ETag + `"`} w.Header()[xhttp.ETag] = []string{`"` + objInfo.ETag + `"`}
// Set the relevant version ID as part of the response header. // Set the relevant version ID as part of the response header.
if objInfo.VersionID != "" { if objInfo.VersionID != "" && objInfo.VersionID != nullVersionID {
w.Header()[xhttp.AmzVersionID] = []string{objInfo.VersionID} w.Header()[xhttp.AmzVersionID] = []string{objInfo.VersionID}
} }
@@ -1134,6 +1418,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
UserAgent: r.UserAgent(), UserAgent: r.UserAgent(),
Host: handlers.GetSourceIP(r), Host: handlers.GetSourceIP(r),
}) })
if objInfo.NumVersions > dataScannerExcessiveVersionsThreshold { if objInfo.NumVersions > dataScannerExcessiveVersionsThreshold {
defer sendEvent(eventArgs{ defer sendEvent(eventArgs{
EventName: event.ObjectManyVersions, EventName: event.ObjectManyVersions,
@@ -1156,6 +1441,11 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
return return
} }
// Add checksum header.
if checksum != nil && checksum.Valid() {
hash.AddChecksumHeader(w, checksum.AsMap())
}
// Decide what http response to send depending on success_action_status parameter // Decide what http response to send depending on success_action_status parameter
switch successStatus { switch successStatus {
case "201": case "201":
@@ -1324,14 +1614,6 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
} }
} }
if globalDNSConfig != nil {
if err := globalDNSConfig.Delete(bucket); err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to delete bucket DNS entry %w, please delete it manually", err))
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
}
deleteBucket := objectAPI.DeleteBucket deleteBucket := objectAPI.DeleteBucket
// Attempt to delete bucket. // Attempt to delete bucket.
@@ -1345,15 +1627,18 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
apiErr.Description = "The bucket you tried to delete is not empty. You must delete all versions in the bucket." apiErr.Description = "The bucket you tried to delete is not empty. You must delete all versions in the bucket."
} }
} }
if globalDNSConfig != nil {
if err2 := globalDNSConfig.Put(bucket); err2 != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to restore bucket DNS entry %w, please fix it manually", err2))
}
}
writeErrorResponse(ctx, w, apiErr, r.URL) writeErrorResponse(ctx, w, apiErr, r.URL)
return return
} }
if globalDNSConfig != nil {
if err := globalDNSConfig.Delete(bucket); err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to delete bucket DNS entry %w, please delete it manually, bucket on MinIO no longer exists", err))
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
}
globalNotificationSys.DeleteBucketMetadata(ctx, bucket) globalNotificationSys.DeleteBucketMetadata(ctx, bucket)
globalReplicationPool.deleteResyncMetadata(ctx, bucket) globalReplicationPool.deleteResyncMetadata(ctx, bucket)
@@ -1398,7 +1683,7 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
config, err := objectlock.ParseObjectLockConfig(r.Body) config, err := objectlock.ParseObjectLockConfig(r.Body)
if err != nil { if err != nil {
apiErr := errorCodes.ToAPIErr(ErrMalformedXML) apiErr := errorCodes.ToAPIErr(ErrInvalidArgument)
apiErr.Description = err.Error() apiErr.Description = err.Error()
writeErrorResponse(ctx, w, apiErr, r.URL) writeErrorResponse(ctx, w, apiErr, r.URL)
return return
@@ -1412,7 +1697,11 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
// Deny object locking configuration settings on existing buckets without object lock enabled. // Deny object locking configuration settings on existing buckets without object lock enabled.
if _, _, err = globalBucketMetadataSys.GetObjectLockConfig(bucket); err != nil { if _, _, err = globalBucketMetadataSys.GetObjectLockConfig(bucket); err != nil {
if _, ok := err.(BucketObjectLockConfigNotFound); ok {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrObjectLockConfigurationNotAllowed), r.URL)
} else {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
}
return return
} }
@@ -1614,5 +1903,5 @@ func (api objectAPIHandlers) DeleteBucketTaggingHandler(w http.ResponseWriter, r
})) }))
// Write success response. // Write success response.
writeSuccessResponseHeadersOnly(w) writeSuccessNoContent(w)
} }
+14 -2
View File
@@ -657,13 +657,14 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
) { ) {
var err error var err error
contentBytes := []byte("hello")
sha256sum := "" sha256sum := ""
var objectNames []string var objectNames []string
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
contentBytes := []byte("hello")
objectName := "test-object-" + strconv.Itoa(i) objectName := "test-object-" + strconv.Itoa(i)
if i == 0 { if i == 0 {
objectName += "/" objectName += "/"
contentBytes = []byte{}
} }
// uploading the object. // uploading the object.
_, err = obj.PutObject(GlobalContext, bucketName, objectName, mustGetPutObjReader(t, bytes.NewReader(contentBytes), int64(len(contentBytes)), "", sha256sum), ObjectOptions{}) _, err = obj.PutObject(GlobalContext, bucketName, objectName, mustGetPutObjReader(t, bytes.NewReader(contentBytes), int64(len(contentBytes)), "", sha256sum), ObjectOptions{})
@@ -676,6 +677,7 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
objectNames = append(objectNames, objectName) objectNames = append(objectNames, objectName)
} }
contentBytes := []byte("hello")
for _, name := range []string{"private/object", "public/object"} { for _, name := range []string{"private/object", "public/object"} {
// Uploading the object with retention enabled // Uploading the object with retention enabled
_, err = obj.PutObject(GlobalContext, bucketName, name, mustGetPutObjReader(t, bytes.NewReader(contentBytes), int64(len(contentBytes)), "", sha256sum), ObjectOptions{}) _, err = obj.PutObject(GlobalContext, bucketName, name, mustGetPutObjReader(t, bytes.NewReader(contentBytes), int64(len(contentBytes)), "", sha256sum), ObjectOptions{})
@@ -745,8 +747,13 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
deletedObjects := make([]DeletedObject, len(requestList[0].Objects)) deletedObjects := make([]DeletedObject, len(requestList[0].Objects))
for i := range requestList[0].Objects { for i := range requestList[0].Objects {
var vid string
if isDirObject(requestList[0].Objects[i].ObjectName) {
vid = ""
}
deletedObjects[i] = DeletedObject{ deletedObjects[i] = DeletedObject{
ObjectName: requestList[0].Objects[i].ObjectName, ObjectName: requestList[0].Objects[i].ObjectName,
VersionID: vid,
} }
} }
@@ -756,9 +763,14 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
successRequest1 := encodeResponse(requestList[1]) successRequest1 := encodeResponse(requestList[1])
deletedObjects = make([]DeletedObject, len(requestList[1].Objects)) deletedObjects = make([]DeletedObject, len(requestList[1].Objects))
for i := range requestList[0].Objects { for i := range requestList[1].Objects {
var vid string
if isDirObject(requestList[0].Objects[i].ObjectName) {
vid = ""
}
deletedObjects[i] = DeletedObject{ deletedObjects[i] = DeletedObject{
ObjectName: requestList[1].Objects[i].ObjectName, ObjectName: requestList[1].Objects[i].ObjectName,
VersionID: vid,
} }
} }
+88
View File
@@ -0,0 +1,88 @@
// Copyright (c) 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 "github.com/minio/minio/internal/bucket/lifecycle"
//go:generate stringer -type lcEventSrc -trimprefix lcEventSrc_ $GOFILE
type lcEventSrc uint8
//revive:disable:var-naming Underscores is used here to indicate where common prefix ends and the enumeration name begins
const (
lcEventSrc_None lcEventSrc = iota
lcEventSrc_Scanner
lcEventSrc_Decom
lcEventSrc_Rebal
lcEventSrc_s3HeadObject
lcEventSrc_s3GetObject
lcEventSrc_s3ListObjects
lcEventSrc_s3PutObject
lcEventSrc_s3CopyObject
lcEventSrc_s3CompleteMultipartUpload
)
//revive:enable:var-naming
type lcAuditEvent struct {
lifecycle.Event
source lcEventSrc
}
func (lae lcAuditEvent) Tags() map[string]interface{} {
event := lae.Event
src := lae.source
const (
ilmSrc = "ilm-src"
ilmAction = "ilm-action"
ilmDue = "ilm-due"
ilmRuleID = "ilm-rule-id"
ilmTier = "ilm-tier"
ilmNewerNoncurrentVersions = "ilm-newer-noncurrent-versions"
ilmNoncurrentDays = "ilm-noncurrent-days"
)
tags := make(map[string]interface{}, 5)
if src > lcEventSrc_None {
tags[ilmSrc] = src.String()
}
tags[ilmAction] = event.Action.String()
tags[ilmRuleID] = event.RuleID
if !event.Due.IsZero() {
tags[ilmDue] = event.Due
}
// rule with Transition/NoncurrentVersionTransition in effect
if event.StorageClass != "" {
tags[ilmTier] = event.StorageClass
}
// rule with NewernoncurrentVersions in effect
if event.NewerNoncurrentVersions > 0 {
tags[ilmNewerNoncurrentVersions] = event.NewerNoncurrentVersions
}
if event.NoncurrentDays > 0 {
tags[ilmNoncurrentDays] = event.NoncurrentDays
}
return tags
}
func newLifecycleAuditEvent(src lcEventSrc, event lifecycle.Event) lcAuditEvent {
return lcAuditEvent{
Event: event,
source: src,
}
}
+15 -1
View File
@@ -21,6 +21,7 @@ import (
"encoding/xml" "encoding/xml"
"io" "io"
"net/http" "net/http"
"strconv"
"github.com/minio/minio/internal/bucket/lifecycle" "github.com/minio/minio/internal/bucket/lifecycle"
xhttp "github.com/minio/minio/internal/http" xhttp "github.com/minio/minio/internal/http"
@@ -115,6 +116,16 @@ func (api objectAPIHandlers) GetBucketLifecycleHandler(w http.ResponseWriter, r
vars := mux.Vars(r) vars := mux.Vars(r)
bucket := vars["bucket"] bucket := vars["bucket"]
var withUpdatedAt bool
if updatedAtStr := r.Form.Get("withUpdatedAt"); updatedAtStr != "" {
var err error
withUpdatedAt, err = strconv.ParseBool(updatedAtStr)
if err != nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidLifecycleQueryParameter), r.URL)
return
}
}
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketLifecycleAction, bucket, ""); s3Error != ErrNone { if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketLifecycleAction, bucket, ""); s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
return return
@@ -126,7 +137,7 @@ func (api objectAPIHandlers) GetBucketLifecycleHandler(w http.ResponseWriter, r
return return
} }
config, err := globalBucketMetadataSys.GetLifecycleConfig(bucket) config, updatedAt, err := globalBucketMetadataSys.GetLifecycleConfig(bucket)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
@@ -138,6 +149,9 @@ func (api objectAPIHandlers) GetBucketLifecycleHandler(w http.ResponseWriter, r
return return
} }
if withUpdatedAt {
w.Header().Set(xhttp.MinIOLifecycleCfgUpdatedAt, updatedAt.Format(iso8601Format))
}
// Write lifecycle configuration to client. // Write lifecycle configuration to client.
writeSuccessResponseXML(w, configData) writeSuccessResponseXML(w, configData)
} }
+2 -2
View File
@@ -179,7 +179,7 @@ func testBucketLifecycleHandlers(obj ObjectLayer, instanceType, bucketName strin
lifecycleResponse: []byte(``), lifecycleResponse: []byte(``),
errorResponse: APIErrorResponse{ errorResponse: APIErrorResponse{
Resource: SlashSeparator + bucketName + SlashSeparator, Resource: SlashSeparator + bucketName + SlashSeparator,
Code: "InvalidRequest", Code: "InvalidArgument",
Message: "Filter must have exactly one of Prefix, Tag, or And specified", Message: "Filter must have exactly one of Prefix, Tag, or And specified",
}, },
@@ -196,7 +196,7 @@ func testBucketLifecycleHandlers(obj ObjectLayer, instanceType, bucketName strin
lifecycleResponse: []byte(``), lifecycleResponse: []byte(``),
errorResponse: APIErrorResponse{ errorResponse: APIErrorResponse{
Resource: SlashSeparator + bucketName + SlashSeparator, Resource: SlashSeparator + bucketName + SlashSeparator,
Code: "InvalidRequest", Code: "InvalidArgument",
Message: "Date must be provided in ISO 8601 format", Message: "Date must be provided in ISO 8601 format",
}, },
+72 -53
View File
@@ -32,6 +32,7 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/minio/madmin-go/v3"
"github.com/minio/minio-go/v7/pkg/tags" "github.com/minio/minio-go/v7/pkg/tags"
"github.com/minio/minio/internal/amztime" "github.com/minio/minio/internal/amztime"
sse "github.com/minio/minio/internal/bucket/encryption" sse "github.com/minio/minio/internal/bucket/encryption"
@@ -40,8 +41,8 @@ import (
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/minio/minio/internal/s3select" "github.com/minio/minio/internal/s3select"
"github.com/minio/minio/internal/workers"
"github.com/minio/pkg/env" "github.com/minio/pkg/env"
"github.com/minio/pkg/workers"
) )
const ( const (
@@ -62,7 +63,8 @@ type LifecycleSys struct{}
// Get - gets lifecycle config associated to a given bucket name. // Get - gets lifecycle config associated to a given bucket name.
func (sys *LifecycleSys) Get(bucketName string) (lc *lifecycle.Lifecycle, err error) { func (sys *LifecycleSys) Get(bucketName string) (lc *lifecycle.Lifecycle, err error) {
return globalBucketMetadataSys.GetLifecycleConfig(bucketName) lc, _, err = globalBucketMetadataSys.GetLifecycleConfig(bucketName)
return lc, err
} }
// NewLifecycleSys - creates new lifecycle system. // NewLifecycleSys - creates new lifecycle system.
@@ -70,10 +72,34 @@ func NewLifecycleSys() *LifecycleSys {
return &LifecycleSys{} return &LifecycleSys{}
} }
func ilmTrace(startTime time.Time, duration time.Duration, oi ObjectInfo, event string) madmin.TraceInfo {
return madmin.TraceInfo{
TraceType: madmin.TraceILM,
Time: startTime,
NodeName: globalLocalNodeName,
FuncName: event,
Duration: duration,
Path: pathJoin(oi.Bucket, oi.Name),
Error: "",
Message: getSource(4),
Custom: map[string]string{"version-id": oi.VersionID},
}
}
func (sys *LifecycleSys) trace(oi ObjectInfo) func(event string) {
startTime := time.Now()
return func(event string) {
duration := time.Since(startTime)
if globalTrace.NumSubscribers(madmin.TraceILM) > 0 {
globalTrace.Publish(ilmTrace(startTime, duration, oi, event))
}
}
}
type expiryTask struct { type expiryTask struct {
objInfo ObjectInfo objInfo ObjectInfo
versionExpiry bool event lifecycle.Event
restoredObject bool src lcEventSrc
} }
type expiryState struct { type expiryState struct {
@@ -96,22 +122,22 @@ func (es *expiryState) close() {
} }
// enqueueByDays enqueues object versions expired by days for expiry. // enqueueByDays enqueues object versions expired by days for expiry.
func (es *expiryState) enqueueByDays(oi ObjectInfo, restoredObject bool, rmVersion bool) { func (es *expiryState) enqueueByDays(oi ObjectInfo, event lifecycle.Event, src lcEventSrc) {
select { select {
case <-GlobalContext.Done(): case <-GlobalContext.Done():
es.close() es.close()
case es.byDaysCh <- expiryTask{objInfo: oi, versionExpiry: rmVersion, restoredObject: restoredObject}: case es.byDaysCh <- expiryTask{objInfo: oi, event: event, src: src}:
default: default:
} }
} }
// enqueueByNewerNoncurrent enqueues object versions expired by // enqueueByNewerNoncurrent enqueues object versions expired by
// NewerNoncurrentVersions limit for expiry. // NewerNoncurrentVersions limit for expiry.
func (es *expiryState) enqueueByNewerNoncurrent(bucket string, versions []ObjectToDelete) { func (es *expiryState) enqueueByNewerNoncurrent(bucket string, versions []ObjectToDelete, lcEvent lifecycle.Event) {
select { select {
case <-GlobalContext.Done(): case <-GlobalContext.Done():
es.close() es.close()
case es.byNewerNoncurrentCh <- newerNoncurrentTask{bucket: bucket, versions: versions}: case es.byNewerNoncurrentCh <- newerNoncurrentTask{bucket: bucket, versions: versions, event: lcEvent}:
default: default:
} }
} }
@@ -129,7 +155,9 @@ func initBackgroundExpiry(ctx context.Context, objectAPI ObjectLayer) {
globalExpiryState = newExpiryState() globalExpiryState = newExpiryState()
workerSize, _ := strconv.Atoi(env.Get("_MINIO_ILM_EXPIRY_WORKERS", strconv.Itoa((runtime.GOMAXPROCS(0)+1)/2))) workerSize, _ := strconv.Atoi(env.Get("_MINIO_ILM_EXPIRY_WORKERS", strconv.Itoa((runtime.GOMAXPROCS(0)+1)/2)))
if workerSize == 0 {
workerSize = 4
}
ewk, err := workers.New(workerSize) ewk, err := workers.New(workerSize)
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
@@ -146,9 +174,9 @@ func initBackgroundExpiry(ctx context.Context, objectAPI ObjectLayer) {
go func(t expiryTask) { go func(t expiryTask) {
defer ewk.Give() defer ewk.Give()
if t.objInfo.TransitionedObject.Status != "" { if t.objInfo.TransitionedObject.Status != "" {
applyExpiryOnTransitionedObject(ctx, objectAPI, t.objInfo, t.restoredObject) applyExpiryOnTransitionedObject(ctx, objectAPI, t.objInfo, t.event, t.src)
} else { } else {
applyExpiryOnNonTransitionedObjects(ctx, objectAPI, t.objInfo, t.versionExpiry) applyExpiryOnNonTransitionedObjects(ctx, objectAPI, t.objInfo, t.event, t.src)
} }
}(t) }(t)
} }
@@ -159,7 +187,7 @@ func initBackgroundExpiry(ctx context.Context, objectAPI ObjectLayer) {
nwk.Take() nwk.Take()
go func(t newerNoncurrentTask) { go func(t newerNoncurrentTask) {
defer nwk.Give() defer nwk.Give()
deleteObjectVersions(ctx, objectAPI, t.bucket, t.versions) deleteObjectVersions(ctx, objectAPI, t.bucket, t.versions, t.event)
}(t) }(t)
} }
nwk.Wait() nwk.Wait()
@@ -171,11 +199,13 @@ func initBackgroundExpiry(ctx context.Context, objectAPI ObjectLayer) {
type newerNoncurrentTask struct { type newerNoncurrentTask struct {
bucket string bucket string
versions []ObjectToDelete versions []ObjectToDelete
event lifecycle.Event
} }
type transitionTask struct { type transitionTask struct {
tier string
objInfo ObjectInfo objInfo ObjectInfo
src lcEventSrc
event lifecycle.Event
} }
type transitionState struct { type transitionState struct {
@@ -193,10 +223,10 @@ type transitionState struct {
lastDayStats map[string]*lastDayTierStats lastDayStats map[string]*lastDayTierStats
} }
func (t *transitionState) queueTransitionTask(oi ObjectInfo, sc string) { func (t *transitionState) queueTransitionTask(oi ObjectInfo, event lifecycle.Event, src lcEventSrc) {
select { select {
case <-t.ctx.Done(): case <-t.ctx.Done():
case t.transitionCh <- transitionTask{objInfo: oi, tier: sc}: case t.transitionCh <- transitionTask{objInfo: oi, event: event, src: src}:
default: default:
} }
} }
@@ -249,9 +279,9 @@ func (t *transitionState) worker(objectAPI ObjectLayer) {
return return
} }
atomic.AddInt32(&t.activeTasks, 1) atomic.AddInt32(&t.activeTasks, 1)
if err := transitionObject(t.ctx, objectAPI, task.objInfo, task.tier); err != nil { if err := transitionObject(t.ctx, objectAPI, task.objInfo, newLifecycleAuditEvent(task.src, task.event)); err != nil {
logger.LogIf(t.ctx, fmt.Errorf("Transition failed for %s/%s version:%s with %w", logger.LogIf(t.ctx, fmt.Errorf("Transition to %s failed for %s/%s version:%s with %w",
task.objInfo.Bucket, task.objInfo.Name, task.objInfo.VersionID, err)) task.event.StorageClass, task.objInfo.Bucket, task.objInfo.Name, task.objInfo.VersionID, err))
} else { } else {
ts := tierStats{ ts := tierStats{
TotalSize: uint64(task.objInfo.Size), TotalSize: uint64(task.objInfo.Size),
@@ -260,7 +290,7 @@ func (t *transitionState) worker(objectAPI ObjectLayer) {
if task.objInfo.IsLatest { if task.objInfo.IsLatest {
ts.NumObjects = 1 ts.NumObjects = 1
} }
t.addLastDayStats(task.tier, ts) t.addLastDayStats(task.event.StorageClass, ts)
} }
atomic.AddInt32(&t.activeTasks, -1) atomic.AddInt32(&t.activeTasks, -1)
} }
@@ -331,41 +361,40 @@ func validateTransitionTier(lc *lifecycle.Lifecycle) error {
// enqueueTransitionImmediate enqueues obj for transition if eligible. // enqueueTransitionImmediate enqueues obj for transition if eligible.
// This is to be called after a successful upload of an object (version). // This is to be called after a successful upload of an object (version).
func enqueueTransitionImmediate(obj ObjectInfo) { func enqueueTransitionImmediate(obj ObjectInfo, src lcEventSrc) {
if lc, err := globalLifecycleSys.Get(obj.Bucket); err == nil { if lc, err := globalLifecycleSys.Get(obj.Bucket); err == nil {
event := lc.Eval(obj.ToLifecycleOpts()) switch event := lc.Eval(obj.ToLifecycleOpts()); event.Action {
switch event.Action {
case lifecycle.TransitionAction, lifecycle.TransitionVersionAction: case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
globalTransitionState.queueTransitionTask(obj, event.StorageClass) globalTransitionState.queueTransitionTask(obj, event, src)
} }
} }
} }
// expireAction represents different actions to be performed on expiry of a
// restored/transitioned object
type expireAction int
const (
// ignore the zero value
_ expireAction = iota
// expireObj indicates expiry of 'regular' transitioned objects.
expireObj
// expireRestoredObj indicates expiry of restored objects.
expireRestoredObj
)
// expireTransitionedObject handles expiry of transitioned/restored objects // expireTransitionedObject handles expiry of transitioned/restored objects
// (versions) in one of the following situations: // (versions) in one of the following situations:
// //
// 1. when a restored (via PostRestoreObject API) object expires. // 1. when a restored (via PostRestoreObject API) object expires.
// 2. when a transitioned object expires (based on an ILM rule). // 2. when a transitioned object expires (based on an ILM rule).
func expireTransitionedObject(ctx context.Context, objectAPI ObjectLayer, oi *ObjectInfo, lcOpts lifecycle.ObjectOpts, action expireAction) error { func expireTransitionedObject(ctx context.Context, objectAPI ObjectLayer, oi *ObjectInfo, lcOpts lifecycle.ObjectOpts, lcEvent lifecycle.Event, src lcEventSrc) error {
traceFn := globalLifecycleSys.trace(*oi)
var opts ObjectOptions var opts ObjectOptions
opts.Versioned = globalBucketVersioningSys.PrefixEnabled(oi.Bucket, oi.Name) opts.Versioned = globalBucketVersioningSys.PrefixEnabled(oi.Bucket, oi.Name)
opts.VersionID = lcOpts.VersionID opts.VersionID = lcOpts.VersionID
opts.Expiration = ExpirationOptions{Expire: true} opts.Expiration = ExpirationOptions{Expire: true}
switch action { tags := newLifecycleAuditEvent(src, lcEvent).Tags()
case expireObj: if lcEvent.Action.DeleteRestored() {
// delete locally restored copy of object or object version
// from the source, while leaving metadata behind. The data on
// transitioned tier lies untouched and still accessible
opts.Transition.ExpireRestored = true
_, err := objectAPI.DeleteObject(ctx, oi.Bucket, oi.Name, opts)
if err == nil {
// TODO consider including expiry of restored object to events we
// notify.
auditLogLifecycle(ctx, *oi, ILMExpiry, tags, traceFn)
}
return err
}
// When an object is past expiry or when a transitioned object is being // When an object is past expiry or when a transitioned object is being
// deleted, 'mark' the data in the remote tier for delete. // deleted, 'mark' the data in the remote tier for delete.
entry := jentry{ entry := jentry{
@@ -385,7 +414,7 @@ func expireTransitionedObject(ctx context.Context, objectAPI ObjectLayer, oi *Ob
} }
// Send audit for the lifecycle delete operation // Send audit for the lifecycle delete operation
auditLogLifecycle(ctx, *oi, ILMExpiry) defer auditLogLifecycle(ctx, *oi, ILMExpiry, tags, traceFn)
eventName := event.ObjectRemovedDelete eventName := event.ObjectRemovedDelete
if lcOpts.DeleteMarker { if lcOpts.DeleteMarker {
@@ -405,17 +434,6 @@ func expireTransitionedObject(ctx context.Context, objectAPI ObjectLayer, oi *Ob
Host: globalLocalNodeName, Host: globalLocalNodeName,
}) })
case expireRestoredObj:
// delete locally restored copy of object or object version
// from the source, while leaving metadata behind. The data on
// transitioned tier lies untouched and still accessible
opts.Transition.ExpireRestored = true
_, err := objectAPI.DeleteObject(ctx, oi.Bucket, oi.Name, opts)
return err
default:
return fmt.Errorf("Unknown expire action %v", action)
}
return nil return nil
} }
@@ -434,13 +452,14 @@ func genTransitionObjName(bucket string) (string, error) {
// storage specified by the transition ARN, the metadata is left behind on source cluster and original content // storage specified by the transition ARN, the metadata is left behind on source cluster and original content
// is moved to the transition tier. Note that in the case of encrypted objects, entire encrypted stream is moved // is moved to the transition tier. Note that in the case of encrypted objects, entire encrypted stream is moved
// to the transition tier without decrypting or re-encrypting. // to the transition tier without decrypting or re-encrypting.
func transitionObject(ctx context.Context, objectAPI ObjectLayer, oi ObjectInfo, tier string) error { func transitionObject(ctx context.Context, objectAPI ObjectLayer, oi ObjectInfo, lae lcAuditEvent) error {
opts := ObjectOptions{ opts := ObjectOptions{
Transition: TransitionOptions{ Transition: TransitionOptions{
Status: lifecycle.TransitionPending, Status: lifecycle.TransitionPending,
Tier: tier, Tier: lae.StorageClass,
ETag: oi.ETag, ETag: oi.ETag,
}, },
LifecycleAuditEvent: lae,
VersionID: oi.VersionID, VersionID: oi.VersionID,
Versioned: globalBucketVersioningSys.PrefixEnabled(oi.Bucket, oi.Name), Versioned: globalBucketVersioningSys.PrefixEnabled(oi.Bucket, oi.Name),
VersionSuspended: globalBucketVersioningSys.PrefixSuspended(oi.Bucket, oi.Name), VersionSuspended: globalBucketVersioningSys.PrefixSuspended(oi.Bucket, oi.Name),
-1
View File
@@ -190,7 +190,6 @@ func (api objectAPIHandlers) listObjectsV2Handler(ctx context.Context, w http.Re
} }
// Validate the query params before beginning to serve the request. // Validate the query params before beginning to serve the request.
// fetch-owner is not validated since it is a boolean
if s3Error := validateListObjectsArgs(prefix, token, delimiter, encodingType, maxKeys); s3Error != ErrNone { if s3Error := validateListObjectsArgs(prefix, token, delimiter, encodingType, maxKeys); s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
return return
+10 -9
View File
@@ -25,7 +25,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/minio-go/v7/pkg/tags" "github.com/minio/minio-go/v7/pkg/tags"
bucketsse "github.com/minio/minio/internal/bucket/encryption" bucketsse "github.com/minio/minio/internal/bucket/encryption"
@@ -36,8 +36,8 @@ import (
"github.com/minio/minio/internal/event" "github.com/minio/minio/internal/event"
"github.com/minio/minio/internal/kms" "github.com/minio/minio/internal/kms"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/minio/minio/internal/sync/errgroup"
"github.com/minio/pkg/bucket/policy" "github.com/minio/pkg/bucket/policy"
"github.com/minio/pkg/sync/errgroup"
) )
// BucketMetadataSys captures all bucket metadata for a given cluster. // BucketMetadataSys captures all bucket metadata for a given cluster.
@@ -121,6 +121,7 @@ func (sys *BucketMetadataSys) updateAndParse(ctx context.Context, bucket string,
meta.NotificationConfigXML = configData meta.NotificationConfigXML = configData
case bucketLifecycleConfig: case bucketLifecycleConfig:
meta.LifecycleConfigXML = configData meta.LifecycleConfigXML = configData
meta.LifecycleConfigUpdatedAt = updatedAt
case bucketSSEConfig: case bucketSSEConfig:
meta.EncryptionConfigXML = configData meta.EncryptionConfigXML = configData
meta.EncryptionConfigUpdatedAt = updatedAt meta.EncryptionConfigUpdatedAt = updatedAt
@@ -246,18 +247,18 @@ func (sys *BucketMetadataSys) GetObjectLockConfig(bucket string) (*objectlock.Co
// GetLifecycleConfig returns configured lifecycle config // GetLifecycleConfig returns configured lifecycle config
// The returned object may not be modified. // The returned object may not be modified.
func (sys *BucketMetadataSys) GetLifecycleConfig(bucket string) (*lifecycle.Lifecycle, error) { func (sys *BucketMetadataSys) GetLifecycleConfig(bucket string) (*lifecycle.Lifecycle, time.Time, error) {
meta, _, err := sys.GetConfig(GlobalContext, bucket) meta, _, err := sys.GetConfig(GlobalContext, bucket)
if err != nil { if err != nil {
if errors.Is(err, errConfigNotFound) { if errors.Is(err, errConfigNotFound) {
return nil, BucketLifecycleNotFound{Bucket: bucket} return nil, time.Time{}, BucketLifecycleNotFound{Bucket: bucket}
} }
return nil, err return nil, time.Time{}, err
} }
if meta.lifecycleConfig == nil { if meta.lifecycleConfig == nil {
return nil, BucketLifecycleNotFound{Bucket: bucket} return nil, time.Time{}, BucketLifecycleNotFound{Bucket: bucket}
} }
return meta.lifecycleConfig, nil return meta.lifecycleConfig, meta.LifecycleConfigUpdatedAt, nil
} }
// GetNotificationConfig returns configured notification config // GetNotificationConfig returns configured notification config
@@ -419,8 +420,8 @@ func (sys *BucketMetadataSys) Init(ctx context.Context, buckets []BucketInfo, ob
sys.objAPI = objAPI sys.objAPI = objAPI
// Load bucket metadata sys in background // Load bucket metadata sys.
go sys.init(ctx, buckets) sys.init(ctx, buckets)
return nil return nil
} }
+16 -1
View File
@@ -29,7 +29,7 @@ import (
"path" "path"
"time" "time"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio-go/v7/pkg/tags" "github.com/minio/minio-go/v7/pkg/tags"
bucketsse "github.com/minio/minio/internal/bucket/encryption" bucketsse "github.com/minio/minio/internal/bucket/encryption"
"github.com/minio/minio/internal/bucket/lifecycle" "github.com/minio/minio/internal/bucket/lifecycle"
@@ -88,6 +88,7 @@ type BucketMetadata struct {
QuotaConfigUpdatedAt time.Time QuotaConfigUpdatedAt time.Time
ReplicationConfigUpdatedAt time.Time ReplicationConfigUpdatedAt time.Time
VersioningConfigUpdatedAt time.Time VersioningConfigUpdatedAt time.Time
LifecycleConfigUpdatedAt time.Time
// Unexported fields. Must be updated atomically. // Unexported fields. Must be updated atomically.
policyConfig *policy.Policy policyConfig *policy.Policy
@@ -119,6 +120,16 @@ func newBucketMetadata(name string) BucketMetadata {
} }
} }
// Versioning returns true if versioning is enabled
func (b BucketMetadata) Versioning() bool {
return b.LockEnabled || (b.versioningConfig != nil && b.versioningConfig.Enabled()) || (b.objectLockConfig != nil && b.objectLockConfig.Enabled())
}
// ObjectLocking returns true if object locking is enabled
func (b BucketMetadata) ObjectLocking() bool {
return b.LockEnabled || (b.objectLockConfig != nil && b.objectLockConfig.Enabled())
}
// SetCreatedAt preserves the CreatedAt time for bucket across sites in site replication. It defaults to // SetCreatedAt preserves the CreatedAt time for bucket across sites in site replication. It defaults to
// creation time of bucket on this cluster in all other cases. // creation time of bucket on this cluster in all other cases.
func (b *BucketMetadata) SetCreatedAt(createdAt time.Time) { func (b *BucketMetadata) SetCreatedAt(createdAt time.Time) {
@@ -417,6 +428,10 @@ func (b *BucketMetadata) defaultTimestamps() {
if b.VersioningConfigUpdatedAt.IsZero() { if b.VersioningConfigUpdatedAt.IsZero() {
b.VersioningConfigUpdatedAt = b.Created b.VersioningConfigUpdatedAt = b.Created
} }
if b.LifecycleConfigUpdatedAt.IsZero() {
b.LifecycleConfigUpdatedAt = b.Created
}
} }
// Save config to supplied ObjectLayer api. // Save config to supplied ObjectLayer api.
+30 -5
View File
@@ -150,6 +150,12 @@ func (z *BucketMetadata) DecodeMsg(dc *msgp.Reader) (err error) {
err = msgp.WrapError(err, "VersioningConfigUpdatedAt") err = msgp.WrapError(err, "VersioningConfigUpdatedAt")
return return
} }
case "LifecycleConfigUpdatedAt":
z.LifecycleConfigUpdatedAt, err = dc.ReadTime()
if err != nil {
err = msgp.WrapError(err, "LifecycleConfigUpdatedAt")
return
}
default: default:
err = dc.Skip() err = dc.Skip()
if err != nil { if err != nil {
@@ -163,9 +169,9 @@ func (z *BucketMetadata) DecodeMsg(dc *msgp.Reader) (err error) {
// EncodeMsg implements msgp.Encodable // EncodeMsg implements msgp.Encodable
func (z *BucketMetadata) EncodeMsg(en *msgp.Writer) (err error) { func (z *BucketMetadata) EncodeMsg(en *msgp.Writer) (err error) {
// map header, size 21 // map header, size 22
// write "Name" // write "Name"
err = en.Append(0xde, 0x0, 0x15, 0xa4, 0x4e, 0x61, 0x6d, 0x65) err = en.Append(0xde, 0x0, 0x16, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
if err != nil { if err != nil {
return return
} }
@@ -374,15 +380,25 @@ func (z *BucketMetadata) EncodeMsg(en *msgp.Writer) (err error) {
err = msgp.WrapError(err, "VersioningConfigUpdatedAt") err = msgp.WrapError(err, "VersioningConfigUpdatedAt")
return return
} }
// write "LifecycleConfigUpdatedAt"
err = en.Append(0xb8, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
if err != nil {
return
}
err = en.WriteTime(z.LifecycleConfigUpdatedAt)
if err != nil {
err = msgp.WrapError(err, "LifecycleConfigUpdatedAt")
return
}
return return
} }
// MarshalMsg implements msgp.Marshaler // MarshalMsg implements msgp.Marshaler
func (z *BucketMetadata) MarshalMsg(b []byte) (o []byte, err error) { func (z *BucketMetadata) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize()) o = msgp.Require(b, z.Msgsize())
// map header, size 21 // map header, size 22
// string "Name" // string "Name"
o = append(o, 0xde, 0x0, 0x15, 0xa4, 0x4e, 0x61, 0x6d, 0x65) o = append(o, 0xde, 0x0, 0x16, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
o = msgp.AppendString(o, z.Name) o = msgp.AppendString(o, z.Name)
// string "Created" // string "Created"
o = append(o, 0xa7, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64) o = append(o, 0xa7, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64)
@@ -444,6 +460,9 @@ func (z *BucketMetadata) MarshalMsg(b []byte) (o []byte, err error) {
// string "VersioningConfigUpdatedAt" // string "VersioningConfigUpdatedAt"
o = append(o, 0xb9, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74) o = append(o, 0xb9, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
o = msgp.AppendTime(o, z.VersioningConfigUpdatedAt) o = msgp.AppendTime(o, z.VersioningConfigUpdatedAt)
// string "LifecycleConfigUpdatedAt"
o = append(o, 0xb8, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
o = msgp.AppendTime(o, z.LifecycleConfigUpdatedAt)
return return
} }
@@ -591,6 +610,12 @@ func (z *BucketMetadata) UnmarshalMsg(bts []byte) (o []byte, err error) {
err = msgp.WrapError(err, "VersioningConfigUpdatedAt") err = msgp.WrapError(err, "VersioningConfigUpdatedAt")
return return
} }
case "LifecycleConfigUpdatedAt":
z.LifecycleConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
if err != nil {
err = msgp.WrapError(err, "LifecycleConfigUpdatedAt")
return
}
default: default:
bts, err = msgp.Skip(bts) bts, err = msgp.Skip(bts)
if err != nil { if err != nil {
@@ -605,6 +630,6 @@ func (z *BucketMetadata) UnmarshalMsg(bts []byte) (o []byte, err error) {
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z *BucketMetadata) Msgsize() (s int) { func (z *BucketMetadata) Msgsize() (s int) {
s = 3 + 5 + msgp.StringPrefixSize + len(z.Name) + 8 + msgp.TimeSize + 12 + msgp.BoolSize + 17 + msgp.BytesPrefixSize + len(z.PolicyConfigJSON) + 22 + msgp.BytesPrefixSize + len(z.NotificationConfigXML) + 19 + msgp.BytesPrefixSize + len(z.LifecycleConfigXML) + 20 + msgp.BytesPrefixSize + len(z.ObjectLockConfigXML) + 20 + msgp.BytesPrefixSize + len(z.VersioningConfigXML) + 20 + msgp.BytesPrefixSize + len(z.EncryptionConfigXML) + 17 + msgp.BytesPrefixSize + len(z.TaggingConfigXML) + 16 + msgp.BytesPrefixSize + len(z.QuotaConfigJSON) + 21 + msgp.BytesPrefixSize + len(z.ReplicationConfigXML) + 24 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigJSON) + 28 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigMetaJSON) + 22 + msgp.TimeSize + 26 + msgp.TimeSize + 26 + msgp.TimeSize + 23 + msgp.TimeSize + 21 + msgp.TimeSize + 27 + msgp.TimeSize + 26 + msgp.TimeSize s = 3 + 5 + msgp.StringPrefixSize + len(z.Name) + 8 + msgp.TimeSize + 12 + msgp.BoolSize + 17 + msgp.BytesPrefixSize + len(z.PolicyConfigJSON) + 22 + msgp.BytesPrefixSize + len(z.NotificationConfigXML) + 19 + msgp.BytesPrefixSize + len(z.LifecycleConfigXML) + 20 + msgp.BytesPrefixSize + len(z.ObjectLockConfigXML) + 20 + msgp.BytesPrefixSize + len(z.VersioningConfigXML) + 20 + msgp.BytesPrefixSize + len(z.EncryptionConfigXML) + 17 + msgp.BytesPrefixSize + len(z.TaggingConfigXML) + 16 + msgp.BytesPrefixSize + len(z.QuotaConfigJSON) + 21 + msgp.BytesPrefixSize + len(z.ReplicationConfigXML) + 24 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigJSON) + 28 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigMetaJSON) + 22 + msgp.TimeSize + 26 + msgp.TimeSize + 26 + msgp.TimeSize + 23 + msgp.TimeSize + 21 + msgp.TimeSize + 27 + msgp.TimeSize + 26 + msgp.TimeSize + 25 + msgp.TimeSize
return return
} }
+1 -7
View File
@@ -83,12 +83,6 @@ func enforceRetentionForDeletion(ctx context.Context, objInfo ObjectInfo) (locke
// governance bypass headers are set and user has governance bypass permissions. // governance bypass headers are set and user has governance bypass permissions.
// Objects in "Compliance" mode can be overwritten only if retention date is past. // Objects in "Compliance" mode can be overwritten only if retention date is past.
func enforceRetentionBypassForDelete(ctx context.Context, r *http.Request, bucket string, object ObjectToDelete, oi ObjectInfo, gerr error) APIErrorCode { func enforceRetentionBypassForDelete(ctx context.Context, r *http.Request, bucket string, object ObjectToDelete, oi ObjectInfo, gerr error) APIErrorCode {
opts, err := getOpts(ctx, r, bucket, object.ObjectName)
if err != nil {
return toAPIErrorCode(ctx, err)
}
opts.VersionID = object.VersionID
if gerr != nil { // error from GetObjectInfo if gerr != nil { // error from GetObjectInfo
if _, ok := gerr.(MethodNotAllowed); ok { if _, ok := gerr.(MethodNotAllowed); ok {
// This happens usually for a delete marker // This happens usually for a delete marker
@@ -312,7 +306,7 @@ func checkPutObjectLockAllowed(ctx context.Context, rq *http.Request, bucket, ob
return mode, retainDate, legalHold, toAPIErrorCode(ctx, err) return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
} }
rMode, rDate, err := objectlock.ParseObjectLockRetentionHeaders(rq.Header) rMode, rDate, err := objectlock.ParseObjectLockRetentionHeaders(rq.Header)
if err != nil { if err != nil && !(replica && rMode == "" && rDate.IsZero()) {
return mode, retainDate, legalHold, toAPIErrorCode(ctx, err) return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
} }
if retentionPermErr != ErrNone { if retentionPermErr != ErrNone {
+2 -2
View File
@@ -24,7 +24,7 @@ import (
"net/http" "net/http"
humanize "github.com/dustin/go-humanize" humanize "github.com/dustin/go-humanize"
"github.com/minio/madmin-go/v2" "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"
"github.com/minio/pkg/bucket/policy" "github.com/minio/pkg/bucket/policy"
@@ -87,7 +87,7 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
bucketPolicy, err := policy.ParseConfig(bytes.NewReader(bucketPolicyBytes), bucket) bucketPolicy, err := policy.ParseConfig(bytes.NewReader(bucketPolicyBytes), bucket)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, APIError{ writeErrorResponse(ctx, w, APIError{
Code: "ErrMalformedXML", Code: "MalformedPolicy",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
Description: err.Error(), Description: err.Error(),
}, r.URL) }, r.URL)
+21 -9
View File
@@ -24,7 +24,7 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
) )
@@ -47,9 +47,13 @@ func NewBucketQuotaSys() *BucketQuotaSys {
// 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() { sys.bucketStorageCache.Once.Do(func() {
sys.bucketStorageCache.TTL = 1 * time.Second // Set this to 10 secs since its enough, as scanner
// does not update the bucket usage values frequently.
sys.bucketStorageCache.TTL = 10 * time.Second
// Rely on older value if usage loading fails from disk.
sys.bucketStorageCache.Relax = true
sys.bucketStorageCache.Update = func() (interface{}, error) { sys.bucketStorageCache.Update = func() (interface{}, error) {
ctx, done := context.WithTimeout(context.Background(), 5*time.Second) ctx, done := context.WithTimeout(context.Background(), 1*time.Second)
defer done() defer done()
return loadDataUsageFromBackend(ctx, objAPI) return loadDataUsageFromBackend(ctx, objAPI)
@@ -60,16 +64,20 @@ 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 := sys.bucketStorageCache.Get()
if err != nil { if err != nil && v != nil {
return BucketUsageInfo{}, err logger.LogIf(GlobalContext, fmt.Errorf("unable to retrieve usage information for bucket: %s, relying on older value cached in-memory: err(%v)", bucket, err))
} }
if v == nil {
logger.LogIf(GlobalContext, errors.New("unable to retrieve usage information for bucket: %s, no reliable usage value available - quota will not be enforced"))
}
var bui BucketUsageInfo
dui, ok := v.(DataUsageInfo) dui, ok := v.(DataUsageInfo)
if !ok { if ok {
return BucketUsageInfo{}, fmt.Errorf("internal error: Unexpected DUI data type: %T", v) bui = dui.BucketsUsage[bucket]
} }
bui := dui.BucketsUsage[bucket]
return bui, nil return bui, nil
} }
@@ -82,7 +90,7 @@ func parseBucketQuota(bucket string, data []byte) (quotaCfg *madmin.BucketQuota,
if !quotaCfg.IsValid() { if !quotaCfg.IsValid() {
if quotaCfg.Type == "fifo" { if quotaCfg.Type == "fifo" {
logger.LogIf(GlobalContext, errors.New("Detected older 'fifo' quota config, 'fifo' feature is removed and not supported anymore. Please clear your quota configs using 'mc admin bucket quota alias/bucket --clear' and use 'mc ilm add' for expiration of objects")) logger.LogIf(GlobalContext, errors.New("Detected older 'fifo' quota config, 'fifo' feature is removed and not supported anymore. Please clear your quota configs using 'mc admin bucket quota alias/bucket --clear' and use 'mc ilm add' for expiration of objects"))
return quotaCfg, nil return quotaCfg, fmt.Errorf("invalid quota type 'fifo'")
} }
return quotaCfg, fmt.Errorf("Invalid quota config %#v", quotaCfg) return quotaCfg, fmt.Errorf("Invalid quota config %#v", quotaCfg)
} }
@@ -100,6 +108,10 @@ func (sys *BucketQuotaSys) enforceQuotaHard(ctx context.Context, bucket string,
} }
if q != nil && q.Type == madmin.HardQuota && q.Quota > 0 { if q != nil && q.Type == madmin.HardQuota && q.Quota > 0 {
if uint64(size) >= q.Quota { // check if file size already exceeds the quota
return BucketQuotaExceeded{Bucket: bucket}
}
bui, err := sys.GetBucketUsageInfo(bucket) bui, err := sys.GetBucketUsageInfo(bucket)
if err != nil { if err != nil {
return err return err
+6 -3
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
// //
@@ -28,7 +28,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/bucket/replication" "github.com/minio/minio/internal/bucket/replication"
xhttp "github.com/minio/minio/internal/http" xhttp "github.com/minio/minio/internal/http"
) )
@@ -512,7 +512,10 @@ func getHealReplicateObjectInfo(objInfo ObjectInfo, rcfg replicationConfig) Repl
ObjectName: oi.Name, ObjectName: oi.Name,
VersionID: oi.VersionID, VersionID: oi.VersionID,
}, },
}, oi, ObjectOptions{VersionSuspended: globalBucketVersioningSys.PrefixSuspended(oi.Bucket, oi.Name)}, nil) }, oi, ObjectOptions{
Versioned: globalBucketVersioningSys.PrefixEnabled(oi.Bucket, oi.Name),
VersionSuspended: globalBucketVersioningSys.PrefixSuspended(oi.Bucket, oi.Name),
}, nil)
} else { } else {
dsc = mustReplicate(GlobalContext, oi.Bucket, oi.Name, getMustReplicateOptions(ObjectInfo{ dsc = mustReplicate(GlobalContext, oi.Bucket, oi.Name, getMustReplicateOptions(ObjectInfo{
UserDefined: oi.UserDefined, UserDefined: oi.UserDefined,
+143 -43
View File
@@ -26,6 +26,7 @@ import (
"math" "math"
"math/rand" "math/rand"
"net/http" "net/http"
"net/url"
"path" "path"
"reflect" "reflect"
"strings" "strings"
@@ -34,7 +35,7 @@ import (
"time" "time"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/minio/madmin-go/v2" "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/encrypt" "github.com/minio/minio-go/v7/pkg/encrypt"
"github.com/minio/minio-go/v7/pkg/tags" "github.com/minio/minio-go/v7/pkg/tags"
@@ -48,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/minio/pkg/workers"
"github.com/zeebo/xxh3" "github.com/zeebo/xxh3"
) )
@@ -138,6 +140,13 @@ func validateReplicationDestination(ctx context.Context, bucket string, rCfg *re
// validate replication ARN against target endpoint // validate replication ARN against target endpoint
c := globalBucketTargetSys.GetRemoteTargetClient(ctx, arnStr) c := globalBucketTargetSys.GetRemoteTargetClient(ctx, arnStr)
if c != nil { if c != nil {
if err := checkRemoteEndpoint(ctx, c.EndpointURL()); err != nil {
switch err.(type) {
case BucketRemoteIdenticalToSource:
return true, errorCodes.ToAPIErrWithErr(ErrBucketRemoteIdenticalToSource, fmt.Errorf("remote target endpoint %s is self referential", c.EndpointURL().String()))
default:
}
}
if c.EndpointURL().String() == clnt.EndpointURL().String() { if c.EndpointURL().String() == clnt.EndpointURL().String() {
selfTarget, _ := isLocalHost(clnt.EndpointURL().Hostname(), clnt.EndpointURL().Port(), globalMinioPort) selfTarget, _ := isLocalHost(clnt.EndpointURL().Hostname(), clnt.EndpointURL().Port(), globalMinioPort)
if !sameTarget { if !sameTarget {
@@ -154,6 +163,45 @@ func validateReplicationDestination(ctx context.Context, bucket string, rCfg *re
return sameTarget, toAPIError(ctx, nil) return sameTarget, toAPIError(ctx, nil)
} }
// performs a http request to remote endpoint to check if deployment id of remote endpoint is same as
// local cluster deployment id. This is to prevent replication to self, especially in case of a loadbalancer
// in front of MinIO.
func checkRemoteEndpoint(ctx context.Context, epURL *url.URL) error {
reqURL := &url.URL{
Scheme: epURL.Scheme,
Host: epURL.Host,
Path: healthCheckPathPrefix + healthCheckReadinessPath,
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL.String(), nil)
if err != nil {
return err
}
client := &http.Client{
Transport: NewHTTPTransport(),
Timeout: 10 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
return err
}
if err == nil {
// Drain the connection.
xhttp.DrainBody(resp.Body)
}
if resp != nil {
amzid := resp.Header.Get(xhttp.AmzRequestHostID)
if _, ok := globalNodeNamesHex[amzid]; ok {
return BucketRemoteIdenticalToSource{
Endpoint: epURL.String(),
}
}
}
return nil
}
type mustReplicateOptions struct { type mustReplicateOptions struct {
meta map[string]string meta map[string]string
status replication.StatusType status replication.StatusType
@@ -557,7 +605,7 @@ func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationI
return return
} }
if globalBucketTargetSys.isOffline(tgt.EndpointURL()) { if globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
logger.LogIf(ctx, fmt.Errorf("remote target is offline for bucket:%s arn:%s", dobj.Bucket, tgt.ARN)) logger.LogOnceIf(ctx, fmt.Errorf("remote target is offline for bucket:%s arn:%s", dobj.Bucket, tgt.ARN), "replication-target-offline-delete"+tgt.ARN)
sendEvent(eventArgs{ sendEvent(eventArgs{
BucketName: dobj.Bucket, BucketName: dobj.Bucket,
Object: ObjectInfo{ Object: ObjectInfo{
@@ -922,7 +970,7 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
cfg, err := getReplicationConfig(ctx, bucket) cfg, err := getReplicationConfig(ctx, bucket)
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogOnceIf(ctx, err, "get-replication-config"+bucket)
sendEvent(eventArgs{ sendEvent(eventArgs{
EventName: event.ObjectReplicationNotTracked, EventName: event.ObjectReplicationNotTracked,
BucketName: bucket, BucketName: bucket,
@@ -984,11 +1032,13 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
}(i, tgt) }(i, tgt)
} }
wg.Wait() wg.Wait()
replicationStatus = rinfos.ReplicationStatus() // used in defer function
// FIXME: add support for missing replication events // FIXME: add support for missing replication events
// - event.ObjectReplicationMissedThreshold // - event.ObjectReplicationMissedThreshold
// - event.ObjectReplicationReplicatedAfterThreshold // - event.ObjectReplicationReplicatedAfterThreshold
eventName := event.ObjectReplicationComplete eventName := event.ObjectReplicationComplete
if rinfos.ReplicationStatus() == replication.Failed { if replicationStatus == replication.Failed {
eventName = event.ObjectReplicationFailed eventName = event.ObjectReplicationFailed
} }
newReplStatusInternal := rinfos.ReplicationStatusInternal() newReplStatusInternal := rinfos.ReplicationStatusInternal()
@@ -1071,7 +1121,7 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
} }
if globalBucketTargetSys.isOffline(tgt.EndpointURL()) { if globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
logger.LogIf(ctx, fmt.Errorf("remote target is offline for bucket:%s arn:%s", bucket, tgt.ARN)) logger.LogOnceIf(ctx, fmt.Errorf("remote target is offline for bucket:%s arn:%s", bucket, tgt.ARN), "replication-target-offline-obj"+tgt.ARN)
sendEvent(eventArgs{ sendEvent(eventArgs{
EventName: event.ObjectReplicationNotTracked, EventName: event.ObjectReplicationNotTracked,
BucketName: bucket, BucketName: bucket,
@@ -1085,7 +1135,7 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
versioned := globalBucketVersioningSys.PrefixEnabled(bucket, object) versioned := globalBucketVersioningSys.PrefixEnabled(bucket, object)
versionSuspended := globalBucketVersioningSys.PrefixSuspended(bucket, object) versionSuspended := globalBucketVersioningSys.PrefixSuspended(bucket, object)
gr, err := objectAPI.GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, readLock, ObjectOptions{ gr, err := objectAPI.GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, ObjectOptions{
VersionID: objInfo.VersionID, VersionID: objInfo.VersionID,
Versioned: versioned, Versioned: versioned,
VersionSuspended: versionSuspended, VersionSuspended: versionSuspended,
@@ -1099,13 +1149,15 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
UserAgent: "Internal: [Replication]", UserAgent: "Internal: [Replication]",
Host: globalLocalNodeName, Host: globalLocalNodeName,
}) })
logger.LogIf(ctx, fmt.Errorf("Unable to update replicate metadata for %s/%s(%s): %w", bucket, object, objInfo.VersionID, err)) logger.LogIf(ctx, fmt.Errorf("unable to update replicate metadata for %s/%s(%s): %w", bucket, object, objInfo.VersionID, err))
} }
return return
} }
defer gr.Close() defer gr.Close()
objInfo = gr.ObjInfo objInfo = gr.ObjInfo
// make sure we have the latest metadata for metrics calculation
rinfo.PrevReplicationStatus = objInfo.TargetReplicationStatus(tgt.ARN)
size, err := objInfo.GetActualSize() size, err := objInfo.GetActualSize()
if err != nil { if err != nil {
@@ -1121,7 +1173,7 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
} }
if tgt.Bucket == "" { if tgt.Bucket == "" {
logger.LogIf(ctx, fmt.Errorf("Unable to replicate object %s(%s), bucket is empty", objInfo.Name, objInfo.VersionID)) logger.LogIf(ctx, fmt.Errorf("unable to replicate object %s(%s), bucket is empty", objInfo.Name, objInfo.VersionID))
sendEvent(eventArgs{ sendEvent(eventArgs{
EventName: event.ObjectReplicationNotTracked, EventName: event.ObjectReplicationNotTracked,
BucketName: bucket, BucketName: bucket,
@@ -1180,14 +1232,14 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
r, objInfo, putOpts); err != nil { r, objInfo, putOpts); err != nil {
if minio.ToErrorResponse(err).Code != "PreconditionFailed" { if minio.ToErrorResponse(err).Code != "PreconditionFailed" {
rinfo.ReplicationStatus = replication.Failed rinfo.ReplicationStatus = replication.Failed
logger.LogIf(ctx, fmt.Errorf("Unable to replicate for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err)) logger.LogIf(ctx, fmt.Errorf("unable to replicate for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err))
} }
} }
} else { } else {
if _, err = c.PutObject(ctx, tgt.Bucket, object, r, size, "", "", putOpts); err != nil { if _, err = c.PutObject(ctx, tgt.Bucket, object, r, size, "", "", putOpts); err != nil {
if minio.ToErrorResponse(err).Code != "PreconditionFailed" { if minio.ToErrorResponse(err).Code != "PreconditionFailed" {
rinfo.ReplicationStatus = replication.Failed rinfo.ReplicationStatus = replication.Failed
logger.LogIf(ctx, fmt.Errorf("Unable to replicate for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err)) logger.LogIf(ctx, fmt.Errorf("unable to replicate for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err))
} }
} }
} }
@@ -1219,7 +1271,7 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
} }
if globalBucketTargetSys.isOffline(tgt.EndpointURL()) { if globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
logger.LogIf(ctx, fmt.Errorf("remote target is offline for bucket:%s arn:%s", bucket, tgt.ARN)) logger.LogOnceIf(ctx, fmt.Errorf("remote target is offline for bucket:%s arn:%s", bucket, tgt.ARN), "replication-target-offline-all"+tgt.ARN)
sendEvent(eventArgs{ sendEvent(eventArgs{
EventName: event.ObjectReplicationNotTracked, EventName: event.ObjectReplicationNotTracked,
BucketName: bucket, BucketName: bucket,
@@ -1233,7 +1285,7 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
versioned := globalBucketVersioningSys.PrefixEnabled(bucket, object) versioned := globalBucketVersioningSys.PrefixEnabled(bucket, object)
versionSuspended := globalBucketVersioningSys.PrefixSuspended(bucket, object) versionSuspended := globalBucketVersioningSys.PrefixSuspended(bucket, object)
gr, err := objectAPI.GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, readLock, ObjectOptions{ gr, err := objectAPI.GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, ObjectOptions{
VersionID: objInfo.VersionID, VersionID: objInfo.VersionID,
Versioned: versioned, Versioned: versioned,
VersionSuspended: versionSuspended, VersionSuspended: versionSuspended,
@@ -1247,13 +1299,15 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
UserAgent: "Internal: [Replication]", UserAgent: "Internal: [Replication]",
Host: globalLocalNodeName, Host: globalLocalNodeName,
}) })
logger.LogIf(ctx, fmt.Errorf("Unable to update replicate metadata for %s/%s(%s): %w", bucket, object, objInfo.VersionID, err)) logger.LogIf(ctx, fmt.Errorf("unable to update replicate metadata for %s/%s(%s): %w", bucket, object, objInfo.VersionID, err))
} }
return return
} }
defer gr.Close() defer gr.Close()
objInfo = gr.ObjInfo objInfo = gr.ObjInfo
// make sure we have the latest metadata for metrics calculation
rinfo.PrevReplicationStatus = objInfo.TargetReplicationStatus(tgt.ARN)
// use latest ObjectInfo to check if previous replication attempt succeeded // use latest ObjectInfo to check if previous replication attempt succeeded
if objInfo.TargetReplicationStatus(tgt.ARN) == replication.Completed && !ri.ExistingObjResync.Empty() && !ri.ExistingObjResync.mustResyncTarget(tgt.ARN) { if objInfo.TargetReplicationStatus(tgt.ARN) == replication.Completed && !ri.ExistingObjResync.Empty() && !ri.ExistingObjResync.mustResyncTarget(tgt.ARN) {
@@ -1330,6 +1384,25 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
return return
} }
} }
// if target returns error other than NoSuchKey, defer replication attempt
if cerr != nil {
errResp := minio.ToErrorResponse(cerr)
switch errResp.Code {
case "NoSuchKey", "NoSuchVersion", "SlowDownRead":
rAction = replicateAll
default:
logger.LogIf(ctx, fmt.Errorf("unable to replicate %s/%s (%s). Target (%s) returned %s error on HEAD",
bucket, object, objInfo.VersionID, tgt.EndpointURL(), cerr))
sendEvent(eventArgs{
EventName: event.ObjectReplicationNotTracked,
BucketName: bucket,
Object: objInfo,
UserAgent: "Internal: [Replication]",
Host: globalLocalNodeName,
})
return
}
}
rinfo.ReplicationStatus = replication.Completed rinfo.ReplicationStatus = replication.Completed
rinfo.Size = size rinfo.Size = size
rinfo.ReplicationAction = rAction rinfo.ReplicationAction = rAction
@@ -1409,6 +1482,8 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, object string, r io.Reader, objInfo ObjectInfo, opts minio.PutObjectOptions) (err error) { func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, object string, r io.Reader, objInfo ObjectInfo, opts minio.PutObjectOptions) (err error) {
var uploadedParts []minio.CompletePart var uploadedParts []minio.CompletePart
// 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()
uploadID, err := c.NewMultipartUpload(context.Background(), bucket, object, opts) uploadID, err := c.NewMultipartUpload(context.Background(), bucket, object, opts)
if err != nil { if err != nil {
return err return err
@@ -1438,7 +1513,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(r, partInfo.ActualSize, "", "", partInfo.ActualSize) hr, err = hash.NewReader(io.LimitReader(r, partInfo.ActualSize), partInfo.ActualSize, "", "", partInfo.ActualSize)
if err != nil { if err != nil {
return err return err
} }
@@ -2038,6 +2113,7 @@ func proxyHeadToRepTarget(ctx context.Context, bucket, object string, rs *HTTPRa
StorageClass: objInfo.StorageClass, StorageClass: objInfo.StorageClass,
ReplicationStatusInternal: objInfo.ReplicationStatus, ReplicationStatusInternal: objInfo.ReplicationStatus,
UserTags: tags.String(), UserTags: tags.String(),
ReplicationStatus: replication.StatusType(objInfo.ReplicationStatus),
} }
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 {
@@ -2252,7 +2328,10 @@ func (s *replicationResyncer) PersistToDisk(ctx context.Context, objectAPI Objec
} }
} }
const resyncWorkerCnt = 50 // limit of number of bucket resyncs is progress at any given time const (
resyncWorkerCnt = 10 // limit of number of bucket resyncs is progress at any given time
resyncParallelRoutines = 10 // number of parallel resync ops per bucket
)
func newresyncer() *replicationResyncer { func newresyncer() *replicationResyncer {
rs := replicationResyncer{ rs := replicationResyncer{
@@ -2267,6 +2346,36 @@ func newresyncer() *replicationResyncer {
return &rs return &rs
} }
// mark status of replication resync on remote target for the bucket
func (s *replicationResyncer) markStatus(status ResyncStatusType, opts resyncOpts) {
s.Lock()
defer s.Unlock()
m := s.statusMap[opts.bucket]
st := m.TargetsMap[opts.arn]
st.LastUpdate = UTCNow()
st.ResyncStatus = status
m.TargetsMap[opts.arn] = st
m.LastUpdate = UTCNow()
s.statusMap[opts.bucket] = m
}
// update replication resync stats for bucket's remote target
func (s *replicationResyncer) incStats(ts TargetReplicationResyncStatus, opts resyncOpts) {
s.Lock()
defer s.Unlock()
m := s.statusMap[opts.bucket]
st := m.TargetsMap[opts.arn]
st.Object = ts.Object
st.ReplicatedCount += ts.ReplicatedCount
st.FailedCount += ts.FailedCount
st.ReplicatedSize += ts.ReplicatedSize
st.FailedSize += ts.FailedSize
m.TargetsMap[opts.arn] = st
m.LastUpdate = UTCNow()
s.statusMap[opts.bucket] = m
}
// resyncBucket resyncs all qualifying objects as per replication rules for the target // resyncBucket resyncs all qualifying objects as per replication rules for the target
// ARN // ARN
func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI ObjectLayer, heal bool, opts resyncOpts) { func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI ObjectLayer, heal bool, opts resyncOpts) {
@@ -2278,15 +2387,7 @@ func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI Object
resyncStatus := ResyncFailed resyncStatus := ResyncFailed
defer func() { defer func() {
s.Lock() s.markStatus(resyncStatus, opts)
m := s.statusMap[opts.bucket]
st := m.TargetsMap[opts.arn]
st.LastUpdate = UTCNow()
st.ResyncStatus = resyncStatus
m.TargetsMap[opts.arn] = st
m.LastUpdate = UTCNow()
s.statusMap[opts.bucket] = m
s.Unlock()
globalSiteResyncMetrics.incBucket(opts, resyncStatus) globalSiteResyncMetrics.incBucket(opts, resyncStatus)
s.workerCh <- struct{}{} s.workerCh <- struct{}{}
}() }()
@@ -2322,15 +2423,9 @@ func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI Object
} }
// mark resync status as resync started // mark resync status as resync started
if !heal { if !heal {
s.Lock() s.markStatus(ResyncStarted, opts)
m := s.statusMap[opts.bucket]
st := m.TargetsMap[opts.arn]
st.ResyncStatus = ResyncStarted
m.TargetsMap[opts.arn] = st
m.LastUpdate = UTCNow()
s.statusMap[opts.bucket] = m
s.Unlock()
} }
// Walk through all object versions - Walk() is always in ascending order needed to ensure // Walk through all object versions - Walk() is always in ascending order needed to ensure
// delete marker replicated to target after object version is first created. // delete marker replicated to target after object version is first created.
if err := objectAPI.Walk(ctx, opts.bucket, "", objInfoCh, ObjectOptions{}); err != nil { if err := objectAPI.Walk(ctx, opts.bucket, "", objInfoCh, ObjectOptions{}); err != nil {
@@ -2346,21 +2441,27 @@ func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI Object
if st.ResyncStatus == ResyncStarted || st.ResyncStatus == ResyncFailed { if st.ResyncStatus == ResyncStarted || st.ResyncStatus == ResyncFailed {
lastCheckpoint = st.Object lastCheckpoint = st.Object
} }
workers, err := workers.New(resyncParallelRoutines)
for obj := range objInfoCh { for obj := range objInfoCh {
select { select {
case <-s.resyncCancelCh: case <-s.resyncCancelCh:
resyncStatus = ResyncCanceled resyncStatus = ResyncCanceled
return return
case <-ctx.Done():
return
default: default:
} }
if heal && lastCheckpoint != "" && lastCheckpoint != obj.Name { if heal && lastCheckpoint != "" && lastCheckpoint != obj.Name {
continue continue
} }
lastCheckpoint = "" lastCheckpoint = ""
obj := obj
workers.Take()
go func() {
defer workers.Give()
roi := getHealReplicateObjectInfo(obj, rcfg) roi := getHealReplicateObjectInfo(obj, rcfg)
if !roi.ExistingObjResync.mustResync() { if !roi.ExistingObjResync.mustResync() {
continue return
} }
traceFn := s.trace(tgt.ResetID, fmt.Sprintf("%s/%s (%s)", opts.bucket, roi.Name, roi.VersionID)) traceFn := s.trace(tgt.ResetID, fmt.Sprintf("%s/%s (%s)", opts.bucket, roi.Name, roi.VersionID))
if roi.DeleteMarker || !roi.VersionPurgeStatus.Empty() { if roi.DeleteMarker || !roi.VersionPurgeStatus.Empty() {
@@ -2397,10 +2498,10 @@ func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI Object
ReplicationProxyRequest: "false", ReplicationProxyRequest: "false",
}, },
}) })
s.Lock() st := TargetReplicationResyncStatus{
m = s.statusMap[opts.bucket] Object: roi.Name,
st = m.TargetsMap[opts.arn] Bucket: roi.Bucket,
st.Object = roi.Name }
success := true success := true
if err != nil { if err != nil {
if roi.DeleteMarker && isErrMethodNotAllowed(ErrorRespToObjectError(err, opts.bucket, roi.Name)) { if roi.DeleteMarker && isErrMethodNotAllowed(ErrorRespToObjectError(err, opts.bucket, roi.Name)) {
@@ -2413,13 +2514,12 @@ func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI Object
st.ReplicatedCount++ st.ReplicatedCount++
st.ReplicatedSize += roi.Size st.ReplicatedSize += roi.Size
} }
m.TargetsMap[opts.arn] = st s.incStats(st, opts)
m.LastUpdate = UTCNow()
s.statusMap[opts.bucket] = m
s.Unlock()
traceFn(err) traceFn(err)
globalSiteResyncMetrics.updateMetric(roi, success, opts.resyncID) globalSiteResyncMetrics.updateMetric(roi, success, opts.resyncID)
}()
} }
workers.Wait()
resyncStatus = ResyncCompleted resyncStatus = ResyncCompleted
} }
@@ -3051,7 +3151,7 @@ func (p *ReplicationPool) loadStatsFromDisk() (rs map[string]BucketReplicationSt
data, err := readConfig(p.ctx, p.objLayer, getReplicationStatsPath()) data, err := readConfig(p.ctx, p.objLayer, getReplicationStatsPath())
if err != nil { if err != nil {
if !errors.Is(err, errConfigNotFound) { if errors.Is(err, errConfigNotFound) {
return rs, nil return rs, nil
} }
return rs, err return rs, err
+1 -1
View File
@@ -24,7 +24,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/bucket/replication" "github.com/minio/minio/internal/bucket/replication"
xhttp "github.com/minio/minio/internal/http" xhttp "github.com/minio/minio/internal/http"
) )
+3 -3
View File
@@ -25,7 +25,7 @@ import (
"time" "time"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"github.com/minio/madmin-go/v2" "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/credentials"
"github.com/minio/minio/internal/bucket/replication" "github.com/minio/minio/internal/bucket/replication"
@@ -208,14 +208,14 @@ func (sys *BucketTargetSys) SetTarget(ctx context.Context, bucket string, tgt *m
} }
clnt, err := sys.getRemoteTargetClient(tgt) clnt, err := sys.getRemoteTargetClient(tgt)
if err != nil { if err != nil {
return BucketRemoteTargetNotFound{Bucket: tgt.TargetBucket} return BucketRemoteTargetNotFound{Bucket: tgt.TargetBucket, Err: err}
} }
// validate if target credentials are ok // validate if target credentials are ok
exists, err := clnt.BucketExists(ctx, tgt.TargetBucket) exists, err := clnt.BucketExists(ctx, tgt.TargetBucket)
if err != nil { if err != nil {
switch minio.ToErrorResponse(err).Code { switch minio.ToErrorResponse(err).Code {
case "NoSuchBucket": case "NoSuchBucket":
return BucketRemoteTargetNotFound{Bucket: tgt.TargetBucket} return BucketRemoteTargetNotFound{Bucket: tgt.TargetBucket, Err: err}
case "AccessDenied": case "AccessDenied":
return RemoteTargetConnectionErr{Bucket: tgt.TargetBucket, AccessKey: tgt.Credentials.AccessKey, Err: err} return RemoteTargetConnectionErr{Bucket: tgt.TargetBucket, AccessKey: tgt.Credentials.AccessKey, Err: err}
} }
+1 -1
View File
@@ -24,7 +24,7 @@ import (
"net/http" "net/http"
humanize "github.com/dustin/go-humanize" humanize "github.com/dustin/go-humanize"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/bucket/versioning" "github.com/minio/minio/internal/bucket/versioning"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/minio/mux" "github.com/minio/mux"
+1 -1
View File
@@ -28,7 +28,7 @@ import (
"net/url" "net/url"
"time" "time"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
) )
+12 -14
View File
@@ -52,14 +52,13 @@ import (
"github.com/minio/console/restapi" "github.com/minio/console/restapi"
"github.com/minio/console/restapi/operations" "github.com/minio/console/restapi/operations"
"github.com/minio/kes-go" "github.com/minio/kes-go"
"github.com/minio/madmin-go/v2" "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/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"
"github.com/minio/minio/internal/config" "github.com/minio/minio/internal/config"
"github.com/minio/minio/internal/handlers"
"github.com/minio/minio/internal/kms" "github.com/minio/minio/internal/kms"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/minio/pkg/certs" "github.com/minio/pkg/certs"
@@ -133,20 +132,11 @@ func init() {
} }
}() }()
globalForwarder = handlers.NewForwarder(&handlers.Forwarder{
PassHost: true,
RoundTripper: NewHTTPTransportWithTimeout(1 * time.Hour),
Logger: func(err error) {
if err != nil && !errors.Is(err, context.Canceled) {
logger.LogIf(GlobalContext, err)
}
},
})
console.SetColor("Debug", fcolor.New()) console.SetColor("Debug", fcolor.New())
gob.Register(StorageErr("")) gob.Register(StorageErr(""))
gob.Register(madmin.TimeInfo{}) gob.Register(madmin.TimeInfo{})
gob.Register(madmin.XFSErrorConfigs{})
gob.Register(map[string]interface{}{}) gob.Register(map[string]interface{}{})
defaultAWSCredProvider = []credentials.Provider{ defaultAWSCredProvider = []credentials.Provider{
@@ -187,7 +177,7 @@ func minioConfigToConsoleFeatures() {
} }
} }
// pass the console subpath configuration // pass the console subpath configuration
if value := env.Get(config.EnvMinIOBrowserRedirectURL, ""); value != "" { if value := env.Get(config.EnvBrowserRedirectURL, ""); value != "" {
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)
@@ -208,6 +198,11 @@ func minioConfigToConsoleFeatures() {
if globalIAMSys.LDAPConfig.Enabled() { if globalIAMSys.LDAPConfig.Enabled() {
os.Setenv("CONSOLE_LDAP_ENABLED", config.EnableOn) os.Setenv("CONSOLE_LDAP_ENABLED", config.EnableOn)
} }
// Handle animation in welcome page
if value := env.Get(config.EnvBrowserLoginAnimation, "on"); value != "" {
os.Setenv("CONSOLE_ANIMATED_LOGIN", value)
}
os.Setenv("CONSOLE_MINIO_REGION", globalSite.Region) os.Setenv("CONSOLE_MINIO_REGION", globalSite.Region)
os.Setenv("CONSOLE_CERT_PASSWD", env.Get("MINIO_CERT_PASSWD", "")) os.Setenv("CONSOLE_CERT_PASSWD", env.Get("MINIO_CERT_PASSWD", ""))
@@ -632,7 +627,7 @@ func handleCommonEnvVars() {
logger.Fatal(config.ErrInvalidBrowserValue(err), "Invalid MINIO_BROWSER value in environment variable") logger.Fatal(config.ErrInvalidBrowserValue(err), "Invalid MINIO_BROWSER value in environment variable")
} }
if globalBrowserEnabled { if globalBrowserEnabled {
if redirectURL := env.Get(config.EnvMinIOBrowserRedirectURL, ""); redirectURL != "" { if redirectURL := env.Get(config.EnvBrowserRedirectURL, ""); redirectURL != "" {
u, err := xnet.ParseHTTPURL(redirectURL) u, err := xnet.ParseHTTPURL(redirectURL)
if err != nil { if err != nil {
logger.Fatal(err, "Invalid MINIO_BROWSER_REDIRECT_URL value in environment variable") logger.Fatal(err, "Invalid MINIO_BROWSER_REDIRECT_URL value in environment variable")
@@ -777,9 +772,12 @@ func handleCommonEnvVars() {
logger.Info(color.RedBold(msg)) logger.Info(color.RedBold(msg))
} }
globalActiveCred = cred globalActiveCred = cred
globalCredViaEnv = true
} else { } else {
globalActiveCred = auth.DefaultCredentials globalActiveCred = auth.DefaultCredentials
} }
globalDisableFreezeOnBoot = env.Get("_MINIO_DISABLE_API_FREEZE_ON_BOOT", "") == "true" || serverDebugLog
} }
// Initialize KMS global variable after valiadating and loading the configuration. // Initialize KMS global variable after valiadating and loading the configuration.
+1 -6
View File
@@ -30,12 +30,7 @@ import (
var errConfigNotFound = errors.New("config file not found") var errConfigNotFound = errors.New("config file not found")
func readConfigWithMetadata(ctx context.Context, store objectIO, configFile string, opts ObjectOptions) ([]byte, ObjectInfo, error) { func readConfigWithMetadata(ctx context.Context, store objectIO, configFile string, opts ObjectOptions) ([]byte, ObjectInfo, error) {
lockType := readLock r, err := store.GetObjectNInfo(ctx, minioMetaBucket, configFile, nil, http.Header{}, opts)
if opts.NoLock {
lockType = noLock // erasureObjects.GetObjectNInfo honors lockType argument but not opts.NoLock.
}
r, err := store.GetObjectNInfo(ctx, minioMetaBucket, configFile, nil, http.Header{}, lockType, opts)
if err != nil { if err != nil {
if isErrObjectNotFound(err) { if isErrObjectNotFound(err) {
return nil, ObjectInfo{}, errConfigNotFound return nil, ObjectInfo{}, errConfigNotFound
+12 -9
View File
@@ -24,7 +24,7 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/config" "github.com/minio/minio/internal/config"
"github.com/minio/minio/internal/config/api" "github.com/minio/minio/internal/config/api"
"github.com/minio/minio/internal/config/cache" "github.com/minio/minio/internal/config/cache"
@@ -283,7 +283,7 @@ var (
globalServerConfigMu sync.RWMutex globalServerConfigMu sync.RWMutex
) )
func validateSubSysConfig(s config.Config, subSys string, objAPI ObjectLayer) error { func validateSubSysConfig(ctx context.Context, s config.Config, subSys string, objAPI ObjectLayer) error {
switch subSys { switch subSys {
case config.SiteSubSys: case config.SiteSubSys:
if _, err := config.LookupSite(s[config.SiteSubSys][config.Default], s[config.RegionSubSys][config.Default]); err != nil { if _, err := config.LookupSite(s[config.SiteSubSys][config.Default], s[config.RegionSubSys][config.Default]); err != nil {
@@ -393,7 +393,7 @@ func validateSubSysConfig(s config.Config, subSys string, objAPI ObjectLayer) er
} }
if config.NotifySubSystems.Contains(subSys) { if config.NotifySubSystems.Contains(subSys) {
if err := notify.TestSubSysNotificationTargets(GlobalContext, s, subSys, NewHTTPTransport()); err != nil { if err := notify.TestSubSysNotificationTargets(ctx, s, subSys, NewHTTPTransport()); err != nil {
return err return err
} }
} }
@@ -407,7 +407,7 @@ func validateSubSysConfig(s config.Config, subSys string, objAPI ObjectLayer) er
return nil return nil
} }
func validateConfig(s config.Config, subSys string) error { func validateConfig(ctx context.Context, s config.Config, subSys string) error {
objAPI := newObjectLayerFn() objAPI := newObjectLayerFn()
// We must have a global lock for this so nobody else modifies env while we do. // We must have a global lock for this so nobody else modifies env while we do.
@@ -419,12 +419,12 @@ func validateConfig(s config.Config, subSys string) error {
// Enable env values to validate KMS. // Enable env values to validate KMS.
defer env.SetEnvOn() defer env.SetEnvOn()
if subSys != "" { if subSys != "" {
return validateSubSysConfig(s, subSys, objAPI) return validateSubSysConfig(ctx, s, subSys, objAPI)
} }
// No sub-system passed. Validate all of them. // No sub-system passed. Validate all of them.
for _, ss := range config.SubSystems.ToSlice() { for _, ss := range config.SubSystems.ToSlice() {
if err := validateSubSysConfig(s, ss, objAPI); err != nil { if err := validateSubSysConfig(ctx, s, ss, objAPI); err != nil {
return err return err
} }
} }
@@ -588,7 +588,7 @@ func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s conf
} }
loggerCfg.HTTP[n] = l loggerCfg.HTTP[n] = l
} }
if errs := logger.UpdateSystemTargets(loggerCfg); len(errs) > 0 { if errs := logger.UpdateSystemTargets(ctx, loggerCfg); len(errs) > 0 {
logger.LogIf(ctx, fmt.Errorf("Unable to update logger webhook config: %v", errs)) logger.LogIf(ctx, fmt.Errorf("Unable to update logger webhook config: %v", errs))
} }
case config.AuditWebhookSubSys: case config.AuditWebhookSubSys:
@@ -606,7 +606,7 @@ func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s conf
loggerCfg.AuditWebhook[n] = l loggerCfg.AuditWebhook[n] = l
} }
if errs := logger.UpdateAuditWebhookTargets(loggerCfg); len(errs) > 0 { if errs := logger.UpdateAuditWebhookTargets(ctx, loggerCfg); len(errs) > 0 {
logger.LogIf(ctx, fmt.Errorf("Unable to update audit webhook targets: %v", errs)) logger.LogIf(ctx, fmt.Errorf("Unable to update audit webhook targets: %v", errs))
} }
case config.AuditKafkaSubSys: case config.AuditKafkaSubSys:
@@ -616,11 +616,14 @@ func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s conf
} }
for n, l := range loggerCfg.AuditKafka { for n, l := range loggerCfg.AuditKafka {
if l.Enabled { if l.Enabled {
if l.TLS.Enable {
l.TLS.RootCAs = globalRootCAs
}
l.LogOnce = logger.LogOnceIf l.LogOnce = logger.LogOnceIf
loggerCfg.AuditKafka[n] = l loggerCfg.AuditKafka[n] = l
} }
} }
if errs := logger.UpdateAuditKafkaTargets(loggerCfg); len(errs) > 0 { if errs := logger.UpdateAuditKafkaTargets(ctx, loggerCfg); len(errs) > 0 {
logger.LogIf(ctx, fmt.Errorf("Unable to update audit kafka targets: %v", errs)) logger.LogIf(ctx, fmt.Errorf("Unable to update audit kafka targets: %v", errs))
} }
case config.StorageClassSubSys: case config.StorageClassSubSys:
-215
View File
@@ -1,215 +0,0 @@
// Copyright (c) 2015-2021 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package cmd
import (
"bytes"
"context"
"fmt"
"path"
"time"
"unicode/utf8"
"github.com/minio/madmin-go/v2"
"github.com/minio/minio/internal/config"
"github.com/minio/minio/internal/kms"
"github.com/minio/minio/internal/logger"
etcd "go.etcd.io/etcd/client/v3"
)
func handleEncryptedConfigBackend(objAPI ObjectLayer) error {
encrypted, err := checkBackendEncrypted(objAPI)
if err != nil {
return fmt.Errorf("Unable to encrypt config %w", err)
}
if !encrypted {
return nil
}
if err = migrateConfigPrefixToEncrypted(objAPI); err != nil {
return fmt.Errorf("Unable to migrate all config at .minio.sys/config/: %w", err)
}
return nil
}
const backendEncryptedFile = "backend-encrypted"
var backendEncryptedMigrationComplete = []byte("encrypted")
func checkBackendEtcdEncrypted(ctx context.Context, client *etcd.Client) (bool, error) {
bootstrapTrace("check if etcd backend is encrypted")
data, err := readKeyEtcd(ctx, client, backendEncryptedFile)
if err != nil && err != errConfigNotFound {
return false, err
}
return err == nil && bytes.Equal(data, backendEncryptedMigrationComplete), nil
}
func checkBackendEncrypted(objAPI ObjectLayer) (bool, error) {
bootstrapTrace("check if the config backend is encrypted")
data, err := readConfig(GlobalContext, objAPI, backendEncryptedFile)
if err != nil && err != errConfigNotFound {
return false, err
}
return err == nil && bytes.Equal(data, backendEncryptedMigrationComplete), nil
}
func migrateIAMConfigsEtcdToEncrypted(ctx context.Context, client *etcd.Client) error {
encrypted, err := checkBackendEtcdEncrypted(ctx, client)
if err != nil {
return err
}
// If backend doesn't have this file means we have already
// attempted then migration
if !encrypted {
return nil
}
bootstrapTrace("encrypt etcd config")
if GlobalKMS != nil {
stat, err := GlobalKMS.Stat(ctx)
if err != nil {
return err
}
logger.Info(fmt.Sprintf("Attempting to re-encrypt IAM users and policies on etcd with %q (%s)", stat.DefaultKey, stat.Name))
}
listCtx, cancel := context.WithTimeout(ctx, 1*time.Minute)
defer cancel()
r, err := client.Get(listCtx, minioConfigPrefix, etcd.WithPrefix(), etcd.WithKeysOnly())
if err != nil {
return err
}
for _, kv := range r.Kvs {
data, err := readKeyEtcd(ctx, client, string(kv.Key))
if err == errConfigNotFound { // Perhaps not present or someone deleted it.
continue
}
if err != nil {
return err
}
if !utf8.Valid(data) {
pdata, err := madmin.DecryptData(globalActiveCred.String(), bytes.NewReader(data))
if err != nil {
if GlobalKMS != nil {
pdata, err = config.DecryptBytes(GlobalKMS, data, kms.Context{
minioMetaBucket: path.Join(minioMetaBucket, string(kv.Key)),
})
if err != nil {
pdata, err = config.DecryptBytes(GlobalKMS, data, kms.Context{
minioMetaBucket: string(kv.Key),
})
if err != nil {
return fmt.Errorf("Decrypting IAM config failed %w, possibly credentials are incorrect", err)
}
}
} else {
return fmt.Errorf("Decrypting IAM config failed %w, possibly credentials are incorrect", err)
}
}
data = pdata
}
if GlobalKMS != nil {
data, err = config.EncryptBytes(GlobalKMS, data, kms.Context{
minioMetaBucket: path.Join(minioMetaBucket, string(kv.Key)),
})
if err != nil {
return err
}
}
if err = saveKeyEtcd(ctx, client, string(kv.Key), data); err != nil {
return err
}
}
if GlobalKMS != nil {
logger.Info("Migration of encrypted IAM config data completed. All data is now encrypted on etcd.")
}
return deleteKeyEtcd(ctx, client, backendEncryptedFile)
}
func migrateConfigPrefixToEncrypted(objAPI ObjectLayer) error {
bootstrapTrace("migrating config prefix to encrypted")
if GlobalKMS != nil {
stat, err := GlobalKMS.Stat(context.Background())
if err != nil {
return err
}
logger.Info(fmt.Sprintf("Attempting to re-encrypt config, IAM users and policies on MinIO with %q (%s)", stat.DefaultKey, stat.Name))
}
results := make(chan ObjectInfo)
if err := objAPI.Walk(GlobalContext, minioMetaBucket, minioConfigPrefix, results, ObjectOptions{}); err != nil {
return err
}
for obj := range results {
data, err := readConfig(GlobalContext, objAPI, obj.Name)
if err != nil {
return err
}
if !utf8.Valid(data) {
pdata, err := madmin.DecryptData(globalActiveCred.String(), bytes.NewReader(data))
if err != nil {
if GlobalKMS != nil {
pdata, err = config.DecryptBytes(GlobalKMS, data, kms.Context{
minioMetaBucket: path.Join(minioMetaBucket, obj.Name),
})
if err != nil {
pdata, err = config.DecryptBytes(GlobalKMS, data, kms.Context{
minioMetaBucket: obj.Name,
})
if err != nil {
return fmt.Errorf("Decrypting IAM config failed %w, possibly credentials are incorrect", err)
}
}
} else {
return fmt.Errorf("Decrypting IAM config failed %w, possibly credentials are incorrect", err)
}
}
data = pdata
}
if GlobalKMS != nil {
data, err = config.EncryptBytes(GlobalKMS, data, kms.Context{
obj.Bucket: path.Join(obj.Bucket, obj.Name),
})
if err != nil {
return err
}
}
if err = saveConfig(GlobalContext, objAPI, obj.Name, data); err != nil {
return err
}
}
if GlobalKMS != nil {
logger.Info("Migration of encrypted config data completed. All config data is now encrypted.")
}
return deleteConfig(GlobalContext, objAPI, backendEncryptedFile)
}
+1 -1
View File
@@ -21,7 +21,7 @@ import (
"bytes" "bytes"
"testing" "testing"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/auth"
) )
+62 -257
View File
@@ -18,16 +18,15 @@
package cmd package cmd
import ( import (
"bytes" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"strings" "strings"
"unicode/utf8"
"github.com/minio/madmin-go/v2"
"github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/config" "github.com/minio/minio/internal/config"
"github.com/minio/minio/internal/config/cache" "github.com/minio/minio/internal/config/cache"
@@ -39,7 +38,6 @@ import (
"github.com/minio/minio/internal/config/storageclass" "github.com/minio/minio/internal/config/storageclass"
"github.com/minio/minio/internal/event" "github.com/minio/minio/internal/event"
"github.com/minio/minio/internal/event/target" "github.com/minio/minio/internal/event/target"
"github.com/minio/minio/internal/kms"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/minio/minio/internal/logger/target/http" "github.com/minio/minio/internal/logger/target/http"
xnet "github.com/minio/pkg/net" xnet "github.com/minio/pkg/net"
@@ -2471,284 +2469,97 @@ func migrateConfigToMinioSys(objAPI ObjectLayer) (err error) {
return saveServerConfig(GlobalContext, objAPI, config) return saveServerConfig(GlobalContext, objAPI, config)
} }
// Migrates '.minio.sys/config.json' to v33. func readConfigWithoutMigrate(ctx context.Context, objAPI ObjectLayer) (config.Config, error) {
func migrateMinioSysConfig(objAPI ObjectLayer) error {
bootstrapTrace("migrate .minio.sys/config/config.json to latest version")
// Construct path to config.json for the given bucket. // Construct path to config.json for the given bucket.
configFile := path.Join(minioConfigPrefix, minioConfigFile) configFile := path.Join(minioConfigPrefix, minioConfigFile)
// Check if the config version is latest, if not migrate. configFiles := []string{
ok, _, err := checkConfigVersion(objAPI, configFile, "33") getConfigFile(),
getConfigFile() + ".deprecated",
configFile,
}
newServerCfg := func() (config.Config, error) {
// Initialize server config.
srvCfg := newServerConfig()
return srvCfg, saveServerConfig(ctx, objAPI, srvCfg)
}
var data []byte
var err error
cfg := &serverConfigV33{}
for _, cfgFile := range configFiles {
if _, err = Load(cfgFile, cfg); err != nil {
if !osIsNotExist(err) && !osIsPermission(err) {
return nil, err
}
continue
}
data, _ = json.Marshal(cfg)
break
}
if osIsPermission(err) {
logger.Info("Older config found but is not readable %s, proceeding to read config from other places", err)
}
if osIsNotExist(err) || osIsPermission(err) || len(data) == 0 {
data, err = readConfig(GlobalContext, objAPI, configFile)
if err != nil { if err != nil {
return err // when config.json is not found, then we freshly initialize.
if errors.Is(err, errConfigNotFound) {
return newServerCfg()
} }
if ok { return nil, err
return nil
} }
if err := migrateV27ToV28MinioSys(objAPI); err != nil { data, err = decryptData(data, configFile)
return err
}
if err := migrateV28ToV29MinioSys(objAPI); err != nil {
return err
}
if err := migrateV29ToV30MinioSys(objAPI); err != nil {
return err
}
if err := migrateV30ToV31MinioSys(objAPI); err != nil {
return err
}
if err := migrateV31ToV32MinioSys(objAPI); err != nil {
return err
}
return migrateV32ToV33MinioSys(objAPI)
}
func checkConfigVersion(objAPI ObjectLayer, configFile string, version string) (bool, []byte, error) {
data, err := readConfig(GlobalContext, objAPI, configFile)
if err != nil { if err != nil {
return false, nil, err return nil, err
} }
if !utf8.Valid(data) { newCfg, err := readServerConfig(GlobalContext, objAPI, data)
if GlobalKMS != nil { if err == nil {
data, err = config.DecryptBytes(GlobalKMS, data, kms.Context{ return newCfg, nil
minioMetaBucket: path.Join(minioMetaBucket, configFile),
})
} }
if GlobalKMS == nil && err != nil { // Read older `.minio.sys/config/config.json`, if not
data, err = madmin.DecryptData(globalActiveCred.String(), bytes.NewReader(data)) // possible just fail.
if err != nil {
if err == madmin.ErrMaliciousData || err == madmin.ErrUnexpectedHeader {
return false, nil, config.ErrInvalidConfigDecryptionKey(nil)
}
return false, nil, err
}
}
}
if version == "kvs" {
// Check api config values are present.
var vcfg struct {
API struct {
E []struct {
Key string `json:"key"`
Value string `json:"value"`
} `json:"_"`
} `json:"api"`
}
if err = json.Unmarshal(data, &vcfg); err != nil {
return false, nil, nil
}
return len(vcfg.API.E) > 0, data, nil
}
var versionConfig struct {
Version string `json:"version"`
}
vcfg := &versionConfig
if err = json.Unmarshal(data, vcfg); err != nil {
return false, nil, err
}
return vcfg.Version == version, data, nil
}
func migrateV27ToV28MinioSys(objAPI ObjectLayer) error {
configFile := path.Join(minioConfigPrefix, minioConfigFile)
ok, data, err := checkConfigVersion(objAPI, configFile, "27")
if err == errConfigNotFound {
return nil
} else if err != nil {
return fmt.Errorf("Unable to load config file. %w", err)
}
if !ok {
return nil
}
cfg := &serverConfigV28{}
if err = json.Unmarshal(data, cfg); err != nil { if err = json.Unmarshal(data, cfg); err != nil {
return err // Unable to parse old JSON simply re-initialize a new one.
return newServerCfg()
}
} }
cfg.Version = "28" if !globalCredViaEnv && cfg.Credential.IsValid() {
if err = saveServerConfig(GlobalContext, objAPI, cfg); err != nil { // Preserve older credential if we do not have
return fmt.Errorf("Failed to migrate config from ‘27’ to ‘28’. %w", err) // root credentials set via environment variable.
globalActiveCred = cfg.Credential
} }
logger.Info(configMigrateMSGTemplate, configFile, "27", "28")
return nil
}
func migrateV28ToV29MinioSys(objAPI ObjectLayer) error {
configFile := path.Join(minioConfigPrefix, minioConfigFile)
ok, data, err := checkConfigVersion(objAPI, configFile, "28")
if err == errConfigNotFound {
return nil
} else if err != nil {
return fmt.Errorf("Unable to load config file. %w", err)
}
if !ok {
return nil
}
cfg := &serverConfigV29{}
if err = json.Unmarshal(data, cfg); err != nil {
return err
}
cfg.Version = "29"
if err = saveServerConfig(GlobalContext, objAPI, cfg); err != nil {
return fmt.Errorf("Failed to migrate config from ‘28’ to ‘29’. %w", err)
}
logger.Info(configMigrateMSGTemplate, configFile, "28", "29")
return nil
}
func migrateV29ToV30MinioSys(objAPI ObjectLayer) error {
configFile := path.Join(minioConfigPrefix, minioConfigFile)
ok, data, err := checkConfigVersion(objAPI, configFile, "29")
if err == errConfigNotFound {
return nil
} else if err != nil {
return fmt.Errorf("Unable to load config file. %w", err)
}
if !ok {
return nil
}
cfg := &serverConfigV30{}
if err = json.Unmarshal(data, cfg); err != nil {
return err
}
cfg.Version = "30"
// Init compression config. For future migration, Compression config needs to be copied over from previous version. // Init compression config. For future migration, Compression config needs to be copied over from previous version.
switch cfg.Version {
case "29":
// V29 -> V30
cfg.Compression.Enabled = false cfg.Compression.Enabled = false
cfg.Compression.Extensions = strings.Split(compress.DefaultExtensions, config.ValueSeparator) cfg.Compression.Extensions = strings.Split(compress.DefaultExtensions, config.ValueSeparator)
cfg.Compression.MimeTypes = strings.Split(compress.DefaultMimeTypes, config.ValueSeparator) cfg.Compression.MimeTypes = strings.Split(compress.DefaultMimeTypes, config.ValueSeparator)
case "30":
if err = saveServerConfig(GlobalContext, objAPI, cfg); err != nil { // V30 -> V31
return fmt.Errorf("Failed to migrate config from ‘29’ to ‘30’. %w", err)
}
logger.Info(configMigrateMSGTemplate, configFile, "29", "30")
return nil
}
func migrateV30ToV31MinioSys(objAPI ObjectLayer) error {
configFile := path.Join(minioConfigPrefix, minioConfigFile)
ok, data, err := checkConfigVersion(objAPI, configFile, "30")
if err == errConfigNotFound {
return nil
} else if err != nil {
return fmt.Errorf("Unable to load config file. %w", err)
}
if !ok {
return nil
}
cfg := &serverConfigV31{}
if err = json.Unmarshal(data, cfg); err != nil {
return err
}
cfg.Version = "31"
cfg.OpenID = openid.Config{} cfg.OpenID = openid.Config{}
cfg.Policy.OPA = opa.Args{ cfg.Policy.OPA = opa.Args{
URL: &xnet.URL{}, URL: &xnet.URL{},
AuthToken: "", AuthToken: "",
} }
case "31":
if err = saveServerConfig(GlobalContext, objAPI, cfg); err != nil { // V31 -> V32
return fmt.Errorf("Failed to migrate config from ‘30’ to ‘31’. %w", err)
}
logger.Info(configMigrateMSGTemplate, configFile, "30", "31")
return nil
}
func migrateV31ToV32MinioSys(objAPI ObjectLayer) error {
configFile := path.Join(minioConfigPrefix, minioConfigFile)
ok, data, err := checkConfigVersion(objAPI, configFile, "31")
if err == errConfigNotFound {
return nil
} else if err != nil {
return fmt.Errorf("Unable to load config file. %w", err)
}
if !ok {
return nil
}
cfg := &serverConfigV32{}
if err = json.Unmarshal(data, cfg); err != nil {
return err
}
cfg.Version = "32"
cfg.Notify.NSQ = make(map[string]target.NSQArgs) cfg.Notify.NSQ = make(map[string]target.NSQArgs)
cfg.Notify.NSQ["1"] = target.NSQArgs{} cfg.Notify.NSQ["1"] = target.NSQArgs{}
if err = saveServerConfig(GlobalContext, objAPI, cfg); err != nil {
return fmt.Errorf("Failed to migrate config from ‘31’ to ‘32’. %w", err)
}
logger.Info(configMigrateMSGTemplate, configFile, "31", "32")
return nil
}
func migrateV32ToV33MinioSys(objAPI ObjectLayer) error {
configFile := path.Join(minioConfigPrefix, minioConfigFile)
ok, data, err := checkConfigVersion(objAPI, configFile, "32")
if err == errConfigNotFound {
return nil
} else if err != nil {
return fmt.Errorf("Unable to load config file. %w", err)
}
if !ok {
return nil
}
cfg := &serverConfigV33{}
if err = json.Unmarshal(data, cfg); err != nil {
return err
} }
// Move to latest.
cfg.Version = "33" cfg.Version = "33"
if err = saveServerConfig(GlobalContext, objAPI, cfg); err != nil {
return fmt.Errorf("Failed to migrate config from '32' to '33' . %w", err)
}
logger.Info(configMigrateMSGTemplate, configFile, "32", "33")
return nil
}
func migrateMinioSysConfigToKV(objAPI ObjectLayer) error {
bootstrapTrace("migrate config to KV style")
configFile := path.Join(minioConfigPrefix, minioConfigFile)
// Check if the config version is latest, if not migrate.
ok, data, err := checkConfigVersion(objAPI, configFile, "33")
if err != nil {
return err
}
if !ok {
return nil
}
cfg := &serverConfigV33{}
if err = json.Unmarshal(data, cfg); err != nil {
return err
}
newCfg := newServerConfig() newCfg := newServerConfig()
config.SetRegion(newCfg, cfg.Region) config.SetRegion(newCfg, cfg.Region)
@@ -2797,11 +2608,5 @@ func migrateMinioSysConfigToKV(objAPI ObjectLayer) error {
notify.SetNotifyWebhook(newCfg, k, args) notify.SetNotifyWebhook(newCfg, k, args)
} }
if err = saveServerConfig(GlobalContext, objAPI, newCfg); err != nil { return newCfg, nil
return err
}
logger.Info("Configuration file %s migrated from version '%s' to new KV format successfully.",
configFile, "33")
return nil
} }
+3 -2
View File
@@ -196,11 +196,12 @@ func TestServerConfigMigrateV2toV33(t *testing.T) {
t.Fatal("Unexpected error: ", err) t.Fatal("Unexpected error: ", err)
} }
if err := migrateMinioSysConfig(objLayer); err != nil { srvCfg, err := readConfigWithoutMigrate(context.Background(), objLayer)
if err != nil {
t.Fatal("Unexpected error: ", err) t.Fatal("Unexpected error: ", err)
} }
if err := migrateMinioSysConfigToKV(objLayer); err != nil { if err = saveServerConfig(GlobalContext, objLayer, srvCfg); err != nil {
t.Fatal("Unexpected error: ", err) t.Fatal("Unexpected error: ", err)
} }
-101
View File
@@ -717,107 +717,6 @@ type serverConfigV28 struct {
Logger logger.Config `json:"logger"` Logger logger.Config `json:"logger"`
} }
// serverConfigV29 is just like version '28'.
type serverConfigV29 serverConfigV28
// serverConfigV30 is just like version '29', stores additionally
// extensions and mimetypes fields for compression.
type serverConfigV30 struct {
Version string `json:"version"`
// S3 API configuration.
Credential auth.Credentials `json:"credential"`
Region string `json:"region"`
Worm config.BoolFlag `json:"worm"`
// Storage class configuration
StorageClass storageclass.Config `json:"storageclass"`
// Cache configuration
Cache cache.Config `json:"cache"`
// Notification queue configuration.
Notify notifierV3 `json:"notify"`
// Logger configuration
Logger logger.Config `json:"logger"`
// Compression configuration
Compression compress.Config `json:"compress"`
}
// serverConfigV31 is just like version '30', with OPA and OpenID configuration.
type serverConfigV31 struct {
Version string `json:"version"`
// S3 API configuration.
Credential auth.Credentials `json:"credential"`
Region string `json:"region"`
Worm config.BoolFlag `json:"worm"`
// Storage class configuration
StorageClass storageclass.Config `json:"storageclass"`
// Cache configuration
Cache cache.Config `json:"cache"`
// Notification queue configuration.
Notify notifierV3 `json:"notify"`
// Logger configuration
Logger logger.Config `json:"logger"`
// Compression configuration
Compression compress.Config `json:"compress"`
// OpenID configuration
OpenID openid.Config `json:"openid"`
// External policy enforcements.
Policy struct {
// OPA configuration.
OPA opa.Args `json:"opa"`
// Add new external policy enforcements here.
} `json:"policy"`
}
// serverConfigV32 is just like version '31' with added nsq notifer.
type serverConfigV32 struct {
Version string `json:"version"`
// S3 API configuration.
Credential auth.Credentials `json:"credential"`
Region string `json:"region"`
Worm config.BoolFlag `json:"worm"`
// Storage class configuration
StorageClass storageclass.Config `json:"storageclass"`
// Cache configuration
Cache cache.Config `json:"cache"`
// Notification queue configuration.
Notify notify.Config `json:"notify"`
// Logger configuration
Logger logger.Config `json:"logger"`
// Compression configuration
Compression compress.Config `json:"compress"`
// OpenID configuration
OpenID openid.Config `json:"openid"`
// External policy enforcements.
Policy struct {
// OPA configuration.
OPA opa.Args `json:"opa"`
// Add new external policy enforcements here.
} `json:"policy"`
}
// serverConfigV33 is just like version '32', removes clientID from NATS and MQTT, and adds queueDir, queueLimit in all notification targets. // serverConfigV33 is just like version '32', removes clientID from NATS and MQTT, and adds queueDir, queueLimit in all notification targets.
type serverConfigV33 struct { type serverConfigV33 struct {
quick.Config `json:"-"` // ignore interfaces quick.Config `json:"-"` // ignore interfaces
+26 -51
View File
@@ -25,10 +25,9 @@ import (
"path" "path"
"sort" "sort"
"strings" "strings"
"unicode/utf8"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/config" "github.com/minio/minio/internal/config"
"github.com/minio/minio/internal/kms" "github.com/minio/minio/internal/kms"
) )
@@ -65,16 +64,16 @@ func listServerConfigHistory(ctx context.Context, objAPI ObjectLayer, withData b
if withData { if withData {
data, err := readConfig(ctx, objAPI, obj.Name) data, err := readConfig(ctx, objAPI, obj.Name)
if err != nil { if err != nil {
return nil, err // ignore history file if not readable.
continue
} }
if GlobalKMS != nil {
data, err = config.DecryptBytes(GlobalKMS, data, kms.Context{ data, err = decryptData(data, obj.Name)
obj.Bucket: path.Join(obj.Bucket, obj.Name),
})
if err != nil { if err != nil {
return nil, err // ignore history file that cannot be loaded.
} continue
} }
cfgEntry.Data = string(data) cfgEntry.Data = string(data)
} }
configHistory = append(configHistory, cfgEntry) configHistory = append(configHistory, cfgEntry)
@@ -110,12 +109,7 @@ func readServerConfigHistory(ctx context.Context, objAPI ObjectLayer, uuidKV str
return nil, err return nil, err
} }
if GlobalKMS != nil { return decryptData(data, historyFile)
data, err = config.DecryptBytes(GlobalKMS, data, kms.Context{
minioMetaBucket: path.Join(minioMetaBucket, historyFile),
})
}
return data, err
} }
func saveServerConfigHistory(ctx context.Context, objAPI ObjectLayer, kv []byte) error { func saveServerConfigHistory(ctx context.Context, objAPI ObjectLayer, kv []byte) error {
@@ -167,16 +161,12 @@ func readServerConfig(ctx context.Context, objAPI ObjectLayer, data []byte) (con
return nil, err return nil, err
} }
if GlobalKMS != nil && !utf8.Valid(data) { data, err = decryptData(data, configFile)
data, err = config.DecryptBytes(GlobalKMS, data, kms.Context{
minioMetaBucket: path.Join(minioMetaBucket, configFile),
})
if err != nil { if err != nil {
lookupConfigs(srvCfg, objAPI) lookupConfigs(srvCfg, objAPI)
return nil, err return nil, err
} }
} }
}
json := jsoniter.ConfigCompatibleWithStandardLibrary json := jsoniter.ConfigCompatibleWithStandardLibrary
if err := json.Unmarshal(data, &srvCfg); err != nil { if err := json.Unmarshal(data, &srvCfg); err != nil {
@@ -205,47 +195,32 @@ func NewConfigSys() *ConfigSys {
} }
// Initialize and load config from remote etcd or local config directory // Initialize and load config from remote etcd or local config directory
func initConfig(objAPI ObjectLayer) error { func initConfig(objAPI ObjectLayer) (err error) {
bootstrapTrace("load the configuration") bootstrapTrace("load the configuration")
defer func() {
if err != nil {
bootstrapTrace(fmt.Sprintf("loading configuration failed: %v", err))
}
}()
if objAPI == nil { if objAPI == nil {
return errServerNotInitialized return errServerNotInitialized
} }
if isFile(getConfigFile()) { srvCfg, err := readConfigWithoutMigrate(GlobalContext, objAPI)
if err := migrateConfig(); err != nil { if err != nil {
return err return err
} }
}
// Check if the config version is latest (kvs), if not migrate. bootstrapTrace("lookup the configuration")
ok, data, err := checkConfigVersion(objAPI, path.Join(minioConfigPrefix, minioConfigFile), "kvs")
if err != nil && !errors.Is(err, errConfigNotFound) {
return err
}
if ok {
return loadConfig(objAPI, data)
}
// Migrates ${HOME}/.minio/config.json or config.json.deprecated // Override any values from ENVs.
// to '<export_path>/.minio.sys/config/config.json' lookupConfigs(srvCfg, objAPI)
// ignore if the file doesn't exist.
// If etcd is set then migrates /config/config.json
// to '<export_path>/.minio.sys/config/config.json'
if err := migrateConfigToMinioSys(objAPI); err != nil {
return fmt.Errorf("migrateConfigToMinioSys: %w", err)
}
// Migrates backend '<export_path>/.minio.sys/config/config.json' to latest version. // hold the mutex lock before a new config is assigned.
if err := migrateMinioSysConfig(objAPI); err != nil { globalServerConfigMu.Lock()
return fmt.Errorf("migrateMinioSysConfig: %w", err) globalServerConfig = srvCfg
} globalServerConfigMu.Unlock()
// Migrates backend '<export_path>/.minio.sys/config/config.json' to return nil
// latest config format.
if err := migrateMinioSysConfigToKV(objAPI); err != nil {
return fmt.Errorf("migrateMinioSysConfigToKV: %w", err)
}
return loadConfig(objAPI, nil)
} }
+5 -5
View File
@@ -23,7 +23,7 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/minio/minio/internal/logger/target/console" "github.com/minio/minio/internal/logger/target/console"
"github.com/minio/minio/internal/logger/target/types" "github.com/minio/minio/internal/logger/target/types"
@@ -58,7 +58,7 @@ func NewConsoleLogger(ctx context.Context) *HTTPConsoleLoggerSys {
} }
// IsOnline always true in case of console logger // IsOnline always true in case of console logger
func (sys *HTTPConsoleLoggerSys) IsOnline() bool { func (sys *HTTPConsoleLoggerSys) IsOnline(_ context.Context) bool {
return true return true
} }
@@ -87,7 +87,7 @@ func (sys *HTTPConsoleLoggerSys) HasLogListeners() bool {
func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan log.Info, doneCh <-chan struct{}, node string, last int, logKind madmin.LogMask, filter func(entry log.Info) bool) error { func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan log.Info, doneCh <-chan struct{}, node string, last int, logKind madmin.LogMask, filter func(entry log.Info) bool) error {
// Enable console logging for remote client. // Enable console logging for remote client.
if !sys.HasLogListeners() { if !sys.HasLogListeners() {
logger.AddSystemTarget(sys) logger.AddSystemTarget(GlobalContext, sys)
} }
cnt := 0 cnt := 0
@@ -128,7 +128,7 @@ func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan log.Info, doneCh <-chan st
} }
// Init if HTTPConsoleLoggerSys is valid, always returns nil right now // Init if HTTPConsoleLoggerSys is valid, always returns nil right now
func (sys *HTTPConsoleLoggerSys) Init() error { func (sys *HTTPConsoleLoggerSys) Init(_ context.Context) error {
return nil return nil
} }
@@ -180,7 +180,7 @@ func (sys *HTTPConsoleLoggerSys) Type() types.TargetType {
// Send log message 'e' to console and publish to console // Send log message 'e' to console and publish to console
// log pubsub system // log pubsub system
func (sys *HTTPConsoleLoggerSys) Send(entry interface{}) error { func (sys *HTTPConsoleLoggerSys) Send(ctx context.Context, entry interface{}) error {
var lg log.Info var lg log.Info
switch e := entry.(type) { switch e := entry.(type) {
case log.Entry: case log.Entry:
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"time" "time"
"unsafe" "unsafe"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/bucket/lifecycle" "github.com/minio/minio/internal/bucket/lifecycle"
) )
+37 -33
View File
@@ -34,7 +34,7 @@ import (
"time" "time"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/bucket/lifecycle" "github.com/minio/minio/internal/bucket/lifecycle"
"github.com/minio/minio/internal/bucket/object/lock" "github.com/minio/minio/internal/bucket/object/lock"
"github.com/minio/minio/internal/bucket/replication" "github.com/minio/minio/internal/bucket/replication"
@@ -373,7 +373,7 @@ func (f *folderScanner) sendUpdate() {
} }
if flat := f.updateCache.sizeRecursive(f.newCache.Info.Name); flat != nil { if flat := f.updateCache.sizeRecursive(f.newCache.Info.Name); flat != nil {
select { select {
case f.updates <- *flat: case f.updates <- flat.clone():
default: default:
} }
f.lastUpdate = time.Now() f.lastUpdate = time.Now()
@@ -941,13 +941,13 @@ func (i *scannerItem) applyLifecycle(ctx context.Context, o ObjectLayer, oi Obje
console.Debugf(applyActionsLogPrefix+" lifecycle: %q Initial scan: %v\n", i.objectPath(), lcEvt.Action) console.Debugf(applyActionsLogPrefix+" lifecycle: %q Initial scan: %v\n", i.objectPath(), lcEvt.Action)
} }
} }
defer globalScannerMetrics.timeILM(lcEvt.Action) defer globalScannerMetrics.timeILM(lcEvt.Action)()
switch lcEvt.Action { switch lcEvt.Action {
case lifecycle.DeleteAction, lifecycle.DeleteVersionAction, lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction: case lifecycle.DeleteAction, lifecycle.DeleteVersionAction, lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction:
return applyLifecycleAction(lcEvt.Action, oi, ""), 0 return applyLifecycleAction(lcEvt, lcEventSrc_Scanner, oi), 0
case lifecycle.TransitionAction, lifecycle.TransitionVersionAction: case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
return applyLifecycleAction(lcEvt.Action, oi, lcEvt.StorageClass), size return applyLifecycleAction(lcEvt, lcEventSrc_Scanner, oi), size
default: default:
// No action. // No action.
return false, size return false, size
@@ -957,6 +957,7 @@ func (i *scannerItem) applyLifecycle(ctx context.Context, o ObjectLayer, oi Obje
// applyTierObjSweep removes remote object pending deletion and the free-version // applyTierObjSweep removes remote object pending deletion and the free-version
// tracking this information. // tracking this information.
func (i *scannerItem) applyTierObjSweep(ctx context.Context, o ObjectLayer, oi ObjectInfo) { func (i *scannerItem) applyTierObjSweep(ctx context.Context, o ObjectLayer, oi ObjectInfo) {
traceFn := globalLifecycleSys.trace(oi)
if !oi.TransitionedObject.FreeVersion { if !oi.TransitionedObject.FreeVersion {
// nothing to be done // nothing to be done
return return
@@ -982,7 +983,7 @@ func (i *scannerItem) applyTierObjSweep(ctx context.Context, o ObjectLayer, oi O
InclFreeVersions: true, InclFreeVersions: true,
}) })
if err == nil { if err == nil {
auditLogLifecycle(ctx, oi, ILMFreeVersionDelete) auditLogLifecycle(ctx, oi, ILMFreeVersionDelete, nil, traceFn)
} }
if ignoreNotFoundErr(err) != nil { if ignoreNotFoundErr(err) != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
@@ -1010,7 +1011,8 @@ func (i *scannerItem) applyNewerNoncurrentVersionLimit(ctx context.Context, _ Ob
return objectInfos, nil return objectInfos, nil
} }
_, days, lim := i.lifeCycle.NoncurrentVersionsExpirationLimit(lifecycle.ObjectOpts{Name: i.objectPath()}) event := i.lifeCycle.NoncurrentVersionsExpirationLimit(lifecycle.ObjectOpts{Name: i.objectPath()})
lim := event.NewerNoncurrentVersions
if lim == 0 || len(fivs) <= lim+1 { // fewer than lim _noncurrent_ versions if lim == 0 || len(fivs) <= lim+1 { // fewer than lim _noncurrent_ versions
for _, fi := range fivs { for _, fi := range fivs {
objectInfos = append(objectInfos, fi.ToObjectInfo(i.bucket, i.objectPath(), versioned)) objectInfos = append(objectInfos, fi.ToObjectInfo(i.bucket, i.objectPath(), versioned))
@@ -1039,7 +1041,7 @@ func (i *scannerItem) applyNewerNoncurrentVersionLimit(ctx context.Context, _ Ob
} }
// NoncurrentDays not passed yet. // NoncurrentDays not passed yet.
if time.Now().UTC().Before(lifecycle.ExpectedExpiryTime(obj.SuccessorModTime, days)) { if time.Now().UTC().Before(lifecycle.ExpectedExpiryTime(obj.SuccessorModTime, event.NoncurrentDays)) {
// add this version back to remaining versions for // add this version back to remaining versions for
// subsequent lifecycle policy applications // subsequent lifecycle policy applications
fivs = append(fivs, fi) fivs = append(fivs, fi)
@@ -1054,7 +1056,7 @@ func (i *scannerItem) applyNewerNoncurrentVersionLimit(ctx context.Context, _ Ob
}) })
} }
globalExpiryState.enqueueByNewerNoncurrent(i.bucket, toDel) globalExpiryState.enqueueByNewerNoncurrent(i.bucket, toDel, event)
return objectInfos, nil return objectInfos, nil
} }
@@ -1152,20 +1154,16 @@ func evalActionFromLifecycle(ctx context.Context, lc lifecycle.Lifecycle, lr loc
return event return event
} }
func applyTransitionRule(obj ObjectInfo, storageClass string) bool { func applyTransitionRule(event lifecycle.Event, src lcEventSrc, obj ObjectInfo) bool {
if obj.DeleteMarker { if obj.DeleteMarker {
return false return false
} }
globalTransitionState.queueTransitionTask(obj, storageClass) globalTransitionState.queueTransitionTask(obj, event, src)
return true return true
} }
func applyExpiryOnTransitionedObject(ctx context.Context, objLayer ObjectLayer, obj ObjectInfo, restoredObject bool) bool { func applyExpiryOnTransitionedObject(ctx context.Context, objLayer ObjectLayer, obj ObjectInfo, lcEvent lifecycle.Event, src lcEventSrc) bool {
action := expireObj if err := expireTransitionedObject(ctx, objLayer, &obj, obj.ToLifecycleOpts(), lcEvent, src); err != nil {
if restoredObject {
action = expireRestoredObj
}
if err := expireTransitionedObject(ctx, objLayer, &obj, obj.ToLifecycleOpts(), action); err != nil {
if isErrObjectNotFound(err) || isErrVersionNotFound(err) { if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
return false return false
} }
@@ -1176,18 +1174,22 @@ func applyExpiryOnTransitionedObject(ctx context.Context, objLayer ObjectLayer,
return true return true
} }
func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLayer, obj ObjectInfo, applyOnVersion bool) bool { func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLayer, obj ObjectInfo, lcEvent lifecycle.Event, src lcEventSrc) bool {
traceFn := globalLifecycleSys.trace(obj)
opts := ObjectOptions{ opts := ObjectOptions{
Expiration: ExpirationOptions{Expire: true}, Expiration: ExpirationOptions{Expire: true},
} }
if applyOnVersion { if lcEvent.Action.DeleteVersioned() {
opts.VersionID = obj.VersionID opts.VersionID = obj.VersionID
} }
if opts.VersionID == "" { if opts.VersionID == "" {
opts.Versioned = globalBucketVersioningSys.PrefixEnabled(obj.Bucket, obj.Name) opts.Versioned = globalBucketVersioningSys.PrefixEnabled(obj.Bucket, obj.Name)
opts.VersionSuspended = globalBucketVersioningSys.PrefixSuspended(obj.Bucket, obj.Name) opts.VersionSuspended = globalBucketVersioningSys.PrefixSuspended(obj.Bucket, obj.Name)
} }
if lcEvent.Action.DeleteAll() {
opts.DeletePrefix = true
}
obj, err := objLayer.DeleteObject(ctx, obj.Bucket, obj.Name, opts) obj, err := objLayer.DeleteObject(ctx, obj.Bucket, obj.Name, opts)
if err != nil { if err != nil {
@@ -1195,12 +1197,13 @@ func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLay
return false return false
} }
// Assume it is still there. // Assume it is still there.
logger.LogIf(ctx, err) logger.LogOnceIf(ctx, err, "non-transition-expiry")
return false return false
} }
tags := newLifecycleAuditEvent(src, lcEvent).Tags()
// Send audit for the lifecycle delete operation // Send audit for the lifecycle delete operation
auditLogLifecycle(ctx, obj, ILMExpiry) auditLogLifecycle(ctx, obj, ILMExpiry, tags, traceFn)
eventName := event.ObjectRemovedDelete eventName := event.ObjectRemovedDelete
if obj.DeleteMarker { if obj.DeleteMarker {
@@ -1220,20 +1223,19 @@ func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLay
} }
// Apply object, object version, restored object or restored object version action on the given object // Apply object, object version, restored object or restored object version action on the given object
func applyExpiryRule(obj ObjectInfo, restoredObject, applyOnVersion bool) bool { func applyExpiryRule(event lifecycle.Event, src lcEventSrc, obj ObjectInfo) bool {
globalExpiryState.enqueueByDays(obj, restoredObject, applyOnVersion) globalExpiryState.enqueueByDays(obj, event, src)
return true return true
} }
// Perform actions (removal or transitioning of objects), return true the action is successfully performed // Perform actions (removal or transitioning of objects), return true the action is successfully performed
func applyLifecycleAction(action lifecycle.Action, obj ObjectInfo, storageClass string) (success bool) { func applyLifecycleAction(event lifecycle.Event, src lcEventSrc, obj ObjectInfo) (success bool) {
switch action { switch action := event.Action; action {
case lifecycle.DeleteVersionAction, lifecycle.DeleteAction: case lifecycle.DeleteVersionAction, lifecycle.DeleteAction,
success = applyExpiryRule(obj, false, action == lifecycle.DeleteVersionAction) lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction:
case lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction: success = applyExpiryRule(event, src, obj)
success = applyExpiryRule(obj, true, action == lifecycle.DeleteRestoredVersionAction)
case lifecycle.TransitionAction, lifecycle.TransitionVersionAction: case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
success = applyTransitionRule(obj, storageClass) success = applyTransitionRule(event, src, obj)
} }
return return
} }
@@ -1347,11 +1349,11 @@ func (d *dynamicSleeper) Timer(ctx context.Context) func() {
select { select {
case <-ctx.Done(): case <-ctx.Done():
if !timer.Stop() { if !timer.Stop() {
<-timer.C
}
if d.isScanner { if d.isScanner {
globalScannerMetrics.incTime(scannerMetricYield, wantSleep) globalScannerMetrics.incTime(scannerMetricYield, wantSleep)
} }
<-timer.C
}
return return
case <-timer.C: case <-timer.C:
if d.isScanner { if d.isScanner {
@@ -1443,7 +1445,7 @@ const (
ILMTransition = " ilm:transition" ILMTransition = " ilm:transition"
) )
func auditLogLifecycle(ctx context.Context, oi ObjectInfo, event string) { func auditLogLifecycle(ctx context.Context, oi ObjectInfo, event string, tags map[string]interface{}, traceFn func(event string)) {
var apiName string var apiName string
switch event { switch event {
case ILMExpiry: case ILMExpiry:
@@ -1459,5 +1461,7 @@ func auditLogLifecycle(ctx context.Context, oi ObjectInfo, event string) {
Bucket: oi.Bucket, Bucket: oi.Bucket,
Object: oi.Name, Object: oi.Name,
VersionID: oi.VersionID, VersionID: oi.VersionID,
Tags: tags,
}) })
traceFn(event)
} }
+39 -9
View File
@@ -33,7 +33,7 @@ import (
"github.com/cespare/xxhash/v2" "github.com/cespare/xxhash/v2"
"github.com/klauspost/compress/zstd" "github.com/klauspost/compress/zstd"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/bucket/lifecycle" "github.com/minio/minio/internal/bucket/lifecycle"
"github.com/minio/minio/internal/hash" "github.com/minio/minio/internal/hash"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
@@ -88,6 +88,20 @@ func (ats *allTierStats) merge(other *allTierStats) {
} }
} }
func (ats *allTierStats) clone() *allTierStats {
if ats == nil {
return nil
}
dst := *ats
if dst.Tiers != nil {
dst.Tiers = make(map[string]tierStats, len(dst.Tiers))
for tier, st := range dst.Tiers {
dst.Tiers[tier] = st
}
}
return &dst
}
func (ats *allTierStats) adminStats(stats map[string]madmin.TierStats) map[string]madmin.TierStats { func (ats *allTierStats) adminStats(stats map[string]madmin.TierStats) map[string]madmin.TierStats {
if ats == nil { if ats == nil {
return stats return stats
@@ -168,6 +182,25 @@ type replicationAllStatsV1 struct {
ReplicaSize uint64 `msg:"ReplicaSize,omitempty"` ReplicaSize uint64 `msg:"ReplicaSize,omitempty"`
} }
// clone creates a deep-copy clone.
func (r *replicationAllStats) clone() *replicationAllStats {
if r == nil {
return nil
}
// Shallow copy
dst := *r
// Copy individual targets.
if dst.Targets != nil {
dst.Targets = make(map[string]replicationStats, len(dst.Targets))
for k, v := range r.Targets {
dst.Targets[k] = v
}
}
return &dst
}
//msgp:encode ignore dataUsageEntryV2 dataUsageEntryV3 dataUsageEntryV4 dataUsageEntryV5 dataUsageEntryV6 //msgp:encode ignore dataUsageEntryV2 dataUsageEntryV3 dataUsageEntryV4 dataUsageEntryV5 dataUsageEntryV6
//msgp:marshal ignore dataUsageEntryV2 dataUsageEntryV3 dataUsageEntryV4 dataUsageEntryV5 dataUsageEntryV6 //msgp:marshal ignore dataUsageEntryV2 dataUsageEntryV3 dataUsageEntryV4 dataUsageEntryV5 dataUsageEntryV6
@@ -413,14 +446,11 @@ func (e dataUsageEntry) clone() dataUsageEntry {
e.Children = ch e.Children = ch
} }
if e.ReplicationStats != nil { if e.ReplicationStats != nil {
// Copy to new struct // Clone ReplicationStats
r := *e.ReplicationStats e.ReplicationStats = e.ReplicationStats.clone()
e.ReplicationStats = &r
} }
if e.AllTierStats != nil { if e.AllTierStats != nil {
ats := newAllTierStats() e.AllTierStats = e.AllTierStats.clone()
ats.merge(e.AllTierStats)
e.AllTierStats = ats
} }
return e return e
} }
@@ -873,7 +903,7 @@ func (d *dataUsageCache) merge(other dataUsageCache) {
} }
type objectIO interface { type objectIO interface {
GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (reader *GetObjectReader, err error) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions) (reader *GetObjectReader, err error)
PutObject(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) PutObject(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error)
} }
@@ -889,7 +919,7 @@ func (d *dataUsageCache) load(ctx context.Context, store objectIO, name string)
// Caches are read+written without locks, // Caches are read+written without locks,
retries := 0 retries := 0
for retries < 5 { for retries < 5 {
r, err := store.GetObjectNInfo(ctx, dataUsageBucket, name, nil, http.Header{}, noLock, 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:
+1 -1
View File
@@ -21,7 +21,7 @@ import (
"sort" "sort"
"time" "time"
"github.com/minio/madmin-go/v2" "github.com/minio/madmin-go/v3"
) )
// BucketTargetUsageInfo - bucket target usage info provides // BucketTargetUsageInfo - bucket target usage info provides
+1 -1
View File
@@ -81,7 +81,7 @@ func loadPrefixUsageFromBackend(ctx context.Context, objAPI ObjectLayer, bucket
for id, usageInfo := range cache.flattenChildrens(*root) { for id, usageInfo := range cache.flattenChildrens(*root) {
prefix := decodeDirObject(strings.TrimPrefix(id, bucket+slashSeparator)) prefix := decodeDirObject(strings.TrimPrefix(id, bucket+slashSeparator))
// decodeDirObject to avoid any __XL_DIR__ objects // decodeDirObject to avoid any __XLDIR__ objects
m[prefix] += uint64(usageInfo.Size) m[prefix] += uint64(usageInfo.Size)
} }
} }
+16 -16
View File
@@ -33,8 +33,8 @@ import (
"github.com/minio/minio/internal/disk" "github.com/minio/minio/internal/disk"
"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/minio/minio/internal/sync/errgroup"
xnet "github.com/minio/pkg/net" xnet "github.com/minio/pkg/net"
"github.com/minio/pkg/sync/errgroup"
"github.com/minio/pkg/wildcard" "github.com/minio/pkg/wildcard"
) )
@@ -80,7 +80,7 @@ type CacheStorageInfo struct {
// CacheObjectLayer implements primitives for cache object API layer. // CacheObjectLayer implements primitives for cache object API layer.
type CacheObjectLayer interface { type CacheObjectLayer interface {
// Object operations. // Object operations.
GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions) (gr *GetObjectReader, err error)
GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
DeleteObject(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) DeleteObject(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error)
DeleteObjects(ctx context.Context, bucket string, objects []ObjectToDelete, opts ObjectOptions) ([]DeletedObject, []error) DeleteObjects(ctx context.Context, bucket string, objects []ObjectToDelete, opts ObjectOptions) ([]DeletedObject, []error)
@@ -117,7 +117,7 @@ type cacheObjects struct {
// Cache stats // Cache stats
cacheStats *CacheStats cacheStats *CacheStats
InnerGetObjectNInfoFn func(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) InnerGetObjectNInfoFn func(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions) (gr *GetObjectReader, err error)
InnerGetObjectInfoFn func(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) InnerGetObjectInfoFn func(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
InnerDeleteObjectFn func(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) InnerDeleteObjectFn func(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
InnerPutObjectFn func(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) InnerPutObjectFn func(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error)
@@ -231,16 +231,16 @@ func (c *cacheObjects) incCacheStats(size int64) {
c.cacheStats.incBytesServed(size) c.cacheStats.incBytesServed(size)
} }
func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) { func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions) (gr *GetObjectReader, err error) {
if c.isCacheExclude(bucket, object) || c.skipCache() { if c.isCacheExclude(bucket, object) || c.skipCache() {
return c.InnerGetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts) return c.InnerGetObjectNInfoFn(ctx, bucket, object, rs, h, opts)
} }
var cc *cacheControl var cc *cacheControl
var cacheObjSize int64 var cacheObjSize int64
// fetch diskCache if object is currently cached or nearest available cache drive // fetch diskCache if object is currently cached or nearest available cache drive
dcache, err := c.getCacheToLoc(ctx, bucket, object) dcache, err := c.getCacheToLoc(ctx, bucket, object)
if err != nil { if err != nil {
return c.InnerGetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts) return c.InnerGetObjectNInfoFn(ctx, bucket, object, rs, h, opts)
} }
cacheReader, numCacheHits, cacheErr := dcache.Get(ctx, bucket, object, rs, h, opts) cacheReader, numCacheHits, cacheErr := dcache.Get(ctx, bucket, object, rs, h, opts)
@@ -269,7 +269,7 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
if cc != nil && cc.noStore { if cc != nil && cc.noStore {
cacheReader.Close() cacheReader.Close()
c.cacheStats.incMiss() c.cacheStats.incMiss()
bReader, err := c.InnerGetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts) bReader, err := c.InnerGetObjectNInfoFn(ctx, bucket, object, rs, h, opts)
bReader.ObjInfo.CacheLookupStatus = CacheHit bReader.ObjInfo.CacheLookupStatus = CacheHit
bReader.ObjInfo.CacheStatus = CacheMiss bReader.ObjInfo.CacheStatus = CacheMiss
return bReader, err return bReader, err
@@ -304,7 +304,7 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
cacheReader.Close() cacheReader.Close()
} }
c.cacheStats.incMiss() c.cacheStats.incMiss()
return c.InnerGetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts) return c.InnerGetObjectNInfoFn(ctx, bucket, object, rs, h, opts)
} }
// skip cache for objects with locks // skip cache for objects with locks
objRetention := objectlock.GetObjectRetentionMeta(objInfo.UserDefined) objRetention := objectlock.GetObjectRetentionMeta(objInfo.UserDefined)
@@ -314,7 +314,7 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
cacheReader.Close() cacheReader.Close()
} }
c.cacheStats.incMiss() c.cacheStats.incMiss()
return c.InnerGetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts) return c.InnerGetObjectNInfoFn(ctx, bucket, object, rs, h, opts)
} }
if cacheErr == nil { if cacheErr == nil {
// if ETag matches for stale cache entry, serve from cache // if ETag matches for stale cache entry, serve from cache
@@ -332,7 +332,7 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
// Reaching here implies cache miss // Reaching here implies cache miss
c.cacheStats.incMiss() c.cacheStats.incMiss()
bkReader, bkErr := c.InnerGetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts) bkReader, bkErr := c.InnerGetObjectNInfoFn(ctx, bucket, object, rs, h, opts)
if bkErr != nil { if bkErr != nil {
return bkReader, bkErr return bkReader, bkErr
@@ -359,7 +359,7 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
// if range caching is disabled, download entire object. // if range caching is disabled, download entire object.
rs = nil rs = nil
// fill cache in the background for range GET requests // fill cache in the background for range GET requests
bReader, bErr := c.InnerGetObjectNInfoFn(GlobalContext, bucket, object, rs, h, lockType, opts) bReader, bErr := c.InnerGetObjectNInfoFn(GlobalContext, bucket, object, rs, h, opts)
if bErr != nil { if bErr != nil {
return return
} }
@@ -713,7 +713,7 @@ func (c *cacheObjects) PutObject(ctx context.Context, bucket, object string, r *
if err == nil { if err == nil {
go func() { go func() {
// fill cache in the background // fill cache in the background
bReader, bErr := c.InnerGetObjectNInfoFn(GlobalContext, bucket, object, nil, http.Header{}, readLock, ObjectOptions{}) bReader, bErr := c.InnerGetObjectNInfoFn(GlobalContext, bucket, object, nil, http.Header{}, ObjectOptions{})
if bErr != nil { if bErr != nil {
return return
} }
@@ -857,8 +857,8 @@ func newServerCacheObjects(ctx context.Context, config cache.Config) (CacheObjec
InnerGetObjectInfoFn: func(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) { InnerGetObjectInfoFn: func(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
return newObjectLayerFn().GetObjectInfo(ctx, bucket, object, opts) return newObjectLayerFn().GetObjectInfo(ctx, bucket, object, opts)
}, },
InnerGetObjectNInfoFn: func(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) { InnerGetObjectNInfoFn: func(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions) (gr *GetObjectReader, err error) {
return newObjectLayerFn().GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts) return newObjectLayerFn().GetObjectNInfo(ctx, bucket, object, rs, h, opts)
}, },
InnerDeleteObjectFn: func(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) { InnerDeleteObjectFn: func(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
return newObjectLayerFn().DeleteObject(ctx, bucket, object, opts) return newObjectLayerFn().DeleteObject(ctx, bucket, object, opts)
@@ -1143,7 +1143,7 @@ func (c *cacheObjects) CopyObjectPart(ctx context.Context, srcBucket, srcObject,
End: startOffset + length, End: startOffset + length,
} }
// fill cache in the background // fill cache in the background
bReader, bErr := c.InnerGetObjectNInfoFn(GlobalContext, srcBucket, srcObject, rs, http.Header{}, readLock, ObjectOptions{}) bReader, bErr := c.InnerGetObjectNInfoFn(GlobalContext, srcBucket, srcObject, rs, http.Header{}, ObjectOptions{})
if bErr != nil { if bErr != nil {
return return
} }
@@ -1176,7 +1176,7 @@ func (c *cacheObjects) CompleteMultipartUpload(ctx context.Context, bucket, obje
_, err := dcache.CompleteMultipartUpload(bgContext(ctx), bucket, object, uploadID, uploadedParts, oi, opts) _, err := dcache.CompleteMultipartUpload(bgContext(ctx), bucket, object, uploadID, uploadedParts, oi, opts)
if err != nil { if err != nil {
// fill cache in the background // fill cache in the background
bReader, bErr := c.InnerGetObjectNInfoFn(GlobalContext, bucket, object, nil, http.Header{}, readLock, ObjectOptions{}) bReader, bErr := c.InnerGetObjectNInfoFn(GlobalContext, bucket, object, nil, http.Header{}, ObjectOptions{})
if bErr != nil { if bErr != nil {
return return
} }
+9 -8
View File
@@ -818,8 +818,8 @@ func getDecryptedETag(headers http.Header, objInfo ObjectInfo, copySource bool)
// As per AWS S3 Spec, ETag for SSE-C encrypted objects need not be MD5Sum of the data. // As per AWS S3 Spec, ETag for SSE-C encrypted objects need not be MD5Sum of the data.
// Since server side copy with same source and dest just replaces the ETag, we save // Since server side copy with same source and dest just replaces the ETag, we save
// encrypted content MD5Sum as ETag for both SSE-C and SSE-S3, we standardize the ETag // encrypted content MD5Sum as ETag for both SSE-C and SSE-KMS, we standardize the ETag
// encryption across SSE-C and SSE-S3, and only return last 32 bytes for SSE-C // encryption across SSE-C and SSE-KMS, and only return last 32 bytes for SSE-C
if (crypto.SSEC.IsEncrypted(objInfo.UserDefined) || crypto.S3KMS.IsEncrypted(objInfo.UserDefined)) && !copySource { if (crypto.SSEC.IsEncrypted(objInfo.UserDefined) || crypto.S3KMS.IsEncrypted(objInfo.UserDefined)) && !copySource {
return objInfo.ETag[len(objInfo.ETag)-32:] return objInfo.ETag[len(objInfo.ETag)-32:]
} }
@@ -828,15 +828,15 @@ func getDecryptedETag(headers http.Header, objInfo ObjectInfo, copySource bool)
if err != nil { if err != nil {
return objInfo.ETag return objInfo.ETag
} }
return tryDecryptETag(objectEncryptionKey, objInfo.ETag, false) return tryDecryptETag(objectEncryptionKey, objInfo.ETag, true)
} }
// helper to decrypt Etag given object encryption key and encrypted ETag // helper to decrypt Etag given object encryption key and encrypted ETag
func tryDecryptETag(key []byte, encryptedETag string, ssec bool) string { func tryDecryptETag(key []byte, encryptedETag string, sses3 bool) string {
// ETag for SSE-C encrypted objects need not be content MD5Sum.While encrypted // ETag for SSE-C or SSE-KMS encrypted objects need not be content MD5Sum.While encrypted
// md5sum is stored internally, return just the last 32 bytes of hex-encoded and // md5sum is stored internally, return just the last 32 bytes of hex-encoded and
// encrypted md5sum string for SSE-C // encrypted md5sum string for SSE-C
if ssec { if !sses3 {
return encryptedETag[len(encryptedETag)-32:] return encryptedETag[len(encryptedETag)-32:]
} }
var objectKey crypto.ObjectKey var objectKey crypto.ObjectKey
@@ -1124,7 +1124,8 @@ func (o *ObjectInfo) metadataEncryptFn(headers http.Header) (objectMetaEncryptFn
} }
// decryptChecksums will attempt to decode checksums and return it/them if set. // decryptChecksums will attempt to decode checksums and return it/them if set.
func (o *ObjectInfo) decryptChecksums() map[string]string { // if part > 0, and we have the checksum for the part that will be returned.
func (o *ObjectInfo) decryptChecksums(part int) map[string]string {
data := o.Checksum data := o.Checksum
if len(data) == 0 { if len(data) == 0 {
return nil return nil
@@ -1137,5 +1138,5 @@ func (o *ObjectInfo) decryptChecksums() map[string]string {
} }
data = decrypted data = decrypted
} }
return hash.ReadCheckSums(data) return hash.ReadCheckSums(data, part)
} }
+15 -13
View File
@@ -355,10 +355,13 @@ func createServerEndpoints(serverAddr string, args ...string) (
if err != nil { if err != nil {
return nil, -1, err return nil, -1, err
} }
endpointList, newSetupType, err := CreateEndpoints(serverAddr, false, setArgs...) endpointList, newSetupType, err := CreateEndpoints(serverAddr, setArgs...)
if err != nil { if err != nil {
return nil, -1, err return nil, -1, err
} }
for i := range endpointList {
endpointList[i].SetPool(0)
}
endpointServerPools = append(endpointServerPools, PoolEndpoints{ endpointServerPools = append(endpointServerPools, PoolEndpoints{
Legacy: true, Legacy: true,
SetCount: len(setArgs), SetCount: len(setArgs),
@@ -371,37 +374,36 @@ func createServerEndpoints(serverAddr string, args ...string) (
return endpointServerPools, setupType, nil return endpointServerPools, setupType, nil
} }
var foundPrevLocal bool var poolArgs [][][]string
for _, arg := range args { for _, arg := range args {
if !ellipses.HasEllipses(arg) && len(args) > 1 { if !ellipses.HasEllipses(arg) && len(args) > 1 {
// TODO: support SNSD deployments to be decommissioned in future // TODO: support SNSD deployments to be decommissioned in future
return nil, -1, fmt.Errorf("all args must have ellipses for pool expansion (%w) args: %s", errInvalidArgument, args) return nil, -1, fmt.Errorf("all args must have ellipses for pool expansion (%w) args: %s", errInvalidArgument, args)
} }
setArgs, err := GetAllSets(arg) setArgs, err := GetAllSets(arg)
if err != nil { if err != nil {
return nil, -1, err return nil, -1, err
} }
endpointList, gotSetupType, err := CreateEndpoints(serverAddr, foundPrevLocal, setArgs...) poolArgs = append(poolArgs, setArgs)
}
poolEndpoints, setupType, err := CreatePoolEndpoints(serverAddr, poolArgs...)
if err != nil { if err != nil {
return nil, -1, err return nil, -1, err
} }
for i, endpointList := range poolEndpoints {
if err = endpointServerPools.Add(PoolEndpoints{ if err = endpointServerPools.Add(PoolEndpoints{
SetCount: len(setArgs), SetCount: len(poolArgs[i]),
DrivesPerSet: len(setArgs[0]), DrivesPerSet: len(poolArgs[i][0]),
Endpoints: endpointList, Endpoints: endpointList,
CmdLine: arg,
Platform: fmt.Sprintf("OS: %s | Arch: %s", runtime.GOOS, runtime.GOARCH), Platform: fmt.Sprintf("OS: %s | Arch: %s", runtime.GOOS, runtime.GOARCH),
CmdLine: args[i],
}); err != nil { }); err != nil {
return nil, -1, err return nil, -1, err
} }
foundPrevLocal = endpointList.atleastOneEndpointLocal()
if setupType == UnknownSetupType {
setupType = gotSetupType
}
if setupType == ErasureSetupType && gotSetupType == DistErasureSetupType {
setupType = DistErasureSetupType
}
} }
return endpointServerPools, setupType, nil return endpointServerPools, setupType, nil
+400 -14
View File
@@ -38,6 +38,7 @@ import (
"github.com/minio/minio/internal/mountinfo" "github.com/minio/minio/internal/mountinfo"
"github.com/minio/pkg/env" "github.com/minio/pkg/env"
xnet "github.com/minio/pkg/net" xnet "github.com/minio/pkg/net"
"golang.org/x/exp/slices"
) )
// EndpointType - enum for endpoint type. // EndpointType - enum for endpoint type.
@@ -58,9 +59,17 @@ type ProxyEndpoint struct {
Transport http.RoundTripper Transport http.RoundTripper
} }
// Node holds information about a node in this cluster
type Node struct {
*url.URL
Pools []int
IsLocal bool
}
// Endpoint - any type of endpoint. // Endpoint - any type of endpoint.
type Endpoint struct { type Endpoint struct {
*url.URL *url.URL
Pool int
IsLocal bool IsLocal bool
} }
@@ -97,6 +106,11 @@ func (endpoint *Endpoint) UpdateIsLocal() (err error) {
return nil return nil
} }
// SetPool sets a specific pool number to this node
func (endpoint *Endpoint) SetPool(i int) {
endpoint.Pool = i
}
// NewEndpoint - returns new endpoint based on given arguments. // NewEndpoint - returns new endpoint based on given arguments.
func NewEndpoint(arg string) (ep Endpoint, e error) { func NewEndpoint(arg string) (ep Endpoint, e error) {
// isEmptyPath - check whether given path is not empty. // isEmptyPath - check whether given path is not empty.
@@ -209,6 +223,35 @@ type PoolEndpoints struct {
// EndpointServerPools - list of list of endpoints // EndpointServerPools - list of list of endpoints
type EndpointServerPools []PoolEndpoints type EndpointServerPools []PoolEndpoints
// GetNodes returns a sorted list of nodes in this cluster
func (l EndpointServerPools) GetNodes() (nodes []Node) {
nodesMap := make(map[string]Node)
for _, pool := range l {
for _, ep := range pool.Endpoints {
node, ok := nodesMap[ep.Host]
if !ok {
node.IsLocal = ep.IsLocal
node.URL = &url.URL{
Scheme: ep.Scheme,
Host: ep.Host,
}
}
if !slices.Contains(node.Pools, ep.Pool) {
node.Pools = append(node.Pools, ep.Pool)
}
nodesMap[ep.Host] = node
}
}
nodes = make([]Node, 0, len(nodesMap))
for _, v := range nodesMap {
nodes = append(nodes, v)
}
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].Host < nodes[j].Host
})
return
}
// GetPoolIdx return pool index // GetPoolIdx return pool index
func (l EndpointServerPools) GetPoolIdx(pool string) int { func (l EndpointServerPools) GetPoolIdx(pool string) int {
for id, ep := range globalEndpoints { for id, ep := range globalEndpoints {
@@ -409,15 +452,6 @@ func (endpoints Endpoints) GetAllStrings() (all []string) {
return return
} }
func (endpoints Endpoints) atleastOneEndpointLocal() bool {
for _, endpoint := range endpoints {
if endpoint.IsLocal {
return true
}
}
return false
}
func hostResolveToLocalhost(endpoint Endpoint) bool { func hostResolveToLocalhost(endpoint Endpoint) bool {
hostIPs, err := getHostIP(endpoint.Hostname()) hostIPs, err := getHostIP(endpoint.Hostname())
if err != nil { if err != nil {
@@ -440,7 +474,7 @@ func hostResolveToLocalhost(endpoint Endpoint) bool {
} }
// UpdateIsLocal - resolves the host and discovers the local host. // UpdateIsLocal - resolves the host and discovers the local host.
func (endpoints Endpoints) UpdateIsLocal(foundPrevLocal bool) error { func (endpoints Endpoints) UpdateIsLocal() error {
orchestrated := IsDocker() || IsKubernetes() orchestrated := IsDocker() || IsKubernetes()
var epsResolved int var epsResolved int
@@ -448,13 +482,14 @@ func (endpoints Endpoints) UpdateIsLocal(foundPrevLocal bool) error {
resolvedList := make([]bool, len(endpoints)) resolvedList := make([]bool, len(endpoints))
// Mark the starting time // Mark the starting time
startTime := time.Now() startTime := time.Now()
keepAliveTicker := time.NewTicker(10 * time.Millisecond) keepAliveTicker := time.NewTicker(500 * time.Millisecond)
defer keepAliveTicker.Stop() defer keepAliveTicker.Stop()
for { for {
// Break if the local endpoint is found already Or all the endpoints are resolved. // Break if the local endpoint is found already Or all the endpoints are resolved.
if foundLocal || (epsResolved == len(endpoints)) { if foundLocal || (epsResolved == len(endpoints)) {
break break
} }
// Retry infinitely on Kubernetes and Docker swarm. // Retry infinitely on Kubernetes and Docker swarm.
// This is needed as the remote hosts are sometime // This is needed as the remote hosts are sometime
// not available immediately. // not available immediately.
@@ -610,8 +645,359 @@ func checkCrossDeviceMounts(endpoints Endpoints) (err error) {
return mountinfo.CheckCrossDevice(absPaths) return mountinfo.CheckCrossDevice(absPaths)
} }
// PoolEndpointList is a temporary type to holds the list of endpoints
type PoolEndpointList []Endpoints
// UpdateIsLocal - resolves all hosts and discovers which are local
func (p PoolEndpointList) UpdateIsLocal() error {
orchestrated := IsDocker() || IsKubernetes()
var epsResolved int
var epCount int
for _, endpoints := range p {
epCount += len(endpoints)
}
var foundLocal bool
resolvedList := make(map[Endpoint]bool)
// Mark the starting time
startTime := time.Now()
keepAliveTicker := time.NewTicker(1 * time.Second)
defer keepAliveTicker.Stop()
for {
// Break if the local endpoint is found already Or all the endpoints are resolved.
if foundLocal || (epsResolved == epCount) {
break
}
// Retry infinitely on Kubernetes and Docker swarm.
// This is needed as the remote hosts are sometime
// not available immediately.
select {
case <-globalOSSignalCh:
return fmt.Errorf("The endpoint resolution got interrupted")
default:
for i, endpoints := range p {
for j, endpoint := range endpoints {
if resolvedList[endpoint] {
// Continue if host is already resolved.
continue
}
// Log the message to console about the host resolving
reqInfo := (&logger.ReqInfo{}).AppendTags(
"host",
endpoint.Hostname(),
)
if orchestrated && hostResolveToLocalhost(endpoint) {
// time elapsed
timeElapsed := time.Since(startTime)
// log error only if more than a second has elapsed
if timeElapsed > time.Second {
reqInfo.AppendTags("elapsedTime",
humanize.RelTime(startTime,
startTime.Add(timeElapsed),
"elapsed",
"",
))
ctx := logger.SetReqInfo(GlobalContext,
reqInfo)
logger.LogOnceIf(ctx, fmt.Errorf("%s resolves to localhost in a containerized deployment, waiting for it to resolve to a valid IP",
endpoint.Hostname()), endpoint.Hostname(), logger.Application)
}
continue
}
// return err if not Docker or Kubernetes
// We use IsDocker() to check for Docker environment
// We use IsKubernetes() to check for Kubernetes environment
isLocal, err := isLocalHost(endpoint.Hostname(),
endpoint.Port(),
globalMinioPort,
)
if err != nil && !orchestrated {
return err
}
if err != nil {
// time elapsed
timeElapsed := time.Since(startTime)
// log error only if more than a second has elapsed
if timeElapsed > time.Second {
reqInfo.AppendTags("elapsedTime",
humanize.RelTime(startTime,
startTime.Add(timeElapsed),
"elapsed",
"",
))
ctx := logger.SetReqInfo(GlobalContext,
reqInfo)
logger.LogOnceIf(ctx, err, endpoint.Hostname(), logger.Application)
}
} else {
resolvedList[endpoint] = true
endpoint.IsLocal = isLocal
epsResolved++
if !foundLocal {
foundLocal = isLocal
}
endpoints[j] = endpoint
}
}
p[i] = endpoints
// Wait for the tick, if the there exist a local endpoint in discovery.
// Non docker/kubernetes environment we do not need to wait.
if !foundLocal && orchestrated {
<-keepAliveTicker.C
}
}
}
}
// On Kubernetes/Docker setups DNS resolves inappropriately sometimes
// where there are situations same endpoints with multiple disks
// come online indicating either one of them is local and some
// of them are not local. This situation can never happen and
// its only a possibility in orchestrated deployments with dynamic
// DNS. Following code ensures that we treat if one of the endpoint
// says its local for a given host - it is true for all endpoints
// for the same host. Following code ensures that this assumption
// is true and it works in all scenarios and it is safe to assume
// for a given host.
for i, endpoints := range p {
endpointLocalMap := make(map[string]bool)
for _, ep := range endpoints {
if ep.IsLocal {
endpointLocalMap[ep.Host] = ep.IsLocal
}
}
for i := range endpoints {
endpoints[i].IsLocal = endpointLocalMap[endpoints[i].Host]
}
p[i] = endpoints
}
return nil
}
// CreatePoolEndpoints creates a list of endpoints per pool, resolves their relevant hostnames and
// discovers those are local or remote.
func CreatePoolEndpoints(serverAddr string, poolArgs ...[][]string) ([]Endpoints, SetupType, error) {
var setupType SetupType
// Check whether serverAddr is valid for this host.
if err := CheckLocalServerAddr(serverAddr); err != nil {
return nil, setupType, err
}
_, serverAddrPort := mustSplitHostPort(serverAddr)
poolEndpoints := make(PoolEndpointList, len(poolArgs))
// For single arg, return single drive EC setup.
if len(poolArgs) == 1 && len(poolArgs[0]) == 1 && len(poolArgs[0][0]) == 1 && len(poolArgs[0][0][0]) == 1 {
endpoint, err := NewEndpoint(poolArgs[0][0][0])
if err != nil {
return nil, setupType, err
}
if err := endpoint.UpdateIsLocal(); err != nil {
return nil, setupType, err
}
if endpoint.Type() != PathEndpointType {
return nil, setupType, config.ErrInvalidEndpoint(nil).Msg("use path style endpoint for single node setup")
}
endpoint.SetPool(0)
var endpoints Endpoints
endpoints = append(endpoints, endpoint)
setupType = ErasureSDSetupType
poolEndpoints[0] = endpoints
// Check for cross device mounts if any.
if err = checkCrossDeviceMounts(endpoints); err != nil {
return nil, setupType, config.ErrInvalidEndpoint(nil).Msg(err.Error())
}
return poolEndpoints, setupType, nil
}
for poolIdx, args := range poolArgs {
var endpoints Endpoints
for _, iargs := range args {
// Convert args to endpoints
eps, err := NewEndpoints(iargs...)
if err != nil {
return nil, setupType, config.ErrInvalidErasureEndpoints(nil).Msg(err.Error())
}
// Check for cross device mounts if any.
if err = checkCrossDeviceMounts(eps); err != nil {
return nil, setupType, config.ErrInvalidErasureEndpoints(nil).Msg(err.Error())
}
for i := range eps {
eps[i].SetPool(poolIdx)
}
endpoints = append(endpoints, eps...)
}
if len(endpoints) == 0 {
return nil, setupType, config.ErrInvalidErasureEndpoints(nil).Msg("invalid number of endpoints")
}
poolEndpoints[poolIdx] = endpoints
}
for _, endpoints := range poolEndpoints {
// Return Erasure setup when all endpoints are path style.
if endpoints[0].Type() == PathEndpointType {
setupType = ErasureSetupType
}
if endpoints[0].Type() == URLEndpointType && setupType != DistErasureSetupType {
setupType = DistErasureSetupType
}
}
if setupType == ErasureSetupType {
return poolEndpoints, setupType, nil
}
if err := poolEndpoints.UpdateIsLocal(); err != nil {
return nil, setupType, config.ErrInvalidErasureEndpoints(nil).Msg(err.Error())
}
uniqueArgs := set.NewStringSet()
for i, endpoints := range poolEndpoints {
// Here all endpoints are URL style.
endpointPathSet := set.NewStringSet()
localEndpointCount := 0
localServerHostSet := set.NewStringSet()
localPortSet := set.NewStringSet()
for _, endpoint := range endpoints {
endpointPathSet.Add(endpoint.Path)
if endpoint.IsLocal {
localServerHostSet.Add(endpoint.Hostname())
_, port, err := net.SplitHostPort(endpoint.Host)
if err != nil {
port = serverAddrPort
}
localPortSet.Add(port)
localEndpointCount++
}
}
orchestrated := IsKubernetes() || IsDocker()
if !orchestrated {
// Check whether same path is not used in endpoints of a host on different port.
// Only verify this on baremetal setups, DNS is not available in orchestrated
// environments so we can't do much here.
{
pathIPMap := make(map[string]set.StringSet)
hostIPCache := make(map[string]set.StringSet)
for _, endpoint := range endpoints {
host := endpoint.Hostname()
hostIPSet, ok := hostIPCache[host]
if !ok {
var err error
hostIPSet, err = getHostIP(host)
if err != nil {
return nil, setupType, config.ErrInvalidErasureEndpoints(nil).Msg(fmt.Sprintf("host '%s' cannot resolve: %s", host, err))
}
hostIPCache[host] = hostIPSet
}
if IPSet, ok := pathIPMap[endpoint.Path]; ok {
if !IPSet.Intersection(hostIPSet).IsEmpty() {
return nil, setupType,
config.ErrInvalidErasureEndpoints(nil).Msg(fmt.Sprintf("same path '%s' can not be served by different port on same address", endpoint.Path))
}
pathIPMap[endpoint.Path] = IPSet.Union(hostIPSet)
} else {
pathIPMap[endpoint.Path] = hostIPSet
}
}
}
}
// Check whether same path is used for more than 1 local endpoints.
{
localPathSet := set.CreateStringSet()
for _, endpoint := range endpoints {
if !endpoint.IsLocal {
continue
}
if localPathSet.Contains(endpoint.Path) {
return nil, setupType,
config.ErrInvalidErasureEndpoints(nil).Msg(fmt.Sprintf("path '%s' cannot be served by different address on same server", endpoint.Path))
}
localPathSet.Add(endpoint.Path)
}
}
// Add missing port in all endpoints.
for i := range endpoints {
_, port, err := net.SplitHostPort(endpoints[i].Host)
if err != nil {
endpoints[i].Host = net.JoinHostPort(endpoints[i].Host, serverAddrPort)
} else if endpoints[i].IsLocal && serverAddrPort != port {
// If endpoint is local, but port is different than serverAddrPort, then make it as remote.
endpoints[i].IsLocal = false
}
}
// All endpoints are pointing to local host
if len(endpoints) == localEndpointCount {
// If all endpoints have same port number, Just treat it as local erasure setup
// using URL style endpoints.
if len(localPortSet) == 1 {
if len(localServerHostSet) > 1 {
return nil, setupType,
config.ErrInvalidErasureEndpoints(nil).Msg("all local endpoints should not have different hostnames/ips")
}
}
// Even though all endpoints are local, but those endpoints use different ports.
// This means it is DistErasure setup.
}
poolEndpoints[i] = endpoints
for _, endpoint := range endpoints {
uniqueArgs.Add(endpoint.Host)
}
poolEndpoints[i] = endpoints
}
publicIPs := env.Get(config.EnvPublicIPs, "")
if len(publicIPs) == 0 {
updateDomainIPs(uniqueArgs)
}
for _, endpoints := range poolEndpoints {
// Return Erasure setup when all endpoints are path style.
if endpoints[0].Type() == PathEndpointType {
setupType = ErasureSetupType
}
if endpoints[0].Type() == URLEndpointType && setupType != DistErasureSetupType {
setupType = DistErasureSetupType
}
}
return poolEndpoints, setupType, nil
}
// CreateEndpoints - validates and creates new endpoints for given args. // CreateEndpoints - validates and creates new endpoints for given args.
func CreateEndpoints(serverAddr string, foundLocal bool, args ...[]string) (Endpoints, SetupType, error) { func CreateEndpoints(serverAddr string, args ...[]string) (Endpoints, SetupType, error) {
var endpoints Endpoints var endpoints Endpoints
var setupType SetupType var setupType SetupType
var err error var err error
@@ -623,7 +1009,7 @@ func CreateEndpoints(serverAddr string, foundLocal bool, args ...[]string) (Endp
_, serverAddrPort := mustSplitHostPort(serverAddr) _, serverAddrPort := mustSplitHostPort(serverAddr)
// For single arg, return FS setup. // For single arg, return single drive setup.
if len(args) == 1 && len(args[0]) == 1 { if len(args) == 1 && len(args[0]) == 1 {
var endpoint Endpoint var endpoint Endpoint
endpoint, err = NewEndpoint(args[0][0]) endpoint, err = NewEndpoint(args[0][0])
@@ -672,7 +1058,7 @@ func CreateEndpoints(serverAddr string, foundLocal bool, args ...[]string) (Endp
return endpoints, setupType, nil return endpoints, setupType, nil
} }
if err = endpoints.UpdateIsLocal(foundLocal); err != nil { if err = endpoints.UpdateIsLocal(); err != nil {
return endpoints, setupType, config.ErrInvalidErasureEndpoints(nil).Msg(err.Error()) return endpoints, setupType, config.ErrInvalidErasureEndpoints(nil).Msg(err.Error())
} }
+3 -3
View File
@@ -314,7 +314,7 @@ func TestCreateEndpoints(t *testing.T) {
for _, testCase := range testCases { for _, testCase := range testCases {
testCase := testCase testCase := testCase
t.Run("", func(t *testing.T) { t.Run("", func(t *testing.T) {
endpoints, setupType, err := CreateEndpoints(testCase.serverAddr, false, testCase.args...) endpoints, setupType, err := CreateEndpoints(testCase.serverAddr, testCase.args...)
if err == nil && testCase.expectedErr != nil { if err == nil && testCase.expectedErr != nil {
t.Errorf("error: expected = %v, got = <nil>", testCase.expectedErr) t.Errorf("error: expected = %v, got = <nil>", testCase.expectedErr)
} }
@@ -374,7 +374,7 @@ func TestGetLocalPeer(t *testing.T) {
for i, testCase := range testCases { for i, testCase := range testCases {
zendpoints := mustGetPoolEndpoints(testCase.endpointArgs...) zendpoints := mustGetPoolEndpoints(testCase.endpointArgs...)
if !zendpoints[0].Endpoints[0].IsLocal { if !zendpoints[0].Endpoints[0].IsLocal {
if err := zendpoints[0].Endpoints.UpdateIsLocal(false); err != nil { if err := zendpoints[0].Endpoints.UpdateIsLocal(); err != nil {
t.Fatalf("error: expected = <nil>, got = %v", err) t.Fatalf("error: expected = <nil>, got = %v", err)
} }
} }
@@ -407,7 +407,7 @@ func TestGetRemotePeers(t *testing.T) {
for _, testCase := range testCases { for _, testCase := range testCases {
zendpoints := mustGetPoolEndpoints(testCase.endpointArgs...) zendpoints := mustGetPoolEndpoints(testCase.endpointArgs...)
if !zendpoints[0].Endpoints[0].IsLocal { if !zendpoints[0].Endpoints[0].IsLocal {
if err := zendpoints[0].Endpoints.UpdateIsLocal(false); err != nil { if err := zendpoints[0].Endpoints.UpdateIsLocal(); err != nil {
t.Errorf("error: expected = <nil>, got = %v", err) t.Errorf("error: expected = <nil>, got = %v", err)
} }
} }

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