Compare commits

..

178 Commits

Author SHA1 Message Date
爱折腾的小竹同学 f2619d1f62 Fix description error in README (#21099)
There is prefix in json, but not in the equivalent command line. Although the role of prefix has been explained in the previous example, I think it should be supplemented.
2025-04-03 07:56:28 -07:00
Harshavardhana 8c70975283 make sure to validate signature unsigned trailer stream (#21103)
This is a security incident fix, it would seem like since
the implementation of unsigned payload trailer on PUTs,
we do not validate the signature of the incoming request.

The signature can be invalid and is totally being ignored,
this in-turn allows any arbitrary secret to upload objects
given the user has "WRITE" permissions on the bucket, since
acces-key is a public information in general exposes these
potential users with WRITE on the bucket to be used by any
arbitrary client to make a fake request to MinIO the signature
under Authorization: header is totally ignored.

A test has been added to cover this scenario and fail
appropriately.
2025-04-03 07:55:52 -07:00
Krishnan Parthasarathi 01447d2438 Fix evaluation of NewerNoncurrentVersions (#21096)
- Move VersionPurgeStatus into replication package
- ilm: Evaluate policy w/ obj retention/replication
- lifecycle: Use Evaluator to enforce ILM in scanner
- Unit tests covering ILM, replication and retention
- Simplify NewEvaluator constructor
2025-04-02 23:45:06 -07:00
Shubhendu 07f31e574c Try reconnect IAM systems if failed initially (#20333)
Fixes: https://github.com/minio/minio/issues/20118

Signed-off-by: Shubhendu Ram Tripathi <shubhendu@minio.io>
2025-04-02 10:29:33 -07:00
iamsagar99 8d223e07fb Fix: Change TTFB metric type to histogram (#20999) 2025-04-01 22:48:58 -07:00
Harshavardhana 4041a8727c start publishing latest-cicd images 2025-04-01 20:53:54 -07:00
Klaus Post 5f243fde9a Fix anonymous unsigned trailing headers (#21095)
Do not fail on anonymous requests with trailing headers.

Fixes #21005

With modified minio-go (will send PR):

```
<DEBUG> PUT /tbb/mc.exe HTTP/1.1
Host: 127.0.0.1:9001
User-Agent: MinIO (windows; amd64) minio-go/v7.0.90 mc/DEVELOPMENT.GOGET
Content-Length: 44301288
Accept-Encoding: zstd,gzip
Content-Encoding: aws-chunked
Content-Type: application/x-msdownload
X-Amz-Content-Sha256: STREAMING-UNSIGNED-PAYLOAD-TRAILER
X-Amz-Date: 20250401T150402Z
X-Amz-Decoded-Content-Length: 44295168
X-Amz-Trailer: x-amz-checksum-crc32

mc: <DEBUG> HTTP/1.1 200 OK
Content-Length: 0
Accept-Ranges: bytes
Date: Tue, 01 Apr 2025 15:04:02 GMT
Etag: "46273a30f232dc015ead1c0da8925c98"
Server: MinIO
Strict-Transport-Security: max-age=31536000; includeSubDomains
Vary: Origin
Vary: Accept-Encoding
X-Amz-Checksum-Crc32: wElc/A==
X-Amz-Id-2: 7987905dee74cdeb212432486a178e511309594cee7cb75f892cd53e35f09ea4
X-Amz-Request-Id: 18323A0F322B41C8
X-Content-Type-Options: nosniff
X-Ratelimit-Limit: 2478
X-Ratelimit-Remaining: 2478
X-Xss-Protection: 1; mode=block
```

Tested on multipart uploads as well.
2025-04-01 11:23:27 -07:00
Burkov Egor a0e3f1cc18 internal: add handling of KVS config parse (#21079) 2025-04-01 08:28:26 -07:00
Name b1bc641105 chore(all): replace map key deletion loop with clear() (#21082) 2025-04-01 08:28:06 -07:00
jiuker e0c8738230 fix: token is invalid for admin heal when minio is distErasure on windows (#21092) 2025-04-01 08:21:33 -07:00
alingse 9aa24b1920 fix call toAPIErrorCode with a nil value error after check another err (#21083)
if check lerr != nil and return a toAPIErrorCode(nil)

it should  return toAPIErrorCode(lerr)
2025-03-31 13:31:15 -07:00
Taran Pelkey 53d40e41bc Add new API endpoint to revoke STS tokens (#21072) 2025-03-31 11:51:24 -07:00
Taran Pelkey e88d494775 Migrate golanglint-ci config to V2 (#21081) 2025-03-29 17:56:02 -07:00
dependabot[bot] b67f0cf721 build(deps): bump github.com/golang-jwt/jwt/v4 from 4.5.1 to 4.5.2 (#21056)
Bumps [github.com/golang-jwt/jwt/v4](https://github.com/golang-jwt/jwt) from 4.5.1 to 4.5.2.
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md)
- [Commits](https://github.com/golang-jwt/jwt/compare/v4.5.1...v4.5.2)

---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-23 08:18:21 -07:00
Alexander Kalaj 46922c71b7 Updating Prom queries to include tilde needed to work (#21054) 2025-03-22 08:22:29 -07:00
dependabot[bot] 670edb4fcf build(deps): bump github.com/golang-jwt/jwt/v5 from 5.2.1 to 5.2.2 (#21055)
Bumps [github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) from 5.2.1 to 5.2.2.
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md)
- [Commits](https://github.com/golang-jwt/jwt/compare/v5.2.1...v5.2.2)

---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-22 08:21:04 -07:00
itsJohnySmith 42d4ab2a0a fix(templates): replace dash with underscore (#19566) 2025-03-14 13:01:11 -07:00
Harshavardhana 5e2eb372bf update dependencies for CVE fix x/net 2025-03-12 22:29:51 -07:00
Minio Trusted cccb37a5ac Update yaml files to latest version RELEASE.2025-03-12T18-04-18Z 2025-03-12 18:22:31 +00:00
Anis Eleuch dbf31af6cb decom: Ignore not found buckets (#509) (#21023)
When decommissioning is started, the list of buckets to decommission is
calculated, however, a bucket can be removed before decommissioning reaches
it. This will cause an infinite loop of listing error complaining about
the non-existence of the bucket. This commit will ignore
errVolumeNotFound to skip the not found bucket.
2025-03-12 11:04:18 -07:00
Klaus Post 93e40c3ab4 Disable unstable test (#20996)
Disable unstable test in vendored package. Only used for s3 select.
2025-03-12 10:26:50 -07:00
Aditya Manthramurthy 8aa0e9ff7c Update ssh and jws libs for fixed CVEs (#21017)
- https://pkg.go.dev/vuln/GO-2025-3488
- https://pkg.go.dev/vuln/GO-2025-3487
2025-03-12 08:16:19 -07:00
Aditya Manthramurthy bbd6f18afb Update typos config (#21018) 2025-03-11 08:44:54 -07:00
Harshavardhana 2a3acc4f24 drive heal if we have enough success, do not error setList() (#516) 2025-03-10 19:57:24 -07:00
Klaus Post 11507d46da Enforce a bucket limit of 100 to v2 metrics calls (#20761)
Enforce a bucket count limit on metrics for v2 calls.

If people hit this limit, they should move to v3, as certain calls explode with high bucket count.

Reviewers: This *should* only affect v2 calls, but the complexity is overwhelming.
2025-02-28 11:33:08 -08:00
Minio Trusted f9c62dea55 Update yaml files to latest version RELEASE.2025-02-28T09-55-16Z 2025-02-28 18:16:28 +00:00
Klaus Post 8c2c92f7af Fix healing probability for skipped folders (#20988)
We must update the heal probability when selectively skipping folders.
2025-02-28 01:55:16 -08:00
Aditya Manthramurthy 4c71f1b4ec fix: SFTP auth bypass with no pub key in LDAP (#20986)
If a user attempts to authenticate with a key but does not have an
sshpubkey attribute in LDAP, the server allows the connection, which 
means the server trusted the key without reason. This is now fixed, 
and a test has been added for validation.
2025-02-27 10:43:32 -08:00
Poorna 6cd8a372cb replication: set checksum type correctly (#20985)
Fixes: #20978
2025-02-26 15:17:28 -08:00
Anis Eleuch 953a3e2bbd check for errors on bitrotWriter Close() (#20982) 2025-02-26 11:26:13 -08:00
Mark Theunissen 7cc0c69228 Allow disabling of all X-Forwarded-For header processing (#20977) 2025-02-26 11:25:49 -08:00
Anis Eleuch f129fd48f2 Update golang.org/x/crypto to address govulncheck complaint (#20983) 2025-02-26 08:15:09 -08:00
TripleChecker bc4008ced4 Fix typos (#20970) 2025-02-26 01:25:50 -08:00
dependabot[bot] 526053339b build(deps): bump github.com/go-jose/go-jose/v4 from 4.0.4 to 4.0.5 (#20976)
Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.0.4 to 4.0.5.
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Changelog](https://github.com/go-jose/go-jose/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-jose/go-jose/compare/v4.0.4...v4.0.5)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-26 01:25:19 -08:00
Taran Pelkey 62a35b3e77 Update SRSvcAccCreate with new type (#20974) 2025-02-24 17:43:59 -08:00
Taran Pelkey 39df134204 Fix importIAM issue with importing implied policies (#20956) 2025-02-19 10:10:53 -08:00
Minio Trusted ad4cbce22d Update yaml files to latest version RELEASE.2025-02-18T16-25-55Z 2025-02-18 20:59:14 +00:00
Klaus Post 90f5e1e5f6 tests: Do not allow forced type asserts (#20905) 2025-02-18 08:25:55 -08:00
Klaus Post aeabac9181 Test checksum types for invalid combinations (#20953) 2025-02-18 08:24:01 -08:00
Klaus Post b312f13473 Extract all files from encrypted stream with inspect (#20937)
Allow multiple private keys and extract all files from streams.

Place files in the folder with `.enc` removed.

Do basic checks so streams cannot traverse outside of the folder.
2025-02-17 09:09:42 -08:00
Rodrigo dos Santos Felix 727a803bc0 fix(docs): update mc admin trace link to MinIO official docs (#20943) 2025-02-16 20:52:27 -08:00
Name d0e443172d chore: remove unused and incorrect IsEmpty method from TargetIDSet (#20939) 2025-02-16 08:43:15 -08:00
Jeeva Kandasamy 60446e7ac0 ftp: Enable trailing headers, just like sftp (#20938) 2025-02-15 02:32:09 -08:00
Harshavardhana b8544266e5 fix: typo in queuestore.go 2025-02-15 02:31:50 -08:00
Ramon de Klein 437dd4e32a Fix missing authorization check for PutObjectRetentionHandler (#20929) 2025-02-12 08:08:13 -08:00
Cesar N. 447054b841 Update console to 1.7.6 (#20925) 2025-02-11 15:43:04 -08:00
Harshavardhana 9bf43e54cd allow ARCH specific hotfixes 2025-02-11 14:33:31 -08:00
Manuel Reis 60f8423157 Quick patch for Snowball AutoExtract: #20883 (#20885)
* Checking allowance on empty prefix or Snowball-prefix - fixes #20883
* Check the policy for each object during Snowball auto-extraction
2025-02-10 15:52:59 -08:00
Klaus Post 4355ea3c3f (s)ftp: Enable trailing headers for upload (#20914)
Since we always "connect" to minio, it is fine.
2025-02-10 08:35:49 -08:00
Klaus Post e30f1ad7bd Fix nil pointer deref in PeerPolicyMappingHandler (#20913)
The following lines will attempt to de-reference the nil value. Instead just return the error at once.
2025-02-10 08:35:13 -08:00
Minio Trusted f00c8c4cce Update yaml files to latest version RELEASE.2025-02-07T23-21-09Z 2025-02-08 21:03:40 +00:00
Andreas Auernhammer 703f51164d kms: add MINIO_KMS_REPLICATE_KEYID option (#20909)
This commit adds the `MINIO_KMS_REPLICATE_KEYID` env. variable.
By default - if not specified or not set to `off` - MinIO will
replicate the KMS key ID of an object.

If `MINIO_KMS_REPLICATE_KEYID=off`, MinIO does not include the
object's KMS Key ID when replicating an object. However, it always
sets the SSE-KMS encryption header. This ensures that the object
gets encrypted using SSE-KMS. The target site chooses the KMS key
ID that gets used based on the site and bucket config.

Signed-off-by: Andreas Auernhammer <github@aead.dev>
2025-02-07 15:21:09 -08:00
Klaus Post b8dde47d4e fix: multipart replication with single part objects (#20895)
x-amz-checksum-algorithm is not set, causing all multipart single-part objects
to fail to replicate going via sftp/FTP uploads.
2025-02-05 15:06:02 -08:00
Andreas Auernhammer 7fa3e39f85 sts: allow client-provided intermediate CAs (#20896)
This commit allows clients to provide a set of intermediate CA
certificates (up to `MaxIntermediateCAs`) that the server will
use as intermediate CAs when verifying the trust chain from the
client leaf certificate up to one trusted root CA.

This is required if the client leaf certificate is not issued by
a trusted CA directly but by an intermediate CA. Without this commit,
MinIO rejects such certificates.

Signed-off-by: Andreas Auernhammer <github@aead.dev>
2025-02-04 16:29:41 -08:00
Poorna 4df7a3aa8f fix: site replication of bucket deletion sync (#352)
Bucket deletion timestamp was not being passed back
in GetBucketInfo, which is needed to decide on the bucket
creation/deletion
2025-02-04 00:36:03 -08:00
Poorna 64a8f2e554 replication: default tag timestamps in CopyObject call (#20891)
If object is uploaded with tags, the internal tagging-timestamp tracked
for replication will be missing. Default to ModTime in such cases to
allow tags to be synced correctly.

Also fixing a regression in fetching tags and tag comparison
2025-02-04 00:35:55 -08:00
Minio Trusted f4fd4ea66d Update yaml files to latest version RELEASE.2025-02-03T21-03-04Z 2025-02-04 06:55:11 +00:00
Anis Eleuch 712fe1a8df fix: proxy requests to honor global transport
* fix: proxy requests to honor global transport 
Load the globalProxyEndpoint properly

also, currently, the proxy requests will fail silently for batch cancel
even if the proxy fails; instead,d properly send the corresponding error back
for such proxy failures if opted

* pass the transport to the GetProxyEnpoints function

---------

Co-authored-by: Praveen raj Mani <praveen@minio.io>
2025-02-03 22:03:04 +01:00
Klaus Post 4a319bedc9 Redact sensitive fields from DescribeBatchJob (#20881)
Redacts the following if set:

* replicate/credentials/secretKey
* replicate/credentials/sessionToken
* expire/notify/token
2025-02-03 08:56:26 -08:00
Klaus Post bdb3db6dad Add lock overload protection (#20876)
Reject new lock requests immediately when 1000 goroutines are queued 
for the local lock mutex.

We do not reject unlocking, refreshing, or maintenance; they add to the count.

The limit is set to allow for bursty behavior but prevent requests from 
overloading the server completely.
2025-01-31 11:54:34 -08:00
Klaus Post abb385af41 Check for valid checksum (#20878)
Add a few safety measures for checksums.
2025-01-28 16:59:23 -08:00
Harshavardhana 4ee62606e4 update govulncheck 2025-01-28 11:11:08 -08:00
Anis Eleuch 079d64c801 DeleteObjects: Send delete to all pools (#172) (#20821)
Currently, DeleteObjects() tries to find the object's pool before
sending a delete request. This only works well when an object has
multiple versions in different pools since looking for the pool does
not consider the version-id. When an S3 client wants to
remove a version-id that exists in pool 2, the delete request will be
directed to pool one because it has another version of the same object.

This commit will remove looking for pool logic and will send a delete
request to all pools in parallel. This should not cause any performance
regression in most of the cases since the object will unlikely exist
in only one pool, and the performance price will be similar to
getPoolIndex() in that case.
2025-01-28 08:57:18 -08:00
Klaus Post dcc000ae2c Allow URLs up to 32KB and improve parsing speed (#20874)
Before/after...
```
Benchmark_hasBadPathComponent/long-32          	   43936	     27232 ns/op	 146.89 MB/s	   32768 B/op	       1 allocs/op
Benchmark_hasBadPathComponent/long-32          	   89956	     13375 ns/op	 299.07 MB/s	       0 B/op	       0 allocs/op
```

* Remove unused.
2025-01-27 08:42:45 -08:00
Harshavardhana c5d19ecebb do not expose secret-key to lambda event handler (#20870) 2025-01-24 11:27:43 -08:00
Harshavardhana ed29a525b3 remove fips builds 2025-01-21 02:10:10 -08:00
Minio Trusted 020c46cd3c Update yaml files to latest version RELEASE.2025-01-20T14-49-07Z 2025-01-21 09:44:32 +00:00
Klaus Post 827004cd6d Add Full Object Checksums and CRC64-NVME (#20855)
Backport of AIStor PR 247.

Add support for full object checksums as described here:

https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html

New checksum types are fully supported. Mint tests from https://github.com/minio/minio-go/pull/2026 are now passing.

Includes fixes from https://github.com/minio/minio/pull/20743 for mint tests.

Add using checksums as validation for object content. Fixes #20845 #20849

Fixes checksum replication (downstream PR 250)
2025-01-20 06:49:07 -08:00
Harshavardhana 779ec8f0d4 do not list buckets without local quorum (#20852)
ListBuckets() would result in listing buckets
without quorum, this PR fixes the behavior.
2025-01-19 15:13:17 -08:00
Minio Trusted 3d0f513ee2 Update yaml files to latest version RELEASE.2025-01-18T00-31-37Z 2025-01-18 01:38:52 +00:00
Harshavardhana 4b6eadbd80 update deps (#20851) 2025-01-17 16:31:37 -08:00
Shubhendu 6f47414b23 Correct bucket metrics name (#20823)
Earlier, cluster and bucket metrics were named 
`minio_usage_last_activity_nano_seconds`.

The bucket level is now named as 
`minio_bucket_usage_last_activity_nano_seconds`

Signed-off-by: Shubhendu Ram Tripathi <shubhendu@minio.io>
2025-01-17 13:12:21 -08:00
Harshavardhana 224a27992a update go-deps on debugging tools 2025-01-17 11:41:46 -08:00
Harshavardhana 232544e1d8 update inspect export, which was missed from #20846 2025-01-17 11:38:44 -08:00
Anis Eleuch dbcb71828d s3: Provide enough buffer when the object final size is unknown (#20847)
When compression is enabled, the final object size is not calculated in
that case, we need to make sure that the provided buffer is always
more significant than the shard size, the bitrot will always calculate 
the hash of blocks with shard size, except the last block.
2025-01-17 11:19:30 -08:00
Klaus Post b9196757fd Fix inconsistently written compressed files. (#20846)
Before https://github.com/minio/minio/pull/20575, files could pick up indices 
from unrelated files if no index was added.

This would result in these files not being consistent across a set.

When loading, search for the compression indicators and check if they 
are within the problematic date range, and clean up any parts that have 
an index but shouldn't.

The test validates that the signature matches the one in files stored without an index.

Bumps xlMetaVersion, so this check doesn't have to be made for future versions.
2025-01-17 11:17:18 -08:00
Andreas Auernhammer b4ac53d157 update github.com/minio/kms-go/kes to v0.3.1 (#20843)
Signed-off-by: Andreas Auernhammer <github@aead.dev>
2025-01-16 01:13:28 -08:00
Poorna 4952bdb770 decom: avoid skipping single delete markers for replication (#20836)
It is possible delete marker was received on old pool as decom
move in progress, this PR allows decom retry to ensure these
delete markers are moved to new pool so that decommission can
be completed.

Fixes #20819
2025-01-14 11:53:02 -08:00
Anis Eleuch 00b2ef2932 Bump golang.org/x/net to silence wrong vuln checker (#20814) 2025-01-08 16:39:24 +05:30
Klaus Post 4536ecfaa4 Add cpuio profiling potential crash workaround (#20809)
Add profiling potential crash wourkaround

Using admin traces could potentially crash the server (or handler more likely) due to upstream divide by 0: https://github.com/felixge/fgprof/pull/34

Ensure the profile always runs 100ms before stopping, so sample count isn't 0 (default sample rate ~10ms/sample, but allow for cpu starvation)
2025-01-06 21:21:54 +05:30
Harshavardhana 43a7402968 update helm release v5.4.0 2025-01-02 21:34:47 -08:00
Allan Roger Reid 330dca9a35 Add resiliency tests (#20786) 2024-12-20 20:24:45 -08:00
Klaus Post ddd137d317 ListObjectParts should return actual size (#20782)
Fixes #20781:

```
λ aws --endpoint-url http://127.0.0.1:9001 s3api list-parts --bucket testbucket --key test.testcompress --upload-id "ZDM0YzUwM2YtZWM1Zi00NWI2LTgxMzYtZTIwMGE3Yjc0Y2Y1LjYyMzgyMmFhLWU2N2QtNGUyYS04NDE1LWUzZDFlZmJmMWUyZHgxNzM0NjI1MjgyMDkyNzY4MDAw"
{
    "Parts": [
        {
            "PartNumber": 1,
            "LastModified": "2024-12-19T16:47:04.334000+00:00",
            "ETag": "\"7025f242f56479e06c435c0b500cdbb2\"",
            "Size": 2002
        }
    ],
    "ChecksumAlgorithm": "",
    "Initiator": {
        "ID": "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4",
        "DisplayName": "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4"
    },
    "Owner": {
        "DisplayName": "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4",
        "ID": "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4"
    },
    "StorageClass": "STANDARD"
}
```

(for whatever reason the python script generated a 2002 byte file ;)
2024-12-19 23:51:46 +05:30
Minio Trusted 06ddd8770e Update yaml files to latest version RELEASE.2024-12-18T13-15-44Z 2024-12-19 00:24:40 +00:00
Anis Eleuch 16f8cf1c52 heal: Include more use case of not healable but readable objects (#248) (#20776)
If one object has many parts where all parts are readable but some parts
are missing from some drives, this object can be sometimes un-healable,
which is wrong.

This commit will avoid reading from drives that have missing, corrupted or
outdated xl.meta. It will also check if any part is unreadable to avoid
healing in that case.
2024-12-18 05:15:44 -08:00
Mark Theunissen 01e520eb23 s3: Sanitize the source object name in CopyObject handler (#20774) 2024-12-17 07:01:07 -08:00
Harshavardhana 02f770a0c0 update all dependencies and use latest msgp (#20768) 2024-12-16 04:20:12 +05:30
dependabot[bot] 2f4c79bc0f Bump golang.org/x/crypto from 0.29.0 to 0.31.0 (#20767)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.29.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.29.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-14 23:04:23 -08:00
dependabot[bot] 969ee7dfbe Bump golang.org/x/crypto from 0.23.0 to 0.31.0 in /docs/debugging/inspect (#20760)
Bump golang.org/x/crypto in /docs/debugging/inspect

Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.23.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.23.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-14 22:04:58 -08:00
Minio Trusted 5f0b086b05 Update yaml files to latest version RELEASE.2024-12-13T22-19-12Z 2024-12-15 03:52:57 +00:00
Artur Melanchyk 68b004a48f fix: specify size in some map allocations (#20764) 2024-12-13 14:19:12 -08:00
Harshavardhana 54ecce66f0 move readLock on uploadId at EOF in PutObjectPart (#258)
we do not need to hold the read locks at the higher
layer instead before reading the body, instead hold
the read locks properly at the time of renamePart()
for protection from racy part overwrites to compete
with concurrent completeMultipart().
2024-12-13 14:06:18 -08:00
Artur Melanchyk 2b008c598b fix: replace mutex with atomic (#20762) 2024-12-13 19:32:46 +05:30
dependabot[bot] 86d02b17cf Bump golang.org/x/crypto from 0.23.0 to 0.31.0 in /docs/debugging/s3-verify (#20757)
Bump golang.org/x/crypto in /docs/debugging/s3-verify

Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.23.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.23.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-12 05:00:05 -08:00
Anis Eleuch c1a95a70ac heal: Move CheckParts from single handler to streaming RPC (#20755)
CheckParts call can take time to verify 10k parts of a single object in a single drive.
To avoid an internal dealine of one minute in the single handler RPC, this commit will
switch to streaming RPC instead.
2024-12-12 11:20:57 +05:30
Aditya Manthramurthy f246c9053f fix: Privilege escalation in IAM import API (#20756)
This API had missing permissions checking, allowing a user to change
their policy mapping by:

1. Craft iam-info.zip file: Update own user permission in
user_mappings.json
2. Upload it via `mc admin cluster iam import nobody iam-info.zip`

Here `nobody` can be a user with pretty much any kind of permission (but
not anonymous) and this ends up working.

Some more detailed steps - start from a fresh setup:

```
./minio server /tmp/d{1...4} &
mc alias set myminio http://localhost:9000 minioadmin minioadmin
mc admin user add myminio nobody nobody123
mc admin policy attach myminio readwrite nobody nobody123
mc alias set nobody http://localhost:9000 nobody nobody123

mc admin cluster iam export myminio
mkdir /tmp/x && mv myminio-iam-info.zip /tmp/x
cd /tmp/x
unzip myminio-iam-info.zip
echo '{"nobody":{"version":1,"policy":"consoleAdmin","updatedAt":"2024-08-13T19:47:10.1Z"}}' > \
      iam-assets/user_mappings.json
zip -r myminio-iam-info-updated.zip iam-assets/

mc admin cluster iam import nobody ./myminio-iam-info-updated.zip
mc admin service restart nobody
```
2024-12-12 07:39:40 +05:30
Cesar N. 9cdd204ae4 Upgrade Console version to v1.7.5 (#20748) 2024-12-11 16:23:53 +05:30
Harshavardhana 7b3eb9f7f8 fix: groups lookup performance issue with users with lots of groups (#20740)
fixes https://github.com/minio/minio/issues/20717
2024-12-11 16:23:28 +05:30
ebozduman d56ef8dbe1 Adds AIstore documentation link (#20738) 2024-12-11 16:22:17 +05:30
Mark Theunissen a248ed5ff5 Fixes for POST policy checks and the x-ignore implementation (#20674) 2024-12-11 16:21:34 +05:30
Klaus Post 5bb31e4883 Disable mint full object tests (#20743)
Remove expected failures from https://github.com/minio/minio-go/pull/2026
2024-12-09 18:59:22 -08:00
Taran Pelkey aff2a76d80 Return error when attempting to create a policy with commas in name (#20724) 2024-12-04 03:51:26 -08:00
Anis Eleuch eddbe6bca2 heal: Report bucket healing result correctly (#20721) 2024-12-04 04:42:25 +05:30
Anis Eleuch 734d1e320a heal: Single object heal to look for older versions as well (#203) (#20723)
`mc admin heal ALIAS/bucket/object` does not have any flag to heal
object noncurrent versions, this commit will make healing of the object
noncurrent versions implicitly asked.

This also fixes the 'mc admin heal ALIAS/bucket/object' that does not work 
correctly when the bucket is versioned. This has been broken since Apr 2023.
2024-12-04 04:42:04 +05:30
Anis Eleuch b8dab7b1a9 Set http server read/write timeout from --idle-timeout (#228) (#20715)
Golang http.Server will call SetReadDeadline overwriting the previous
deadline configuration set after a new connection Accept in the custom
listener code. Therefore, --idle-timeout was not correctly respected.

Make http.Server read/write timeout similar to --idle-timeout.
2024-12-02 18:51:17 +05:30
Klaus Post abd6bf060d Add 'X-Forwarded-For' to (s)FTP requests (#20709)
Fixes #20707
2024-11-29 18:25:37 +05:30
Alex f0d4ef604c Updated Console to v1.7.4 (#20693)
Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
2024-11-28 13:14:11 +05:30
Ramon de Klein 2712f75762 prevent IAM cleanup errors (#20691) 2024-11-28 13:13:51 +05:30
Krutika Dhananjay 4c46668da8 Add a test case for fix #20684 (#20688)
The test fails without the change.
Also, removed a duplicate test case involving lifecycle config with no rules.
2024-11-28 13:13:31 +05:30
Anis Eleuch 02e93fd6ba heal: Better reporting to mc with dangling/timeout errors (#20690)
The code assigns corrupted state to a drive for any unexpected error,
which is confusing for users. This change will make sure to assign
corrupted state only for corrupted parts or xl.meta. Use unknown state
with a explanation for any unexpected error, like canceled, deadline
errors, drive timeout, ...

Also make sure to return the bucket/object name when the object is not
found or marked not found by the heal dangling code.
2024-11-26 10:45:35 -08:00
Krutika Dhananjay 366876e98b Fix prefix validation in lifecycle rule (#20684) 2024-11-25 12:12:21 -08:00
Mark Theunissen d202fdd022 Add the policy name to the audit logs tags when doing policy-based API calls. Add retention settings to tags (#20638)
* Add the policy name to the audit log tags when doing policy-based API calls

* Audit log the retention settings requested in the API call

* Audit log of retention on PutObjectRetention API path too
2024-11-25 09:17:12 -08:00
Eng Zer Jun c07e5b49d4 refactor: replace experimental maps and slices with stdlib (#20679)
The experimental functions are now available in the standard library in
Go 1.23 [1].

[1]: https://go.dev/doc/go1.23#new-unique-package

Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>
2024-11-25 09:10:22 -08:00
Aditya Manthramurthy 9a39f8ad4d fix: Remove User should fail for a service account (#20677)
The RemoveUser API only removes internal users, and it reports success
when it didnt find the internal user account for deletion. When provided
with a service account, it should not report success as that is misleading.
2024-11-21 18:24:04 -08:00
Nao Yonashiro 7e0c1c9413 feat: bump github.com/cosnicolaou/pbzip2 from 1.0.3 to 1.0.5 (#20671)
Update github.com/cosnicolaou/pbzip2 to latest version for
significant performance improvements. This update brings a 45%
reduction in processing time.
2024-11-20 18:53:52 -08:00
John Morales 485d833cd7 fix: API label casing and count value for +Inf bucket v2 metrics (#20656) 2024-11-18 11:17:52 -08:00
Klaus Post e8a476ef5a Keep larger merge buffers for RPC (#20654)
Keep larger merge buffers

When sending large messages >1K, the merge buffer would continuously be reallocated.

This could happen on listings, where blocks are typically 4->8K.

Keep merge buffer of up to 256KB.

Benchmark with 4096b messages:

```
benchmark                                          old ns/op     new ns/op     delta
BenchmarkRequests/servers=2/bytes/par=32-32        8271          6360          -23.10%
BenchmarkRequests/servers=2/bytes/par=64-32        7840          4731          -39.66%
BenchmarkRequests/servers=2/bytes/par=128-32       7291          4740          -34.99%
BenchmarkRequests/servers=2/bytes/par=256-32       7095          4580          -35.45%
BenchmarkRequests/servers=2/bytes/par=512-32       6757          4584          -32.16%
BenchmarkRequests/servers=2/bytes/par=1024-32      6429          4453          -30.74%

benchmark                                          old bytes     new bytes     delta
BenchmarkRequests/servers=2/bytes/par=32-32        12090         821           -93.21%
BenchmarkRequests/servers=2/bytes/par=64-32        17423         820           -95.29%
BenchmarkRequests/servers=2/bytes/par=128-32       18493         822           -95.56%
BenchmarkRequests/servers=2/bytes/par=256-32       18892         821           -95.65%
BenchmarkRequests/servers=2/bytes/par=512-32       19064         826           -95.67%
BenchmarkRequests/servers=2/bytes/par=1024-32      19038         842           -95.58%
```
2024-11-16 09:18:48 -08:00
Krutika Dhananjay 267f0ecea2 Fix 0 httpTimeout for logger webhook (#20653)
Previously, not setting http.Config.HTTPTimeout for logger webhook
was resulting in a timeout of 0, and causing "deadline exceeded"
errors in log webhook.

This change introduces a new env variable for configuring log webhook
timeout and more importantly sets it when the config is initialised.
2024-11-16 02:01:36 -08:00
Harshavardhana 4ee3434854 updating all dependencies as per regular cadence (#20646) 2024-11-14 12:33:18 -08:00
Anis Eleuch 0e9854372e heal/batch: Fix missing redirection to the first node (#20642)
Manual heal can return XMinioHealInvalidClientToken if the manual
healing is started in the first node, and the next mc call to get the
heal status is landed on another node. The reason is that redirection
based on the token ID is not able to redirect requests to the first node
due to a typo.

This also affects the batch cancel command if the batch is being done in
the first node, the user will never be able to cancel it due to the same
bug.
2024-11-13 04:07:28 -08:00
Klaus Post b5177993b3 Make DeadlineConn http.Listener compatible (#20635)
HTTP likes to slap an infinite read deadline on a connection and 
do a blocking read while the response is being written.

This effectively means that a reading deadline becomes the 
request-response deadline.

Instead of enforcing our timeout, we pass it through and keep 
"infinite deadline" is sticky on connections.

However, we still "record" when reads are aborted, so we never overwrite that.

The HTTP server should have `ReadTimeout` and `IdleTimeout` set for the deadline to be effective.

Use --idle-timeout for incoming connections.
2024-11-12 12:41:41 -08:00
Klaus Post 55f5c18fd9 Harden internode DeadlineConn (#20631)
Since DeadlineConn would send deadline updates directly upstream,
it would race with Read/Write operations. The stdlib will perform a read, 
but do an async SetReadDeadLine(unix(1)) to cancel the Read in 
`abortPendingRead`. In this case, the Read may override the 
deadline intended to cancel the read.

Stop updating deadlines if a deadline in the past is seen and when Close is called. 
A mutex now protects all upstream deadline calls to avoid races. 

This should fix the short-term buildup of...

```
365 @ 0x44112e 0x4756b9 0x475699 0x483525 0x732286 0x737407 0x73816b 0x479601
#	0x475698	sync.runtime_notifyListWait+0x138		runtime/sema.go:569
#	0x483524	sync.(*Cond).Wait+0x84				sync/cond.go:70
#	0x732285	net/http.(*connReader).abortPendingRead+0xa5	net/http/server.go:729
#	0x737406	net/http.(*response).finishRequest+0x86		net/http/server.go:1676
#	0x73816a	net/http.(*conn).serve+0x62a			net/http/server.go:2050
```

AFAICT Only affects internode calls that create a connection (non-grid).
2024-11-11 09:15:17 -08:00
Harshavardhana 8ce101c174 fix: LDAP service port number in tests 2024-11-11 07:01:29 -08:00
Klaus Post 4972735507 Fix lint issues from v1.62.0 upgrade (#20633)
* Fix lint issues from v1.62.0 upgrade

* Fix xlMetaV2TrimData version checks.
2024-11-11 06:51:43 -08:00
Minio Trusted e6ca6de194 Update yaml files to latest version RELEASE.2024-11-07T00-52-20Z 2024-11-07 23:58:42 +00:00
Harshavardhana cefc43e4da simplify the Get()/GetMultiple() re-use GetRaw() for both (#179)
Remember GetMultiple() must be used if your target is calling
PutMultiple(), without that the multiple events will not be
replayed.
2024-11-06 16:52:20 -08:00
Ramon de Klein 25e34fda5f decompress audit log properly before sending to remote target (#20619) 2024-11-06 13:25:24 -08:00
Erfan 4208d7af5a docs: remove redundant prometheus metric (#20618) 2024-11-06 07:44:21 -08:00
Klaus Post 8d42f37e4b Fix msgUnPath crash (#20614)
These are needed checks for the functions to be un-crashable with any input 
given to `msgUnPath` (tested with fuzzing).

Both conditions would result in a crash, which prevents that. Some 
additional upstream checks are needed.

Fixes #20610
2024-11-05 04:37:59 -08:00
dependabot[bot] 7cb4b5c636 Bump github.com/golang-jwt/jwt/v4 from 4.5.0 to 4.5.1 (#20611)
Bumps [github.com/golang-jwt/jwt/v4](https://github.com/golang-jwt/jwt) from 4.5.0 to 4.5.1.
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md)
- [Commits](https://github.com/golang-jwt/jwt/compare/v4.5.0...v4.5.1)

---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-05 02:22:36 -08:00
Harshavardhana 1615920f48 fix typos reported in CI/CD 2024-11-04 11:06:02 -08:00
Cesar N. 7ee42b3ff5 Update console package to v1.7.3 (#20606) 2024-11-04 08:47:08 -08:00
Harshavardhana a6f1e727fb add tests for ILM transition and healing (#166) (#20601)
This PR fixes a regression introduced in https://github.com/minio/minio/pull/19797
by restoring the healing ability of transitioned objects

Bonus: support for transitioned objects to carry original
The object name is for future reverse lookups if necessary.

Also fix parity calculation for tiered objects to n/2 for n/2 == (parity)
2024-10-31 15:10:24 -07:00
Aditya Manthramurthy c1fc7779ca Remove expires field from list objects metadata (#20600)
This field was always 0 regardless of whether the object had an expiry
so we are basically removing dead code.
2024-10-31 12:27:06 -07:00
Allan Roger Reid b3ab7546ee Fix typos in README.md (#20599) 2024-10-31 10:38:53 -07:00
Minio Trusted ad88a81e3d Update yaml files to latest version RELEASE.2024-10-29T16-01-48Z 2024-10-30 21:24:58 +00:00
Aditya Manthramurthy c4239ced22 run IAM purge routines deterministically every hr (#20587)
Existing implementation runs IAM purge routines for expired LDAP and
OIDC accounts with a probability of 0.25 after every IAM refresh. This
change ensures that they are run once in each hour.
2024-10-29 09:01:48 -07:00
Anis Eleuch f85c28e960 heal: large objects fix and avoid .healing.bin corner case premature exit (#20577)
xlStorage.Healing() returns nil if there is an error reading
.healing.bin or if this latter is empty. healing.bin update()
call returns early if .healing.bin is empty; hence, no further update
of .healing.bin is possible.

A .healing.bin can be empty if os.Open() with O_TRUNC is successful
but the next Write returns an error.

To avoid this weird situation, avoid making healingTracker.update()
to return early if .healing.bin is empty, so write again.

This commit also fixes wrong error log printing when an object is 
healed in another drive in the same erasure set but not in the drive 
that is actively healing by fresh drive healing code. Currently, it prints 
<nil> instead of a factual error.

* heal: Scan .minio.sys metadata only during site-wide heal (#137)

mc admin heal always invoke .minio.sys heal, but sometimes, this latter
contains a lot of data, many service accounts, STS accounts etc, which
makes mc admin heal command very slow.

Only invoke .minio.sys healing when no bucket was specified in `mc admin
heal` command.
2024-10-26 02:58:27 -07:00
Anis Eleuch f7e176d4ca heal: Avoid deadline error with very large objects (#140) (#20586)
Healing a large object with a normal scan mode where no parts read 
is involved can still fail after 30 seconds if an object has

There are too many parts when hard disks are being used mainly. 
The reason is there is a general deadline that checks for all parts we 
do a deadline per part.
2024-10-26 02:56:26 -07:00
Aditya Manthramurthy 72a0d14195 fix: avoid useless expires value in listing meta (#20584)
When listing objects with metadata, avoid returning an "expires" time
metadata value when its value is the zero time as this means that no
expires value is set on the object.
2024-10-24 19:13:19 -07:00
Klaus Post 6abe4128d7 Fix ILM expire workers exiting (#20578)
Fix expire workers exiting

Under 2 conditions ILM expire workers would exit, eventually causing all workers to terminate.
2024-10-23 08:35:37 -07:00
Klaus Post ed5ed7e490 Trace ILM errors (#20576)
Some paths would attempt transitions but in case of failures 
no traces would be emitted.

Add traces (with errors) when transition operations fail.
2024-10-22 14:10:34 -07:00
Klaus Post 51410c9023 Clear omitted fields (#20575)
Searched `msg:"[a-zA-Z0-9]*,omitempty` through the codebase.

Uses latest tinylib master.
2024-10-22 08:30:50 -07:00
Shubhendu 96ca402dcd Correct the date filter check for batch replication (#20569)
The condition were incorrect as we were comparing the filter
value against the modification time object.

For example if created after filter date is after modification
time of object, that means object was created before the filter
time and should be skipped while replication because as per the
filter we need only the objects created after the filter date.

Signed-off-by: Shubhendu Ram Tripathi <shubhendu@minio.io>
2024-10-18 08:32:09 -07:00
Anis Eleuch 3da7c9cce3 repl: Fix removal of replicator svc when keycloak is configured (#120)
When Keycloak vendor is set, the code will start to clean up service
accounts that parents do not exist anymore. However, the code will also
look for the parent user of site-replicator-0, MINIO_ROOT_USER, which
obviously does not exist in Keycloak. Therefore, the site-replicator-0
will be removed automatically.

This commit will avoid cleaning up service accounts generated from
the root user.
2024-10-14 09:35:37 -07:00
Harshavardhana a14e19ec54 remove support for s390x 2024-10-13 07:06:17 -07:00
Minio Trusted e091dde041 Update yaml files to latest version RELEASE.2024-10-13T13-34-11Z 2024-10-13 14:05:41 +00:00
Harshavardhana d10bb7e1b6 remove reference to s390x binary, we wont publish them anymore 2024-10-13 06:34:11 -07:00
Anis Eleuch 7ebceacac6 heal: Fix deep scan failing to heal objects (#117)
The verify file handler response format was changed from gob to msgp
since two months but we forgot updating the verify handler client.

VerifyFile is only called during a heal deep scan (bitrot check).
HealObject() will fail in that case and will mark all disks corrupted and
will return early (as unrecoverable object but it will also not be
removed)

It is a bit rare for HealObject to be called with a deep scan flag. It
is called when a HealObject with a normal scan (e.g. new drive healing)
detects a bitrot corruption, therefore healing objects with a detected
bitrot corruption will fail.
2024-10-13 06:07:21 -07:00
Harshavardhana 1593cb615d avoid unnecessary logging for KMS secret key mismatch (#20549) 2024-10-13 06:06:08 -07:00
Yannis Mazzer 86a41d1631 fix(helm) removing clusterDomain from startup command to avoid local … (#20547) 2024-10-11 05:21:05 -07:00
Taran Pelkey d4157b819c Allow LDAP DNs with slashes to be loaded from object store (#20541) 2024-10-10 16:40:37 -07:00
Yannis Mazzer e0aceca1b7 feat(helm) making securityContext consistent (#20546) 2024-10-10 08:48:31 -07:00
Harrison Brown 87804624fe Helm: Add extraVolumes and extraVolumeMounts to the customCommandJob section (#19988)
added extraVolumes and extraVolumeMounts to the customCommandJob section
2024-10-10 08:48:16 -07:00
Poorna e029f8a9d7 set kms keyid in replication opts (#20542) 2024-10-09 23:49:55 -07:00
Poorna 1bc6681176 fix tagging overwrite during resync (#20525) 2024-10-04 22:16:15 -07:00
Poorna 28322124e2 remove replication stats from data usage cache (#20524)
This is no longer needed since historical stats are not maintained anymore.
2024-10-04 15:23:33 -07:00
Harshavardhana cbfe9de3e7 do not download binary before verifying the version (#20523)
fixes https://github.com/minio/mc/issues/4980
2024-10-04 04:32:32 -07:00
Harshavardhana dc86b8d9d4 fix: when readQuorum, inconsistent metadata return 404 (#20522)
in cases where we cannot possibly know a way to read and 
construct the object,  it is impossible to achieve any form of 
quorum via xl.meta while we have sufficient responses from 
all the drives, we should return object not found.
2024-10-04 00:13:14 -07:00
Taran Pelkey ba70118e2b Add root user to ListAccessKeysBulk (#20517) 2024-10-03 16:11:02 -07:00
Minio Trusted cb1d3e50f7 Update yaml files to latest version RELEASE.2024-10-02T17-50-41Z 2024-10-02 21:26:16 +00:00
Harshavardhana ded0b19d97 avoid audit logs with unexpected errors (#20516)
fixes #20513
2024-10-02 10:50:41 -07:00
Poorna d0bb3dd136 list all batch job types (#20510)
continues #20480
2024-10-01 23:38:17 -07:00
Harshavardhana ab7714b01e upgrade relevant dependencies (#20507) 2024-10-01 23:37:55 -07:00
Ramon de Klein e5b18df6db Fix checksum error during startup when minio is loaded via PATH environment variable (#20509) 2024-10-01 15:13:18 -07:00
Anis Eleuch 0abfd1bcb1 heal: Use etag as quorum when none found for modtime (#20500) 2024-10-01 08:19:10 -07:00
Harshavardhana 6186d11761 handle the locks properly for multi-pool callers (#20495)
- PutObjectMetadata()
- PutObjectTags()
- DeleteObjectTags()
- TransitionObject()
- RestoreTransitionObject()

Also improve the behavior of multipart code across
pool locks, hold locks only once per upload ID for

- CompleteMultipartUpload()
- AbortMultipartUpload()
- ListObjectParts() (read-lock)
- GetMultipartInfo() (read-lock)
- PutObjectPart() (read-lock)

This avoids lock attempts across pools for no
reason, this increases O(n) when there are n-pools.
2024-09-29 15:40:36 -07:00
Poorna e8b457e8a6 Change delete marker proxy test to use distributed setup (#20494) 2024-09-27 18:02:26 -07:00
Harshavardhana afea40cc0f fix: keep locks based on the first pool, first EC set (#93)
multi-object deletion may or may not compete with locks
granted for other callers, causing concurrent operations
to succeed on each other.

A continuation of the PR https://github.com/minio/minio/pull/20356
2024-09-27 03:41:37 -07:00
Aditya Manthramurthy 402b798f1b fix: allow all console actions with custom authZ (#20489)
When custom authorization via plugin is enabled, the console will now
render the UI as if all actions are allowed. Since server cannot
determine the exact policy allowed for a user via the plugin, this is
acceptable to do. If a particular action is actually not allowed by the
plugin the call will result in an error.

Previously the server was evaluating a policy when custom authZ is
enabled - this is fixed now.
2024-09-26 23:44:44 -07:00
Klaus Post 4759532e90 Fix PPC cgroup memory limit (#20488)
The "unlimited" value on PPC wasn't exactly the same as amd64.

Instead compare against an "unreasonably big value".

Would cause OOM in anything using the concurrent request limit.
2024-09-26 10:07:10 -07:00
Harshavardhana 7f1e1713ab use absolute path for binary checksum verification (#20487) 2024-09-26 08:03:08 -07:00
Poorna b2c5819dbc hold on to batch job stats till cleanup (#20480)
This PR also fixes job stats not available after restart
2024-09-24 14:50:11 -07:00
Anis Eleuch 2b0156b1fc Add TTFB to all APIs and enable for responses without body (#20479)
Add TTFB for all requests in metrics-v3 in addition to the existing
GetObject. Also for the requests that do not return a body in the
response, calculate TTFB as the HTTP status code and the headers are
sent.
2024-09-24 10:13:00 -07:00
Harshavardhana f6f0807c86 cleanup existing part.N's before renamePart() (#20466)
this is a safety-net to avoid any unexpected parts to show up.
2024-09-24 04:26:41 -07:00
Harshavardhana 0c53d86017 remove the list from 'mc stat' from testing via '--no-list' (#20468)
avoid 'listing' as it may get incorrect results for
these tests, we are only interested in 'mc stat' as
in HEAD object here.
2024-09-24 01:03:58 -07:00
Minio Trusted 6a6ee46d76 Update yaml files to latest version RELEASE.2024-09-22T00-33-43Z 2024-09-23 19:42:46 +00:00
Klaus Post 974cbb3bb7 Limit jstream parse depth (#20474)
Add https://github.com/bcicen/jstream/pull/15 by vendoring the package.

Sets JSON depth limit to 100 entries in S3 Select.
2024-09-23 12:35:41 -07:00
357 changed files with 15151 additions and 7007 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
go-version: [1.22.x] go-version: [1.23.x]
os: [ubuntu-latest] os: [ubuntu-latest]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
go-version: [1.22.x] go-version: [1.23.x]
os: [ubuntu-latest] os: [ubuntu-latest]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
go-version: [1.22.x] go-version: [1.23.x]
os: [ubuntu-latest] os: [ubuntu-latest]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
go-version: [1.22.x] go-version: [1.23.x]
os: [ubuntu-latest] os: [ubuntu-latest]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
+39
View File
@@ -0,0 +1,39 @@
name: Resiliency Functional 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.23.x]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
check-latest: true
- name: Build on ${{ matrix.os }}
if: matrix.os == 'ubuntu-latest'
env:
CGO_ENABLED: 0
GO111MODULE: on
run: |
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
make test-resiliency
+2 -1
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
go-version: [1.22.x] go-version: [1.23.x]
os: [ubuntu-latest] os: [ubuntu-latest]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -39,3 +39,4 @@ jobs:
sudo sysctl net.ipv6.conf.all.disable_ipv6=0 sudo sysctl net.ipv6.conf.all.disable_ipv6=0
sudo sysctl net.ipv6.conf.default.disable_ipv6=0 sudo sysctl net.ipv6.conf.default.disable_ipv6=0
make verify make verify
make test-timeout
+1 -1
View File
@@ -61,7 +61,7 @@ jobs:
# are turned off - i.e. if ldap="", then ldap server is not enabled for # are turned off - i.e. if ldap="", then ldap server is not enabled for
# the tests. # the tests.
matrix: matrix:
go-version: [1.22.x] go-version: [1.23.x]
ldap: ["", "localhost:389"] ldap: ["", "localhost:389"]
etcd: ["", "http://localhost:2379"] etcd: ["", "http://localhost:2379"]
openid: ["", "http://127.0.0.1:5556/dex"] openid: ["", "http://127.0.0.1:5556/dex"]
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
- name: setup-go-step - name: setup-go-step
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: 1.22.x go-version: 1.23.x
- name: github sha short - name: github sha short
id: vars id: vars
+2 -1
View File
@@ -21,7 +21,7 @@ jobs:
strategy: strategy:
matrix: matrix:
go-version: [1.22.x] go-version: [1.23.x]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -40,6 +40,7 @@ jobs:
sudo sysctl net.ipv6.conf.all.disable_ipv6=0 sudo sysctl net.ipv6.conf.all.disable_ipv6=0
sudo sysctl net.ipv6.conf.default.disable_ipv6=0 sudo sysctl net.ipv6.conf.default.disable_ipv6=0
make test-ilm make test-ilm
make test-ilm-transition
- name: Test PBAC - name: Test PBAC
run: | run: |
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
go-version: [1.22.x] go-version: [1.23.x]
os: [ubuntu-latest] os: [ubuntu-latest]
steps: steps:
+3
View File
@@ -15,6 +15,9 @@ docker volume rm $(docker volume ls -f dangling=true) || true
## change working directory ## change working directory
cd .github/workflows/mint cd .github/workflows/mint
## always pull latest
docker pull docker.io/minio/mint:edge
docker-compose -f minio-${MODE}.yaml up -d docker-compose -f minio-${MODE}.yaml up -d
sleep 1m sleep 1m
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
go-version: [1.22.x] go-version: [1.23.x]
os: [ubuntu-latest] os: [ubuntu-latest]
steps: steps:
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: 1.22.7 go-version: 1.23.5
- 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
shell: bash shell: bash
+51 -23
View File
@@ -1,36 +1,64 @@
linters-settings: version: "2"
gofumpt:
simplify: true
misspell:
locale: US
staticcheck:
checks: ['all', '-ST1005', '-ST1000', '-SA4000', '-SA9004', '-SA1019', '-SA1008', '-U1000', '-ST1016']
linters: linters:
disable-all: true default: none
enable: enable:
- durationcheck - durationcheck
- forcetypeassert
- gocritic - gocritic
- gofumpt
- goimports
- gomodguard - gomodguard
- govet - govet
- ineffassign - ineffassign
- misspell - misspell
- revive - revive
- staticcheck - staticcheck
- tenv
- typecheck
- unconvert - unconvert
- unused - unused
- usetesting
- whitespace
settings:
misspell:
locale: US
staticcheck:
checks:
- all
- -SA1008
- -SA1019
- -SA4000
- -SA9004
- -ST1000
- -ST1005
- -ST1016
- -U1000
exclusions:
generated: lax
rules:
- linters:
- forcetypeassert
path: _test\.go
- path: (.+)\.go$
text: 'empty-block:'
- path: (.+)\.go$
text: 'unused-parameter:'
- path: (.+)\.go$
text: 'dot-imports:'
- path: (.+)\.go$
text: should have a package comment
- path: (.+)\.go$
text: error strings should not be capitalized or end with punctuation or a newline
paths:
- third_party$
- builtin$
- examples$
issues: issues:
exclude-use-default: false max-issues-per-linter: 100
exclude: max-same-issues: 100
- "empty-block:" formatters:
- "unused-parameter:" enable:
- "dot-imports:" - gofumpt
- should have a package comment - goimports
- error strings should not be capitalized or end with punctuation or a newline exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
+10 -7
View File
@@ -1,11 +1,5 @@
[files] [files]
extend-exclude = [ extend-exclude = [".git/", "docs/", "CREDITS", "go.mod", "go.sum"]
".git/",
"docs/",
"CREDITS",
"go.mod",
"go.sum",
]
ignore-hidden = false ignore-hidden = false
[default] [default]
@@ -20,6 +14,7 @@ extend-ignore-re = [
'http\.Header\{"X-Amz-Server-Side-Encryptio":', 'http\.Header\{"X-Amz-Server-Side-Encryptio":',
"ZoEoZdLlzVbOlT9rbhD7ZN7TLyiYXSAlB79uGEge", "ZoEoZdLlzVbOlT9rbhD7ZN7TLyiYXSAlB79uGEge",
"ERRO:", "ERRO:",
"(?Rm)^.*(#|//)\\s*spellchecker:disable-line$", # ignore line
] ]
[default.extend-words] [default.extend-words]
@@ -40,3 +35,11 @@ extend-ignore-re = [
"TestGetPartialObjectMisAligned" = "TestGetPartialObjectMisAligned" "TestGetPartialObjectMisAligned" = "TestGetPartialObjectMisAligned"
"thr" = "thr" "thr" = "thr"
"toi" = "toi" "toi" = "toi"
[type.go]
extend-ignore-identifiers-re = [
# Variants of `typ` used to mean `type` in golang as it is otherwise a
# keyword - some of these (like typ1 -> type1) can be fixed, but probably
# not worth the effort.
"[tT]yp[0-9]*",
]
+3593 -151
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.22-alpine as build FROM golang:1.23-alpine as build
ARG TARGETARCH ARG TARGETARCH
ARG RELEASE ARG RELEASE
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.22-alpine AS build FROM golang:1.23-alpine AS build
ARG TARGETARCH ARG TARGETARCH
ARG RELEASE ARG RELEASE
-69
View File
@@ -1,69 +0,0 @@
FROM golang:1.22-alpine AS build
ARG TARGETARCH
ARG RELEASE
ENV GOPATH=/go
ENV CGO_ENABLED=0
# Install curl and minisign
RUN apk add -U --no-cache ca-certificates && \
apk add -U --no-cache curl && \
go install aead.dev/minisign/cmd/minisign@v0.2.1
# Download minio binary and signature files
RUN curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.fips -o /go/bin/minio && \
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.fips.minisig -o /go/bin/minio.minisig && \
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.fips.sha256sum -o /go/bin/minio.sha256sum && \
chmod +x /go/bin/minio
# Download mc binary and signature files
RUN curl -s -q https://dl.min.io/client/mc/release/linux-${TARGETARCH}/mc.fips -o /go/bin/mc && \
curl -s -q https://dl.min.io/client/mc/release/linux-${TARGETARCH}/mc.fips.minisig -o /go/bin/mc.minisig && \
curl -s -q https://dl.min.io/client/mc/release/linux-${TARGETARCH}/mc.fips.sha256sum -o /go/bin/mc.sha256sum && \
chmod +x /go/bin/mc
RUN if [ "$TARGETARCH" = "amd64" ]; then \
curl -L -s -q https://github.com/moparisthebest/static-curl/releases/latest/download/curl-${TARGETARCH} -o /go/bin/curl; \
chmod +x /go/bin/curl; \
fi
# Verify binary signature using public key "RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGavRUN"
RUN minisign -Vqm /go/bin/minio -x /go/bin/minio.minisig -P RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav
FROM registry.access.redhat.com/ubi9/ubi-micro:latest
ARG RELEASE
LABEL name="MinIO" \
vendor="MinIO Inc <dev@min.io>" \
maintainer="MinIO Inc <dev@min.io>" \
version="${RELEASE}" \
release="${RELEASE}" \
summary="MinIO is a High Performance Object Storage, API compatible with Amazon S3 cloud storage service." \
description="MinIO object storage is fundamentally different. Designed for performance and the S3 API, it is 100% open-source. MinIO is ideal for large, private cloud environments with stringent security requirements and delivers mission-critical availability across a diverse range of workloads."
ENV MINIO_ACCESS_KEY_FILE=access_key \
MINIO_SECRET_KEY_FILE=secret_key \
MINIO_ROOT_USER_FILE=access_key \
MINIO_ROOT_PASSWORD_FILE=secret_key \
MINIO_KMS_SECRET_KEY_FILE=kms_master_key \
MINIO_UPDATE_MINISIGN_PUBKEY="RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav" \
MINIO_CONFIG_ENV_FILE=config.env
RUN chmod -R 777 /usr/bin
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /go/bin/minio* /usr/bin/
COPY --from=build /go/bin/mc* /usr/bin/
COPY --from=build /go/bin/cur* /usr/bin/
COPY CREDITS /licenses/CREDITS
COPY LICENSE /licenses/LICENSE
COPY dockerscripts/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
EXPOSE 9000
VOLUME ["/data"]
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
CMD ["minio"]
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.22-alpine AS build FROM golang:1.23-alpine AS build
ARG TARGETARCH ARG TARGETARCH
ARG RELEASE ARG RELEASE
+24 -12
View File
@@ -2,8 +2,8 @@ PWD := $(shell pwd)
GOPATH := $(shell go env GOPATH) GOPATH := $(shell go env GOPATH)
LDFLAGS := $(shell go run buildscripts/gen-ldflags.go) LDFLAGS := $(shell go run buildscripts/gen-ldflags.go)
GOARCH := $(shell go env GOARCH) GOOS ?= $(shell go env GOOS)
GOOS := $(shell go env GOOS) GOARCH ?= $(shell go env GOARCH)
VERSION ?= $(shell git describe --tags) VERSION ?= $(shell git describe --tags)
REPO ?= quay.io/minio REPO ?= quay.io/minio
@@ -24,7 +24,7 @@ help: ## print this help
getdeps: ## fetch necessary dependencies getdeps: ## fetch necessary dependencies
@mkdir -p ${GOPATH}/bin @mkdir -p ${GOPATH}/bin
@echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOLANGCI_DIR) @echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOLANGCI_DIR)
@echo "Installing msgp" && go install -v github.com/tinylib/msgp@v1.1.10-0.20240227114326-6d6f813fff1b @echo "Installing msgp" && go install -v github.com/tinylib/msgp@v1.2.5
@echo "Installing stringer" && go install -v golang.org/x/tools/cmd/stringer@latest @echo "Installing stringer" && go install -v golang.org/x/tools/cmd/stringer@latest
crosscompile: ## cross compile minio crosscompile: ## cross compile minio
@@ -60,6 +60,10 @@ test-ilm: install-race
@echo "Running ILM tests" @echo "Running ILM tests"
@env bash $(PWD)/docs/bucket/replication/setup_ilm_expiry_replication.sh @env bash $(PWD)/docs/bucket/replication/setup_ilm_expiry_replication.sh
test-ilm-transition: install-race
@echo "Running ILM tiering tests with healing"
@env bash $(PWD)/docs/bucket/lifecycle/setup_ilm_transition.sh
test-pbac: install-race test-pbac: install-race
@echo "Running bucket policies tests" @echo "Running bucket policies tests"
@env bash $(PWD)/docs/iam/policies/pbac-tests.sh @env bash $(PWD)/docs/iam/policies/pbac-tests.sh
@@ -145,6 +149,10 @@ test-multipart: install-race ## test multipart
@echo "Test multipart behavior when part files are missing" @echo "Test multipart behavior when part files are missing"
@(env bash $(PWD)/buildscripts/multipart-quorum-test.sh) @(env bash $(PWD)/buildscripts/multipart-quorum-test.sh)
test-timeout: install-race ## test multipart
@echo "Test server timeout"
@(env bash $(PWD)/buildscripts/test-timeout.sh)
verify: install-race ## verify minio various setups verify: install-race ## verify minio various setups
@echo "Verifying build with race" @echo "Verifying build with race"
@(env bash $(PWD)/buildscripts/verify-build.sh) @(env bash $(PWD)/buildscripts/verify-build.sh)
@@ -172,7 +180,7 @@ build-debugging:
build: checks build-debugging ## builds minio to $(PWD) build: checks build-debugging ## builds minio to $(PWD)
@echo "Building minio binary to './minio'" @echo "Building minio binary to './minio'"
@CGO_ENABLED=0 go build -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null @CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) go build -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
hotfix-vars: hotfix-vars:
$(eval LDFLAGS := $(shell MINIO_RELEASE="RELEASE" MINIO_HOTFIX="hotfix.$(shell git rev-parse --short HEAD)" go run buildscripts/gen-ldflags.go $(shell git describe --tags --abbrev=0 | \ $(eval LDFLAGS := $(shell MINIO_RELEASE="RELEASE" MINIO_HOTFIX="hotfix.$(shell git rev-parse --short HEAD)" go run buildscripts/gen-ldflags.go $(shell git describe --tags --abbrev=0 | \
@@ -180,9 +188,9 @@ hotfix-vars:
$(eval VERSION := $(shell git describe --tags --abbrev=0).hotfix.$(shell git rev-parse --short HEAD)) $(eval VERSION := $(shell git describe --tags --abbrev=0).hotfix.$(shell git rev-parse --short HEAD))
hotfix: hotfix-vars clean install ## builds minio binary with hotfix tags hotfix: hotfix-vars clean install ## builds minio binary with hotfix tags
@wget -q -c https://github.com/minio/pkger/releases/download/v2.3.1/pkger_2.3.1_linux_amd64.deb @wget -q -c https://github.com/minio/pkger/releases/download/v2.3.10/pkger_2.3.10_linux_amd64.deb
@wget -q -c https://raw.githubusercontent.com/minio/minio-service/v1.0.1/linux-systemd/distributed/minio.service @wget -q -c https://raw.githubusercontent.com/minio/minio-service/v1.1.0/linux-systemd/distributed/minio.service
@sudo apt install ./pkger_2.3.1_linux_amd64.deb --yes @sudo apt install ./pkger_2.3.10_linux_amd64.deb --yes
@mkdir -p minio-release/$(GOOS)-$(GOARCH)/archive @mkdir -p minio-release/$(GOOS)-$(GOARCH)/archive
@cp -af ./minio minio-release/$(GOOS)-$(GOARCH)/minio @cp -af ./minio minio-release/$(GOOS)-$(GOARCH)/minio
@cp -af ./minio minio-release/$(GOOS)-$(GOARCH)/minio.$(VERSION) @cp -af ./minio minio-release/$(GOOS)-$(GOARCH)/minio.$(VERSION)
@@ -192,11 +200,11 @@ hotfix: hotfix-vars clean install ## builds minio binary with hotfix tags
@pkger -r $(VERSION) --ignore @pkger -r $(VERSION) --ignore
hotfix-push: hotfix hotfix-push: hotfix
@scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-0.minio.io:~/releases/server/minio/hotfixes/linux-amd64/ @scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-0.minio.io:~/releases/server/minio/hotfixes/linux-$(GOOS)/
@scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-0.minio.io:~/releases/server/minio/hotfixes/linux-amd64/archive @scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-0.minio.io:~/releases/server/minio/hotfixes/linux-$(GOOS)/archive
@scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-1.minio.io:~/releases/server/minio/hotfixes/linux-amd64/ @scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-1.minio.io:~/releases/server/minio/hotfixes/linux-$(GOOS)/
@scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-1.minio.io:~/releases/server/minio/hotfixes/linux-amd64/archive @scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-1.minio.io:~/releases/server/minio/hotfixes/linux-$(GOOS)/archive
@echo "Published new hotfix binaries at https://dl.min.io/server/minio/hotfixes/linux-amd64/archive/minio.$(VERSION)" @echo "Published new hotfix binaries at https://dl.min.io/server/minio/hotfixes/linux-$(GOOS)/archive/minio.$(VERSION)"
docker-hotfix-push: docker-hotfix docker-hotfix-push: docker-hotfix
@docker push -q $(TAG) && echo "Published new container $(TAG)" @docker push -q $(TAG) && echo "Published new container $(TAG)"
@@ -209,6 +217,10 @@ docker: build ## builds minio docker container
@echo "Building minio docker image '$(TAG)'" @echo "Building minio docker image '$(TAG)'"
@docker build -q --no-cache -t $(TAG) . -f Dockerfile @docker build -q --no-cache -t $(TAG) . -f Dockerfile
test-resiliency: build
@echo "Running resiliency tests"
@(DOCKER_COMPOSE_FILE=$(PWD)/docs/resiliency/docker-compose.yaml env bash $(PWD)/docs/resiliency/resiliency-tests.sh)
install-race: checks build-debugging ## builds minio to $(PWD) install-race: checks build-debugging ## builds minio to $(PWD)
@echo "Building minio binary with -race to './minio'" @echo "Building minio binary with -race to './minio'"
@GORACE=history_size=7 CGO_ENABLED=1 go build -tags kqueue,dev -race -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null @GORACE=history_size=7 CGO_ENABLED=1 go build -tags kqueue,dev -race -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
+1 -2
View File
@@ -4,7 +4,7 @@
[![MinIO](https://raw.githubusercontent.com/minio/minio/master/.github/logo.svg?sanitize=true)](https://min.io) [![MinIO](https://raw.githubusercontent.com/minio/minio/master/.github/logo.svg?sanitize=true)](https://min.io)
MinIO is a High Performance Object Storage released under GNU Affero General Public License v3.0. It is API compatible with Amazon S3 cloud storage service. Use MinIO to build high performance infrastructure for machine learning, analytics and application data workloads. MinIO is a High Performance Object Storage released under GNU Affero General Public License v3.0. It is API compatible with Amazon S3 cloud storage service. Use MinIO to build high performance infrastructure for machine learning, analytics and application data workloads. To learn more about what MinIO is doing for AI storage, go to [AI storage documentation](https://min.io/solutions/object-storage-for-ai).
This README provides quickstart instructions on running MinIO on bare metal hardware, including container-based installations. For Kubernetes environments, use the [MinIO Kubernetes Operator](https://github.com/minio/operator/blob/master/README.md). This README provides quickstart instructions on running MinIO on bare metal hardware, including container-based installations. For Kubernetes environments, use the [MinIO Kubernetes Operator](https://github.com/minio/operator/blob/master/README.md).
@@ -93,7 +93,6 @@ The following table lists supported architectures. Replace the `wget` URL with t
| 64-bit Intel/AMD | <https://dl.min.io/server/minio/release/linux-amd64/minio> | | 64-bit Intel/AMD | <https://dl.min.io/server/minio/release/linux-amd64/minio> |
| 64-bit ARM | <https://dl.min.io/server/minio/release/linux-arm64/minio> | | 64-bit ARM | <https://dl.min.io/server/minio/release/linux-arm64/minio> |
| 64-bit PowerPC LE (ppc64le) | <https://dl.min.io/server/minio/release/linux-ppc64le/minio> | | 64-bit PowerPC LE (ppc64le) | <https://dl.min.io/server/minio/release/linux-ppc64le/minio> |
| IBM Z-Series (S390X) | <https://dl.min.io/server/minio/release/linux-s390x/minio> |
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Console, an embedded web-based object browser built into MinIO Server. Point a web browser running on the host machine to <http://127.0.0.1:9000> and log in with the root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server. The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Console, an embedded web-based object browser built into MinIO Server. Point a web browser running on the host machine to <http://127.0.0.1:9000> and log in with the root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
@@ -8,7 +8,7 @@
# #
# This script assumes that LDAP server is at: # This script assumes that LDAP server is at:
# #
# `localhost:1389` # `localhost:389`
# #
# if this is not the case, set the environment variable # if this is not the case, set the environment variable
# `_MINIO_LDAP_TEST_SERVER`. # `_MINIO_LDAP_TEST_SERVER`.
@@ -41,7 +41,7 @@ __init__() {
fi fi
if [ -z "$_MINIO_LDAP_TEST_SERVER" ]; then if [ -z "$_MINIO_LDAP_TEST_SERVER" ]; then
export _MINIO_LDAP_TEST_SERVER=localhost:1389 export _MINIO_LDAP_TEST_SERVER=localhost:389
echo "Using default LDAP endpoint: $_MINIO_LDAP_TEST_SERVER" echo "Using default LDAP endpoint: $_MINIO_LDAP_TEST_SERVER"
fi fi
@@ -58,7 +58,7 @@ create_iam_content_in_old_minio() {
mc alias set old-minio http://localhost:9000 minioadmin minioadmin mc alias set old-minio http://localhost:9000 minioadmin minioadmin
mc ready old-minio mc ready old-minio
mc idp ldap add old-minio \ mc idp ldap add old-minio \
server_addr=localhost:1389 \ server_addr=localhost:389 \
server_insecure=on \ server_insecure=on \
lookup_bind_dn=cn=admin,dc=min,dc=io \ lookup_bind_dn=cn=admin,dc=min,dc=io \
lookup_bind_password=admin \ lookup_bind_password=admin \
+137
View File
@@ -0,0 +1,137 @@
#!/bin/bash
if [ -n "$TEST_DEBUG" ]; then
set -x
fi
WORK_DIR="$PWD/.verify-$RANDOM"
MINIO_CONFIG_DIR="$WORK_DIR/.minio"
MINIO=("$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" server)
if [ ! -x "$PWD/minio" ]; then
echo "minio executable binary not found in current directory"
exit 1
fi
if [ ! -x "$PWD/minio" ]; then
echo "minio executable binary not found in current directory"
exit 1
fi
trap 'catch $LINENO' ERR
function purge() {
rm -rf "$1"
}
# shellcheck disable=SC2120
catch() {
if [ $# -ne 0 ]; then
echo "error on line $1"
fi
echo "Cleaning up instances of MinIO"
pkill minio || true
pkill -9 minio || true
purge "$WORK_DIR"
if [ $# -ne 0 ]; then
exit $#
fi
}
catch
function gen_put_request() {
hdr_sleep=$1
body_sleep=$2
echo "PUT /testbucket/testobject HTTP/1.1"
sleep $hdr_sleep
echo "Host: foo-header"
echo "User-Agent: curl/8.2.1"
echo "Accept: */*"
echo "Content-Length: 30"
echo ""
sleep $body_sleep
echo "random line 0"
echo "random line 1"
echo ""
echo ""
}
function send_put_object_request() {
hdr_timeout=$1
body_timeout=$2
start=$(date +%s)
timeout 5m bash -c "gen_put_request $hdr_timeout $body_timeout | netcat 127.0.0.1 $start_port | read" || return -1
[ $(($(date +%s) - start)) -gt $((srv_hdr_timeout + srv_idle_timeout + 1)) ] && return -1
return 0
}
function test_minio_with_timeout() {
start_port=$1
export MINIO_ROOT_USER=minio
export MINIO_ROOT_PASSWORD=minio123
export MC_HOST_minio="http://minio:minio123@127.0.0.1:${start_port}/"
export MINIO_CI_CD=1
mkdir ${WORK_DIR}
C_PWD=${PWD}
if [ ! -x "$PWD/mc" ]; then
MC_BUILD_DIR="mc-$RANDOM"
if ! git clone --quiet https://github.com/minio/mc "$MC_BUILD_DIR"; then
echo "failed to download https://github.com/minio/mc"
purge "${MC_BUILD_DIR}"
exit 1
fi
(cd "${MC_BUILD_DIR}" && go build -o "$C_PWD/mc")
# remove mc source.
purge "${MC_BUILD_DIR}"
fi
"${MINIO[@]}" --address ":$start_port" --read-header-timeout ${srv_hdr_timeout}s --idle-timeout ${srv_idle_timeout}s "${WORK_DIR}/disk/" >"${WORK_DIR}/server1.log" 2>&1 &
pid=$!
disown $pid
sleep 1
if ! ps -p ${pid} 1>&2 >/dev/null; then
echo "server1 log:"
cat "${WORK_DIR}/server1.log"
echo "FAILED"
purge "$WORK_DIR"
exit 1
fi
set -e
"${PWD}/mc" mb minio/testbucket
"${PWD}/mc" anonymous set public minio/testbucket
# slow header writing
send_put_object_request 20 0 && exit -1
"${PWD}/mc" stat minio/testbucket/testobject && exit -1
# quick header write and slow bodywrite
send_put_object_request 0 40 && exit -1
"${PWD}/mc" stat minio/testbucket/testobject && exit -1
# quick header and body write
send_put_object_request 1 1 || exit -1
"${PWD}/mc" stat minio/testbucket/testobject || exit -1
}
function main() {
export start_port=$(shuf -i 10000-65000 -n 1)
export srv_hdr_timeout=5
export srv_idle_timeout=5
export -f gen_put_request
test_minio_with_timeout ${start_port}
}
main "$@"
+5 -5
View File
@@ -38,6 +38,7 @@ import (
objectlock "github.com/minio/minio/internal/bucket/object/lock" objectlock "github.com/minio/minio/internal/bucket/object/lock"
"github.com/minio/minio/internal/bucket/versioning" "github.com/minio/minio/internal/bucket/versioning"
"github.com/minio/minio/internal/event" "github.com/minio/minio/internal/event"
xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/kms" "github.com/minio/minio/internal/kms"
"github.com/minio/mux" "github.com/minio/mux"
"github.com/minio/pkg/v3/policy" "github.com/minio/pkg/v3/policy"
@@ -980,7 +981,6 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *
rpt.SetStatus(bucket, "", err) rpt.SetStatus(bucket, "", err)
continue continue
} }
} }
rptData, err := json.Marshal(rpt.BucketMetaImportErrs) rptData, err := json.Marshal(rpt.BucketMetaImportErrs)
@@ -1039,7 +1039,7 @@ func (a adminAPIHandlers) ReplicationDiffHandler(w http.ResponseWriter, r *http.
} }
if len(diffCh) == 0 { if len(diffCh) == 0 {
// Flush if nothing is queued // Flush if nothing is queued
w.(http.Flusher).Flush() xhttp.Flush(w)
} }
case <-keepAliveTicker.C: case <-keepAliveTicker.C:
if len(diffCh) > 0 { if len(diffCh) > 0 {
@@ -1048,7 +1048,7 @@ func (a adminAPIHandlers) ReplicationDiffHandler(w http.ResponseWriter, r *http.
if _, err := w.Write([]byte(" ")); err != nil { if _, err := w.Write([]byte(" ")); err != nil {
return return
} }
w.(http.Flusher).Flush() xhttp.Flush(w)
case <-ctx.Done(): case <-ctx.Done():
return return
} }
@@ -1098,7 +1098,7 @@ func (a adminAPIHandlers) ReplicationMRFHandler(w http.ResponseWriter, r *http.R
} }
if len(mrfCh) == 0 { if len(mrfCh) == 0 {
// Flush if nothing is queued // Flush if nothing is queued
w.(http.Flusher).Flush() xhttp.Flush(w)
} }
case <-keepAliveTicker.C: case <-keepAliveTicker.C:
if len(mrfCh) > 0 { if len(mrfCh) > 0 {
@@ -1107,7 +1107,7 @@ func (a adminAPIHandlers) ReplicationMRFHandler(w http.ResponseWriter, r *http.R
if _, err := w.Write([]byte(" ")); err != nil { if _, err := w.Write([]byte(" ")); err != nil {
return return
} }
w.(http.Flusher).Flush() xhttp.Flush(w)
case <-ctx.Done(): case <-ctx.Done():
return return
} }
-2
View File
@@ -125,7 +125,6 @@ func addOrUpdateIDPHandler(ctx context.Context, w http.ResponseWriter, r *http.R
} }
if err = validateConfig(ctx, 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) {
// If we got an LDAP validation error, we need to send appropriate // If we got an LDAP validation error, we need to send appropriate
@@ -416,7 +415,6 @@ func (a adminAPIHandlers) DeleteIdentityProviderCfg(w http.ResponseWriter, r *ht
return return
} }
if err = validateConfig(ctx, 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) {
// If we got an LDAP validation error, we need to send appropriate // If we got an LDAP validation error, we need to send appropriate
+2 -5
View File
@@ -214,10 +214,7 @@ func (a adminAPIHandlers) AddServiceAccountLDAP(w http.ResponseWriter, r *http.R
} }
// Check if we are creating svc account for request sender. // Check if we are creating svc account for request sender.
isSvcAccForRequestor := false isSvcAccForRequestor := targetUser == requestorUser || targetUser == requestorParentUser
if targetUser == requestorUser || targetUser == requestorParentUser {
isSvcAccForRequestor = true
}
var ( var (
targetGroups []string targetGroups []string
@@ -345,7 +342,7 @@ func (a adminAPIHandlers) AddServiceAccountLDAP(w http.ResponseWriter, r *http.R
Name: newCred.Name, Name: newCred.Name,
Description: newCred.Description, Description: newCred.Description,
Claims: opts.claims, Claims: opts.claims,
SessionPolicy: createReq.Policy, SessionPolicy: madmin.SRSessionPolicy(createReq.Policy),
Status: auth.AccountOn, Status: auth.AccountOn,
Expiration: createReq.Expiration, Expiration: createReq.Expiration,
}, },
+6 -6
View File
@@ -258,8 +258,8 @@ func (a adminAPIHandlers) RebalanceStart(w http.ResponseWriter, r *http.Request)
// concurrent rebalance-start commands. // concurrent rebalance-start commands.
if ep := globalEndpoints[0].Endpoints[0]; !ep.IsLocal { if ep := globalEndpoints[0].Endpoints[0]; !ep.IsLocal {
for nodeIdx, proxyEp := range globalProxyEndpoints { for nodeIdx, proxyEp := range globalProxyEndpoints {
if proxyEp.Endpoint.Host == ep.Host { if proxyEp.Host == ep.Host {
if proxyRequestByNodeIndex(ctx, w, r, nodeIdx) { if proxied, success := proxyRequestByNodeIndex(ctx, w, r, nodeIdx, false); proxied && success {
return return
} }
} }
@@ -329,8 +329,8 @@ func (a adminAPIHandlers) RebalanceStatus(w http.ResponseWriter, r *http.Request
// pools may temporarily have out of date info on the others. // pools may temporarily have out of date info on the others.
if ep := globalEndpoints[0].Endpoints[0]; !ep.IsLocal { if ep := globalEndpoints[0].Endpoints[0]; !ep.IsLocal {
for nodeIdx, proxyEp := range globalProxyEndpoints { for nodeIdx, proxyEp := range globalProxyEndpoints {
if proxyEp.Endpoint.Host == ep.Host { if proxyEp.Host == ep.Host {
if proxyRequestByNodeIndex(ctx, w, r, nodeIdx) { if proxied, success := proxyRequestByNodeIndex(ctx, w, r, nodeIdx, false); proxied && success {
return return
} }
} }
@@ -383,8 +383,8 @@ func proxyDecommissionRequest(ctx context.Context, defaultEndPoint Endpoint, w h
return return
} }
for nodeIdx, proxyEp := range globalProxyEndpoints { for nodeIdx, proxyEp := range globalProxyEndpoints {
if proxyEp.Endpoint.Host == host && !proxyEp.IsLocal { if proxyEp.Host == host && !proxyEp.IsLocal {
if proxyRequestByNodeIndex(ctx, w, r, nodeIdx) { if proxied, success := proxyRequestByNodeIndex(ctx, w, r, nodeIdx, false); proxied && success {
return true return true
} }
} }
+128 -69
View File
@@ -37,6 +37,7 @@ import (
"github.com/minio/madmin-go/v3" "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/config/dns" "github.com/minio/minio/internal/config/dns"
"github.com/minio/minio/internal/logger"
"github.com/minio/mux" "github.com/minio/mux"
xldap "github.com/minio/pkg/v3/ldap" xldap "github.com/minio/pkg/v3/ldap"
"github.com/minio/pkg/v3/policy" "github.com/minio/pkg/v3/policy"
@@ -65,6 +66,17 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
return return
} }
// This API only supports removal of internal users not service accounts.
ok, _, err = globalIAMSys.IsServiceAccount(accessKey)
if err != nil {
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 // When the user is root credential you are not allowed to
// remove the root user. Also you cannot delete yourself. // remove the root user. Also you cannot delete yourself.
if accessKey == globalActiveCred.AccessKey || accessKey == cred.AccessKey { if accessKey == globalActiveCred.AccessKey || accessKey == cred.AccessKey {
@@ -185,12 +197,7 @@ func (a adminAPIHandlers) GetUserInfo(w http.ResponseWriter, r *http.Request) {
return return
} }
checkDenyOnly := false checkDenyOnly := name == cred.AccessKey
if name == cred.AccessKey {
// Check that there is no explicit deny - otherwise it's allowed
// to view one's own info.
checkDenyOnly = true
}
if !globalIAMSys.IsAllowed(policy.Args{ if !globalIAMSys.IsAllowed(policy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
@@ -481,12 +488,7 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
return return
} }
checkDenyOnly := false checkDenyOnly := accessKey == cred.AccessKey
if accessKey == cred.AccessKey {
// Check that there is no explicit deny - otherwise it's allowed
// to change one's own password.
checkDenyOnly = true
}
if !globalIAMSys.IsAllowed(policy.Args{ if !globalIAMSys.IsAllowed(policy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
@@ -677,10 +679,7 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
} }
// Check if we are creating svc account for request sender. // Check if we are creating svc account for request sender.
isSvcAccForRequestor := false isSvcAccForRequestor := targetUser == requestorUser || targetUser == requestorParentUser
if targetUser == requestorUser || targetUser == requestorParentUser {
isSvcAccForRequestor = true
}
// If we are creating svc account for request sender, ensure // If we are creating svc account for request sender, ensure
// that targetUser is a real user (i.e. not derived // that targetUser is a real user (i.e. not derived
@@ -771,7 +770,7 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
Name: newCred.Name, Name: newCred.Name,
Description: newCred.Description, Description: newCred.Description,
Claims: opts.claims, Claims: opts.claims,
SessionPolicy: createReq.Policy, SessionPolicy: madmin.SRSessionPolicy(createReq.Policy),
Status: auth.AccountOn, Status: auth.AccountOn,
Expiration: createReq.Expiration, Expiration: createReq.Expiration,
}, },
@@ -895,7 +894,7 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re
Status: opts.status, Status: opts.status,
Name: opts.name, Name: opts.name,
Description: opts.description, Description: opts.description,
SessionPolicy: updateReq.NewPolicy, SessionPolicy: madmin.SRSessionPolicy(updateReq.NewPolicy),
Expiration: updateReq.NewExpiration, Expiration: updateReq.NewExpiration,
}, },
}, },
@@ -1244,6 +1243,7 @@ func (a adminAPIHandlers) ListAccessKeysBulk(w http.ResponseWriter, r *http.Requ
for user := range users { for user := range users {
checkedUserList = append(checkedUserList, user) checkedUserList = append(checkedUserList, user)
} }
checkedUserList = append(checkedUserList, globalActiveCred.AccessKey)
} else { } else {
for _, user := range users { for _, user := range users {
// Validate the user // Validate the user
@@ -1441,7 +1441,12 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
var buf []byte var buf []byte
switch { switch {
case accountName == globalActiveCred.AccessKey: case accountName == globalActiveCred.AccessKey || newGlobalAuthZPluginFn() != nil:
// For owner account and when plugin authZ is configured always set
// effective policy as `consoleAdmin`.
//
// In the latter case, we let the UI render everything, but individual
// actions would fail if not permitted by the external authZ service.
for _, policy := range policy.DefaultPolicies { for _, policy := range policy.DefaultPolicies {
if policy.Name == "consoleAdmin" { if policy.Name == "consoleAdmin" {
effectivePolicy = policy.Definition effectivePolicy = policy.Definition
@@ -1469,8 +1474,8 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
return return
} }
effectivePolicy = globalIAMSys.GetCombinedPolicy(policies...) effectivePolicy = globalIAMSys.GetCombinedPolicy(policies...)
} }
buf, err = json.MarshalIndent(effectivePolicy, "", " ") buf, err = json.MarshalIndent(effectivePolicy, "", " ")
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
@@ -1562,6 +1567,7 @@ func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Reques
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errTooManyPolicies), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errTooManyPolicies), r.URL)
return return
} }
setReqInfoPolicyName(ctx, name)
policyDoc, err := globalIAMSys.InfoPolicy(name) policyDoc, err := globalIAMSys.InfoPolicy(name)
if err != nil { if err != nil {
@@ -1665,6 +1671,7 @@ func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Requ
vars := mux.Vars(r) vars := mux.Vars(r)
policyName := vars["name"] policyName := vars["name"]
setReqInfoPolicyName(ctx, policyName)
if err := globalIAMSys.DeletePolicy(ctx, policyName, true); err != nil { if err := globalIAMSys.DeletePolicy(ctx, policyName, true); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
@@ -1697,6 +1704,13 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
return return
} }
setReqInfoPolicyName(ctx, policyName)
// Reject policy names with commas.
if strings.Contains(policyName, ",") {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrPolicyInvalidName), r.URL)
return
}
// Error out if Content-Length is missing. // Error out if Content-Length is missing.
if r.ContentLength <= 0 { if r.ContentLength <= 0 {
@@ -1762,6 +1776,7 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http
policyName := vars["policyName"] policyName := vars["policyName"]
entityName := vars["userOrGroup"] entityName := vars["userOrGroup"]
isGroup := vars["isGroup"] == "true" isGroup := vars["isGroup"] == "true"
setReqInfoPolicyName(ctx, policyName)
if !isGroup { if !isGroup {
ok, _, err := globalIAMSys.IsTempUser(entityName) ok, _, err := globalIAMSys.IsTempUser(entityName)
@@ -1847,7 +1862,7 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http
})) }))
} }
// ListPolicyMappingEntities - GET /minio/admin/v3/idp/builtin/polciy-entities?policy=xxx&user=xxx&group=xxx // ListPolicyMappingEntities - GET /minio/admin/v3/idp/builtin/policy-entities?policy=xxx&user=xxx&group=xxx
func (a adminAPIHandlers) ListPolicyMappingEntities(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) ListPolicyMappingEntities(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@@ -1949,6 +1964,7 @@ func (a adminAPIHandlers) AttachDetachPolicyBuiltin(w http.ResponseWriter, r *ht
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return return
} }
setReqInfoPolicyName(ctx, strings.Join(addedOrRemoved, ","))
respBody := madmin.PolicyAssociationResp{ respBody := madmin.PolicyAssociationResp{
UpdatedAt: updatedAt, UpdatedAt: updatedAt,
@@ -1974,6 +1990,84 @@ func (a adminAPIHandlers) AttachDetachPolicyBuiltin(w http.ResponseWriter, r *ht
writeSuccessResponseJSON(w, encryptedData) writeSuccessResponseJSON(w, encryptedData)
} }
// RevokeTokens - POST /minio/admin/v3/revoke-tokens/{userProvider}
func (a adminAPIHandlers) RevokeTokens(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Get current object layer instance.
objectAPI := newObjectLayerFn()
if objectAPI == nil || globalNotificationSys == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return
}
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
if s3Err != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
return
}
userProvider := mux.Vars(r)["userProvider"]
user := r.Form.Get("user")
tokenRevokeType := r.Form.Get("tokenRevokeType")
fullRevoke := r.Form.Get("fullRevoke") == "true"
isTokenSelfRevoke := user == ""
if !isTokenSelfRevoke {
var err error
user, err = getUserWithProvider(ctx, userProvider, user, false)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
}
if (user != "" && tokenRevokeType == "" && !fullRevoke) || (tokenRevokeType != "" && fullRevoke) {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
return
}
adminPrivilege := globalIAMSys.IsAllowed(policy.Args{
AccountName: cred.AccessKey,
Groups: cred.Groups,
Action: policy.RemoveServiceAccountAdminAction,
ConditionValues: getConditionValues(r, "", cred),
IsOwner: owner,
Claims: cred.Claims,
})
if !adminPrivilege || isTokenSelfRevoke {
parentUser := cred.AccessKey
if cred.ParentUser != "" {
parentUser = cred.ParentUser
}
if !isTokenSelfRevoke && user != parentUser {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return
}
user = parentUser
}
// Infer token revoke type from the request if requestor is STS.
if isTokenSelfRevoke && tokenRevokeType == "" && !fullRevoke {
if cred.IsTemp() {
tokenRevokeType, _ = cred.Claims[tokenRevokeTypeClaim].(string)
}
if tokenRevokeType == "" {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNoTokenRevokeType), r.URL)
return
}
}
err := globalIAMSys.RevokeTokens(ctx, user, tokenRevokeType)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
writeSuccessNoContent(w)
}
const ( const (
allPoliciesFile = "policies.json" allPoliciesFile = "policies.json"
allUsersFile = "users.json" allUsersFile = "users.json"
@@ -2003,6 +2097,7 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
// Get current object layer instance. // Get current object layer instance.
objectAPI, _ := validateAdminReq(ctx, w, r, policy.ExportIAMAction) objectAPI, _ := validateAdminReq(ctx, w, r, policy.ExportIAMAction)
if objectAPI == nil { if objectAPI == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return return
} }
// Initialize a zip writer which will provide a zipped content // Initialize a zip writer which will provide a zipped content
@@ -2139,7 +2234,7 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
SecretKey: acc.Credentials.SecretKey, SecretKey: acc.Credentials.SecretKey,
Groups: acc.Credentials.Groups, Groups: acc.Credentials.Groups,
Claims: claims, Claims: claims,
SessionPolicy: json.RawMessage(policyJSON), SessionPolicy: policyJSON,
Status: acc.Credentials.Status, Status: acc.Credentials.Status,
Name: sa.Name, Name: sa.Name,
Description: sa.Description, Description: sa.Description,
@@ -2225,17 +2320,13 @@ func (a adminAPIHandlers) ImportIAMV2(w http.ResponseWriter, r *http.Request) {
func (a adminAPIHandlers) importIAM(w http.ResponseWriter, r *http.Request, apiVer string) { func (a adminAPIHandlers) importIAM(w http.ResponseWriter, r *http.Request, apiVer string) {
ctx := r.Context() ctx := r.Context()
// Get current object layer instance. // Validate signature, permissions and get current object layer instance.
objectAPI := newObjectLayerFn() objectAPI, _ := validateAdminReq(ctx, w, r, policy.ImportIAMAction)
if objectAPI == nil || globalNotificationSys == nil { if objectAPI == nil || globalNotificationSys == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return return
} }
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
if s3Err != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
return
}
data, err := io.ReadAll(r.Body) data, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
@@ -2253,7 +2344,6 @@ func (a adminAPIHandlers) importIAM(w http.ResponseWriter, r *http.Request, apiV
// import policies first // import policies first
{ {
f, err := zr.Open(pathJoin(iamAssetsDir, allPoliciesFile)) f, err := zr.Open(pathJoin(iamAssetsDir, allPoliciesFile))
switch { switch {
case errors.Is(err, os.ErrNotExist): case errors.Is(err, os.ErrNotExist):
@@ -2325,44 +2415,17 @@ func (a adminAPIHandlers) importIAM(w http.ResponseWriter, r *http.Request, apiV
return return
} }
if (cred.IsTemp() || cred.IsServiceAccount()) && cred.ParentUser == accessKey {
// Incoming access key matches parent user then we should
// reject password change requests.
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAddUserInvalidArgument, err, allUsersFile, accessKey), r.URL)
return
}
// Check if accessKey has beginning and end space characters, this only applies to new users. // Check if accessKey has beginning and end space characters, this only applies to new users.
if !exists && hasSpaceBE(accessKey) { if !exists && hasSpaceBE(accessKey) {
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminResourceInvalidArgument, err, allUsersFile, accessKey), r.URL) writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminResourceInvalidArgument, err, allUsersFile, accessKey), r.URL)
return return
} }
checkDenyOnly := false
if accessKey == cred.AccessKey {
// Check that there is no explicit deny - otherwise it's allowed
// to change one's own password.
checkDenyOnly = true
}
if !globalIAMSys.IsAllowed(policy.Args{
AccountName: cred.AccessKey,
Groups: cred.Groups,
Action: policy.CreateUserAdminAction,
ConditionValues: getConditionValues(r, "", cred),
IsOwner: owner,
Claims: cred.Claims,
DenyOnly: checkDenyOnly,
}) {
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAccessDenied, err, allUsersFile, accessKey), r.URL)
return
}
if _, err = globalIAMSys.CreateUser(ctx, accessKey, ureq); err != nil { if _, err = globalIAMSys.CreateUser(ctx, accessKey, ureq); err != nil {
failed.Users = append(failed.Users, madmin.IAMErrEntity{Name: accessKey, Error: err}) failed.Users = append(failed.Users, madmin.IAMErrEntity{Name: accessKey, Error: err})
} else { } else {
added.Users = append(added.Users, accessKey) added.Users = append(added.Users, accessKey)
} }
} }
} }
} }
@@ -2456,17 +2519,6 @@ func (a adminAPIHandlers) importIAM(w http.ResponseWriter, r *http.Request, apiV
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
return return
} }
if !globalIAMSys.IsAllowed(policy.Args{
AccountName: cred.AccessKey,
Groups: cred.Groups,
Action: policy.CreateServiceAccountAdminAction,
ConditionValues: getConditionValues(r, "", cred),
IsOwner: owner,
Claims: cred.Claims,
}) {
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAccessDenied, err, allSvcAcctsFile, user), r.URL)
return
}
updateReq := true updateReq := true
_, _, err = globalIAMSys.GetServiceAccount(ctx, svcAcctReq.AccessKey) _, _, err = globalIAMSys.GetServiceAccount(ctx, svcAcctReq.AccessKey)
if err != nil { if err != nil {
@@ -2686,7 +2738,7 @@ func addExpirationToCondValues(exp *time.Time, condValues map[string][]string) e
if exp == nil || exp.IsZero() || exp.Equal(timeSentinel) { if exp == nil || exp.IsZero() || exp.Equal(timeSentinel) {
return nil return nil
} }
dur := exp.Sub(time.Now()) dur := time.Until(*exp)
if dur <= 0 { if dur <= 0 {
return errors.New("unsupported expiration time") return errors.New("unsupported expiration time")
} }
@@ -2795,3 +2847,10 @@ func commonAddServiceAccount(r *http.Request, ldap bool) (context.Context, auth.
return ctx, cred, opts, createReq, targetUser, APIError{} return ctx, cred, opts, createReq, targetUser, APIError{}
} }
// setReqInfoPolicyName will set the given policyName as a tag on the context's request info,
// so that it appears in audit logs.
func setReqInfoPolicyName(ctx context.Context, policyName string) {
reqInfo := logger.GetReqInfo(ctx)
reqInfo.SetTags("policyName", policyName)
}
+7 -1
View File
@@ -160,7 +160,7 @@ func (s *TestSuiteIAM) SetUpSuite(c *check) {
} }
func (s *TestSuiteIAM) RestartIAMSuite(c *check) { func (s *TestSuiteIAM) RestartIAMSuite(c *check) {
s.TestSuiteCommon.RestartTestServer(c) s.RestartTestServer(c)
s.iamSetup(c) s.iamSetup(c)
} }
@@ -716,6 +716,12 @@ func (s *TestSuiteIAM) TestCannedPolicies(c *check) {
c.Fatalf("policy info err: %v", err) c.Fatalf("policy info err: %v", err)
} }
// Check that policy with comma is rejected.
err = s.adm.AddCannedPolicy(ctx, "invalid,policy", policyBytes)
if err == nil {
c.Fatalf("invalid policy created successfully")
}
infoStr := string(info) infoStr := string(info)
if !strings.Contains(infoStr, `"s3:PutObject"`) || !strings.Contains(infoStr, ":"+bucket+"/") { if !strings.Contains(infoStr, `"s3:PutObject"`) || !strings.Contains(infoStr, ":"+bucket+"/") {
c.Fatalf("policy contains unexpected content!") c.Fatalf("policy contains unexpected content!")
+80 -53
View File
@@ -49,6 +49,7 @@ import (
"github.com/klauspost/compress/zip" "github.com/klauspost/compress/zip"
"github.com/minio/madmin-go/v3" "github.com/minio/madmin-go/v3"
"github.com/minio/madmin-go/v3/estream" "github.com/minio/madmin-go/v3/estream"
"github.com/minio/madmin-go/v3/logger/log"
"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/dsync" "github.com/minio/minio/internal/dsync"
@@ -59,7 +60,6 @@ import (
"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"
"github.com/minio/pkg/v3/logger/message/log"
xnet "github.com/minio/pkg/v3/net" xnet "github.com/minio/pkg/v3/net"
"github.com/minio/pkg/v3/policy" "github.com/minio/pkg/v3/policy"
"github.com/secure-io/sio-go" "github.com/secure-io/sio-go"
@@ -93,11 +93,18 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
return return
} }
if globalInplaceUpdateDisabled || currentReleaseTime.IsZero() { if globalInplaceUpdateDisabled {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
return return
} }
if currentReleaseTime.IsZero() || currentReleaseTime.Equal(timeSentinel) {
apiErr := errorCodes.ToAPIErr(ErrMethodNotAllowed)
apiErr.Description = fmt.Sprintf("unable to perform in-place update, release time is unrecognized: %s", currentReleaseTime)
writeErrorResponseJSON(ctx, w, apiErr, r.URL)
return
}
vars := mux.Vars(r) vars := mux.Vars(r)
updateURL := vars["updateURL"] updateURL := vars["updateURL"]
dryRun := r.Form.Get("dry-run") == "true" dryRun := r.Form.Get("dry-run") == "true"
@@ -110,6 +117,11 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
} }
} }
local := globalLocalNodeName
if local == "" {
local = "127.0.0.1"
}
u, err := url.Parse(updateURL) u, err := url.Parse(updateURL)
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
@@ -128,6 +140,39 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
return return
} }
updateStatus := madmin.ServerUpdateStatusV2{
DryRun: dryRun,
Results: make([]madmin.ServerPeerUpdateStatus, 0, len(globalNotificationSys.allPeerClients)),
}
peerResults := make(map[string]madmin.ServerPeerUpdateStatus, len(globalNotificationSys.allPeerClients))
failedClients := make(map[int]bool, len(globalNotificationSys.allPeerClients))
if lrTime.Sub(currentReleaseTime) <= 0 {
updateStatus.Results = append(updateStatus.Results, madmin.ServerPeerUpdateStatus{
Host: local,
Err: fmt.Sprintf("server is running the latest version: %s", Version),
CurrentVersion: Version,
})
for _, client := range globalNotificationSys.peerClients {
updateStatus.Results = append(updateStatus.Results, madmin.ServerPeerUpdateStatus{
Host: client.String(),
Err: fmt.Sprintf("server is running the latest version: %s", Version),
CurrentVersion: Version,
})
}
// Marshal API response
jsonBytes, err := json.Marshal(updateStatus)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
writeSuccessResponseJSON(w, jsonBytes)
return
}
u.Path = path.Dir(u.Path) + SlashSeparator + releaseInfo u.Path = path.Dir(u.Path) + SlashSeparator + releaseInfo
// Download Binary Once // Download Binary Once
binC, bin, err := downloadBinary(u, mode) binC, bin, err := downloadBinary(u, mode)
@@ -137,16 +182,6 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
return return
} }
updateStatus := madmin.ServerUpdateStatusV2{DryRun: dryRun}
peerResults := make(map[string]madmin.ServerPeerUpdateStatus)
local := globalLocalNodeName
if local == "" {
local = "127.0.0.1"
}
failedClients := make(map[int]struct{})
if globalIsDistErasure { if globalIsDistErasure {
// Push binary to other servers // Push binary to other servers
for idx, nerr := range globalNotificationSys.VerifyBinary(ctx, u, sha256Sum, releaseInfo, binC) { for idx, nerr := range globalNotificationSys.VerifyBinary(ctx, u, sha256Sum, releaseInfo, binC) {
@@ -156,7 +191,7 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
Err: nerr.Err.Error(), Err: nerr.Err.Error(),
CurrentVersion: Version, CurrentVersion: Version,
} }
failedClients[idx] = struct{}{} failedClients[idx] = true
} else { } else {
peerResults[nerr.Host.String()] = madmin.ServerPeerUpdateStatus{ peerResults[nerr.Host.String()] = madmin.ServerPeerUpdateStatus{
Host: nerr.Host.String(), Host: nerr.Host.String(),
@@ -167,25 +202,17 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
} }
} }
if lrTime.Sub(currentReleaseTime) > 0 { if err = verifyBinary(u, sha256Sum, releaseInfo, mode, bytes.NewReader(bin)); err != nil {
if err = verifyBinary(u, sha256Sum, releaseInfo, mode, bytes.NewReader(bin)); err != nil { peerResults[local] = madmin.ServerPeerUpdateStatus{
peerResults[local] = madmin.ServerPeerUpdateStatus{ Host: local,
Host: local, Err: err.Error(),
Err: err.Error(), CurrentVersion: Version,
CurrentVersion: Version,
}
} else {
peerResults[local] = madmin.ServerPeerUpdateStatus{
Host: local,
CurrentVersion: Version,
UpdatedVersion: lrTime.Format(MinioReleaseTagTimeLayout),
}
} }
} else { } else {
peerResults[local] = madmin.ServerPeerUpdateStatus{ peerResults[local] = madmin.ServerPeerUpdateStatus{
Host: local, Host: local,
Err: fmt.Sprintf("server is already running the latest version: %s", Version),
CurrentVersion: Version, CurrentVersion: Version,
UpdatedVersion: lrTime.Format(MinioReleaseTagTimeLayout),
} }
} }
@@ -193,8 +220,7 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
if globalIsDistErasure { if globalIsDistErasure {
ng := WithNPeers(len(globalNotificationSys.peerClients)) ng := WithNPeers(len(globalNotificationSys.peerClients))
for idx, client := range globalNotificationSys.peerClients { for idx, client := range globalNotificationSys.peerClients {
_, ok := failedClients[idx] if failedClients[idx] {
if ok {
continue continue
} }
client := client client := client
@@ -240,14 +266,14 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
startTime := time.Now().Add(restartUpdateDelay) startTime := time.Now().Add(restartUpdateDelay)
ng := WithNPeers(len(globalNotificationSys.peerClients)) ng := WithNPeers(len(globalNotificationSys.peerClients))
for idx, client := range globalNotificationSys.peerClients { for idx, client := range globalNotificationSys.peerClients {
_, ok := failedClients[idx] if failedClients[idx] {
if ok {
continue continue
} }
client := client client := client
ng.Go(ctx, func() error { ng.Go(ctx, func() error {
prs, ok := peerResults[client.String()] prs, ok := peerResults[client.String()]
if ok && prs.CurrentVersion != prs.UpdatedVersion && prs.UpdatedVersion != "" { // We restart only on success, not for any failures.
if ok && prs.Err == "" {
return client.SignalService(serviceRestart, "", dryRun, &startTime) return client.SignalService(serviceRestart, "", dryRun, &startTime)
} }
return nil return nil
@@ -284,7 +310,9 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
writeSuccessResponseJSON(w, jsonBytes) writeSuccessResponseJSON(w, jsonBytes)
if !dryRun { if !dryRun {
if lrTime.Sub(currentReleaseTime) > 0 { prs, ok := peerResults[local]
// We restart only on success, not for any failures.
if ok && prs.Err == "" {
globalServiceSignalCh <- serviceRestart globalServiceSignalCh <- serviceRestart
} }
} }
@@ -801,7 +829,7 @@ func (a adminAPIHandlers) MetricsHandler(w http.ResponseWriter, r *http.Request)
} }
// Flush before waiting for next... // Flush before waiting for next...
w.(http.Flusher).Flush() xhttp.Flush(w)
select { select {
case <-ticker.C: case <-ticker.C:
@@ -1292,7 +1320,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
} }
// Analyze the heal token and route the request accordingly // Analyze the heal token and route the request accordingly
token, success := proxyRequestByToken(ctx, w, r, hip.clientToken) token, _, success := proxyRequestByToken(ctx, w, r, hip.clientToken, false)
if success { if success {
return return
} }
@@ -1331,7 +1359,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
if _, err := w.Write([]byte(" ")); err != nil { if _, err := w.Write([]byte(" ")); err != nil {
return return
} }
w.(http.Flusher).Flush() xhttp.Flush(w)
case hr := <-respCh: case hr := <-respCh:
switch hr.apiErr { switch hr.apiErr {
case noError: case noError:
@@ -1339,7 +1367,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
if _, err := w.Write(hr.respBytes); err != nil { if _, err := w.Write(hr.respBytes); err != nil {
return return
} }
w.(http.Flusher).Flush() xhttp.Flush(w)
} else { } else {
writeSuccessResponseJSON(w, hr.respBytes) writeSuccessResponseJSON(w, hr.respBytes)
} }
@@ -1366,7 +1394,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
if _, err := w.Write(errorRespJSON); err != nil { if _, err := w.Write(errorRespJSON); err != nil {
return return
} }
w.(http.Flusher).Flush() xhttp.Flush(w)
} }
break forLoop break forLoop
} }
@@ -1379,7 +1407,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
if exists && !nh.hasEnded() && len(nh.currentStatus.Items) > 0 { if exists && !nh.hasEnded() && len(nh.currentStatus.Items) > 0 {
clientToken := nh.clientToken clientToken := nh.clientToken
if globalIsDistErasure { if globalIsDistErasure {
clientToken = fmt.Sprintf("%s:%d", nh.clientToken, GetProxyEndpointLocalIndex(globalProxyEndpoints)) clientToken = fmt.Sprintf("%s%s%d", nh.clientToken, getKeySeparator(), GetProxyEndpointLocalIndex(globalProxyEndpoints))
} }
b, err := json.Marshal(madmin.HealStartSuccess{ b, err := json.Marshal(madmin.HealStartSuccess{
ClientToken: clientToken, ClientToken: clientToken,
@@ -1583,7 +1611,6 @@ func (a adminAPIHandlers) ClientDevNull(w http.ResponseWriter, r *http.Request)
if err != nil || ctx.Err() != nil || totalRx > 100*humanize.GiByte { if err != nil || ctx.Err() != nil || totalRx > 100*humanize.GiByte {
break break
} }
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
@@ -1812,7 +1839,7 @@ func (a adminAPIHandlers) ObjectSpeedTestHandler(w http.ResponseWriter, r *http.
return return
} }
} }
w.(http.Flusher).Flush() xhttp.Flush(w)
case result, ok := <-ch: case result, ok := <-ch:
if !ok { if !ok {
return return
@@ -1821,7 +1848,7 @@ func (a adminAPIHandlers) ObjectSpeedTestHandler(w http.ResponseWriter, r *http.
return return
} }
prevResult = result prevResult = result
w.(http.Flusher).Flush() xhttp.Flush(w)
} }
} }
} }
@@ -1930,7 +1957,7 @@ func (a adminAPIHandlers) DriveSpeedtestHandler(w http.ResponseWriter, r *http.R
if err := enc.Encode(madmin.DriveSpeedTestResult{}); err != nil { if err := enc.Encode(madmin.DriveSpeedTestResult{}); err != nil {
return return
} }
w.(http.Flusher).Flush() xhttp.Flush(w)
case result, ok := <-ch: case result, ok := <-ch:
if !ok { if !ok {
return return
@@ -1938,7 +1965,7 @@ func (a adminAPIHandlers) DriveSpeedtestHandler(w http.ResponseWriter, r *http.R
if err := enc.Encode(result); err != nil { if err := enc.Encode(result); err != nil {
return return
} }
w.(http.Flusher).Flush() xhttp.Flush(w)
} }
} }
} }
@@ -2055,7 +2082,7 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
grid.PutByteBuffer(entry) grid.PutByteBuffer(entry)
if len(traceCh) == 0 { if len(traceCh) == 0 {
// Flush if nothing is queued // Flush if nothing is queued
w.(http.Flusher).Flush() xhttp.Flush(w)
} }
case <-keepAliveTicker.C: case <-keepAliveTicker.C:
if len(traceCh) > 0 { if len(traceCh) > 0 {
@@ -2064,7 +2091,7 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
if _, err := w.Write([]byte(" ")); err != nil { if _, err := w.Write([]byte(" ")); err != nil {
return return
} }
w.(http.Flusher).Flush() xhttp.Flush(w)
case <-ctx.Done(): case <-ctx.Done():
return return
} }
@@ -2156,7 +2183,7 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque
grid.PutByteBuffer(log) grid.PutByteBuffer(log)
if len(logCh) == 0 { if len(logCh) == 0 {
// Flush if nothing is queued // Flush if nothing is queued
w.(http.Flusher).Flush() xhttp.Flush(w)
} }
case <-keepAliveTicker.C: case <-keepAliveTicker.C:
if len(logCh) > 0 { if len(logCh) > 0 {
@@ -2165,7 +2192,7 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque
if _, err := w.Write([]byte(" ")); err != nil { if _, err := w.Write([]byte(" ")); err != nil {
return return
} }
w.(http.Flusher).Flush() xhttp.Flush(w)
case <-ctx.Done(): case <-ctx.Done():
return return
} }
@@ -2649,7 +2676,7 @@ func fetchHealthInfo(healthCtx context.Context, objectAPI ObjectLayer, query *ur
// disk metrics are already included under drive info of each server // disk metrics are already included under drive info of each server
getRealtimeMetrics := func() *madmin.RealtimeMetrics { getRealtimeMetrics := func() *madmin.RealtimeMetrics {
var m madmin.RealtimeMetrics var m madmin.RealtimeMetrics
var types madmin.MetricType = madmin.MetricsAll &^ madmin.MetricsDisk types := madmin.MetricsAll &^ madmin.MetricsDisk
mLocal := collectLocalMetrics(types, collectMetricsOpts{}) mLocal := collectLocalMetrics(types, collectMetricsOpts{})
m.Merge(&mLocal) m.Merge(&mLocal)
cctx, cancel := context.WithTimeout(healthCtx, time.Second/2) cctx, cancel := context.WithTimeout(healthCtx, time.Second/2)
@@ -2693,7 +2720,7 @@ func fetchHealthInfo(healthCtx context.Context, objectAPI ObjectLayer, query *ur
poolsArgs := re.ReplaceAllString(cmdLine, `$3`) poolsArgs := re.ReplaceAllString(cmdLine, `$3`)
var anonPools []string var anonPools []string
if !(strings.Contains(poolsArgs, "{") && strings.Contains(poolsArgs, "}")) { if !strings.Contains(poolsArgs, "{") || !strings.Contains(poolsArgs, "}") {
// No ellipses pattern. Anonymize host name from every pool arg // No ellipses pattern. Anonymize host name from every pool arg
pools := strings.Fields(poolsArgs) pools := strings.Fields(poolsArgs)
anonPools = make([]string, len(pools)) anonPools = make([]string, len(pools))
@@ -2935,13 +2962,13 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
} }
if len(healthInfoCh) == 0 { if len(healthInfoCh) == 0 {
// Flush if nothing is queued // Flush if nothing is queued
w.(http.Flusher).Flush() xhttp.Flush(w)
} }
case <-ticker.C: case <-ticker.C:
if _, err := w.Write([]byte(" ")); err != nil { if _, err := w.Write([]byte(" ")); err != nil {
return return
} }
w.(http.Flusher).Flush() xhttp.Flush(w)
case <-healthCtx.Done(): case <-healthCtx.Done():
return return
} }
@@ -3393,7 +3420,7 @@ func (a adminAPIHandlers) InspectDataHandler(w http.ResponseWriter, r *http.Requ
} }
// save the format.json as part of inspect by default // save the format.json as part of inspect by default
if !(volume == minioMetaBucket && file == formatConfigFile) { if volume != minioMetaBucket || file != formatConfigFile {
err = o.GetRawData(ctx, minioMetaBucket, formatConfigFile, rawDataFn) err = o.GetRawData(ctx, minioMetaBucket, formatConfigFile, rawDataFn)
} }
if !errors.Is(err, errFileNotFound) { if !errors.Is(err, errFileNotFound) {
+13 -21
View File
@@ -260,7 +260,7 @@ func (ahs *allHealState) stopHealSequence(path string) ([]byte, APIError) {
} else { } else {
clientToken := he.clientToken clientToken := he.clientToken
if globalIsDistErasure { if globalIsDistErasure {
clientToken = fmt.Sprintf("%s:%d", he.clientToken, GetProxyEndpointLocalIndex(globalProxyEndpoints)) clientToken = fmt.Sprintf("%s%s%d", he.clientToken, getKeySeparator(), GetProxyEndpointLocalIndex(globalProxyEndpoints))
} }
hsp = madmin.HealStopSuccess{ hsp = madmin.HealStopSuccess{
@@ -331,7 +331,7 @@ func (ahs *allHealState) LaunchNewHealSequence(h *healSequence, objAPI ObjectLay
clientToken := h.clientToken clientToken := h.clientToken
if globalIsDistErasure { if globalIsDistErasure {
clientToken = fmt.Sprintf("%s:%d", h.clientToken, GetProxyEndpointLocalIndex(globalProxyEndpoints)) clientToken = fmt.Sprintf("%s%s%d", h.clientToken, getKeySeparator(), GetProxyEndpointLocalIndex(globalProxyEndpoints))
} }
if h.clientToken == bgHealingUUID { if h.clientToken == bgHealingUUID {
@@ -806,18 +806,20 @@ func (h *healSequence) healDiskMeta(objAPI ObjectLayer) error {
return h.healMinioSysMeta(objAPI, minioConfigPrefix)() return h.healMinioSysMeta(objAPI, minioConfigPrefix)()
} }
func (h *healSequence) healItems(objAPI ObjectLayer, bucketsOnly bool) error { func (h *healSequence) healItems(objAPI ObjectLayer) error {
if h.clientToken == bgHealingUUID { if h.clientToken == bgHealingUUID {
// For background heal do nothing. // For background heal do nothing.
return nil return nil
} }
if err := h.healDiskMeta(objAPI); err != nil { if h.bucket == "" { // heal internal meta only during a site-wide heal
return err if err := h.healDiskMeta(objAPI); err != nil {
return err
}
} }
// Heal buckets and objects // Heal buckets and objects
return h.healBuckets(objAPI, bucketsOnly) return h.healBuckets(objAPI)
} }
// traverseAndHeal - traverses on-disk data and performs healing // traverseAndHeal - traverses on-disk data and performs healing
@@ -828,8 +830,7 @@ func (h *healSequence) healItems(objAPI ObjectLayer, bucketsOnly bool) error {
// has to wait until a safe point is reached, such as between scanning // has to wait until a safe point is reached, such as between scanning
// two objects. // two objects.
func (h *healSequence) traverseAndHeal(objAPI ObjectLayer) { func (h *healSequence) traverseAndHeal(objAPI ObjectLayer) {
bucketsOnly := false // Heals buckets and objects also. h.traverseAndHealDoneCh <- h.healItems(objAPI)
h.traverseAndHealDoneCh <- h.healItems(objAPI, bucketsOnly)
xioutil.SafeClose(h.traverseAndHealDoneCh) xioutil.SafeClose(h.traverseAndHealDoneCh)
} }
@@ -840,6 +841,7 @@ func (h *healSequence) healMinioSysMeta(objAPI ObjectLayer, metaPrefix string) f
// NOTE: Healing on meta is run regardless // NOTE: Healing on meta is run regardless
// of any bucket being selected, this is to ensure that // of any bucket being selected, this is to ensure that
// meta are always upto date and correct. // meta are always upto date and correct.
h.settings.Recursive = true
return objAPI.HealObjects(h.ctx, minioMetaBucket, metaPrefix, h.settings, func(bucket, object, versionID string, scanMode madmin.HealScanMode) error { return objAPI.HealObjects(h.ctx, minioMetaBucket, metaPrefix, h.settings, func(bucket, object, versionID string, scanMode madmin.HealScanMode) error {
if h.isQuitting() { if h.isQuitting() {
return errHealStopSignalled return errHealStopSignalled
@@ -856,14 +858,14 @@ func (h *healSequence) healMinioSysMeta(objAPI ObjectLayer, metaPrefix string) f
} }
// healBuckets - check for all buckets heal or just particular bucket. // healBuckets - check for all buckets heal or just particular bucket.
func (h *healSequence) healBuckets(objAPI ObjectLayer, bucketsOnly bool) error { func (h *healSequence) healBuckets(objAPI ObjectLayer) error {
if h.isQuitting() { if h.isQuitting() {
return errHealStopSignalled return errHealStopSignalled
} }
// 1. If a bucket was specified, heal only the bucket. // 1. If a bucket was specified, heal only the bucket.
if h.bucket != "" { if h.bucket != "" {
return h.healBucket(objAPI, h.bucket, bucketsOnly) return h.healBucket(objAPI, h.bucket, false)
} }
buckets, err := objAPI.ListBuckets(h.ctx, BucketOptions{}) buckets, err := objAPI.ListBuckets(h.ctx, BucketOptions{})
@@ -877,7 +879,7 @@ func (h *healSequence) healBuckets(objAPI ObjectLayer, bucketsOnly bool) error {
}) })
for _, bucket := range buckets { for _, bucket := range buckets {
if err = h.healBucket(objAPI, bucket.Name, bucketsOnly); err != nil { if err = h.healBucket(objAPI, bucket.Name, false); err != nil {
return err return err
} }
} }
@@ -895,16 +897,6 @@ func (h *healSequence) healBucket(objAPI ObjectLayer, bucket string, bucketsOnly
return nil return nil
} }
if !h.settings.Recursive {
if h.object != "" {
if err := h.healObject(bucket, h.object, "", h.settings.ScanMode); err != nil {
return err
}
}
return nil
}
if err := objAPI.HealObjects(h.ctx, bucket, h.object, h.settings, h.healObject); err != nil { if err := objAPI.HealObjects(h.ctx, bucket, h.object, h.settings, h.healObject); err != nil {
return errFnHealFromAPIErr(h.ctx, err) return errFnHealFromAPIErr(h.ctx, err)
} }
+3
View File
@@ -424,6 +424,9 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
// -- Health API -- // -- Health API --
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/healthinfo"). adminRouter.Methods(http.MethodGet).Path(adminVersion + "/healthinfo").
HandlerFunc(adminMiddleware(adminAPI.HealthInfoHandler)) HandlerFunc(adminMiddleware(adminAPI.HealthInfoHandler))
// STS Revocation
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/revoke-tokens/{userProvider}").HandlerFunc(adminMiddleware(adminAPI.RevokeTokens))
} }
// If none of the routes match add default error handler routes // If none of the routes match add default error handler routes
+4 -2
View File
@@ -32,6 +32,8 @@ type DeletedObject struct {
DeleteMarkerMTime DeleteMarkerMTime `xml:"-"` DeleteMarkerMTime DeleteMarkerMTime `xml:"-"`
// MinIO extensions to support delete marker replication // MinIO extensions to support delete marker replication
ReplicationState ReplicationState `xml:"-"` ReplicationState ReplicationState `xml:"-"`
found bool // the object was found during deletion
} }
// DeleteMarkerMTime is an embedded type containing time.Time for XML marshal // DeleteMarkerMTime is an embedded type containing time.Time for XML marshal
@@ -42,10 +44,10 @@ type DeleteMarkerMTime struct {
// MarshalXML encodes expiration date if it is non-zero and encodes // MarshalXML encodes expiration date if it is non-zero and encodes
// empty string otherwise // empty string otherwise
func (t DeleteMarkerMTime) MarshalXML(e *xml.Encoder, startElement xml.StartElement) error { func (t DeleteMarkerMTime) MarshalXML(e *xml.Encoder, startElement xml.StartElement) error {
if t.Time.IsZero() { if t.IsZero() {
return nil return nil
} }
return e.EncodeElement(t.Time.Format(time.RFC3339), startElement) return e.EncodeElement(t.Format(time.RFC3339), startElement)
} }
// ObjectV object version key/versionId // ObjectV object version key/versionId
+16 -2
View File
@@ -213,6 +213,8 @@ const (
ErrPolicyAlreadyAttached ErrPolicyAlreadyAttached
ErrPolicyNotAttached ErrPolicyNotAttached
ErrExcessData ErrExcessData
ErrPolicyInvalidName
ErrNoTokenRevokeType
// Add new error codes here. // Add new error codes here.
// SSE-S3/SSE-KMS related API errors // SSE-S3/SSE-KMS related API errors
@@ -561,6 +563,11 @@ var errorCodes = errorCodeMap{
Description: "More data provided than indicated content length", Description: "More data provided than indicated content length",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrPolicyInvalidName: {
Code: "PolicyInvalidName",
Description: "Policy name may not contain comma",
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.",
@@ -623,7 +630,7 @@ var errorCodes = errorCodeMap{
}, },
ErrMissingContentMD5: { ErrMissingContentMD5: {
Code: "MissingContentMD5", Code: "MissingContentMD5",
Description: "Missing required header for this request: Content-Md5.", Description: "Missing or invalid required header for this request: Content-Md5 or Amz-Content-Checksum",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrMissingSecurityHeader: { ErrMissingSecurityHeader: {
@@ -978,7 +985,7 @@ var errorCodes = errorCodeMap{
}, },
ErrReplicationNoExistingObjects: { ErrReplicationNoExistingObjects: {
Code: "XMinioReplicationNoExistingObjects", Code: "XMinioReplicationNoExistingObjects",
Description: "No matching ExistingsObjects rule enabled", Description: "No matching ExistingObjects rule enabled",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrRemoteTargetDenyAddError: { ErrRemoteTargetDenyAddError: {
@@ -1258,6 +1265,11 @@ var errorCodes = errorCodeMap{
Description: "The security token included in the request is invalid", Description: "The security token included in the request is invalid",
HTTPStatusCode: http.StatusForbidden, HTTPStatusCode: http.StatusForbidden,
}, },
ErrNoTokenRevokeType: {
Code: "InvalidArgument",
Description: "No token revoke type specified and one could not be inferred from the request",
HTTPStatusCode: http.StatusBadRequest,
},
// S3 extensions. // S3 extensions.
ErrContentSHA256Mismatch: { ErrContentSHA256Mismatch: {
@@ -2248,6 +2260,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
apiErr = ErrServerNotInitialized apiErr = ErrServerNotInitialized
case errBucketMetadataNotInitialized: case errBucketMetadataNotInitialized:
apiErr = ErrBucketMetadataNotInitialized apiErr = ErrBucketMetadataNotInitialized
case hash.ErrInvalidChecksum:
apiErr = ErrInvalidChecksum
} }
// Compression errors // Compression errors
+2 -1
View File
@@ -34,7 +34,8 @@ func TestNewRequestID(t *testing.T) {
e = char e = char
// Ensure that it is alphanumeric, in this case, between 0-9 and A-Z. // Ensure that it is alphanumeric, in this case, between 0-9 and A-Z.
if !(('0' <= e && e <= '9') || ('A' <= e && e <= 'Z')) { isAlnum := ('0' <= e && e <= '9') || ('A' <= e && e <= 'Z')
if !isAlnum {
t.Fail() t.Fail()
} }
} }
+21 -18
View File
@@ -166,10 +166,11 @@ type Part struct {
Size int64 Size int64
// Checksum values // Checksum values
ChecksumCRC32 string `xml:"ChecksumCRC32,omitempty"` ChecksumCRC32 string `xml:"ChecksumCRC32,omitempty"`
ChecksumCRC32C string `xml:"ChecksumCRC32C,omitempty"` ChecksumCRC32C string `xml:"ChecksumCRC32C,omitempty"`
ChecksumSHA1 string `xml:"ChecksumSHA1,omitempty"` ChecksumSHA1 string `xml:"ChecksumSHA1,omitempty"`
ChecksumSHA256 string `xml:"ChecksumSHA256,omitempty"` ChecksumSHA256 string `xml:"ChecksumSHA256,omitempty"`
ChecksumCRC64NVME string `xml:",omitempty"`
} }
// ListPartsResponse - format for list parts response. // ListPartsResponse - format for list parts response.
@@ -192,6 +193,8 @@ type ListPartsResponse struct {
IsTruncated bool IsTruncated bool
ChecksumAlgorithm string ChecksumAlgorithm string
ChecksumType string
// List of parts. // List of parts.
Parts []Part `xml:"Part"` Parts []Part `xml:"Part"`
} }
@@ -413,10 +416,11 @@ type CompleteMultipartUploadResponse struct {
Key string Key string
ETag string ETag string
ChecksumCRC32 string `xml:"ChecksumCRC32,omitempty"` ChecksumCRC32 string `xml:"ChecksumCRC32,omitempty"`
ChecksumCRC32C string `xml:"ChecksumCRC32C,omitempty"` ChecksumCRC32C string `xml:"ChecksumCRC32C,omitempty"`
ChecksumSHA1 string `xml:"ChecksumSHA1,omitempty"` ChecksumSHA1 string `xml:"ChecksumSHA1,omitempty"`
ChecksumSHA256 string `xml:"ChecksumSHA256,omitempty"` ChecksumSHA256 string `xml:"ChecksumSHA256,omitempty"`
ChecksumCRC64NVME string `xml:",omitempty"`
} }
// DeleteError structure. // DeleteError structure.
@@ -516,7 +520,6 @@ func cleanReservedKeys(metadata map[string]string) map[string]string {
} }
case crypto.SSEC: case crypto.SSEC:
m[xhttp.AmzServerSideEncryptionCustomerAlgorithm] = xhttp.AmzEncryptionAES m[xhttp.AmzServerSideEncryptionCustomerAlgorithm] = xhttp.AmzEncryptionAES
} }
var toRemove []string var toRemove []string
@@ -593,8 +596,6 @@ func generateListVersionsResponse(ctx context.Context, bucket, prefix, marker, v
for k, v := range cleanReservedKeys(object.UserDefined) { for k, v := range cleanReservedKeys(object.UserDefined) {
content.UserMetadata.Set(k, v) content.UserMetadata.Set(k, v)
} }
content.UserMetadata.Set("expires", object.Expires.Format(http.TimeFormat))
content.Internal = &ObjectInternalInfo{ content.Internal = &ObjectInternalInfo{
K: object.DataBlocks, K: object.DataBlocks,
M: object.ParityBlocks, M: object.ParityBlocks,
@@ -729,7 +730,6 @@ func generateListObjectsV2Response(ctx context.Context, bucket, prefix, token, n
for k, v := range cleanReservedKeys(object.UserDefined) { for k, v := range cleanReservedKeys(object.UserDefined) {
content.UserMetadata.Set(k, v) content.UserMetadata.Set(k, v)
} }
content.UserMetadata.Set("expires", object.Expires.Format(http.TimeFormat))
content.Internal = &ObjectInternalInfo{ content.Internal = &ObjectInternalInfo{
K: object.DataBlocks, K: object.DataBlocks,
M: object.ParityBlocks, M: object.ParityBlocks,
@@ -790,17 +790,18 @@ 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 generateCompleteMultipartUploadResponse(bucket, key, location string, oi ObjectInfo, h http.Header) CompleteMultipartUploadResponse { func generateCompleteMultipartUploadResponse(bucket, key, location string, oi ObjectInfo, h http.Header) CompleteMultipartUploadResponse {
cs := oi.decryptChecksums(0, h) cs, _ := oi.decryptChecksums(0, h)
c := CompleteMultipartUploadResponse{ c := CompleteMultipartUploadResponse{
Location: location, Location: location,
Bucket: bucket, Bucket: bucket,
Key: key, Key: key,
// AWS S3 quotes the ETag in XML, make sure we are compatible here. // AWS S3 quotes the ETag in XML, make sure we are compatible here.
ETag: "\"" + oi.ETag + "\"", ETag: "\"" + oi.ETag + "\"",
ChecksumSHA1: cs[hash.ChecksumSHA1.String()], ChecksumSHA1: cs[hash.ChecksumSHA1.String()],
ChecksumSHA256: cs[hash.ChecksumSHA256.String()], ChecksumSHA256: cs[hash.ChecksumSHA256.String()],
ChecksumCRC32: cs[hash.ChecksumCRC32.String()], ChecksumCRC32: cs[hash.ChecksumCRC32.String()],
ChecksumCRC32C: cs[hash.ChecksumCRC32C.String()], ChecksumCRC32C: cs[hash.ChecksumCRC32C.String()],
ChecksumCRC64NVME: cs[hash.ChecksumCRC64NVME.String()],
} }
return c return c
} }
@@ -828,6 +829,7 @@ func generateListPartsResponse(partsInfo ListPartsInfo, encodingType string) Lis
listPartsResponse.IsTruncated = partsInfo.IsTruncated listPartsResponse.IsTruncated = partsInfo.IsTruncated
listPartsResponse.NextPartNumberMarker = partsInfo.NextPartNumberMarker listPartsResponse.NextPartNumberMarker = partsInfo.NextPartNumberMarker
listPartsResponse.ChecksumAlgorithm = partsInfo.ChecksumAlgorithm listPartsResponse.ChecksumAlgorithm = partsInfo.ChecksumAlgorithm
listPartsResponse.ChecksumType = partsInfo.ChecksumType
listPartsResponse.Parts = make([]Part, len(partsInfo.Parts)) listPartsResponse.Parts = make([]Part, len(partsInfo.Parts))
for index, part := range partsInfo.Parts { for index, part := range partsInfo.Parts {
@@ -840,6 +842,7 @@ func generateListPartsResponse(partsInfo ListPartsInfo, encodingType string) Lis
newPart.ChecksumCRC32C = part.ChecksumCRC32C newPart.ChecksumCRC32C = part.ChecksumCRC32C
newPart.ChecksumSHA1 = part.ChecksumSHA1 newPart.ChecksumSHA1 = part.ChecksumSHA1
newPart.ChecksumSHA256 = part.ChecksumSHA256 newPart.ChecksumSHA256 = part.ChecksumSHA256
newPart.ChecksumCRC64NVME = part.ChecksumCRC64NVME
listPartsResponse.Parts[index] = newPart listPartsResponse.Parts[index] = newPart
} }
return listPartsResponse return listPartsResponse
+2 -2
View File
@@ -227,13 +227,13 @@ func s3APIMiddleware(f http.HandlerFunc, flags ...s3HFlag) http.HandlerFunc {
} }
// Skip wrapping with the gzip middleware if specified. // Skip wrapping with the gzip middleware if specified.
var gzippedHandler http.HandlerFunc = tracedHandler gzippedHandler := tracedHandler
if !handlerFlags.has(noGZS3HFlag) { if !handlerFlags.has(noGZS3HFlag) {
gzippedHandler = gzipHandler(gzippedHandler) gzippedHandler = gzipHandler(gzippedHandler)
} }
// Skip wrapping with throttling middleware if specified. // Skip wrapping with throttling middleware if specified.
var throttledHandler http.HandlerFunc = gzippedHandler throttledHandler := gzippedHandler
if !handlerFlags.has(noThrottleS3HFlag) { if !handlerFlags.has(noThrottleS3HFlag) {
throttledHandler = maxClients(throttledHandler) throttledHandler = maxClients(throttledHandler)
} }
+201 -199
View File
File diff suppressed because one or more lines are too long
+9 -30
View File
@@ -96,7 +96,7 @@ func isRequestSignStreamingTrailerV4(r *http.Request) bool {
// Verify if the request has AWS Streaming Signature Version '4', with unsigned content and trailer. // Verify if the request has AWS Streaming Signature Version '4', with unsigned content and trailer.
func isRequestUnsignedTrailerV4(r *http.Request) bool { func isRequestUnsignedTrailerV4(r *http.Request) bool {
return r.Header.Get(xhttp.AmzContentSha256) == unsignedPayloadTrailer && return r.Header.Get(xhttp.AmzContentSha256) == unsignedPayloadTrailer &&
r.Method == http.MethodPut && strings.Contains(r.Header.Get(xhttp.ContentEncoding), streamingContentEncoding) r.Method == http.MethodPut
} }
// Authorization type. // Authorization type.
@@ -162,7 +162,6 @@ func validateAdminSignature(ctx context.Context, r *http.Request, region string)
s3Err := ErrAccessDenied s3Err := ErrAccessDenied
if _, ok := r.Header[xhttp.AmzContentSha256]; ok && if _, ok := r.Header[xhttp.AmzContentSha256]; ok &&
getRequestAuthType(r) == authTypeSigned { getRequestAuthType(r) == authTypeSigned {
// Get credential information from the request. // Get credential information from the request.
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3) cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
if s3Err != ErrNone { if s3Err != ErrNone {
@@ -364,7 +363,7 @@ func authenticateRequest(ctx context.Context, r *http.Request, action policy.Act
var cred auth.Credentials var cred auth.Credentials
var owner bool var owner bool
switch getRequestAuthType(r) { switch getRequestAuthType(r) {
case authTypeUnknown, authTypeStreamingSigned: case authTypeUnknown, authTypeStreamingSigned, authTypeStreamingSignedTrailer, authTypeStreamingUnsignedTrailer:
return ErrSignatureVersionNotSupported return ErrSignatureVersionNotSupported
case authTypePresignedV2, authTypeSignedV2: case authTypePresignedV2, authTypeSignedV2:
if s3Err = isReqAuthenticatedV2(r); s3Err != ErrNone { if s3Err = isReqAuthenticatedV2(r); s3Err != ErrNone {
@@ -675,32 +674,6 @@ func setAuthMiddleware(h http.Handler) http.Handler {
}) })
} }
func validateSignature(atype authType, r *http.Request) (auth.Credentials, bool, APIErrorCode) {
var cred auth.Credentials
var owner bool
var s3Err APIErrorCode
switch atype {
case authTypeUnknown, authTypeStreamingSigned:
return cred, owner, ErrSignatureVersionNotSupported
case authTypeSignedV2, authTypePresignedV2:
if s3Err = isReqAuthenticatedV2(r); s3Err != ErrNone {
return cred, owner, s3Err
}
cred, owner, s3Err = getReqAccessKeyV2(r)
case authTypePresigned, authTypeSigned:
region := globalSite.Region()
if s3Err = isReqAuthenticated(GlobalContext, r, region, serviceS3); s3Err != ErrNone {
return cred, owner, s3Err
}
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
}
if s3Err != ErrNone {
return cred, owner, s3Err
}
return cred, owner, ErrNone
}
func isPutRetentionAllowed(bucketName, objectName string, retDays int, retDate time.Time, retMode objectlock.RetMode, byPassSet bool, r *http.Request, cred auth.Credentials, owner bool) (s3Err APIErrorCode) { func isPutRetentionAllowed(bucketName, objectName string, retDays int, retDate time.Time, retMode objectlock.RetMode, byPassSet bool, r *http.Request, cred auth.Credentials, owner bool) (s3Err APIErrorCode) {
var retSet bool var retSet bool
if cred.AccessKey == "" { if cred.AccessKey == "" {
@@ -755,8 +728,14 @@ 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, authTypeStreamingSignedTrailer, authTypeStreamingUnsignedTrailer: case authTypeStreamingSigned, authTypePresigned, authTypeSigned, authTypeStreamingSignedTrailer:
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3) cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
case authTypeStreamingUnsignedTrailer:
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
if s3Err == ErrMissingFields {
// Could be anonymous. cred + owner is zero value.
s3Err = ErrNone
}
} }
if s3Err != ErrNone { if s3Err != ErrNone {
return s3Err return s3Err
-3
View File
@@ -223,9 +223,6 @@ func (h *healingTracker) updateProgress(success, skipped bool, bytes uint64) {
// update will update the tracker on the disk. // update will update the tracker on the disk.
// If the tracker has been deleted an error is returned. // If the tracker has been deleted an error is returned.
func (h *healingTracker) update(ctx context.Context) error { func (h *healingTracker) update(ctx context.Context) error {
if h.disk.Healing() == nil {
return fmt.Errorf("healingTracker: drive %q is not marked as healing", h.ID)
}
h.mu.Lock() 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()
+14 -11
View File
@@ -195,8 +195,8 @@ func (ef BatchJobExpireFilter) Matches(obj ObjectInfo, now time.Time) bool {
return false return false
} }
} }
} }
if len(ef.Metadata) > 0 && !obj.DeleteMarker { if len(ef.Metadata) > 0 && !obj.DeleteMarker {
for _, kv := range ef.Metadata { for _, kv := range ef.Metadata {
// Object (version) must match all x-amz-meta and // Object (version) must match all x-amz-meta and
@@ -289,6 +289,16 @@ type BatchJobExpire struct {
var _ yaml.Unmarshaler = &BatchJobExpire{} var _ yaml.Unmarshaler = &BatchJobExpire{}
// RedactSensitive will redact any sensitive information in b.
func (r *BatchJobExpire) RedactSensitive() {
if r == nil {
return
}
if r.NotificationCfg.Token != "" {
r.NotificationCfg.Token = redactedText
}
}
// UnmarshalYAML - BatchJobExpire extends default unmarshal to extract line, col information. // UnmarshalYAML - BatchJobExpire extends default unmarshal to extract line, col information.
func (r *BatchJobExpire) UnmarshalYAML(val *yaml.Node) error { func (r *BatchJobExpire) UnmarshalYAML(val *yaml.Node) error {
type expireJob BatchJobExpire type expireJob BatchJobExpire
@@ -435,14 +445,14 @@ func batchObjsForDelete(ctx context.Context, r *BatchJobExpire, ri *batchJobInfo
oiCache.Add(od, &exp.ObjectInfo) oiCache.Add(od, &exp.ObjectInfo)
} }
var done bool
// DeleteObject(deletePrefix: true) to expire all versions of an object // DeleteObject(deletePrefix: true) to expire all versions of an object
for _, exp := range toExpireAll { for _, exp := range toExpireAll {
var success bool var success bool
for attempts := 1; attempts <= retryAttempts; attempts++ { for attempts := 1; attempts <= retryAttempts; attempts++ {
select { select {
case <-ctx.Done(): case <-ctx.Done():
done = true ri.trackMultipleObjectVersions(exp, success)
return
default: default:
} }
stopFn := globalBatchJobsMetrics.trace(batchJobMetricExpire, ri.JobID, attempts) stopFn := globalBatchJobsMetrics.trace(batchJobMetricExpire, ri.JobID, attempts)
@@ -459,14 +469,7 @@ func batchObjsForDelete(ctx context.Context, r *BatchJobExpire, ri *batchJobInfo
break break
} }
} }
ri.trackMultipleObjectVersions(r.Bucket, exp, success) ri.trackMultipleObjectVersions(exp, success)
if done {
break
}
}
if done {
return
} }
// DeleteMultiple objects // DeleteMultiple objects
+109 -56
View File
@@ -39,7 +39,6 @@ import (
"github.com/lithammer/shortuuid/v4" "github.com/lithammer/shortuuid/v4"
"github.com/minio/madmin-go/v3" "github.com/minio/madmin-go/v3"
"github.com/minio/minio-go/v7" "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"
"github.com/minio/minio-go/v7/pkg/tags" "github.com/minio/minio-go/v7/pkg/tags"
@@ -47,7 +46,6 @@ import (
"github.com/minio/minio/internal/crypto" "github.com/minio/minio/internal/crypto"
"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"
xioutil "github.com/minio/minio/internal/ioutil" xioutil "github.com/minio/minio/internal/ioutil"
"github.com/minio/pkg/v3/console" "github.com/minio/pkg/v3/console"
"github.com/minio/pkg/v3/env" "github.com/minio/pkg/v3/env"
@@ -61,6 +59,8 @@ var globalBatchConfig batch.Config
const ( const (
// Keep the completed/failed job stats 3 days before removing it // Keep the completed/failed job stats 3 days before removing it
oldJobsExpiration = 3 * 24 * time.Hour oldJobsExpiration = 3 * 24 * time.Hour
redactedText = "**REDACTED**"
) )
// BatchJobRequest this is an internal data structure not for external consumption. // BatchJobRequest this is an internal data structure not for external consumption.
@@ -74,6 +74,29 @@ type BatchJobRequest struct {
ctx context.Context `msg:"-"` ctx context.Context `msg:"-"`
} }
// RedactSensitive will redact any sensitive information in b.
func (j *BatchJobRequest) RedactSensitive() {
j.Replicate.RedactSensitive()
j.Expire.RedactSensitive()
j.KeyRotate.RedactSensitive()
}
// RedactSensitive will redact any sensitive information in b.
func (r *BatchJobReplicateV1) RedactSensitive() {
if r == nil {
return
}
if r.Target.Creds.SecretKey != "" {
r.Target.Creds.SecretKey = redactedText
}
if r.Target.Creds.SessionToken != "" {
r.Target.Creds.SessionToken = redactedText
}
}
// RedactSensitive will redact any sensitive information in b.
func (r *BatchJobKeyRotateV1) RedactSensitive() {}
func notifyEndpoint(ctx context.Context, ri *batchJobInfo, endpoint, token string) error { func notifyEndpoint(ctx context.Context, ri *batchJobInfo, endpoint, token string) error {
if endpoint == "" { if endpoint == "" {
return nil return nil
@@ -117,7 +140,7 @@ func (r BatchJobReplicateV1) Notify(ctx context.Context, ri *batchJobInfo) 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 *miniogo.Core, srcObjInfo ObjectInfo, retry bool) error { func (r *BatchJobReplicateV1) ReplicateFromSource(ctx context.Context, api ObjectLayer, core *minio.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
@@ -164,7 +187,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 := miniogo.GetObjectOptions{ gopts := minio.GetObjectOptions{
VersionID: srcObjInfo.VersionID, VersionID: srcObjInfo.VersionID,
} }
if err := gopts.SetMatchETag(srcObjInfo.ETag); err != nil { if err := gopts.SetMatchETag(srcObjInfo.ETag); err != nil {
@@ -185,7 +208,7 @@ func (r *BatchJobReplicateV1) ReplicateFromSource(ctx context.Context, api Objec
return err return err
} }
func (r *BatchJobReplicateV1) copyWithMultipartfromSource(ctx context.Context, api ObjectLayer, c *miniogo.Core, srcObjInfo ObjectInfo, opts ObjectOptions, partsCount int) (err error) { func (r *BatchJobReplicateV1) copyWithMultipartfromSource(ctx context.Context, api ObjectLayer, c *minio.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
@@ -226,7 +249,7 @@ func (r *BatchJobReplicateV1) copyWithMultipartfromSource(ctx context.Context, a
) )
for i := 0; i < partsCount; i++ { for i := 0; i < partsCount; i++ {
gopts := miniogo.GetObjectOptions{ gopts := minio.GetObjectOptions{
VersionID: srcObjInfo.VersionID, VersionID: srcObjInfo.VersionID,
PartNumber: i + 1, PartNumber: i + 1,
} }
@@ -301,12 +324,12 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
return true return true
} }
if !r.Flags.Filter.CreatedAfter.IsZero() && r.Flags.Filter.CreatedAfter.Before(oi.ModTime) { if !r.Flags.Filter.CreatedAfter.IsZero() && r.Flags.Filter.CreatedAfter.After(oi.ModTime) {
// skip all objects that are created before the specified time. // skip all objects that are created before the specified time.
return true return true
} }
if !r.Flags.Filter.CreatedBefore.IsZero() && r.Flags.Filter.CreatedBefore.After(oi.ModTime) { if !r.Flags.Filter.CreatedBefore.IsZero() && r.Flags.Filter.CreatedBefore.Before(oi.ModTime) {
// skip all objects that are created after the specified time. // skip all objects that are created after the specified time.
return true return true
} }
@@ -357,7 +380,7 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
cred := r.Source.Creds cred := r.Source.Creds
c, err := miniogo.New(u.Host, &miniogo.Options{ c, err := minio.New(u.Host, &minio.Options{
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(),
@@ -368,7 +391,7 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
} }
c.SetAppInfo("minio-"+batchJobPrefix, r.APIVersion+" "+job.ID) c.SetAppInfo("minio-"+batchJobPrefix, r.APIVersion+" "+job.ID)
core := &miniogo.Core{Client: c} core := &minio.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 {
@@ -389,14 +412,14 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
minioSrc := r.Source.Type == BatchJobReplicateResourceMinIO minioSrc := r.Source.Type == BatchJobReplicateResourceMinIO
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
objInfoCh := make(chan miniogo.ObjectInfo, 1) objInfoCh := make(chan minio.ObjectInfo, 1)
go func() { go func() {
prefixes := r.Source.Prefix.F() prefixes := r.Source.Prefix.F()
if len(prefixes) == 0 { if len(prefixes) == 0 {
prefixes = []string{""} prefixes = []string{""}
} }
for _, prefix := range prefixes { for _, prefix := range prefixes {
prefixObjInfoCh := c.ListObjects(ctx, r.Source.Bucket, miniogo.ListObjectsOptions{ prefixObjInfoCh := c.ListObjects(ctx, r.Source.Bucket, minio.ListObjectsOptions{
Prefix: prefix, Prefix: prefix,
WithVersions: minioSrc, WithVersions: minioSrc,
Recursive: true, Recursive: true,
@@ -419,7 +442,7 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
// all user metadata or just storageClass. If its only storageClass // all user metadata or just storageClass. If its only storageClass
// List() already returns relevant information for filter to be applied. // List() already returns relevant information for filter to be applied.
if isMetadata && !isStorageClassOnly { if isMetadata && !isStorageClassOnly {
oi2, err := c.StatObject(ctx, r.Source.Bucket, obj.Key, miniogo.StatObjectOptions{}) oi2, err := c.StatObject(ctx, r.Source.Bucket, obj.Key, minio.StatObjectOptions{})
if err == nil { if err == nil {
oi = toObjectInfo(r.Source.Bucket, obj.Key, oi2) oi = toObjectInfo(r.Source.Bucket, obj.Key, oi2)
} else { } else {
@@ -515,7 +538,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 miniogo.ObjectInfo) ObjectInfo { func toObjectInfo(bucket, object string, objInfo minio.ObjectInfo) ObjectInfo {
tags, _ := tags.MapToObjectTags(objInfo.UserTags) tags, _ := tags.MapToObjectTags(objInfo.UserTags)
oi := ObjectInfo{ oi := ObjectInfo{
Bucket: bucket, Bucket: bucket,
@@ -597,12 +620,12 @@ func (r BatchJobReplicateV1) writeAsArchive(ctx context.Context, objAPI ObjectLa
}, },
} }
opts, err := batchReplicationOpts(ctx, "", gr.ObjInfo) opts, _, err := batchReplicationOpts(ctx, "", gr.ObjInfo)
if err != nil { if err != nil {
batchLogIf(ctx, err) batchLogIf(ctx, err)
continue continue
} }
// TODO: I am not sure we read it back, but we aren't sending whether checksums are single/multipart.
for k, vals := range opts.Header() { for k, vals := range opts.Header() {
for _, v := range vals { for _, v := range vals {
snowballObj.Headers.Add(k, v) snowballObj.Headers.Add(k, v)
@@ -618,7 +641,7 @@ func (r BatchJobReplicateV1) writeAsArchive(ctx context.Context, objAPI ObjectLa
} }
// ReplicateToTarget read from source and replicate to configured target // ReplicateToTarget read from source and replicate to configured target
func (r *BatchJobReplicateV1) ReplicateToTarget(ctx context.Context, api ObjectLayer, c *miniogo.Core, srcObjInfo ObjectInfo, retry bool) error { func (r *BatchJobReplicateV1) ReplicateToTarget(ctx context.Context, api ObjectLayer, c *minio.Core, srcObjInfo ObjectInfo, retry bool) error {
srcBucket := r.Source.Bucket srcBucket := r.Source.Bucket
tgtBucket := r.Target.Bucket tgtBucket := r.Target.Bucket
tgtPrefix := r.Target.Prefix tgtPrefix := r.Target.Prefix
@@ -627,9 +650,9 @@ func (r *BatchJobReplicateV1) ReplicateToTarget(ctx context.Context, api ObjectL
if srcObjInfo.DeleteMarker || !srcObjInfo.VersionPurgeStatus.Empty() { if srcObjInfo.DeleteMarker || !srcObjInfo.VersionPurgeStatus.Empty() {
if retry && !s3Type { if retry && !s3Type {
if _, err := c.StatObject(ctx, tgtBucket, pathJoin(tgtPrefix, srcObject), miniogo.StatObjectOptions{ if _, err := c.StatObject(ctx, tgtBucket, pathJoin(tgtPrefix, srcObject), minio.StatObjectOptions{
VersionID: srcObjInfo.VersionID, VersionID: srcObjInfo.VersionID,
Internal: miniogo.AdvancedGetOptions{ Internal: minio.AdvancedGetOptions{
ReplicationProxyRequest: "false", ReplicationProxyRequest: "false",
}, },
}); isErrMethodNotAllowed(ErrorRespToObjectError(err, tgtBucket, pathJoin(tgtPrefix, srcObject))) { }); isErrMethodNotAllowed(ErrorRespToObjectError(err, tgtBucket, pathJoin(tgtPrefix, srcObject))) {
@@ -646,19 +669,19 @@ func (r *BatchJobReplicateV1) ReplicateToTarget(ctx context.Context, api ObjectL
dmVersionID = "" dmVersionID = ""
versionID = "" versionID = ""
} }
return c.RemoveObject(ctx, tgtBucket, pathJoin(tgtPrefix, srcObject), miniogo.RemoveObjectOptions{ return c.RemoveObject(ctx, tgtBucket, pathJoin(tgtPrefix, srcObject), minio.RemoveObjectOptions{
VersionID: versionID, VersionID: versionID,
Internal: miniogo.AdvancedRemoveOptions{ Internal: minio.AdvancedRemoveOptions{
ReplicationDeleteMarker: dmVersionID != "", ReplicationDeleteMarker: dmVersionID != "",
ReplicationMTime: srcObjInfo.ModTime, ReplicationMTime: srcObjInfo.ModTime,
ReplicationStatus: miniogo.ReplicationStatusReplica, ReplicationStatus: minio.ReplicationStatusReplica,
ReplicationRequest: true, // always set this to distinguish between `mc mirror` replication and serverside ReplicationRequest: true, // always set this to distinguish between `mc mirror` replication and serverside
}, },
}) })
} }
if retry && !s3Type { // when we are retrying avoid copying if necessary. if retry && !s3Type { // when we are retrying avoid copying if necessary.
gopts := miniogo.GetObjectOptions{} gopts := minio.GetObjectOptions{}
if err := gopts.SetMatchETag(srcObjInfo.ETag); err != nil { if err := gopts.SetMatchETag(srcObjInfo.ETag); err != nil {
return err return err
} }
@@ -687,14 +710,14 @@ func (r *BatchJobReplicateV1) ReplicateToTarget(ctx context.Context, api ObjectL
return err return err
} }
putOpts, err := batchReplicationOpts(ctx, "", objInfo) putOpts, isMP, err := batchReplicationOpts(ctx, "", objInfo)
if err != nil { if err != nil {
return err return err
} }
if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 { if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 {
putOpts.Internal = miniogo.AdvancedPutOptions{} putOpts.Internal = minio.AdvancedPutOptions{}
} }
if objInfo.isMultipart() { if isMP {
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
} }
@@ -971,7 +994,7 @@ func (ri *batchJobInfo) updateAfter(ctx context.Context, api ObjectLayer, durati
// Note: to be used only with batch jobs that affect multiple versions through // Note: to be used only with batch jobs that affect multiple versions through
// a single action. e.g batch-expire has an option to expire all versions of an // a single action. e.g batch-expire has an option to expire all versions of an
// object which matches the given filters. // object which matches the given filters.
func (ri *batchJobInfo) trackMultipleObjectVersions(bucket string, info ObjectInfo, success bool) { func (ri *batchJobInfo) trackMultipleObjectVersions(info ObjectInfo, success bool) {
if success { if success {
ri.Objects += int64(info.NumVersions) ri.Objects += int64(info.NumVersions)
} else { } else {
@@ -1047,12 +1070,12 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
return false return false
} }
if !r.Flags.Filter.CreatedAfter.IsZero() && r.Flags.Filter.CreatedAfter.Before(info.ModTime) { if !r.Flags.Filter.CreatedAfter.IsZero() && r.Flags.Filter.CreatedAfter.After(info.ModTime) {
// skip all objects that are created before the specified time. // skip all objects that are created before the specified time.
return false return false
} }
if !r.Flags.Filter.CreatedBefore.IsZero() && r.Flags.Filter.CreatedBefore.After(info.ModTime) { if !r.Flags.Filter.CreatedBefore.IsZero() && r.Flags.Filter.CreatedBefore.Before(info.ModTime) {
// skip all objects that are created after the specified time. // skip all objects that are created after the specified time.
return false return false
} }
@@ -1099,7 +1122,8 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
} }
// if one of source or target is non MinIO, just replicate the top most version like `mc mirror` // if one of source or target is non MinIO, just replicate the top most version like `mc mirror`
return !((r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3) && !info.IsLatest) isSourceOrTargetS3 := r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3
return !isSourceOrTargetS3 || info.IsLatest
} }
u, err := url.Parse(r.Target.Endpoint) u, err := url.Parse(r.Target.Endpoint)
@@ -1109,7 +1133,7 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
cred := r.Target.Creds cred := r.Target.Creds
c, err := miniogo.NewCore(u.Host, &miniogo.Options{ c, err := minio.NewCore(u.Host, &minio.Options{
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(),
@@ -1132,14 +1156,14 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
if r.Source.Snowball.Disable != nil && !*r.Source.Snowball.Disable && r.Source.Type.isMinio() && r.Target.Type.isMinio() { if r.Source.Snowball.Disable != nil && !*r.Source.Snowball.Disable && r.Source.Type.isMinio() && r.Target.Type.isMinio() {
go func() { go func() {
// Snowball currently needs the high level minio-go Client, not the Core one // Snowball currently needs the high level minio-go Client, not the Core one
cl, err := miniogo.New(u.Host, &miniogo.Options{ cl, err := minio.New(u.Host, &minio.Options{
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), BucketLookup: lookupStyle(r.Target.Path),
}) })
if err != nil { if err != nil {
batchLogOnceIf(ctx, err, job.ID+"miniogo.New") batchLogOnceIf(ctx, err, job.ID+"minio.New")
return return
} }
@@ -1249,7 +1273,7 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
stopFn := globalBatchJobsMetrics.trace(batchJobMetricReplication, job.ID, attempts) stopFn := globalBatchJobsMetrics.trace(batchJobMetricReplication, job.ID, attempts)
success := true success := true
if err := r.ReplicateToTarget(ctx, api, c, result, retry); err != nil { if err := r.ReplicateToTarget(ctx, api, c, result, retry); err != nil {
if miniogo.ToErrorResponse(err).Code == "PreconditionFailed" { if minio.ToErrorResponse(err).Code == "PreconditionFailed" {
// pre-condition failed means we already have the object copied over. // pre-condition failed means we already have the object copied over.
return return
} }
@@ -1425,7 +1449,6 @@ func (r *BatchJobReplicateV1) Validate(ctx context.Context, job BatchJobRequest,
cred = r.Source.Creds cred = r.Source.Creds
remoteBkt = r.Source.Bucket remoteBkt = r.Source.Bucket
pathStyle = r.Source.Path pathStyle = r.Source.Path
} }
u, err := url.Parse(remoteEp) u, err := url.Parse(remoteEp)
@@ -1433,7 +1456,7 @@ func (r *BatchJobReplicateV1) Validate(ctx context.Context, job BatchJobRequest,
return err return err
} }
c, err := miniogo.NewCore(u.Host, &miniogo.Options{ c, err := minio.NewCore(u.Host, &minio.Options{
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(),
@@ -1446,7 +1469,7 @@ func (r *BatchJobReplicateV1) Validate(ctx context.Context, job BatchJobRequest,
vcfg, err := c.GetBucketVersioning(ctx, remoteBkt) vcfg, err := c.GetBucketVersioning(ctx, remoteBkt)
if err != nil { if err != nil {
if miniogo.ToErrorResponse(err).Code == "NoSuchBucket" { if minio.ToErrorResponse(err).Code == "NoSuchBucket" {
return batchReplicationJobError{ return batchReplicationJobError{
Code: "NoSuchTargetBucket", Code: "NoSuchTargetBucket",
Description: "The specified target bucket does not exist", Description: "The specified target bucket does not exist",
@@ -1551,19 +1574,19 @@ func (j *BatchJobRequest) load(ctx context.Context, api ObjectLayer, name string
return err return err
} }
func batchReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (putOpts miniogo.PutObjectOptions, err error) { func batchReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (putOpts minio.PutObjectOptions, isMP bool, err error) {
// TODO: support custom storage class for remote replication // TODO: support custom storage class for remote replication
putOpts, err = putReplicationOpts(ctx, "", objInfo, 0) putOpts, isMP, err = putReplicationOpts(ctx, "", objInfo)
if err != nil { if err != nil {
return putOpts, err return putOpts, isMP, err
} }
putOpts.Internal = miniogo.AdvancedPutOptions{ putOpts.Internal = minio.AdvancedPutOptions{
SourceVersionID: objInfo.VersionID, SourceVersionID: objInfo.VersionID,
SourceMTime: objInfo.ModTime, SourceMTime: objInfo.ModTime,
SourceETag: objInfo.ETag, SourceETag: objInfo.ETag,
ReplicationRequest: true, ReplicationRequest: true,
} }
return putOpts, nil return putOpts, isMP, nil
} }
// ListBatchJobs - lists all currently active batch jobs, optionally takes {jobType} // ListBatchJobs - lists all currently active batch jobs, optionally takes {jobType}
@@ -1577,9 +1600,6 @@ func (a adminAPIHandlers) ListBatchJobs(w http.ResponseWriter, r *http.Request)
} }
jobType := r.Form.Get("jobType") jobType := r.Form.Get("jobType")
if jobType == "" {
jobType = string(madmin.BatchJobReplicate)
}
resultCh := make(chan itemOrErr[ObjectInfo]) resultCh := make(chan itemOrErr[ObjectInfo])
@@ -1608,7 +1628,7 @@ func (a adminAPIHandlers) ListBatchJobs(w http.ResponseWriter, r *http.Request)
continue continue
} }
if jobType == string(req.Type()) { if jobType == string(req.Type()) || jobType == "" {
listResult.Jobs = append(listResult.Jobs, madmin.BatchJobResult{ listResult.Jobs = append(listResult.Jobs, madmin.BatchJobResult{
ID: req.ID, ID: req.ID,
Type: req.Type(), Type: req.Type(),
@@ -1698,6 +1718,8 @@ func (a adminAPIHandlers) DescribeBatchJob(w http.ResponseWriter, r *http.Reques
return return
} }
// Remove sensitive fields.
req.RedactSensitive()
buf, err := yaml.Marshal(req) buf, err := yaml.Marshal(req)
if err != nil { if err != nil {
batchLogIf(ctx, err) batchLogIf(ctx, err)
@@ -1717,7 +1739,7 @@ func (a adminAPIHandlers) StartBatchJob(w http.ResponseWriter, r *http.Request)
return return
} }
buf, err := io.ReadAll(ioutil.HardLimitReader(r.Body, humanize.MiByte*4)) buf, err := io.ReadAll(xioutil.HardLimitReader(r.Body, humanize.MiByte*4))
if err != nil { if err != nil {
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
return return
@@ -1805,7 +1827,7 @@ func (a adminAPIHandlers) CancelBatchJob(w http.ResponseWriter, r *http.Request)
return return
} }
if _, success := proxyRequestByToken(ctx, w, r, jobID); success { if _, proxied, _ := proxyRequestByToken(ctx, w, r, jobID, true); proxied {
return return
} }
@@ -1989,7 +2011,6 @@ func (j *BatchJobPool) AddWorker() {
} }
} }
} }
job.delete(j.ctx, j.objLayer)
j.canceler(job.ID, false) j.canceler(job.ID, false)
case <-j.workerKillCh: case <-j.workerKillCh:
return return
@@ -2050,7 +2071,9 @@ func (j *BatchJobPool) canceler(jobID string, cancel bool) error {
canceler() canceler()
} }
} }
delete(j.jobCancelers, jobID) if cancel {
delete(j.jobCancelers, jobID)
}
return nil return nil
} }
@@ -2176,11 +2199,42 @@ func (m *batchJobMetrics) purgeJobMetrics() {
m.RUnlock() m.RUnlock()
for _, jobID := range toDeleteJobMetrics { for _, jobID := range toDeleteJobMetrics {
m.delete(jobID) m.delete(jobID)
j := BatchJobRequest{
ID: jobID,
}
j.delete(GlobalContext, newObjectLayerFn())
} }
} }
} }
} }
// load metrics from disk on startup
func (m *batchJobMetrics) init(ctx context.Context, objectAPI ObjectLayer) error {
resultCh := make(chan itemOrErr[ObjectInfo])
ctx, cancel := context.WithCancel(ctx)
defer cancel()
if err := objectAPI.Walk(ctx, minioMetaBucket, batchJobReportsPrefix, resultCh, WalkOptions{}); err != nil {
return err
}
for result := range resultCh {
if result.Err != nil {
return result.Err
}
ri := &batchJobInfo{}
if err := ri.loadByPath(ctx, objectAPI, result.Item.Name); err != nil {
if !errors.Is(err, errNoSuchJob) {
batchLogIf(ctx, err)
}
continue
}
m.metrics[ri.JobID] = ri
}
return nil
}
func (m *batchJobMetrics) delete(jobID string) { func (m *batchJobMetrics) delete(jobID string) {
m.Lock() m.Lock()
defer m.Unlock() defer m.Unlock()
@@ -2245,16 +2299,15 @@ func (m *batchJobMetrics) trace(d batchJobMetric, job string, attempts int) func
} }
} }
func lookupStyle(s string) miniogo.BucketLookupType { func lookupStyle(s string) minio.BucketLookupType {
var lookup miniogo.BucketLookupType var lookup minio.BucketLookupType
switch s { switch s {
case "on": case "on":
lookup = miniogo.BucketLookupPath lookup = minio.BucketLookupPath
case "off": case "off":
lookup = miniogo.BucketLookupDNS lookup = minio.BucketLookupDNS
default: default:
lookup = miniogo.BucketLookupAuto lookup = minio.BucketLookupAuto
} }
return lookup return lookup
} }
+11
View File
@@ -20,6 +20,7 @@ package cmd
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"hash" "hash"
"io" "io"
"sync" "sync"
@@ -37,12 +38,22 @@ type streamingBitrotWriter struct {
shardSize int64 shardSize int64
canClose *sync.WaitGroup canClose *sync.WaitGroup
byteBuf []byte byteBuf []byte
finished bool
} }
func (b *streamingBitrotWriter) Write(p []byte) (int, error) { func (b *streamingBitrotWriter) Write(p []byte) (int, error) {
if len(p) == 0 { if len(p) == 0 {
return 0, nil return 0, nil
} }
if b.finished {
return 0, errors.New("bitrot write not allowed")
}
if int64(len(p)) > b.shardSize {
return 0, errors.New("unexpected bitrot buffer size")
}
if int64(len(p)) < b.shardSize {
b.finished = true
}
b.h.Reset() b.h.Reset()
b.h.Write(p) b.h.Write(p)
hashBytes := b.h.Sum(nil) hashBytes := b.h.Sum(nil)
+13 -7
View File
@@ -128,14 +128,20 @@ func closeBitrotReaders(rs []io.ReaderAt) {
} }
// Close all the writers. // Close all the writers.
func closeBitrotWriters(ws []io.Writer) { func closeBitrotWriters(ws []io.Writer) []error {
for _, w := range ws { errs := make([]error, len(ws))
if w != nil { for i, w := range ws {
if bw, ok := w.(io.Closer); ok { if w == nil {
bw.Close() errs[i] = errDiskNotFound
} continue
}
if bw, ok := w.(io.Closer); ok {
errs[i] = bw.Close()
} else {
errs[i] = nil
} }
} }
return errs
} }
// Returns hash sum for whole-bitrot, nil for streaming-bitrot. // Returns hash sum for whole-bitrot, nil for streaming-bitrot.
@@ -178,7 +184,7 @@ func bitrotVerify(r io.Reader, wantSize, partSize int64, algo BitrotAlgorithm, w
return errFileCorrupt return errFileCorrupt
} }
bufp := xioutil.ODirectPoolSmall.Get().(*[]byte) bufp := xioutil.ODirectPoolSmall.Get()
defer xioutil.ODirectPoolSmall.Put(bufp) defer xioutil.ODirectPoolSmall.Put(bufp)
for left > 0 { for left > 0 {
+1 -3
View File
@@ -48,9 +48,7 @@ func (bs *bootstrapTracer) Events() []madmin.TraceInfo {
traceInfo := make([]madmin.TraceInfo, 0, bootstrapTraceLimit) traceInfo := make([]madmin.TraceInfo, 0, bootstrapTraceLimit)
bs.mu.RLock() bs.mu.RLock()
for _, i := range bs.info { traceInfo = append(traceInfo, bs.info...)
traceInfo = append(traceInfo, i)
}
bs.mu.RUnlock() bs.mu.RUnlock()
return traceInfo return traceInfo
+6 -1
View File
@@ -183,7 +183,12 @@ var binaryChecksum = getBinaryChecksum()
func getBinaryChecksum() string { func getBinaryChecksum() string {
mw := md5.New() mw := md5.New()
b, err := os.Open(os.Args[0]) binPath, err := os.Executable()
if err != nil {
logger.Error("Calculating checksum failed: %s", err)
return "00000000000000000000000000000000"
}
b, err := os.Open(binPath)
if err != nil { if err != nil {
logger.Error("Calculating checksum failed: %s", err) logger.Error("Calculating checksum failed: %s", err)
return "00000000000000000000000000000000" return "00000000000000000000000000000000"
+8 -6
View File
@@ -344,11 +344,9 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
Created: dnsRecords[0].CreationDate, Created: dnsRecords[0].CreationDate,
}) })
} }
sort.Slice(bucketsInfo, func(i, j int) bool { sort.Slice(bucketsInfo, func(i, j int) bool {
return bucketsInfo[i].Name < bucketsInfo[j].Name return bucketsInfo[i].Name < bucketsInfo[j].Name
}) })
} else { } else {
// Invoke the list buckets. // Invoke the list buckets.
var err error var err error
@@ -429,7 +427,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
// Content-Md5 is required should be set // Content-Md5 is required should be set
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html // http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
if _, ok := r.Header[xhttp.ContentMD5]; !ok { if !validateLengthAndChecksum(r) {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL)
return return
} }
@@ -561,7 +559,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
}, goi, opts, gerr) }, goi, opts, gerr)
if dsc.ReplicateAny() { if dsc.ReplicateAny() {
if object.VersionID != "" { if object.VersionID != "" {
object.VersionPurgeStatus = Pending object.VersionPurgeStatus = replication.VersionPurgePending
object.VersionPurgeStatuses = dsc.PendingStatus() object.VersionPurgeStatuses = dsc.PendingStatus()
} else { } else {
object.DeleteMarkerReplicationStatus = dsc.PendingStatus() object.DeleteMarkerReplicationStatus = dsc.PendingStatus()
@@ -671,7 +669,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
continue continue
} }
if replicateDeletes && (dobj.DeleteMarkerReplicationStatus() == replication.Pending || dobj.VersionPurgeStatus() == Pending) { if replicateDeletes && (dobj.DeleteMarkerReplicationStatus() == replication.Pending || dobj.VersionPurgeStatus() == replication.VersionPurgePending) {
// copy so we can re-add null ID. // copy so we can re-add null ID.
dobj := dobj dobj := dobj
if isDirObject(dobj.ObjectName) && dobj.VersionID == "" { if isDirObject(dobj.ObjectName) && dobj.VersionID == "" {
@@ -841,7 +839,6 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
} }
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
} }
apiErr := ErrBucketAlreadyExists apiErr := ErrBucketAlreadyExists
if !globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(sr)...)).IsEmpty() { if !globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(sr)...)).IsEmpty() {
@@ -1338,6 +1335,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
return return
} }
} }
opts.EncryptFn = metadataEncrypter(objectEncryptionKey)
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)
@@ -1809,6 +1807,10 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
return return
} }
// Audit log tags.
reqInfo := logger.GetReqInfo(ctx)
reqInfo.SetTags("retention", config.String())
configData, err := xml.Marshal(config) configData, err := xml.Marshal(config)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
-2
View File
@@ -188,7 +188,6 @@ func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName stri
if errorResponse.Code != testCase.errorResponse.Code { if errorResponse.Code != testCase.errorResponse.Code {
t.Errorf("Test %d: %s: Expected the error code to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Code, errorResponse.Code) t.Errorf("Test %d: %s: Expected the error code to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Code, errorResponse.Code)
} }
} }
// Test for Anonymous/unsigned http request. // Test for Anonymous/unsigned http request.
@@ -290,7 +289,6 @@ func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, api
if recV2.Code != testCase.expectedRespStatus { if recV2.Code != testCase.expectedRespStatus {
t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, recV2.Code) t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, recV2.Code)
} }
} }
// Test for Anonymous/unsigned http request. // Test for Anonymous/unsigned http request.
+1 -1
View File
@@ -26,7 +26,7 @@ import (
//go:generate stringer -type lcEventSrc -trimprefix lcEventSrc_ $GOFILE //go:generate stringer -type lcEventSrc -trimprefix lcEventSrc_ $GOFILE
type lcEventSrc uint8 type lcEventSrc uint8
//revive:disable:var-naming Underscores is used here to indicate where common prefix ends and the enumeration name begins //nolint:staticcheck,revive // Underscores are used here to indicate where common prefix ends and the enumeration name begins
const ( const (
lcEventSrc_None lcEventSrc = iota lcEventSrc_None lcEventSrc = iota
lcEventSrc_Heal lcEventSrc_Heal
+2 -3
View File
@@ -19,7 +19,6 @@ package cmd
import ( import (
"encoding/xml" "encoding/xml"
"io"
"net/http" "net/http"
"strconv" "strconv"
"time" "time"
@@ -53,7 +52,7 @@ func (api objectAPIHandlers) PutBucketLifecycleHandler(w http.ResponseWriter, r
bucket := vars["bucket"] bucket := vars["bucket"]
// PutBucketLifecycle always needs a Content-Md5 // PutBucketLifecycle always needs a Content-Md5
if _, ok := r.Header[xhttp.ContentMD5]; !ok { if !validateLengthAndChecksum(r) {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL)
return return
} }
@@ -70,7 +69,7 @@ func (api objectAPIHandlers) PutBucketLifecycleHandler(w http.ResponseWriter, r
return return
} }
bucketLifecycle, err := lifecycle.ParseLifecycleConfigWithID(io.LimitReader(r.Body, r.ContentLength)) bucketLifecycle, err := lifecycle.ParseLifecycleConfigWithID(r.Body)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
+43 -31
View File
@@ -71,8 +71,12 @@ func NewLifecycleSys() *LifecycleSys {
return &LifecycleSys{} return &LifecycleSys{}
} }
func ilmTrace(startTime time.Time, duration time.Duration, oi ObjectInfo, event string, metadata map[string]string) madmin.TraceInfo { func ilmTrace(startTime time.Time, duration time.Duration, oi ObjectInfo, event string, metadata map[string]string, err string) madmin.TraceInfo {
sz, _ := oi.GetActualSize() sz, _ := oi.GetActualSize()
if metadata == nil {
metadata = make(map[string]string)
}
metadata["version-id"] = oi.VersionID
return madmin.TraceInfo{ return madmin.TraceInfo{
TraceType: madmin.TraceILM, TraceType: madmin.TraceILM,
Time: startTime, Time: startTime,
@@ -81,18 +85,22 @@ func ilmTrace(startTime time.Time, duration time.Duration, oi ObjectInfo, event
Duration: duration, Duration: duration,
Path: pathJoin(oi.Bucket, oi.Name), Path: pathJoin(oi.Bucket, oi.Name),
Bytes: sz, Bytes: sz,
Error: "", Error: err,
Message: getSource(4), Message: getSource(4),
Custom: metadata, Custom: metadata,
} }
} }
func (sys *LifecycleSys) trace(oi ObjectInfo) func(event string, metadata map[string]string) { func (sys *LifecycleSys) trace(oi ObjectInfo) func(event string, metadata map[string]string, err error) {
startTime := time.Now() startTime := time.Now()
return func(event string, metadata map[string]string) { return func(event string, metadata map[string]string, err error) {
duration := time.Since(startTime) duration := time.Since(startTime)
if globalTrace.NumSubscribers(madmin.TraceILM) > 0 { if globalTrace.NumSubscribers(madmin.TraceILM) > 0 {
globalTrace.Publish(ilmTrace(startTime, duration, oi, event, metadata)) e := ""
if err != nil {
e = err.Error()
}
globalTrace.Publish(ilmTrace(startTime, duration, oi, event, metadata, e))
} }
} }
} }
@@ -147,8 +155,8 @@ func (f freeVersionTask) OpHash() uint64 {
return xxh3.HashString(f.TransitionedObject.Tier + f.TransitionedObject.Name) return xxh3.HashString(f.TransitionedObject.Tier + f.TransitionedObject.Name)
} }
func (n newerNoncurrentTask) OpHash() uint64 { func (n noncurrentVersionsTask) OpHash() uint64 {
return xxh3.HashString(n.bucket + n.versions[0].ObjectV.ObjectName) return xxh3.HashString(n.bucket + n.versions[0].ObjectName)
} }
func (j jentry) OpHash() uint64 { func (j jentry) OpHash() uint64 {
@@ -232,14 +240,16 @@ func (es *expiryState) enqueueByDays(oi ObjectInfo, event lifecycle.Event, src l
} }
} }
// enqueueByNewerNoncurrent enqueues object versions expired by func (es *expiryState) enqueueNoncurrentVersions(bucket string, versions []ObjectToDelete, events []lifecycle.Event) {
// NewerNoncurrentVersions limit for expiry.
func (es *expiryState) enqueueByNewerNoncurrent(bucket string, versions []ObjectToDelete, lcEvent lifecycle.Event) {
if len(versions) == 0 { if len(versions) == 0 {
return return
} }
task := newerNoncurrentTask{bucket: bucket, versions: versions, event: lcEvent} task := noncurrentVersionsTask{
bucket: bucket,
versions: versions,
events: events,
}
wrkr := es.getWorkerCh(task.OpHash()) wrkr := es.getWorkerCh(task.OpHash())
if wrkr == nil { if wrkr == nil {
es.stats.missedExpiryTasks.Add(1) es.stats.missedExpiryTasks.Add(1)
@@ -339,8 +349,8 @@ func (es *expiryState) Worker(input <-chan expiryOp) {
} else { } else {
applyExpiryOnNonTransitionedObjects(es.ctx, es.objAPI, v.objInfo, v.event, v.src) applyExpiryOnNonTransitionedObjects(es.ctx, es.objAPI, v.objInfo, v.event, v.src)
} }
case newerNoncurrentTask: case noncurrentVersionsTask:
deleteObjectVersions(es.ctx, es.objAPI, v.bucket, v.versions, v.event) deleteObjectVersions(es.ctx, es.objAPI, v.bucket, v.versions, v.events)
case jentry: case jentry:
transitionLogIf(es.ctx, deleteObjectFromRemoteTier(es.ctx, v.ObjName, v.VersionID, v.TierName)) transitionLogIf(es.ctx, deleteObjectFromRemoteTier(es.ctx, v.ObjName, v.VersionID, v.TierName))
case freeVersionTask: case freeVersionTask:
@@ -348,7 +358,7 @@ func (es *expiryState) Worker(input <-chan expiryOp) {
traceFn := globalLifecycleSys.trace(oi) traceFn := globalLifecycleSys.trace(oi)
if !oi.TransitionedObject.FreeVersion { if !oi.TransitionedObject.FreeVersion {
// nothing to be done // nothing to be done
return continue
} }
ignoreNotFoundErr := func(err error) error { ignoreNotFoundErr := func(err error) error {
@@ -362,7 +372,8 @@ func (es *expiryState) Worker(input <-chan expiryOp) {
err := deleteObjectFromRemoteTier(es.ctx, oi.TransitionedObject.Name, oi.TransitionedObject.VersionID, oi.TransitionedObject.Tier) err := deleteObjectFromRemoteTier(es.ctx, oi.TransitionedObject.Name, oi.TransitionedObject.VersionID, oi.TransitionedObject.Tier)
if ignoreNotFoundErr(err) != nil { if ignoreNotFoundErr(err) != nil {
transitionLogIf(es.ctx, err) transitionLogIf(es.ctx, err)
return traceFn(ILMFreeVersionDelete, nil, err)
continue
} }
// Remove this free version // Remove this free version
@@ -387,12 +398,10 @@ func initBackgroundExpiry(ctx context.Context, objectAPI ObjectLayer) {
globalExpiryState = newExpiryState(ctx, objectAPI, globalILMConfig.getExpirationWorkers()) globalExpiryState = newExpiryState(ctx, objectAPI, globalILMConfig.getExpirationWorkers())
} }
// newerNoncurrentTask encapsulates arguments required by worker to expire objects type noncurrentVersionsTask struct {
// by NewerNoncurrentVersions
type newerNoncurrentTask struct {
bucket string bucket string
versions []ObjectToDelete versions []ObjectToDelete
event lifecycle.Event events []lifecycle.Event
} }
type transitionTask struct { type transitionTask struct {
@@ -1099,17 +1108,20 @@ func isRestoredObjectOnDisk(meta map[string]string) (onDisk bool) {
// ToLifecycleOpts returns lifecycle.ObjectOpts value for oi. // ToLifecycleOpts returns lifecycle.ObjectOpts value for oi.
func (oi ObjectInfo) ToLifecycleOpts() lifecycle.ObjectOpts { func (oi ObjectInfo) ToLifecycleOpts() lifecycle.ObjectOpts {
return lifecycle.ObjectOpts{ return lifecycle.ObjectOpts{
Name: oi.Name, Name: oi.Name,
UserTags: oi.UserTags, UserTags: oi.UserTags,
VersionID: oi.VersionID, VersionID: oi.VersionID,
ModTime: oi.ModTime, ModTime: oi.ModTime,
Size: oi.Size, Size: oi.Size,
IsLatest: oi.IsLatest, IsLatest: oi.IsLatest,
NumVersions: oi.NumVersions, NumVersions: oi.NumVersions,
DeleteMarker: oi.DeleteMarker, DeleteMarker: oi.DeleteMarker,
SuccessorModTime: oi.SuccessorModTime, SuccessorModTime: oi.SuccessorModTime,
RestoreOngoing: oi.RestoreOngoing, RestoreOngoing: oi.RestoreOngoing,
RestoreExpires: oi.RestoreExpires, RestoreExpires: oi.RestoreExpires,
TransitionStatus: oi.TransitionedObject.Status, TransitionStatus: oi.TransitionedObject.Status,
UserDefined: oi.UserDefined,
VersionPurgeStatus: oi.VersionPurgeStatus,
ReplicationStatus: oi.ReplicationStatus,
} }
} }
+10 -10
View File
@@ -243,26 +243,26 @@ func parseRequestToken(token string) (subToken string, nodeIndex int) {
return subToken, nodeIndex return subToken, nodeIndex
} }
func proxyRequestByToken(ctx context.Context, w http.ResponseWriter, r *http.Request, token string) (string, bool) { func proxyRequestByToken(ctx context.Context, w http.ResponseWriter, r *http.Request, token string, returnErr bool) (subToken string, proxied bool, success bool) {
subToken, nodeIndex := parseRequestToken(token) var nodeIndex int
if nodeIndex > 0 { if subToken, nodeIndex = parseRequestToken(token); nodeIndex >= 0 {
return subToken, proxyRequestByNodeIndex(ctx, w, r, nodeIndex) proxied, success = proxyRequestByNodeIndex(ctx, w, r, nodeIndex, returnErr)
} }
return subToken, false return
} }
func proxyRequestByNodeIndex(ctx context.Context, w http.ResponseWriter, r *http.Request, index int) (success bool) { func proxyRequestByNodeIndex(ctx context.Context, w http.ResponseWriter, r *http.Request, index int, returnErr bool) (proxied, success bool) {
if len(globalProxyEndpoints) == 0 { if len(globalProxyEndpoints) == 0 {
return false return
} }
if index < 0 || index >= len(globalProxyEndpoints) { if index < 0 || index >= len(globalProxyEndpoints) {
return false return
} }
ep := globalProxyEndpoints[index] ep := globalProxyEndpoints[index]
if ep.IsLocal { if ep.IsLocal {
return false return
} }
return proxyRequest(ctx, w, r, ep) return true, proxyRequest(ctx, w, r, ep, returnErr)
} }
// ListObjectsV1Handler - GET Bucket (List Objects) Version 1. // ListObjectsV1Handler - GET Bucket (List Objects) Version 1.
+1 -3
View File
@@ -647,9 +647,7 @@ func (sys *BucketMetadataSys) init(ctx context.Context, buckets []string) {
// Reset the state of the BucketMetadataSys. // Reset the state of the BucketMetadataSys.
func (sys *BucketMetadataSys) Reset() { func (sys *BucketMetadataSys) Reset() {
sys.Lock() sys.Lock()
for k := range sys.metadataMap { clear(sys.metadataMap)
delete(sys.metadataMap, k)
}
sys.Unlock() sys.Unlock()
} }
+2 -2
View File
@@ -295,7 +295,7 @@ func checkPutObjectLockAllowed(ctx context.Context, rq *http.Request, bucket, ob
if legalHoldRequested { if legalHoldRequested {
var lerr error var lerr error
if legalHold, lerr = objectlock.ParseObjectLockLegalHoldHeaders(rq.Header); lerr != nil { if legalHold, lerr = objectlock.ParseObjectLockLegalHoldHeaders(rq.Header); lerr != nil {
return mode, retainDate, legalHold, toAPIErrorCode(ctx, err) return mode, retainDate, legalHold, toAPIErrorCode(ctx, lerr)
} }
} }
@@ -305,7 +305,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 && !(replica && rMode == "" && rDate.IsZero()) { 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 {
-1
View File
@@ -112,7 +112,6 @@ func (r *ReplicationStats) collectWorkerMetrics(ctx context.Context) {
r.wlock.Lock() r.wlock.Lock()
r.workers.update() r.workers.update()
r.wlock.Unlock() r.wlock.Unlock()
} }
} }
} }
+12 -33
View File
@@ -125,16 +125,16 @@ func (ri replicatedInfos) VersionPurgeStatus() VersionPurgeStatusType {
completed := 0 completed := 0
for _, v := range ri.Targets { for _, v := range ri.Targets {
switch v.VersionPurgeStatus { switch v.VersionPurgeStatus {
case Failed: case replication.VersionPurgeFailed:
return Failed return replication.VersionPurgeFailed
case Complete: case replication.VersionPurgeComplete:
completed++ completed++
} }
} }
if completed == len(ri.Targets) { if completed == len(ri.Targets) {
return Complete return replication.VersionPurgeComplete
} }
return Pending return replication.VersionPurgePending
} }
func (ri replicatedInfos) VersionPurgeStatusInternal() string { func (ri replicatedInfos) VersionPurgeStatusInternal() string {
@@ -380,7 +380,7 @@ func (rs *ReplicationState) CompositeReplicationStatus() (st replication.StatusT
// CompositeVersionPurgeStatus returns overall replication purge status for the permanent delete being replicated. // CompositeVersionPurgeStatus returns overall replication purge status for the permanent delete being replicated.
func (rs *ReplicationState) CompositeVersionPurgeStatus() VersionPurgeStatusType { func (rs *ReplicationState) CompositeVersionPurgeStatus() VersionPurgeStatusType {
switch VersionPurgeStatusType(rs.VersionPurgeStatusInternal) { switch VersionPurgeStatusType(rs.VersionPurgeStatusInternal) {
case Pending, Complete, Failed: // for backward compatibility case replication.VersionPurgePending, replication.VersionPurgeComplete, replication.VersionPurgeFailed: // for backward compatibility
return VersionPurgeStatusType(rs.VersionPurgeStatusInternal) return VersionPurgeStatusType(rs.VersionPurgeStatusInternal)
default: default:
return getCompositeVersionPurgeStatus(rs.PurgeTargets) return getCompositeVersionPurgeStatus(rs.PurgeTargets)
@@ -478,16 +478,16 @@ func getCompositeVersionPurgeStatus(m map[string]VersionPurgeStatusType) Version
completed := 0 completed := 0
for _, v := range m { for _, v := range m {
switch v { switch v {
case Failed: case replication.VersionPurgeFailed:
return Failed return replication.VersionPurgeFailed
case Complete: case replication.VersionPurgeComplete:
completed++ completed++
} }
} }
if completed == len(m) { if completed == len(m) {
return Complete return replication.VersionPurgeComplete
} }
return Pending return replication.VersionPurgePending
} }
// getHealReplicateObjectInfo returns info needed by heal replication in ReplicateObjectInfo // getHealReplicateObjectInfo returns info needed by heal replication in ReplicateObjectInfo
@@ -635,28 +635,7 @@ type ResyncTarget struct {
} }
// VersionPurgeStatusType represents status of a versioned delete or permanent delete w.r.t bucket replication // VersionPurgeStatusType represents status of a versioned delete or permanent delete w.r.t bucket replication
type VersionPurgeStatusType string type VersionPurgeStatusType = replication.VersionPurgeStatusType
const (
// Pending - versioned delete replication is pending.
Pending VersionPurgeStatusType = "PENDING"
// Complete - versioned delete replication is now complete, erase version on disk.
Complete VersionPurgeStatusType = "COMPLETE"
// Failed - versioned delete replication failed.
Failed VersionPurgeStatusType = "FAILED"
)
// Empty returns true if purge status was not set.
func (v VersionPurgeStatusType) Empty() bool {
return string(v) == ""
}
// Pending returns true if the version is pending purge.
func (v VersionPurgeStatusType) Pending() bool {
return v == Pending || v == Failed
}
type replicationResyncer struct { type replicationResyncer struct {
// map of bucket to their resync status // map of bucket to their resync status
+25 -81
View File
@@ -915,33 +915,29 @@ func (z *ReplicationState) DecodeMsg(dc *msgp.Reader) (err error) {
err = msgp.WrapError(err, "PurgeTargets") err = msgp.WrapError(err, "PurgeTargets")
return return
} }
{ err = za0004.DecodeMsg(dc)
var zb0004 string if err != nil {
zb0004, err = dc.ReadString() err = msgp.WrapError(err, "PurgeTargets", za0003)
if err != nil { return
err = msgp.WrapError(err, "PurgeTargets", za0003)
return
}
za0004 = VersionPurgeStatusType(zb0004)
} }
z.PurgeTargets[za0003] = za0004 z.PurgeTargets[za0003] = za0004
} }
case "ResetStatusesMap": case "ResetStatusesMap":
var zb0005 uint32 var zb0004 uint32
zb0005, err = dc.ReadMapHeader() zb0004, err = dc.ReadMapHeader()
if err != nil { if err != nil {
err = msgp.WrapError(err, "ResetStatusesMap") err = msgp.WrapError(err, "ResetStatusesMap")
return return
} }
if z.ResetStatusesMap == nil { if z.ResetStatusesMap == nil {
z.ResetStatusesMap = make(map[string]string, zb0005) z.ResetStatusesMap = make(map[string]string, zb0004)
} else if len(z.ResetStatusesMap) > 0 { } else if len(z.ResetStatusesMap) > 0 {
for key := range z.ResetStatusesMap { for key := range z.ResetStatusesMap {
delete(z.ResetStatusesMap, key) delete(z.ResetStatusesMap, key)
} }
} }
for zb0005 > 0 { for zb0004 > 0 {
zb0005-- zb0004--
var za0005 string var za0005 string
var za0006 string var za0006 string
za0005, err = dc.ReadString() za0005, err = dc.ReadString()
@@ -1078,7 +1074,7 @@ func (z *ReplicationState) EncodeMsg(en *msgp.Writer) (err error) {
err = msgp.WrapError(err, "PurgeTargets") err = msgp.WrapError(err, "PurgeTargets")
return return
} }
err = en.WriteString(string(za0004)) err = za0004.EncodeMsg(en)
if err != nil { if err != nil {
err = msgp.WrapError(err, "PurgeTargets", za0003) err = msgp.WrapError(err, "PurgeTargets", za0003)
return return
@@ -1154,7 +1150,11 @@ func (z *ReplicationState) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.AppendMapHeader(o, uint32(len(z.PurgeTargets))) o = msgp.AppendMapHeader(o, uint32(len(z.PurgeTargets)))
for za0003, za0004 := range z.PurgeTargets { for za0003, za0004 := range z.PurgeTargets {
o = msgp.AppendString(o, za0003) o = msgp.AppendString(o, za0003)
o = msgp.AppendString(o, string(za0004)) o, err = za0004.MarshalMsg(o)
if err != nil {
err = msgp.WrapError(err, "PurgeTargets", za0003)
return
}
} }
// string "ResetStatusesMap" // string "ResetStatusesMap"
o = append(o, 0xb0, 0x52, 0x65, 0x73, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x4d, 0x61, 0x70) o = append(o, 0xb0, 0x52, 0x65, 0x73, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x4d, 0x61, 0x70)
@@ -1279,35 +1279,31 @@ func (z *ReplicationState) UnmarshalMsg(bts []byte) (o []byte, err error) {
err = msgp.WrapError(err, "PurgeTargets") err = msgp.WrapError(err, "PurgeTargets")
return return
} }
{ bts, err = za0004.UnmarshalMsg(bts)
var zb0004 string if err != nil {
zb0004, bts, err = msgp.ReadStringBytes(bts) err = msgp.WrapError(err, "PurgeTargets", za0003)
if err != nil { return
err = msgp.WrapError(err, "PurgeTargets", za0003)
return
}
za0004 = VersionPurgeStatusType(zb0004)
} }
z.PurgeTargets[za0003] = za0004 z.PurgeTargets[za0003] = za0004
} }
case "ResetStatusesMap": case "ResetStatusesMap":
var zb0005 uint32 var zb0004 uint32
zb0005, bts, err = msgp.ReadMapHeaderBytes(bts) zb0004, bts, err = msgp.ReadMapHeaderBytes(bts)
if err != nil { if err != nil {
err = msgp.WrapError(err, "ResetStatusesMap") err = msgp.WrapError(err, "ResetStatusesMap")
return return
} }
if z.ResetStatusesMap == nil { if z.ResetStatusesMap == nil {
z.ResetStatusesMap = make(map[string]string, zb0005) z.ResetStatusesMap = make(map[string]string, zb0004)
} else if len(z.ResetStatusesMap) > 0 { } else if len(z.ResetStatusesMap) > 0 {
for key := range z.ResetStatusesMap { for key := range z.ResetStatusesMap {
delete(z.ResetStatusesMap, key) delete(z.ResetStatusesMap, key)
} }
} }
for zb0005 > 0 { for zb0004 > 0 {
var za0005 string var za0005 string
var za0006 string var za0006 string
zb0005-- zb0004--
za0005, bts, err = msgp.ReadStringBytes(bts) za0005, bts, err = msgp.ReadStringBytes(bts)
if err != nil { if err != nil {
err = msgp.WrapError(err, "ResetStatusesMap") err = msgp.WrapError(err, "ResetStatusesMap")
@@ -1345,7 +1341,7 @@ func (z *ReplicationState) Msgsize() (s int) {
if z.PurgeTargets != nil { if z.PurgeTargets != nil {
for za0003, za0004 := range z.PurgeTargets { for za0003, za0004 := range z.PurgeTargets {
_ = za0004 _ = za0004
s += msgp.StringPrefixSize + len(za0003) + msgp.StringPrefixSize + len(string(za0004)) s += msgp.StringPrefixSize + len(za0003) + za0004.Msgsize()
} }
} }
s += 17 + msgp.MapHeaderSize s += 17 + msgp.MapHeaderSize
@@ -2507,55 +2503,3 @@ func (z *TargetReplicationResyncStatus) Msgsize() (s int) {
s = 1 + 3 + msgp.TimeSize + 4 + msgp.TimeSize + 3 + msgp.StringPrefixSize + len(z.ResyncID) + 4 + msgp.TimeSize + 4 + msgp.IntSize + 3 + msgp.Int64Size + 4 + msgp.Int64Size + 3 + msgp.Int64Size + 4 + msgp.Int64Size + 4 + msgp.StringPrefixSize + len(z.Bucket) + 4 + msgp.StringPrefixSize + len(z.Object) s = 1 + 3 + msgp.TimeSize + 4 + msgp.TimeSize + 3 + msgp.StringPrefixSize + len(z.ResyncID) + 4 + msgp.TimeSize + 4 + msgp.IntSize + 3 + msgp.Int64Size + 4 + msgp.Int64Size + 3 + msgp.Int64Size + 4 + msgp.Int64Size + 4 + msgp.StringPrefixSize + len(z.Bucket) + 4 + msgp.StringPrefixSize + len(z.Object)
return return
} }
// DecodeMsg implements msgp.Decodable
func (z *VersionPurgeStatusType) DecodeMsg(dc *msgp.Reader) (err error) {
{
var zb0001 string
zb0001, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err)
return
}
(*z) = VersionPurgeStatusType(zb0001)
}
return
}
// EncodeMsg implements msgp.Encodable
func (z VersionPurgeStatusType) EncodeMsg(en *msgp.Writer) (err error) {
err = en.WriteString(string(z))
if err != nil {
err = msgp.WrapError(err)
return
}
return
}
// MarshalMsg implements msgp.Marshaler
func (z VersionPurgeStatusType) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize())
o = msgp.AppendString(o, string(z))
return
}
// UnmarshalMsg implements msgp.Unmarshaler
func (z *VersionPurgeStatusType) UnmarshalMsg(bts []byte) (o []byte, err error) {
{
var zb0001 string
zb0001, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
(*z) = VersionPurgeStatusType(zb0001)
}
o = bts
return
}
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z VersionPurgeStatusType) Msgsize() (s int) {
s = msgp.StringPrefixSize + len(string(z))
return
}
+108 -45
View File
@@ -49,12 +49,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"
xioutil "github.com/minio/minio/internal/ioutil" xioutil "github.com/minio/minio/internal/ioutil"
"github.com/minio/minio/internal/kms"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/minio/minio/internal/once" "github.com/minio/minio/internal/once"
"github.com/tinylib/msgp/msgp" "github.com/tinylib/msgp/msgp"
"github.com/zeebo/xxh3" "github.com/zeebo/xxh3"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
) )
const ( const (
@@ -147,7 +146,7 @@ func validateReplicationDestination(ctx context.Context, bucket string, rCfg *re
if errInt == nil { if errInt == nil {
err = nil err = nil
} else { } else {
err = errInt.(error) err, _ = errInt.(error)
} }
} }
switch err.(type) { switch err.(type) {
@@ -391,7 +390,7 @@ func checkReplicateDelete(ctx context.Context, bucket string, dobj ObjectToDelet
// can be the case that other cluster is down and duplicate `mc rm --vid` // can be the case that other cluster is down and duplicate `mc rm --vid`
// is issued - this still needs to be replicated back to the other target // is issued - this still needs to be replicated back to the other target
if !oi.VersionPurgeStatus.Empty() { if !oi.VersionPurgeStatus.Empty() {
replicate = oi.VersionPurgeStatus == Pending || oi.VersionPurgeStatus == Failed replicate = oi.VersionPurgeStatus == replication.VersionPurgePending || oi.VersionPurgeStatus == replication.VersionPurgeFailed
dsc.Set(newReplicateTargetDecision(tgtArn, replicate, sync)) dsc.Set(newReplicateTargetDecision(tgtArn, replicate, sync))
} }
continue continue
@@ -619,7 +618,7 @@ func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationI
rinfo.ReplicationStatus = rinfo.PrevReplicationStatus rinfo.ReplicationStatus = rinfo.PrevReplicationStatus
return return
} }
if dobj.VersionID != "" && rinfo.VersionPurgeStatus == Complete { if dobj.VersionID != "" && rinfo.VersionPurgeStatus == replication.VersionPurgeComplete {
return return
} }
if globalBucketTargetSys.isOffline(tgt.EndpointURL()) { if globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
@@ -639,7 +638,7 @@ func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationI
if dobj.VersionID == "" { if dobj.VersionID == "" {
rinfo.ReplicationStatus = replication.Failed rinfo.ReplicationStatus = replication.Failed
} else { } else {
rinfo.VersionPurgeStatus = Failed rinfo.VersionPurgeStatus = replication.VersionPurgeFailed
} }
return return
} }
@@ -663,7 +662,7 @@ func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationI
case isErrObjectNotFound(serr), isErrVersionNotFound(serr): case isErrObjectNotFound(serr), isErrVersionNotFound(serr):
// version being purged is already not found on target. // version being purged is already not found on target.
if !rinfo.VersionPurgeStatus.Empty() { if !rinfo.VersionPurgeStatus.Empty() {
rinfo.VersionPurgeStatus = Complete rinfo.VersionPurgeStatus = replication.VersionPurgeComplete
return return
} }
case isErrReadQuorum(serr), isErrWriteQuorum(serr): case isErrReadQuorum(serr), isErrWriteQuorum(serr):
@@ -696,7 +695,7 @@ func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationI
if dobj.VersionID == "" { if dobj.VersionID == "" {
rinfo.ReplicationStatus = replication.Failed rinfo.ReplicationStatus = replication.Failed
} else { } else {
rinfo.VersionPurgeStatus = Failed rinfo.VersionPurgeStatus = replication.VersionPurgeFailed
} }
replLogIf(ctx, fmt.Errorf("unable to replicate delete marker to %s: %s/%s(%s): %w", tgt.EndpointURL(), tgt.Bucket, dobj.ObjectName, versionID, rmErr)) replLogIf(ctx, fmt.Errorf("unable to replicate delete marker to %s: %s/%s(%s): %w", tgt.EndpointURL(), tgt.Bucket, dobj.ObjectName, versionID, rmErr))
if rmErr != nil && minio.IsNetworkOrHostDown(rmErr, true) && !globalBucketTargetSys.isOffline(tgt.EndpointURL()) { if rmErr != nil && minio.IsNetworkOrHostDown(rmErr, true) && !globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
@@ -706,7 +705,7 @@ func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationI
if dobj.VersionID == "" { if dobj.VersionID == "" {
rinfo.ReplicationStatus = replication.Completed rinfo.ReplicationStatus = replication.Completed
} else { } else {
rinfo.VersionPurgeStatus = Complete rinfo.VersionPurgeStatus = replication.VersionPurgeComplete
} }
} }
return return
@@ -775,13 +774,14 @@ func (m caseInsensitiveMap) Lookup(key string) (string, bool) {
return "", false return "", false
} }
func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, partNum int) (putOpts minio.PutObjectOptions, err error) { func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (putOpts minio.PutObjectOptions, isMP bool, err error) {
meta := make(map[string]string) meta := make(map[string]string)
isSSEC := crypto.SSEC.IsEncrypted(objInfo.UserDefined) isSSEC := crypto.SSEC.IsEncrypted(objInfo.UserDefined)
for k, v := range objInfo.UserDefined { for k, v := range objInfo.UserDefined {
_, isValidSSEHeader := validSSEReplicationHeaders[k]
// In case of SSE-C objects copy the allowed internal headers as well // In case of SSE-C objects copy the allowed internal headers as well
if !isSSEC || !slices.Contains(maps.Keys(validSSEReplicationHeaders), k) { if !isSSEC || !isValidSSEHeader {
if stringsHasPrefixFold(k, ReservedMetadataPrefixLower) { if stringsHasPrefixFold(k, ReservedMetadataPrefixLower) {
continue continue
} }
@@ -789,24 +789,28 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
continue continue
} }
} }
if slices.Contains(maps.Keys(validSSEReplicationHeaders), k) { if isValidSSEHeader {
meta[validSSEReplicationHeaders[k]] = v meta[validSSEReplicationHeaders[k]] = v
} else { } else {
meta[k] = v meta[k] = v
} }
} }
isMP = objInfo.isMultipart()
if len(objInfo.Checksum) > 0 { if len(objInfo.Checksum) > 0 {
// Add encrypted CRC to metadata for SSE-C objects. // Add encrypted CRC to metadata for SSE-C objects.
if isSSEC { if isSSEC {
meta[ReplicationSsecChecksumHeader] = base64.StdEncoding.EncodeToString(objInfo.Checksum) meta[ReplicationSsecChecksumHeader] = base64.StdEncoding.EncodeToString(objInfo.Checksum)
} else { } else {
for _, pi := range objInfo.Parts { cs, mp := getCRCMeta(objInfo, 0, nil)
if pi.Number == partNum { // Set object checksum.
for k, v := range pi.Checksums { for k, v := range cs {
meta[k] = v meta[k] = v
} }
} isMP = mp
if !objInfo.isMultipart() && cs[xhttp.AmzChecksumType] == xhttp.AmzChecksumTypeFullObject {
// For objects where checksum is full object, it will be the same.
// Therefore, we use the cheaper PutObject replication.
isMP = false
} }
} }
} }
@@ -837,7 +841,7 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
if tagTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+TaggingTimestamp]; ok { if tagTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+TaggingTimestamp]; ok {
tagTimestamp, err = time.Parse(time.RFC3339Nano, tagTmstampStr) tagTimestamp, err = time.Parse(time.RFC3339Nano, tagTmstampStr)
if err != nil { if err != nil {
return putOpts, err return putOpts, false, err
} }
} }
putOpts.Internal.TaggingTimestamp = tagTimestamp putOpts.Internal.TaggingTimestamp = tagTimestamp
@@ -861,7 +865,7 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
if retainDateStr, ok := lkMap.Lookup(xhttp.AmzObjectLockRetainUntilDate); ok { if retainDateStr, ok := lkMap.Lookup(xhttp.AmzObjectLockRetainUntilDate); ok {
rdate, err := amztime.ISO8601Parse(retainDateStr) rdate, err := amztime.ISO8601Parse(retainDateStr)
if err != nil { if err != nil {
return putOpts, err return putOpts, false, err
} }
putOpts.RetainUntilDate = rdate putOpts.RetainUntilDate = rdate
// set retention timestamp in opts // set retention timestamp in opts
@@ -869,7 +873,7 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
if retainTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp]; ok { if retainTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp]; ok {
retTimestamp, err = time.Parse(time.RFC3339Nano, retainTmstampStr) retTimestamp, err = time.Parse(time.RFC3339Nano, retainTmstampStr)
if err != nil { if err != nil {
return putOpts, err return putOpts, false, err
} }
} }
putOpts.Internal.RetentionTimestamp = retTimestamp putOpts.Internal.RetentionTimestamp = retTimestamp
@@ -881,7 +885,7 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
if lholdTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp]; ok { if lholdTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp]; ok {
lholdTimestamp, err = time.Parse(time.RFC3339Nano, lholdTmstampStr) lholdTimestamp, err = time.Parse(time.RFC3339Nano, lholdTmstampStr)
if err != nil { if err != nil {
return putOpts, err return putOpts, false, err
} }
} }
putOpts.Internal.LegalholdTimestamp = lholdTimestamp putOpts.Internal.LegalholdTimestamp = lholdTimestamp
@@ -889,6 +893,24 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
if crypto.S3.IsEncrypted(objInfo.UserDefined) { if crypto.S3.IsEncrypted(objInfo.UserDefined) {
putOpts.ServerSideEncryption = encrypt.NewSSE() putOpts.ServerSideEncryption = encrypt.NewSSE()
} }
if crypto.S3KMS.IsEncrypted(objInfo.UserDefined) {
// If KMS key ID replication is enabled (as by default)
// we include the object's KMS key ID. In any case, we
// always set the SSE-KMS header. If no KMS key ID is
// specified, MinIO is supposed to use whatever default
// config applies on the site or bucket.
var keyID string
if kms.ReplicateKeyID() {
keyID = objInfo.KMSKeyID()
}
sseEnc, err := encrypt.NewSSEKMS(keyID, nil)
if err != nil {
return putOpts, false, err
}
putOpts.ServerSideEncryption = sseEnc
}
return return
} }
@@ -946,7 +968,11 @@ func getReplicationAction(oi1 ObjectInfo, oi2 minio.ObjectInfo, opType replicati
} }
t, _ := tags.ParseObjectTags(oi1.UserTags) t, _ := tags.ParseObjectTags(oi1.UserTags)
if (oi2.UserTagCount > 0 && !reflect.DeepEqual(oi2.UserTags, t.ToMap())) || (oi2.UserTagCount != len(t.ToMap())) { oi2Map := make(map[string]string)
for k, v := range oi2.UserTags {
oi2Map[k] = v
}
if (oi2.UserTagCount > 0 && !reflect.DeepEqual(oi2Map, t.ToMap())) || (oi2.UserTagCount != len(t.ToMap())) {
return replicateMetadata return replicateMetadata
} }
@@ -1274,7 +1300,7 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
// use core client to avoid doing multipart on PUT // use core client to avoid doing multipart on PUT
c := &minio.Core{Client: tgt.Client} c := &minio.Core{Client: tgt.Client}
putOpts, err := putReplicationOpts(ctx, tgt.StorageClass, objInfo, 0) putOpts, isMP, err := putReplicationOpts(ctx, tgt.StorageClass, objInfo)
if err != nil { if err != nil {
replLogIf(ctx, fmt.Errorf("failure setting options for replication bucket:%s err:%w", bucket, err)) replLogIf(ctx, fmt.Errorf("failure setting options for replication bucket:%s err:%w", bucket, err))
sendEvent(eventArgs{ sendEvent(eventArgs{
@@ -1306,7 +1332,7 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
defer cancel() defer cancel()
} }
r := bandwidth.NewMonitoredReader(newCtx, globalBucketMonitor, gr, opts) r := bandwidth.NewMonitoredReader(newCtx, globalBucketMonitor, gr, opts)
if objInfo.isMultipart() { if isMP {
rinfo.Err = replicateObjectWithMultipart(ctx, c, tgt.Bucket, object, r, objInfo, putOpts) rinfo.Err = replicateObjectWithMultipart(ctx, c, tgt.Bucket, object, r, objInfo, putOpts)
} else { } else {
_, rinfo.Err = c.PutObject(ctx, tgt.Bucket, object, r, size, "", "", putOpts) _, rinfo.Err = c.PutObject(ctx, tgt.Bucket, object, r, size, "", "", putOpts)
@@ -1435,13 +1461,14 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
} }
rinfo.Duration = time.Since(startTime) rinfo.Duration = time.Since(startTime)
}() }()
sOpts := minio.StatObjectOptions{
oi, cerr := tgt.StatObject(ctx, tgt.Bucket, object, minio.StatObjectOptions{
VersionID: objInfo.VersionID, VersionID: objInfo.VersionID,
Internal: minio.AdvancedGetOptions{ Internal: minio.AdvancedGetOptions{
ReplicationProxyRequest: "false", ReplicationProxyRequest: "false",
}, },
}) }
sOpts.Set(xhttp.AmzTagDirective, "ACCESS")
oi, cerr := tgt.StatObject(ctx, tgt.Bucket, object, sOpts)
if cerr == nil { if cerr == nil {
rAction = getReplicationAction(objInfo, oi, ri.OpType) rAction = getReplicationAction(objInfo, oi, ri.OpType)
rinfo.ReplicationStatus = replication.Completed rinfo.ReplicationStatus = replication.Completed
@@ -1526,19 +1553,30 @@ applyAction:
ReplicationRequest: true, // always set this to distinguish between `mc mirror` replication and serverside ReplicationRequest: true, // always set this to distinguish between `mc mirror` replication and serverside
}, },
} }
if tagTmStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+TaggingTimestamp]; ok { // default timestamps to ModTime unless present in metadata
lkMap := caseInsensitiveMap(objInfo.UserDefined)
if _, ok := lkMap.Lookup(xhttp.AmzObjectLockLegalHold); ok {
dstOpts.Internal.LegalholdTimestamp = objInfo.ModTime
}
if _, ok := lkMap.Lookup(xhttp.AmzObjectLockRetainUntilDate); ok {
dstOpts.Internal.RetentionTimestamp = objInfo.ModTime
}
if objInfo.UserTags != "" {
dstOpts.Internal.TaggingTimestamp = objInfo.ModTime
}
if tagTmStr, ok := lkMap.Lookup(ReservedMetadataPrefixLower + TaggingTimestamp); ok {
ondiskTimestamp, err := time.Parse(time.RFC3339, tagTmStr) ondiskTimestamp, err := time.Parse(time.RFC3339, tagTmStr)
if err == nil { if err == nil {
dstOpts.Internal.TaggingTimestamp = ondiskTimestamp dstOpts.Internal.TaggingTimestamp = ondiskTimestamp
} }
} }
if retTmStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp]; ok { if retTmStr, ok := lkMap.Lookup(ReservedMetadataPrefixLower + ObjectLockRetentionTimestamp); ok {
ondiskTimestamp, err := time.Parse(time.RFC3339, retTmStr) ondiskTimestamp, err := time.Parse(time.RFC3339, retTmStr)
if err == nil { if err == nil {
dstOpts.Internal.RetentionTimestamp = ondiskTimestamp dstOpts.Internal.RetentionTimestamp = ondiskTimestamp
} }
} }
if lholdTmStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp]; ok { if lholdTmStr, ok := lkMap.Lookup(ReservedMetadataPrefixLower + ObjectLockLegalHoldTimestamp); ok {
ondiskTimestamp, err := time.Parse(time.RFC3339, lholdTmStr) ondiskTimestamp, err := time.Parse(time.RFC3339, lholdTmStr)
if err == nil { if err == nil {
dstOpts.Internal.LegalholdTimestamp = ondiskTimestamp dstOpts.Internal.LegalholdTimestamp = ondiskTimestamp
@@ -1549,8 +1587,7 @@ applyAction:
replLogIf(ctx, fmt.Errorf("unable to replicate metadata for object %s/%s(%s) to target %s: %w", bucket, objInfo.Name, objInfo.VersionID, tgt.EndpointURL(), rinfo.Err)) replLogIf(ctx, fmt.Errorf("unable to replicate metadata for object %s/%s(%s) to target %s: %w", bucket, objInfo.Name, objInfo.VersionID, tgt.EndpointURL(), rinfo.Err))
} }
} else { } else {
var putOpts minio.PutObjectOptions putOpts, isMP, err := putReplicationOpts(ctx, tgt.StorageClass, objInfo)
putOpts, err = putReplicationOpts(ctx, tgt.StorageClass, objInfo, 0)
if err != nil { if err != nil {
replLogIf(ctx, fmt.Errorf("failed to set replicate options for object %s/%s(%s) (target %s) err:%w", bucket, objInfo.Name, objInfo.VersionID, tgt.EndpointURL(), err)) replLogIf(ctx, fmt.Errorf("failed to set replicate options for object %s/%s(%s) (target %s) err:%w", bucket, objInfo.Name, objInfo.VersionID, tgt.EndpointURL(), err))
sendEvent(eventArgs{ sendEvent(eventArgs{
@@ -1581,7 +1618,7 @@ applyAction:
defer cancel() defer cancel()
} }
r := bandwidth.NewMonitoredReader(newCtx, globalBucketMonitor, gr, opts) r := bandwidth.NewMonitoredReader(newCtx, globalBucketMonitor, gr, opts)
if objInfo.isMultipart() { if isMP {
rinfo.Err = replicateObjectWithMultipart(ctx, c, tgt.Bucket, object, r, objInfo, putOpts) rinfo.Err = replicateObjectWithMultipart(ctx, c, tgt.Bucket, object, r, objInfo, putOpts)
} else { } else {
_, rinfo.Err = c.PutObject(ctx, tgt.Bucket, object, r, size, "", "", putOpts) _, rinfo.Err = c.PutObject(ctx, tgt.Bucket, object, r, size, "", "", putOpts)
@@ -1659,7 +1696,8 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob
cHeader := http.Header{} cHeader := http.Header{}
cHeader.Add(xhttp.MinIOSourceReplicationRequest, "true") cHeader.Add(xhttp.MinIOSourceReplicationRequest, "true")
if !isSSEC { if !isSSEC {
for k, v := range partInfo.Checksums { cs, _ := getCRCMeta(objInfo, partInfo.Number, nil)
for k, v := range cs {
cHeader.Add(k, v) cHeader.Add(k, v)
} }
} }
@@ -1683,12 +1721,13 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob
return fmt.Errorf("ssec(%t): Part size mismatch: got %d, want %d", isSSEC, pInfo.Size, size) return fmt.Errorf("ssec(%t): Part size mismatch: got %d, want %d", isSSEC, pInfo.Size, size)
} }
uploadedParts = append(uploadedParts, minio.CompletePart{ uploadedParts = append(uploadedParts, minio.CompletePart{
PartNumber: pInfo.PartNumber, PartNumber: pInfo.PartNumber,
ETag: pInfo.ETag, ETag: pInfo.ETag,
ChecksumCRC32: pInfo.ChecksumCRC32, ChecksumCRC32: pInfo.ChecksumCRC32,
ChecksumCRC32C: pInfo.ChecksumCRC32C, ChecksumCRC32C: pInfo.ChecksumCRC32C,
ChecksumSHA1: pInfo.ChecksumSHA1, ChecksumSHA1: pInfo.ChecksumSHA1,
ChecksumSHA256: pInfo.ChecksumSHA256, ChecksumSHA256: pInfo.ChecksumSHA256,
ChecksumCRC64NVME: pInfo.ChecksumCRC64NVME,
}) })
} }
userMeta := map[string]string{ userMeta := map[string]string{
@@ -1701,6 +1740,13 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob
// really big value but its okay on heavily loaded systems. This is just tail end timeout. // really big value but its okay on heavily loaded systems. This is just tail end timeout.
cctx, ccancel := context.WithTimeout(ctx, 10*time.Minute) cctx, ccancel := context.WithTimeout(ctx, 10*time.Minute)
defer ccancel() defer ccancel()
if len(objInfo.Checksum) > 0 {
cs, _ := getCRCMeta(objInfo, 0, nil)
for k, v := range cs {
userMeta[k] = strings.Split(v, "-")[0]
}
}
_, err = c.CompleteMultipartUpload(cctx, bucket, object, uploadID, uploadedParts, minio.PutObjectOptions{ _, err = c.CompleteMultipartUpload(cctx, bucket, object, uploadID, uploadedParts, minio.PutObjectOptions{
UserMetadata: userMeta, UserMetadata: userMeta,
Internal: minio.AdvancedPutOptions{ Internal: minio.AdvancedPutOptions{
@@ -3317,7 +3363,7 @@ func getReplicationDiff(ctx context.Context, objAPI ObjectLayer, bucket string,
} }
for arn, st := range roi.TargetPurgeStatuses { for arn, st := range roi.TargetPurgeStatuses {
if opts.ARN == "" || opts.ARN == arn { if opts.ARN == "" || opts.ARN == arn {
if !opts.Verbose && st == Complete { if !opts.Verbose && st == replication.VersionPurgeComplete {
continue continue
} }
t, ok := tgtsMap[arn] t, ok := tgtsMap[arn]
@@ -3416,7 +3462,7 @@ func queueReplicationHeal(ctx context.Context, bucket string, oi ObjectInfo, rcf
// heal delete marker replication failure or versioned delete replication failure // heal delete marker replication failure or versioned delete replication failure
if roi.ReplicationStatus == replication.Pending || if roi.ReplicationStatus == replication.Pending ||
roi.ReplicationStatus == replication.Failed || roi.ReplicationStatus == replication.Failed ||
roi.VersionPurgeStatus == Failed || roi.VersionPurgeStatus == Pending { roi.VersionPurgeStatus == replication.VersionPurgeFailed || roi.VersionPurgeStatus == replication.VersionPurgePending {
globalReplicationPool.Get().queueReplicaDeleteTask(dv) globalReplicationPool.Get().queueReplicaDeleteTask(dv)
return return
} }
@@ -3704,7 +3750,7 @@ func (p *ReplicationPool) queueMRFHeal() error {
} }
func (p *ReplicationPool) initialized() bool { func (p *ReplicationPool) initialized() bool {
return !(p == nil || p.objLayer == nil) return p != nil && p.objLayer != nil
} }
// getMRF returns MRF entries for this node. // getMRF returns MRF entries for this node.
@@ -3746,3 +3792,20 @@ type validateReplicationDestinationOptions struct {
checkReadyErr sync.Map checkReadyErr sync.Map
} }
func getCRCMeta(oi ObjectInfo, partNum int, h http.Header) (cs map[string]string, isMP bool) {
meta := make(map[string]string)
cs, isMP = oi.decryptChecksums(partNum, h)
for k, v := range cs {
cksum := hash.NewChecksumString(k, v)
if cksum == nil {
continue
}
if cksum.Valid() {
meta[cksum.Type.Key()] = v
meta[xhttp.AmzChecksumType] = cs[xhttp.AmzChecksumType]
meta[xhttp.AmzChecksumAlgo] = cksum.Type.String()
}
}
return meta, isMP
}
+7 -7
View File
@@ -108,18 +108,18 @@ func (l ReplicationLastHour) merge(o ReplicationLastHour) (merged ReplicationLas
// Add a new duration data // Add a new duration data
func (l *ReplicationLastHour) addsize(sz int64) { func (l *ReplicationLastHour) addsize(sz int64) {
min := time.Now().Unix() / 60 minutes := time.Now().Unix() / 60
l.forwardTo(min) l.forwardTo(minutes)
winIdx := min % 60 winIdx := minutes % 60
l.Totals[winIdx].merge(AccElem{Total: min, Size: sz, N: 1}) l.Totals[winIdx].merge(AccElem{Total: minutes, Size: sz, N: 1})
l.LastMin = min l.LastMin = minutes
} }
// Merge all recorded counts of last hour into one // Merge all recorded counts of last hour into one
func (l *ReplicationLastHour) getTotal() AccElem { func (l *ReplicationLastHour) getTotal() AccElem {
var res AccElem var res AccElem
min := time.Now().Unix() / 60 minutes := time.Now().Unix() / 60
l.forwardTo(min) l.forwardTo(minutes)
for _, elem := range l.Totals[:] { for _, elem := range l.Totals[:] {
res.merge(elem) res.merge(elem)
} }
+4 -11
View File
@@ -91,9 +91,6 @@ func init() {
logger.Init(GOPATH, GOROOT) logger.Init(GOPATH, GOROOT)
logger.RegisterError(config.FmtError) logger.RegisterError(config.FmtError)
globalBatchJobsMetrics = batchJobMetrics{metrics: make(map[string]*batchJobInfo)}
go globalBatchJobsMetrics.purgeJobMetrics()
t, _ := minioVersionToReleaseTime(Version) t, _ := minioVersionToReleaseTime(Version)
if !t.IsZero() { if !t.IsZero() {
globalVersionUnix = uint64(t.Unix()) globalVersionUnix = uint64(t.Unix())
@@ -385,7 +382,7 @@ func buildServerCtxt(ctx *cli.Context, ctxt *serverCtxt) (err error) {
} }
// Check "no-compat" flag from command line argument. // Check "no-compat" flag from command line argument.
ctxt.StrictS3Compat = !(ctx.IsSet("no-compat") || ctx.GlobalIsSet("no-compat")) ctxt.StrictS3Compat = !ctx.IsSet("no-compat") && !ctx.GlobalIsSet("no-compat")
switch { switch {
case ctx.IsSet("config-dir"): case ctx.IsSet("config-dir"):
@@ -720,9 +717,7 @@ func serverHandleEnvVars() {
logger.Fatal(err, "Invalid MINIO_BROWSER_REDIRECT_URL value in environment variable") logger.Fatal(err, "Invalid MINIO_BROWSER_REDIRECT_URL value in environment variable")
} }
// Look for if URL has invalid values and return error. // Look for if URL has invalid values and return error.
if !((u.Scheme == "http" || u.Scheme == "https") && if !isValidURLEndpoint((*url.URL)(u)) {
u.Opaque == "" &&
!u.ForceQuery && u.RawQuery == "" && u.Fragment == "") {
err := fmt.Errorf("URL contains unexpected resources, expected URL to be one of http(s)://console.example.com or as a subpath via API endpoint http(s)://minio.example.com/minio format: %v", u) err := fmt.Errorf("URL contains unexpected resources, expected URL to be one of http(s)://console.example.com or as a subpath via API endpoint http(s)://minio.example.com/minio format: %v", u)
logger.Fatal(err, "Invalid MINIO_BROWSER_REDIRECT_URL value is environment variable") logger.Fatal(err, "Invalid MINIO_BROWSER_REDIRECT_URL value is environment variable")
} }
@@ -737,9 +732,7 @@ func serverHandleEnvVars() {
logger.Fatal(err, "Invalid MINIO_SERVER_URL value in environment variable") logger.Fatal(err, "Invalid MINIO_SERVER_URL value in environment variable")
} }
// Look for if URL has invalid values and return error. // Look for if URL has invalid values and return error.
if !((u.Scheme == "http" || u.Scheme == "https") && if !isValidURLEndpoint((*url.URL)(u)) {
(u.Path == "/" || u.Path == "") && u.Opaque == "" &&
!u.ForceQuery && u.RawQuery == "" && u.Fragment == "") {
err := fmt.Errorf("URL contains unexpected resources, expected URL to be of http(s)://minio.example.com format: %v", u) err := fmt.Errorf("URL contains unexpected resources, expected URL to be of http(s)://minio.example.com format: %v", u)
logger.Fatal(err, "Invalid MINIO_SERVER_URL value is environment variable") logger.Fatal(err, "Invalid MINIO_SERVER_URL value is environment variable")
} }
@@ -918,7 +911,7 @@ func handleKMSConfig() {
} }
func getTLSConfig() (x509Certs []*x509.Certificate, manager *certs.Manager, secureConn bool, err error) { func getTLSConfig() (x509Certs []*x509.Certificate, manager *certs.Manager, secureConn bool, err error) {
if !(isFile(getPublicCertFile()) && isFile(getPrivateKeyFile())) { if !isFile(getPublicCertFile()) || !isFile(getPrivateKeyFile()) {
return nil, nil, false, nil return nil, nil, false, nil
} }
+2 -2
View File
@@ -45,7 +45,7 @@ func Test_readFromSecret(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) {
tmpfile, err := os.CreateTemp("", "testfile") tmpfile, err := os.CreateTemp(t.TempDir(), "testfile")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@@ -157,7 +157,7 @@ MINIO_ROOT_PASSWORD=minio123`,
for _, testCase := range testCases { for _, testCase := range testCases {
testCase := testCase testCase := testCase
t.Run("", func(t *testing.T) { t.Run("", func(t *testing.T) {
tmpfile, err := os.CreateTemp("", "testfile") tmpfile, err := os.CreateTemp(t.TempDir(), "testfile")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
+1 -1
View File
@@ -25,11 +25,11 @@ import (
"sync/atomic" "sync/atomic"
"github.com/minio/madmin-go/v3" "github.com/minio/madmin-go/v3"
"github.com/minio/madmin-go/v3/logger/log"
"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"
"github.com/minio/minio/internal/pubsub" "github.com/minio/minio/internal/pubsub"
"github.com/minio/pkg/v3/logger/message/log"
xnet "github.com/minio/pkg/v3/net" xnet "github.com/minio/pkg/v3/net"
) )
+16
View File
@@ -104,6 +104,22 @@ func (p *scannerMetrics) log(s scannerMetric, paths ...string) func(custom map[s
} }
} }
// time n scanner actions.
// Use for s < scannerMetricLastRealtime
func (p *scannerMetrics) timeN(s scannerMetric) func(n int) func() {
startTime := time.Now()
return func(n int) func() {
return func() {
duration := time.Since(startTime)
atomic.AddUint64(&p.operations[s], uint64(n))
if s < scannerMetricLastRealtime {
p.latency[s].add(duration)
}
}
}
}
// time a scanner action. // time a scanner action.
// Use for s < scannerMetricLastRealtime // Use for s < scannerMetricLastRealtime
func (p *scannerMetrics) time(s scannerMetric) func() { func (p *scannerMetrics) time(s scannerMetric) func() {
+130 -204
View File
@@ -37,9 +37,7 @@ import (
"github.com/minio/madmin-go/v3" "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"
objectlock "github.com/minio/minio/internal/bucket/object/lock"
"github.com/minio/minio/internal/bucket/replication" "github.com/minio/minio/internal/bucket/replication"
"github.com/minio/minio/internal/bucket/versioning"
"github.com/minio/minio/internal/color" "github.com/minio/minio/internal/color"
"github.com/minio/minio/internal/config/heal" "github.com/minio/minio/internal/config/heal"
"github.com/minio/minio/internal/event" "github.com/minio/minio/internal/event"
@@ -664,6 +662,12 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
into.addChild(h) into.addChild(h)
continue continue
} }
// Adjust the probability of healing.
// This first removes lowest x from the mod check and makes it x times more likely.
// So if duudc = 10 and we want heal check every 50 cycles, we check
// if (cycle/10) % (50/10) == 0, which would make heal checks run once every 50 cycles,
// if the objects are pre-selected as 1:10.
folder.objectHealProbDiv = dataUsageUpdateDirCycles
} }
f.updateCurrentPath(folder.name) f.updateCurrentPath(folder.name)
stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, folder.name) stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, folder.name)
@@ -858,8 +862,8 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
} }
} }
} }
} }
if compact { if compact {
stop := globalScannerMetrics.log(scannerMetricCompactFolder, folder.name) stop := globalScannerMetrics.log(scannerMetricCompactFolder, folder.name)
f.newCache.deleteRecursive(thisHash) f.newCache.deleteRecursive(thisHash)
@@ -873,7 +877,6 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
} }
stop(total) stop(total)
} }
} }
// Compact if too many children... // Compact if too many children...
if !into.Compacted { if !into.Compacted {
@@ -946,10 +949,7 @@ func (i *scannerItem) transformMetaDir() {
i.objectName = split[len(split)-1] i.objectName = split[len(split)-1]
} }
var ( var applyActionsLogPrefix = color.Green("applyActions:")
applyActionsLogPrefix = color.Green("applyActions:")
applyVersionActionsLogPrefix = color.Green("applyVersionActions:")
)
func (i *scannerItem) applyHealing(ctx context.Context, o ObjectLayer, oi ObjectInfo) (size int64) { func (i *scannerItem) applyHealing(ctx context.Context, o ObjectLayer, oi ObjectInfo) (size int64) {
if i.debug { if i.debug {
@@ -974,153 +974,8 @@ func (i *scannerItem) applyHealing(ctx context.Context, o ObjectLayer, oi Object
return 0 return 0
} }
func (i *scannerItem) applyLifecycle(ctx context.Context, o ObjectLayer, oi ObjectInfo) (action lifecycle.Action, size int64) { func (i *scannerItem) alertExcessiveVersions(remainingVersions int, cumulativeSize int64) {
size, err := oi.GetActualSize() if remainingVersions >= int(scannerExcessObjectVersions.Load()) {
if i.debug {
scannerLogIf(ctx, err)
}
if i.lifeCycle == nil {
return action, size
}
versionID := oi.VersionID
var vc *versioning.Versioning
var lr objectlock.Retention
var rcfg *replication.Config
if !isMinioMetaBucketName(i.bucket) {
vc, err = globalBucketVersioningSys.Get(i.bucket)
if err != nil {
scannerLogOnceIf(ctx, err, i.bucket)
return
}
// Check if bucket is object locked.
lr, err = globalBucketObjectLockSys.Get(i.bucket)
if err != nil {
scannerLogOnceIf(ctx, err, i.bucket)
return
}
rcfg, err = getReplicationConfig(ctx, i.bucket)
if err != nil {
scannerLogOnceIf(ctx, err, i.bucket)
return
}
}
lcEvt := evalActionFromLifecycle(ctx, *i.lifeCycle, lr, rcfg, oi)
if i.debug {
if versionID != "" {
console.Debugf(applyActionsLogPrefix+" lifecycle: %q (version-id=%s), Initial scan: %v\n", i.objectPath(), versionID, lcEvt.Action)
} else {
console.Debugf(applyActionsLogPrefix+" lifecycle: %q Initial scan: %v\n", i.objectPath(), lcEvt.Action)
}
}
switch lcEvt.Action {
// This version doesn't contribute towards sizeS only when it is permanently deleted.
// This can happen when,
// - ExpireObjectAllVersions flag is enabled
// - NoncurrentVersionExpiration is applicable
case lifecycle.DeleteVersionAction, lifecycle.DeleteAllVersionsAction, lifecycle.DelMarkerDeleteAllVersionsAction:
size = 0
case lifecycle.DeleteAction:
// On a non-versioned bucket, DeleteObject removes the only version permanently.
if !vc.PrefixEnabled(oi.Name) {
size = 0
}
}
applyLifecycleAction(lcEvt, lcEventSrc_Scanner, oi)
return lcEvt.Action, size
}
// applyNewerNoncurrentVersionLimit removes noncurrent versions older than the most recent NewerNoncurrentVersions configured.
// Note: This function doesn't update sizeSummary since it always removes versions that it doesn't return.
func (i *scannerItem) applyNewerNoncurrentVersionLimit(ctx context.Context, _ ObjectLayer, fivs []FileInfo, expState *expiryState) ([]ObjectInfo, error) {
done := globalScannerMetrics.time(scannerMetricApplyNonCurrent)
defer done()
rcfg, _ := globalBucketObjectLockSys.Get(i.bucket)
vcfg, _ := globalBucketVersioningSys.Get(i.bucket)
versioned := vcfg != nil && vcfg.Versioned(i.objectPath())
objectInfos := make([]ObjectInfo, 0, len(fivs))
if i.lifeCycle == nil {
for _, fi := range fivs {
objectInfos = append(objectInfos, fi.ToObjectInfo(i.bucket, i.objectPath(), versioned))
}
return objectInfos, nil
}
event := i.lifeCycle.NoncurrentVersionsExpirationLimit(lifecycle.ObjectOpts{Name: i.objectPath()})
lim := event.NewerNoncurrentVersions
if lim == 0 || len(fivs) <= lim+1 { // fewer than lim _noncurrent_ versions
for _, fi := range fivs {
objectInfos = append(objectInfos, fi.ToObjectInfo(i.bucket, i.objectPath(), versioned))
}
return objectInfos, nil
}
overflowVersions := fivs[lim+1:]
// Retain the current version + most recent lim noncurrent versions
for _, fi := range fivs[:lim+1] {
objectInfos = append(objectInfos, fi.ToObjectInfo(i.bucket, i.objectPath(), versioned))
}
toDel := make([]ObjectToDelete, 0, len(overflowVersions))
for _, fi := range overflowVersions {
obj := fi.ToObjectInfo(i.bucket, i.objectPath(), versioned)
// skip versions with object locking enabled
if rcfg.LockEnabled && enforceRetentionForDeletion(ctx, obj) {
if i.debug {
if obj.VersionID != "" {
console.Debugf(applyVersionActionsLogPrefix+" lifecycle: %s v(%s) is locked, not deleting\n", obj.Name, obj.VersionID)
} else {
console.Debugf(applyVersionActionsLogPrefix+" lifecycle: %s is locked, not deleting\n", obj.Name)
}
}
// add this version back to remaining versions for
// subsequent lifecycle policy applications
objectInfos = append(objectInfos, obj)
continue
}
// NoncurrentDays not passed yet.
if time.Now().UTC().Before(lifecycle.ExpectedExpiryTime(obj.SuccessorModTime, event.NoncurrentDays)) {
// add this version back to remaining versions for
// subsequent lifecycle policy applications
objectInfos = append(objectInfos, obj)
continue
}
toDel = append(toDel, ObjectToDelete{
ObjectV: ObjectV{
ObjectName: obj.Name,
VersionID: obj.VersionID,
},
})
}
if len(toDel) > 0 {
expState.enqueueByNewerNoncurrent(i.bucket, toDel, event)
}
return objectInfos, nil
}
// applyVersionActions will apply lifecycle checks on all versions of a scanned item. Returns versions that remain
// after applying lifecycle checks configured.
func (i *scannerItem) applyVersionActions(ctx context.Context, o ObjectLayer, fivs []FileInfo, expState *expiryState) ([]ObjectInfo, error) {
objInfos, err := i.applyNewerNoncurrentVersionLimit(ctx, o, fivs, expState)
if err != nil {
return nil, err
}
// Check if we have many versions after applyNewerNoncurrentVersionLimit.
if len(objInfos) >= int(scannerExcessObjectVersions.Load()) {
// Notify object accessed via a GET request. // Notify object accessed via a GET request.
sendEvent(eventArgs{ sendEvent(eventArgs{
EventName: event.ObjectManyVersions, EventName: event.ObjectManyVersions,
@@ -1130,7 +985,7 @@ func (i *scannerItem) applyVersionActions(ctx context.Context, o ObjectLayer, fi
}, },
UserAgent: "Scanner", UserAgent: "Scanner",
Host: globalLocalNodeName, Host: globalLocalNodeName,
RespElements: map[string]string{"x-minio-versions": strconv.Itoa(len(objInfos))}, RespElements: map[string]string{"x-minio-versions": strconv.Itoa(remainingVersions)},
}) })
auditLogInternal(context.Background(), AuditLogOptions{ auditLogInternal(context.Background(), AuditLogOptions{
@@ -1139,15 +994,11 @@ func (i *scannerItem) applyVersionActions(ctx context.Context, o ObjectLayer, fi
Bucket: i.bucket, Bucket: i.bucket,
Object: i.objectPath(), Object: i.objectPath(),
Tags: map[string]string{ Tags: map[string]string{
"x-minio-versions": strconv.Itoa(len(objInfos)), "x-minio-versions": strconv.Itoa(remainingVersions),
}, },
}) })
} }
cumulativeSize := int64(0)
for _, objInfo := range objInfos {
cumulativeSize += objInfo.Size
}
// Check if the cumulative size of all versions of this object is high. // Check if the cumulative size of all versions of this object is high.
if cumulativeSize >= scannerExcessObjectVersionsTotalSize.Load() { if cumulativeSize >= scannerExcessObjectVersionsTotalSize.Load() {
// Notify object accessed via a GET request. // Notify object accessed via a GET request.
@@ -1160,7 +1011,7 @@ func (i *scannerItem) applyVersionActions(ctx context.Context, o ObjectLayer, fi
UserAgent: "Scanner", UserAgent: "Scanner",
Host: globalLocalNodeName, Host: globalLocalNodeName,
RespElements: map[string]string{ RespElements: map[string]string{
"x-minio-versions-count": strconv.Itoa(len(objInfos)), "x-minio-versions-count": strconv.Itoa(remainingVersions),
"x-minio-versions-size": strconv.FormatInt(cumulativeSize, 10), "x-minio-versions-size": strconv.FormatInt(cumulativeSize, 10),
}, },
}) })
@@ -1171,43 +1022,30 @@ func (i *scannerItem) applyVersionActions(ctx context.Context, o ObjectLayer, fi
Bucket: i.bucket, Bucket: i.bucket,
Object: i.objectPath(), Object: i.objectPath(),
Tags: map[string]string{ Tags: map[string]string{
"x-minio-versions-count": strconv.Itoa(len(objInfos)), "x-minio-versions-count": strconv.Itoa(remainingVersions),
"x-minio-versions-size": strconv.FormatInt(cumulativeSize, 10), "x-minio-versions-size": strconv.FormatInt(cumulativeSize, 10),
}, },
}) })
} }
return objInfos, nil
} }
type actionsAccountingFn func(oi ObjectInfo, sz, actualSz int64, sizeS *sizeSummary)
// applyActions will apply lifecycle checks on to a scanned item. // applyActions will apply lifecycle checks on to a scanned item.
// The resulting size on disk will always be returned. // The resulting size on disk will always be returned.
// The metadata will be compared to consensus on the object layer before any changes are applied. // The metadata will be compared to consensus on the object layer before any changes are applied.
// If no metadata is supplied, -1 is returned if no action is taken. // If no metadata is supplied, -1 is returned if no action is taken.
func (i *scannerItem) applyActions(ctx context.Context, o ObjectLayer, oi ObjectInfo, sizeS *sizeSummary) (objDeleted bool, size int64) { func (i *scannerItem) applyActions(ctx context.Context, objAPI ObjectLayer, objInfos []ObjectInfo, lr lock.Retention, sizeS *sizeSummary, fn actionsAccountingFn) {
done := globalScannerMetrics.time(scannerMetricILM) healActions := func(oi ObjectInfo, actualSz int64) int64 {
var action lifecycle.Action size := actualSz
action, size = i.applyLifecycle(ctx, o, oi)
done()
// Note: objDeleted is true if and only if action ==
// lifecycle.DeleteAllVersionsAction
if action.DeleteAll() {
return true, 0
}
// For instance, an applied lifecycle means we remove/transitioned an object
// from the current deployment, which means we don't have to call healing
// routine even if we are asked to do via heal flag.
if action == lifecycle.NoneAction {
if i.heal.enabled { if i.heal.enabled {
done := globalScannerMetrics.time(scannerMetricHealCheck) done := globalScannerMetrics.time(scannerMetricHealCheck)
size = i.applyHealing(ctx, o, oi) size = i.applyHealing(ctx, objAPI, oi)
done() done()
if healDeleteDangling { if healDeleteDangling {
done := globalScannerMetrics.time(scannerMetricCleanAbandoned) done := globalScannerMetrics.time(scannerMetricCleanAbandoned)
err := o.CheckAbandonedParts(ctx, i.bucket, i.objectPath(), madmin.HealOpts{Remove: healDeleteDangling}) err := objAPI.CheckAbandonedParts(ctx, i.bucket, i.objectPath(), madmin.HealOpts{Remove: healDeleteDangling})
done() done()
if err != nil { if err != nil {
healingLogIf(ctx, fmt.Errorf("unable to check object %s/%s for abandoned data: %w", i.bucket, i.objectPath(), err), i.objectPath()) healingLogIf(ctx, fmt.Errorf("unable to check object %s/%s for abandoned data: %w", i.bucket, i.objectPath(), err), i.objectPath())
@@ -1217,10 +1055,109 @@ func (i *scannerItem) applyActions(ctx context.Context, o ObjectLayer, oi Object
// replicate only if lifecycle rules are not applied. // replicate only if lifecycle rules are not applied.
done := globalScannerMetrics.time(scannerMetricCheckReplication) done := globalScannerMetrics.time(scannerMetricCheckReplication)
i.healReplication(ctx, o, oi.Clone(), sizeS) i.healReplication(ctx, oi.Clone(), sizeS)
done() done()
return size
} }
return false, size
vc, err := globalBucketVersioningSys.Get(i.bucket)
if err != nil {
scannerLogOnceIf(ctx, err, i.bucket)
return
}
// start ILM check timer
done := globalScannerMetrics.timeN(scannerMetricILM)
if i.lifeCycle == nil { // no ILM configured, apply healing and replication checks
var cumulativeSize int64
for _, oi := range objInfos {
actualSz, err := oi.GetActualSize()
if err != nil {
scannerLogIf(ctx, err)
continue
}
size := healActions(oi, actualSz)
if fn != nil { // call accountingfn
fn(oi, size, actualSz, sizeS)
}
cumulativeSize += size
}
// end ILM check timer
done(len(objInfos))
i.alertExcessiveVersions(len(objInfos), cumulativeSize)
return
}
objOpts := make([]lifecycle.ObjectOpts, len(objInfos))
for i, oi := range objInfos {
objOpts[i] = oi.ToLifecycleOpts()
}
evaluator := lifecycle.NewEvaluator(*i.lifeCycle).WithLockRetention(&lr).WithReplicationConfig(i.replication.Config)
events, err := evaluator.Eval(objOpts)
if err != nil {
// This error indicates that the objOpts passed to Eval is invalid.
bugLogIf(ctx, err, i.bucket, i.objectPath())
done(len(objInfos)) // end ILM check timer
return
}
done(len(objInfos)) // end ILM check timer
var (
toDel []ObjectToDelete
noncurrentEvents []lifecycle.Event
cumulativeSize int64
)
remainingVersions := len(objInfos)
eventLoop:
for idx, event := range events {
oi := objInfos[idx]
actualSz, err := oi.GetActualSize()
if i.debug {
scannerLogIf(ctx, err)
}
size := actualSz
switch event.Action {
case lifecycle.DeleteAllVersionsAction, lifecycle.DelMarkerDeleteAllVersionsAction:
remainingVersions = 0
applyExpiryRule(event, lcEventSrc_Scanner, oi)
break eventLoop
case lifecycle.DeleteAction, lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction:
if !vc.PrefixEnabled(i.objectPath()) && event.Action == lifecycle.DeleteAction {
remainingVersions--
size = 0
}
applyExpiryRule(event, lcEventSrc_Scanner, oi)
case lifecycle.DeleteVersionAction: // noncurrent versions expiration
opts := objOpts[idx]
remainingVersions--
size = 0
toDel = append(toDel, ObjectToDelete{
ObjectV: ObjectV{
ObjectName: opts.Name,
VersionID: opts.VersionID,
},
})
noncurrentEvents = append(noncurrentEvents, event)
case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
applyTransitionRule(event, lcEventSrc_Scanner, oi)
case lifecycle.NoneAction:
size = healActions(oi, actualSz)
}
// NB fn must be called for every object version except if it is
// expired or was a dangling object.
if fn != nil {
fn(oi, size, actualSz, sizeS)
}
cumulativeSize += size
}
if len(toDel) > 0 {
globalExpiryState.enqueueNoncurrentVersions(i.bucket, toDel, noncurrentEvents)
}
i.alertExcessiveVersions(remainingVersions, cumulativeSize)
} }
func evalActionFromLifecycle(ctx context.Context, lc lifecycle.Lifecycle, lr lock.Retention, rcfg *replication.Config, obj ObjectInfo) lifecycle.Event { func evalActionFromLifecycle(ctx context.Context, lc lifecycle.Lifecycle, lr lock.Retention, rcfg *replication.Config, obj ObjectInfo) lifecycle.Event {
@@ -1325,10 +1262,13 @@ func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLay
dobj, err = objLayer.DeleteObject(ctx, obj.Bucket, encodeDirObject(obj.Name), opts) dobj, err = objLayer.DeleteObject(ctx, obj.Bucket, encodeDirObject(obj.Name), opts)
if err != nil { if err != nil {
if isErrObjectNotFound(err) || isErrVersionNotFound(err) { if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
traceFn(ILMExpiry, nil, nil)
return false return false
} }
// Assume it is still there. // Assume it is still there.
ilmLogOnceIf(ctx, fmt.Errorf("DeleteObject(%s, %s): %w", obj.Bucket, obj.Name, err), "non-transition-expiry"+obj.Name) err := fmt.Errorf("DeleteObject(%s, %s): %w", obj.Bucket, obj.Name, err)
ilmLogOnceIf(ctx, err, "non-transition-expiry"+obj.Name)
traceFn(ILMExpiry, nil, err)
return false return false
} }
if dobj.Name == "" { if dobj.Name == "" {
@@ -1364,22 +1304,8 @@ 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(event lifecycle.Event, src lcEventSrc, obj ObjectInfo) bool { func applyExpiryRule(event lifecycle.Event, src lcEventSrc, obj ObjectInfo) {
globalExpiryState.enqueueByDays(obj, event, src) globalExpiryState.enqueueByDays(obj, event, src)
return true
}
// Perform actions (removal or transitioning of objects), return true the action is successfully performed
func applyLifecycleAction(event lifecycle.Event, src lcEventSrc, obj ObjectInfo) (success bool) {
switch action := event.Action; action {
case lifecycle.DeleteVersionAction, lifecycle.DeleteAction,
lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction,
lifecycle.DeleteAllVersionsAction, lifecycle.DelMarkerDeleteAllVersionsAction:
success = applyExpiryRule(event, src, obj)
case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
success = applyTransitionRule(event, src, obj)
}
return
} }
// objectPath returns the prefix and object name. // objectPath returns the prefix and object name.
@@ -1388,7 +1314,7 @@ func (i *scannerItem) objectPath() string {
} }
// healReplication will heal a scanned item that has failed replication. // healReplication will heal a scanned item that has failed replication.
func (i *scannerItem) healReplication(ctx context.Context, o ObjectLayer, oi ObjectInfo, sizeS *sizeSummary) { func (i *scannerItem) healReplication(ctx context.Context, oi ObjectInfo, sizeS *sizeSummary) {
if oi.VersionID == "" { if oi.VersionID == "" {
return return
} }
@@ -1549,7 +1475,7 @@ const (
ILMTransition = " ilm:transition" ILMTransition = " ilm:transition"
) )
func auditLogLifecycle(ctx context.Context, oi ObjectInfo, event string, tags map[string]string, traceFn func(event string, metadata map[string]string)) { func auditLogLifecycle(ctx context.Context, oi ObjectInfo, event string, tags map[string]string, traceFn func(event string, metadata map[string]string, err error)) {
var apiName string var apiName string
switch event { switch event {
case ILMExpiry: case ILMExpiry:
@@ -1567,5 +1493,5 @@ func auditLogLifecycle(ctx context.Context, oi ObjectInfo, event string, tags ma
VersionID: oi.VersionID, VersionID: oi.VersionID,
Tags: tags, Tags: tags,
}) })
traceFn(event, tags) traceFn(event, tags, nil)
} }
+231 -68
View File
@@ -21,58 +21,65 @@ import (
"context" "context"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"slices"
"strings" "strings"
"sync" "sync"
"testing" "testing"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/minio/minio/internal/amztime"
"github.com/minio/minio/internal/bucket/lifecycle" "github.com/minio/minio/internal/bucket/lifecycle"
"github.com/minio/minio/internal/bucket/object/lock" objectlock "github.com/minio/minio/internal/bucket/object/lock"
"github.com/minio/minio/internal/bucket/replication"
"github.com/minio/minio/internal/bucket/versioning" "github.com/minio/minio/internal/bucket/versioning"
xhttp "github.com/minio/minio/internal/http"
) )
func TestApplyNewerNoncurrentVersionsLimit(t *testing.T) { func TestApplyNewerNoncurrentVersionsLimit(t *testing.T) {
// Prepare object layer
objAPI, disks, err := prepareErasure(context.Background(), 8) objAPI, disks, err := prepareErasure(context.Background(), 8)
if err != nil { if err != nil {
t.Fatalf("Failed to initialize object layer: %v", err) t.Fatalf("Failed to initialize object layer: %v", err)
} }
defer removeRoots(disks) defer removeRoots(disks)
setObjectLayer(objAPI) setObjectLayer(objAPI)
// Prepare bucket metadata
globalBucketMetadataSys = NewBucketMetadataSys() globalBucketMetadataSys = NewBucketMetadataSys()
globalBucketObjectLockSys = &BucketObjectLockSys{} globalBucketObjectLockSys = &BucketObjectLockSys{}
globalBucketVersioningSys = &BucketVersioningSys{} globalBucketVersioningSys = &BucketVersioningSys{}
es := newExpiryState(context.Background(), objAPI, 0)
workers := []chan expiryOp{make(chan expiryOp)} lcXML := `
es.workers.Store(&workers) <LifecycleConfiguration>
globalExpiryState = es <Rule>
var wg sync.WaitGroup <ID>max-versions</ID>
wg.Add(1) <Status>Enabled</Status>
expired := make([]ObjectToDelete, 0, 5) <NoncurrentVersionExpiration>
go func() { <NewerNoncurrentVersions>2</NewerNoncurrentVersions>
defer wg.Done() </NoncurrentVersionExpiration>
workers := globalExpiryState.workers.Load() </Rule>
for t := range (*workers)[0] { <Rule>
if t, ok := t.(newerNoncurrentTask); ok { <ID>delete-all-versions</ID>
expired = append(expired, t.versions...) <Status>Enabled</Status>
} <Filter>
} <Tag>
}() <Key>del-all</Key>
lc := lifecycle.Lifecycle{ <Value>true</Value>
Rules: []lifecycle.Rule{ </Tag>
{ </Filter>
ID: "max-versions", <Expiration>
Status: "Enabled", <Days>1</Days>
NoncurrentVersionExpiration: lifecycle.NoncurrentVersionExpiration{ <ExpiredObjectAllVersions>true</ExpiredObjectAllVersions>
NewerNoncurrentVersions: 1, </Expiration>
}, </Rule>
}, </LifecycleConfiguration>
}, `
} lc, err := lifecycle.ParseLifecycleConfig(strings.NewReader(lcXML))
lcXML, err := xml.Marshal(lc)
if err != nil { if err != nil {
t.Fatalf("Failed to marshal lifecycle config: %v", err) t.Fatalf("Failed to unmarshal lifecycle config: %v", err)
} }
vcfg := versioning.Versioning{ vcfg := versioning.Versioning{
Status: "Enabled", Status: "Enabled",
} }
@@ -82,33 +89,45 @@ func TestApplyNewerNoncurrentVersionsLimit(t *testing.T) {
} }
bucket := "bucket" bucket := "bucket"
obj := "obj-1"
now := time.Now() now := time.Now()
meta := BucketMetadata{ meta := BucketMetadata{
Name: bucket, Name: bucket,
Created: now, Created: now,
LifecycleConfigXML: lcXML, LifecycleConfigXML: []byte(lcXML),
VersioningConfigXML: vcfgXML, VersioningConfigXML: vcfgXML,
VersioningConfigUpdatedAt: now, VersioningConfigUpdatedAt: now,
LifecycleConfigUpdatedAt: now, LifecycleConfigUpdatedAt: now,
lifecycleConfig: &lc, lifecycleConfig: lc,
versioningConfig: &vcfg, versioningConfig: &vcfg,
} }
globalBucketMetadataSys.Set(bucket, meta) globalBucketMetadataSys.Set(bucket, meta)
item := scannerItem{ // Prepare lifecycle expiration workers
Path: obj, es := newExpiryState(context.Background(), objAPI, 0)
bucket: bucket, globalExpiryState = es
prefix: "",
objectName: obj,
lifeCycle: &lc,
}
modTime := time.Now() // Prepare object versions
obj := "obj-1"
// Simulate objects uploaded 30 hours ago
modTime := now.Add(-48 * time.Hour)
uuids := make([]uuid.UUID, 5) uuids := make([]uuid.UUID, 5)
for i := range uuids { for i := range uuids {
uuids[i] = uuid.UUID([16]byte{15: uint8(i + 1)}) uuids[i] = uuid.UUID([16]byte{15: uint8(i + 1)})
} }
fivs := make([]FileInfo, 5) fivs := make([]FileInfo, 5)
objInfos := make([]ObjectInfo, 5)
objRetentionMeta := make(map[string]string)
objRetentionMeta[strings.ToLower(xhttp.AmzObjectLockMode)] = string(objectlock.RetCompliance)
// Set retain until date 12 hours into the future
objRetentionMeta[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = amztime.ISO8601Format(now.Add(12 * time.Hour))
/*
objInfos:
version stack for obj-1
v5 uuid-5 modTime
v4 uuid-4 modTime -1m
v3 uuid-3 modTime -2m
v2 uuid-2 modTime -3m
v1 uuid-1 modTime -4m
*/
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
fivs[i] = FileInfo{ fivs[i] = FileInfo{
Volume: bucket, Volume: bucket,
@@ -119,41 +138,183 @@ func TestApplyNewerNoncurrentVersionsLimit(t *testing.T) {
Size: 1 << 10, Size: 1 << 10,
NumVersions: 5, NumVersions: 5,
} }
objInfos[i] = fivs[i].ToObjectInfo(bucket, obj, true)
} }
versioned := vcfg.Status == "Enabled" /*
wants := make([]ObjectInfo, 2) lrObjInfos: objInfos with following modifications
for i, fi := range fivs[:2] { version stack for obj-1
wants[i] = fi.ToObjectInfo(bucket, obj, versioned) v2 uuid-2 modTime -3m objRetentionMeta
*/
lrObjInfos := slices.Clone(objInfos)
lrObjInfos[3].UserDefined = objRetentionMeta
var lrWants []ObjectInfo
lrWants = append(lrWants, lrObjInfos[:4]...)
/*
replObjInfos: objInfos with following modifications
version stack for obj-1
v1 uuid-1 modTime -4m "VersionPurgeStatus: replication.VersionPurgePending"
*/
replObjInfos := slices.Clone(objInfos)
replObjInfos[4].VersionPurgeStatus = replication.VersionPurgePending
var replWants []ObjectInfo
replWants = append(replWants, replObjInfos[:3]...)
replWants = append(replWants, replObjInfos[4])
allVersExpObjInfos := slices.Clone(objInfos)
allVersExpObjInfos[0].UserTags = "del-all=true"
replCfg := replication.Config{
Rules: []replication.Rule{
{
ID: "",
Status: "Enabled",
Priority: 1,
Destination: replication.Destination{
ARN: "arn:minio:replication:::dest-bucket",
Bucket: "dest-bucket",
},
},
},
} }
gots, err := item.applyNewerNoncurrentVersionLimit(context.TODO(), objAPI, fivs, es) lr := objectlock.Retention{
if err != nil { Mode: objectlock.RetCompliance,
t.Fatalf("Failed with err: %v", err) Validity: 12 * time.Hour,
} LockEnabled: true,
if len(gots) != len(wants) {
t.Fatalf("Expected %d objects but got %d", len(wants), len(gots))
} }
// Close expiry state's channel to inspect object versions enqueued for expiration expiryWorker := func(wg *sync.WaitGroup, readyCh chan<- struct{}, taskCh <-chan expiryOp, gotExpired *[]ObjectToDelete) {
close(workers[0]) defer wg.Done()
wg.Wait() // signal the calling goroutine that the worker is ready tor receive tasks
for _, obj := range expired { close(readyCh)
switch obj.ObjectV.VersionID { var expired []ObjectToDelete
case uuids[2].String(), uuids[3].String(), uuids[4].String(): for t := range taskCh {
default: switch v := t.(type) {
t.Errorf("Unexpected versionID being expired: %#v\n", obj) case noncurrentVersionsTask:
expired = append(expired, v.versions...)
case expiryTask:
expired = append(expired, ObjectToDelete{
ObjectV: ObjectV{
ObjectName: v.objInfo.Name,
VersionID: v.objInfo.VersionID,
},
})
}
} }
if len(expired) > 0 {
*gotExpired = expired
}
}
tests := []struct {
replCfg replicationConfig
lr objectlock.Retention
objInfos []ObjectInfo
wants []ObjectInfo
wantExpired []ObjectToDelete
}{
{
// With replication configured, version(s) with PENDING purge status
replCfg: replicationConfig{Config: &replCfg},
objInfos: replObjInfos,
wants: replWants,
wantExpired: []ObjectToDelete{
{ObjectV: ObjectV{ObjectName: obj, VersionID: objInfos[3].VersionID}},
},
},
{
// With lock retention configured and version(s) with retention metadata
lr: lr,
objInfos: lrObjInfos,
wants: lrWants,
wantExpired: []ObjectToDelete{
{ObjectV: ObjectV{ObjectName: obj, VersionID: objInfos[4].VersionID}},
},
},
{
// With replication configured, but no versions with PENDING purge status
replCfg: replicationConfig{Config: &replCfg},
objInfos: objInfos,
wants: objInfos[:3],
wantExpired: []ObjectToDelete{
{ObjectV: ObjectV{ObjectName: obj, VersionID: objInfos[3].VersionID}},
{ObjectV: ObjectV{ObjectName: obj, VersionID: objInfos[4].VersionID}},
},
},
{
objInfos: allVersExpObjInfos,
wants: nil,
wantExpired: []ObjectToDelete{{ObjectV: ObjectV{ObjectName: obj, VersionID: allVersExpObjInfos[0].VersionID}}},
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("TestApplyNewerNoncurrentVersionsLimit-%d", i), func(t *testing.T) {
workers := []chan expiryOp{make(chan expiryOp)}
es.workers.Store(&workers)
workerReady := make(chan struct{})
var wg sync.WaitGroup
wg.Add(1)
var gotExpired []ObjectToDelete
go expiryWorker(&wg, workerReady, workers[0], &gotExpired)
<-workerReady
item := scannerItem{
Path: obj,
bucket: bucket,
prefix: "",
objectName: obj,
lifeCycle: lc,
replication: test.replCfg,
}
var (
sizeS sizeSummary
gots []ObjectInfo
)
item.applyActions(context.TODO(), objAPI, test.objInfos, test.lr, &sizeS, func(oi ObjectInfo, sz, _ int64, _ *sizeSummary) {
if sz != 0 {
gots = append(gots, oi)
}
})
if len(gots) != len(test.wants) {
t.Fatalf("Expected %d objects but got %d", len(test.wants), len(gots))
}
if slices.CompareFunc(gots, test.wants, func(g, w ObjectInfo) int {
if g.VersionID == w.VersionID {
return 0
}
return -1
}) != 0 {
t.Fatalf("Expected %v but got %v", test.wants, gots)
}
// verify the objects to be deleted
close(workers[0])
wg.Wait()
if len(gotExpired) != len(test.wantExpired) {
t.Fatalf("Expected expiry of %d objects but got %d", len(test.wantExpired), len(gotExpired))
}
if slices.CompareFunc(gotExpired, test.wantExpired, func(g, w ObjectToDelete) int {
if g.VersionID == w.VersionID {
return 0
}
return -1
}) != 0 {
t.Fatalf("Expected %v but got %v", test.wantExpired, gotExpired)
}
})
} }
} }
func TestEvalActionFromLifecycle(t *testing.T) { func TestEvalActionFromLifecycle(t *testing.T) {
// Tests cover only ExpiredObjectDeleteAllVersions and DelMarkerExpiration actions // Tests cover only ExpiredObjectDeleteAllVersions and DelMarkerExpiration actions
numVersions := 4
obj := ObjectInfo{ obj := ObjectInfo{
Name: "foo", Name: "foo",
ModTime: time.Now().Add(-31 * 24 * time.Hour), ModTime: time.Now().Add(-31 * 24 * time.Hour),
Size: 100 << 20, Size: 100 << 20,
VersionID: uuid.New().String(), VersionID: uuid.New().String(),
IsLatest: true, IsLatest: true,
NumVersions: 4, NumVersions: numVersions,
} }
delMarker := ObjectInfo{ delMarker := ObjectInfo{
Name: "foo-deleted", Name: "foo-deleted",
@@ -162,8 +323,9 @@ func TestEvalActionFromLifecycle(t *testing.T) {
VersionID: uuid.New().String(), VersionID: uuid.New().String(),
IsLatest: true, IsLatest: true,
DeleteMarker: true, DeleteMarker: true,
NumVersions: 4, NumVersions: numVersions,
} }
deleteAllILM := `<LifecycleConfiguration> deleteAllILM := `<LifecycleConfiguration>
<Rule> <Rule>
<Expiration> <Expiration>
@@ -195,35 +357,35 @@ func TestEvalActionFromLifecycle(t *testing.T) {
} }
tests := []struct { tests := []struct {
ilm lifecycle.Lifecycle ilm lifecycle.Lifecycle
retention lock.Retention retention *objectlock.Retention
obj ObjectInfo obj ObjectInfo
want lifecycle.Action want lifecycle.Action
}{ }{
{ {
// with object locking // with object locking
ilm: *deleteAllLc, ilm: *deleteAllLc,
retention: lock.Retention{LockEnabled: true}, retention: &objectlock.Retention{LockEnabled: true},
obj: obj, obj: obj,
want: lifecycle.NoneAction, want: lifecycle.NoneAction,
}, },
{ {
// without object locking // without object locking
ilm: *deleteAllLc, ilm: *deleteAllLc,
retention: lock.Retention{}, retention: &objectlock.Retention{},
obj: obj, obj: obj,
want: lifecycle.DeleteAllVersionsAction, want: lifecycle.DeleteAllVersionsAction,
}, },
{ {
// with object locking // with object locking
ilm: *delMarkerLc, ilm: *delMarkerLc,
retention: lock.Retention{LockEnabled: true}, retention: &objectlock.Retention{LockEnabled: true},
obj: delMarker, obj: delMarker,
want: lifecycle.NoneAction, want: lifecycle.NoneAction,
}, },
{ {
// without object locking // without object locking
ilm: *delMarkerLc, ilm: *delMarkerLc,
retention: lock.Retention{}, retention: &objectlock.Retention{},
obj: delMarker, obj: delMarker,
want: lifecycle.DelMarkerDeleteAllVersionsAction, want: lifecycle.DelMarkerDeleteAllVersionsAction,
}, },
@@ -231,8 +393,9 @@ func TestEvalActionFromLifecycle(t *testing.T) {
for i, test := range tests { for i, test := range tests {
t.Run(fmt.Sprintf("TestEvalAction-%d", i), func(t *testing.T) { t.Run(fmt.Sprintf("TestEvalAction-%d", i), func(t *testing.T) {
if got := evalActionFromLifecycle(context.TODO(), test.ilm, test.retention, nil, test.obj); got.Action != test.want { gotEvent := evalActionFromLifecycle(context.Background(), test.ilm, *test.retention, nil, test.obj)
t.Fatalf("Expected %v but got %v", test.want, got) if gotEvent.Action != test.want {
t.Fatalf("Expected %v but got %v", test.want, gotEvent.Action)
} }
}) })
} }
+48 -284
View File
@@ -39,6 +39,8 @@ import (
"github.com/valyala/bytebufferpool" "github.com/valyala/bytebufferpool"
) )
//msgp:clearomitted
//go:generate msgp -file $GOFILE -unexported //go:generate msgp -file $GOFILE -unexported
// dataUsageHash is the hash type used. // dataUsageHash is the hash type used.
@@ -57,15 +59,14 @@ type versionsHistogram [dataUsageVersionLen]uint64
type dataUsageEntry struct { type dataUsageEntry struct {
Children dataUsageHashMap `msg:"ch"` Children dataUsageHashMap `msg:"ch"`
// These fields do no include any children. // These fields do no include any children.
Size int64 `msg:"sz"` Size int64 `msg:"sz"`
Objects uint64 `msg:"os"` Objects uint64 `msg:"os"`
Versions uint64 `msg:"vs"` // Versions that are not delete markers. Versions uint64 `msg:"vs"` // Versions that are not delete markers.
DeleteMarkers uint64 `msg:"dms"` DeleteMarkers uint64 `msg:"dms"`
ObjSizes sizeHistogram `msg:"szs"` ObjSizes sizeHistogram `msg:"szs"`
ObjVersions versionsHistogram `msg:"vh"` ObjVersions versionsHistogram `msg:"vh"`
ReplicationStats *replicationAllStats `msg:"rs,omitempty"` AllTierStats *allTierStats `msg:"ats,omitempty"`
AllTierStats *allTierStats `msg:"ats,omitempty"` Compacted bool `msg:"c"`
Compacted bool `msg:"c"`
} }
// allTierStats is a collection of per-tier stats across all configured remote // allTierStats is a collection of per-tier stats across all configured remote
@@ -117,7 +118,6 @@ func (ats *allTierStats) populateStats(stats map[string]madmin.TierStats) {
NumObjects: st.NumObjects, NumObjects: st.NumObjects,
} }
} }
return
} }
// tierStats holds per-tier stats of a remote tier. // tierStats holds per-tier stats of a remote tier.
@@ -135,93 +135,6 @@ func (ts tierStats) add(u tierStats) tierStats {
} }
} }
//msgp:tuple replicationStatsV1
type replicationStatsV1 struct {
PendingSize uint64
ReplicatedSize uint64
FailedSize uint64
ReplicaSize uint64
FailedCount uint64
PendingCount uint64
MissedThresholdSize uint64
AfterThresholdSize uint64
MissedThresholdCount uint64
AfterThresholdCount uint64
}
func (rsv1 replicationStatsV1) Empty() bool {
return rsv1.ReplicatedSize == 0 &&
rsv1.FailedSize == 0 &&
rsv1.FailedCount == 0
}
//msgp:tuple replicationStats
type replicationStats struct {
PendingSize uint64
ReplicatedSize uint64
FailedSize uint64
FailedCount uint64
PendingCount uint64
MissedThresholdSize uint64
AfterThresholdSize uint64
MissedThresholdCount uint64
AfterThresholdCount uint64
ReplicatedCount uint64
}
func (rs replicationStats) Empty() bool {
return rs.ReplicatedSize == 0 &&
rs.FailedSize == 0 &&
rs.FailedCount == 0
}
type replicationAllStats struct {
Targets map[string]replicationStats `msg:"t,omitempty"`
ReplicaSize uint64 `msg:"r,omitempty"`
ReplicaCount uint64 `msg:"rc,omitempty"`
}
//msgp:tuple replicationAllStatsV1
type replicationAllStatsV1 struct {
Targets map[string]replicationStats
ReplicaSize uint64 `msg:"ReplicaSize,omitempty"`
ReplicaCount uint64 `msg:"ReplicaCount,omitempty"`
}
// empty returns true if the replicationAllStats is empty (contains no entries).
func (r *replicationAllStats) empty() bool {
if r == nil {
return true
}
if r.ReplicaSize != 0 || r.ReplicaCount != 0 {
return false
}
for _, v := range r.Targets {
if !v.Empty() {
return false
}
}
return true
}
// clone creates a deep-copy clone.
func (r *replicationAllStats) clone() *replicationAllStats {
if r == nil {
return nil
}
// Shallow copy
dst := *r
// Copy individual targets.
dst.Targets = make(map[string]replicationStats, len(r.Targets))
for k, v := range r.Targets {
dst.Targets[k] = v
}
return &dst
}
//msgp:encode ignore dataUsageEntryV2 dataUsageEntryV3 dataUsageEntryV4 dataUsageEntryV5 dataUsageEntryV6 dataUsageEntryV7 //msgp:encode ignore dataUsageEntryV2 dataUsageEntryV3 dataUsageEntryV4 dataUsageEntryV5 dataUsageEntryV6 dataUsageEntryV7
//msgp:marshal ignore dataUsageEntryV2 dataUsageEntryV3 dataUsageEntryV4 dataUsageEntryV5 dataUsageEntryV6 dataUsageEntryV7 //msgp:marshal ignore dataUsageEntryV2 dataUsageEntryV3 dataUsageEntryV4 dataUsageEntryV5 dataUsageEntryV6 dataUsageEntryV7
@@ -237,62 +150,54 @@ type dataUsageEntryV2 struct {
//msgp:tuple dataUsageEntryV3 //msgp:tuple dataUsageEntryV3
type dataUsageEntryV3 struct { type dataUsageEntryV3 struct {
// These fields do no include any children. // These fields do no include any children.
Size int64 Size int64
ReplicatedSize uint64 Objects uint64
ReplicationPendingSize uint64 ObjSizes sizeHistogram
ReplicationFailedSize uint64 Children dataUsageHashMap
ReplicaSize uint64
Objects uint64
ObjSizes sizeHistogram
Children dataUsageHashMap
} }
//msgp:tuple dataUsageEntryV4 //msgp:tuple dataUsageEntryV4
type dataUsageEntryV4 struct { type dataUsageEntryV4 struct {
Children dataUsageHashMap Children dataUsageHashMap
// These fields do no include any children. // These fields do no include any children.
Size int64 Size int64
Objects uint64 Objects uint64
ObjSizes sizeHistogram ObjSizes sizeHistogram
ReplicationStats replicationStatsV1
} }
//msgp:tuple dataUsageEntryV5 //msgp:tuple dataUsageEntryV5
type dataUsageEntryV5 struct { type dataUsageEntryV5 struct {
Children dataUsageHashMap Children dataUsageHashMap
// These fields do no include any children. // These fields do no include any children.
Size int64 Size int64
Objects uint64 Objects uint64
Versions uint64 // Versions that are not delete markers. Versions uint64 // Versions that are not delete markers.
ObjSizes sizeHistogram ObjSizes sizeHistogram
ReplicationStats *replicationStatsV1 Compacted bool
Compacted bool
} }
//msgp:tuple dataUsageEntryV6 //msgp:tuple dataUsageEntryV6
type dataUsageEntryV6 struct { type dataUsageEntryV6 struct {
Children dataUsageHashMap Children dataUsageHashMap
// These fields do no include any children. // These fields do no include any children.
Size int64 Size int64
Objects uint64 Objects uint64
Versions uint64 // Versions that are not delete markers. Versions uint64 // Versions that are not delete markers.
ObjSizes sizeHistogram ObjSizes sizeHistogram
ReplicationStats *replicationAllStatsV1 Compacted bool
Compacted bool
} }
type dataUsageEntryV7 struct { type dataUsageEntryV7 struct {
Children dataUsageHashMap `msg:"ch"` Children dataUsageHashMap `msg:"ch"`
// These fields do no include any children. // These fields do no include any children.
Size int64 `msg:"sz"` Size int64 `msg:"sz"`
Objects uint64 `msg:"os"` Objects uint64 `msg:"os"`
Versions uint64 `msg:"vs"` // Versions that are not delete markers. Versions uint64 `msg:"vs"` // Versions that are not delete markers.
DeleteMarkers uint64 `msg:"dms"` DeleteMarkers uint64 `msg:"dms"`
ObjSizes sizeHistogramV1 `msg:"szs"` ObjSizes sizeHistogramV1 `msg:"szs"`
ObjVersions versionsHistogram `msg:"vh"` ObjVersions versionsHistogram `msg:"vh"`
ReplicationStats *replicationAllStats `msg:"rs,omitempty"` AllTierStats *allTierStats `msg:"ats,omitempty"`
AllTierStats *allTierStats `msg:"ats,omitempty"` Compacted bool `msg:"c"`
Compacted bool `msg:"c"`
} }
// dataUsageCache contains a cache of data usage entries latest version. // dataUsageCache contains a cache of data usage entries latest version.
@@ -373,29 +278,6 @@ func (e *dataUsageEntry) addSizes(summary sizeSummary) {
e.ObjSizes.add(summary.totalSize) e.ObjSizes.add(summary.totalSize)
e.ObjVersions.add(summary.versions) e.ObjVersions.add(summary.versions)
if e.ReplicationStats == nil {
e.ReplicationStats = &replicationAllStats{
Targets: make(map[string]replicationStats),
}
} else if e.ReplicationStats.Targets == nil {
e.ReplicationStats.Targets = make(map[string]replicationStats)
}
e.ReplicationStats.ReplicaSize += uint64(summary.replicaSize)
e.ReplicationStats.ReplicaCount += uint64(summary.replicaCount)
for arn, st := range summary.replTargetStats {
tgtStat, ok := e.ReplicationStats.Targets[arn]
if !ok {
tgtStat = replicationStats{}
}
tgtStat.PendingSize += uint64(st.pendingSize)
tgtStat.FailedSize += uint64(st.failedSize)
tgtStat.ReplicatedSize += uint64(st.replicatedSize)
tgtStat.ReplicatedCount += uint64(st.replicatedCount)
tgtStat.FailedCount += st.failedCount
tgtStat.PendingCount += st.pendingCount
e.ReplicationStats.Targets[arn] = tgtStat
}
if len(summary.tiers) != 0 { if len(summary.tiers) != 0 {
if e.AllTierStats == nil { if e.AllTierStats == nil {
e.AllTierStats = newAllTierStats() e.AllTierStats = newAllTierStats()
@@ -410,26 +292,6 @@ func (e *dataUsageEntry) merge(other dataUsageEntry) {
e.Versions += other.Versions e.Versions += other.Versions
e.DeleteMarkers += other.DeleteMarkers e.DeleteMarkers += other.DeleteMarkers
e.Size += other.Size e.Size += other.Size
if other.ReplicationStats != nil {
if e.ReplicationStats == nil {
e.ReplicationStats = &replicationAllStats{Targets: make(map[string]replicationStats)}
} else if e.ReplicationStats.Targets == nil {
e.ReplicationStats.Targets = make(map[string]replicationStats)
}
e.ReplicationStats.ReplicaSize += other.ReplicationStats.ReplicaSize
e.ReplicationStats.ReplicaCount += other.ReplicationStats.ReplicaCount
for arn, stat := range other.ReplicationStats.Targets {
st := e.ReplicationStats.Targets[arn]
e.ReplicationStats.Targets[arn] = replicationStats{
PendingSize: stat.PendingSize + st.PendingSize,
FailedSize: stat.FailedSize + st.FailedSize,
ReplicatedSize: stat.ReplicatedSize + st.ReplicatedSize,
PendingCount: stat.PendingCount + st.PendingCount,
FailedCount: stat.FailedCount + st.FailedCount,
ReplicatedCount: stat.ReplicatedCount + st.ReplicatedCount,
}
}
}
for i, v := range other.ObjSizes[:] { for i, v := range other.ObjSizes[:] {
e.ObjSizes[i] += v e.ObjSizes[i] += v
@@ -490,10 +352,7 @@ func (e dataUsageEntry) clone() dataUsageEntry {
} }
e.Children = ch e.Children = ch
} }
if e.ReplicationStats != nil {
// Clone ReplicationStats
e.ReplicationStats = e.ReplicationStats.clone()
}
if e.AllTierStats != nil { if e.AllTierStats != nil {
e.AllTierStats = e.AllTierStats.clone() e.AllTierStats = e.AllTierStats.clone()
} }
@@ -931,22 +790,6 @@ func (d *dataUsageCache) bucketsUsageInfo(buckets []BucketInfo) map[string]Bucke
ObjectSizesHistogram: flat.ObjSizes.toMap(), ObjectSizesHistogram: flat.ObjSizes.toMap(),
ObjectVersionsHistogram: flat.ObjVersions.toMap(), ObjectVersionsHistogram: flat.ObjVersions.toMap(),
} }
if flat.ReplicationStats != nil {
bui.ReplicaSize = flat.ReplicationStats.ReplicaSize
bui.ReplicaCount = flat.ReplicationStats.ReplicaCount
bui.ReplicationInfo = make(map[string]BucketTargetUsageInfo, len(flat.ReplicationStats.Targets))
for arn, stat := range flat.ReplicationStats.Targets {
bui.ReplicationInfo[arn] = BucketTargetUsageInfo{
ReplicationPendingSize: stat.PendingSize,
ReplicatedSize: stat.ReplicatedSize,
ReplicationFailedSize: stat.FailedSize,
ReplicationPendingCount: stat.PendingCount,
ReplicationFailedCount: stat.FailedCount,
ReplicatedCount: stat.ReplicatedCount,
}
}
}
dst[bucket.Name] = bui dst[bucket.Name] = bui
} }
return dst return dst
@@ -959,9 +802,6 @@ func (d *dataUsageCache) sizeRecursive(path string) *dataUsageEntry {
return root return root
} }
flat := d.flatten(*root) flat := d.flatten(*root)
if flat.ReplicationStats.empty() {
flat.ReplicationStats = nil
}
return &flat return &flat
} }
@@ -1238,20 +1078,6 @@ func (d *dataUsageCache) deserialize(r io.Reader) error {
ObjSizes: v.ObjSizes, ObjSizes: v.ObjSizes,
Children: v.Children, Children: v.Children,
} }
if v.ReplicatedSize > 0 || v.ReplicaSize > 0 || v.ReplicationFailedSize > 0 || v.ReplicationPendingSize > 0 {
cfg, _ := getReplicationConfig(GlobalContext, d.Info.Name)
if cfg != nil && cfg.RoleArn != "" {
due.ReplicationStats = &replicationAllStats{
Targets: make(map[string]replicationStats),
}
due.ReplicationStats.ReplicaSize = v.ReplicaSize
due.ReplicationStats.Targets[cfg.RoleArn] = replicationStats{
ReplicatedSize: v.ReplicatedSize,
FailedSize: v.ReplicationFailedSize,
PendingSize: v.ReplicationPendingSize,
}
}
}
due.Compacted = len(due.Children) == 0 && k != d.Info.Name due.Compacted = len(due.Children) == 0 && k != d.Info.Name
d.Cache[k] = due d.Cache[k] = due
@@ -1277,36 +1103,10 @@ func (d *dataUsageCache) deserialize(r io.Reader) error {
ObjSizes: v.ObjSizes, ObjSizes: v.ObjSizes,
Children: v.Children, Children: v.Children,
} }
empty := replicationStatsV1{}
if v.ReplicationStats != empty {
cfg, _ := getReplicationConfig(GlobalContext, d.Info.Name)
if cfg != nil && cfg.RoleArn != "" {
due.ReplicationStats = &replicationAllStats{
Targets: make(map[string]replicationStats),
}
due.ReplicationStats.Targets[cfg.RoleArn] = replicationStats{
ReplicatedSize: v.ReplicationStats.ReplicatedSize,
FailedSize: v.ReplicationStats.FailedSize,
FailedCount: v.ReplicationStats.FailedCount,
PendingSize: v.ReplicationStats.PendingSize,
PendingCount: v.ReplicationStats.PendingCount,
}
due.ReplicationStats.ReplicaSize = v.ReplicationStats.ReplicaSize
}
}
due.Compacted = len(due.Children) == 0 && k != d.Info.Name due.Compacted = len(due.Children) == 0 && k != d.Info.Name
d.Cache[k] = due d.Cache[k] = due
} }
// Populate compacted value and remove unneeded replica stats.
for k, e := range d.Cache {
if e.ReplicationStats != nil && len(e.ReplicationStats.Targets) == 0 {
e.ReplicationStats = nil
}
d.Cache[k] = e
}
return nil return nil
case dataUsageCacheVerV5: case dataUsageCacheVerV5:
// Zstd compressed. // Zstd compressed.
@@ -1328,36 +1128,10 @@ func (d *dataUsageCache) deserialize(r io.Reader) error {
ObjSizes: v.ObjSizes, ObjSizes: v.ObjSizes,
Children: v.Children, Children: v.Children,
} }
if v.ReplicationStats != nil && !v.ReplicationStats.Empty() {
cfg, _ := getReplicationConfig(GlobalContext, d.Info.Name)
if cfg != nil && cfg.RoleArn != "" {
due.ReplicationStats = &replicationAllStats{
Targets: make(map[string]replicationStats),
}
d.Info.replication = replicationConfig{Config: cfg}
due.ReplicationStats.Targets[cfg.RoleArn] = replicationStats{
ReplicatedSize: v.ReplicationStats.ReplicatedSize,
FailedSize: v.ReplicationStats.FailedSize,
FailedCount: v.ReplicationStats.FailedCount,
PendingSize: v.ReplicationStats.PendingSize,
PendingCount: v.ReplicationStats.PendingCount,
}
due.ReplicationStats.ReplicaSize = v.ReplicationStats.ReplicaSize
}
}
due.Compacted = len(due.Children) == 0 && k != d.Info.Name due.Compacted = len(due.Children) == 0 && k != d.Info.Name
d.Cache[k] = due d.Cache[k] = due
} }
// Populate compacted value and remove unneeded replica stats.
for k, e := range d.Cache {
if e.ReplicationStats != nil && len(e.ReplicationStats.Targets) == 0 {
e.ReplicationStats = nil
}
d.Cache[k] = e
}
return nil return nil
case dataUsageCacheVerV6: case dataUsageCacheVerV6:
// Zstd compressed. // Zstd compressed.
@@ -1373,22 +1147,13 @@ func (d *dataUsageCache) deserialize(r io.Reader) error {
d.Info = dold.Info d.Info = dold.Info
d.Cache = make(map[string]dataUsageEntry, len(dold.Cache)) d.Cache = make(map[string]dataUsageEntry, len(dold.Cache))
for k, v := range dold.Cache { for k, v := range dold.Cache {
var replicationStats *replicationAllStats
if v.ReplicationStats != nil {
replicationStats = &replicationAllStats{
Targets: v.ReplicationStats.Targets,
ReplicaSize: v.ReplicationStats.ReplicaSize,
ReplicaCount: v.ReplicationStats.ReplicaCount,
}
}
due := dataUsageEntry{ due := dataUsageEntry{
Children: v.Children, Children: v.Children,
Size: v.Size, Size: v.Size,
Objects: v.Objects, Objects: v.Objects,
Versions: v.Versions, Versions: v.Versions,
ObjSizes: v.ObjSizes, ObjSizes: v.ObjSizes,
ReplicationStats: replicationStats, Compacted: v.Compacted,
Compacted: v.Compacted,
} }
d.Cache[k] = due d.Cache[k] = due
} }
@@ -1410,13 +1175,12 @@ func (d *dataUsageCache) deserialize(r io.Reader) error {
var szHist sizeHistogram var szHist sizeHistogram
szHist.mergeV1(v.ObjSizes) szHist.mergeV1(v.ObjSizes)
d.Cache[k] = dataUsageEntry{ d.Cache[k] = dataUsageEntry{
Children: v.Children, Children: v.Children,
Size: v.Size, Size: v.Size,
Objects: v.Objects, Objects: v.Objects,
Versions: v.Versions, Versions: v.Versions,
ObjSizes: szHist, ObjSizes: szHist,
ReplicationStats: v.ReplicationStats, Compacted: v.Compacted,
Compacted: v.Compacted,
} }
} }
+615 -1330
View File
File diff suppressed because it is too large Load Diff
-452
View File
@@ -519,458 +519,6 @@ func BenchmarkDecodedataUsageEntry(b *testing.B) {
} }
} }
func TestMarshalUnmarshalreplicationAllStats(t *testing.T) {
v := replicationAllStats{}
bts, err := v.MarshalMsg(nil)
if err != nil {
t.Fatal(err)
}
left, err := v.UnmarshalMsg(bts)
if err != nil {
t.Fatal(err)
}
if len(left) > 0 {
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
}
left, err = msgp.Skip(bts)
if err != nil {
t.Fatal(err)
}
if len(left) > 0 {
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
}
}
func BenchmarkMarshalMsgreplicationAllStats(b *testing.B) {
v := replicationAllStats{}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v.MarshalMsg(nil)
}
}
func BenchmarkAppendMsgreplicationAllStats(b *testing.B) {
v := replicationAllStats{}
bts := make([]byte, 0, v.Msgsize())
bts, _ = v.MarshalMsg(bts[0:0])
b.SetBytes(int64(len(bts)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
bts, _ = v.MarshalMsg(bts[0:0])
}
}
func BenchmarkUnmarshalreplicationAllStats(b *testing.B) {
v := replicationAllStats{}
bts, _ := v.MarshalMsg(nil)
b.ReportAllocs()
b.SetBytes(int64(len(bts)))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := v.UnmarshalMsg(bts)
if err != nil {
b.Fatal(err)
}
}
}
func TestEncodeDecodereplicationAllStats(t *testing.T) {
v := replicationAllStats{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
m := v.Msgsize()
if buf.Len() > m {
t.Log("WARNING: TestEncodeDecodereplicationAllStats Msgsize() is inaccurate")
}
vn := replicationAllStats{}
err := msgp.Decode(&buf, &vn)
if err != nil {
t.Error(err)
}
buf.Reset()
msgp.Encode(&buf, &v)
err = msgp.NewReader(&buf).Skip()
if err != nil {
t.Error(err)
}
}
func BenchmarkEncodereplicationAllStats(b *testing.B) {
v := replicationAllStats{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
b.SetBytes(int64(buf.Len()))
en := msgp.NewWriter(msgp.Nowhere)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v.EncodeMsg(en)
}
en.Flush()
}
func BenchmarkDecodereplicationAllStats(b *testing.B) {
v := replicationAllStats{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
b.SetBytes(int64(buf.Len()))
rd := msgp.NewEndlessReader(buf.Bytes(), b)
dc := msgp.NewReader(rd)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
err := v.DecodeMsg(dc)
if err != nil {
b.Fatal(err)
}
}
}
func TestMarshalUnmarshalreplicationAllStatsV1(t *testing.T) {
v := replicationAllStatsV1{}
bts, err := v.MarshalMsg(nil)
if err != nil {
t.Fatal(err)
}
left, err := v.UnmarshalMsg(bts)
if err != nil {
t.Fatal(err)
}
if len(left) > 0 {
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
}
left, err = msgp.Skip(bts)
if err != nil {
t.Fatal(err)
}
if len(left) > 0 {
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
}
}
func BenchmarkMarshalMsgreplicationAllStatsV1(b *testing.B) {
v := replicationAllStatsV1{}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v.MarshalMsg(nil)
}
}
func BenchmarkAppendMsgreplicationAllStatsV1(b *testing.B) {
v := replicationAllStatsV1{}
bts := make([]byte, 0, v.Msgsize())
bts, _ = v.MarshalMsg(bts[0:0])
b.SetBytes(int64(len(bts)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
bts, _ = v.MarshalMsg(bts[0:0])
}
}
func BenchmarkUnmarshalreplicationAllStatsV1(b *testing.B) {
v := replicationAllStatsV1{}
bts, _ := v.MarshalMsg(nil)
b.ReportAllocs()
b.SetBytes(int64(len(bts)))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := v.UnmarshalMsg(bts)
if err != nil {
b.Fatal(err)
}
}
}
func TestEncodeDecodereplicationAllStatsV1(t *testing.T) {
v := replicationAllStatsV1{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
m := v.Msgsize()
if buf.Len() > m {
t.Log("WARNING: TestEncodeDecodereplicationAllStatsV1 Msgsize() is inaccurate")
}
vn := replicationAllStatsV1{}
err := msgp.Decode(&buf, &vn)
if err != nil {
t.Error(err)
}
buf.Reset()
msgp.Encode(&buf, &v)
err = msgp.NewReader(&buf).Skip()
if err != nil {
t.Error(err)
}
}
func BenchmarkEncodereplicationAllStatsV1(b *testing.B) {
v := replicationAllStatsV1{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
b.SetBytes(int64(buf.Len()))
en := msgp.NewWriter(msgp.Nowhere)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v.EncodeMsg(en)
}
en.Flush()
}
func BenchmarkDecodereplicationAllStatsV1(b *testing.B) {
v := replicationAllStatsV1{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
b.SetBytes(int64(buf.Len()))
rd := msgp.NewEndlessReader(buf.Bytes(), b)
dc := msgp.NewReader(rd)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
err := v.DecodeMsg(dc)
if err != nil {
b.Fatal(err)
}
}
}
func TestMarshalUnmarshalreplicationStats(t *testing.T) {
v := replicationStats{}
bts, err := v.MarshalMsg(nil)
if err != nil {
t.Fatal(err)
}
left, err := v.UnmarshalMsg(bts)
if err != nil {
t.Fatal(err)
}
if len(left) > 0 {
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
}
left, err = msgp.Skip(bts)
if err != nil {
t.Fatal(err)
}
if len(left) > 0 {
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
}
}
func BenchmarkMarshalMsgreplicationStats(b *testing.B) {
v := replicationStats{}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v.MarshalMsg(nil)
}
}
func BenchmarkAppendMsgreplicationStats(b *testing.B) {
v := replicationStats{}
bts := make([]byte, 0, v.Msgsize())
bts, _ = v.MarshalMsg(bts[0:0])
b.SetBytes(int64(len(bts)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
bts, _ = v.MarshalMsg(bts[0:0])
}
}
func BenchmarkUnmarshalreplicationStats(b *testing.B) {
v := replicationStats{}
bts, _ := v.MarshalMsg(nil)
b.ReportAllocs()
b.SetBytes(int64(len(bts)))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := v.UnmarshalMsg(bts)
if err != nil {
b.Fatal(err)
}
}
}
func TestEncodeDecodereplicationStats(t *testing.T) {
v := replicationStats{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
m := v.Msgsize()
if buf.Len() > m {
t.Log("WARNING: TestEncodeDecodereplicationStats Msgsize() is inaccurate")
}
vn := replicationStats{}
err := msgp.Decode(&buf, &vn)
if err != nil {
t.Error(err)
}
buf.Reset()
msgp.Encode(&buf, &v)
err = msgp.NewReader(&buf).Skip()
if err != nil {
t.Error(err)
}
}
func BenchmarkEncodereplicationStats(b *testing.B) {
v := replicationStats{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
b.SetBytes(int64(buf.Len()))
en := msgp.NewWriter(msgp.Nowhere)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v.EncodeMsg(en)
}
en.Flush()
}
func BenchmarkDecodereplicationStats(b *testing.B) {
v := replicationStats{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
b.SetBytes(int64(buf.Len()))
rd := msgp.NewEndlessReader(buf.Bytes(), b)
dc := msgp.NewReader(rd)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
err := v.DecodeMsg(dc)
if err != nil {
b.Fatal(err)
}
}
}
func TestMarshalUnmarshalreplicationStatsV1(t *testing.T) {
v := replicationStatsV1{}
bts, err := v.MarshalMsg(nil)
if err != nil {
t.Fatal(err)
}
left, err := v.UnmarshalMsg(bts)
if err != nil {
t.Fatal(err)
}
if len(left) > 0 {
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
}
left, err = msgp.Skip(bts)
if err != nil {
t.Fatal(err)
}
if len(left) > 0 {
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
}
}
func BenchmarkMarshalMsgreplicationStatsV1(b *testing.B) {
v := replicationStatsV1{}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v.MarshalMsg(nil)
}
}
func BenchmarkAppendMsgreplicationStatsV1(b *testing.B) {
v := replicationStatsV1{}
bts := make([]byte, 0, v.Msgsize())
bts, _ = v.MarshalMsg(bts[0:0])
b.SetBytes(int64(len(bts)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
bts, _ = v.MarshalMsg(bts[0:0])
}
}
func BenchmarkUnmarshalreplicationStatsV1(b *testing.B) {
v := replicationStatsV1{}
bts, _ := v.MarshalMsg(nil)
b.ReportAllocs()
b.SetBytes(int64(len(bts)))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := v.UnmarshalMsg(bts)
if err != nil {
b.Fatal(err)
}
}
}
func TestEncodeDecodereplicationStatsV1(t *testing.T) {
v := replicationStatsV1{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
m := v.Msgsize()
if buf.Len() > m {
t.Log("WARNING: TestEncodeDecodereplicationStatsV1 Msgsize() is inaccurate")
}
vn := replicationStatsV1{}
err := msgp.Decode(&buf, &vn)
if err != nil {
t.Error(err)
}
buf.Reset()
msgp.Encode(&buf, &v)
err = msgp.NewReader(&buf).Skip()
if err != nil {
t.Error(err)
}
}
func BenchmarkEncodereplicationStatsV1(b *testing.B) {
v := replicationStatsV1{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
b.SetBytes(int64(buf.Len()))
en := msgp.NewWriter(msgp.Nowhere)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v.EncodeMsg(en)
}
en.Flush()
}
func BenchmarkDecodereplicationStatsV1(b *testing.B) {
v := replicationStatsV1{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
b.SetBytes(int64(buf.Len()))
rd := msgp.NewEndlessReader(buf.Bytes(), b)
dc := msgp.NewReader(rd)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
err := v.DecodeMsg(dc)
if err != nil {
b.Fatal(err)
}
}
}
func TestMarshalUnmarshalsizeHistogram(t *testing.T) { func TestMarshalUnmarshalsizeHistogram(t *testing.T) {
v := sizeHistogram{} v := sizeHistogram{}
bts, err := v.MarshalMsg(nil) bts, err := v.MarshalMsg(nil)
-11
View File
@@ -587,17 +587,6 @@ func TestDataUsageCacheSerialize(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
e := want.find("abucket/dir2") e := want.find("abucket/dir2")
e.ReplicationStats = &replicationAllStats{
Targets: map[string]replicationStats{
"arn": {
PendingSize: 1,
ReplicatedSize: 2,
FailedSize: 3,
FailedCount: 5,
PendingCount: 6,
},
},
}
want.replace("abucket/dir2", "", *e) want.replace("abucket/dir2", "", *e)
var buf bytes.Buffer var buf bytes.Buffer
err = want.serializeTo(&buf) err = want.serializeTo(&buf)
+1 -1
View File
@@ -165,7 +165,7 @@ func TestCmpReaders(t *testing.T) {
r1 := bytes.NewReader([]byte("abc")) r1 := bytes.NewReader([]byte("abc"))
r2 := bytes.NewReader([]byte("abc")) r2 := bytes.NewReader([]byte("abc"))
ok, msg := cmpReaders(r1, r2) ok, msg := cmpReaders(r1, r2)
if !(ok && msg == "") { if !ok || msg != "" {
t.Fatalf("unexpected") t.Fatalf("unexpected")
} }
} }
+6 -7
View File
@@ -98,7 +98,6 @@ func (dt *dynamicTimeout) logEntry(duration time.Duration) {
// We leak entries while we copy // We leak entries while we copy
if entries == dynamicTimeoutLogSize { if entries == dynamicTimeoutLogSize {
// Make copy on stack in order to call adjust() // Make copy on stack in order to call adjust()
logCopy := [dynamicTimeoutLogSize]time.Duration{} logCopy := [dynamicTimeoutLogSize]time.Duration{}
copy(logCopy[:], dt.log[:]) copy(logCopy[:], dt.log[:])
@@ -117,12 +116,12 @@ func (dt *dynamicTimeout) logEntry(duration time.Duration) {
// adjust changes the value of the dynamic timeout based on the // adjust changes the value of the dynamic timeout based on the
// previous results // previous results
func (dt *dynamicTimeout) adjust(entries [dynamicTimeoutLogSize]time.Duration) { func (dt *dynamicTimeout) adjust(entries [dynamicTimeoutLogSize]time.Duration) {
failures, max := 0, time.Duration(0) failures, maxDur := 0, time.Duration(0)
for _, dur := range entries[:] { for _, dur := range entries[:] {
if dur == maxDuration { if dur == maxDuration {
failures++ failures++
} else if dur > max { } else if dur > maxDur {
max = dur maxDur = dur
} }
} }
@@ -144,12 +143,12 @@ func (dt *dynamicTimeout) adjust(entries [dynamicTimeoutLogSize]time.Duration) {
} else if failPct < dynamicTimeoutDecreaseThresholdPct { } else if failPct < dynamicTimeoutDecreaseThresholdPct {
// We are hitting the timeout relatively few times, // We are hitting the timeout relatively few times,
// so decrease the timeout towards 25 % of maximum time spent. // so decrease the timeout towards 25 % of maximum time spent.
max = max * 125 / 100 maxDur = maxDur * 125 / 100
timeout := atomic.LoadInt64(&dt.timeout) timeout := atomic.LoadInt64(&dt.timeout)
if max < time.Duration(timeout) { if maxDur < time.Duration(timeout) {
// Move 50% toward the max. // Move 50% toward the max.
timeout = (int64(max) + timeout) / 2 timeout = (int64(maxDur) + timeout) / 2
} }
if timeout < dt.minimum { if timeout < dt.minimum {
timeout = dt.minimum timeout = dt.minimum
-1
View File
@@ -167,7 +167,6 @@ func testDynamicTimeoutAdjust(t *testing.T, timeout *dynamicTimeout, f func() fl
const successTimeout = 20 * time.Second const successTimeout = 20 * time.Second
for i := 0; i < dynamicTimeoutLogSize; i++ { for i := 0; i < dynamicTimeoutLogSize; i++ {
rnd := f() rnd := f()
duration := time.Duration(float64(successTimeout) * rnd) duration := time.Duration(float64(successTimeout) * rnd)
+11 -9
View File
@@ -109,7 +109,7 @@ func kmsKeyIDFromMetadata(metadata map[string]string) string {
// be AWS S3 compliant. // be AWS S3 compliant.
// //
// DecryptETags uses a KMS bulk decryption API, if available, which // DecryptETags uses a KMS bulk decryption API, if available, which
// is more efficient than decrypting ETags sequentually. // is more efficient than decrypting ETags sequentially.
func DecryptETags(ctx context.Context, k *kms.KMS, objects []ObjectInfo) error { func DecryptETags(ctx context.Context, k *kms.KMS, objects []ObjectInfo) error {
const BatchSize = 250 // We process the objects in batches - 250 is a reasonable default. const BatchSize = 250 // We process the objects in batches - 250 is a reasonable default.
var ( var (
@@ -347,8 +347,8 @@ func rotateKey(ctx context.Context, oldKey []byte, newKeyID string, newKey []byt
return errInvalidSSEParameters // AWS returns special error for equal but invalid keys. return errInvalidSSEParameters // AWS returns special error for equal but invalid keys.
} }
return crypto.ErrInvalidCustomerKey // To provide strict AWS S3 compatibility we return: access denied. return crypto.ErrInvalidCustomerKey // To provide strict AWS S3 compatibility we return: access denied.
} }
if subtle.ConstantTimeCompare(oldKey, newKey) == 1 && sealedKey.Algorithm == crypto.SealAlgorithm { if subtle.ConstantTimeCompare(oldKey, newKey) == 1 && sealedKey.Algorithm == crypto.SealAlgorithm {
return nil // don't rotate on equal keys if seal algorithm is latest return nil // don't rotate on equal keys if seal algorithm is latest
} }
@@ -1015,7 +1015,7 @@ func DecryptObjectInfo(info *ObjectInfo, r *http.Request) (encrypted bool, err e
if encrypted { if encrypted {
if crypto.SSEC.IsEncrypted(info.UserDefined) { if crypto.SSEC.IsEncrypted(info.UserDefined) {
if !(crypto.SSEC.IsRequested(headers) || crypto.SSECopy.IsRequested(headers)) { if !crypto.SSEC.IsRequested(headers) && !crypto.SSECopy.IsRequested(headers) {
if r.Header.Get(xhttp.MinIOSourceReplicationRequest) != "true" { if r.Header.Get(xhttp.MinIOSourceReplicationRequest) != "true" {
return encrypted, errEncryptedObject return encrypted, errEncryptedObject
} }
@@ -1099,7 +1099,9 @@ func (o *ObjectInfo) decryptPartsChecksums(h http.Header) {
if _, encrypted := crypto.IsEncrypted(o.UserDefined); encrypted { if _, encrypted := crypto.IsEncrypted(o.UserDefined); encrypted {
decrypted, err := o.metadataDecrypter(h)("object-checksum", data) decrypted, err := o.metadataDecrypter(h)("object-checksum", data)
if err != nil { if err != nil {
encLogIf(GlobalContext, err) if !errors.Is(err, crypto.ErrSecretKeyMismatch) {
encLogIf(GlobalContext, err)
}
return return
} }
data = decrypted data = decrypted
@@ -1110,7 +1112,6 @@ func (o *ObjectInfo) decryptPartsChecksums(h http.Header) {
o.Parts[i].Checksums = cs[i] o.Parts[i].Checksums = cs[i]
} }
} }
return
} }
// metadataEncryptFn provides an encryption function for metadata. // metadataEncryptFn provides an encryption function for metadata.
@@ -1153,16 +1154,17 @@ 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.
// if part > 0, and we have the checksum for the part that will be returned. // if part > 0, and we have the checksum for the part that will be returned.
func (o *ObjectInfo) decryptChecksums(part int, h http.Header) map[string]string { // Returns whether the checksum (main part 0) is a multipart checksum.
func (o *ObjectInfo) decryptChecksums(part int, h http.Header) (cs map[string]string, isMP bool) {
data := o.Checksum data := o.Checksum
if len(data) == 0 { if len(data) == 0 {
return nil return nil, false
} }
if part > 0 && !crypto.SSEC.IsEncrypted(o.UserDefined) { if part > 0 && !crypto.SSEC.IsEncrypted(o.UserDefined) {
// already decrypted in ToObjectInfo for multipart objects // already decrypted in ToObjectInfo for multipart objects
for _, pi := range o.Parts { for _, pi := range o.Parts {
if pi.Number == part { if pi.Number == part {
return pi.Checksums return pi.Checksums, true
} }
} }
} }
@@ -1172,7 +1174,7 @@ func (o *ObjectInfo) decryptChecksums(part int, h http.Header) map[string]string
if err != crypto.ErrSecretKeyMismatch { if err != crypto.ErrSecretKeyMismatch {
encLogIf(GlobalContext, err) encLogIf(GlobalContext, err)
} }
return nil return nil, part > 0
} }
data = decrypted data = decrypted
} }
-2
View File
@@ -362,7 +362,6 @@ func TestGetDecryptedRange(t *testing.T) {
t.Errorf("Case %d: test failed: %d %d %d %d %d", i, o, l, skip, sn, ps) t.Errorf("Case %d: test failed: %d %d %d %d %d", i, o, l, skip, sn, ps)
} }
} }
} }
// Multipart object tests // Multipart object tests
@@ -538,7 +537,6 @@ func TestGetDecryptedRange(t *testing.T) {
i, o, l, skip, sn, ps, oRef, lRef, skipRef, snRef, psRef) i, o, l, skip, sn, ps, oRef, lRef, skipRef, snRef, psRef)
} }
} }
} }
} }
+17 -14
View File
@@ -26,6 +26,7 @@ import (
"path/filepath" "path/filepath"
"reflect" "reflect"
"runtime" "runtime"
"slices"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -38,7 +39,6 @@ import (
"github.com/minio/minio/internal/mountinfo" "github.com/minio/minio/internal/mountinfo"
"github.com/minio/pkg/v3/env" "github.com/minio/pkg/v3/env"
xnet "github.com/minio/pkg/v3/net" xnet "github.com/minio/pkg/v3/net"
"golang.org/x/exp/slices"
) )
// EndpointType - enum for endpoint type. // EndpointType - enum for endpoint type.
@@ -138,6 +138,17 @@ func (endpoint *Endpoint) SetDiskIndex(i int) {
endpoint.DiskIdx = i endpoint.DiskIdx = i
} }
func isValidURLEndpoint(u *url.URL) bool {
// URL style of endpoint.
// Valid URL style endpoint is
// - Scheme field must contain "http" or "https"
// - All field should be empty except Host and Path.
isURLOk := (u.Scheme == "http" || u.Scheme == "https") &&
u.User == nil && u.Opaque == "" && !u.ForceQuery &&
u.RawQuery == "" && u.Fragment == ""
return isURLOk
}
// 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.
@@ -157,8 +168,7 @@ func NewEndpoint(arg string) (ep Endpoint, e error) {
// Valid URL style endpoint is // Valid URL style endpoint is
// - Scheme field must contain "http" or "https" // - Scheme field must contain "http" or "https"
// - All field should be empty except Host and Path. // - All field should be empty except Host and Path.
if !((u.Scheme == "http" || u.Scheme == "https") && if !isValidURLEndpoint(u) {
u.User == nil && u.Opaque == "" && !u.ForceQuery && u.RawQuery == "" && u.Fragment == "") {
return ep, fmt.Errorf("invalid URL endpoint format") return ep, fmt.Errorf("invalid URL endpoint format")
} }
@@ -213,7 +223,6 @@ func NewEndpoint(arg string) (ep Endpoint, e error) {
u.Path = u.Path[1:] u.Path = u.Path[1:]
} }
} }
} else { } else {
// Only check if the arg is an ip address and ask for scheme since its absent. // Only check if the arg is an ip address and ask for scheme since its absent.
// localhost, example.com, any FQDN cannot be disambiguated from a regular file path such as // localhost, example.com, any FQDN cannot be disambiguated from a regular file path such as
@@ -605,11 +614,8 @@ func (endpoints Endpoints) UpdateIsLocal() error {
startTime := time.Now() startTime := time.Now()
keepAliveTicker := time.NewTicker(500 * time.Millisecond) keepAliveTicker := time.NewTicker(500 * time.Millisecond)
defer keepAliveTicker.Stop() defer keepAliveTicker.Stop()
for { for !foundLocal && (epsResolved != len(endpoints)) {
// 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)) {
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
@@ -795,11 +801,8 @@ func (p PoolEndpointList) UpdateIsLocal() error {
startTime := time.Now() startTime := time.Now()
keepAliveTicker := time.NewTicker(1 * time.Second) keepAliveTicker := time.NewTicker(1 * time.Second)
defer keepAliveTicker.Stop() defer keepAliveTicker.Stop()
for { for !foundLocal && (epsResolved != epCount) {
// 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 == epCount) {
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
@@ -1194,7 +1197,7 @@ func GetProxyEndpointLocalIndex(proxyEps []ProxyEndpoint) int {
} }
// GetProxyEndpoints - get all endpoints that can be used to proxy list request. // GetProxyEndpoints - get all endpoints that can be used to proxy list request.
func GetProxyEndpoints(endpointServerPools EndpointServerPools) []ProxyEndpoint { func GetProxyEndpoints(endpointServerPools EndpointServerPools, transport http.RoundTripper) []ProxyEndpoint {
var proxyEps []ProxyEndpoint var proxyEps []ProxyEndpoint
proxyEpSet := set.NewStringSet() proxyEpSet := set.NewStringSet()
@@ -1213,7 +1216,7 @@ func GetProxyEndpoints(endpointServerPools EndpointServerPools) []ProxyEndpoint
proxyEps = append(proxyEps, ProxyEndpoint{ proxyEps = append(proxyEps, ProxyEndpoint{
Endpoint: endpoint, Endpoint: endpoint,
Transport: globalRemoteTargetTransport, Transport: transport,
}) })
} }
} }
-1
View File
@@ -201,7 +201,6 @@ func erasureSelfTest() {
ok = false ok = false
continue continue
} }
} }
} }
if !ok { if !ok {
+27 -36
View File
@@ -277,23 +277,21 @@ func partNeedsHealing(partErrs []int) bool {
return slices.IndexFunc(partErrs, func(i int) bool { return i != checkPartSuccess && i != checkPartUnknown }) > -1 return slices.IndexFunc(partErrs, func(i int) bool { return i != checkPartSuccess && i != checkPartUnknown }) > -1
} }
func hasPartErr(partErrs []int) bool { func countPartNotSuccess(partErrs []int) (c int) {
return slices.IndexFunc(partErrs, func(i int) bool { return i != checkPartSuccess }) > -1 for _, pe := range partErrs {
if pe != checkPartSuccess {
c++
}
}
return
} }
// disksWithAllParts - This function needs to be called with // checkObjectWithAllParts sets partsMetadata and onlineDisks when xl.meta is inexistant/corrupted or outdated
// []StorageAPI returned by listOnlineDisks. Returns, // it also checks if the status of each part (corrupted, missing, ok) in each drive
// func checkObjectWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetadata []FileInfo,
// - disks which have all parts specified in the latest xl.meta. errs []error, latestMeta FileInfo, filterByETag bool, bucket, object string,
//
// - slice of errors about the state of data files on disk - can have
// a not-found error or a hash-mismatch error.
func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetadata []FileInfo,
errs []error, latestMeta FileInfo, bucket, object string,
scanMode madmin.HealScanMode, scanMode madmin.HealScanMode,
) (availableDisks []StorageAPI, dataErrsByDisk map[int][]int, dataErrsByPart map[int][]int) { ) (dataErrsByDisk map[int][]int, dataErrsByPart map[int][]int) {
availableDisks = make([]StorageAPI, len(onlineDisks))
dataErrsByDisk = make(map[int][]int, len(onlineDisks)) dataErrsByDisk = make(map[int][]int, len(onlineDisks))
for i := range onlineDisks { for i := range onlineDisks {
dataErrsByDisk[i] = make([]int, len(latestMeta.Parts)) dataErrsByDisk[i] = make([]int, len(latestMeta.Parts))
@@ -325,29 +323,32 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
} }
} }
erasureDistributionReliable := true erasureDistributionReliable := inconsistent <= len(partsMetadata)/2
if inconsistent > len(partsMetadata)/2 {
// If there are too many inconsistent files, then we can't trust erasure.Distribution (most likely
// because of bugs found in CopyObject/PutObjectTags) https://github.com/minio/minio/pull/10772
erasureDistributionReliable = false
}
metaErrs := make([]error, len(errs)) metaErrs := make([]error, len(errs))
for i, onlineDisk := range onlineDisks { for i := range onlineDisks {
if errs[i] != nil { if errs[i] != nil {
metaErrs[i] = errs[i] metaErrs[i] = errs[i]
continue continue
} }
if onlineDisk == OfflineDisk { if onlineDisks[i] == OfflineDisk {
metaErrs[i] = errDiskNotFound metaErrs[i] = errDiskNotFound
continue continue
} }
meta := partsMetadata[i] meta := partsMetadata[i]
if !meta.ModTime.Equal(latestMeta.ModTime) || meta.DataDir != latestMeta.DataDir { corrupted := false
if filterByETag {
corrupted = meta.Metadata["etag"] != latestMeta.Metadata["etag"]
} else {
corrupted = !meta.ModTime.Equal(latestMeta.ModTime) || meta.DataDir != latestMeta.DataDir
}
if corrupted {
metaErrs[i] = errFileCorrupt metaErrs[i] = errFileCorrupt
partsMetadata[i] = FileInfo{} partsMetadata[i] = FileInfo{}
onlineDisks[i] = nil
continue continue
} }
@@ -355,6 +356,7 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
if !meta.IsValid() { if !meta.IsValid() {
partsMetadata[i] = FileInfo{} partsMetadata[i] = FileInfo{}
metaErrs[i] = errFileCorrupt metaErrs[i] = errFileCorrupt
onlineDisks[i] = nil
continue continue
} }
@@ -365,6 +367,7 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
// might have the right erasure distribution. // might have the right erasure distribution.
partsMetadata[i] = FileInfo{} partsMetadata[i] = FileInfo{}
metaErrs[i] = errFileCorrupt metaErrs[i] = errFileCorrupt
onlineDisks[i] = nil
continue continue
} }
} }
@@ -385,8 +388,8 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
if metaErrs[i] != nil { if metaErrs[i] != nil {
continue continue
} }
meta := partsMetadata[i]
meta := partsMetadata[i]
if meta.Deleted || meta.IsRemote() { if meta.Deleted || meta.IsRemote() {
continue continue
} }
@@ -408,7 +411,6 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
verifyResp *CheckPartsResp verifyResp *CheckPartsResp
) )
meta.DataDir = latestMeta.DataDir
switch scanMode { switch scanMode {
case madmin.HealDeepScan: case madmin.HealDeepScan:
// disk has a valid xl.meta but may not have all the // disk has a valid xl.meta but may not have all the
@@ -434,16 +436,5 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
dataErrsByDisk[disk][part] = dataErrsByPart[part][disk] dataErrsByDisk[disk][part] = dataErrsByPart[part][disk]
} }
} }
for i, onlineDisk := range onlineDisks {
if metaErrs[i] == nil && !hasPartErr(dataErrsByDisk[i]) {
// All parts verified, mark it as all data available.
availableDisks[i] = onlineDisk
} else {
// upon errors just make that disk's fileinfo invalid
partsMetadata[i] = FileInfo{}
}
}
return return
} }
+22 -41
View File
@@ -304,7 +304,6 @@ func TestListOnlineDisks(t *testing.T) {
f.Close() f.Close()
break break
} }
} }
rQuorum := len(errs) - z.serverPools[0].sets[0].defaultParityCount rQuorum := len(errs) - z.serverPools[0].sets[0].defaultParityCount
@@ -313,11 +312,11 @@ func TestListOnlineDisks(t *testing.T) {
t.Fatalf("Expected modTime to be equal to %v but was found to be %v", t.Fatalf("Expected modTime to be equal to %v but was found to be %v",
test.expectedTime, modTime) test.expectedTime, modTime)
} }
availableDisks, _, _ := disksWithAllParts(ctx, onlineDisks, partsMetadata, _, _ = checkObjectWithAllParts(ctx, onlineDisks, partsMetadata,
test.errs, fi, bucket, object, madmin.HealDeepScan) test.errs, fi, false, bucket, object, madmin.HealDeepScan)
if test._tamperBackend != noTamper { if test._tamperBackend != noTamper {
if tamperedIndex != -1 && availableDisks[tamperedIndex] != nil { if tamperedIndex != -1 && onlineDisks[tamperedIndex] != nil {
t.Fatalf("Drive (%v) with part.1 missing is not a drive with available data", t.Fatalf("Drive (%v) with part.1 missing is not a drive with available data",
erasureDisks[tamperedIndex]) erasureDisks[tamperedIndex])
} }
@@ -485,7 +484,6 @@ func TestListOnlineDisksSmallObjects(t *testing.T) {
f.Close() f.Close()
break break
} }
} }
rQuorum := len(errs) - z.serverPools[0].sets[0].defaultParityCount rQuorum := len(errs) - z.serverPools[0].sets[0].defaultParityCount
@@ -495,11 +493,11 @@ func TestListOnlineDisksSmallObjects(t *testing.T) {
test.expectedTime, modTime) test.expectedTime, modTime)
} }
availableDisks, _, _ := disksWithAllParts(ctx, onlineDisks, partsMetadata, _, _ = checkObjectWithAllParts(ctx, onlineDisks, partsMetadata,
test.errs, fi, bucket, object, madmin.HealDeepScan) test.errs, fi, false, bucket, object, madmin.HealDeepScan)
if test._tamperBackend != noTamper { if test._tamperBackend != noTamper {
if tamperedIndex != -1 && availableDisks[tamperedIndex] != nil { if tamperedIndex != -1 && onlineDisks[tamperedIndex] != nil {
t.Fatalf("Drive (%v) with part.1 missing is not a drive with available data", t.Fatalf("Drive (%v) with part.1 missing is not a drive with available data",
erasureDisks[tamperedIndex]) erasureDisks[tamperedIndex])
} }
@@ -545,6 +543,7 @@ func TestDisksWithAllParts(t *testing.T) {
// Test 1: Test that all disks are returned without any failures with // Test 1: Test that all disks are returned without any failures with
// unmodified meta data // unmodified meta data
erasureDisks = s.getDisks()
partsMetadata, errs := readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true) partsMetadata, errs := readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true)
if err != nil { if err != nil {
t.Fatalf("Failed to read xl meta data %v", err) t.Fatalf("Failed to read xl meta data %v", err)
@@ -557,14 +556,10 @@ func TestDisksWithAllParts(t *testing.T) {
erasureDisks, _, _ = listOnlineDisks(erasureDisks, partsMetadata, errs, readQuorum) erasureDisks, _, _ = listOnlineDisks(erasureDisks, partsMetadata, errs, readQuorum)
filteredDisks, _, dataErrsPerDisk := disksWithAllParts(ctx, erasureDisks, partsMetadata, dataErrsPerDisk, _ := checkObjectWithAllParts(ctx, erasureDisks, partsMetadata,
errs, fi, bucket, object, madmin.HealDeepScan) errs, fi, false, bucket, object, madmin.HealDeepScan)
if len(filteredDisks) != len(erasureDisks) { for diskIndex, disk := range erasureDisks {
t.Errorf("Unexpected number of drives: %d", len(filteredDisks))
}
for diskIndex, disk := range filteredDisks {
if partNeedsHealing(dataErrsPerDisk[diskIndex]) { if partNeedsHealing(dataErrsPerDisk[diskIndex]) {
t.Errorf("Unexpected error: %v", dataErrsPerDisk[diskIndex]) t.Errorf("Unexpected error: %v", dataErrsPerDisk[diskIndex])
} }
@@ -575,17 +570,15 @@ func TestDisksWithAllParts(t *testing.T) {
} }
// Test 2: Not synchronized modtime // Test 2: Not synchronized modtime
erasureDisks = s.getDisks()
partsMetadataBackup := partsMetadata[0] partsMetadataBackup := partsMetadata[0]
partsMetadata[0].ModTime = partsMetadata[0].ModTime.Add(-1 * time.Hour) partsMetadata[0].ModTime = partsMetadata[0].ModTime.Add(-1 * time.Hour)
errs = make([]error, len(erasureDisks)) errs = make([]error, len(erasureDisks))
filteredDisks, _, _ = disksWithAllParts(ctx, erasureDisks, partsMetadata, _, _ = checkObjectWithAllParts(ctx, erasureDisks, partsMetadata,
errs, fi, bucket, object, madmin.HealDeepScan) errs, fi, false, bucket, object, madmin.HealDeepScan)
if len(filteredDisks) != len(erasureDisks) { for diskIndex, disk := range erasureDisks {
t.Errorf("Unexpected number of drives: %d", len(filteredDisks))
}
for diskIndex, disk := range filteredDisks {
if diskIndex == 0 && disk != nil { if diskIndex == 0 && disk != nil {
t.Errorf("Drive not filtered as expected, drive: %d", diskIndex) t.Errorf("Drive not filtered as expected, drive: %d", diskIndex)
} }
@@ -596,17 +589,15 @@ func TestDisksWithAllParts(t *testing.T) {
partsMetadata[0] = partsMetadataBackup // Revert before going to the next test partsMetadata[0] = partsMetadataBackup // Revert before going to the next test
// Test 3: Not synchronized DataDir // Test 3: Not synchronized DataDir
erasureDisks = s.getDisks()
partsMetadataBackup = partsMetadata[1] partsMetadataBackup = partsMetadata[1]
partsMetadata[1].DataDir = "foo-random" partsMetadata[1].DataDir = "foo-random"
errs = make([]error, len(erasureDisks)) errs = make([]error, len(erasureDisks))
filteredDisks, _, _ = disksWithAllParts(ctx, erasureDisks, partsMetadata, _, _ = checkObjectWithAllParts(ctx, erasureDisks, partsMetadata,
errs, fi, bucket, object, madmin.HealDeepScan) errs, fi, false, bucket, object, madmin.HealDeepScan)
if len(filteredDisks) != len(erasureDisks) { for diskIndex, disk := range erasureDisks {
t.Errorf("Unexpected number of drives: %d", len(filteredDisks))
}
for diskIndex, disk := range filteredDisks {
if diskIndex == 1 && disk != nil { if diskIndex == 1 && disk != nil {
t.Errorf("Drive not filtered as expected, drive: %d", diskIndex) t.Errorf("Drive not filtered as expected, drive: %d", diskIndex)
} }
@@ -617,6 +608,7 @@ func TestDisksWithAllParts(t *testing.T) {
partsMetadata[1] = partsMetadataBackup // Revert before going to the next test partsMetadata[1] = partsMetadataBackup // Revert before going to the next test
// Test 4: key = disk index, value = part name with hash mismatch // Test 4: key = disk index, value = part name with hash mismatch
erasureDisks = s.getDisks()
diskFailures := make(map[int]string) diskFailures := make(map[int]string)
diskFailures[0] = "part.1" diskFailures[0] = "part.1"
diskFailures[3] = "part.1" diskFailures[3] = "part.1"
@@ -637,29 +629,18 @@ func TestDisksWithAllParts(t *testing.T) {
} }
errs = make([]error, len(erasureDisks)) errs = make([]error, len(erasureDisks))
filteredDisks, dataErrsPerDisk, _ = disksWithAllParts(ctx, erasureDisks, partsMetadata, dataErrsPerDisk, _ = checkObjectWithAllParts(ctx, erasureDisks, partsMetadata,
errs, fi, bucket, object, madmin.HealDeepScan) errs, fi, false, bucket, object, madmin.HealDeepScan)
if len(filteredDisks) != len(erasureDisks) { for diskIndex := range erasureDisks {
t.Errorf("Unexpected number of drives: %d", len(filteredDisks))
}
for diskIndex, disk := range filteredDisks {
if _, ok := diskFailures[diskIndex]; ok { if _, ok := diskFailures[diskIndex]; ok {
if disk != nil {
t.Errorf("Drive not filtered as expected, drive: %d", diskIndex)
}
if !partNeedsHealing(dataErrsPerDisk[diskIndex]) { if !partNeedsHealing(dataErrsPerDisk[diskIndex]) {
t.Errorf("Disk expected to be healed, driveIndex: %d", diskIndex) t.Errorf("Disk expected to be healed, driveIndex: %d", diskIndex)
} }
} else { } else {
if disk == nil {
t.Errorf("Drive erroneously filtered, driveIndex: %d", diskIndex)
}
if partNeedsHealing(dataErrsPerDisk[diskIndex]) { if partNeedsHealing(dataErrsPerDisk[diskIndex]) {
t.Errorf("Disk not expected to be healed, driveIndex: %d", diskIndex) t.Errorf("Disk not expected to be healed, driveIndex: %d", diskIndex)
} }
} }
} }
} }
+128 -90
View File
@@ -32,7 +32,7 @@ import (
"github.com/minio/minio/internal/grid" "github.com/minio/minio/internal/grid"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/minio/pkg/v3/sync/errgroup" "github.com/minio/pkg/v3/sync/errgroup"
"golang.org/x/exp/slices" "github.com/puzpuzpuz/xsync/v3"
) )
//go:generate stringer -type=healingMetric -trimprefix=healingMetric $GOFILE //go:generate stringer -type=healingMetric -trimprefix=healingMetric $GOFILE
@@ -45,7 +45,8 @@ const (
healingMetricCheckAbandonedParts healingMetricCheckAbandonedParts
) )
func (er erasureObjects) listAndHeal(ctx context.Context, bucket, prefix string, scanMode madmin.HealScanMode, healEntry func(string, metaCacheEntry, madmin.HealScanMode) error) error { // List a prefix or a single object versions and heal
func (er erasureObjects) listAndHeal(ctx context.Context, bucket, prefix string, recursive bool, scanMode madmin.HealScanMode, healEntry func(string, metaCacheEntry, madmin.HealScanMode) error) error {
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
@@ -78,11 +79,14 @@ func (er erasureObjects) listAndHeal(ctx context.Context, bucket, prefix string,
bucket: bucket, bucket: bucket,
path: path, path: path,
filterPrefix: filterPrefix, filterPrefix: filterPrefix,
recursive: true, recursive: recursive,
forwardTo: "", forwardTo: "",
minDisks: 1, minDisks: 1,
reportNotFound: false, reportNotFound: false,
agreed: func(entry metaCacheEntry) { agreed: func(entry metaCacheEntry) {
if !recursive && prefix != entry.name {
return
}
if err := healEntry(bucket, entry, scanMode); err != nil { if err := healEntry(bucket, entry, scanMode); err != nil {
cancel() cancel()
} }
@@ -94,7 +98,9 @@ func (er erasureObjects) listAndHeal(ctx context.Context, bucket, prefix string,
// proceed to heal nonetheless. // proceed to heal nonetheless.
entry, _ = entries.firstFound() entry, _ = entries.firstFound()
} }
if !recursive && prefix != entry.name {
return
}
if err := healEntry(bucket, *entry, scanMode); err != nil { if err := healEntry(bucket, *entry, scanMode); err != nil {
cancel() cancel()
return return
@@ -112,9 +118,8 @@ func (er erasureObjects) listAndHeal(ctx context.Context, bucket, prefix string,
// listAllBuckets lists all buckets from all disks. It also // listAllBuckets lists all buckets from all disks. It also
// returns the occurrence of each buckets in all disks // returns the occurrence of each buckets in all disks
func listAllBuckets(ctx context.Context, storageDisks []StorageAPI, healBuckets map[string]VolInfo, readQuorum int) error { func listAllBuckets(ctx context.Context, storageDisks []StorageAPI, healBuckets *xsync.MapOf[string, VolInfo], readQuorum int) error {
g := errgroup.WithNErrs(len(storageDisks)) g := errgroup.WithNErrs(len(storageDisks))
var mu sync.Mutex
for index := range storageDisks { for index := range storageDisks {
index := index index := index
g.Go(func() error { g.Go(func() error {
@@ -126,6 +131,7 @@ func listAllBuckets(ctx context.Context, storageDisks []StorageAPI, healBuckets
if err != nil { if err != nil {
return err return err
} }
for _, volInfo := range volsInfo { for _, volInfo := range volsInfo {
// StorageAPI can send volume names which are // StorageAPI can send volume names which are
// incompatible with buckets - these are // incompatible with buckets - these are
@@ -133,53 +139,75 @@ func listAllBuckets(ctx context.Context, storageDisks []StorageAPI, healBuckets
if isReservedOrInvalidBucket(volInfo.Name, false) { if isReservedOrInvalidBucket(volInfo.Name, false) {
continue continue
} }
mu.Lock()
if _, ok := healBuckets[volInfo.Name]; !ok { healBuckets.Compute(volInfo.Name, func(oldValue VolInfo, loaded bool) (newValue VolInfo, del bool) {
healBuckets[volInfo.Name] = volInfo if loaded {
} newValue = oldValue
mu.Unlock() newValue.count = oldValue.count + 1
return newValue, false
}
return VolInfo{
Name: volInfo.Name,
Created: volInfo.Created,
count: 1,
}, false
})
} }
return nil return nil
}, index) }, index)
} }
return reduceReadQuorumErrs(ctx, g.Wait(), bucketMetadataOpIgnoredErrs, readQuorum)
if err := reduceReadQuorumErrs(ctx, g.Wait(), bucketMetadataOpIgnoredErrs, readQuorum); err != nil {
return err
}
healBuckets.Range(func(volName string, volInfo VolInfo) bool {
if volInfo.count < readQuorum {
healBuckets.Delete(volName)
}
return true
})
return nil
} }
var errLegacyXLMeta = errors.New("legacy XL meta") var (
errLegacyXLMeta = errors.New("legacy XL meta")
var errOutdatedXLMeta = errors.New("outdated XL meta") errOutdatedXLMeta = errors.New("outdated XL meta")
errPartCorrupt = errors.New("part corrupt")
var errPartMissingOrCorrupt = errors.New("part missing or corrupt") errPartMissing = errors.New("part missing")
)
// Only heal on disks where we are sure that healing is needed. We can expand // Only heal on disks where we are sure that healing is needed. We can expand
// this list as and when we figure out more errors can be added to this list safely. // this list as and when we figure out more errors can be added to this list safely.
func shouldHealObjectOnDisk(erErr error, partsErrs []int, meta FileInfo, latestMeta FileInfo) (bool, error) { func shouldHealObjectOnDisk(erErr error, partsErrs []int, meta FileInfo, latestMeta FileInfo) (bool, bool, error) {
if errors.Is(erErr, errFileNotFound) || errors.Is(erErr, errFileVersionNotFound) || errors.Is(erErr, errFileCorrupt) { if errors.Is(erErr, errFileNotFound) || errors.Is(erErr, errFileVersionNotFound) || errors.Is(erErr, errFileCorrupt) {
return true, erErr return true, true, erErr
} }
if erErr == nil { if erErr == nil {
if meta.XLV1 { if meta.XLV1 {
// Legacy means heal always // Legacy means heal always
// always check first. // always check first.
return true, errLegacyXLMeta return true, true, errLegacyXLMeta
} }
if !latestMeta.Equals(meta) { if !latestMeta.Equals(meta) {
return true, errOutdatedXLMeta return true, true, errOutdatedXLMeta
} }
if !meta.Deleted && !meta.IsRemote() { if !meta.Deleted && !meta.IsRemote() {
// If xl.meta was read fine but there may be problem with the part.N files. // If xl.meta was read fine but there may be problem with the part.N files.
for _, partErr := range partsErrs { for _, partErr := range partsErrs {
if slices.Contains([]int{ if partErr == checkPartFileNotFound {
checkPartFileNotFound, return true, false, errPartMissing
checkPartFileCorrupt, }
}, partErr) { if partErr == checkPartFileCorrupt {
return true, errPartMissingOrCorrupt return true, false, errPartCorrupt
} }
} }
} }
return false, nil return false, false, nil
} }
return false, erErr return false, false, erErr
} }
const ( const (
@@ -234,11 +262,12 @@ func (er *erasureObjects) auditHealObject(ctx context.Context, bucket, object, v
} }
b, a := result.GetCorruptedCounts() b, a := result.GetCorruptedCounts()
if b == a { if b > 0 && b == a {
opts.Error = fmt.Sprintf("unable to heal %d corrupted blocks on drives", b) opts.Error = fmt.Sprintf("unable to heal %d corrupted blocks on drives", b)
} }
b, a = result.GetMissingCounts() b, a = result.GetMissingCounts()
if b == a { if b > 0 && b == a {
opts.Error = fmt.Sprintf("unable to heal %d missing blocks on drives", b) opts.Error = fmt.Sprintf("unable to heal %d missing blocks on drives", b)
} }
@@ -253,6 +282,21 @@ func (er *erasureObjects) auditHealObject(ctx context.Context, bucket, object, v
auditLogInternal(ctx, opts) auditLogInternal(ctx, opts)
} }
func objectErrToDriveState(reason error) string {
switch {
case reason == nil:
return madmin.DriveStateOk
case IsErr(reason, errDiskNotFound):
return madmin.DriveStateOffline
case IsErr(reason, errFileNotFound, errFileVersionNotFound, errVolumeNotFound, errPartMissing, errOutdatedXLMeta, errLegacyXLMeta):
return madmin.DriveStateMissing
case IsErr(reason, errFileCorrupt, errPartCorrupt):
return madmin.DriveStateCorrupt
default:
return fmt.Sprintf("%s (%s)", madmin.DriveStateUnknown, reason.Error())
}
}
// Heals an object by re-writing corrupt/missing erasure blocks. // Heals an object by re-writing corrupt/missing erasure blocks.
func (er *erasureObjects) healObject(ctx context.Context, bucket string, object string, versionID string, opts madmin.HealOpts) (result madmin.HealResultItem, err error) { func (er *erasureObjects) healObject(ctx context.Context, bucket string, object string, versionID string, opts madmin.HealOpts) (result madmin.HealResultItem, err error) {
dryRun := opts.DryRun dryRun := opts.DryRun
@@ -327,26 +371,20 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
// List of disks having latest version of the object xl.meta // List of disks having latest version of the object xl.meta
// (by modtime). // (by modtime).
onlineDisks, modTime, etag := listOnlineDisks(storageDisks, partsMetadata, errs, readQuorum) onlineDisks, quorumModTime, quorumETag := listOnlineDisks(storageDisks, partsMetadata, errs, readQuorum)
// Latest FileInfo for reference. If a valid metadata is not // Latest FileInfo for reference. If a valid metadata is not
// present, it is as good as object not found. // present, it is as good as object not found.
latestMeta, err := pickValidFileInfo(ctx, partsMetadata, modTime, etag, readQuorum) latestMeta, err := pickValidFileInfo(ctx, partsMetadata, quorumModTime, quorumETag, readQuorum)
if err != nil { if err != nil {
return result, err return result, err
} }
// List of disks having all parts as per latest metadata. // No modtime quorum
// NOTE: do not pass in latestDisks to diskWithAllParts since filterDisksByETag := quorumETag != ""
// the diskWithAllParts needs to reach the drive to ensure
// validity of the metadata content, we should make sure that dataErrsByDisk, dataErrsByPart := checkObjectWithAllParts(ctx, onlineDisks, partsMetadata,
// we pass in disks as is for it to be verified. Once verified errs, latestMeta, filterDisksByETag, bucket, object, scanMode)
// the disksWithAllParts() returns the actual disks that can be
// used here for reconstruction. This is done to ensure that
// we do not skip drives that have inconsistent metadata to be
// skipped from purging when they are stale.
availableDisks, dataErrsByDisk, dataErrsByPart := disksWithAllParts(ctx, onlineDisks, partsMetadata,
errs, latestMeta, bucket, object, scanMode)
var erasure Erasure var erasure Erasure
if !latestMeta.Deleted && !latestMeta.IsRemote() { if !latestMeta.Deleted && !latestMeta.IsRemote() {
@@ -367,26 +405,18 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
// data state and a list of outdated disks on which data needs // data state and a list of outdated disks on which data needs
// to be healed. // to be healed.
outDatedDisks := make([]StorageAPI, len(storageDisks)) outDatedDisks := make([]StorageAPI, len(storageDisks))
disksToHealCount := 0 disksToHealCount, xlMetaToHealCount := 0, 0
for i := range availableDisks { for i := range onlineDisks {
yes, reason := shouldHealObjectOnDisk(errs[i], dataErrsByDisk[i], partsMetadata[i], latestMeta) yes, isMeta, reason := shouldHealObjectOnDisk(errs[i], dataErrsByDisk[i], partsMetadata[i], latestMeta)
if yes { if yes {
outDatedDisks[i] = storageDisks[i] outDatedDisks[i] = storageDisks[i]
disksToHealCount++ disksToHealCount++
if isMeta {
xlMetaToHealCount++
}
} }
driveState := "" driveState := objectErrToDriveState(reason)
switch {
case reason == nil:
driveState = madmin.DriveStateOk
case IsErr(reason, errDiskNotFound):
driveState = madmin.DriveStateOffline
case IsErr(reason, errFileNotFound, errFileVersionNotFound, errVolumeNotFound, errPartMissingOrCorrupt, errOutdatedXLMeta, errLegacyXLMeta):
driveState = madmin.DriveStateMissing
default:
// all remaining cases imply corrupt data/metadata
driveState = madmin.DriveStateCorrupt
}
result.Before.Drives = append(result.Before.Drives, madmin.HealDriveInfo{ result.Before.Drives = append(result.Before.Drives, madmin.HealDriveInfo{
UUID: "", UUID: "",
@@ -400,16 +430,6 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
}) })
} }
if isAllNotFound(errs) {
// File is fully gone, fileInfo is empty.
err := errFileNotFound
if versionID != "" {
err = errFileVersionNotFound
}
return er.defaultHealResult(FileInfo{}, storageDisks, storageEndpoints, errs,
bucket, object, versionID), err
}
if disksToHealCount == 0 { if disksToHealCount == 0 {
// Nothing to heal! // Nothing to heal!
return result, nil return result, nil
@@ -421,7 +441,24 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
return result, nil return result, nil
} }
if !latestMeta.XLV1 && !latestMeta.Deleted && disksToHealCount > latestMeta.Erasure.ParityBlocks { cannotHeal := !latestMeta.XLV1 && !latestMeta.Deleted && xlMetaToHealCount > latestMeta.Erasure.ParityBlocks
if cannotHeal && quorumETag != "" {
// This is an object that is supposed to be removed by the dangling code
// but we noticed that ETag is the same for all objects, let's give it a shot
cannotHeal = false
}
if !latestMeta.Deleted && !latestMeta.IsRemote() {
// check if there is a part that lost its quorum
for _, partErrs := range dataErrsByPart {
if countPartNotSuccess(partErrs) > latestMeta.Erasure.ParityBlocks {
cannotHeal = true
break
}
}
}
if cannotHeal {
// Allow for dangling deletes, on versions that have DataDir missing etc. // Allow for dangling deletes, on versions that have DataDir missing etc.
// this would end up restoring the correct readable versions. // this would end up restoring the correct readable versions.
m, err := er.deleteIfDangling(ctx, bucket, object, partsMetadata, errs, dataErrsByPart, ObjectOptions{ m, err := er.deleteIfDangling(ctx, bucket, object, partsMetadata, errs, dataErrsByPart, ObjectOptions{
@@ -459,16 +496,15 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
tmpID := mustGetUUID() tmpID := mustGetUUID()
migrateDataDir := mustGetUUID() migrateDataDir := mustGetUUID()
// Reorder so that we have data disks first and parity disks next. if !latestMeta.Deleted && len(latestMeta.Erasure.Distribution) != len(onlineDisks) {
if !latestMeta.Deleted && len(latestMeta.Erasure.Distribution) != len(availableDisks) { err := fmt.Errorf("unexpected file distribution (%v) from online disks (%v), looks like backend disks have been manually modified refusing to heal %s/%s(%s)",
err := fmt.Errorf("unexpected file distribution (%v) from available disks (%v), looks like backend disks have been manually modified refusing to heal %s/%s(%s)", latestMeta.Erasure.Distribution, onlineDisks, bucket, object, versionID)
latestMeta.Erasure.Distribution, availableDisks, bucket, object, versionID) healingLogOnceIf(ctx, err, "heal-object-online-disks")
healingLogOnceIf(ctx, err, "heal-object-available-disks")
return er.defaultHealResult(latestMeta, storageDisks, storageEndpoints, errs, return er.defaultHealResult(latestMeta, storageDisks, storageEndpoints, errs,
bucket, object, versionID), err bucket, object, versionID), err
} }
latestDisks := shuffleDisks(availableDisks, latestMeta.Erasure.Distribution) latestDisks := shuffleDisks(onlineDisks, latestMeta.Erasure.Distribution)
if !latestMeta.Deleted && len(latestMeta.Erasure.Distribution) != len(outDatedDisks) { if !latestMeta.Deleted && len(latestMeta.Erasure.Distribution) != len(outDatedDisks) {
err := fmt.Errorf("unexpected file distribution (%v) from outdated disks (%v), looks like backend disks have been manually modified refusing to heal %s/%s(%s)", err := fmt.Errorf("unexpected file distribution (%v) from outdated disks (%v), looks like backend disks have been manually modified refusing to heal %s/%s(%s)",
@@ -539,12 +575,15 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
if disk == OfflineDisk { if disk == OfflineDisk {
continue continue
} }
thisPartErrs := shuffleCheckParts(dataErrsByPart[partIndex], latestMeta.Erasure.Distribution)
if thisPartErrs[i] != checkPartSuccess {
continue
}
checksumInfo := copyPartsMetadata[i].Erasure.GetChecksumInfo(partNumber) checksumInfo := copyPartsMetadata[i].Erasure.GetChecksumInfo(partNumber)
partPath := pathJoin(object, srcDataDir, fmt.Sprintf("part.%d", partNumber)) partPath := pathJoin(object, srcDataDir, fmt.Sprintf("part.%d", partNumber))
readers[i] = newBitrotReader(disk, copyPartsMetadata[i].Data, bucket, partPath, tillOffset, checksumAlgo, readers[i] = newBitrotReader(disk, copyPartsMetadata[i].Data, bucket, partPath, tillOffset, checksumAlgo,
checksumInfo.Hash, erasure.ShardSize()) checksumInfo.Hash, erasure.ShardSize())
prefer[i] = disk.Hostname() == "" prefer[i] = disk.Hostname() == ""
} }
writers := make([]io.Writer, len(outDatedDisks)) writers := make([]io.Writer, len(outDatedDisks))
for i, disk := range outDatedDisks { for i, disk := range outDatedDisks {
@@ -569,7 +608,7 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
// later to the final location. // later to the final location.
err = erasure.Heal(ctx, writers, readers, partSize, prefer) err = erasure.Heal(ctx, writers, readers, partSize, prefer)
closeBitrotReaders(readers) closeBitrotReaders(readers)
closeBitrotWriters(writers) closeErrs := closeBitrotWriters(writers)
if err != nil { if err != nil {
return result, err return result, err
} }
@@ -589,6 +628,13 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
continue continue
} }
// A non-nil stale disk which got error on Close()
if closeErrs[i] != nil {
outDatedDisks[i] = nil
disksToHealCount--
continue
}
partsMetadata[i].DataDir = dstDataDir partsMetadata[i].DataDir = dstDataDir
partsMetadata[i].AddObjectPart(partNumber, "", partSize, partActualSize, partModTime, partIdx, partChecksums) partsMetadata[i].AddObjectPart(partNumber, "", partSize, partActualSize, partModTime, partIdx, partChecksums)
if len(inlineBuffers) > 0 && inlineBuffers[i] != nil { if len(inlineBuffers) > 0 && inlineBuffers[i] != nil {
@@ -603,9 +649,7 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
if disksToHealCount == 0 { if disksToHealCount == 0 {
return result, fmt.Errorf("all drives had write errors, unable to heal %s/%s", bucket, object) return result, fmt.Errorf("all drives had write errors, unable to heal %s/%s", bucket, object)
} }
} }
} }
defer er.deleteAll(context.Background(), minioMetaTmpBucket, tmpID) defer er.deleteAll(context.Background(), minioMetaTmpBucket, tmpID)
@@ -806,13 +850,7 @@ func (er *erasureObjects) defaultHealResult(lfi FileInfo, storageDisks []Storage
}) })
continue continue
} }
driveState := madmin.DriveStateCorrupt driveState := objectErrToDriveState(errs[index])
switch errs[index] {
case errFileNotFound, errVolumeNotFound:
driveState = madmin.DriveStateMissing
case nil:
driveState = madmin.DriveStateOk
}
result.Before.Drives = append(result.Before.Drives, madmin.HealDriveInfo{ result.Before.Drives = append(result.Before.Drives, madmin.HealDriveInfo{
UUID: "", UUID: "",
Endpoint: storageEndpoints[index].String(), Endpoint: storageEndpoints[index].String(),
@@ -905,12 +943,12 @@ func isObjectDirDangling(errs []error) (ok bool) {
var foundNotEmpty int var foundNotEmpty int
var otherFound int var otherFound int
for _, readErr := range errs { for _, readErr := range errs {
switch { switch readErr {
case readErr == nil: case nil:
found++ found++
case readErr == errFileNotFound || readErr == errVolumeNotFound: case errFileNotFound, errVolumeNotFound:
notFound++ notFound++
case readErr == errVolumeNotEmpty: case errVolumeNotEmpty:
foundNotEmpty++ foundNotEmpty++
default: default:
otherFound++ otherFound++
+3 -3
View File
@@ -733,7 +733,7 @@ func TestHealingDanglingObject(t *testing.T) {
t.Fatalf("Expected versions 1, got %d", fileInfoPreHeal.NumVersions) t.Fatalf("Expected versions 1, got %d", fileInfoPreHeal.NumVersions)
} }
if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Remove: true}, if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Recursive: true, Remove: true},
func(bucket, object, vid string, scanMode madmin.HealScanMode) error { func(bucket, object, vid string, scanMode madmin.HealScanMode) error {
_, err := objLayer.HealObject(ctx, bucket, object, vid, madmin.HealOpts{ScanMode: scanMode, Remove: true}) _, err := objLayer.HealObject(ctx, bucket, object, vid, madmin.HealOpts{ScanMode: scanMode, Remove: true})
return err return err
@@ -780,7 +780,7 @@ func TestHealingDanglingObject(t *testing.T) {
t.Fatalf("Expected versions 1, got %d", fileInfoPreHeal.NumVersions) t.Fatalf("Expected versions 1, got %d", fileInfoPreHeal.NumVersions)
} }
if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Remove: true}, if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Recursive: true, Remove: true},
func(bucket, object, vid string, scanMode madmin.HealScanMode) error { func(bucket, object, vid string, scanMode madmin.HealScanMode) error {
_, err := objLayer.HealObject(ctx, bucket, object, vid, madmin.HealOpts{ScanMode: scanMode, Remove: true}) _, err := objLayer.HealObject(ctx, bucket, object, vid, madmin.HealOpts{ScanMode: scanMode, Remove: true})
return err return err
@@ -829,7 +829,7 @@ func TestHealingDanglingObject(t *testing.T) {
t.Fatalf("Expected versions 3, got %d", fileInfoPreHeal.NumVersions) t.Fatalf("Expected versions 3, got %d", fileInfoPreHeal.NumVersions)
} }
if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Remove: true}, if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Recursive: true, Remove: true},
func(bucket, object, vid string, scanMode madmin.HealScanMode) error { func(bucket, object, vid string, scanMode madmin.HealScanMode) error {
_, err := objLayer.HealObject(ctx, bucket, object, vid, madmin.HealOpts{ScanMode: scanMode, Remove: true}) _, err := objLayer.HealObject(ctx, bucket, object, vid, madmin.HealOpts{ScanMode: scanMode, Remove: true})
return err return err
+32 -32
View File
@@ -52,15 +52,15 @@ func reduceCommonVersions(diskVersions [][]byte, writeQuorum int) (versions []by
} }
var commonVersions uint64 var commonVersions uint64
max := 0 maxCnt := 0
for versions, count := range diskVersionsCount { for versions, count := range diskVersionsCount {
if max < count { if maxCnt < count {
max = count maxCnt = count
commonVersions = versions commonVersions = versions
} }
} }
if max >= writeQuorum { if maxCnt >= writeQuorum {
for _, versions := range diskVersions { for _, versions := range diskVersions {
if binary.BigEndian.Uint64(versions) == commonVersions { if binary.BigEndian.Uint64(versions) == commonVersions {
return versions return versions
@@ -80,15 +80,15 @@ func reduceCommonDataDir(dataDirs []string, writeQuorum int) (dataDir string) {
dataDirsCount[ddir]++ dataDirsCount[ddir]++
} }
max := 0 maxCnt := 0
for ddir, count := range dataDirsCount { for ddir, count := range dataDirsCount {
if max < count { if maxCnt < count {
max = count maxCnt = count
dataDir = ddir dataDir = ddir
} }
} }
if max >= writeQuorum { if maxCnt >= writeQuorum {
return dataDir return dataDir
} }
@@ -115,20 +115,20 @@ func reduceErrs(errs []error, ignoredErrs []error) (maxCount int, maxErr error)
errorCounts[err]++ errorCounts[err]++
} }
max := 0 maxCnt := 0
for err, count := range errorCounts { for err, count := range errorCounts {
switch { switch {
case max < count: case maxCnt < count:
max = count maxCnt = count
maxErr = err maxErr = err
// Prefer `nil` over other error values with the same // Prefer `nil` over other error values with the same
// number of occurrences. // number of occurrences.
case max == count && err == nil: case maxCnt == count && err == nil:
maxErr = err maxErr = err
} }
} }
return max, maxErr return maxCnt, maxErr
} }
// reduceQuorumErrs behaves like reduceErrs by only for returning // reduceQuorumErrs behaves like reduceErrs by only for returning
@@ -295,34 +295,34 @@ func shuffleDisksAndPartsMetadata(disks []StorageAPI, partsMetadata []FileInfo,
return shuffledDisks, shuffledPartsMetadata return shuffledDisks, shuffledPartsMetadata
} }
// Return shuffled partsMetadata depending on distribution. func shuffleWithDist[T any](input []T, distribution []int) []T {
func shufflePartsMetadata(partsMetadata []FileInfo, distribution []int) (shuffledPartsMetadata []FileInfo) {
if distribution == nil { if distribution == nil {
return partsMetadata return input
} }
shuffledPartsMetadata = make([]FileInfo, len(partsMetadata)) shuffled := make([]T, len(input))
// Shuffle slice xl metadata for expected distribution. for index := range input {
for index := range partsMetadata {
blockIndex := distribution[index] blockIndex := distribution[index]
shuffledPartsMetadata[blockIndex-1] = partsMetadata[index] shuffled[blockIndex-1] = input[index]
} }
return shuffledPartsMetadata return shuffled
}
// Return shuffled partsMetadata depending on distribution.
func shufflePartsMetadata(partsMetadata []FileInfo, distribution []int) []FileInfo {
return shuffleWithDist[FileInfo](partsMetadata, distribution)
}
// shuffleCheckParts - shuffle CheckParts slice depending on the
// erasure distribution.
func shuffleCheckParts(parts []int, distribution []int) []int {
return shuffleWithDist[int](parts, distribution)
} }
// shuffleDisks - shuffle input disks slice depending on the // shuffleDisks - shuffle input disks slice depending on the
// erasure distribution. Return shuffled slice of disks with // erasure distribution. Return shuffled slice of disks with
// their expected distribution. // their expected distribution.
func shuffleDisks(disks []StorageAPI, distribution []int) (shuffledDisks []StorageAPI) { func shuffleDisks(disks []StorageAPI, distribution []int) []StorageAPI {
if distribution == nil { return shuffleWithDist[StorageAPI](disks, distribution)
return disks
}
shuffledDisks = make([]StorageAPI, len(disks))
// Shuffle disks for expected distribution.
for index := range disks {
blockIndex := distribution[index]
shuffledDisks[blockIndex-1] = disks[index]
}
return shuffledDisks
} }
// evalDisks - returns a new slice of disks where nil is set if // evalDisks - returns a new slice of disks where nil is set if
+4 -2
View File
@@ -514,8 +514,10 @@ func listObjectParities(partsMetadata []FileInfo, errs []error) (parities []int)
if metadata.Deleted || metadata.Size == 0 { if metadata.Deleted || metadata.Size == 0 {
parities[index] = totalShards / 2 parities[index] = totalShards / 2
} else if metadata.TransitionStatus == lifecycle.TransitionComplete { } else if metadata.TransitionStatus == lifecycle.TransitionComplete {
// For tiered objects, read quorum is N/2+1 to ensure simple majority on xl.meta. It is not equal to EcM because the data integrity is entrusted with the warm tier. // For tiered objects, read quorum is N/2+1 to ensure simple majority on xl.meta.
parities[index] = totalShards - (totalShards/2 + 1) // It is not equal to EcM because the data integrity is entrusted with the warm tier.
// However, we never go below EcM, in case of a EcM=EcN setup.
parities[index] = max(totalShards-(totalShards/2+1), metadata.Erasure.ParityBlocks)
} else { } else {
parities[index] = metadata.Erasure.ParityBlocks parities[index] = metadata.Erasure.ParityBlocks
} }
+89 -94
View File
@@ -480,6 +480,7 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
if opts.WantChecksum != nil && opts.WantChecksum.Type.IsSet() { if opts.WantChecksum != nil && opts.WantChecksum.Type.IsSet() {
userDefined[hash.MinIOMultipartChecksum] = opts.WantChecksum.Type.String() userDefined[hash.MinIOMultipartChecksum] = opts.WantChecksum.Type.String()
userDefined[hash.MinIOMultipartChecksumType] = opts.WantChecksum.Type.ObjType()
} }
modTime := opts.MTime modTime := opts.MTime
@@ -508,6 +509,7 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
return &NewMultipartUploadResult{ return &NewMultipartUploadResult{
UploadID: uploadID, UploadID: uploadID,
ChecksumAlgo: userDefined[hash.MinIOMultipartChecksum], ChecksumAlgo: userDefined[hash.MinIOMultipartChecksum],
ChecksumType: userDefined[hash.MinIOMultipartChecksumType],
}, nil }, nil
} }
@@ -526,6 +528,14 @@ func (er erasureObjects) NewMultipartUpload(ctx context.Context, bucket, object
// renamePart - renames multipart part to its relevant location under uploadID. // renamePart - renames multipart part to its relevant location under uploadID.
func (er erasureObjects) renamePart(ctx context.Context, disks []StorageAPI, srcBucket, srcEntry, dstBucket, dstEntry string, optsMeta []byte, writeQuorum int) ([]StorageAPI, error) { func (er erasureObjects) renamePart(ctx context.Context, disks []StorageAPI, srcBucket, srcEntry, dstBucket, dstEntry string, optsMeta []byte, writeQuorum int) ([]StorageAPI, error) {
paths := []string{
dstEntry,
dstEntry + ".meta",
}
// cleanup existing paths first across all drives.
er.cleanupMultipartPath(ctx, paths...)
g := errgroup.WithNErrs(len(disks)) g := errgroup.WithNErrs(len(disks))
// Rename file on all underlying storage disks. // Rename file on all underlying storage disks.
@@ -542,11 +552,6 @@ func (er erasureObjects) renamePart(ctx context.Context, disks []StorageAPI, src
// Wait for all renames to finish. // Wait for all renames to finish.
errs := g.Wait() errs := g.Wait()
paths := []string{
dstEntry,
dstEntry + ".meta",
}
err := reduceWriteQuorumErrs(ctx, errs, objectOpIgnoredErrs, writeQuorum) err := reduceWriteQuorumErrs(ctx, errs, objectOpIgnoredErrs, writeQuorum)
if err != nil { if err != nil {
er.cleanupMultipartPath(ctx, paths...) er.cleanupMultipartPath(ctx, paths...)
@@ -574,19 +579,9 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
return pi, toObjectErr(errInvalidArgument) return pi, toObjectErr(errInvalidArgument)
} }
// Read lock for upload id.
// Only held while reading the upload metadata.
uploadIDRLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
rlkctx, err := uploadIDRLock.GetRLock(ctx, globalOperationTimeout)
if err != nil {
return PartInfo{}, err
}
rctx := rlkctx.Context()
defer uploadIDRLock.RUnlock(rlkctx)
uploadIDPath := er.getUploadIDDir(bucket, object, uploadID) uploadIDPath := er.getUploadIDDir(bucket, object, uploadID)
// Validates if upload ID exists. // Validates if upload ID exists.
fi, _, err := er.checkUploadIDExists(rctx, bucket, object, uploadID, true) fi, _, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, true)
if err != nil { if err != nil {
if errors.Is(err, errVolumeNotFound) { if errors.Is(err, errVolumeNotFound) {
return pi, toObjectErr(err, bucket) return pi, toObjectErr(err, bucket)
@@ -633,17 +628,13 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
switch size := data.Size(); { switch size := data.Size(); {
case size == 0: case size == 0:
buffer = make([]byte, 1) // Allocate at least a byte to reach EOF buffer = make([]byte, 1) // Allocate at least a byte to reach EOF
case size == -1: case size >= fi.Erasure.BlockSize || size == -1:
if size := data.ActualSize(); size > 0 && size < fi.Erasure.BlockSize { if int64(globalBytePoolCap.Load().Width()) < fi.Erasure.BlockSize {
// Account for padding and forced compression overhead and encryption. buffer = make([]byte, fi.Erasure.BlockSize, 2*fi.Erasure.BlockSize)
buffer = make([]byte, data.ActualSize()+256+32+32, data.ActualSize()*2+512)
} else { } else {
buffer = globalBytePoolCap.Load().Get() buffer = globalBytePoolCap.Load().Get()
defer globalBytePoolCap.Load().Put(buffer) defer globalBytePoolCap.Load().Put(buffer)
} }
case size >= fi.Erasure.BlockSize:
buffer = globalBytePoolCap.Load().Get()
defer globalBytePoolCap.Load().Put(buffer)
case size < fi.Erasure.BlockSize: case size < fi.Erasure.BlockSize:
// No need to allocate fully fi.Erasure.BlockSize buffer if the incoming data is smaller. // No need to allocate fully fi.Erasure.BlockSize buffer if the incoming data is smaller.
buffer = make([]byte, size, 2*size+int64(fi.Erasure.ParityBlocks+fi.Erasure.DataBlocks-1)) buffer = make([]byte, size, 2*size+int64(fi.Erasure.ParityBlocks+fi.Erasure.DataBlocks-1))
@@ -677,10 +668,13 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
} }
n, err := erasure.Encode(ctx, toEncode, writers, buffer, writeQuorum) n, err := erasure.Encode(ctx, toEncode, writers, buffer, writeQuorum)
closeBitrotWriters(writers) closeErrs := closeBitrotWriters(writers)
if err != nil { if err != nil {
return pi, toObjectErr(err, bucket, object) return pi, toObjectErr(err, bucket, object)
} }
if closeErr := reduceWriteQuorumErrs(ctx, closeErrs, objectOpIgnoredErrs, writeQuorum); closeErr != nil {
return pi, toObjectErr(closeErr, bucket, object)
}
// Should return IncompleteBody{} error when reader has fewer bytes // Should return IncompleteBody{} error when reader has fewer bytes
// than specified in request header. // than specified in request header.
@@ -741,10 +735,7 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
return pi, toObjectErr(err, minioMetaMultipartBucket, partPath) return pi, toObjectErr(err, minioMetaMultipartBucket, partPath)
} }
// Write lock for this part ID, only hold it if we are planning to read from the // Serialize concurrent part uploads.
// stream avoid any concurrent updates.
//
// Must be held throughout this call.
partIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID, strconv.Itoa(partID))) partIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID, strconv.Itoa(partID)))
plkctx, err := partIDLock.GetLock(ctx, globalOperationTimeout) plkctx, err := partIDLock.GetLock(ctx, globalOperationTimeout)
if err != nil { if err != nil {
@@ -754,6 +745,15 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
ctx = plkctx.Context() ctx = plkctx.Context()
defer partIDLock.Unlock(plkctx) defer partIDLock.Unlock(plkctx)
// Read lock for upload id, only held while reading the upload metadata.
uploadIDRLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
rlkctx, err := uploadIDRLock.GetRLock(ctx, globalOperationTimeout)
if err != nil {
return PartInfo{}, err
}
ctx = rlkctx.Context()
defer uploadIDRLock.RUnlock(rlkctx)
onlineDisks, err = er.renamePart(ctx, onlineDisks, minioMetaTmpBucket, tmpPartPath, minioMetaMultipartBucket, partPath, partFI, writeQuorum) onlineDisks, err = er.renamePart(ctx, onlineDisks, minioMetaTmpBucket, tmpPartPath, minioMetaMultipartBucket, partPath, partFI, writeQuorum)
if err != nil { if err != nil {
if errors.Is(err, errFileNotFound) { if errors.Is(err, errFileNotFound) {
@@ -770,15 +770,16 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
// Return success. // Return success.
return PartInfo{ return PartInfo{
PartNumber: partInfo.Number, PartNumber: partInfo.Number,
ETag: partInfo.ETag, ETag: partInfo.ETag,
LastModified: partInfo.ModTime, LastModified: partInfo.ModTime,
Size: partInfo.Size, Size: partInfo.Size,
ActualSize: partInfo.ActualSize, ActualSize: partInfo.ActualSize,
ChecksumCRC32: partInfo.Checksums["CRC32"], ChecksumCRC32: partInfo.Checksums["CRC32"],
ChecksumCRC32C: partInfo.Checksums["CRC32C"], ChecksumCRC32C: partInfo.Checksums["CRC32C"],
ChecksumSHA1: partInfo.Checksums["SHA1"], ChecksumSHA1: partInfo.Checksums["SHA1"],
ChecksumSHA256: partInfo.Checksums["SHA256"], ChecksumSHA256: partInfo.Checksums["SHA256"],
ChecksumCRC64NVME: partInfo.Checksums["CRC64NVME"],
}, nil }, nil
} }
@@ -798,14 +799,6 @@ func (er erasureObjects) GetMultipartInfo(ctx context.Context, bucket, object, u
UploadID: uploadID, UploadID: uploadID,
} }
uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
lkctx, err := uploadIDLock.GetRLock(ctx, globalOperationTimeout)
if err != nil {
return MultipartInfo{}, err
}
ctx = lkctx.Context()
defer uploadIDLock.RUnlock(lkctx)
fi, _, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, false) fi, _, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, false)
if err != nil { if err != nil {
if errors.Is(err, errVolumeNotFound) { if errors.Is(err, errVolumeNotFound) {
@@ -885,14 +878,6 @@ func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, up
auditObjectErasureSet(ctx, "ListObjectParts", object, &er) auditObjectErasureSet(ctx, "ListObjectParts", object, &er)
} }
uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
lkctx, err := uploadIDLock.GetRLock(ctx, globalOperationTimeout)
if err != nil {
return ListPartsInfo{}, err
}
ctx = lkctx.Context()
defer uploadIDLock.RUnlock(lkctx)
fi, _, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, false) fi, _, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, false)
if err != nil { if err != nil {
return result, toObjectErr(err, bucket, object, uploadID) return result, toObjectErr(err, bucket, object, uploadID)
@@ -916,6 +901,7 @@ func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, up
result.PartNumberMarker = partNumberMarker result.PartNumberMarker = partNumberMarker
result.UserDefined = cloneMSS(fi.Metadata) result.UserDefined = cloneMSS(fi.Metadata)
result.ChecksumAlgorithm = fi.Metadata[hash.MinIOMultipartChecksum] result.ChecksumAlgorithm = fi.Metadata[hash.MinIOMultipartChecksum]
result.ChecksumType = fi.Metadata[hash.MinIOMultipartChecksumType]
if maxParts == 0 { if maxParts == 0 {
return result, nil return result, nil
@@ -962,15 +948,16 @@ func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, up
count := maxParts count := maxParts
for _, objPart := range objParts { for _, objPart := range objParts {
result.Parts = append(result.Parts, PartInfo{ result.Parts = append(result.Parts, PartInfo{
PartNumber: objPart.Number, PartNumber: objPart.Number,
LastModified: objPart.ModTime, LastModified: objPart.ModTime,
ETag: objPart.ETag, ETag: objPart.ETag,
Size: objPart.Size, Size: objPart.Size,
ActualSize: objPart.ActualSize, ActualSize: objPart.ActualSize,
ChecksumCRC32: objPart.Checksums["CRC32"], ChecksumCRC32: objPart.Checksums["CRC32"],
ChecksumCRC32C: objPart.Checksums["CRC32C"], ChecksumCRC32C: objPart.Checksums["CRC32C"],
ChecksumSHA1: objPart.Checksums["SHA1"], ChecksumSHA1: objPart.Checksums["SHA1"],
ChecksumSHA256: objPart.Checksums["SHA256"], ChecksumSHA256: objPart.Checksums["SHA256"],
ChecksumCRC64NVME: objPart.Checksums["CRC64NVME"],
}) })
count-- count--
if count == 0 { if count == 0 {
@@ -1065,7 +1052,6 @@ func readParts(ctx context.Context, disks []StorageAPI, bucket string, partMetaP
PartNumber: partNumbers[pidx], PartNumber: partNumbers[pidx],
}.Error(), }.Error(),
} }
} }
return partInfosInQuorum, nil return partInfosInQuorum, nil
} }
@@ -1115,16 +1101,6 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
} }
} }
// Hold write locks to verify uploaded parts, also disallows any
// parallel PutObjectPart() requests.
uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
wlkctx, err := uploadIDLock.GetLock(ctx, globalOperationTimeout)
if err != nil {
return oi, err
}
ctx = wlkctx.Context()
defer uploadIDLock.Unlock(wlkctx)
fi, partsMetadata, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, true) fi, partsMetadata, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, true)
if err != nil { if err != nil {
if errors.Is(err, errVolumeNotFound) { if errors.Is(err, errVolumeNotFound) {
@@ -1162,12 +1138,12 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
// Checksum type set when upload started. // Checksum type set when upload started.
var checksumType hash.ChecksumType var checksumType hash.ChecksumType
if cs := fi.Metadata[hash.MinIOMultipartChecksum]; cs != "" { if cs := fi.Metadata[hash.MinIOMultipartChecksum]; cs != "" {
checksumType = hash.NewChecksumType(cs) checksumType = hash.NewChecksumType(cs, fi.Metadata[hash.MinIOMultipartChecksumType])
if opts.WantChecksum != nil && !opts.WantChecksum.Type.Is(checksumType) { if opts.WantChecksum != nil && !opts.WantChecksum.Type.Is(checksumType) {
return oi, InvalidArgument{ return oi, InvalidArgument{
Bucket: bucket, Bucket: bucket,
Object: fi.Name, Object: fi.Name,
Err: fmt.Errorf("checksum type mismatch"), Err: fmt.Errorf("checksum type mismatch. got %q (%s) expected %q (%s)", checksumType.String(), checksumType.ObjType(), opts.WantChecksum.Type.String(), opts.WantChecksum.Type.ObjType()),
} }
} }
} }
@@ -1247,6 +1223,9 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
// Allocate parts similar to incoming slice. // Allocate parts similar to incoming slice.
fi.Parts = make([]ObjectPartInfo, len(parts)) fi.Parts = make([]ObjectPartInfo, len(parts))
var checksum hash.Checksum
checksum.Type = checksumType
// Validate each part and then commit to disk. // Validate each part and then commit to disk.
for i, part := range parts { for i, part := range parts {
partIdx := objectPartIndex(currentFI.Parts, part.PartNumber) partIdx := objectPartIndex(currentFI.Parts, part.PartNumber)
@@ -1280,10 +1259,11 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
} }
} }
wantCS := map[string]string{ wantCS := map[string]string{
hash.ChecksumCRC32.String(): part.ChecksumCRC32, hash.ChecksumCRC32.String(): part.ChecksumCRC32,
hash.ChecksumCRC32C.String(): part.ChecksumCRC32C, hash.ChecksumCRC32C.String(): part.ChecksumCRC32C,
hash.ChecksumSHA1.String(): part.ChecksumSHA1, hash.ChecksumSHA1.String(): part.ChecksumSHA1,
hash.ChecksumSHA256.String(): part.ChecksumSHA256, hash.ChecksumSHA256.String(): part.ChecksumSHA256,
hash.ChecksumCRC64NVME.String(): part.ChecksumCRC64NVME,
} }
if wantCS[checksumType.String()] != crc { if wantCS[checksumType.String()] != crc {
return oi, InvalidPart{ return oi, InvalidPart{
@@ -1298,6 +1278,15 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
PartNumber: part.PartNumber, PartNumber: part.PartNumber,
} }
} }
if checksumType.FullObjectRequested() {
if err := checksum.AddPart(*cs, expPart.ActualSize); err != nil {
return oi, InvalidPart{
PartNumber: part.PartNumber,
ExpETag: "<nil>",
GotETag: err.Error(),
}
}
}
checksumCombined = append(checksumCombined, cs.Raw...) checksumCombined = append(checksumCombined, cs.Raw...)
} }
@@ -1328,9 +1317,19 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
} }
if opts.WantChecksum != nil { if opts.WantChecksum != nil {
err := opts.WantChecksum.Matches(checksumCombined, len(parts)) if checksumType.FullObjectRequested() {
if err != nil { if opts.WantChecksum.Encoded != checksum.Encoded {
return oi, err err := hash.ChecksumMismatch{
Want: opts.WantChecksum.Encoded,
Got: checksum.Encoded,
}
return oi, err
}
} else {
err := opts.WantChecksum.Matches(checksumCombined, len(parts))
if err != nil {
return oi, err
}
} }
} }
@@ -1344,14 +1343,18 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
if checksumType.IsSet() { if checksumType.IsSet() {
checksumType |= hash.ChecksumMultipart | hash.ChecksumIncludesMultipart checksumType |= hash.ChecksumMultipart | hash.ChecksumIncludesMultipart
var cs *hash.Checksum checksum.Type = checksumType
cs = hash.NewChecksumFromData(checksumType, checksumCombined) if !checksumType.FullObjectRequested() {
fi.Checksum = cs.AppendTo(nil, checksumCombined) checksum = *hash.NewChecksumFromData(checksumType, checksumCombined)
}
fi.Checksum = checksum.AppendTo(nil, checksumCombined)
if opts.EncryptFn != nil { if opts.EncryptFn != nil {
fi.Checksum = opts.EncryptFn("object-checksum", fi.Checksum) fi.Checksum = opts.EncryptFn("object-checksum", fi.Checksum)
} }
} }
delete(fi.Metadata, hash.MinIOMultipartChecksum) // Not needed in final object. // Remove superfluous internal headers.
delete(fi.Metadata, hash.MinIOMultipartChecksum)
delete(fi.Metadata, hash.MinIOMultipartChecksumType)
// Save the final object size and modtime. // Save the final object size and modtime.
fi.Size = objectSize fi.Size = objectSize
@@ -1491,14 +1494,6 @@ func (er erasureObjects) AbortMultipartUpload(ctx context.Context, bucket, objec
auditObjectErasureSet(ctx, "AbortMultipartUpload", object, &er) auditObjectErasureSet(ctx, "AbortMultipartUpload", object, &er)
} }
lk := er.NewNSLock(bucket, pathJoin(object, uploadID))
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
if err != nil {
return err
}
ctx = lkctx.Context()
defer lk.Unlock(lkctx)
// Validates if upload ID exists. // Validates if upload ID exists.
if _, _, err = er.checkUploadIDExists(ctx, bucket, object, uploadID, false); err != nil { if _, _, err = er.checkUploadIDExists(ctx, bucket, object, uploadID, false); err != nil {
if errors.Is(err, errVolumeNotFound) { if errors.Is(err, errVolumeNotFound) {
+77 -39
View File
@@ -27,6 +27,8 @@ import (
"net/http" "net/http"
"path" "path"
"runtime" "runtime"
"slices"
"sort"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -102,7 +104,7 @@ func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, d
readQuorum, writeQuorum, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount) readQuorum, writeQuorum, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
if err != nil { if err != nil {
if errors.Is(err, errErasureReadQuorum) && !strings.HasPrefix(srcBucket, minioMetaBucket) { if shouldCheckForDangling(err, errs, srcBucket) {
_, derr := er.deleteIfDangling(context.Background(), srcBucket, srcObject, metaArr, errs, nil, srcOpts) _, derr := er.deleteIfDangling(context.Background(), srcBucket, srcObject, metaArr, errs, nil, srcOpts)
if derr == nil { if derr == nil {
if srcOpts.VersionID != "" { if srcOpts.VersionID != "" {
@@ -690,13 +692,9 @@ func shouldCheckForDangling(err error, errs []error, bucket string) bool {
// Check if we have a read quorum issue // Check if we have a read quorum issue
case errors.Is(err, errErasureReadQuorum): case errors.Is(err, errErasureReadQuorum):
return true return true
// Check if the object is inexistent in most disks but not all of them // Check if the object is non-existent on most disks but not all of them
case errors.Is(err, errFileNotFound) || errors.Is(err, errFileVersionNotFound): case (errors.Is(err, errFileNotFound) || errors.Is(err, errFileVersionNotFound)) && (countErrs(errs, nil) > 0):
for i := range errs { return true
if errs[i] == nil {
return true
}
}
} }
return false return false
} }
@@ -815,8 +813,6 @@ func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object s
PoolIndex: er.poolIndex, PoolIndex: er.poolIndex,
}) })
} }
return
}() }()
validResp := 0 validResp := 0
@@ -921,6 +917,16 @@ func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object s
} }
} }
} }
// when we have insufficient read quorum and inconsistent metadata return
// file not found, since we can't possibly have a way to recover this object
// anyway.
if v, ok := err.(InsufficientReadQuorum); ok && v.Type == RQInconsistentMeta {
if opts.VersionID != "" {
err = errFileVersionNotFound
} else {
err = errFileNotFound
}
}
return fi, nil, nil, toObjectErr(err, bucket, object) return fi, nil, nil, toObjectErr(err, bucket, object)
} }
@@ -1170,11 +1176,15 @@ func (er erasureObjects) putMetacacheObject(ctx context.Context, key string, r *
} }
n, erasureErr := erasure.Encode(ctx, data, writers, buffer, writeQuorum) n, erasureErr := erasure.Encode(ctx, data, writers, buffer, writeQuorum)
closeBitrotWriters(writers) closeErrs := closeBitrotWriters(writers)
if erasureErr != nil { if erasureErr != nil {
return ObjectInfo{}, toObjectErr(erasureErr, minioMetaBucket, key) return ObjectInfo{}, toObjectErr(erasureErr, minioMetaBucket, key)
} }
if closeErr := reduceWriteQuorumErrs(ctx, closeErrs, objectOpIgnoredErrs, writeQuorum); closeErr != nil {
return ObjectInfo{}, toObjectErr(closeErr, minioMetaBucket, key)
}
// Should return IncompleteBody{} error when reader has fewer bytes // Should return IncompleteBody{} error when reader has fewer bytes
// than specified in request header. // than specified in request header.
if n < data.Size() { if n < data.Size() {
@@ -1417,11 +1427,15 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
bugLogIf(ctx, err) bugLogIf(ctx, err)
} }
n, erasureErr := erasure.Encode(ctx, toEncode, writers, buffer, writeQuorum) n, erasureErr := erasure.Encode(ctx, toEncode, writers, buffer, writeQuorum)
closeBitrotWriters(writers) closeErrs := closeBitrotWriters(writers)
if erasureErr != nil { if erasureErr != nil {
return ObjectInfo{}, toObjectErr(erasureErr, bucket, object) return ObjectInfo{}, toObjectErr(erasureErr, bucket, object)
} }
if closeErr := reduceWriteQuorumErrs(ctx, closeErrs, objectOpIgnoredErrs, writeQuorum); closeErr != nil {
return ObjectInfo{}, toObjectErr(closeErr, bucket, object)
}
// Should return IncompleteBody{} error when reader has fewer bytes // Should return IncompleteBody{} error when reader has fewer bytes
// than specified in request header. // than specified in request header.
if n < data.Size() { if n < data.Size() {
@@ -1618,7 +1632,7 @@ func (er erasureObjects) deleteObjectVersion(ctx context.Context, bucket, object
func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objects []ObjectToDelete, opts ObjectOptions) ([]DeletedObject, []error) { func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objects []ObjectToDelete, opts ObjectOptions) ([]DeletedObject, []error) {
if !opts.NoAuditLog { if !opts.NoAuditLog {
for _, obj := range objects { for _, obj := range objects {
auditObjectErasureSet(ctx, "DeleteObjects", obj.ObjectV.ObjectName, &er) auditObjectErasureSet(ctx, "DeleteObjects", obj.ObjectName, &er)
} }
} }
@@ -1700,8 +1714,21 @@ func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objec
} }
dedupVersions := make([]FileInfoVersions, 0, len(versionsMap)) dedupVersions := make([]FileInfoVersions, 0, len(versionsMap))
for _, version := range versionsMap { for _, fivs := range versionsMap {
dedupVersions = append(dedupVersions, version) // Removal of existing versions and adding a delete marker in the same
// request is supported. At the same time, we cannot allow adding
// two delete markers on top of any object. To avoid this situation,
// we will sort deletions to execute existing deletion first,
// then add only one delete marker if requested
sort.SliceStable(fivs.Versions, func(i, j int) bool {
return !fivs.Versions[i].Deleted
})
if idx := slices.IndexFunc(fivs.Versions, func(fi FileInfo) bool {
return fi.Deleted
}); idx > -1 {
fivs.Versions = fivs.Versions[:idx+1]
}
dedupVersions = append(dedupVersions, fivs)
} }
// Initialize list of errors. // Initialize list of errors.
@@ -1726,12 +1753,6 @@ func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objec
continue continue
} }
for _, v := range dedupVersions[i].Versions { for _, v := range dedupVersions[i].Versions {
if err == errFileNotFound || err == errFileVersionNotFound {
if !dobjects[v.Idx].DeleteMarker {
// Not delete marker, if not found, ok.
continue
}
}
delObjErrs[index][v.Idx] = err delObjErrs[index][v.Idx] = err
} }
} }
@@ -1751,6 +1772,13 @@ func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objec
} }
} }
err := reduceWriteQuorumErrs(ctx, diskErrs, objectOpIgnoredErrs, writeQuorums[objIndex]) err := reduceWriteQuorumErrs(ctx, diskErrs, objectOpIgnoredErrs, writeQuorums[objIndex])
if err == nil {
dobjects[objIndex].found = true
} else if isErrVersionNotFound(err) || isErrObjectNotFound(err) {
if !dobjects[objIndex].DeleteMarker {
err = nil
}
}
if objects[objIndex].VersionID != "" { if objects[objIndex].VersionID != "" {
errs[objIndex] = toObjectErr(err, bucket, objects[objIndex].ObjectName, objects[objIndex].VersionID) errs[objIndex] = toObjectErr(err, bucket, objects[objIndex].ObjectName, objects[objIndex].VersionID)
} else { } else {
@@ -2004,7 +2032,7 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
if opts.VersionPurgeStatus().Empty() && opts.DeleteMarkerReplicationStatus().Empty() { if opts.VersionPurgeStatus().Empty() && opts.DeleteMarkerReplicationStatus().Empty() {
markDelete = false markDelete = false
} }
if opts.VersionPurgeStatus() == Complete { if opts.VersionPurgeStatus() == replication.VersionPurgeComplete {
markDelete = false markDelete = false
} }
// now, since VersionPurgeStatus() is already set, we can let the // now, since VersionPurgeStatus() is already set, we can let the
@@ -2138,7 +2166,7 @@ func (er erasureObjects) PutObjectMetadata(ctx context.Context, bucket, object s
readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount) readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
if err != nil { if err != nil {
if errors.Is(err, errErasureReadQuorum) && !strings.HasPrefix(bucket, minioMetaBucket) { if shouldCheckForDangling(err, errs, bucket) {
_, derr := er.deleteIfDangling(context.Background(), bucket, object, metaArr, errs, nil, opts) _, derr := er.deleteIfDangling(context.Background(), bucket, object, metaArr, errs, nil, opts)
if derr == nil { if derr == nil {
if opts.VersionID != "" { if opts.VersionID != "" {
@@ -2192,14 +2220,16 @@ func (er erasureObjects) PutObjectMetadata(ctx context.Context, bucket, object s
// PutObjectTags - replace or add tags to an existing object // PutObjectTags - replace or add tags to an existing object
func (er erasureObjects) PutObjectTags(ctx context.Context, bucket, object string, tags string, opts ObjectOptions) (ObjectInfo, error) { func (er erasureObjects) PutObjectTags(ctx context.Context, bucket, object string, tags string, opts ObjectOptions) (ObjectInfo, error) {
// Lock the object before updating tags. if !opts.NoLock {
lk := er.NewNSLock(bucket, object) // Lock the object before updating tags.
lkctx, err := lk.GetLock(ctx, globalOperationTimeout) lk := er.NewNSLock(bucket, object)
if err != nil { lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
return ObjectInfo{}, err if err != nil {
return ObjectInfo{}, err
}
ctx = lkctx.Context()
defer lk.Unlock(lkctx)
} }
ctx = lkctx.Context()
defer lk.Unlock(lkctx)
disks := er.getDisks() disks := er.getDisks()
@@ -2215,7 +2245,7 @@ func (er erasureObjects) PutObjectTags(ctx context.Context, bucket, object strin
readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount) readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
if err != nil { if err != nil {
if errors.Is(err, errErasureReadQuorum) && !strings.HasPrefix(bucket, minioMetaBucket) { if shouldCheckForDangling(err, errs, bucket) {
_, derr := er.deleteIfDangling(context.Background(), bucket, object, metaArr, errs, nil, opts) _, derr := er.deleteIfDangling(context.Background(), bucket, object, metaArr, errs, nil, opts)
if derr == nil { if derr == nil {
if opts.VersionID != "" { if opts.VersionID != "" {
@@ -2310,14 +2340,16 @@ func (er erasureObjects) TransitionObject(ctx context.Context, bucket, object st
return err return err
} }
// Acquire write lock before starting to transition the object. if !opts.NoLock {
lk := er.NewNSLock(bucket, object) // Acquire write lock before starting to transition the object.
lkctx, err := lk.GetLock(ctx, globalDeleteOperationTimeout) lk := er.NewNSLock(bucket, object)
if err != nil { lkctx, err := lk.GetLock(ctx, globalDeleteOperationTimeout)
return err if err != nil {
return err
}
ctx = lkctx.Context()
defer lk.Unlock(lkctx)
} }
ctx = lkctx.Context()
defer lk.Unlock(lkctx)
fi, metaArr, onlineDisks, err := er.getObjectFileInfo(ctx, bucket, object, opts, true) fi, metaArr, onlineDisks, err := er.getObjectFileInfo(ctx, bucket, object, opts, true)
if err != nil { if err != nil {
@@ -2355,6 +2387,7 @@ func (er erasureObjects) TransitionObject(ctx context.Context, bucket, object st
destObj, err := genTransitionObjName(bucket) destObj, err := genTransitionObjName(bucket)
if err != nil { if err != nil {
traceFn(ILMTransition, nil, err)
return err return err
} }
@@ -2365,9 +2398,14 @@ func (er erasureObjects) TransitionObject(ctx context.Context, bucket, object st
}() }()
var rv remoteVersionID var rv remoteVersionID
rv, err = tgtClient.Put(ctx, destObj, pr, fi.Size) rv, err = tgtClient.PutWithMeta(ctx, destObj, pr, fi.Size, map[string]string{
"name": object, // preserve the original name of the object on the remote tier object metadata.
// this is just for future reverse lookup() purposes (applies only for new objects)
// does not apply retro-actively on already transitioned objects.
})
pr.CloseWithError(err) pr.CloseWithError(err)
if err != nil { if err != nil {
traceFn(ILMTransition, nil, err)
return err return err
} }
fi.TransitionStatus = lifecycle.TransitionComplete fi.TransitionStatus = lifecycle.TransitionComplete
+70 -2
View File
@@ -131,6 +131,75 @@ func TestErasureDeleteObjectBasic(t *testing.T) {
removeRoots(fsDirs) removeRoots(fsDirs)
} }
func TestDeleteObjectsVersionedTwoPools(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
obj, fsDirs, err := prepareErasurePools()
if err != nil {
t.Fatal("Unable to initialize 'Erasure' object layer.", err)
}
// Remove all dirs.
for _, dir := range fsDirs {
defer os.RemoveAll(dir)
}
bucketName := "bucket"
objectName := "myobject"
err = obj.MakeBucket(ctx, bucketName, MakeBucketOptions{
VersioningEnabled: true,
})
if err != nil {
t.Fatal(err)
}
z, ok := obj.(*erasureServerPools)
if !ok {
t.Fatal("unexpected object layer type")
}
versions := make([]string, 2)
for i := range z.serverPools {
objInfo, err := z.serverPools[i].PutObject(ctx, bucketName, objectName,
mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), ObjectOptions{
Versioned: true,
})
if err != nil {
t.Fatalf("Erasure Object upload failed: <ERROR> %s", err)
}
versions[i] = objInfo.VersionID
}
// Remove and check the version in the second pool, then
// remove and check the version in the first pool
for testIdx, vid := range []string{versions[1], versions[0]} {
names := []ObjectToDelete{
{
ObjectV: ObjectV{
ObjectName: objectName,
VersionID: vid,
},
},
}
_, delErrs := obj.DeleteObjects(ctx, bucketName, names, ObjectOptions{
Versioned: true,
})
for i := range delErrs {
if delErrs[i] != nil {
t.Errorf("Test %d: Failed to remove object `%v` with the error: `%v`", testIdx, names[i], delErrs[i])
}
_, statErr := obj.GetObjectInfo(ctx, bucketName, objectName, ObjectOptions{
VersionID: names[i].VersionID,
})
switch statErr.(type) {
case VersionNotFound:
default:
t.Errorf("Test %d: Object %s is not removed", testIdx, objectName)
}
}
}
}
func TestDeleteObjectsVersioned(t *testing.T) { func TestDeleteObjectsVersioned(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
@@ -177,7 +246,6 @@ func TestDeleteObjectsVersioned(t *testing.T) {
VersionID: objInfo.VersionID, VersionID: objInfo.VersionID,
}, },
} }
} }
names = append(names, ObjectToDelete{ names = append(names, ObjectToDelete{
ObjectV: ObjectV{ ObjectV: ObjectV{
@@ -197,7 +265,7 @@ func TestDeleteObjectsVersioned(t *testing.T) {
for i, test := range testCases { for i, test := range testCases {
_, statErr := obj.GetObjectInfo(ctx, test.bucket, test.object, ObjectOptions{ _, statErr := obj.GetObjectInfo(ctx, test.bucket, test.object, ObjectOptions{
VersionID: names[i].ObjectV.VersionID, VersionID: names[i].VersionID,
}) })
switch statErr.(type) { switch statErr.(type) {
case VersionNotFound: case VersionNotFound:
+3 -7
View File
@@ -845,7 +845,7 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
// to decommission, just skip it, this also includes // to decommission, just skip it, this also includes
// any other versions that have already expired. // any other versions that have already expired.
remainingVersions := len(fivs.Versions) - expired remainingVersions := len(fivs.Versions) - expired
if version.Deleted && remainingVersions == 1 { if version.Deleted && remainingVersions == 1 && rcfg == nil {
decommissioned++ decommissioned++
stopFn(version.Size, errors.New("DELETE marked object with no other non-current versions will be skipped")) stopFn(version.Size, errors.New("DELETE marked object with no other non-current versions will be skipped"))
continue continue
@@ -1014,18 +1014,14 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
defer wk.Give() defer wk.Give()
// We will perpetually retry listing if it fails, since we cannot // We will perpetually retry listing if it fails, since we cannot
// possibly give up in this matter // possibly give up in this matter
for { for !contextCanceled(ctx) {
if contextCanceled(ctx) {
break
}
err := set.listObjectsToDecommission(ctx, bi, err := set.listObjectsToDecommission(ctx, bi,
func(entry metaCacheEntry) { func(entry metaCacheEntry) {
wk.Take() wk.Take()
go decommissionEntry(entry) go decommissionEntry(entry)
}, },
) )
if err == nil || errors.Is(err, context.Canceled) { if err == nil || errors.Is(err, context.Canceled) || errors.Is(err, errVolumeNotFound) {
break break
} }
setN := humanize.Ordinal(setIdx + 1) setN := humanize.Ordinal(setIdx + 1)
+2 -2
View File
@@ -32,9 +32,9 @@ func prepareErasurePools() (ObjectLayer, []string, error) {
pools := mustGetPoolEndpoints(0, fsDirs[:16]...) pools := mustGetPoolEndpoints(0, fsDirs[:16]...)
pools = append(pools, mustGetPoolEndpoints(1, fsDirs[16:]...)...) pools = append(pools, mustGetPoolEndpoints(1, fsDirs[16:]...)...)
// Everything is fine, should return nil objLayer, _, err := initObjectLayer(context.Background(), pools)
objLayer, err := newErasureServerPools(context.Background(), pools)
if err != nil { if err != nil {
removeRoots(fsDirs)
return nil, nil, err return nil, nil, err
} }
return objLayer, fsDirs, nil return objLayer, fsDirs, nil
+140 -82
View File
@@ -256,7 +256,7 @@ func (z *erasureServerPools) NewNSLock(bucket string, objects ...string) RWLocke
// GetDisksID will return disks by their ID. // GetDisksID will return disks by their ID.
func (z *erasureServerPools) GetDisksID(ids ...string) []StorageAPI { func (z *erasureServerPools) GetDisksID(ids ...string) []StorageAPI {
idMap := make(map[string]struct{}) idMap := make(map[string]struct{}, len(ids))
for _, id := range ids { for _, id := range ids {
idMap[id] = struct{}{} idMap[id] = struct{}{}
} }
@@ -357,7 +357,7 @@ func (p serverPoolsAvailableSpace) TotalAvailable() uint64 {
// FilterMaxUsed will filter out any pools that has used percent bigger than max, // FilterMaxUsed will filter out any pools that has used percent bigger than max,
// unless all have that, in which case all are preserved. // unless all have that, in which case all are preserved.
func (p serverPoolsAvailableSpace) FilterMaxUsed(max int) { func (p serverPoolsAvailableSpace) FilterMaxUsed(maxUsed int) {
// We aren't modifying p, only entries in it, so we don't need to receive a pointer. // We aren't modifying p, only entries in it, so we don't need to receive a pointer.
if len(p) <= 1 { if len(p) <= 1 {
// Nothing to do. // Nothing to do.
@@ -365,7 +365,7 @@ func (p serverPoolsAvailableSpace) FilterMaxUsed(max int) {
} }
var ok bool var ok bool
for _, z := range p { for _, z := range p {
if z.Available > 0 && z.MaxUsedPct < max { if z.Available > 0 && z.MaxUsedPct < maxUsed {
ok = true ok = true
break break
} }
@@ -378,7 +378,7 @@ func (p serverPoolsAvailableSpace) FilterMaxUsed(max int) {
// Remove entries that are above. // Remove entries that are above.
for i, z := range p { for i, z := range p {
if z.Available > 0 && z.MaxUsedPct < max { if z.Available > 0 && z.MaxUsedPct < maxUsed {
continue continue
} }
p[i].Available = 0 p[i].Available = 0
@@ -1248,89 +1248,42 @@ func (z *erasureServerPools) DeleteObjects(ctx context.Context, bucket string, o
ctx = lkctx.Context() ctx = lkctx.Context()
defer multiDeleteLock.Unlock(lkctx) defer multiDeleteLock.Unlock(lkctx)
// Fetch location of up to 10 objects concurrently. dObjectsByPool := make([][]DeletedObject, len(z.serverPools))
poolObjIdxMap := map[int][]ObjectToDelete{} dErrsByPool := make([][]error, len(z.serverPools))
origIndexMap := map[int][]int{}
// Always perform 1/10th of the number of objects per delete eg := errgroup.WithNErrs(len(z.serverPools)).WithConcurrency(len(z.serverPools))
concurrent := len(objects) / 10 for i, pool := range z.serverPools {
if concurrent <= 10 { i := i
// if we cannot get 1/10th then choose the number of pool := pool
// objects as concurrent.
concurrent = len(objects)
}
var mu sync.Mutex
eg := errgroup.WithNErrs(len(objects)).WithConcurrency(concurrent)
for j, obj := range objects {
j := j
obj := obj
eg.Go(func() error { eg.Go(func() error {
pinfo, _, err := z.getPoolInfoExistingWithOpts(ctx, bucket, obj.ObjectName, ObjectOptions{ dObjectsByPool[i], dErrsByPool[i] = pool.DeleteObjects(ctx, bucket, objects, opts)
NoLock: true,
})
if err != nil {
if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
derrs[j] = err
}
dobjects[j] = DeletedObject{
ObjectName: decodeDirObject(obj.ObjectName),
VersionID: obj.VersionID,
}
return nil
}
// Delete marker already present we are not going to create new delete markers.
if pinfo.ObjInfo.DeleteMarker && obj.VersionID == "" {
dobjects[j] = DeletedObject{
DeleteMarker: pinfo.ObjInfo.DeleteMarker,
DeleteMarkerVersionID: pinfo.ObjInfo.VersionID,
DeleteMarkerMTime: DeleteMarkerMTime{pinfo.ObjInfo.ModTime},
ObjectName: decodeDirObject(pinfo.ObjInfo.Name),
}
return nil
}
idx := pinfo.Index
mu.Lock()
defer mu.Unlock()
poolObjIdxMap[idx] = append(poolObjIdxMap[idx], obj)
origIndexMap[idx] = append(origIndexMap[idx], j)
return nil return nil
}, j) }, i)
} }
eg.Wait() // wait to check all the pools. eg.Wait() // wait to check all the pools.
if len(poolObjIdxMap) > 0 { for i := range dobjects {
// Delete concurrently in all server pools. // Iterate over pools
var wg sync.WaitGroup for pool := range z.serverPools {
wg.Add(len(z.serverPools)) if dErrsByPool[pool][i] == nil && dObjectsByPool[pool][i].found {
for idx, pool := range z.serverPools { // A fast exit when the object is found and removed
go func(idx int, pool *erasureSets) { dobjects[i] = dObjectsByPool[pool][i]
defer wg.Done() derrs[i] = nil
objs := poolObjIdxMap[idx] break
if len(objs) == 0 { }
return if derrs[i] == nil {
} // No error related to this object is found, assign this pool result
orgIndexes := origIndexMap[idx] // whether it is nil because there is no object found or because of
deletedObjects, errs := pool.DeleteObjects(ctx, bucket, objs, opts) // some other errors such erasure quorum errors.
mu.Lock() dobjects[i] = dObjectsByPool[pool][i]
for i, derr := range errs { derrs[i] = dErrsByPool[pool][i]
if derr != nil { }
derrs[orgIndexes[i]] = derr
}
deletedObjects[i].ObjectName = decodeDirObject(deletedObjects[i].ObjectName)
dobjects[orgIndexes[i]] = deletedObjects[i]
}
mu.Unlock()
}(idx, pool)
} }
wg.Wait()
} }
for i := range dobjects {
dobjects[i].ObjectName = decodeDirObject(dobjects[i].ObjectName)
}
return dobjects, derrs return dobjects, derrs
} }
@@ -1890,9 +1843,18 @@ func (z *erasureServerPools) GetMultipartInfo(ctx context.Context, bucket, objec
return MultipartInfo{}, err return MultipartInfo{}, err
} }
uploadIDLock := z.NewNSLock(bucket, pathJoin(object, uploadID))
lkctx, err := uploadIDLock.GetRLock(ctx, globalOperationTimeout)
if err != nil {
return MultipartInfo{}, err
}
ctx = lkctx.Context()
defer uploadIDLock.RUnlock(lkctx)
if z.SinglePool() { if z.SinglePool() {
return z.serverPools[0].GetMultipartInfo(ctx, bucket, object, uploadID, opts) return z.serverPools[0].GetMultipartInfo(ctx, bucket, object, uploadID, opts)
} }
for idx, pool := range z.serverPools { for idx, pool := range z.serverPools {
if z.IsSuspended(idx) { if z.IsSuspended(idx) {
continue continue
@@ -1908,6 +1870,7 @@ func (z *erasureServerPools) GetMultipartInfo(ctx context.Context, bucket, objec
// any other unhandled error return right here. // any other unhandled error return right here.
return MultipartInfo{}, err return MultipartInfo{}, err
} }
return MultipartInfo{}, InvalidUploadID{ return MultipartInfo{}, InvalidUploadID{
Bucket: bucket, Bucket: bucket,
Object: object, Object: object,
@@ -1921,9 +1884,18 @@ func (z *erasureServerPools) ListObjectParts(ctx context.Context, bucket, object
return ListPartsInfo{}, err return ListPartsInfo{}, err
} }
uploadIDLock := z.NewNSLock(bucket, pathJoin(object, uploadID))
lkctx, err := uploadIDLock.GetRLock(ctx, globalOperationTimeout)
if err != nil {
return ListPartsInfo{}, err
}
ctx = lkctx.Context()
defer uploadIDLock.RUnlock(lkctx)
if z.SinglePool() { if z.SinglePool() {
return z.serverPools[0].ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts) return z.serverPools[0].ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts)
} }
for idx, pool := range z.serverPools { for idx, pool := range z.serverPools {
if z.IsSuspended(idx) { if z.IsSuspended(idx) {
continue continue
@@ -1937,6 +1909,7 @@ func (z *erasureServerPools) ListObjectParts(ctx context.Context, bucket, object
} }
return ListPartsInfo{}, err return ListPartsInfo{}, err
} }
return ListPartsInfo{}, InvalidUploadID{ return ListPartsInfo{}, InvalidUploadID{
Bucket: bucket, Bucket: bucket,
Object: object, Object: object,
@@ -1957,6 +1930,14 @@ func (z *erasureServerPools) AbortMultipartUpload(ctx context.Context, bucket, o
} }
}() }()
lk := z.NewNSLock(bucket, pathJoin(object, uploadID))
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
if err != nil {
return err
}
ctx = lkctx.Context()
defer lk.Unlock(lkctx)
if z.SinglePool() { if z.SinglePool() {
return z.serverPools[0].AbortMultipartUpload(ctx, bucket, object, uploadID, opts) return z.serverPools[0].AbortMultipartUpload(ctx, bucket, object, uploadID, opts)
} }
@@ -1995,6 +1976,16 @@ func (z *erasureServerPools) CompleteMultipartUpload(ctx context.Context, bucket
} }
}() }()
// Hold write locks to verify uploaded parts, also disallows any
// parallel PutObjectPart() requests.
uploadIDLock := z.NewNSLock(bucket, pathJoin(object, uploadID))
wlkctx, err := uploadIDLock.GetLock(ctx, globalOperationTimeout)
if err != nil {
return objInfo, err
}
ctx = wlkctx.Context()
defer uploadIDLock.Unlock(wlkctx)
if z.SinglePool() { if z.SinglePool() {
return z.serverPools[0].CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts) return z.serverPools[0].CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts)
} }
@@ -2430,6 +2421,7 @@ func (z *erasureServerPools) Walk(ctx context.Context, bucket, prefix string, re
// HealObjectFn closure function heals the object. // HealObjectFn closure function heals the object.
type HealObjectFn func(bucket, object, versionID string, scanMode madmin.HealScanMode) error type HealObjectFn func(bucket, object, versionID string, scanMode madmin.HealScanMode) error
// List a prefix or a single object versions and heal
func (z *erasureServerPools) HealObjects(ctx context.Context, bucket, prefix string, opts madmin.HealOpts, healObjectFn HealObjectFn) error { func (z *erasureServerPools) HealObjects(ctx context.Context, bucket, prefix string, opts madmin.HealOpts, healObjectFn HealObjectFn) error {
healEntry := func(bucket string, entry metaCacheEntry, scanMode madmin.HealScanMode) error { healEntry := func(bucket string, entry metaCacheEntry, scanMode madmin.HealScanMode) error {
if entry.isDir() { if entry.isDir() {
@@ -2493,7 +2485,7 @@ func (z *erasureServerPools) HealObjects(ctx context.Context, bucket, prefix str
go func(idx int, set *erasureObjects) { go func(idx int, set *erasureObjects) {
defer wg.Done() defer wg.Done()
errs[idx] = set.listAndHeal(ctx, bucket, prefix, opts.ScanMode, healEntry) errs[idx] = set.listAndHeal(ctx, bucket, prefix, opts.Recursive, opts.ScanMode, healEntry)
}(idx, set) }(idx, set)
} }
wg.Wait() wg.Wait()
@@ -2545,15 +2537,22 @@ func (z *erasureServerPools) HealObject(ctx context.Context, bucket, object, ver
} }
} }
hr := madmin.HealResultItem{
Type: madmin.HealItemObject,
Bucket: bucket,
Object: object,
VersionID: versionID,
}
// At this stage, all errors are 'not found' // At this stage, all errors are 'not found'
if versionID != "" { if versionID != "" {
return madmin.HealResultItem{}, VersionNotFound{ return hr, VersionNotFound{
Bucket: bucket, Bucket: bucket,
Object: object, Object: object,
VersionID: versionID, VersionID: versionID,
} }
} }
return madmin.HealResultItem{}, ObjectNotFound{ return hr, ObjectNotFound{
Bucket: bucket, Bucket: bucket,
Object: object, Object: object,
} }
@@ -2774,7 +2773,19 @@ func (z *erasureServerPools) PutObjectMetadata(ctx context.Context, bucket, obje
return z.serverPools[0].PutObjectMetadata(ctx, bucket, object, opts) return z.serverPools[0].PutObjectMetadata(ctx, bucket, object, opts)
} }
if !opts.NoLock {
// Lock the object before updating metadata.
lk := z.NewNSLock(bucket, object)
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
if err != nil {
return ObjectInfo{}, err
}
ctx = lkctx.Context()
defer lk.Unlock(lkctx)
}
opts.MetadataChg = true opts.MetadataChg = true
opts.NoLock = true
// We don't know the size here set 1GiB at least. // We don't know the size here set 1GiB at least.
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts) idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
if err != nil { if err != nil {
@@ -2791,7 +2802,19 @@ func (z *erasureServerPools) PutObjectTags(ctx context.Context, bucket, object s
return z.serverPools[0].PutObjectTags(ctx, bucket, object, tags, opts) return z.serverPools[0].PutObjectTags(ctx, bucket, object, tags, opts)
} }
if !opts.NoLock {
// Lock the object before updating tags.
lk := z.NewNSLock(bucket, object)
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
if err != nil {
return ObjectInfo{}, err
}
ctx = lkctx.Context()
defer lk.Unlock(lkctx)
}
opts.MetadataChg = true opts.MetadataChg = true
opts.NoLock = true
// We don't know the size here set 1GiB at least. // We don't know the size here set 1GiB at least.
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts) idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
@@ -2809,8 +2832,19 @@ func (z *erasureServerPools) DeleteObjectTags(ctx context.Context, bucket, objec
return z.serverPools[0].DeleteObjectTags(ctx, bucket, object, opts) return z.serverPools[0].DeleteObjectTags(ctx, bucket, object, opts)
} }
opts.MetadataChg = true if !opts.NoLock {
// Lock the object before deleting tags.
lk := z.NewNSLock(bucket, object)
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
if err != nil {
return ObjectInfo{}, err
}
ctx = lkctx.Context()
defer lk.Unlock(lkctx)
}
opts.MetadataChg = true
opts.NoLock = true
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts) idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
if err != nil { if err != nil {
return ObjectInfo{}, err return ObjectInfo{}, err
@@ -2841,8 +2875,20 @@ func (z *erasureServerPools) TransitionObject(ctx context.Context, bucket, objec
return z.serverPools[0].TransitionObject(ctx, bucket, object, opts) return z.serverPools[0].TransitionObject(ctx, bucket, object, opts)
} }
if !opts.NoLock {
// Acquire write lock before starting to transition the object.
lk := z.NewNSLock(bucket, object)
lkctx, err := lk.GetLock(ctx, globalDeleteOperationTimeout)
if err != nil {
return err
}
ctx = lkctx.Context()
defer lk.Unlock(lkctx)
}
// Avoid transitioning an object from a pool being decommissioned. // Avoid transitioning an object from a pool being decommissioned.
opts.SkipDecommissioned = true opts.SkipDecommissioned = true
opts.NoLock = true
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts) idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
if err != nil { if err != nil {
return err return err
@@ -2858,8 +2904,20 @@ func (z *erasureServerPools) RestoreTransitionedObject(ctx context.Context, buck
return z.serverPools[0].RestoreTransitionedObject(ctx, bucket, object, opts) return z.serverPools[0].RestoreTransitionedObject(ctx, bucket, object, opts)
} }
if !opts.NoLock {
// Acquire write lock before restoring transitioned object
lk := z.NewNSLock(bucket, object)
lkctx, err := lk.GetLock(ctx, globalDeleteOperationTimeout)
if err != nil {
return err
}
ctx = lkctx.Context()
defer lk.Unlock(lkctx)
}
// Avoid restoring object from a pool being decommissioned. // Avoid restoring object from a pool being decommissioned.
opts.SkipDecommissioned = true opts.SkipDecommissioned = true
opts.NoLock = true
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts) idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
if err != nil { if err != nil {
return err return err
+4 -11
View File
@@ -39,6 +39,7 @@ import (
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/minio/pkg/v3/console" "github.com/minio/pkg/v3/console"
"github.com/minio/pkg/v3/sync/errgroup" "github.com/minio/pkg/v3/sync/errgroup"
"github.com/puzpuzpuz/xsync/v3"
) )
// setsDsyncLockers is encapsulated type for Close() // setsDsyncLockers is encapsulated type for Close()
@@ -570,10 +571,7 @@ func auditObjectErasureSet(ctx context.Context, api, object string, set *erasure
// NewNSLock - initialize a new namespace RWLocker instance. // NewNSLock - initialize a new namespace RWLocker instance.
func (s *erasureSets) NewNSLock(bucket string, objects ...string) RWLocker { func (s *erasureSets) NewNSLock(bucket string, objects ...string) RWLocker {
if len(objects) == 1 { return s.sets[0].NewNSLock(bucket, objects...)
return s.getHashedSet(objects[0]).NewNSLock(bucket, objects...)
}
return s.getHashedSet("").NewNSLock(bucket, objects...)
} }
// SetDriveCount returns the current drives per set. // SetDriveCount returns the current drives per set.
@@ -704,9 +702,8 @@ func (s *erasureSets) getHashedSet(input string) (set *erasureObjects) {
} }
// listDeletedBuckets lists deleted buckets from all disks. // listDeletedBuckets lists deleted buckets from all disks.
func listDeletedBuckets(ctx context.Context, storageDisks []StorageAPI, delBuckets map[string]VolInfo, readQuorum int) error { func listDeletedBuckets(ctx context.Context, storageDisks []StorageAPI, delBuckets *xsync.MapOf[string, VolInfo], readQuorum int) error {
g := errgroup.WithNErrs(len(storageDisks)) g := errgroup.WithNErrs(len(storageDisks))
var mu sync.Mutex
for index := range storageDisks { for index := range storageDisks {
index := index index := index
g.Go(func() error { g.Go(func() error {
@@ -725,11 +722,7 @@ func listDeletedBuckets(ctx context.Context, storageDisks []StorageAPI, delBucke
vi, err := storageDisks[index].StatVol(ctx, pathJoin(minioMetaBucket, bucketMetaPrefix, deletedBucketsPrefix, volName)) vi, err := storageDisks[index].StatVol(ctx, pathJoin(minioMetaBucket, bucketMetaPrefix, deletedBucketsPrefix, volName))
if err == nil { if err == nil {
vi.Name = strings.TrimSuffix(volName, SlashSeparator) vi.Name = strings.TrimSuffix(volName, SlashSeparator)
mu.Lock() delBuckets.Store(volName, vi)
if _, ok := delBuckets[volName]; !ok {
delBuckets[volName] = vi
}
mu.Unlock()
} }
} }
return nil return nil
-3
View File
@@ -551,9 +551,6 @@ func (er erasureObjects) nsScanner(ctx context.Context, buckets []BucketInfo, wa
var root dataUsageEntry var root dataUsageEntry
if r := cache.root(); r != nil { if r := cache.root(); r != nil {
root = cache.flatten(*r) root = cache.flatten(*r)
if root.ReplicationStats.empty() {
root.ReplicationStats = nil
}
} }
select { select {
case <-ctx.Done(): case <-ctx.Done():
+15 -6
View File
@@ -24,6 +24,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"net"
"net/http"
"os" "os"
"path" "path"
"strings" "strings"
@@ -286,6 +288,10 @@ func (driver *ftpDriver) CheckPasswd(c *ftp.Context, username, password string)
} }
func (driver *ftpDriver) getMinIOClient(ctx *ftp.Context) (*minio.Client, error) { func (driver *ftpDriver) getMinIOClient(ctx *ftp.Context) (*minio.Client, error) {
tr := http.RoundTripper(globalRemoteFTPClientTransport)
if host, _, err := net.SplitHostPort(ctx.Sess.RemoteAddr().String()); err == nil {
tr = forwardForTransport{tr: tr, fwd: host}
}
ui, ok := globalIAMSys.GetUser(context.Background(), ctx.Sess.LoginUser()) ui, ok := globalIAMSys.GetUser(context.Background(), ctx.Sess.LoginUser())
if !ok && !globalIAMSys.LDAPConfig.Enabled() { if !ok && !globalIAMSys.LDAPConfig.Enabled() {
return nil, errNoSuchUser return nil, errNoSuchUser
@@ -361,9 +367,10 @@ func (driver *ftpDriver) getMinIOClient(ctx *ftp.Context) (*minio.Client, error)
} }
return minio.New(driver.endpoint, &minio.Options{ return minio.New(driver.endpoint, &minio.Options{
Creds: mcreds, Creds: mcreds,
Secure: globalIsTLS, Secure: globalIsTLS,
Transport: globalRemoteFTPClientTransport, Transport: tr,
TrailingHeaders: true,
}) })
} }
@@ -375,9 +382,10 @@ func (driver *ftpDriver) getMinIOClient(ctx *ftp.Context) (*minio.Client, error)
} }
return minio.New(driver.endpoint, &minio.Options{ return minio.New(driver.endpoint, &minio.Options{
Creds: credentials.NewStaticV4(ui.Credentials.AccessKey, ui.Credentials.SecretKey, ""), Creds: credentials.NewStaticV4(ui.Credentials.AccessKey, ui.Credentials.SecretKey, ""),
Secure: globalIsTLS, Secure: globalIsTLS,
Transport: globalRemoteFTPClientTransport, Transport: tr,
TrailingHeaders: true,
}) })
} }
@@ -546,6 +554,7 @@ func (driver *ftpDriver) PutFile(ctx *ftp.Context, objPath string, data io.Reade
info, err := clnt.PutObject(context.Background(), bucket, object, data, -1, minio.PutObjectOptions{ info, err := clnt.PutObject(context.Background(), bucket, object, data, -1, minio.PutObjectOptions{
ContentType: mimedb.TypeByExtension(path.Ext(object)), ContentType: mimedb.TypeByExtension(path.Ext(object)),
DisableContentSha256: true, DisableContentSha256: true,
Checksum: minio.ChecksumFullObjectCRC32C,
}) })
n = info.Size n = info.Size
return n, err return n, err
+32 -20
View File
@@ -22,19 +22,17 @@ import (
"net" "net"
"net/http" "net/http"
"path" "path"
"path/filepath"
"runtime/debug" "runtime/debug"
"strings" "strings"
"sync/atomic" "sync/atomic"
"time" "time"
"unicode"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"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/internal/grid" "github.com/minio/minio/internal/grid"
xnet "github.com/minio/pkg/v3/net" xnet "github.com/minio/pkg/v3/net"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"github.com/minio/minio/internal/amztime" "github.com/minio/minio/internal/amztime"
"github.com/minio/minio/internal/config/dns" "github.com/minio/minio/internal/config/dns"
@@ -75,7 +73,7 @@ const (
// and must not set by clients // and must not set by clients
func containsReservedMetadata(header http.Header) bool { func containsReservedMetadata(header http.Header) bool {
for key := range header { for key := range header {
if slices.Contains(maps.Keys(validSSEReplicationHeaders), key) { if _, ok := validSSEReplicationHeaders[key]; ok {
return false return false
} }
if stringsHasPrefixFold(key, ReservedMetadataPrefix) { if stringsHasPrefixFold(key, ReservedMetadataPrefix) {
@@ -295,12 +293,6 @@ func parseAmzDateHeader(req *http.Request) (time.Time, APIErrorCode) {
return time.Time{}, ErrMissingDateHeader return time.Time{}, ErrMissingDateHeader
} }
// Bad path components to be rejected by the path validity handler.
const (
dotdotComponent = ".."
dotComponent = "."
)
func hasBadHost(host string) error { func hasBadHost(host string) error {
if globalIsCICD && strings.TrimSpace(host) == "" { if globalIsCICD && strings.TrimSpace(host) == "" {
// under CI/CD test setups ignore empty hosts as invalid hosts // under CI/CD test setups ignore empty hosts as invalid hosts
@@ -313,21 +305,41 @@ func hasBadHost(host string) error {
// Check if the incoming path has bad path components, // Check if the incoming path has bad path components,
// such as ".." and "." // such as ".." and "."
func hasBadPathComponent(path string) bool { func hasBadPathComponent(path string) bool {
if len(path) > 4096 { n := len(path)
// path cannot be greater than Linux PATH_MAX if n > 32<<10 {
// this is to avoid a busy loop, that can happen // At 32K we are beyond reasonable.
// if the caller sends path of following style
// a/a/a/a/a/a/a/a...
return true return true
} }
path = filepath.ToSlash(strings.TrimSpace(path)) // For windows '\' must be converted to '/' i := 0
for _, p := range strings.Split(path, SlashSeparator) { // Skip leading slashes (for sake of Windows \ is included as well)
switch strings.TrimSpace(p) { for i < n && (path[i] == SlashSeparatorChar || path[i] == '\\') {
case dotdotComponent: i++
}
for i < n {
// Find the next segment
start := i
for i < n && path[i] != SlashSeparatorChar && path[i] != '\\' {
i++
}
// Trim whitespace of segment
segmentStart, segmentEnd := start, i
for segmentStart < segmentEnd && unicode.IsSpace(rune(path[segmentStart])) {
segmentStart++
}
for segmentEnd > segmentStart && unicode.IsSpace(rune(path[segmentEnd-1])) {
segmentEnd--
}
// Check for ".." or "."
switch {
case segmentEnd-segmentStart == 2 && path[segmentStart] == '.' && path[segmentStart+1] == '.':
return true return true
case dotComponent: case segmentEnd-segmentStart == 1 && path[segmentStart] == '.':
return true return true
} }
i++
} }
return false return false
} }
+25
View File
@@ -22,6 +22,7 @@ import (
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"strconv" "strconv"
"strings"
"testing" "testing"
"github.com/minio/minio/internal/crypto" "github.com/minio/minio/internal/crypto"
@@ -184,3 +185,27 @@ func TestSSETLSHandler(t *testing.T) {
} }
} }
} }
func Benchmark_hasBadPathComponent(t *testing.B) {
tests := []struct {
name string
input string
want bool
}{
{name: "empty", input: "", want: false},
{name: "backslashes", input: `\a\a\ \\ \\\\\\\`, want: false},
{name: "long", input: strings.Repeat("a/", 2000), want: false},
{name: "long-fail", input: strings.Repeat("a/", 2000) + "../..", want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(b *testing.B) {
b.SetBytes(int64(len(tt.input)))
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if got := hasBadPathComponent(tt.input); got != tt.want {
t.Fatalf("hasBadPathComponent() = %v, want %v", got, tt.want)
}
}
})
}
}
+9 -8
View File
@@ -459,8 +459,6 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
continue continue
} }
var versionHealed bool
res, err := er.HealObject(ctx, bucket, encodedEntryName, res, err := er.HealObject(ctx, bucket, encodedEntryName,
version.VersionID, madmin.HealOpts{ version.VersionID, madmin.HealOpts{
ScanMode: scanMode, ScanMode: scanMode,
@@ -475,21 +473,23 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
} }
} else { } else {
// Look for the healing results // Look for the healing results
if res.After.Drives[tracker.DiskIndex].State == madmin.DriveStateOk { if res.After.Drives[tracker.DiskIndex].State != madmin.DriveStateOk {
versionHealed = true err = fmt.Errorf("unexpected after heal state: %s", res.After.Drives[tracker.DiskIndex].State)
} }
} }
if versionHealed { if err == nil {
bgSeq.countHealed(madmin.HealItemObject) bgSeq.countHealed(madmin.HealItemObject)
result = healEntrySuccess(uint64(version.Size)) result = healEntrySuccess(uint64(version.Size))
} else { } else {
bgSeq.countFailed(madmin.HealItemObject) bgSeq.countFailed(madmin.HealItemObject)
result = healEntryFailure(uint64(version.Size)) result = healEntryFailure(uint64(version.Size))
if version.VersionID != "" { if version.VersionID != "" {
healingLogIf(ctx, fmt.Errorf("unable to heal object %s/%s-v(%s): %w", bucket, version.Name, version.VersionID, err)) healingLogIf(ctx, fmt.Errorf("unable to heal object %s/%s (version-id=%s): %w",
bucket, version.Name, version.VersionID, err))
} else { } else {
healingLogIf(ctx, fmt.Errorf("unable to heal object %s/%s: %w", bucket, version.Name, err)) healingLogIf(ctx, fmt.Errorf("unable to heal object %s/%s: %w",
bucket, version.Name, err))
} }
} }
@@ -539,7 +539,8 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
go healEntry(bucket, *entry) go healEntry(bucket, *entry)
}, },
finished: func(errs []error) { finished: func(errs []error) {
if countErrs(errs, nil) != len(errs) { success := countErrs(errs, nil)
if success < expectedDisks {
retErr = fmt.Errorf("one or more errors reported during listing: %v", errors.Join(errs...)) retErr = fmt.Errorf("one or more errors reported during listing: %v", errors.Join(errs...))
} }
}, },
+3 -6
View File
@@ -28,6 +28,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/dustin/go-humanize"
"github.com/shirou/gopsutil/v3/mem" "github.com/shirou/gopsutil/v3/mem"
"github.com/minio/minio/internal/config/api" "github.com/minio/minio/internal/config/api"
@@ -61,7 +62,6 @@ type apiConfig struct {
const ( const (
cgroupV1MemLimitFile = "/sys/fs/cgroup/memory/memory.limit_in_bytes" cgroupV1MemLimitFile = "/sys/fs/cgroup/memory/memory.limit_in_bytes"
cgroupV2MemLimitFile = "/sys/fs/cgroup/memory.max" cgroupV2MemLimitFile = "/sys/fs/cgroup/memory.max"
cgroupMemNoLimit = 9223372036854771712
) )
func cgroupMemLimit() (limit uint64) { func cgroupMemLimit() (limit uint64) {
@@ -78,10 +78,8 @@ func cgroupMemLimit() (limit uint64) {
// but still, no need to interpret more // but still, no need to interpret more
return 0 return 0
} }
if limit == cgroupMemNoLimit { if limit >= 100*humanize.TiByte {
// No limit set, It's the highest positive signed 64-bit // No limit set, or unreasonably high. Ignore
// integer (2^63-1), rounded down to multiples of 4096 (2^12),
// the most common page size on x86 systems - for cgroup_limits.
return 0 return 0
} }
return limit return limit
@@ -368,7 +366,6 @@ func maxClients(f http.HandlerFunc) http.HandlerFunc {
writeErrorResponse(ctx, w, writeErrorResponse(ctx, w,
errorCodes.ToAPIErr(ErrTooManyRequests), errorCodes.ToAPIErr(ErrTooManyRequests),
r.URL) r.URL)
} }
} }
} }
+5 -2
View File
@@ -447,7 +447,7 @@ func getHostName(r *http.Request) (hostName string) {
} }
// Proxy any request to an endpoint. // Proxy any request to an endpoint.
func proxyRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, ep ProxyEndpoint) (success bool) { func proxyRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, ep ProxyEndpoint, returnErr bool) (success bool) {
success = true success = true
// Make sure we remove any existing headers before // Make sure we remove any existing headers before
@@ -462,7 +462,10 @@ func proxyRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, e
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
success = false success = false
if err != nil && !errors.Is(err, context.Canceled) { if err != nil && !errors.Is(err, context.Canceled) {
replLogIf(GlobalContext, err) proxyLogIf(GlobalContext, err)
}
if returnErr {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
} }
}, },
}) })
+2 -2
View File
@@ -133,7 +133,7 @@ func (bh *bucketHTTPStats) updateHTTPStats(bucket, api string, w *xhttp.Response
bucketHTTPRequestsDuration.With(prometheus.Labels{ bucketHTTPRequestsDuration.With(prometheus.Labels{
"api": api, "api": api,
"bucket": bucket, "bucket": bucket,
}).Observe(w.TimeToFirstByte.Seconds()) }).Observe(w.TTFB().Seconds())
} }
bh.Lock() bh.Lock()
@@ -433,7 +433,7 @@ func (st *HTTPStats) updateStats(api string, w *xhttp.ResponseRecorder) {
st.totalS3Requests.Inc(api) st.totalS3Requests.Inc(api)
// Increment the prometheus http request response histogram with appropriate label // Increment the prometheus http request response histogram with appropriate label
httpRequestsDuration.With(prometheus.Labels{"api": api}).Observe(w.TimeToFirstByte.Seconds()) httpRequestsDuration.With(prometheus.Labels{"api": api}).Observe(w.TTFB().Seconds())
code := w.StatusCode code := w.StatusCode
+1 -1
View File
@@ -164,7 +164,7 @@ func httpTracerMiddleware(h http.Handler) http.Handler {
Latency: reqEndTime.Sub(respRecorder.StartTime), Latency: reqEndTime.Sub(respRecorder.StartTime),
InputBytes: inputBytes, InputBytes: inputBytes,
OutputBytes: respRecorder.Size(), OutputBytes: respRecorder.Size(),
TimeToFirstByte: respRecorder.TimeToFirstByte, TimeToFirstByte: respRecorder.TTFB(),
}, },
}, },
} }
-1
View File
@@ -493,7 +493,6 @@ func (ies *IAMEtcdStore) watch(ctx context.Context, keyPath string) <-chan iamWa
keyPath: string(event.Kv.Key), keyPath: string(event.Kv.Key),
} }
} }
} }
} }
} }

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