Compare commits

..

151 Commits

Author SHA1 Message Date
Harshavardhana 6d04c9c585 populate additional claims for prometheus endpoint (#13011)
service accounts and STS provide additional claims for
policy authorization which needs to be verified along
with Prometheus issuer claim.
2021-08-20 11:32:01 -07:00
Krishnan Parthasarathi e210cb3670 fix: use transition/replication fields in FileInfo quorum calculation (#13010) 2021-08-19 14:55:42 -07:00
Klaus Post 47b577fcc0 Lock while creating buckets (#12999)
Ensure that one call will succeed and others will serialize

Example failure without code in place:
```
    bucket-policy-handlers_test.go:120: unexpected error: cmd.InsufficientWriteQuorum: Storage resources are insufficient for the write operation doz2wjqaovp5kvlrv11fyacowgcvoziszmkmzzz9nk9au946qwhci4zkane5-1/
    bucket-policy-handlers_test.go:120: unexpected error: cmd.InsufficientWriteQuorum: Storage resources are insufficient for the write operation doz2wjqaovp5kvlrv11fyacowgcvoziszmkmzzz9nk9au946qwhci4zkane5-1/
    bucket-policy-handlers_test.go:135: want 1 ok, got 0
```
2021-08-19 13:21:02 -07:00
Harshavardhana e9d970154d use renameAll instead of deleteAll for metacache-manager (#13005)
renameAll is cheaper, rely on background deletes instead.
2021-08-19 09:16:14 -07:00
Harshavardhana 202d0b64eb fix: enable go1.17 github ci/cd (#12997) 2021-08-18 18:35:22 -07:00
Klaus Post c25816eabc xl walk: Limit walk concurrent IO (#12885)
We are observing heavy system loads, potentially
locking the system up for periods when concurrent
listing operations are performed.

We place a per-disk lock on walk IO operations.
This will minimize the impact of concurrent listing
operations on the entire system and de-prioritize
them compared to other operations.

Single list operations should remain largely unaffected.
2021-08-18 18:10:36 -07:00
Harshavardhana ee028a4693 listObjects optimized to handle max-keys=1 when prefix is object (#13000)
Some applications albeit poorly written rather than using headObject
rely on listObjects to check for existence of object, this unusual
request always has prefix=(to actual object) and max-keys=1

handle this situation specially such that we can avoid readdir()
on the top level parent to avoid sorting and skipping, ensuring
that such type of listObjects() always behaves similar to a
headObject() call.
2021-08-18 18:05:05 -07:00
Harshavardhana 9c65168312 fix: all levels deep flat key match (#12996)
this addresses a regression from #12984
which only addresses flat key from single
level deep at bucket level.

added extra tests as well to cover all
these scenarios.
2021-08-18 07:40:53 -07:00
Minio Trusted 16aeb68c28 Update yaml files to latest version RELEASE.2021-08-17T20-53-08Z 2021-08-17 22:34:33 +00:00
Harshavardhana 6167104644 update console to v0.9.1 2021-08-17 13:53:08 -07:00
Krishnan Parthasarathi 30b77f59b1 doc: Add ilm prometheus metrics information (#12994) 2021-08-17 12:19:36 -07:00
Harshavardhana a690772cc5 add support to set subnet license for embedded console (#12993) 2021-08-17 11:56:01 -07:00
Krishnan Parthasarathi cf8abd8888 Add prometheus metrics for ILM tasks (#12933) 2021-08-17 10:21:19 -07:00
johnnyaug 17e0de1e87 fix: typo in CONTRIBUTING.md (#12907) 2021-08-17 09:20:10 -07:00
Krishnan Parthasarathi b7e3651d3c Set free-version id in case of version/version-suspended buckets (#12982)
This free-version id may be used to track tiered object contents of the
object (version) being deleted.
2021-08-17 08:59:48 -07:00
Harshavardhana ef4d023c85 fix: various performance improvements to tiering (#12965)
- deletes should always Sweep() for tiering at the
  end and does not need an extra getObjectInfo() call
- puts, copy and multipart writes should conditionally
  do getObjectInfo() when tiering targets are configured
- introduce 'TransitionedObject' struct for ease of usage
  and understanding.
- multiple-pools optimization deletes don't need to hold
  read locks verifying objects across namespace and pools.
2021-08-17 07:50:00 -07:00
Harshavardhana 654a6e9871 always set the filter to skip navigating baseDir (#12984)
baseDir is empty if the top level prefix does not
end with `/` this causes large recursive listings
without any filtering, to fix this filtering make
sure to set the filter prefix appropriately.

also do not navigate folders at top level that do
not match the filter prefix, entries don't need
to match prefix since they are never prefixed
with the prefix anyways.
2021-08-17 07:43:24 -07:00
Aditya Manthramurthy 2ca5ee026d Remove default value of STSExpiry for LDAP (#12985)
This ensures that the deprecation warning is shown when the setting is actually
used in a configuration - instead of showing up whenever LDAP is enabled.
2021-08-17 02:25:05 -07:00
Aditya Manthramurthy 9b7d593e28 Add helper script to call assume role (#12978) 2021-08-17 01:46:59 -07:00
Klaus Post ad928f0078 Return list request when canceled (#12977)
* Return list request when canceled
* Cancel list if abandoned
2021-08-16 11:59:16 -07:00
Klaus Post 92bb2928e4 Compress better on amd64 (#12974)
Since S2 has amd64 assembly, it now operates at a reasonable 
speed to use by default.

Here are some examples of stream compression speed, 16 cores:
```
nyc-taxi-data-10M.csv	s2	1	3325605752	-> 1095998837	312ms	10139.07MB/s		67.04% reduction
nyc-taxi-data-10M.csv	s2	2	3325605752	-> 917905514	428ms	7393.74MB/s		72.40%

github-june-2days-2019.json	s2	1	6273951764	-> 1043196283	391ms	15301.99 MB/s		83.37%
github-june-2days-2019.json	s2	2	6273951764	-> 955924506	519ms	11510.81MB/s		84.76%

github-ranks-backup.bin	s2	1	1862623243	-> 623911363	146ms	12133MB/s		66.50%
github-ranks-backup.bin	s2	2	1862623243	-> 563752759	230ms	7705.26MB/s		69.73%
```

We keep non-assembly platforms on the faster, but less efficient mode.
2021-08-16 11:55:07 -07:00
Anis Elleuch 47dfc1b1b0 ldap: Reevalute filter when searching for non eligible users (#12953)
The previous code removes SVC/STS accounts for ldap users that do not
exist anymore in LDAP server. This commit will actually re-evaluate
filter as well if it is changed and remove all local SVC/STS accounts
beloning to the ldap user if the latter is not eligible for the
search filter anymore.

For example: the filter selects enabled users among other criteras in
the LDAP database, if one ldap user changes his status to disabled
later, then associated SVC/STS accounts will be removed because that user
does not meet the filter search anymore.
2021-08-13 11:40:04 -07:00
Klaus Post 7d8413a589 Reuse more metadata buffers (#12955)
Reuse metadata buffers when no longer referenced.

Takes care of most of the happy paths.
2021-08-13 11:39:27 -07:00
Klaus Post 24722ddd02 Remove inline data hack (#12946)
move the code down to the storage layer,
this logic decouples the inline data from the 
size parameter making it flexible and future
proof.
2021-08-13 08:25:54 -07:00
Klaus Post f31a00de01 fix: http stats race in traffic metering (#12956)
Traffic metering was not protected against concurrent updates.

```
WARNING: DATA RACE
Read at 0x00c02b0dace8 by goroutine 235:
  github.com/minio/minio/cmd.setHTTPStatsHandler.func1()
      d:/minio/minio/cmd/generic-handlers.go:360 +0x27d
  net/http.HandlerFunc.ServeHTTP()
...

Previous write at 0x00c02b0dace8 by goroutine 994:
  github.com/minio/minio/internal/http/stats.(*IncomingTrafficMeter).Read()
      d:/minio/minio/internal/http/stats/http-traffic-recorder.go:34 +0xd2

```
2021-08-13 07:30:03 -07:00
Shireesh Anjal d44e4399e6 Add admin api to return sys services info (#12939)
The intention is to provide status of any sys services that can
potentially impact the performance of minio.

At present, it will return information about the `selinux` service
(not-installed/disabled/permissive/enforcing)

Signed-off-by: Shireesh Anjal <shireesh@minio.io>
2021-08-12 18:58:40 -07:00
Harshavardhana f9ae71fd17 fix: deleteMultiObjects performance regression (#12951)
fixes performance regression found in deleteObjects(),
putObject(), copyObject and completeMultipart calls.
2021-08-12 18:57:37 -07:00
Harshavardhana ce28e904c9 pass the current credentials for claims 2021-08-12 18:24:04 -07:00
Harshavardhana 77b8885a24 update console to latest release v0.9.0 2021-08-12 18:08:30 -07:00
Harshavardhana 8f2a3efa85 disallow sub-credentials based on root credentials to gain priviledges (#12947)
This happens because of a change added where any sub-credential
with parentUser == rootCredential i.e (MINIO_ROOT_USER) will
always be an owner, you cannot generate credentials with lower
session policy to restrict their access.

This doesn't affect user service accounts created with regular
users, LDAP or OpenID
2021-08-12 18:07:08 -07:00
Klaus Post 89febdb3d6 Reuse small buffers (#12948)
When reading metadata allow reuse of buffers 
in certain cases. Take the low-hanging fruit.

Reduce GC overhead when listing.
2021-08-12 14:27:22 -07:00
Klaus Post 3eac02f676 Use metadata reader in ReadVersion (#12942)
Use `readMetadata` when reading version 
information without data requested. 

Reduces IO on inlined data.

Bonus: Inline compressed data as well when 
compression is enabled.
2021-08-12 10:05:24 -07:00
Ricardo Katz a526ad2e80 Add headers into AMQP notifications (#12911)
Signed-off-by: Ricardo Katz <rkatz@vmware.com>
2021-08-11 22:24:19 -07:00
Krishnan Parthasarathi 65b6f4aa31 Add dynamic reconfiguration of number of transition workers (#12926) 2021-08-11 22:23:56 -07:00
Harshavardhana 9e88941515 fix: skip disks that are offline when healing the drives (#12931) 2021-08-11 12:57:18 -07:00
Harshavardhana 3becee9e5d use sync map instead of local DNS cache (#12925)
also enable PreferGo only resolver for FIPS builds
2021-08-10 21:20:09 -07:00
Harshavardhana 40a2fa8e81 fix: add more optimizations to putMetacacheObject() (#12916)
- avoid extra lookup for 'xl.meta' since we are
  definitely sure that it doesn't exist.

- use this in newMultipartUpload() as well

- also additionally do not write with O_DSYNC
  to avoid loading the drives, instead create
  'xl.meta' for listing operations without
  O_DSYNC since these are ephemeral objects.

- do the same with newMultipartUpload() since
  it gets synced when the PutObjectPart() is
  attempted, we do not need to tax newMultipartUpload()
  instead.
2021-08-10 11:12:22 -07:00
Harshavardhana 39f81d2c5b update max_delay to max_sleep under healing docs 2021-08-10 08:52:42 -07:00
Aditya Manthramurthy 59bb54ed6a Use common function for authenticating admin requests (#12915) 2021-08-09 18:14:38 -07:00
Klaus Post 9ab5e0312d Simplify gzhttp wrapper (#12912)
The wrapper now accepts interfaces so we don't need a wrapper for that any more.
2021-08-09 12:45:59 -07:00
Harshavardhana 54ab3a1d5b implement putMetacacheObject() optimizing List operations (#12903)
removes unexpected features from regular putObject() such as

- increasing parity when disks are down, avoids
  a lot of DiskInfo() calls.

- triggering MRF for metacache objects
  if disks are offline

- avoiding renames from temporary location
  to actual namespace, not needed since
  metacache files are unique.
2021-08-09 06:58:54 -07:00
Klaus Post 92c94011f1 Skip downed interfaces on Windows (#12910)
Disregard interfaces that are down when selecting bind addresses

Windows often has a number of disabled NICs used for VPN and other services.

This often causes minio to select an address for contacting the console that is on a disabled (virtual) NIC.

This checks if the interface is up before adding it to the pool on Windows.
2021-08-09 06:57:54 -07:00
Anis Elleuch 35cbe43b6d Start gateway when KMS is enabled and encryption is unsupported (#12808)
Before, the gateway will complain that it found KMS configured in the
environment but the gateway mode does not support encryption. This
commit will allow starting of the gateway but ensure that S3 operations
with encryption headers will fail when the gateway doesn't support
encryption. That way, the user can use etcd + KMS and have IAM data
encrypted in the etcd store.

Co-authored-by: Anis Elleuch <anis@min.io>
2021-08-08 12:51:48 -07:00
Harshavardhana a2cd3c9a1d use ParseForm() to allow query param lookups once (#12900)
```
cpu: Intel(R) Core(TM) i5-7200U CPU @ 2.50GHz
BenchmarkURLQueryForm
BenchmarkURLQueryForm-4         247099363                4.809 ns/op           0 B/op          0 allocs/op
BenchmarkURLQuery
BenchmarkURLQuery-4              2517624               462.1 ns/op           432 B/op          4 allocs/op
PASS
ok      github.com/minio/minio/cmd      3.848s
```
2021-08-07 22:43:01 -07:00
Minio Trusted 7b0b0f9101 Update yaml files to latest version RELEASE.2021-08-05T22-01-19Z 2021-08-06 02:19:42 +00:00
Harshavardhana 6c0757eea6 fix: simplify APIEndpoints() usage (#12893)
improvements include

- skip IPv6 correctly
- do not set default value for
  MINIO_SERVER_URL, let it be
  configured if not use local IPs

Bonus:

- In healing return error from listPathRaw()
- update console to v0.8.3
2021-08-05 15:01:19 -07:00
Anis Elleuch 37bef900fd ilm: More compliance to spec related to expired delete markers (#12887)
<Days> tag can remove expired delete markers according to AWS S3 spec.

<NoncurrentVersionExpiration> cannot remove expired delete markers in
anyway.
2021-08-05 11:21:21 -07:00
Harshavardhana 3863a96bdf remove unexpected spaces and newlines 2021-08-04 17:21:32 -07:00
Ravind Kumar c89135fd13 Update License section for documentation (#12878) 2021-08-04 17:07:38 -07:00
Harshavardhana b10f823907 allow diskFillFraction to be 99% (#12879)
larger 4-8TiB sized disks would return
error prematurely even with sufficient
amount of disk space left, increase
diskFillFraction to 1%
2021-08-04 15:23:39 -07:00
Harshavardhana 4d8f81a992 remove replicate set fallback behavior (#12865)
replica set deployment style is an incorrect
style of distributed MinIO deployment, since
statefulsets have been around for many releases
in k8s now.
2021-08-04 15:23:18 -07:00
Harshavardhana c13cbc64d1 fix multiple typos in documentation 2021-08-04 14:15:45 -07:00
Matt Sarrel 109c8acf4f fixed typo in metrics README.md (#12874) 2021-08-04 12:48:57 -07:00
Klaus Post cc60d66909 Fix incremental usage accounting (#12871)
Remote caches were not returned correctly, so they would not get updated on save.

Furthermore make some tweaks for more reliable updates.

Invalidate bloom filter to ensure rescan.
2021-08-04 09:14:14 -07:00
Krishnan Parthasarathi 4197870287 Storage-class headers should depend only on ObjectInfo state (#12864) 2021-08-03 17:35:52 -07:00
Harshavardhana 035882d292 fix: remove parentIsObject() check (#12851)
we will allow situations such as

```
a/b/1.txt
a/b
```

and

```
a/b
a/b/1.txt
```

we are going to document that this usecase is
not supported and we will never support it, if
any application does this users have to delete
the top level parent to make sure namespace is
accessible at lower level.

rest of the situations where the prefixes get
created across sets are supported as is.
2021-08-03 13:26:57 -07:00
Harshavardhana 9371852c7d fix: getAPIEndpoints() should return public_url (#12852)
fixes #12850
2021-08-02 21:50:20 -07:00
Harshavardhana ea64a9263c add deprecation notice for LDAP username format (#12849) 2021-08-02 18:20:06 -07:00
Harshavardhana 510c67de4a update console to v0.8.2 2021-08-02 13:07:49 -07:00
Dmitry Eliseev 27dbe30ecb Allow to specify secrets separately (#12813) 2021-07-31 21:44:51 -07:00
Krishnan Parthasarathi 0a62ae4e61 Revert ignoring inlined objects for transition (#12843) 2021-07-30 16:45:17 -07:00
Harshavardhana a51799d9f0 feat: Add support for audit notifications for transition (#12842)
This PR adds audit notifications for transitioning objects,
similar to audit logging for expiration and replication
traffic.
2021-07-30 12:45:25 -07:00
Harshavardhana e7baf2d7d2 stop all console logging 2021-07-29 23:06:05 -07:00
Shireesh Anjal 56097dfca8 Include system errors in health report (#12776)
Some examples of system errors:

- audit is enabled
- updatedb is installed
2021-07-29 23:05:34 -07:00
Harshavardhana 7281e86d9e update docker-compose to RELEASE.2021-07-30T00-02-00Z 2021-07-29 18:07:06 -07:00
Harshavardhana d004828b20 fix: speedtest Endpoint formatting 2021-07-29 17:51:30 -07:00
Harshavardhana 4567160e4d update madmin-go to v1.0.20 2021-07-29 17:02:00 -07:00
Harshavardhana bc6a2b579b update to console v0.8.1 2021-07-29 16:14:10 -07:00
Harshavardhana 3c34e18a4e allow multipart uploads for single part multipart (#12821)
its possible that some multipart uploads would have
uploaded only single parts so relying on `len(o.Parts)`
alone is not sufficient, we need to look for ETag
pattern to be absolutely sure.
2021-07-28 22:11:55 -07:00
Poorna Krishnamoorthy b6cd54779c Increase context timeout for bandwidth throttled reader (#12820)
increase default timeout up to one hour for toy setups.

fixes #12812
2021-07-28 15:20:01 -07:00
Harshavardhana bfbdb8f0a8 fix: incorrect O_DIRECT behavior for reads (#12811)
O_DIRECT behavior was broken and it was still
caching all the reads, this change properly fixes
this behavior.
2021-07-28 11:20:16 -07:00
Harshavardhana 397637a042 remove x-frame-options HTTP header
fixes #12817
2021-07-28 08:57:22 -07:00
Anis Elleuch 20cacc3b04 fs: Update object modtime when metadata is updated (#12818)
Follow S3 behavior of updating object modtime when object metadata is
updated.
2021-07-28 07:36:38 -07:00
Harshavardhana a78bc7bfdb rename all access token to id_tokens 2021-07-27 22:38:12 -07:00
Harshavardhana 3735450e7e fix: allow audience claim to be an array (#12810)
Some incorrect setups might have multiple audiences
where they are trying to use a single authentication
endpoint for multiple services.

Nevertheless OpenID spec allows it to make it
even more confusin for no good reason.

> It MUST contain the OAuth 2.0 client_id of the
> Relying Party as an audience value. It MAY also
> contain identifiers for other audiences. In the
> general case, the aud value is an array of case
> sensitive strings. In the common special case
> when there is one audience, the aud value MAY
> be a single case sensitive string.

fixes #12809
2021-07-27 18:37:51 -07:00
Krishna Srinivas aa0c28809b Server side speedtest implementation (#12750) 2021-07-27 12:55:56 -07:00
Minio Trusted 471b4fd0c9 Update yaml files to latest version RELEASE.2021-07-27T02-40-15Z 2021-07-27 05:02:00 +00:00
Harshavardhana ddcd419b4c always validate JWT token audience (#12797)
audience for the JWT token should match
the configured client_id, this allows
rejecting valid JWTs not meant for MinIO.
2021-07-26 19:40:15 -07:00
Harshavardhana a9d9b520ec remove short circuited healing optimization (#12796)
this healing optimization caused multiple
regressions in healing

- delete-markers incorrectly missing
  heal and returning incorrect healing
  results to client.

- missing individual 'parts' such
  as for restored object or simply
  for all objects just missing few parts.

This optimization is not necessary, we
should proceed to verify all cases possible
not just when metadata is inconsistent.
2021-07-26 16:51:09 -07:00
Harshavardhana 0c666379fe fix: avoid removing healed parts on dstDataPath (#12795)
destination path and old path will be similar
when healing occurs, this can lead to healed
parts being again purged leading to always an
inconsistent state on an object which might
further cause reduction in quorum eventually.
2021-07-26 15:15:34 -07:00
Harshavardhana a3f7d575e0 improve delete-marker healing (#12794)
delete-markers missing on drives were
not healed due to few things

disksWithAllParts() does not know-how
to deal with delete markers, add support
for that.

fixes #12787
2021-07-26 11:48:09 -07:00
Harshavardhana f175ff8f66 add healing fixes for delete-marker (#12788)
- delete-markers are incorrectly reported
  as corrupt with wrong data sent to client
  'mc admin heal -r' on objects with delete
  marker will report as 'grey' incorrectly.

- do not heal delete-markers during HeadObject()
  this can lead to inconsistent order of heals
  on the object, although this is not an issue
  in terms of order of versions it is rather
  simpler to keep the same order on all drives.

- defaultHealResult() should handle 'err == nil'
  case such that valid cases should be handled
  as 'drive' status OK.
2021-07-26 08:01:41 -07:00
Anis Elleuch 39874b77ed mrf: Avoid rare data race and more simplification (#12791)
This change avoids a rare data race and simplify the function that
returns MRF last activity information.
2021-07-26 08:00:59 -07:00
Aditya Manthramurthy 7106e9394c [LDAP] Document new LDAP synchronization behavior (#12786) 2021-07-25 10:45:53 -07:00
Harshavardhana e124d88788 optimize listing operation concurrency (#12728)
- remove use of getOnlineDisks() instead rely on fallbackDisks()
  when disk return errors like diskNotFound, unformattedDisk
  use other fallback disks to list from, instead of paying the
  price for checking getOnlineDisks()

- optimize getDiskID() further to avoid large write locks when
  looking formatLastCheck time window

This new change allows for a more relaxed fallback for listing
allowing for more tolerance and also eventually gain more
consistency in results even if using '3' disks by default.
2021-07-24 22:03:38 -07:00
Aditya Manthramurthy de00b641da [LDAP] Support syncing user-group memberships with LDAP service (#12785)
When configured in Lookup Bind mode, the server now periodically queries the
LDAP IDP service to find changes to a user's group memberships, and saves this
info to update the access policies for all temporary and service account
credentials belonging to LDAP users.
2021-07-24 11:57:36 -07:00
Harshavardhana e936871b83 update to console v0.8.0 2021-07-23 19:53:56 -07:00
Harshavardhana 922f5da8dc update nats.io to fix critical bug 2021-07-23 18:33:31 -07:00
Harshavardhana 294bafd58b remove unused WebTrace and minio/rpc dependency 2021-07-23 15:50:21 -07:00
Anis Elleuch 23ef25b57a profiling: Return goroutines with sleep duration (#12775)
Add a new goroutine file which has another printing format. We need it
to see how much time each goroutine was blocked. Easier to detect stops.

Co-authored-by: Anis Elleuch <anis@min.io>
2021-07-23 13:16:53 -07:00
Aditya Manthramurthy 9a31030e74 DEPRECATION Warning: Avoid configuring default LDAP STS Expiry (#12781)
- Show notice when `MINIO_IDENTITY_LDAP_STS_EXPIRY` or the 
  corresponding to the configuration option is used at server startup.
- Once support is removed, the default will be fixed at 1 hour.
- Users may specify expiry directly in the STS API.
- Update docs and help message
- Adds example in ldap.go to configure expiry in STS API.
2021-07-22 16:43:57 -07:00
AlexHuang2021 df2871de53 fix: return error when listing fails to retry healing (#12765) 2021-07-22 12:14:44 -07:00
Aditya Manthramurthy a3079a7de2 fix: Add support for DurationSeconds in LDAP STS API (#12778) 2021-07-22 12:13:21 -07:00
Aditya Manthramurthy 17a37241f0 fix: regression in AssumeRole session policy handling (fixes #12756) (#12773) 2021-07-22 08:42:07 -07:00
Minio Trusted 026a005b64 Update yaml files to latest version RELEASE.2021-07-22T05-23-32Z 2021-07-22 06:12:29 +00:00
Harshavardhana b865b9f43d update console to fix assets 2021-07-21 22:23:32 -07:00
Krishnan Parthasarathi 209e6d00c6 Use ObjectInfo.ToLifecycleOpts instead of literal values (#12772)
Promote getLifecycleTransitionTier to a method on lifecycle.Lifecycle.
2021-07-21 19:12:44 -07:00
Minio Trusted dbd7f74bb9 Update yaml files to latest version RELEASE.2021-07-21T22-15-23Z 2021-07-21 23:46:28 +00:00
Harshavardhana 4a2cb23690 update console to master 2021-07-21 15:15:23 -07:00
Harshavardhana 320e1533c4 use expected MinIO URLs for console (#12770)
when TLS is configured using IPs directly
might interfere and not work properly when
the server is configured with TLS certs but
the certs only have domain certs.

Also additionally allow users to specify
a public accessible URL for console to talk
to MinIO i.e `MINIO_SERVER_URL` this would
allow them to use an external ingress domain
to talk to MinIO. This internally fixes few
problems such as presigned URL generation on
the console UI etc.

This needs to be done additionally for any
MinIO deployments that might have a much more
stricter requirement when running in standalone
mode such as FS or standalone erasure code.
2021-07-21 14:51:16 -07:00
Harshavardhana 7f45e80755 update console to master with more UI fixes 2021-07-21 11:19:36 -07:00
Aditya Manthramurthy 0db1c94e7d [IDP:LDAP] Cleanup creds for removed LDAP user entries (#12759) 2021-07-20 23:33:12 -07:00
Aditya Manthramurthy e7a4967726 fix: openID cleanup all creds associated with OIDC user (#12758) 2021-07-20 17:42:10 -07:00
Krishnan Parthasarathi d0963974a5 pkg/lifecycle: Add SetPredictionHeaders method (#12755)
This method is used to add expected expiration and transition time 
for an object in GET/HEAD Object response headers.

Also fixed bugs in lifecycle.PredictTransitionTime and
getLifecycleTransitionTier in handling current and 
non-current versions.
2021-07-20 17:36:55 -07:00
Krishnan Parthasarathi 6ea083d197 Add deployment-id and source bucket to transitioned object name (#12693)
This allows remote bucket admin to identify the origin of transitioned
objects by simply inspecting the object prefixes.

e.g let's take a remote tier TIER-1 pointing to a remote bucket (prefix)
testbucket/testprefix-1. The remote bucket admin can list all transitioned objects
from a MinIO deployment identified by '2e78e906-1c5d-4f94-8689-9df44cafde39' and
source bucket 'mybucket' like so,

```
$ ./mc ls -r minio-tier-target/testbucket/testprefix-1/2e78e906-1c5d-4f94-8689-9df44cafde39/mybucket/
[2021-07-12 17:15:50 PDT]   160B 48/fb/48fbc0e6-3a73-458b-9337-8e722c619ca4
[2021-07-12 16:58:46 PDT]   160B 7d/1c/7d1c96bd-031a-48d4-99ea-b1304e870830
```
2021-07-20 10:49:52 -07:00
Harshavardhana ab7584eeac update to latest console fs/gateway 2021-07-19 17:49:54 -07:00
sakkiii 69e0faa278 add more XSS HTTP headers (#12256) 2021-07-19 16:05:02 -07:00
Shireesh Anjal fa98014bbe fix: hostname inside --console-address not anonymized (#12743)
In case of non-distributed setup, if the server start command contains a
`--console-address` flag and its value contains a hostname, it is not
getting anonymized.

Fixed by replacing the console host also with `server1`
2021-07-19 15:16:26 -07:00
Ali Malek c25ad94b7e Refactor docker-compose, Use YAML variable for common minio configs (#12735) 2021-07-18 18:06:40 -07:00
Harshavardhana 27e07e80bc fix: accountInfo() error FS mode (#12734) 2021-07-17 01:17:35 -07:00
Harshavardhana b13bca85d0 send backendInfo as part AccountInfo (#12733)
This is needed for console UI to change
the UI behavior for different modes of
operation.
2021-07-16 14:37:06 -07:00
Krishnan Parthasarathi 29eea52e14 Skip transitioning of object versions if inlined (#12705) 2021-07-16 09:38:27 -07:00
Anis Elleuch b0b4696a64 heal: Add MRF metrics to background heal API response (#12398)
This commit gathers MRF metrics from 
all nodes in a cluster and return it to the caller. This will show information about the 
number of objects in the MRF queues 
waiting to be healed.
2021-07-15 22:32:06 -07:00
Minio Trusted ead8778305 Update yaml files to latest version RELEASE.2021-07-15T22-27-34Z 2021-07-16 04:27:54 +00:00
Aditya Manthramurthy eae9c2f65b Add changes to ensure session policy is enforced in LDAP STS (#12716)
- Bonus: Fix bug in webidentity sts that doesnt parse session policy correctly.

- update ldap.go to support session policy argument
2021-07-15 15:27:34 -07:00
Poorna Krishnamoorthy a6ec405443 fix: UpdateObjectVersion should compare versionID through versions (#12726)
fixes #12703
2021-07-15 15:01:59 -07:00
Harshavardhana 9516587a6c update console to master branch
with new fixes

- improve download behavior
- avoid response timeouts
2021-07-15 14:44:18 -07:00
Ravind Kumar ce800ed347 DOCS: Remove http links in favor of HTTPS (#12725) 2021-07-15 13:31:59 -07:00
Harshavardhana 017722cf85 fix: propagate service account deletes properly (#12717)
service account deletes were not propagating
to remote peers, fix this.
2021-07-14 21:28:53 -07:00
Harshavardhana 10fc30a989 fix crash when config is not properly initialized (#12714)
fixes #12709
2021-07-14 18:27:25 -07:00
Lenin Alevski 92ffe5e5ef Allow MinIO to load configurations from env file (#12706)
docker-entrypoint.sh will load configuration values from
'config.env' file, this is useful when MinIO is deployed in Kubernetes
environments and want to avoid reading secrets from environment
variables

Signed-off-by: Lenin Alevski <alevsk.8772@gmail.com>
2021-07-14 16:55:59 -07:00
Lazzaretti e8cbfa7af2 fix: rename error message for MINIO_BROWSER_REDIRECT_URL (#12700)
rename the error message for MINIO_BROWSER_REDIRECT_URL to match the field name
2021-07-14 12:38:25 -07:00
Anis Elleuch b8f95fb3d4 fix: Use correct replication status in replication healing (#12711)
In case of replication healing, we always store completed status in the
object metadata, which is wrong because replication could fail in the
further retries.
2021-07-14 09:58:46 -07:00
Anis Elleuch a4b8928660 Move prefix usage in admin AccountInfo API (#12710) 2021-07-14 08:51:10 -07:00
Shireesh Anjal 4d6d4244f1 Anonymize hosts in subnet health report (#12627)
Ensure that hostnames / ip addresses are not printed in the subnet
health report. Anonymize them by replacing them with `servern` where `n`
represents the position of the server in the pool.

This is done by building a `host anonymizer` map that maps every
possible value containing the host e.g. host, host:port,
http://host:port, etc to the corresponding anonymized name and using
this map to replace the values at the time of health report generation.

A different logic is used to anonymize host names in the `procinfo`
data, as the host names are part of an ellipses pattern in the process
start command. Here we just replace the prefix/suffix of the ellipses
pattern with their hashes.
2021-07-14 00:23:22 -07:00
Harshavardhana 8d19efe7e0 feat: increase allowed maximum STS expiration timeout to 365 days (#12704) 2021-07-14 00:08:22 -07:00
ebozduman 83c37a44b6 'starts-with' support for 'success_action_status' (#12698) 2021-07-13 13:33:03 -07:00
Harshavardhana 9dae5a7c85 fix: only redirect for GET/HEAD requests (#12702)
fixes #12701
2021-07-13 11:25:08 -07:00
Anis Elleuch aa78505181 Add prefixes usage in Accounting Usage Info (#12687) 2021-07-13 10:42:11 -07:00
Harshavardhana e316873f84 feat: Add support for kakfa audit logger target (#12678) 2021-07-13 09:39:13 -07:00
Harshavardhana 559d075627 fix: properly send group unset policy (#12694)
In distributed setups `mc admin group unset`
was not correctly working due to incorrect
handling of query parameters between peers.
2021-07-13 09:32:35 -07:00
Harshavardhana e25ae1c01f add console-address flag for spoonfed users 2021-07-12 17:05:38 -07:00
Harshavardhana a655e7f820 fix: use browser redirect URL for IDP callback (#12689)
if browser_redirect_url is set use that for IDP callback
automatically, if we do not have to set REDIRECT_URI
for OpenID callback URL.
2021-07-12 15:21:07 -07:00
Harshavardhana c438eda624 update console dependency for derivedKey fix 2021-07-12 14:37:25 -07:00
Harshavardhana 2b6d0a1d84 fix: typo in REDIRECT_URL 2021-07-12 11:43:42 -07:00
Klaus Post 3201bae773 feat: gzip S3/Admin API responses whenever applicable (#12631)
Gzip responses if appropriate, except GetObject requests.
List reponses has an almost 10:1 compression ratio with no
measurable slowdown (in fact it seems a bit faster).
2021-07-12 10:49:50 -07:00
Anis Elleuch 8625cfdfae update madmin-go dependency, update related code (#12669) 2021-07-12 10:16:10 -07:00
Minio Trusted bc8962102f Update yaml files to latest version RELEASE.2021-07-12T02-44-53Z 2021-07-12 03:45:14 +00:00
Harshavardhana 55b08403cb update docker docs with --console-address
also update the orchestration docs for nginx
proxy for console.

fixes #12660
2021-07-11 19:44:53 -07:00
Harshavardhana d86ef6256d fix: ldap:username variable substitution in policies 2021-07-11 18:39:06 -07:00
Harshavardhana cd36019450 fix: allow STS credentials with dynamic policies (#12681)
- ParentUser for OIDC auth changed to `openid:`
  instead of `jwt:` to avoid clashes with variable
  substitution

- Do not pass in random parents into IsAllowed()
  policy evaluation as it can change the behavior
  of looking for correct policies underneath.

fixes #12676
fixes #12680
2021-07-11 17:39:52 -07:00
Minio Trusted e4c3953947 update console dependency 2021-07-10 12:16:07 -07:00
Harshavardhana 931f73f59a fix: add IAM dummy store for gateway operations (#12670)
with console addition users cannot login with
root credentials without etcd persistent layer,
allow a dummy store such that such functionalities
can be supported when running as non-persistent
manner, this enables all calls and operations.
2021-07-10 08:32:52 -07:00
Anis Elleuch 9be040dd14 Add option in readDir to enable symlink following of dirs (#12668) 2021-07-09 16:20:51 -07:00
Harshavardhana da0fd5f056 fix: allow customizing console redirection (#12665)
MinIO might be running inside proxies, and
console while being on another port might not be
reachable on a specific port behind such proxies.

For such scenarios customize the redirect URL
such that console can be redirected to correct
proxy endpoint instead.

fixes #12661
2021-07-09 14:27:09 -07:00
Anis Elleuch c8cf4c5eb8 Fix force bucket deletion in distributed mode (#12659)
storageRESTForceDelete is not always passed by client, use r.URL().Query
instead.

Co-authored-by: Anis Elleuch <anis@min.io>
2021-07-09 13:29:38 -07:00
Harshavardhana bab72f6887 fix: allow authToken for webhook to support Splunk (#12663) 2021-07-09 11:47:04 -07:00
Klaus Post d6a2fe02d3 Add admin file inspector (#12635)
Download files from *any* bucket/path as an encrypted zip file.

The key is included in the response but can be separated so zip 
and the key doesn't have to be sent on the same channel.

Requires https://github.com/minio/pkg/pull/6
2021-07-09 11:29:16 -07:00
Harshavardhana 28adb29db3 feat: Add support to poll users on external SSO (#12592)
Additional support for vendor-specific admin API
integrations for OpenID, to ensure validity of
credentials on MinIO.

Every 5minutes check for validity of credentials
on MinIO with vendor specific IDP.
2021-07-09 11:17:21 -07:00
Harshavardhana b79cdc1611 fix: allow gateway to work with root credentials (#12655) 2021-07-09 10:35:09 -07:00
Minio Trusted b6dd9b55a7 Update yaml files to latest version RELEASE.2021-07-08T19-43-25Z 2021-07-08 22:53:49 -07:00
273 changed files with 7106 additions and 3498 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
go-version: [1.16.x]
go-version: [1.16.x, 1.17.x]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v2
+3 -2
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
go-version: [1.16.x]
go-version: [1.16.x, 1.17.x]
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v2
@@ -32,10 +32,11 @@ jobs:
CGO_ENABLED: 0
GO111MODULE: on
run: |
sudo apt install jq -y
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
nancy_version=$(curl --retry 10 -Ls -o /dev/null -w "%{url_effective}" https://github.com/sonatype-nexus-community/nancy/releases/latest | sed "s/https:\/\/github.com\/sonatype-nexus-community\/nancy\/releases\/tag\///")
curl -L -o nancy https://github.com/sonatype-nexus-community/nancy/releases/download/${nancy_version}/nancy-${nancy_version}-linux-amd64 && chmod +x nancy
go list -m all | ./nancy sleuth
go list -deps -json ./... | jq -s 'unique_by(.Module.Path)|.[]|select(has("Module"))|.Module' | ./nancy sleuth
make
make test-race
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
go-version: [1.16.x]
go-version: [1.16.x, 1.17.x]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v2
+1 -1
View File
@@ -56,7 +56,7 @@ $ git push origin my-new-feature
Pull requests can be created via GitHub. Refer to [this document](https://help.github.com/articles/creating-a-pull-request/) for detailed steps on how to create a pull request. After a Pull Request gets peer reviewed and approved, it will be merged.
## FAQs
### How does ``MinIO`` manages dependencies?
### How does ``MinIO`` manage dependencies?
``MinIO`` uses `go mod` to manage its dependencies.
- Run `go get foo/bar` in the source folder to add the dependency to `go.mod` file.
+2 -1
View File
@@ -17,7 +17,8 @@ ENV MINIO_ACCESS_KEY_FILE=access_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_UPDATE_MINISIGN_PUBKEY="RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav" \
MINIO_CONFIG_ENV_FILE=config.env
COPY dockerscripts/verify-minio.sh /usr/bin/verify-minio.sh
COPY dockerscripts/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
+2 -1
View File
@@ -17,7 +17,8 @@ ENV MINIO_ACCESS_KEY_FILE=access_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_UPDATE_MINISIGN_PUBKEY="RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav" \
MINIO_CONFIG_ENV_FILE=config.env
COPY dockerscripts/verify-minio.sh /usr/bin/verify-minio.sh
COPY dockerscripts/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
+18 -2
View File
@@ -21,7 +21,10 @@ for more complete documentation.
Run the following command to run the latest stable image of MinIO as a container using an ephemeral data volume:
```sh
podman run -p 9000:9000 minio/minio server /data
podman run \
-p 9000:9000 \
-p 9001:9001 \
minio/minio server /data --console-address ":9001"
```
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Console, an embedded
@@ -206,6 +209,18 @@ MinIO Server comes with an embedded web based object browser. Point your web bro
> NOTE: MinIO runs console on random port by default if you wish choose a specific port use `--console-address` to pick a specific interface and port.
### Things to consider
MinIO redirects browser access requests to the configured server port (i.e. `127.0.0.1:9000`) to the configured Console port. MinIO uses the hostname or IP address specified in the request when building the redirect URL. The URL and port *must* be accessible by the client for the redirection to work.
For deployments behind a load balancer, proxy, or ingress rule where the MinIO host IP address or port is not public, use the `MINIO_BROWSER_REDIRECT_URL` environment variable to specify the external hostname for the redirect. The LB/Proxy must have rules for directing traffic to the Console port specifically.
For example, consider a MinIO deployment behind a proxy `https://minio.example.net`, `https://console.minio.example.net` with rules for forwarding traffic on port :9000 and :9001 to MinIO and the MinIO Console respectively on the internal network. Set `MINIO_BROWSER_REDIRECT_URL` to `https://console.minio.example.net` to ensure the browser receives a valid reachable URL.
Similarly, if your TLS certificates do not have the IP SAN for the MinIO server host, the MinIO Console may fail to validate the connection to the server. Use the `MINIO_SERVER_URL` environment variable and specify the proxy-accessible hostname of the MinIO server to allow the Console to use the MinIO server API using the TLS certificate.
For example: `export MINIO_SERVER_URL="https://minio.example.net"`
| Dashboard | Creating a bucket |
| ------------- | ------------- |
| ![Dashboard](https://github.com/minio/minio/blob/master/docs/screenshots/pic1.png?raw=true) | ![Dashboard](https://github.com/minio/minio/blob/master/docs/screenshots/pic2.png?raw=true) |
@@ -244,4 +259,5 @@ mc admin update <minio alias, e.g., myminio>
Please follow MinIO [Contributor's Guide](https://github.com/minio/minio/blob/master/CONTRIBUTING.md)
# License
Use of MinIO is governed by the GNU AGPLv3 license that can be found in the [LICENSE](https://github.com/minio/minio/blob/master/LICENSE) file.
MinIO source is licensed under the GNU AGPLv3 license that can be found in the [LICENSE](https://github.com/minio/minio/blob/master/LICENSE) file.
MinIO [Documentation](https://github.com/minio/minio/tree/master/docs) © 2021 by MinIO, Inc is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
+1
View File
@@ -1,3 +1,4 @@
//go:build ignore
// +build ignore
// Copyright (c) 2015-2021 MinIO, Inc.
+6 -6
View File
@@ -85,7 +85,7 @@ func (a adminAPIHandlers) GetBucketQuotaConfigHandler(w http.ResponseWriter, r *
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.GetBucketQuotaAdminAction)
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.GetBucketQuotaAdminAction)
if objectAPI == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return
@@ -122,7 +122,7 @@ func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http.
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
vars := mux.Vars(r)
bucket := pathClean(vars["bucket"])
update := r.URL.Query().Get("update") == "true"
update := r.Form.Get("update") == "true"
if !globalIsErasure {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
@@ -130,7 +130,7 @@ func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http.
}
// Get current object layer instance.
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.SetBucketTargetAction)
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SetBucketTargetAction)
if objectAPI == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return
@@ -169,7 +169,7 @@ func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http.
target.SourceBucket = bucket
var ops []madmin.TargetUpdateType
if update {
ops = madmin.GetTargetUpdateOps(r.URL.Query())
ops = madmin.GetTargetUpdateOps(r.Form)
} else {
target.Arn = globalBucketTargetSys.getRemoteARN(bucket, &target)
}
@@ -258,7 +258,7 @@ func (a adminAPIHandlers) ListRemoteTargetsHandler(w http.ResponseWriter, r *htt
return
}
// Get current object layer instance.
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.GetBucketTargetAction)
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.GetBucketTargetAction)
if objectAPI == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return
@@ -298,7 +298,7 @@ func (a adminAPIHandlers) RemoveRemoteTargetHandler(w http.ResponseWriter, r *ht
return
}
// Get current object layer instance.
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.SetBucketTargetAction)
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SetBucketTargetAction)
if objectAPI == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return
+172
View File
@@ -0,0 +1,172 @@
// Copyright (c) 2015-2021 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package cmd
import (
"context"
"errors"
"net/http"
"github.com/minio/kes"
"github.com/minio/madmin-go"
"github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/config"
iampolicy "github.com/minio/pkg/iam/policy"
)
func validateAdminReq(ctx context.Context, w http.ResponseWriter, r *http.Request, action iampolicy.AdminAction) (ObjectLayer, auth.Credentials) {
// Get current object layer instance.
objectAPI := newObjectLayerFn()
if objectAPI == nil || globalNotificationSys == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return nil, auth.Credentials{}
}
// Validate request signature.
cred, adminAPIErr := checkAdminRequestAuth(ctx, r, action, "")
if adminAPIErr != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
return nil, cred
}
return objectAPI, cred
}
// AdminError - is a generic error for all admin APIs.
type AdminError struct {
Code string
Message string
StatusCode int
}
func (ae AdminError) Error() string {
return ae.Message
}
func toAdminAPIErr(ctx context.Context, err error) APIError {
if err == nil {
return noError
}
var apiErr APIError
switch e := err.(type) {
case iampolicy.Error:
apiErr = APIError{
Code: "XMinioMalformedIAMPolicy",
Description: e.Error(),
HTTPStatusCode: http.StatusBadRequest,
}
case config.Error:
apiErr = APIError{
Code: "XMinioConfigError",
Description: e.Error(),
HTTPStatusCode: http.StatusBadRequest,
}
case AdminError:
apiErr = APIError{
Code: e.Code,
Description: e.Message,
HTTPStatusCode: e.StatusCode,
}
default:
switch {
case errors.Is(err, errConfigNotFound):
apiErr = APIError{
Code: "XMinioConfigError",
Description: err.Error(),
HTTPStatusCode: http.StatusNotFound,
}
case errors.Is(err, errIAMActionNotAllowed):
apiErr = APIError{
Code: "XMinioIAMActionNotAllowed",
Description: err.Error(),
HTTPStatusCode: http.StatusForbidden,
}
case errors.Is(err, errIAMNotInitialized):
apiErr = APIError{
Code: "XMinioIAMNotInitialized",
Description: err.Error(),
HTTPStatusCode: http.StatusServiceUnavailable,
}
case errors.Is(err, kes.ErrKeyExists):
apiErr = APIError{
Code: "XMinioKMSKeyExists",
Description: err.Error(),
HTTPStatusCode: http.StatusConflict,
}
// Tier admin API errors
case errors.Is(err, madmin.ErrTierNameEmpty):
apiErr = APIError{
Code: "XMinioAdminTierNameEmpty",
Description: err.Error(),
HTTPStatusCode: http.StatusBadRequest,
}
case errors.Is(err, madmin.ErrTierInvalidConfig):
apiErr = APIError{
Code: "XMinioAdminTierInvalidConfig",
Description: err.Error(),
HTTPStatusCode: http.StatusBadRequest,
}
case errors.Is(err, madmin.ErrTierInvalidConfigVersion):
apiErr = APIError{
Code: "XMinioAdminTierInvalidConfigVersion",
Description: err.Error(),
HTTPStatusCode: http.StatusBadRequest,
}
case errors.Is(err, madmin.ErrTierTypeUnsupported):
apiErr = APIError{
Code: "XMinioAdminTierTypeUnsupported",
Description: err.Error(),
HTTPStatusCode: http.StatusBadRequest,
}
case errors.Is(err, errTierBackendInUse):
apiErr = APIError{
Code: "XMinioAdminTierBackendInUse",
Description: err.Error(),
HTTPStatusCode: http.StatusConflict,
}
case errors.Is(err, errTierInsufficientCreds):
apiErr = APIError{
Code: "XMinioAdminTierInsufficientCreds",
Description: err.Error(),
HTTPStatusCode: http.StatusBadRequest,
}
case errIsTierPermError(err):
apiErr = APIError{
Code: "XMinioAdminTierInsufficientPermissions",
Description: err.Error(),
HTTPStatusCode: http.StatusBadRequest,
}
default:
apiErr = errorCodes.ToAPIErrWithErr(toAdminAPIErrCode(ctx, err), err)
}
}
return apiErr
}
// toAdminAPIErrCode - converts errErasureWriteQuorum error to admin API
// specific error.
func toAdminAPIErrCode(ctx context.Context, err error) APIErrorCode {
switch err {
case errErasureWriteQuorum:
return ErrAdminConfigNoQuorum
default:
return toAPIErrorCode(ctx, err)
}
}
+10 -30
View File
@@ -19,7 +19,6 @@ package cmd
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
@@ -28,7 +27,6 @@ import (
"github.com/gorilla/mux"
"github.com/minio/madmin-go"
"github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/config"
"github.com/minio/minio/internal/config/cache"
"github.com/minio/minio/internal/config/etcd"
@@ -40,31 +38,13 @@ import (
iampolicy "github.com/minio/pkg/iam/policy"
)
func validateAdminReqConfigKV(ctx context.Context, w http.ResponseWriter, r *http.Request) (auth.Credentials, ObjectLayer) {
// Get current object layer instance.
objectAPI := newObjectLayerFn()
if objectAPI == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return auth.Credentials{}, nil
}
// Validate request signature.
cred, adminAPIErr := checkAdminRequestAuth(ctx, r, iampolicy.ConfigUpdateAdminAction, "")
if adminAPIErr != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
return cred, nil
}
return cred, objectAPI
}
// DelConfigKVHandler - DELETE /minio/admin/v3/del-config-kv
func (a adminAPIHandlers) DelConfigKVHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "DeleteConfigKV")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
cred, objectAPI := validateAdminReqConfigKV(ctx, w, r)
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction)
if objectAPI == nil {
return
}
@@ -106,7 +86,7 @@ func (a adminAPIHandlers) SetConfigKVHandler(w http.ResponseWriter, r *http.Requ
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
cred, objectAPI := validateAdminReqConfigKV(ctx, w, r)
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction)
if objectAPI == nil {
return
}
@@ -173,7 +153,7 @@ func (a adminAPIHandlers) GetConfigKVHandler(w http.ResponseWriter, r *http.Requ
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
cred, objectAPI := validateAdminReqConfigKV(ctx, w, r)
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction)
if objectAPI == nil {
return
}
@@ -202,7 +182,7 @@ func (a adminAPIHandlers) ClearConfigHistoryKVHandler(w http.ResponseWriter, r *
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
_, objectAPI := validateAdminReqConfigKV(ctx, w, r)
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction)
if objectAPI == nil {
return
}
@@ -239,7 +219,7 @@ func (a adminAPIHandlers) RestoreConfigHistoryKVHandler(w http.ResponseWriter, r
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
_, objectAPI := validateAdminReqConfigKV(ctx, w, r)
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction)
if objectAPI == nil {
return
}
@@ -287,7 +267,7 @@ func (a adminAPIHandlers) ListConfigHistoryKVHandler(w http.ResponseWriter, r *h
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
cred, objectAPI := validateAdminReqConfigKV(ctx, w, r)
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction)
if objectAPI == nil {
return
}
@@ -327,7 +307,7 @@ func (a adminAPIHandlers) HelpConfigKVHandler(w http.ResponseWriter, r *http.Req
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
_, objectAPI := validateAdminReqConfigKV(ctx, w, r)
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction)
if objectAPI == nil {
return
}
@@ -337,7 +317,7 @@ func (a adminAPIHandlers) HelpConfigKVHandler(w http.ResponseWriter, r *http.Req
subSys := vars["subSys"]
key := vars["key"]
_, envOnly := r.URL.Query()["env"]
_, envOnly := r.Form["env"]
rd, err := GetHelp(subSys, key, envOnly)
if err != nil {
@@ -355,7 +335,7 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
cred, objectAPI := validateAdminReqConfigKV(ctx, w, r)
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction)
if objectAPI == nil {
return
}
@@ -407,7 +387,7 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
cred, objectAPI := validateAdminReqConfigKV(ctx, w, r)
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction)
if objectAPI == nil {
return
}
+57 -55
View File
@@ -19,7 +19,6 @@ package cmd
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
@@ -29,40 +28,18 @@ import (
"github.com/gorilla/mux"
"github.com/minio/madmin-go"
"github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/config/dns"
"github.com/minio/minio/internal/logger"
iampolicy "github.com/minio/pkg/iam/policy"
)
func validateAdminUsersReq(ctx context.Context, w http.ResponseWriter, r *http.Request, action iampolicy.AdminAction) (ObjectLayer, auth.Credentials) {
var cred auth.Credentials
var adminAPIErr APIErrorCode
// Get current object layer instance.
objectAPI := newObjectLayerFn()
if objectAPI == nil || globalNotificationSys == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return nil, cred
}
// Validate request signature.
cred, adminAPIErr = checkAdminRequestAuth(ctx, r, action, "")
if adminAPIErr != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
return nil, cred
}
return objectAPI, cred
}
// RemoveUser - DELETE /minio/admin/v3/remove-user?accessKey=<access_key>
func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "RemoveUser")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.DeleteUserAdminAction)
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.DeleteUserAdminAction)
if objectAPI == nil {
return
}
@@ -100,7 +77,7 @@ func (a adminAPIHandlers) ListBucketUsers(w http.ResponseWriter, r *http.Request
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, cred := validateAdminUsersReq(ctx, w, r, iampolicy.ListUsersAdminAction)
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ListUsersAdminAction)
if objectAPI == nil {
return
}
@@ -136,7 +113,7 @@ func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) {
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, cred := validateAdminUsersReq(ctx, w, r, iampolicy.ListUsersAdminAction)
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ListUsersAdminAction)
if objectAPI == nil {
return
}
@@ -234,7 +211,7 @@ func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Requ
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.AddUserToGroupAdminAction)
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.AddUserToGroupAdminAction)
if objectAPI == nil {
return
}
@@ -279,7 +256,7 @@ func (a adminAPIHandlers) GetGroup(w http.ResponseWriter, r *http.Request) {
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.GetGroupAdminAction)
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.GetGroupAdminAction)
if objectAPI == nil {
return
}
@@ -308,7 +285,7 @@ func (a adminAPIHandlers) ListGroups(w http.ResponseWriter, r *http.Request) {
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.ListGroupsAdminAction)
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ListGroupsAdminAction)
if objectAPI == nil {
return
}
@@ -334,7 +311,7 @@ func (a adminAPIHandlers) SetGroupStatus(w http.ResponseWriter, r *http.Request)
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.EnableGroupAdminAction)
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.EnableGroupAdminAction)
if objectAPI == nil {
return
}
@@ -371,7 +348,7 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request)
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.EnableUserAdminAction)
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.EnableUserAdminAction)
if objectAPI == nil {
return
}
@@ -562,10 +539,12 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
}
}
var ldapUsername string
if globalLDAPConfig.Enabled && targetUser != "" {
// If LDAP enabled, service accounts need
// to be created only for LDAP users.
var err error
ldapUsername = targetUser
targetUser, targetGroups, err = globalLDAPConfig.LookupUserDN(targetUser)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
@@ -604,6 +583,9 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
secretKey: createReq.SecretKey,
sessionPolicy: sp,
}
if ldapUsername != "" {
opts.ldapUsername = ldapUsername
}
newCred, err := globalIAMSys.NewServiceAccount(ctx, targetUser, targetGroups, opts)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
@@ -846,7 +828,7 @@ func (a adminAPIHandlers) ListServiceAccounts(w http.ResponseWriter, r *http.Req
var targetAccount string
user := r.URL.Query().Get("user")
user := r.Form.Get("user")
if user != "" {
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey,
@@ -956,6 +938,13 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re
return
}
for _, nerr := range globalNotificationSys.DeleteServiceAccount(serviceAccount) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
writeSuccessNoContent(w)
}
@@ -984,18 +973,16 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
// Set delimiter value for "s3:delimiter" policy conditionals.
r.Header.Set("delimiter", SlashSeparator)
parentUser := cred.AccessKey
if cred.ParentUser != "" {
parentUser = cred.ParentUser
}
// Check if we are asked to return prefix usage
enablePrefixUsage := r.Form.Get("prefix-usage") == "true"
isAllowedAccess := func(bucketName string) (rd, wr bool) {
if globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: parentUser,
AccountName: cred.AccessKey,
Groups: cred.Groups,
Action: iampolicy.ListBucketAction,
BucketName: bucketName,
ConditionValues: getConditionValues(r, "", parentUser, claims),
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
IsOwner: owner,
ObjectName: "",
Claims: claims,
@@ -1004,11 +991,11 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
}
if globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: parentUser,
AccountName: cred.AccessKey,
Groups: cred.Groups,
Action: iampolicy.PutObjectAction,
BucketName: bucketName,
ConditionValues: getConditionValues(r, "", parentUser, claims),
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
IsOwner: owner,
ObjectName: "",
Claims: claims,
@@ -1019,11 +1006,15 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
return rd, wr
}
// Load the latest calculated data usage
dataUsageInfo, err := loadDataUsageFromBackend(ctx, objectAPI)
if err != nil {
// log the error, continue with the accounting response
logger.LogIf(ctx, err)
var dataUsageInfo madmin.DataUsageInfo
var err error
if !globalIsGateway {
// Load the latest calculated data usage
dataUsageInfo, err = loadDataUsageFromBackend(ctx, objectAPI)
if err != nil {
// log the error, continue with the accounting response
logger.LogIf(ctx, err)
}
}
// If etcd, dns federation configured list buckets from etcd.
@@ -1081,21 +1072,32 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
acctInfo := madmin.AccountInfo{
AccountName: accountName,
Server: objectAPI.BackendInfo(),
Policy: buf,
}
for _, bucket := range buckets {
rd, wr := isAllowedAccess(bucket.Name)
if rd || wr {
var size uint64
// Fetch the data usage of the current bucket
var size uint64
if !dataUsageInfo.LastUpdate.IsZero() {
size = dataUsageInfo.BucketsUsage[bucket.Name].Size
}
// Fetch the prefix usage of the current bucket
var prefixUsage map[string]uint64
if enablePrefixUsage {
if pu, err := loadPrefixUsageFromBackend(ctx, objectAPI, bucket.Name); err == nil {
prefixUsage = pu
} else {
logger.LogIf(ctx, err)
}
}
acctInfo.Buckets = append(acctInfo.Buckets, madmin.BucketAccessInfo{
Name: bucket.Name,
Created: bucket.Created,
Size: size,
Name: bucket.Name,
Created: bucket.Created,
Size: size,
PrefixUsage: prefixUsage,
Access: madmin.AccountAccess{
Read: rd,
Write: wr,
@@ -1119,7 +1121,7 @@ func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Reques
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.GetPolicyAdminAction)
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.GetPolicyAdminAction)
if objectAPI == nil {
return
}
@@ -1144,7 +1146,7 @@ func (a adminAPIHandlers) ListBucketPolicies(w http.ResponseWriter, r *http.Requ
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.ListUserPoliciesAdminAction)
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ListUserPoliciesAdminAction)
if objectAPI == nil {
return
}
@@ -1180,7 +1182,7 @@ func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Requ
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.ListUserPoliciesAdminAction)
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ListUserPoliciesAdminAction)
if objectAPI == nil {
return
}
@@ -1214,7 +1216,7 @@ func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Requ
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.DeletePolicyAdminAction)
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.DeletePolicyAdminAction)
if objectAPI == nil {
return
}
@@ -1242,7 +1244,7 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.CreatePolicyAdminAction)
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.CreatePolicyAdminAction)
if objectAPI == nil {
return
}
@@ -1294,7 +1296,7 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.AttachPolicyAdminAction)
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.AttachPolicyAdminAction)
if objectAPI == nil {
return
}
+492 -183
View File
@@ -19,28 +19,29 @@ package cmd
import (
"context"
crand "crypto/rand"
"crypto/subtle"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"hash/crc32"
"io"
"math/rand"
"net/http"
"net/url"
"os"
"path"
"regexp"
"runtime"
"sort"
"strconv"
"strings"
"time"
humanize "github.com/dustin/go-humanize"
"github.com/gorilla/mux"
"github.com/minio/kes"
"github.com/klauspost/compress/zip"
"github.com/minio/madmin-go"
"github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/config"
"github.com/minio/minio/internal/dsync"
"github.com/minio/minio/internal/handlers"
xhttp "github.com/minio/minio/internal/http"
@@ -49,6 +50,7 @@ import (
"github.com/minio/minio/internal/logger/message/log"
iampolicy "github.com/minio/pkg/iam/policy"
xnet "github.com/minio/pkg/net"
"github.com/secure-io/sio-go"
)
const (
@@ -444,7 +446,7 @@ func (a adminAPIHandlers) TopLocksHandler(w http.ResponseWriter, r *http.Request
}
count := 10 // by default list only top 10 entries
if countStr := r.URL.Query().Get("count"); countStr != "" {
if countStr := r.Form.Get("count"); countStr != "" {
var err error
count, err = strconv.Atoi(countStr)
if err != nil {
@@ -452,7 +454,7 @@ func (a adminAPIHandlers) TopLocksHandler(w http.ResponseWriter, r *http.Request
return
}
}
stale := r.URL.Query().Get("stale") == "true" // list also stale locks
stale := r.Form.Get("stale") == "true" // list also stale locks
peerLocks := globalNotificationSys.GetLocks(ctx, r)
@@ -707,7 +709,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
return
}
hip, errCode := extractHealInitParams(mux.Vars(r), r.URL.Query(), r.Body)
hip, errCode := extractHealInitParams(mux.Vars(r), r.Form, r.Body)
if errCode != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(errCode), r.URL)
return
@@ -903,41 +905,54 @@ func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r *
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
}
func (a adminAPIHandlers) SpeedtestHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "SpeedtestHandler")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealAdminAction)
if objectAPI == nil {
return
}
if !globalIsErasure {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
sizeStr := r.Form.Get(peerRESTSize)
durationStr := r.Form.Get(peerRESTDuration)
concurrentStr := r.Form.Get(peerRESTConcurrent)
size, err := strconv.Atoi(sizeStr)
if err != nil {
size = 64 * humanize.MiByte
}
concurrent, err := strconv.Atoi(concurrentStr)
if err != nil {
concurrent = 32
}
duration, err := time.ParseDuration(durationStr)
if err != nil {
duration = time.Second * 10
}
results := globalNotificationSys.Speedtest(ctx, size, concurrent, duration)
if err := json.NewEncoder(w).Encode(results); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
objectAPI.DeleteBucket(ctx, pathJoin(minioMetaSpeedTestBucket, minioMetaSpeedTestBucketPrefix), true)
w.(http.Flusher).Flush()
}
func validateAdminReq(ctx context.Context, w http.ResponseWriter, r *http.Request, action iampolicy.AdminAction) (ObjectLayer, auth.Credentials) {
var cred auth.Credentials
var adminAPIErr APIErrorCode
// Get current object layer instance.
objectAPI := newObjectLayerFn()
if objectAPI == nil || globalNotificationSys == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return nil, cred
}
// Validate request signature.
cred, adminAPIErr = checkAdminRequestAuth(ctx, r, action, "")
if adminAPIErr != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
return nil, cred
}
return objectAPI, cred
}
// AdminError - is a generic error for all admin APIs.
type AdminError struct {
Code string
Message string
StatusCode int
}
func (ae AdminError) Error() string {
return ae.Message
}
// Admin API errors
const (
AdminUpdateUnexpectedFailure = "XMinioAdminUpdateUnexpectedFailure"
@@ -945,119 +960,6 @@ const (
AdminUpdateApplyFailure = "XMinioAdminUpdateApplyFailure"
)
// toAdminAPIErrCode - converts errErasureWriteQuorum error to admin API
// specific error.
func toAdminAPIErrCode(ctx context.Context, err error) APIErrorCode {
switch err {
case errErasureWriteQuorum:
return ErrAdminConfigNoQuorum
default:
return toAPIErrorCode(ctx, err)
}
}
func toAdminAPIErr(ctx context.Context, err error) APIError {
if err == nil {
return noError
}
var apiErr APIError
switch e := err.(type) {
case iampolicy.Error:
apiErr = APIError{
Code: "XMinioMalformedIAMPolicy",
Description: e.Error(),
HTTPStatusCode: http.StatusBadRequest,
}
case config.Error:
apiErr = APIError{
Code: "XMinioConfigError",
Description: e.Error(),
HTTPStatusCode: http.StatusBadRequest,
}
case AdminError:
apiErr = APIError{
Code: e.Code,
Description: e.Message,
HTTPStatusCode: e.StatusCode,
}
default:
switch {
case errors.Is(err, errConfigNotFound):
apiErr = APIError{
Code: "XMinioConfigError",
Description: err.Error(),
HTTPStatusCode: http.StatusNotFound,
}
case errors.Is(err, errIAMActionNotAllowed):
apiErr = APIError{
Code: "XMinioIAMActionNotAllowed",
Description: err.Error(),
HTTPStatusCode: http.StatusForbidden,
}
case errors.Is(err, errIAMNotInitialized):
apiErr = APIError{
Code: "XMinioIAMNotInitialized",
Description: err.Error(),
HTTPStatusCode: http.StatusServiceUnavailable,
}
case errors.Is(err, kes.ErrKeyExists):
apiErr = APIError{
Code: "XMinioKMSKeyExists",
Description: err.Error(),
HTTPStatusCode: http.StatusConflict,
}
// Tier admin API errors
case errors.Is(err, madmin.ErrTierNameEmpty):
apiErr = APIError{
Code: "XMinioAdminTierNameEmpty",
Description: err.Error(),
HTTPStatusCode: http.StatusBadRequest,
}
case errors.Is(err, madmin.ErrTierInvalidConfig):
apiErr = APIError{
Code: "XMinioAdminTierInvalidConfig",
Description: err.Error(),
HTTPStatusCode: http.StatusBadRequest,
}
case errors.Is(err, madmin.ErrTierInvalidConfigVersion):
apiErr = APIError{
Code: "XMinioAdminTierInvalidConfigVersion",
Description: err.Error(),
HTTPStatusCode: http.StatusBadRequest,
}
case errors.Is(err, madmin.ErrTierTypeUnsupported):
apiErr = APIError{
Code: "XMinioAdminTierTypeUnsupported",
Description: err.Error(),
HTTPStatusCode: http.StatusBadRequest,
}
case errors.Is(err, errTierBackendInUse):
apiErr = APIError{
Code: "XMinioAdminTierBackendInUse",
Description: err.Error(),
HTTPStatusCode: http.StatusConflict,
}
case errors.Is(err, errTierInsufficientCreds):
apiErr = APIError{
Code: "XMinioAdminTierInsufficientCreds",
Description: err.Error(),
HTTPStatusCode: http.StatusBadRequest,
}
case errIsTierPermError(err):
apiErr = APIError{
Code: "XMinioAdminTierInsufficientPermissions",
Description: err.Error(),
HTTPStatusCode: http.StatusBadRequest,
}
default:
apiErr = errorCodes.ToAPIErrWithErr(toAdminAPIErrCode(ctx, err), err)
}
}
return apiErr
}
// Returns true if the madmin.TraceInfo should be traced,
// false if certain conditions are not met.
// - input entry is not of the type *madmin.TraceInfo*
@@ -1107,7 +1009,7 @@ func mustTrace(entry interface{}, opts madmin.ServiceTraceOpts) (shouldTrace boo
}
func extractTraceOptions(r *http.Request) (opts madmin.ServiceTraceOpts, err error) {
q := r.URL.Query()
q := r.Form
opts.OnlyErrors = q.Get("err") == "true"
opts.S3 = q.Get("s3") == "true"
@@ -1181,8 +1083,14 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
if err := enc.Encode(entry); err != nil {
return
}
w.(http.Flusher).Flush()
if len(traceCh) == 0 {
// Flush if nothing is queued
w.(http.Flusher).Flush()
}
case <-keepAliveTicker.C:
if len(traceCh) > 0 {
continue
}
if _, err := w.Write([]byte(" ")); err != nil {
return
}
@@ -1203,15 +1111,15 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque
if objectAPI == nil {
return
}
node := r.URL.Query().Get("node")
node := r.Form.Get("node")
// limit buffered console entries if client requested it.
limitStr := r.URL.Query().Get("limit")
limitStr := r.Form.Get("limit")
limitLines, err := strconv.Atoi(limitStr)
if err != nil {
limitLines = 10
}
logKind := r.URL.Query().Get("logType")
logKind := r.Form.Get("logType")
if logKind == "" {
logKind = string(logger.All)
}
@@ -1252,9 +1160,15 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque
if err := enc.Encode(log); err != nil {
return
}
}
if len(logCh) == 0 {
// Flush if nothing is queued
w.(http.Flusher).Flush()
}
case <-keepAliveTicker.C:
if len(logCh) > 0 {
continue
}
if _, err := w.Write([]byte(" ")); err != nil {
return
}
@@ -1280,7 +1194,7 @@ func (a adminAPIHandlers) KMSCreateKeyHandler(w http.ResponseWriter, r *http.Req
return
}
if err := GlobalKMS.CreateKey(r.URL.Query().Get("key-id")); err != nil {
if err := GlobalKMS.CreateKey(r.Form.Get("key-id")); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
@@ -1347,7 +1261,7 @@ func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Req
return
}
keyID := r.URL.Query().Get("key-id")
keyID := r.Form.Get("key-id")
if keyID == "" {
keyID = stat.DefaultKey
}
@@ -1514,7 +1428,7 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
return
}
query := r.URL.Query()
query := r.Form
healthInfo := madmin.HealthInfo{Version: madmin.HealthInfoVersion}
healthInfoCh := make(chan madmin.HealthInfo)
@@ -1538,7 +1452,7 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
}
deadline := 1 * time.Hour
if dstr := r.URL.Query().Get("deadline"); dstr != "" {
if dstr := r.Form.Get("deadline"); dstr != "" {
var err error
deadline, err = time.ParseDuration(dstr)
if err != nil {
@@ -1558,41 +1472,196 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
}
defer nsLock.Unlock(lkctx.Cancel)
go func() {
defer close(healthInfoCh)
hostAnonymizer := createHostAnonymizer()
// anonAddr - Anonymizes hosts in given input string.
anonAddr := func(addr string) string {
newAddr, found := hostAnonymizer[addr]
if found {
return newAddr
}
partialWrite(healthInfo) // Write first message with only version populated
// If we reach here, it means that the given addr doesn't contain any of the hosts.
// Return it as is. Can happen for drive paths in non-distributed mode
return addr
}
// anonymizedAddr - Updated the addr of the node info with anonymized one
anonymizeAddr := func(info madmin.NodeInfo) {
info.SetAddr(anonAddr(info.GetAddr()))
}
getAndWriteCPUs := func() {
if query.Get("syscpu") == "true" {
healthInfo.Sys.CPUInfo = append(healthInfo.Sys.CPUInfo, madmin.GetCPUs(deadlinedCtx, r.Host))
healthInfo.Sys.CPUInfo = append(healthInfo.Sys.CPUInfo, globalNotificationSys.GetCPUs(deadlinedCtx)...)
localCPUInfo := madmin.GetCPUs(deadlinedCtx, globalLocalNodeName)
anonymizeAddr(&localCPUInfo)
healthInfo.Sys.CPUInfo = append(healthInfo.Sys.CPUInfo, localCPUInfo)
peerCPUInfo := globalNotificationSys.GetCPUs(deadlinedCtx)
for _, cpuInfo := range peerCPUInfo {
anonymizeAddr(&cpuInfo)
healthInfo.Sys.CPUInfo = append(healthInfo.Sys.CPUInfo, cpuInfo)
}
partialWrite(healthInfo)
}
}
getAndWritePartitions := func() {
if query.Get("sysdrivehw") == "true" {
healthInfo.Sys.Partitions = append(healthInfo.Sys.Partitions, madmin.GetPartitions(deadlinedCtx, r.Host))
healthInfo.Sys.Partitions = append(healthInfo.Sys.Partitions, globalNotificationSys.GetPartitions(deadlinedCtx)...)
localPartitions := madmin.GetPartitions(deadlinedCtx, globalLocalNodeName)
anonymizeAddr(&localPartitions)
healthInfo.Sys.Partitions = append(healthInfo.Sys.Partitions, localPartitions)
peerPartitions := globalNotificationSys.GetPartitions(deadlinedCtx)
for _, p := range peerPartitions {
anonymizeAddr(&p)
healthInfo.Sys.Partitions = append(healthInfo.Sys.Partitions, p)
}
partialWrite(healthInfo)
}
}
getAndWriteOSInfo := func() {
if query.Get("sysosinfo") == "true" {
healthInfo.Sys.OSInfo = append(healthInfo.Sys.OSInfo, madmin.GetOSInfo(deadlinedCtx, r.Host))
healthInfo.Sys.OSInfo = append(healthInfo.Sys.OSInfo, globalNotificationSys.GetOSInfo(deadlinedCtx)...)
localOSInfo := madmin.GetOSInfo(deadlinedCtx, globalLocalNodeName)
anonymizeAddr(&localOSInfo)
healthInfo.Sys.OSInfo = append(healthInfo.Sys.OSInfo, localOSInfo)
peerOSInfos := globalNotificationSys.GetOSInfo(deadlinedCtx)
for _, o := range peerOSInfos {
anonymizeAddr(&o)
healthInfo.Sys.OSInfo = append(healthInfo.Sys.OSInfo, o)
}
partialWrite(healthInfo)
}
}
getAndWriteMemInfo := func() {
if query.Get("sysmem") == "true" {
healthInfo.Sys.MemInfo = append(healthInfo.Sys.MemInfo, madmin.GetMemInfo(deadlinedCtx, r.Host))
healthInfo.Sys.MemInfo = append(healthInfo.Sys.MemInfo, globalNotificationSys.GetMemInfo(deadlinedCtx)...)
localMemInfo := madmin.GetMemInfo(deadlinedCtx, globalLocalNodeName)
anonymizeAddr(&localMemInfo)
healthInfo.Sys.MemInfo = append(healthInfo.Sys.MemInfo, localMemInfo)
peerMemInfos := globalNotificationSys.GetMemInfo(deadlinedCtx)
for _, m := range peerMemInfos {
anonymizeAddr(&m)
healthInfo.Sys.MemInfo = append(healthInfo.Sys.MemInfo, m)
}
partialWrite(healthInfo)
}
}
getAndWriteSysErrors := func() {
if query.Get(string(madmin.HealthDataTypeSysErrors)) == "true" {
localSysErrors := madmin.GetSysErrors(deadlinedCtx, globalLocalNodeName)
anonymizeAddr(&localSysErrors)
healthInfo.Sys.SysErrs = append(healthInfo.Sys.SysErrs, localSysErrors)
partialWrite(healthInfo)
peerSysErrs := globalNotificationSys.GetSysErrors(deadlinedCtx)
for _, se := range peerSysErrs {
anonymizeAddr(&se)
healthInfo.Sys.SysErrs = append(healthInfo.Sys.SysErrs, se)
}
partialWrite(healthInfo)
}
}
getAndWriteSysServices := func() {
if query.Get(string(madmin.HealthDataTypeSysServices)) == "true" {
localSysServices := madmin.GetSysServices(deadlinedCtx, globalLocalNodeName)
anonymizeAddr(&localSysServices)
healthInfo.Sys.SysServices = append(healthInfo.Sys.SysServices, localSysServices)
partialWrite(healthInfo)
peerSysServices := globalNotificationSys.GetSysServices(deadlinedCtx)
for _, sli := range peerSysServices {
anonymizeAddr(&sli)
healthInfo.Sys.SysServices = append(healthInfo.Sys.SysServices, sli)
}
partialWrite(healthInfo)
}
}
anonymizeCmdLine := func(cmdLine string) string {
if !globalIsDistErasure {
// FS mode - single server - hard code to `server1`
anonCmdLine := strings.Replace(cmdLine, globalLocalNodeName, "server1", -1)
return strings.Replace(anonCmdLine, globalMinioConsoleHost, "server1", -1)
}
// Server start command regex groups:
// 1 - minio server
// 2 - flags e.g. `--address :9000 --certs-dir /etc/minio/certs`
// 3 - pool args e.g. `https://node{01...16}.domain/data/disk{001...204} https://node{17...32}.domain/data/disk{001...204}`
re := regexp.MustCompile(`^(.*minio\s+server\s+)(--[^\s]+\s+[^\s]+\s+)*(.*)`)
// stays unchanged in the anonymized version
cmdLineWithoutPools := re.ReplaceAllString(cmdLine, `$1$2`)
// to be anonymized
poolsArgs := re.ReplaceAllString(cmdLine, `$3`)
var anonPools []string
if !(strings.Contains(poolsArgs, "{") && strings.Contains(poolsArgs, "}")) {
// No ellipses pattern. Anonymize host name from every pool arg
pools := strings.Fields(poolsArgs)
anonPools = make([]string, len(pools))
for _, arg := range pools {
anonPools = append(anonPools, anonAddr(arg))
}
return cmdLineWithoutPools + strings.Join(anonPools, " ")
}
// Ellipses pattern in pool args. Regex groups:
// 1 - server prefix
// 2 - number sequence for servers
// 3 - server suffix
// 4 - drive prefix (starting with /)
// 5 - number sequence for drives
// 6 - drive suffix
re = regexp.MustCompile(`([^\s^{]*)({\d+...\d+})?([^\s^{^/]*)(/[^\s^{]*)({\d+...\d+})?([^\s]*)`)
poolsMatches := re.FindAllStringSubmatch(poolsArgs, -1)
anonPools = make([]string, len(poolsMatches))
idxMap := map[int]string{
1: "spfx",
3: "ssfx",
}
for pi, poolsMatch := range poolsMatches {
// Replace the server prefix/suffix with anonymized ones
for idx, lbl := range idxMap {
if len(poolsMatch[idx]) > 0 {
poolsMatch[idx] = fmt.Sprintf("%s%d", lbl, crc32.ChecksumIEEE([]byte(poolsMatch[idx])))
}
}
// Remove the original pools args present at index 0
anonPools[pi] = strings.Join(poolsMatch[1:], "")
}
return cmdLineWithoutPools + strings.Join(anonPools, " ")
}
anonymizeProcInfo := func(p *madmin.ProcInfo) {
p.CmdLine = anonymizeCmdLine(p.CmdLine)
anonymizeAddr(p)
}
getAndWriteProcInfo := func() {
if query.Get("sysprocess") == "true" {
healthInfo.Sys.ProcInfo = append(healthInfo.Sys.ProcInfo, madmin.GetProcInfo(deadlinedCtx, r.Host))
healthInfo.Sys.ProcInfo = append(healthInfo.Sys.ProcInfo, globalNotificationSys.GetProcInfo(deadlinedCtx)...)
localProcInfo := madmin.GetProcInfo(deadlinedCtx, globalLocalNodeName)
anonymizeProcInfo(&localProcInfo)
healthInfo.Sys.ProcInfo = append(healthInfo.Sys.ProcInfo, localProcInfo)
peerProcInfos := globalNotificationSys.GetProcInfo(deadlinedCtx)
for _, p := range peerProcInfos {
anonymizeProcInfo(&p)
healthInfo.Sys.ProcInfo = append(healthInfo.Sys.ProcInfo, p)
}
partialWrite(healthInfo)
}
}
getAndWriteMinioConfig := func() {
if query.Get("minioconfig") == "true" {
config, err := readServerConfig(ctx, objectAPI)
if err != nil {
@@ -1606,47 +1675,103 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
}
partialWrite(healthInfo)
}
}
getAndWriteDrivePerfInfo := func() {
if query.Get("perfdrive") == "true" {
healthInfo.Perf.Drives = append(healthInfo.Perf.Drives, getDrivePerfInfos(deadlinedCtx, r.Host))
localDPI := getDrivePerfInfos(deadlinedCtx, globalLocalNodeName)
anonymizeAddr(&localDPI)
healthInfo.Perf.Drives = append(healthInfo.Perf.Drives, localDPI)
partialWrite(healthInfo)
perfCh := globalNotificationSys.GetDrivePerfInfos(deadlinedCtx)
for perfInfo := range perfCh {
anonymizeAddr(&perfInfo)
healthInfo.Perf.Drives = append(healthInfo.Perf.Drives, perfInfo)
partialWrite(healthInfo)
}
partialWrite(healthInfo)
}
}
anonymizeNetPerfInfo := func(npi *madmin.NetPerfInfo) {
anonymizeAddr(npi)
rps := npi.RemotePeers
for idx, peer := range rps {
anonymizeAddr(&peer)
rps[idx] = peer
}
npi.RemotePeers = rps
}
getAndWriteNetPerfInfo := func() {
if globalIsDistErasure && query.Get("perfnet") == "true" {
healthInfo.Perf.Net = append(healthInfo.Perf.Net, globalNotificationSys.GetNetPerfInfo(deadlinedCtx))
localNPI := globalNotificationSys.GetNetPerfInfo(deadlinedCtx)
anonymizeNetPerfInfo(&localNPI)
healthInfo.Perf.Net = append(healthInfo.Perf.Net, localNPI)
partialWrite(healthInfo)
netInfos := globalNotificationSys.DispatchNetPerfChan(deadlinedCtx)
for netInfo := range netInfos {
anonymizeNetPerfInfo(&netInfo)
healthInfo.Perf.Net = append(healthInfo.Perf.Net, netInfo)
partialWrite(healthInfo)
}
partialWrite(healthInfo)
healthInfo.Perf.NetParallel = globalNotificationSys.GetParallelNetPerfInfo(deadlinedCtx)
ppi := globalNotificationSys.GetParallelNetPerfInfo(deadlinedCtx)
anonymizeNetPerfInfo(&ppi)
healthInfo.Perf.NetParallel = ppi
partialWrite(healthInfo)
}
}
anonymizeNetwork := func(network map[string]string) map[string]string {
anonNetwork := map[string]string{}
for endpoint, status := range network {
anonEndpoint := anonAddr(endpoint)
anonNetwork[anonEndpoint] = status
}
return anonNetwork
}
anonymizeDrives := func(drives []madmin.Disk) []madmin.Disk {
anonDrives := []madmin.Disk{}
for _, drive := range drives {
drive.Endpoint = anonAddr(drive.Endpoint)
anonDrives = append(anonDrives, drive)
}
return anonDrives
}
go func() {
defer close(healthInfoCh)
partialWrite(healthInfo) // Write first message with only version populated
getAndWriteCPUs()
getAndWritePartitions()
getAndWriteOSInfo()
getAndWriteMemInfo()
getAndWriteProcInfo()
getAndWriteMinioConfig()
getAndWriteDrivePerfInfo()
getAndWriteNetPerfInfo()
getAndWriteSysErrors()
getAndWriteSysServices()
if query.Get("minioinfo") == "true" {
infoMessage := getServerInfo(ctx, r)
servers := []madmin.ServerInfo{}
for _, server := range infoMessage.Servers {
anonEndpoint := anonAddr(server.Endpoint)
servers = append(servers, madmin.ServerInfo{
State: server.State,
Endpoint: server.Endpoint,
Endpoint: anonEndpoint,
Uptime: server.Uptime,
Version: server.Version,
CommitID: server.CommitID,
Network: server.Network,
Drives: server.Disks,
Network: anonymizeNetwork(server.Network),
Drives: anonymizeDrives(server.Disks),
PoolNumber: server.PoolNumber,
MemStats: madmin.MemStats{
Alloc: server.MemStats.Alloc,
@@ -1719,7 +1844,7 @@ func (a adminAPIHandlers) BandwidthMonitorHandler(w http.ResponseWriter, r *http
reportCh := make(chan madmin.BucketBandwidthReport)
keepAliveTicker := time.NewTicker(500 * time.Millisecond)
defer keepAliveTicker.Stop()
bucketsRequestedString := r.URL.Query().Get("buckets")
bucketsRequestedString := r.Form.Get("buckets")
bucketsRequested := strings.Split(bucketsRequestedString, ",")
go func() {
defer close(reportCh)
@@ -1951,3 +2076,187 @@ func checkConnection(endpointStr string, timeout time.Duration) error {
defer xhttp.DrainBody(resp.Body)
return nil
}
// getRawDataer provides an interface for getting raw FS files.
type getRawDataer interface {
GetRawData(ctx context.Context, volume, file string, fn func(r io.Reader, host string, disk string, filename string, size int64, modtime time.Time) error) error
}
// InspectDataHandler - GET /minio/admin/v3/inspect-data
// ----------
// Download file from all nodes in a zip format
func (a adminAPIHandlers) InspectDataHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "InspectData")
// Validate request signature.
_, adminAPIErr := checkAdminRequestAuth(ctx, r, iampolicy.InspectDataAction, "")
if adminAPIErr != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
return
}
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
o, ok := newObjectLayerFn().(getRawDataer)
if !ok {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
volume := r.Form.Get("volume")
file := r.Form.Get("file")
if len(volume) == 0 {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidBucketName), r.URL)
return
}
if len(file) == 0 {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
return
}
var key [32]byte
// MUST use crypto/rand
n, err := crand.Read(key[:])
if err != nil || n != len(key) {
logger.LogIf(ctx, err)
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL)
return
}
stream, err := sio.AES_256_GCM.Stream(key[:])
if err != nil {
logger.LogIf(ctx, err)
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL)
return
}
// Zero nonce, we only use each key once, and 32 bytes is plenty.
nonce := make([]byte, stream.NonceSize())
encw := stream.EncryptWriter(w, nonce, nil)
defer encw.Close()
// Write a version for making *incompatible* changes.
// The AdminClient will reject any version it does not know.
w.Write([]byte{1})
// Write key first (without encryption)
_, err = w.Write(key[:])
if err != nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL)
return
}
// Initialize a zip writer which will provide a zipped content
// of profiling data of all nodes
zipWriter := zip.NewWriter(encw)
defer zipWriter.Close()
err = o.GetRawData(ctx, volume, file, func(r io.Reader, host, disk, filename string, size int64, modtime time.Time) error {
// Prefix host+disk
filename = path.Join(host, disk, filename)
header, zerr := zip.FileInfoHeader(dummyFileInfo{
name: filename,
size: size,
mode: 0600,
modTime: modtime,
isDir: false,
sys: nil,
})
if zerr != nil {
logger.LogIf(ctx, zerr)
return nil
}
header.Method = zip.Deflate
zwriter, zerr := zipWriter.CreateHeader(header)
if zerr != nil {
logger.LogIf(ctx, zerr)
return nil
}
if _, err = io.Copy(zwriter, r); err != nil {
logger.LogIf(ctx, err)
}
return nil
})
logger.LogIf(ctx, err)
}
func createHostAnonymizerForFSMode() map[string]string {
hostAnonymizer := map[string]string{
globalLocalNodeName: "server1",
}
apiEndpoints := getAPIEndpoints()
for _, ep := range apiEndpoints {
if len(ep) == 0 {
continue
}
if url, err := xnet.ParseHTTPURL(ep); err == nil {
// In FS mode the drive names don't include the host.
// So mapping just the host should be sufficient.
hostAnonymizer[url.Host] = "server1"
}
}
return hostAnonymizer
}
// anonymizeHost - Add entries related to given endpoint in the host anonymizer map
// The health report data can contain the hostname in various forms e.g. host, host:port,
// host:port/drivepath, full url (http://host:port/drivepath)
// The anonymizer map will have mappings for all these varients for efficiently replacing
// any of these strings to the anonymized versions at the time of health report generation.
func anonymizeHost(hostAnonymizer map[string]string, endpoint Endpoint, poolNum int, srvrNum int) {
if len(endpoint.Host) == 0 {
return
}
currentURL := endpoint.String()
// mapIfNotPresent - Maps the given key to the value only if the key is not present in the map
mapIfNotPresent := func(m map[string]string, key string, val string) {
_, found := m[key]
if !found {
m[key] = val
}
}
_, found := hostAnonymizer[currentURL]
if !found {
// In distributed setup, anonymized addr = 'poolNum.serverNum'
newHost := fmt.Sprintf("pool%d.server%d", poolNum, srvrNum)
// Hostname
mapIfNotPresent(hostAnonymizer, endpoint.Hostname(), newHost)
newHostPort := newHost
if len(endpoint.Port()) > 0 {
// Host + port
newHostPort = newHost + ":" + endpoint.Port()
mapIfNotPresent(hostAnonymizer, endpoint.Host, newHostPort)
}
newHostPortPath := newHostPort
if len(endpoint.Path) > 0 {
// Host + port + path
currentHostPortPath := endpoint.Host + endpoint.Path
newHostPortPath = newHostPort + endpoint.Path
mapIfNotPresent(hostAnonymizer, currentHostPortPath, newHostPortPath)
}
// Full url
hostAnonymizer[currentURL] = endpoint.Scheme + "://" + newHostPortPath
}
}
// createHostAnonymizer - Creats a map of various strings to corresponding anonymized names
func createHostAnonymizer() map[string]string {
if !globalIsDistErasure {
return createHostAnonymizerForFSMode()
}
hostAnonymizer := map[string]string{}
for poolIdx, pool := range globalEndpoints {
for srvrIdx, endpoint := range pool.Endpoints {
anonymizeHost(hostAnonymizer, endpoint, poolIdx+1, srvrIdx+1)
}
}
return hostAnonymizer
}
+3 -3
View File
@@ -77,7 +77,7 @@ func prepareAdminErasureTestBed(ctx context.Context) (*adminErasureTestBed, erro
// Setup admin mgmt REST API handlers.
adminRouter := mux.NewRouter()
registerAdminRouter(adminRouter, true, true)
registerAdminRouter(adminRouter, true)
return &adminErasureTestBed{
erasureDirs: erasureDirs,
@@ -182,7 +182,7 @@ func testServicesCmdHandler(cmd cmdType, t *testing.T) {
adminTestBed, err := prepareAdminErasureTestBed(ctx)
if err != nil {
t.Fatal("Failed to initialize a single node Erasure backend for admin handler tests.")
t.Fatal("Failed to initialize a single node Erasure backend for admin handler tests.", err)
}
defer adminTestBed.TearDown()
@@ -253,7 +253,7 @@ func TestAdminServerInfo(t *testing.T) {
adminTestBed, err := prepareAdminErasureTestBed(ctx)
if err != nil {
t.Fatal("Failed to initialize a single node Erasure backend for admin handler tests.")
t.Fatal("Failed to initialize a single node Erasure backend for admin handler tests.", err)
}
defer adminTestBed.TearDown()
+90 -80
View File
@@ -21,7 +21,10 @@ import (
"net/http"
"github.com/gorilla/mux"
"github.com/klauspost/compress/gzhttp"
"github.com/klauspost/compress/gzip"
"github.com/minio/madmin-go"
"github.com/minio/minio/internal/logger"
)
const (
@@ -34,7 +37,7 @@ const (
type adminAPIHandlers struct{}
// registerAdminRouter - Add handler functions for each service REST API routes.
func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool) {
func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
adminAPI := adminAPIHandlers{}
// Admin router
@@ -46,174 +49,181 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
adminAPIVersionPrefix,
}
gz, err := gzhttp.NewWrapper(gzhttp.MinSize(1000), gzhttp.CompressionLevel(gzip.BestSpeed))
if err != nil {
// Static params, so this is very unlikely.
logger.Fatal(err, "Unable to initialize server")
}
for _, adminVersion := range adminVersions {
// Restart and stop MinIO service.
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/service").HandlerFunc(httpTraceAll(adminAPI.ServiceHandler)).Queries("action", "{action:.*}")
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/service").HandlerFunc(gz(httpTraceAll(adminAPI.ServiceHandler))).Queries("action", "{action:.*}")
// Update MinIO servers.
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/update").HandlerFunc(httpTraceAll(adminAPI.ServerUpdateHandler)).Queries("updateURL", "{updateURL:.*}")
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/update").HandlerFunc(gz(httpTraceAll(adminAPI.ServerUpdateHandler))).Queries("updateURL", "{updateURL:.*}")
// Info operations
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/info").HandlerFunc(httpTraceAll(adminAPI.ServerInfoHandler))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/info").HandlerFunc(gz(httpTraceAll(adminAPI.ServerInfoHandler)))
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/inspect-data").HandlerFunc(httpTraceHdrs(adminAPI.InspectDataHandler)).Queries("volume", "{volume:.*}", "file", "{file:.*}")
// StorageInfo operations
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/storageinfo").HandlerFunc(httpTraceAll(adminAPI.StorageInfoHandler))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/storageinfo").HandlerFunc(gz(httpTraceAll(adminAPI.StorageInfoHandler)))
// DataUsageInfo operations
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/datausageinfo").HandlerFunc(httpTraceAll(adminAPI.DataUsageInfoHandler))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/datausageinfo").HandlerFunc(gz(httpTraceAll(adminAPI.DataUsageInfoHandler)))
if globalIsDistErasure || globalIsErasure {
/// Heal operations
// Heal processing endpoint.
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/{bucket}").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/{bucket}/{prefix:.*}").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/").HandlerFunc(gz(httpTraceAll(adminAPI.HealHandler)))
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/{bucket}").HandlerFunc(gz(httpTraceAll(adminAPI.HealHandler)))
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/{bucket}/{prefix:.*}").HandlerFunc(gz(httpTraceAll(adminAPI.HealHandler)))
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/background-heal/status").HandlerFunc(httpTraceAll(adminAPI.BackgroundHealStatusHandler))
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/background-heal/status").HandlerFunc(gz(httpTraceAll(adminAPI.BackgroundHealStatusHandler)))
/// Health operations
}
// Profiling operations
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/profiling/start").HandlerFunc(httpTraceAll(adminAPI.StartProfilingHandler)).
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/profiling/start").HandlerFunc(gz(httpTraceAll(adminAPI.StartProfilingHandler))).
Queries("profilerType", "{profilerType:.*}")
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/profiling/download").HandlerFunc(httpTraceAll(adminAPI.DownloadProfilingHandler))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/profiling/download").HandlerFunc(gz(httpTraceAll(adminAPI.DownloadProfilingHandler)))
// Config KV operations.
if enableConfigOps {
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-config-kv").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigKVHandler)).Queries("key", "{key:.*}")
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/set-config-kv").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigKVHandler))
adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/del-config-kv").HandlerFunc(httpTraceHdrs(adminAPI.DelConfigKVHandler))
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-config-kv").HandlerFunc(gz(httpTraceHdrs(adminAPI.GetConfigKVHandler))).Queries("key", "{key:.*}")
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/set-config-kv").HandlerFunc(gz(httpTraceHdrs(adminAPI.SetConfigKVHandler)))
adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/del-config-kv").HandlerFunc(gz(httpTraceHdrs(adminAPI.DelConfigKVHandler)))
}
// Enable config help in all modes.
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/help-config-kv").HandlerFunc(httpTraceAll(adminAPI.HelpConfigKVHandler)).Queries("subSys", "{subSys:.*}", "key", "{key:.*}")
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/help-config-kv").HandlerFunc(gz(httpTraceAll(adminAPI.HelpConfigKVHandler))).Queries("subSys", "{subSys:.*}", "key", "{key:.*}")
// Config KV history operations.
if enableConfigOps {
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-config-history-kv").HandlerFunc(httpTraceAll(adminAPI.ListConfigHistoryKVHandler)).Queries("count", "{count:[0-9]+}")
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/clear-config-history-kv").HandlerFunc(httpTraceHdrs(adminAPI.ClearConfigHistoryKVHandler)).Queries("restoreId", "{restoreId:.*}")
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/restore-config-history-kv").HandlerFunc(httpTraceHdrs(adminAPI.RestoreConfigHistoryKVHandler)).Queries("restoreId", "{restoreId:.*}")
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-config-history-kv").HandlerFunc(gz(httpTraceAll(adminAPI.ListConfigHistoryKVHandler))).Queries("count", "{count:[0-9]+}")
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/clear-config-history-kv").HandlerFunc(gz(httpTraceHdrs(adminAPI.ClearConfigHistoryKVHandler))).Queries("restoreId", "{restoreId:.*}")
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/restore-config-history-kv").HandlerFunc(gz(httpTraceHdrs(adminAPI.RestoreConfigHistoryKVHandler))).Queries("restoreId", "{restoreId:.*}")
}
/// Config import/export bulk operations
if enableConfigOps {
// Get config
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/config").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigHandler))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/config").HandlerFunc(gz(httpTraceHdrs(adminAPI.GetConfigHandler)))
// Set config
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/config").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigHandler))
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/config").HandlerFunc(gz(httpTraceHdrs(adminAPI.SetConfigHandler)))
}
if enableIAMOps {
// -- IAM APIs --
// -- IAM APIs --
// Add policy IAM
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-canned-policy").HandlerFunc(httpTraceAll(adminAPI.AddCannedPolicy)).Queries("name", "{name:.*}")
// Add policy IAM
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-canned-policy").HandlerFunc(gz(httpTraceAll(adminAPI.AddCannedPolicy))).Queries("name", "{name:.*}")
// Add user IAM
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/accountinfo").HandlerFunc(httpTraceAll(adminAPI.AccountInfoHandler))
// Add user IAM
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/accountinfo").HandlerFunc(gz(httpTraceAll(adminAPI.AccountInfoHandler)))
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-user").HandlerFunc(httpTraceHdrs(adminAPI.AddUser)).Queries("accessKey", "{accessKey:.*}")
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-user").HandlerFunc(gz(httpTraceHdrs(adminAPI.AddUser))).Queries("accessKey", "{accessKey:.*}")
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-user-status").HandlerFunc(httpTraceHdrs(adminAPI.SetUserStatus)).Queries("accessKey", "{accessKey:.*}").Queries("status", "{status:.*}")
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-user-status").HandlerFunc(gz(httpTraceHdrs(adminAPI.SetUserStatus))).Queries("accessKey", "{accessKey:.*}").Queries("status", "{status:.*}")
// Service accounts ops
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/add-service-account").HandlerFunc(httpTraceHdrs(adminAPI.AddServiceAccount))
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/update-service-account").HandlerFunc(httpTraceHdrs(adminAPI.UpdateServiceAccount)).Queries("accessKey", "{accessKey:.*}")
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-service-account").HandlerFunc(httpTraceHdrs(adminAPI.InfoServiceAccount)).Queries("accessKey", "{accessKey:.*}")
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-service-accounts").HandlerFunc(httpTraceHdrs(adminAPI.ListServiceAccounts))
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/delete-service-account").HandlerFunc(httpTraceHdrs(adminAPI.DeleteServiceAccount)).Queries("accessKey", "{accessKey:.*}")
// Service accounts ops
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/add-service-account").HandlerFunc(gz(httpTraceHdrs(adminAPI.AddServiceAccount)))
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/update-service-account").HandlerFunc(gz(httpTraceHdrs(adminAPI.UpdateServiceAccount))).Queries("accessKey", "{accessKey:.*}")
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-service-account").HandlerFunc(gz(httpTraceHdrs(adminAPI.InfoServiceAccount))).Queries("accessKey", "{accessKey:.*}")
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-service-accounts").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListServiceAccounts)))
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/delete-service-account").HandlerFunc(gz(httpTraceHdrs(adminAPI.DeleteServiceAccount))).Queries("accessKey", "{accessKey:.*}")
// Info policy IAM latest
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.InfoCannedPolicy)).Queries("name", "{name:.*}")
// List policies latest
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-canned-policies").HandlerFunc(httpTraceHdrs(adminAPI.ListBucketPolicies)).Queries("bucket", "{bucket:.*}")
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-canned-policies").HandlerFunc(httpTraceHdrs(adminAPI.ListCannedPolicies))
// Info policy IAM latest
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-canned-policy").HandlerFunc(gz(httpTraceHdrs(adminAPI.InfoCannedPolicy))).Queries("name", "{name:.*}")
// List policies latest
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-canned-policies").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListBucketPolicies))).Queries("bucket", "{bucket:.*}")
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-canned-policies").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListCannedPolicies)))
// Remove policy IAM
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.RemoveCannedPolicy)).Queries("name", "{name:.*}")
// Remove policy IAM
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-canned-policy").HandlerFunc(gz(httpTraceHdrs(adminAPI.RemoveCannedPolicy))).Queries("name", "{name:.*}")
// Set user or group policy
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-user-or-group-policy").
HandlerFunc(httpTraceHdrs(adminAPI.SetPolicyForUserOrGroup)).
Queries("policyName", "{policyName:.*}", "userOrGroup", "{userOrGroup:.*}", "isGroup", "{isGroup:true|false}")
// Set user or group policy
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-user-or-group-policy").
HandlerFunc(gz(httpTraceHdrs(adminAPI.SetPolicyForUserOrGroup))).
Queries("policyName", "{policyName:.*}", "userOrGroup", "{userOrGroup:.*}", "isGroup", "{isGroup:true|false}")
// Remove user IAM
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-user").HandlerFunc(httpTraceHdrs(adminAPI.RemoveUser)).Queries("accessKey", "{accessKey:.*}")
// Remove user IAM
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-user").HandlerFunc(gz(httpTraceHdrs(adminAPI.RemoveUser))).Queries("accessKey", "{accessKey:.*}")
// List users
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-users").HandlerFunc(httpTraceHdrs(adminAPI.ListBucketUsers)).Queries("bucket", "{bucket:.*}")
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-users").HandlerFunc(httpTraceHdrs(adminAPI.ListUsers))
// List users
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-users").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListBucketUsers))).Queries("bucket", "{bucket:.*}")
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-users").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListUsers)))
// User info
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/user-info").HandlerFunc(httpTraceHdrs(adminAPI.GetUserInfo)).Queries("accessKey", "{accessKey:.*}")
// Add/Remove members from group
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/update-group-members").HandlerFunc(httpTraceHdrs(adminAPI.UpdateGroupMembers))
// User info
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/user-info").HandlerFunc(gz(httpTraceHdrs(adminAPI.GetUserInfo))).Queries("accessKey", "{accessKey:.*}")
// Add/Remove members from group
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/update-group-members").HandlerFunc(gz(httpTraceHdrs(adminAPI.UpdateGroupMembers)))
// Get Group
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/group").HandlerFunc(httpTraceHdrs(adminAPI.GetGroup)).Queries("group", "{group:.*}")
// Get Group
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/group").HandlerFunc(gz(httpTraceHdrs(adminAPI.GetGroup))).Queries("group", "{group:.*}")
// List Groups
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/groups").HandlerFunc(httpTraceHdrs(adminAPI.ListGroups))
// List Groups
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/groups").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListGroups)))
// Set Group Status
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-group-status").HandlerFunc(httpTraceHdrs(adminAPI.SetGroupStatus)).Queries("group", "{group:.*}").Queries("status", "{status:.*}")
}
// Set Group Status
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-group-status").HandlerFunc(gz(httpTraceHdrs(adminAPI.SetGroupStatus))).Queries("group", "{group:.*}").Queries("status", "{status:.*}")
if globalIsDistErasure || globalIsErasure {
// GetBucketQuotaConfig
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-bucket-quota").HandlerFunc(
httpTraceHdrs(adminAPI.GetBucketQuotaConfigHandler)).Queries("bucket", "{bucket:.*}")
gz(httpTraceHdrs(adminAPI.GetBucketQuotaConfigHandler))).Queries("bucket", "{bucket:.*}")
// PutBucketQuotaConfig
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-bucket-quota").HandlerFunc(
httpTraceHdrs(adminAPI.PutBucketQuotaConfigHandler)).Queries("bucket", "{bucket:.*}")
gz(httpTraceHdrs(adminAPI.PutBucketQuotaConfigHandler))).Queries("bucket", "{bucket:.*}")
// Bucket replication operations
// GetBucketTargetHandler
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-remote-targets").HandlerFunc(
httpTraceHdrs(adminAPI.ListRemoteTargetsHandler)).Queries("bucket", "{bucket:.*}", "type", "{type:.*}")
gz(httpTraceHdrs(adminAPI.ListRemoteTargetsHandler))).Queries("bucket", "{bucket:.*}", "type", "{type:.*}")
// SetRemoteTargetHandler
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-remote-target").HandlerFunc(
httpTraceHdrs(adminAPI.SetRemoteTargetHandler)).Queries("bucket", "{bucket:.*}")
gz(httpTraceHdrs(adminAPI.SetRemoteTargetHandler))).Queries("bucket", "{bucket:.*}")
// RemoveRemoteTargetHandler
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-remote-target").HandlerFunc(
httpTraceHdrs(adminAPI.RemoveRemoteTargetHandler)).Queries("bucket", "{bucket:.*}", "arn", "{arn:.*}")
gz(httpTraceHdrs(adminAPI.RemoveRemoteTargetHandler))).Queries("bucket", "{bucket:.*}", "arn", "{arn:.*}")
// Remote Tier management operations
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/tier").HandlerFunc(httpTraceHdrs(adminAPI.AddTierHandler))
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/tier/{tier}").HandlerFunc(httpTraceHdrs(adminAPI.EditTierHandler))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier").HandlerFunc(httpTraceHdrs(adminAPI.ListTierHandler))
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/tier").HandlerFunc(gz(httpTraceHdrs(adminAPI.AddTierHandler)))
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/tier/{tier}").HandlerFunc(gz(httpTraceHdrs(adminAPI.EditTierHandler)))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListTierHandler)))
}
if globalIsDistErasure {
// Top locks
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/top/locks").HandlerFunc(httpTraceHdrs(adminAPI.TopLocksHandler))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/top/locks").HandlerFunc(gz(httpTraceHdrs(adminAPI.TopLocksHandler)))
// Force unlocks paths
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/force-unlock").
Queries("paths", "{paths:.*}").HandlerFunc(httpTraceHdrs(adminAPI.ForceUnlockHandler))
Queries("paths", "{paths:.*}").HandlerFunc(gz(httpTraceHdrs(adminAPI.ForceUnlockHandler)))
}
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest").HandlerFunc(httpTraceHdrs(adminAPI.SpeedtestHandler))
// HTTP Trace
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/trace").HandlerFunc(adminAPI.TraceHandler)
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/trace").HandlerFunc(gz(http.HandlerFunc(adminAPI.TraceHandler)))
// Console Logs
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/log").HandlerFunc(httpTraceAll(adminAPI.ConsoleLogHandler))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/log").HandlerFunc(gz(httpTraceAll(adminAPI.ConsoleLogHandler)))
// -- KMS APIs --
//
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/kms/status").HandlerFunc(httpTraceAll(adminAPI.KMSStatusHandler))
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/kms/key/create").HandlerFunc(httpTraceAll(adminAPI.KMSCreateKeyHandler)).Queries("key-id", "{key-id:.*}")
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/kms/key/status").HandlerFunc(httpTraceAll(adminAPI.KMSKeyStatusHandler))
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/kms/status").HandlerFunc(gz(httpTraceAll(adminAPI.KMSStatusHandler)))
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/kms/key/create").HandlerFunc(gz(httpTraceAll(adminAPI.KMSCreateKeyHandler))).Queries("key-id", "{key-id:.*}")
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/kms/key/status").HandlerFunc(gz(httpTraceAll(adminAPI.KMSKeyStatusHandler)))
if !globalIsGateway {
// Keep obdinfo for backward compatibility with mc
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/obdinfo").
HandlerFunc(httpTraceHdrs(adminAPI.HealthInfoHandler))
HandlerFunc(gz(httpTraceHdrs(adminAPI.HealthInfoHandler)))
// -- Health API --
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/healthinfo").
HandlerFunc(httpTraceHdrs(adminAPI.HealthInfoHandler))
HandlerFunc(gz(httpTraceHdrs(adminAPI.HealthInfoHandler)))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/bandwidth").
HandlerFunc(httpTraceHdrs(adminAPI.BandwidthMonitorHandler))
HandlerFunc(gz(httpTraceHdrs(adminAPI.BandwidthMonitorHandler)))
}
}
-8
View File
@@ -231,7 +231,6 @@ const (
// MinIO extended errors.
ErrReadQuorum
ErrWriteQuorum
ErrParentIsObject
ErrStorageFull
ErrRequestBodyParse
ErrObjectExistsAsDirectory
@@ -1122,11 +1121,6 @@ var errorCodes = errorCodeMap{
Description: "Storage backend has reached its minimum free disk threshold. Please delete a few objects to proceed.",
HTTPStatusCode: http.StatusInsufficientStorage,
},
ErrParentIsObject: {
Code: "XMinioParentIsObject",
Description: "Object-prefix is already an object, please choose a different object-prefix name.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrRequestBodyParse: {
Code: "XMinioRequestBodyParse",
Description: "The request body failed to parse.",
@@ -1899,8 +1893,6 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
apiErr = ErrObjectExistsAsDirectory
case PrefixAccessDenied:
apiErr = ErrAccessDenied
case ParentIsObject:
apiErr = ErrParentIsObject
case BucketNameInvalid:
apiErr = ErrInvalidBucketName
case BucketNotFound:
+7 -37
View File
@@ -28,7 +28,6 @@ import (
"strings"
"time"
"github.com/minio/minio/internal/bucket/lifecycle"
"github.com/minio/minio/internal/crypto"
xhttp "github.com/minio/minio/internal/http"
)
@@ -185,43 +184,14 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSp
w.Header()[xhttp.AmzBucketReplicationStatus] = []string{objInfo.ReplicationStatus.String()}
}
if lc, err := globalLifecycleSys.Get(objInfo.Bucket); err == nil {
if opts.VersionID == "" {
if ruleID, expiryTime := lc.PredictExpiryTime(lifecycle.ObjectOpts{
Name: objInfo.Name,
UserTags: objInfo.UserTags,
VersionID: objInfo.VersionID,
ModTime: objInfo.ModTime,
IsLatest: objInfo.IsLatest,
DeleteMarker: objInfo.DeleteMarker,
SuccessorModTime: objInfo.SuccessorModTime,
}); !expiryTime.IsZero() {
w.Header()[xhttp.AmzExpiration] = []string{
fmt.Sprintf(`expiry-date="%s", rule-id="%s"`, expiryTime.Format(http.TimeFormat), ruleID),
}
}
}
if objInfo.IsRemote() {
// Check if object is being restored. For more information on x-amz-restore header see
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html#API_HeadObject_ResponseSyntax
w.Header()[xhttp.AmzStorageClass] = []string{objInfo.TransitionTier}
}
ruleID, transitionTime := lc.PredictTransitionTime(lifecycle.ObjectOpts{
Name: objInfo.Name,
UserTags: objInfo.UserTags,
VersionID: objInfo.VersionID,
ModTime: objInfo.ModTime,
IsLatest: objInfo.IsLatest,
DeleteMarker: objInfo.DeleteMarker,
TransitionStatus: objInfo.TransitionStatus,
})
if !transitionTime.IsZero() {
// This header is a MinIO centric extension to show expected transition date in a similar spirit as x-amz-expiration
w.Header()[xhttp.MinIOTransition] = []string{
fmt.Sprintf(`transition-date="%s", rule-id="%s"`, transitionTime.Format(http.TimeFormat), ruleID),
}
}
if objInfo.IsRemote() {
// Check if object is being restored. For more information on x-amz-restore header see
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html#API_HeadObject_ResponseSyntax
w.Header()[xhttp.AmzStorageClass] = []string{objInfo.TransitionedObject.Tier}
}
if lc, err := globalLifecycleSys.Get(objInfo.Bucket); err == nil {
lc.SetPredictionHeaders(w, objInfo.ToLifecycleOpts())
}
return nil
+5 -5
View File
@@ -36,11 +36,11 @@ import (
const (
// RFC3339 a subset of the ISO8601 timestamp format. e.g 2014-04-29T18:30:38Z
iso8601TimeFormat = "2006-01-02T15:04:05.000Z" // Reply date format with nanosecond precision.
maxObjectList = metacacheBlockSize - (metacacheBlockSize / 10) // Limit number of objects in a listObjectsResponse/listObjectsVersionsResponse.
maxDeleteList = 10000 // Limit number of objects deleted in a delete call.
maxUploadsList = 10000 // Limit number of uploads in a listUploadsResponse.
maxPartsList = 10000 // Limit number of parts in a listPartsResponse.
iso8601TimeFormat = "2006-01-02T15:04:05.000Z" // Reply date format with nanosecond precision.
maxObjectList = 1000 // Limit number of objects in a listObjectsResponse/listObjectsVersionsResponse.
maxDeleteList = 10000 // Limit number of objects deleted in a delete call.
maxUploadsList = 10000 // Limit number of uploads in a listUploadsResponse.
maxPartsList = 10000 // Limit number of parts in a listPartsResponse.
)
// LocationResponse - format for location response.
+79 -70
View File
@@ -18,11 +18,14 @@
package cmd
import (
"compress/gzip"
"net"
"net/http"
"github.com/gorilla/mux"
"github.com/klauspost/compress/gzhttp"
xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/logger"
"github.com/minio/pkg/wildcard"
"github.com/rs/cors"
)
@@ -206,6 +209,12 @@ func registerAPIRouter(router *mux.Router) {
}
routers = append(routers, apiRouter.PathPrefix("/{bucket}").Subrouter())
gz, err := gzhttp.NewWrapper(gzhttp.MinSize(1000), gzhttp.CompressionLevel(gzip.BestSpeed))
if err != nil {
// Static params, so this is very unlikely.
logger.Fatal(err, "Unable to initialize server")
}
for _, router := range routers {
// Register all rejected object APIs
for _, r := range rejectedObjAPIs {
@@ -218,218 +227,218 @@ func registerAPIRouter(router *mux.Router) {
// Object operations
// HeadObject
router.Methods(http.MethodHead).Path("/{object:.+}").HandlerFunc(
collectAPIStats("headobject", maxClients(httpTraceAll(api.HeadObjectHandler))))
collectAPIStats("headobject", maxClients(gz(httpTraceAll(api.HeadObjectHandler)))))
// CopyObjectPart
router.Methods(http.MethodPut).Path("/{object:.+}").
HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").
HandlerFunc(collectAPIStats("copyobjectpart", maxClients(httpTraceAll(api.CopyObjectPartHandler)))).
HandlerFunc(collectAPIStats("copyobjectpart", maxClients(gz(httpTraceAll(api.CopyObjectPartHandler))))).
Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
// PutObjectPart
router.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
collectAPIStats("putobjectpart", maxClients(httpTraceHdrs(api.PutObjectPartHandler)))).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
collectAPIStats("putobjectpart", maxClients(gz(httpTraceHdrs(api.PutObjectPartHandler))))).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
// ListObjectParts
router.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
collectAPIStats("listobjectparts", maxClients(httpTraceAll(api.ListObjectPartsHandler)))).Queries("uploadId", "{uploadId:.*}")
collectAPIStats("listobjectparts", maxClients(gz(httpTraceAll(api.ListObjectPartsHandler))))).Queries("uploadId", "{uploadId:.*}")
// CompleteMultipartUpload
router.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(
collectAPIStats("completemutipartupload", maxClients(httpTraceAll(api.CompleteMultipartUploadHandler)))).Queries("uploadId", "{uploadId:.*}")
collectAPIStats("completemutipartupload", maxClients(gz(httpTraceAll(api.CompleteMultipartUploadHandler))))).Queries("uploadId", "{uploadId:.*}")
// NewMultipartUpload
router.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(
collectAPIStats("newmultipartupload", maxClients(httpTraceAll(api.NewMultipartUploadHandler)))).Queries("uploads", "")
collectAPIStats("newmultipartupload", maxClients(gz(httpTraceAll(api.NewMultipartUploadHandler))))).Queries("uploads", "")
// AbortMultipartUpload
router.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(
collectAPIStats("abortmultipartupload", maxClients(httpTraceAll(api.AbortMultipartUploadHandler)))).Queries("uploadId", "{uploadId:.*}")
collectAPIStats("abortmultipartupload", maxClients(gz(httpTraceAll(api.AbortMultipartUploadHandler))))).Queries("uploadId", "{uploadId:.*}")
// GetObjectACL - this is a dummy call.
router.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
collectAPIStats("getobjectacl", maxClients(httpTraceHdrs(api.GetObjectACLHandler)))).Queries("acl", "")
collectAPIStats("getobjectacl", maxClients(gz(httpTraceHdrs(api.GetObjectACLHandler))))).Queries("acl", "")
// PutObjectACL - this is a dummy call.
router.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
collectAPIStats("putobjectacl", maxClients(httpTraceHdrs(api.PutObjectACLHandler)))).Queries("acl", "")
collectAPIStats("putobjectacl", maxClients(gz(httpTraceHdrs(api.PutObjectACLHandler))))).Queries("acl", "")
// GetObjectTagging
router.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
collectAPIStats("getobjecttagging", maxClients(httpTraceHdrs(api.GetObjectTaggingHandler)))).Queries("tagging", "")
collectAPIStats("getobjecttagging", maxClients(gz(httpTraceHdrs(api.GetObjectTaggingHandler))))).Queries("tagging", "")
// PutObjectTagging
router.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
collectAPIStats("putobjecttagging", maxClients(httpTraceHdrs(api.PutObjectTaggingHandler)))).Queries("tagging", "")
collectAPIStats("putobjecttagging", maxClients(gz(httpTraceHdrs(api.PutObjectTaggingHandler))))).Queries("tagging", "")
// DeleteObjectTagging
router.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(
collectAPIStats("deleteobjecttagging", maxClients(httpTraceHdrs(api.DeleteObjectTaggingHandler)))).Queries("tagging", "")
collectAPIStats("deleteobjecttagging", maxClients(gz(httpTraceHdrs(api.DeleteObjectTaggingHandler))))).Queries("tagging", "")
// SelectObjectContent
router.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(
collectAPIStats("selectobjectcontent", maxClients(httpTraceHdrs(api.SelectObjectContentHandler)))).Queries("select", "").Queries("select-type", "2")
collectAPIStats("selectobjectcontent", maxClients(gz(httpTraceHdrs(api.SelectObjectContentHandler))))).Queries("select", "").Queries("select-type", "2")
// GetObjectRetention
router.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
collectAPIStats("getobjectretention", maxClients(httpTraceAll(api.GetObjectRetentionHandler)))).Queries("retention", "")
collectAPIStats("getobjectretention", maxClients(gz(httpTraceAll(api.GetObjectRetentionHandler))))).Queries("retention", "")
// GetObjectLegalHold
router.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
collectAPIStats("getobjectlegalhold", maxClients(httpTraceAll(api.GetObjectLegalHoldHandler)))).Queries("legal-hold", "")
// GetObject
collectAPIStats("getobjectlegalhold", maxClients(gz(httpTraceAll(api.GetObjectLegalHoldHandler))))).Queries("legal-hold", "")
// GetObject - note gzip compression is *not* added due to Range requests.
router.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
collectAPIStats("getobject", maxClients(httpTraceHdrs(api.GetObjectHandler))))
// CopyObject
router.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").HandlerFunc(
collectAPIStats("copyobject", maxClients(httpTraceAll(api.CopyObjectHandler))))
collectAPIStats("copyobject", maxClients(gz(httpTraceAll(api.CopyObjectHandler)))))
// PutObjectRetention
router.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
collectAPIStats("putobjectretention", maxClients(httpTraceAll(api.PutObjectRetentionHandler)))).Queries("retention", "")
collectAPIStats("putobjectretention", maxClients(gz(httpTraceAll(api.PutObjectRetentionHandler))))).Queries("retention", "")
// PutObjectLegalHold
router.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
collectAPIStats("putobjectlegalhold", maxClients(httpTraceAll(api.PutObjectLegalHoldHandler)))).Queries("legal-hold", "")
collectAPIStats("putobjectlegalhold", maxClients(gz(httpTraceAll(api.PutObjectLegalHoldHandler))))).Queries("legal-hold", "")
// PutObject with auto-extract support for zip
router.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp(xhttp.AmzSnowballExtract, "true").HandlerFunc(
collectAPIStats("putobject", maxClients(httpTraceHdrs(api.PutObjectExtractHandler))))
collectAPIStats("putobject", maxClients(gz(httpTraceHdrs(api.PutObjectExtractHandler)))))
// PutObject
router.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
collectAPIStats("putobject", maxClients(httpTraceHdrs(api.PutObjectHandler))))
collectAPIStats("putobject", maxClients(gz(httpTraceHdrs(api.PutObjectHandler)))))
// DeleteObject
router.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(
collectAPIStats("deleteobject", maxClients(httpTraceAll(api.DeleteObjectHandler))))
collectAPIStats("deleteobject", maxClients(gz(httpTraceAll(api.DeleteObjectHandler)))))
// PostRestoreObject
router.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(
collectAPIStats("restoreobject", maxClients(httpTraceAll(api.PostRestoreObjectHandler)))).Queries("restore", "")
collectAPIStats("restoreobject", maxClients(gz(httpTraceAll(api.PostRestoreObjectHandler))))).Queries("restore", "")
/// Bucket operations
// GetBucketLocation
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("getbucketlocation", maxClients(httpTraceAll(api.GetBucketLocationHandler)))).Queries("location", "")
collectAPIStats("getbucketlocation", maxClients(gz(httpTraceAll(api.GetBucketLocationHandler))))).Queries("location", "")
// GetBucketPolicy
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("getbucketpolicy", maxClients(httpTraceAll(api.GetBucketPolicyHandler)))).Queries("policy", "")
collectAPIStats("getbucketpolicy", maxClients(gz(httpTraceAll(api.GetBucketPolicyHandler))))).Queries("policy", "")
// GetBucketLifecycle
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("getbucketlifecycle", maxClients(httpTraceAll(api.GetBucketLifecycleHandler)))).Queries("lifecycle", "")
collectAPIStats("getbucketlifecycle", maxClients(gz(httpTraceAll(api.GetBucketLifecycleHandler))))).Queries("lifecycle", "")
// GetBucketEncryption
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("getbucketencryption", maxClients(httpTraceAll(api.GetBucketEncryptionHandler)))).Queries("encryption", "")
collectAPIStats("getbucketencryption", maxClients(gz(httpTraceAll(api.GetBucketEncryptionHandler))))).Queries("encryption", "")
// GetBucketObjectLockConfig
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("getbucketobjectlockconfiguration", maxClients(httpTraceAll(api.GetBucketObjectLockConfigHandler)))).Queries("object-lock", "")
collectAPIStats("getbucketobjectlockconfiguration", maxClients(gz(httpTraceAll(api.GetBucketObjectLockConfigHandler))))).Queries("object-lock", "")
// GetBucketReplicationConfig
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("getbucketreplicationconfiguration", maxClients(httpTraceAll(api.GetBucketReplicationConfigHandler)))).Queries("replication", "")
collectAPIStats("getbucketreplicationconfiguration", maxClients(gz(httpTraceAll(api.GetBucketReplicationConfigHandler))))).Queries("replication", "")
// GetBucketVersioning
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("getbucketversioning", maxClients(httpTraceAll(api.GetBucketVersioningHandler)))).Queries("versioning", "")
collectAPIStats("getbucketversioning", maxClients(gz(httpTraceAll(api.GetBucketVersioningHandler))))).Queries("versioning", "")
// GetBucketNotification
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("getbucketnotification", maxClients(httpTraceAll(api.GetBucketNotificationHandler)))).Queries("notification", "")
collectAPIStats("getbucketnotification", maxClients(gz(httpTraceAll(api.GetBucketNotificationHandler))))).Queries("notification", "")
// ListenNotification
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("listennotification", maxClients(httpTraceAll(api.ListenNotificationHandler)))).Queries("events", "{events:.*}")
collectAPIStats("listennotification", maxClients(gz(httpTraceAll(api.ListenNotificationHandler))))).Queries("events", "{events:.*}")
// Dummy Bucket Calls
// GetBucketACL -- this is a dummy call.
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("getbucketacl", maxClients(httpTraceAll(api.GetBucketACLHandler)))).Queries("acl", "")
collectAPIStats("getbucketacl", maxClients(gz(httpTraceAll(api.GetBucketACLHandler))))).Queries("acl", "")
// PutBucketACL -- this is a dummy call.
router.Methods(http.MethodPut).HandlerFunc(
collectAPIStats("putbucketacl", maxClients(httpTraceAll(api.PutBucketACLHandler)))).Queries("acl", "")
collectAPIStats("putbucketacl", maxClients(gz(httpTraceAll(api.PutBucketACLHandler))))).Queries("acl", "")
// GetBucketCors - this is a dummy call.
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("getbucketcors", maxClients(httpTraceAll(api.GetBucketCorsHandler)))).Queries("cors", "")
collectAPIStats("getbucketcors", maxClients(gz(httpTraceAll(api.GetBucketCorsHandler))))).Queries("cors", "")
// GetBucketWebsiteHandler - this is a dummy call.
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("getbucketwebsite", maxClients(httpTraceAll(api.GetBucketWebsiteHandler)))).Queries("website", "")
collectAPIStats("getbucketwebsite", maxClients(gz(httpTraceAll(api.GetBucketWebsiteHandler))))).Queries("website", "")
// GetBucketAccelerateHandler - this is a dummy call.
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("getbucketaccelerate", maxClients(httpTraceAll(api.GetBucketAccelerateHandler)))).Queries("accelerate", "")
collectAPIStats("getbucketaccelerate", maxClients(gz(httpTraceAll(api.GetBucketAccelerateHandler))))).Queries("accelerate", "")
// GetBucketRequestPaymentHandler - this is a dummy call.
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("getbucketrequestpayment", maxClients(httpTraceAll(api.GetBucketRequestPaymentHandler)))).Queries("requestPayment", "")
collectAPIStats("getbucketrequestpayment", maxClients(gz(httpTraceAll(api.GetBucketRequestPaymentHandler))))).Queries("requestPayment", "")
// GetBucketLoggingHandler - this is a dummy call.
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("getbucketlogging", maxClients(httpTraceAll(api.GetBucketLoggingHandler)))).Queries("logging", "")
collectAPIStats("getbucketlogging", maxClients(gz(httpTraceAll(api.GetBucketLoggingHandler))))).Queries("logging", "")
// GetBucketTaggingHandler
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("getbuckettagging", maxClients(httpTraceAll(api.GetBucketTaggingHandler)))).Queries("tagging", "")
collectAPIStats("getbuckettagging", maxClients(gz(httpTraceAll(api.GetBucketTaggingHandler))))).Queries("tagging", "")
//DeleteBucketWebsiteHandler
router.Methods(http.MethodDelete).HandlerFunc(
collectAPIStats("deletebucketwebsite", maxClients(httpTraceAll(api.DeleteBucketWebsiteHandler)))).Queries("website", "")
collectAPIStats("deletebucketwebsite", maxClients(gz(httpTraceAll(api.DeleteBucketWebsiteHandler))))).Queries("website", "")
// DeleteBucketTaggingHandler
router.Methods(http.MethodDelete).HandlerFunc(
collectAPIStats("deletebuckettagging", maxClients(httpTraceAll(api.DeleteBucketTaggingHandler)))).Queries("tagging", "")
collectAPIStats("deletebuckettagging", maxClients(gz(httpTraceAll(api.DeleteBucketTaggingHandler))))).Queries("tagging", "")
// ListMultipartUploads
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("listmultipartuploads", maxClients(httpTraceAll(api.ListMultipartUploadsHandler)))).Queries("uploads", "")
collectAPIStats("listmultipartuploads", maxClients(gz(httpTraceAll(api.ListMultipartUploadsHandler))))).Queries("uploads", "")
// ListObjectsV2M
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("listobjectsv2M", maxClients(httpTraceAll(api.ListObjectsV2MHandler)))).Queries("list-type", "2", "metadata", "true")
collectAPIStats("listobjectsv2M", maxClients(gz(httpTraceAll(api.ListObjectsV2MHandler))))).Queries("list-type", "2", "metadata", "true")
// ListObjectsV2
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("listobjectsv2", maxClients(httpTraceAll(api.ListObjectsV2Handler)))).Queries("list-type", "2")
collectAPIStats("listobjectsv2", maxClients(gz(httpTraceAll(api.ListObjectsV2Handler))))).Queries("list-type", "2")
// ListObjectVersions
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("listobjectversions", maxClients(httpTraceAll(api.ListObjectVersionsHandler)))).Queries("versions", "")
collectAPIStats("listobjectversions", maxClients(gz(httpTraceAll(api.ListObjectVersionsHandler))))).Queries("versions", "")
// GetBucketPolicyStatus
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("getpolicystatus", maxClients(httpTraceAll(api.GetBucketPolicyStatusHandler)))).Queries("policyStatus", "")
collectAPIStats("getpolicystatus", maxClients(gz(httpTraceAll(api.GetBucketPolicyStatusHandler))))).Queries("policyStatus", "")
// PutBucketLifecycle
router.Methods(http.MethodPut).HandlerFunc(
collectAPIStats("putbucketlifecycle", maxClients(httpTraceAll(api.PutBucketLifecycleHandler)))).Queries("lifecycle", "")
collectAPIStats("putbucketlifecycle", maxClients(gz(httpTraceAll(api.PutBucketLifecycleHandler))))).Queries("lifecycle", "")
// PutBucketReplicationConfig
router.Methods(http.MethodPut).HandlerFunc(
collectAPIStats("putbucketreplicationconfiguration", maxClients(httpTraceAll(api.PutBucketReplicationConfigHandler)))).Queries("replication", "")
collectAPIStats("putbucketreplicationconfiguration", maxClients(gz(httpTraceAll(api.PutBucketReplicationConfigHandler))))).Queries("replication", "")
// PutBucketEncryption
router.Methods(http.MethodPut).HandlerFunc(
collectAPIStats("putbucketencryption", maxClients(httpTraceAll(api.PutBucketEncryptionHandler)))).Queries("encryption", "")
collectAPIStats("putbucketencryption", maxClients(gz(httpTraceAll(api.PutBucketEncryptionHandler))))).Queries("encryption", "")
// PutBucketPolicy
router.Methods(http.MethodPut).HandlerFunc(
collectAPIStats("putbucketpolicy", maxClients(httpTraceAll(api.PutBucketPolicyHandler)))).Queries("policy", "")
collectAPIStats("putbucketpolicy", maxClients(gz(httpTraceAll(api.PutBucketPolicyHandler))))).Queries("policy", "")
// PutBucketObjectLockConfig
router.Methods(http.MethodPut).HandlerFunc(
collectAPIStats("putbucketobjectlockconfig", maxClients(httpTraceAll(api.PutBucketObjectLockConfigHandler)))).Queries("object-lock", "")
collectAPIStats("putbucketobjectlockconfig", maxClients(gz(httpTraceAll(api.PutBucketObjectLockConfigHandler))))).Queries("object-lock", "")
// PutBucketTaggingHandler
router.Methods(http.MethodPut).HandlerFunc(
collectAPIStats("putbuckettagging", maxClients(httpTraceAll(api.PutBucketTaggingHandler)))).Queries("tagging", "")
collectAPIStats("putbuckettagging", maxClients(gz(httpTraceAll(api.PutBucketTaggingHandler))))).Queries("tagging", "")
// PutBucketVersioning
router.Methods(http.MethodPut).HandlerFunc(
collectAPIStats("putbucketversioning", maxClients(httpTraceAll(api.PutBucketVersioningHandler)))).Queries("versioning", "")
collectAPIStats("putbucketversioning", maxClients(gz(httpTraceAll(api.PutBucketVersioningHandler))))).Queries("versioning", "")
// PutBucketNotification
router.Methods(http.MethodPut).HandlerFunc(
collectAPIStats("putbucketnotification", maxClients(httpTraceAll(api.PutBucketNotificationHandler)))).Queries("notification", "")
collectAPIStats("putbucketnotification", maxClients(gz(httpTraceAll(api.PutBucketNotificationHandler))))).Queries("notification", "")
// ResetBucketReplicationState - MinIO extension API
router.Methods(http.MethodPut).HandlerFunc(
collectAPIStats("resetbucketreplicationstate", maxClients(httpTraceAll(api.ResetBucketReplicationStateHandler)))).Queries("replication-reset", "")
collectAPIStats("resetbucketreplicationstate", maxClients(gz(httpTraceAll(api.ResetBucketReplicationStateHandler))))).Queries("replication-reset", "")
// PutBucket
router.Methods(http.MethodPut).HandlerFunc(
collectAPIStats("putbucket", maxClients(httpTraceAll(api.PutBucketHandler))))
collectAPIStats("putbucket", maxClients(gz(httpTraceAll(api.PutBucketHandler)))))
// HeadBucket
router.Methods(http.MethodHead).HandlerFunc(
collectAPIStats("headbucket", maxClients(httpTraceAll(api.HeadBucketHandler))))
collectAPIStats("headbucket", maxClients(gz(httpTraceAll(api.HeadBucketHandler)))))
// PostPolicy
router.Methods(http.MethodPost).HeadersRegexp(xhttp.ContentType, "multipart/form-data*").HandlerFunc(
collectAPIStats("postpolicybucket", maxClients(httpTraceHdrs(api.PostPolicyBucketHandler))))
collectAPIStats("postpolicybucket", maxClients(gz(httpTraceHdrs(api.PostPolicyBucketHandler)))))
// DeleteMultipleObjects
router.Methods(http.MethodPost).HandlerFunc(
collectAPIStats("deletemultipleobjects", maxClients(httpTraceAll(api.DeleteMultipleObjectsHandler)))).Queries("delete", "")
collectAPIStats("deletemultipleobjects", maxClients(gz(httpTraceAll(api.DeleteMultipleObjectsHandler))))).Queries("delete", "")
// DeleteBucketPolicy
router.Methods(http.MethodDelete).HandlerFunc(
collectAPIStats("deletebucketpolicy", maxClients(httpTraceAll(api.DeleteBucketPolicyHandler)))).Queries("policy", "")
collectAPIStats("deletebucketpolicy", maxClients(gz(httpTraceAll(api.DeleteBucketPolicyHandler))))).Queries("policy", "")
// DeleteBucketReplication
router.Methods(http.MethodDelete).HandlerFunc(
collectAPIStats("deletebucketreplicationconfiguration", maxClients(httpTraceAll(api.DeleteBucketReplicationConfigHandler)))).Queries("replication", "")
collectAPIStats("deletebucketreplicationconfiguration", maxClients(gz(httpTraceAll(api.DeleteBucketReplicationConfigHandler))))).Queries("replication", "")
// DeleteBucketLifecycle
router.Methods(http.MethodDelete).HandlerFunc(
collectAPIStats("deletebucketlifecycle", maxClients(httpTraceAll(api.DeleteBucketLifecycleHandler)))).Queries("lifecycle", "")
collectAPIStats("deletebucketlifecycle", maxClients(gz(httpTraceAll(api.DeleteBucketLifecycleHandler))))).Queries("lifecycle", "")
// DeleteBucketEncryption
router.Methods(http.MethodDelete).HandlerFunc(
collectAPIStats("deletebucketencryption", maxClients(httpTraceAll(api.DeleteBucketEncryptionHandler)))).Queries("encryption", "")
collectAPIStats("deletebucketencryption", maxClients(gz(httpTraceAll(api.DeleteBucketEncryptionHandler))))).Queries("encryption", "")
// DeleteBucket
router.Methods(http.MethodDelete).HandlerFunc(
collectAPIStats("deletebucket", maxClients(httpTraceAll(api.DeleteBucketHandler))))
collectAPIStats("deletebucket", maxClients(gz(httpTraceAll(api.DeleteBucketHandler)))))
// MinIO extension API for replication.
//
// GetBucketReplicationMetrics
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("getbucketreplicationmetrics", maxClients(httpTraceAll(api.GetBucketReplicationMetricsHandler)))).Queries("replication-metrics", "")
collectAPIStats("getbucketreplicationmetrics", maxClients(gz(httpTraceAll(api.GetBucketReplicationMetricsHandler))))).Queries("replication-metrics", "")
// Register rejected bucket APIs
for _, r := range rejectedBucketAPIs {
@@ -440,23 +449,23 @@ func registerAPIRouter(router *mux.Router) {
// S3 ListObjectsV1 (Legacy)
router.Methods(http.MethodGet).HandlerFunc(
collectAPIStats("listobjectsv1", maxClients(httpTraceAll(api.ListObjectsV1Handler))))
collectAPIStats("listobjectsv1", maxClients(gz(httpTraceAll(api.ListObjectsV1Handler)))))
}
/// Root operation
// ListenNotification
apiRouter.Methods(http.MethodGet).Path(SlashSeparator).HandlerFunc(
collectAPIStats("listennotification", maxClients(httpTraceAll(api.ListenNotificationHandler)))).Queries("events", "{events:.*}")
collectAPIStats("listennotification", maxClients(gz(httpTraceAll(api.ListenNotificationHandler))))).Queries("events", "{events:.*}")
// ListBuckets
apiRouter.Methods(http.MethodGet).Path(SlashSeparator).HandlerFunc(
collectAPIStats("listbuckets", maxClients(httpTraceAll(api.ListBucketsHandler))))
collectAPIStats("listbuckets", maxClients(gz(httpTraceAll(api.ListBucketsHandler)))))
// S3 browser with signature v4 adds '//' for ListBuckets request, so rather
// than failing with UnknownAPIRequest we simply handle it for now.
apiRouter.Methods(http.MethodGet).Path(SlashSeparator + SlashSeparator).HandlerFunc(
collectAPIStats("listbuckets", maxClients(httpTraceAll(api.ListBucketsHandler))))
collectAPIStats("listbuckets", maxClients(gz(httpTraceAll(api.ListBucketsHandler)))))
// If none of the routes match add default error handler routes
apiRouter.NotFoundHandler = collectAPIStats("notfound", httpTraceAll(errorResponseHandler))
+132 -133
View File
File diff suppressed because one or more lines are too long
+54 -107
View File
@@ -27,6 +27,7 @@ import (
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"sync/atomic"
@@ -61,13 +62,13 @@ func isRequestSignatureV2(r *http.Request) bool {
// Verify if request has AWS PreSign Version '4'.
func isRequestPresignedSignatureV4(r *http.Request) bool {
_, ok := r.URL.Query()[xhttp.AmzCredential]
_, ok := r.Form[xhttp.AmzCredential]
return ok
}
// Verify request has AWS PreSign Version '2'.
func isRequestPresignedSignatureV2(r *http.Request) bool {
_, ok := r.URL.Query()[xhttp.AmzAccessKeyID]
_, ok := r.Form[xhttp.AmzAccessKeyID]
return ok
}
@@ -102,6 +103,14 @@ const (
// Get request authentication type.
func getRequestAuthType(r *http.Request) authType {
if r.URL != nil {
var err error
r.Form, err = url.ParseQuery(r.URL.RawQuery)
if err != nil {
logger.LogIf(r.Context(), err)
return authTypeUnknown
}
}
if isRequestSignatureV2(r) {
return authTypeSignedV2
} else if isRequestPresignedSignatureV2(r) {
@@ -116,7 +125,7 @@ func getRequestAuthType(r *http.Request) authType {
return authTypeJWT
} else if isRequestPostPolicySignatureV4(r) {
return authTypePostPolicy
} else if _, ok := r.URL.Query()[xhttp.Action]; ok {
} else if _, ok := r.Form[xhttp.Action]; ok {
return authTypeSTS
} else if _, ok := r.Header[xhttp.Authorization]; !ok {
return authTypeAnonymous
@@ -146,12 +155,7 @@ func validateAdminSignature(ctx context.Context, r *http.Request, region string)
return cred, nil, owner, s3Err
}
claims, s3Err := checkClaimsFromToken(r, cred)
if s3Err != ErrNone {
return cred, nil, owner, s3Err
}
return cred, claims, owner, ErrNone
return cred, cred.Claims, owner, ErrNone
}
// checkAdminRequestAuth checks for authentication and authorization for the incoming
@@ -183,7 +187,7 @@ func getSessionToken(r *http.Request) (token string) {
if token != "" {
return token
}
return r.URL.Query().Get(xhttp.AmzSecurityToken)
return r.Form.Get(xhttp.AmzSecurityToken)
}
// Fetch claims in the security token returned by the client, doesn't return
@@ -213,27 +217,17 @@ func getClaimsFromToken(token string) (map[string]interface{}, error) {
return nil, errAuthentication
}
if globalPolicyOPA == nil {
// If OPA is not set and if ldap claim key is set, allow the claim.
if _, ok := claims.MapClaims[ldapUser]; ok {
return claims.Map(), nil
}
// If OPA is set, return without any further checks.
if globalPolicyOPA != nil {
return claims.Map(), nil
}
// If OPA is not set, session token should
// have a policy and its mandatory, reject
// requests without policy claim.
_, pokOpenID := claims.MapClaims[iamPolicyClaimNameOpenID()]
_, pokSA := claims.MapClaims[iamPolicyClaimNameSA()]
if !pokOpenID && !pokSA {
return nil, errAuthentication
}
sp, spok := claims.Lookup(iampolicy.SessionPolicyName)
if !spok {
return claims.Map(), nil
}
// Check if a session policy is set. If so, decode it here.
sp, spok := claims.Lookup(iampolicy.SessionPolicyName)
if spok {
// Looks like subpolicy is set and is a string, if set then its
// base64 encoded, decode it. Decoding fails reject such requests.
// base64 encoded, decode it. Decoding fails reject such
// requests.
spBytes, err := base64.StdEncoding.DecodeString(sp)
if err != nil {
// Base64 decoding fails, we should log to indicate
@@ -244,6 +238,19 @@ func getClaimsFromToken(token string) (map[string]interface{}, error) {
claims.MapClaims[iampolicy.SessionPolicyName] = string(spBytes)
}
// If LDAP claim key is set, return here.
if _, ok := claims.MapClaims[ldapUser]; ok {
return claims.Map(), nil
}
// Session token must have a policy, reject requests without policy
// claim.
_, pokOpenID := claims.MapClaims[iamPolicyClaimNameOpenID()]
_, pokSA := claims.MapClaims[iamPolicyClaimNameSA()]
if !pokOpenID && !pokSA {
return nil, errAuthentication
}
return claims.Map(), nil
}
@@ -306,12 +313,6 @@ func checkRequestAuthTypeCredential(ctx context.Context, r *http.Request, action
return cred, owner, s3Err
}
var claims map[string]interface{}
claims, s3Err = checkClaimsFromToken(r, cred)
if s3Err != ErrNone {
return cred, owner, s3Err
}
// LocationConstraint is valid only for CreateBucketAction.
var locationConstraint string
if action == policy.CreateBucketAction {
@@ -376,10 +377,10 @@ func checkRequestAuthTypeCredential(ctx context.Context, r *http.Request, action
Groups: cred.Groups,
Action: iampolicy.Action(action),
BucketName: bucketName,
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
ObjectName: objectName,
IsOwner: owner,
Claims: claims,
Claims: cred.Claims,
}) {
// Request is allowed return the appropriate access key.
return cred, owner, ErrNone
@@ -393,10 +394,10 @@ func checkRequestAuthTypeCredential(ctx context.Context, r *http.Request, action
Groups: cred.Groups,
Action: iampolicy.ListBucketAction,
BucketName: bucketName,
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
ObjectName: objectName,
IsOwner: owner,
Claims: claims,
Claims: cred.Claims,
}) {
// Request is allowed return the appropriate access key.
return cred, owner, ErrNone
@@ -441,7 +442,7 @@ func isReqAuthenticated(ctx context.Context, r *http.Request, region string, sty
// Do not verify 'X-Amz-Content-Sha256' if skipSHA256.
var contentSHA256 []byte
if skipSHA256 := skipContentSha256Cksum(r); !skipSHA256 && isRequestPresignedSignatureV4(r) {
if sha256Sum, ok := r.URL.Query()[xhttp.AmzContentSha256]; ok && len(sha256Sum) > 0 {
if sha256Sum, ok := r.Form[xhttp.AmzContentSha256]; ok && len(sha256Sum) > 0 {
contentSHA256, err = hex.DecodeString(sha256Sum[0])
if err != nil {
return ErrContentSHA256Mismatch
@@ -486,20 +487,7 @@ func setAuthHandler(h http.Handler) http.Handler {
// handler for validating incoming authorization headers.
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
aType := getRequestAuthType(r)
if isSupportedS3AuthType(aType) {
// Let top level caller validate for anonymous and known signed requests.
h.ServeHTTP(w, r)
return
} else if aType == authTypeJWT {
// Validate Authorization header if its valid for JWT request.
if _, _, authErr := webRequestAuthenticate(r); authErr != nil {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(authErr.Error()))
return
}
h.ServeHTTP(w, r)
return
} else if aType == authTypeSTS {
if isSupportedS3AuthType(aType) || aType == authTypeJWT || aType == authTypeSTS {
h.ServeHTTP(w, r)
return
}
@@ -508,75 +496,39 @@ func setAuthHandler(h http.Handler) http.Handler {
})
}
func validateSignature(atype authType, r *http.Request) (auth.Credentials, bool, map[string]interface{}, APIErrorCode) {
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, nil, ErrSignatureVersionNotSupported
return cred, owner, ErrSignatureVersionNotSupported
case authTypeSignedV2, authTypePresignedV2:
if s3Err = isReqAuthenticatedV2(r); s3Err != ErrNone {
return cred, owner, nil, s3Err
return cred, owner, s3Err
}
cred, owner, s3Err = getReqAccessKeyV2(r)
case authTypePresigned, authTypeSigned:
region := globalServerRegion
if s3Err = isReqAuthenticated(GlobalContext, r, region, serviceS3); s3Err != ErrNone {
return cred, owner, nil, s3Err
return cred, owner, s3Err
}
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
}
if s3Err != ErrNone {
return cred, owner, nil, s3Err
return cred, owner, s3Err
}
claims, s3Err := checkClaimsFromToken(r, cred)
if s3Err != ErrNone {
return cred, owner, nil, s3Err
}
return cred, owner, claims, ErrNone
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, claims map[string]interface{}) (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
if cred.AccessKey == "" {
conditions := getConditionValues(r, "", "", nil)
conditions["object-lock-mode"] = []string{string(retMode)}
conditions["object-lock-retain-until-date"] = []string{retDate.Format(time.RFC3339)}
if retDays > 0 {
conditions["object-lock-remaining-retention-days"] = []string{strconv.Itoa(retDays)}
}
if retMode == objectlock.RetGovernance && byPassSet {
byPassSet = globalPolicySys.IsAllowed(policy.Args{
AccountName: cred.AccessKey,
Groups: cred.Groups,
Action: policy.BypassGovernanceRetentionAction,
BucketName: bucketName,
ConditionValues: conditions,
IsOwner: false,
ObjectName: objectName,
})
}
if globalPolicySys.IsAllowed(policy.Args{
AccountName: cred.AccessKey,
Groups: cred.Groups,
Action: policy.PutObjectRetentionAction,
BucketName: bucketName,
ConditionValues: conditions,
IsOwner: false,
ObjectName: objectName,
}) {
retSet = true
}
if byPassSet || retSet {
return ErrNone
}
return ErrAccessDenied
}
conditions := getConditionValues(r, "", cred.AccessKey, claims)
conditions := getConditionValues(r, "", cred.AccessKey, cred.Claims)
conditions["object-lock-mode"] = []string{string(retMode)}
conditions["object-lock-retain-until-date"] = []string{retDate.Format(time.RFC3339)}
if retDays > 0 {
@@ -591,7 +543,7 @@ func isPutRetentionAllowed(bucketName, objectName string, retDays int, retDate t
ObjectName: objectName,
ConditionValues: conditions,
IsOwner: owner,
Claims: claims,
Claims: cred.Claims,
})
}
if globalIAMSys.IsAllowed(iampolicy.Args{
@@ -602,7 +554,7 @@ func isPutRetentionAllowed(bucketName, objectName string, retDays int, retDate t
ConditionValues: conditions,
ObjectName: objectName,
IsOwner: owner,
Claims: claims,
Claims: cred.Claims,
}) {
retSet = true
}
@@ -631,11 +583,6 @@ func isPutActionAllowed(ctx context.Context, atype authType, bucketName, objectN
return s3Err
}
claims, s3Err := checkClaimsFromToken(r, cred)
if s3Err != ErrNone {
return s3Err
}
if cred.AccessKey != "" {
logger.GetReqInfo(ctx).AccessKey = cred.AccessKey
}
@@ -669,10 +616,10 @@ func isPutActionAllowed(ctx context.Context, atype authType, bucketName, objectN
Groups: cred.Groups,
Action: action,
BucketName: bucketName,
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
ObjectName: objectName,
IsOwner: owner,
Claims: claims,
Claims: cred.Claims,
}) {
return ErrNone
}
+5
View File
@@ -38,6 +38,7 @@ func TestGetRequestAuthType(t *testing.T) {
req *http.Request
authT authType
}
nopCloser := ioutil.NopCloser(io.LimitReader(&nullReader{}, 1024))
testCases := []testCase{
// Test case - 1
// Check for generic signature v4 header.
@@ -54,6 +55,7 @@ func TestGetRequestAuthType(t *testing.T) {
"Content-Encoding": []string{streamingContentEncoding},
},
Method: http.MethodPut,
Body: nopCloser,
},
authT: authTypeStreamingSigned,
},
@@ -113,6 +115,7 @@ func TestGetRequestAuthType(t *testing.T) {
"Content-Type": []string{"multipart/form-data"},
},
Method: http.MethodPost,
Body: nopCloser,
},
authT: authTypePostPolicy,
},
@@ -220,6 +223,7 @@ func TestIsRequestPresignedSignatureV2(t *testing.T) {
q := inputReq.URL.Query()
q.Add(testCase.inputQueryKey, testCase.inputQueryValue)
inputReq.URL.RawQuery = q.Encode()
inputReq.ParseForm()
actualResult := isRequestPresignedSignatureV2(inputReq)
if testCase.expectedResult != actualResult {
@@ -254,6 +258,7 @@ func TestIsRequestPresignedSignatureV4(t *testing.T) {
q := inputReq.URL.Query()
q.Add(testCase.inputQueryKey, testCase.inputQueryValue)
inputReq.URL.RawQuery = q.Encode()
inputReq.ParseForm()
actualResult := isRequestPresignedSignatureV4(inputReq)
if testCase.expectedResult != actualResult {
+55 -36
View File
@@ -48,18 +48,23 @@ const (
type healingTracker struct {
disk StorageAPI `msg:"-"`
ID string
PoolIndex int
SetIndex int
DiskIndex int
Path string
Endpoint string
Started time.Time
LastUpdate time.Time
ObjectsHealed uint64
ObjectsFailed uint64
BytesDone uint64
BytesFailed uint64
ID string
PoolIndex int
SetIndex int
DiskIndex int
Path string
Endpoint string
Started time.Time
LastUpdate time.Time
ObjectsTotalCount uint64
ObjectsTotalSize uint64
ItemsHealed uint64
ItemsFailed uint64
BytesDone uint64
BytesFailed uint64
// Last object scanned.
Bucket string `json:"-"`
@@ -67,10 +72,10 @@ type healingTracker struct {
// Numbers when current bucket started healing,
// for resuming with correct numbers.
ResumeObjectsHealed uint64 `json:"-"`
ResumeObjectsFailed uint64 `json:"-"`
ResumeBytesDone uint64 `json:"-"`
ResumeBytesFailed uint64 `json:"-"`
ResumeItemsHealed uint64 `json:"-"`
ResumeItemsFailed uint64 `json:"-"`
ResumeBytesDone uint64 `json:"-"`
ResumeBytesFailed uint64 `json:"-"`
// Filled on startup/restarts.
QueuedBuckets []string
@@ -175,8 +180,8 @@ func (h *healingTracker) isHealed(bucket string) bool {
// resume will reset progress to the numbers at the start of the bucket.
func (h *healingTracker) resume() {
h.ObjectsHealed = h.ResumeObjectsHealed
h.ObjectsFailed = h.ResumeObjectsFailed
h.ItemsHealed = h.ResumeItemsHealed
h.ItemsFailed = h.ResumeItemsFailed
h.BytesDone = h.ResumeBytesDone
h.BytesFailed = h.ResumeBytesFailed
}
@@ -184,8 +189,8 @@ func (h *healingTracker) resume() {
// bucketDone should be called when a bucket is done healing.
// Adds the bucket to the list of healed buckets and updates resume numbers.
func (h *healingTracker) bucketDone(bucket string) {
h.ResumeObjectsHealed = h.ObjectsHealed
h.ResumeObjectsFailed = h.ObjectsFailed
h.ResumeItemsHealed = h.ItemsHealed
h.ResumeItemsFailed = h.ItemsFailed
h.ResumeBytesDone = h.BytesDone
h.ResumeBytesFailed = h.BytesFailed
h.HealedBuckets = append(h.HealedBuckets, bucket)
@@ -220,22 +225,28 @@ func (h *healingTracker) printTo(writer io.Writer) {
// toHealingDisk converts the information to madmin.HealingDisk
func (h *healingTracker) toHealingDisk() madmin.HealingDisk {
return madmin.HealingDisk{
ID: h.ID,
Endpoint: h.Endpoint,
PoolIndex: h.PoolIndex,
SetIndex: h.SetIndex,
DiskIndex: h.DiskIndex,
Path: h.Path,
Started: h.Started.UTC(),
LastUpdate: h.LastUpdate.UTC(),
ObjectsHealed: h.ObjectsHealed,
ObjectsFailed: h.ObjectsFailed,
BytesDone: h.BytesDone,
BytesFailed: h.BytesFailed,
Bucket: h.Bucket,
Object: h.Object,
QueuedBuckets: h.QueuedBuckets,
HealedBuckets: h.HealedBuckets,
ID: h.ID,
Endpoint: h.Endpoint,
PoolIndex: h.PoolIndex,
SetIndex: h.SetIndex,
DiskIndex: h.DiskIndex,
Path: h.Path,
Started: h.Started.UTC(),
LastUpdate: h.LastUpdate.UTC(),
ObjectsTotalCount: h.ObjectsTotalCount,
ObjectsTotalSize: h.ObjectsTotalSize,
ItemsHealed: h.ItemsHealed,
ItemsFailed: h.ItemsFailed,
BytesDone: h.BytesDone,
BytesFailed: h.BytesFailed,
Bucket: h.Bucket,
Object: h.Object,
QueuedBuckets: h.QueuedBuckets,
HealedBuckets: h.HealedBuckets,
ObjectsHealed: h.ItemsHealed, // Deprecated July 2021
ObjectsFailed: h.ItemsFailed, // Deprecated July 2021
}
}
@@ -413,6 +424,14 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools, bgSeq
tracker = newHealingTracker(disk)
}
// Load bucket totals
cache := dataUsageCache{}
if err := cache.load(ctx, z.serverPools[i].sets[setIndex], dataUsageCacheName); err == nil {
dataUsageInfo := cache.dui(dataUsageRoot, nil)
tracker.ObjectsTotalCount = dataUsageInfo.ObjectsTotalCount
tracker.ObjectsTotalSize = dataUsageInfo.ObjectsTotalSize
}
tracker.PoolIndex, tracker.SetIndex, tracker.DiskIndex = disk.GetDiskLoc()
tracker.setQueuedBuckets(buckets)
if err := tracker.save(ctx); err != nil {
+107 -57
View File
@@ -72,16 +72,28 @@ func (z *healingTracker) DecodeMsg(dc *msgp.Reader) (err error) {
err = msgp.WrapError(err, "LastUpdate")
return
}
case "ObjectsHealed":
z.ObjectsHealed, err = dc.ReadUint64()
case "ObjectsTotalCount":
z.ObjectsTotalCount, err = dc.ReadUint64()
if err != nil {
err = msgp.WrapError(err, "ObjectsHealed")
err = msgp.WrapError(err, "ObjectsTotalCount")
return
}
case "ObjectsFailed":
z.ObjectsFailed, err = dc.ReadUint64()
case "ObjectsTotalSize":
z.ObjectsTotalSize, err = dc.ReadUint64()
if err != nil {
err = msgp.WrapError(err, "ObjectsFailed")
err = msgp.WrapError(err, "ObjectsTotalSize")
return
}
case "ItemsHealed":
z.ItemsHealed, err = dc.ReadUint64()
if err != nil {
err = msgp.WrapError(err, "ItemsHealed")
return
}
case "ItemsFailed":
z.ItemsFailed, err = dc.ReadUint64()
if err != nil {
err = msgp.WrapError(err, "ItemsFailed")
return
}
case "BytesDone":
@@ -108,16 +120,16 @@ func (z *healingTracker) DecodeMsg(dc *msgp.Reader) (err error) {
err = msgp.WrapError(err, "Object")
return
}
case "ResumeObjectsHealed":
z.ResumeObjectsHealed, err = dc.ReadUint64()
case "ResumeItemsHealed":
z.ResumeItemsHealed, err = dc.ReadUint64()
if err != nil {
err = msgp.WrapError(err, "ResumeObjectsHealed")
err = msgp.WrapError(err, "ResumeItemsHealed")
return
}
case "ResumeObjectsFailed":
z.ResumeObjectsFailed, err = dc.ReadUint64()
case "ResumeItemsFailed":
z.ResumeItemsFailed, err = dc.ReadUint64()
if err != nil {
err = msgp.WrapError(err, "ResumeObjectsFailed")
err = msgp.WrapError(err, "ResumeItemsFailed")
return
}
case "ResumeBytesDone":
@@ -183,9 +195,9 @@ func (z *healingTracker) DecodeMsg(dc *msgp.Reader) (err error) {
// EncodeMsg implements msgp.Encodable
func (z *healingTracker) EncodeMsg(en *msgp.Writer) (err error) {
// map header, size 20
// map header, size 22
// write "ID"
err = en.Append(0xde, 0x0, 0x14, 0xa2, 0x49, 0x44)
err = en.Append(0xde, 0x0, 0x16, 0xa2, 0x49, 0x44)
if err != nil {
return
}
@@ -264,24 +276,44 @@ func (z *healingTracker) EncodeMsg(en *msgp.Writer) (err error) {
err = msgp.WrapError(err, "LastUpdate")
return
}
// write "ObjectsHealed"
err = en.Append(0xad, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
// write "ObjectsTotalCount"
err = en.Append(0xb1, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x75, 0x6e, 0x74)
if err != nil {
return
}
err = en.WriteUint64(z.ObjectsHealed)
err = en.WriteUint64(z.ObjectsTotalCount)
if err != nil {
err = msgp.WrapError(err, "ObjectsHealed")
err = msgp.WrapError(err, "ObjectsTotalCount")
return
}
// write "ObjectsFailed"
err = en.Append(0xad, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
// write "ObjectsTotalSize"
err = en.Append(0xb0, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x53, 0x69, 0x7a, 0x65)
if err != nil {
return
}
err = en.WriteUint64(z.ObjectsFailed)
err = en.WriteUint64(z.ObjectsTotalSize)
if err != nil {
err = msgp.WrapError(err, "ObjectsFailed")
err = msgp.WrapError(err, "ObjectsTotalSize")
return
}
// write "ItemsHealed"
err = en.Append(0xab, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
if err != nil {
return
}
err = en.WriteUint64(z.ItemsHealed)
if err != nil {
err = msgp.WrapError(err, "ItemsHealed")
return
}
// write "ItemsFailed"
err = en.Append(0xab, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
if err != nil {
return
}
err = en.WriteUint64(z.ItemsFailed)
if err != nil {
err = msgp.WrapError(err, "ItemsFailed")
return
}
// write "BytesDone"
@@ -324,24 +356,24 @@ func (z *healingTracker) EncodeMsg(en *msgp.Writer) (err error) {
err = msgp.WrapError(err, "Object")
return
}
// write "ResumeObjectsHealed"
err = en.Append(0xb3, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
// write "ResumeItemsHealed"
err = en.Append(0xb1, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
if err != nil {
return
}
err = en.WriteUint64(z.ResumeObjectsHealed)
err = en.WriteUint64(z.ResumeItemsHealed)
if err != nil {
err = msgp.WrapError(err, "ResumeObjectsHealed")
err = msgp.WrapError(err, "ResumeItemsHealed")
return
}
// write "ResumeObjectsFailed"
err = en.Append(0xb3, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
// write "ResumeItemsFailed"
err = en.Append(0xb1, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
if err != nil {
return
}
err = en.WriteUint64(z.ResumeObjectsFailed)
err = en.WriteUint64(z.ResumeItemsFailed)
if err != nil {
err = msgp.WrapError(err, "ResumeObjectsFailed")
err = msgp.WrapError(err, "ResumeItemsFailed")
return
}
// write "ResumeBytesDone"
@@ -404,9 +436,9 @@ func (z *healingTracker) EncodeMsg(en *msgp.Writer) (err error) {
// MarshalMsg implements msgp.Marshaler
func (z *healingTracker) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize())
// map header, size 20
// map header, size 22
// string "ID"
o = append(o, 0xde, 0x0, 0x14, 0xa2, 0x49, 0x44)
o = append(o, 0xde, 0x0, 0x16, 0xa2, 0x49, 0x44)
o = msgp.AppendString(o, z.ID)
// string "PoolIndex"
o = append(o, 0xa9, 0x50, 0x6f, 0x6f, 0x6c, 0x49, 0x6e, 0x64, 0x65, 0x78)
@@ -429,12 +461,18 @@ func (z *healingTracker) MarshalMsg(b []byte) (o []byte, err error) {
// string "LastUpdate"
o = append(o, 0xaa, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65)
o = msgp.AppendTime(o, z.LastUpdate)
// string "ObjectsHealed"
o = append(o, 0xad, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
o = msgp.AppendUint64(o, z.ObjectsHealed)
// string "ObjectsFailed"
o = append(o, 0xad, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
o = msgp.AppendUint64(o, z.ObjectsFailed)
// string "ObjectsTotalCount"
o = append(o, 0xb1, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x75, 0x6e, 0x74)
o = msgp.AppendUint64(o, z.ObjectsTotalCount)
// string "ObjectsTotalSize"
o = append(o, 0xb0, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x53, 0x69, 0x7a, 0x65)
o = msgp.AppendUint64(o, z.ObjectsTotalSize)
// string "ItemsHealed"
o = append(o, 0xab, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
o = msgp.AppendUint64(o, z.ItemsHealed)
// string "ItemsFailed"
o = append(o, 0xab, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
o = msgp.AppendUint64(o, z.ItemsFailed)
// string "BytesDone"
o = append(o, 0xa9, 0x42, 0x79, 0x74, 0x65, 0x73, 0x44, 0x6f, 0x6e, 0x65)
o = msgp.AppendUint64(o, z.BytesDone)
@@ -447,12 +485,12 @@ func (z *healingTracker) MarshalMsg(b []byte) (o []byte, err error) {
// string "Object"
o = append(o, 0xa6, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74)
o = msgp.AppendString(o, z.Object)
// string "ResumeObjectsHealed"
o = append(o, 0xb3, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
o = msgp.AppendUint64(o, z.ResumeObjectsHealed)
// string "ResumeObjectsFailed"
o = append(o, 0xb3, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
o = msgp.AppendUint64(o, z.ResumeObjectsFailed)
// string "ResumeItemsHealed"
o = append(o, 0xb1, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
o = msgp.AppendUint64(o, z.ResumeItemsHealed)
// string "ResumeItemsFailed"
o = append(o, 0xb1, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
o = msgp.AppendUint64(o, z.ResumeItemsFailed)
// string "ResumeBytesDone"
o = append(o, 0xaf, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x44, 0x6f, 0x6e, 0x65)
o = msgp.AppendUint64(o, z.ResumeBytesDone)
@@ -540,16 +578,28 @@ func (z *healingTracker) UnmarshalMsg(bts []byte) (o []byte, err error) {
err = msgp.WrapError(err, "LastUpdate")
return
}
case "ObjectsHealed":
z.ObjectsHealed, bts, err = msgp.ReadUint64Bytes(bts)
case "ObjectsTotalCount":
z.ObjectsTotalCount, bts, err = msgp.ReadUint64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "ObjectsHealed")
err = msgp.WrapError(err, "ObjectsTotalCount")
return
}
case "ObjectsFailed":
z.ObjectsFailed, bts, err = msgp.ReadUint64Bytes(bts)
case "ObjectsTotalSize":
z.ObjectsTotalSize, bts, err = msgp.ReadUint64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "ObjectsFailed")
err = msgp.WrapError(err, "ObjectsTotalSize")
return
}
case "ItemsHealed":
z.ItemsHealed, bts, err = msgp.ReadUint64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "ItemsHealed")
return
}
case "ItemsFailed":
z.ItemsFailed, bts, err = msgp.ReadUint64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "ItemsFailed")
return
}
case "BytesDone":
@@ -576,16 +626,16 @@ func (z *healingTracker) UnmarshalMsg(bts []byte) (o []byte, err error) {
err = msgp.WrapError(err, "Object")
return
}
case "ResumeObjectsHealed":
z.ResumeObjectsHealed, bts, err = msgp.ReadUint64Bytes(bts)
case "ResumeItemsHealed":
z.ResumeItemsHealed, bts, err = msgp.ReadUint64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "ResumeObjectsHealed")
err = msgp.WrapError(err, "ResumeItemsHealed")
return
}
case "ResumeObjectsFailed":
z.ResumeObjectsFailed, bts, err = msgp.ReadUint64Bytes(bts)
case "ResumeItemsFailed":
z.ResumeItemsFailed, bts, err = msgp.ReadUint64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "ResumeObjectsFailed")
err = msgp.WrapError(err, "ResumeItemsFailed")
return
}
case "ResumeBytesDone":
@@ -652,7 +702,7 @@ func (z *healingTracker) UnmarshalMsg(bts []byte) (o []byte, err error) {
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z *healingTracker) Msgsize() (s int) {
s = 3 + 3 + msgp.StringPrefixSize + len(z.ID) + 10 + msgp.IntSize + 9 + msgp.IntSize + 10 + msgp.IntSize + 5 + msgp.StringPrefixSize + len(z.Path) + 9 + msgp.StringPrefixSize + len(z.Endpoint) + 8 + msgp.TimeSize + 11 + msgp.TimeSize + 14 + msgp.Uint64Size + 14 + msgp.Uint64Size + 10 + msgp.Uint64Size + 12 + msgp.Uint64Size + 7 + msgp.StringPrefixSize + len(z.Bucket) + 7 + msgp.StringPrefixSize + len(z.Object) + 20 + msgp.Uint64Size + 20 + msgp.Uint64Size + 16 + msgp.Uint64Size + 18 + msgp.Uint64Size + 14 + msgp.ArrayHeaderSize
s = 3 + 3 + msgp.StringPrefixSize + len(z.ID) + 10 + msgp.IntSize + 9 + msgp.IntSize + 10 + msgp.IntSize + 5 + msgp.StringPrefixSize + len(z.Path) + 9 + msgp.StringPrefixSize + len(z.Endpoint) + 8 + msgp.TimeSize + 11 + msgp.TimeSize + 18 + msgp.Uint64Size + 17 + msgp.Uint64Size + 12 + msgp.Uint64Size + 12 + msgp.Uint64Size + 10 + msgp.Uint64Size + 12 + msgp.Uint64Size + 7 + msgp.StringPrefixSize + len(z.Bucket) + 7 + msgp.StringPrefixSize + len(z.Object) + 18 + msgp.Uint64Size + 18 + msgp.Uint64Size + 16 + msgp.Uint64Size + 18 + msgp.Uint64Size + 14 + msgp.ArrayHeaderSize
for za0001 := range z.QueuedBuckets {
s += msgp.StringPrefixSize + len(z.QueuedBuckets[za0001])
}
+3 -1
View File
@@ -188,7 +188,9 @@ func verifyServerSystemConfig(ctx context.Context, endpointServerPools EndpointS
// after 5 retries start logging that servers are not reachable yet
if retries >= 5 {
logger.Info(fmt.Sprintf("Waiting for atleast %d remote servers to be online for bootstrap check", len(clnts)/2))
logger.Info(fmt.Sprintf("Following servers are currently offline or unreachable %s", offlineEndpoints))
if len(offlineEndpoints) > 0 {
logger.Info(fmt.Sprintf("Following servers are currently offline or unreachable %s", offlineEndpoints))
}
retries = 0 // reset to log again after 5 retries.
}
offlineEndpoints = nil
+59 -73
View File
@@ -19,7 +19,6 @@ package cmd
import (
"bytes"
"crypto/subtle"
"encoding/base64"
"encoding/json"
"encoding/xml"
@@ -246,7 +245,7 @@ func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter,
return
}
prefix, keyMarker, uploadIDMarker, delimiter, maxUploads, encodingType, errCode := getBucketMultipartResources(r.URL.Query())
prefix, keyMarker, uploadIDMarker, delimiter, maxUploads, encodingType, errCode := getBucketMultipartResources(r.Form)
if errCode != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(errCode), r.URL)
return
@@ -345,9 +344,6 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
// Set delimiter value for "s3:delimiter" policy conditionals.
r.Header.Set("delimiter", SlashSeparator)
// err will be nil here as we already called this function
// earlier in this request.
claims, _ := getClaimsFromToken(getSessionToken(r))
n := 0
// Use the following trick to filter in place
// https://github.com/golang/go/wiki/SliceTricks#filter-in-place
@@ -357,10 +353,10 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
Groups: cred.Groups,
Action: iampolicy.ListBucketAction,
BucketName: bucketInfo.Name,
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
IsOwner: owner,
ObjectName: "",
Claims: claims,
Claims: cred.Claims,
}) {
bucketsInfo[n] = bucketInfo
n++
@@ -454,6 +450,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
if api.CacheAPI() != nil {
getObjectInfoFn = api.CacheAPI().GetObjectInfo
}
var (
hasLockEnabled, replicateSync bool
goi ObjectInfo
@@ -464,6 +461,9 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
hasLockEnabled = true
}
versioned := globalBucketVersioningSys.Enabled(bucket)
suspended := globalBucketVersioningSys.Suspended(bucket)
dErrs := make([]DeleteError, len(deleteObjects.Objects))
oss := make([]*objSweeper, len(deleteObjects.Objects))
for index, object := range deleteObjects.Objects {
@@ -495,16 +495,24 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
}
}
oss[index] = newObjSweeper(bucket, object.ObjectName).WithVersion(multiDelete(object))
// Mutations of objects on versioning suspended buckets
// affect its null version. Through opts below we select
// the null version's remote object to delete if
// transitioned.
opts := oss[index].GetOpts()
goi, gerr = getObjectInfoFn(ctx, bucket, object.ObjectName, opts)
if gerr == nil {
oss[index].SetTransitionState(goi)
opts := ObjectOptions{
VersionID: object.VersionID,
Versioned: versioned,
VersionSuspended: suspended,
}
if replicateDeletes || object.VersionID != "" && hasLockEnabled || !globalTierConfigMgr.Empty() {
if !globalTierConfigMgr.Empty() && object.VersionID == "" && opts.VersionSuspended {
opts.VersionID = nullVersionID
}
goi, gerr = getObjectInfoFn(ctx, bucket, object.ObjectName, opts)
}
if !globalTierConfigMgr.Empty() {
oss[index] = newObjSweeper(bucket, object.ObjectName).WithVersion(opts.VersionID).WithVersioning(versioned, suspended)
oss[index].SetTransitionState(goi.TransitionedObject)
}
if replicateDeletes {
replicate, repsync := checkReplicateDelete(ctx, bucket, ObjectToDelete{
ObjectName: object.ObjectName,
@@ -526,18 +534,16 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
}
}
}
if object.VersionID != "" {
if hasLockEnabled {
if apiErrCode := enforceRetentionBypassForDelete(ctx, r, bucket, object, goi, gerr); apiErrCode != ErrNone {
apiErr := errorCodes.ToAPIErr(apiErrCode)
dErrs[index] = DeleteError{
Code: apiErr.Code,
Message: apiErr.Description,
Key: object.ObjectName,
VersionID: object.VersionID,
}
continue
if object.VersionID != "" && hasLockEnabled {
if apiErrCode := enforceRetentionBypassForDelete(ctx, r, bucket, object, goi, gerr); apiErrCode != ErrNone {
apiErr := errorCodes.ToAPIErr(apiErrCode)
dErrs[index] = DeleteError{
Code: apiErr.Code,
Message: apiErr.Description,
Key: object.ObjectName,
VersionID: object.VersionID,
}
continue
}
}
@@ -559,8 +565,8 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
deleteList := toNames(objectsToDelete)
dObjects, errs := deleteObjectsFn(ctx, bucket, deleteList, ObjectOptions{
Versioned: globalBucketVersioningSys.Enabled(bucket),
VersionSuspended: globalBucketVersioningSys.Suspended(bucket),
Versioned: versioned,
VersionSuspended: suspended,
})
deletedObjects := make([]DeletedObject, len(deleteObjects.Objects))
for i := range errs {
@@ -622,16 +628,12 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
}
// Clean up transitioned objects from remote tier
for _, os := range oss {
if os == nil { // skip objects that weren't deleted due to invalid versionID etc.
continue
}
logger.LogIf(ctx, os.Sweep())
}
// Notify deleted event for objects.
for _, dobj := range deletedObjects {
if dobj.ObjectName == "" {
continue
}
eventName := event.ObjectRemovedDelete
objInfo := ObjectInfo{
Name: dobj.ObjectName,
@@ -654,6 +656,14 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
Host: handlers.GetSourceIP(r),
})
}
// Clean up transitioned objects from remote tier
for _, os := range oss {
if os == nil { // skip objects that weren't deleted due to invalid versionID etc.
continue
}
logger.LogIf(ctx, os.Sweep())
}
}
// PutBucketHandler - PUT Bucket
@@ -899,41 +909,17 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
// Once signature is validated, check if the user has
// explicit permissions for the user.
{
token := formValues.Get(xhttp.AmzSecurityToken)
if token != "" && cred.AccessKey == "" {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNoAccessKey), r.URL)
return
}
if cred.IsServiceAccount() && token == "" {
token = cred.SessionToken
}
if subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidToken), r.URL)
return
}
// Extract claims if any.
claims, err := getClaimsFromToken(token)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey,
Action: iampolicy.PutObjectAction,
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
BucketName: bucket,
ObjectName: object,
IsOwner: globalActiveCred.AccessKey == cred.AccessKey,
Claims: claims,
}) {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return
}
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey,
Action: iampolicy.PutObjectAction,
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
BucketName: bucket,
ObjectName: object,
IsOwner: globalActiveCred.AccessKey == cred.AccessKey,
Claims: cred.Claims,
}) {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return
}
policyBytes, err := base64.StdEncoding.DecodeString(formValues.Get("Policy"))
@@ -1699,7 +1685,7 @@ func (api objectAPIHandlers) ResetBucketReplicationStateHandler(w http.ResponseW
vars := mux.Vars(r)
bucket := vars["bucket"]
durationStr := r.URL.Query().Get("older-than")
durationStr := r.Form.Get("older-than")
var (
days time.Duration
err error
+1 -1
View File
@@ -80,7 +80,7 @@ func (api objectAPIHandlers) PutBucketLifecycleHandler(w http.ResponseWriter, r
}
// Validate the transition storage ARNs
if err = validateTransitionTier(ctx, bucketLifecycle); err != nil {
if err = validateTransitionTier(bucketLifecycle); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
+97 -61
View File
@@ -24,9 +24,9 @@ import (
"fmt"
"io"
"net/http"
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/google/uuid"
@@ -84,6 +84,11 @@ type expiryState struct {
expiryCh chan expiryTask
}
// PendingTasks returns the number of pending ILM expiry tasks.
func (es *expiryState) PendingTasks() int {
return len(es.expiryCh)
}
func (es *expiryState) queueExpiryTask(oi ObjectInfo, rmVersion bool) {
select {
case <-GlobalContext.Done():
@@ -115,9 +120,16 @@ func initBackgroundExpiry(ctx context.Context, objectAPI ObjectLayer) {
}
type transitionState struct {
once sync.Once
// add future metrics here
once sync.Once
transitionCh chan ObjectInfo
ctx context.Context
objAPI ObjectLayer
mu sync.Mutex
numWorkers int
killCh chan struct{}
activeTasks int32
}
func (t *transitionState) queueTransitionTask(oi ObjectInfo) {
@@ -132,56 +144,77 @@ func (t *transitionState) queueTransitionTask(oi ObjectInfo) {
}
var (
globalTransitionState *transitionState
globalTransitionConcurrent = runtime.GOMAXPROCS(0) / 2
globalTransitionState *transitionState
)
func newTransitionState() *transitionState {
// fix minimum concurrent transition to 1 for single CPU setup
if globalTransitionConcurrent == 0 {
globalTransitionConcurrent = 1
}
func newTransitionState(ctx context.Context, objAPI ObjectLayer) *transitionState {
return &transitionState{
transitionCh: make(chan ObjectInfo, 10000),
ctx: ctx,
objAPI: objAPI,
killCh: make(chan struct{}),
}
}
// addWorker creates a new worker to process tasks
func (t *transitionState) addWorker(ctx context.Context, objectAPI ObjectLayer) {
// Add a new worker.
go func() {
for {
select {
case <-ctx.Done():
return
case oi, ok := <-t.transitionCh:
if !ok {
return
}
// PendingTasks returns the number of ILM transition tasks waiting for a worker
// goroutine.
func (t *transitionState) PendingTasks() int {
return len(globalTransitionState.transitionCh)
}
if err := transitionObject(ctx, objectAPI, oi); err != nil {
logger.LogIf(ctx, fmt.Errorf("Transition failed for %s/%s version:%s with %w", oi.Bucket, oi.Name, oi.VersionID, err))
}
// ActiveTasks returns the number of active (ongoing) ILM transition tasks.
func (t *transitionState) ActiveTasks() int {
return int(atomic.LoadInt32(&t.activeTasks))
}
// worker waits for transition tasks
func (t *transitionState) worker(ctx context.Context, objectAPI ObjectLayer) {
for {
select {
case <-t.killCh:
return
case <-ctx.Done():
return
case oi, ok := <-t.transitionCh:
if !ok {
return
}
atomic.AddInt32(&t.activeTasks, 1)
if err := transitionObject(ctx, objectAPI, oi); err != nil {
logger.LogIf(ctx, fmt.Errorf("Transition failed for %s/%s version:%s with %w", oi.Bucket, oi.Name, oi.VersionID, err))
}
atomic.AddInt32(&t.activeTasks, -1)
}
}()
}
}
// UpdateWorkers at the end of this function leaves n goroutines waiting for
// transition tasks
func (t *transitionState) UpdateWorkers(n int) {
t.mu.Lock()
defer t.mu.Unlock()
for t.numWorkers < n {
go t.worker(t.ctx, t.objAPI)
t.numWorkers++
}
for t.numWorkers > n {
go func() { t.killCh <- struct{}{} }()
t.numWorkers--
}
}
func initBackgroundTransition(ctx context.Context, objectAPI ObjectLayer) {
if globalTransitionState == nil {
return
}
// Start with globalTransitionConcurrent.
for i := 0; i < globalTransitionConcurrent; i++ {
globalTransitionState.addWorker(ctx, objectAPI)
}
globalTransitionState = newTransitionState(ctx, objectAPI)
n := globalAPIConfig.getTransitionWorkers()
globalTransitionState.UpdateWorkers(n)
}
var errInvalidStorageClass = errors.New("invalid storage class")
func validateTransitionTier(ctx context.Context, lfc *lifecycle.Lifecycle) error {
for _, rule := range lfc.Rules {
func validateTransitionTier(lc *lifecycle.Lifecycle) error {
for _, rule := range lc.Rules {
if rule.Transition.StorageClass == "" {
continue
}
@@ -219,9 +252,9 @@ func expireTransitionedObject(ctx context.Context, objectAPI ObjectLayer, oi *Ob
// When an object is past expiry or when a transitioned object is being
// deleted, 'mark' the data in the remote tier for delete.
entry := jentry{
ObjName: oi.transitionedObjName,
VersionID: oi.transitionVersionID,
TierName: oi.TransitionTier,
ObjName: oi.TransitionedObject.Name,
VersionID: oi.TransitionedObject.VersionID,
TierName: oi.TransitionedObject.Tier,
}
if err := globalTierJournal.AddEntry(entry); err != nil {
logger.LogIf(ctx, err)
@@ -269,13 +302,13 @@ func expireTransitionedObject(ctx context.Context, objectAPI ObjectLayer, oi *Ob
}
// generate an object name for transitioned object
func genTransitionObjName() (string, error) {
func genTransitionObjName(bucket string) (string, error) {
u, err := uuid.NewRandom()
if err != nil {
return "", err
}
us := u.String()
obj := fmt.Sprintf("%s/%s/%s", us[0:2], us[2:4], us)
obj := fmt.Sprintf("%s/%s/%s/%s/%s", globalDeploymentID, bucket, us[0:2], us[2:4], us)
return obj, nil
}
@@ -288,15 +321,10 @@ func transitionObject(ctx context.Context, objectAPI ObjectLayer, oi ObjectInfo)
if err != nil {
return err
}
lcOpts := lifecycle.ObjectOpts{
Name: oi.Name,
UserTags: oi.UserTags,
}
tierName := getLifeCycleTransitionTier(ctx, lc, oi.Bucket, lcOpts)
opts := ObjectOptions{
Transition: TransitionOptions{
Status: lifecycle.TransitionPending,
Tier: tierName,
Tier: lc.TransitionTier(oi.ToLifecycleOpts()),
ETag: oi.ETag,
},
VersionID: oi.VersionID,
@@ -306,19 +334,9 @@ func transitionObject(ctx context.Context, objectAPI ObjectLayer, oi ObjectInfo)
return objectAPI.TransitionObject(ctx, oi.Bucket, oi.Name, opts)
}
// getLifeCycleTransitionTier returns storage class for transition target
func getLifeCycleTransitionTier(ctx context.Context, lc *lifecycle.Lifecycle, bucket string, obj lifecycle.ObjectOpts) string {
for _, rule := range lc.FilterActionableRules(obj) {
if rule.Transition.StorageClass != "" {
return rule.Transition.StorageClass
}
}
return ""
}
// getTransitionedObjectReader returns a reader from the transitioned tier.
func getTransitionedObjectReader(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, oi ObjectInfo, opts ObjectOptions) (gr *GetObjectReader, err error) {
tgtClient, err := globalTierConfigMgr.getDriver(oi.TransitionTier)
tgtClient, err := globalTierConfigMgr.getDriver(oi.TransitionedObject.Tier)
if err != nil {
return nil, fmt.Errorf("transition storage class not configured")
}
@@ -335,7 +353,7 @@ func getTransitionedObjectReader(ctx context.Context, bucket, object string, rs
gopts.length = length
}
reader, err := tgtClient.Get(ctx, oi.transitionedObjName, remoteVersionID(oi.transitionVersionID), gopts)
reader, err := tgtClient.Get(ctx, oi.TransitionedObject.Name, remoteVersionID(oi.TransitionedObject.VersionID), gopts)
if err != nil {
return nil, err
}
@@ -473,7 +491,7 @@ func (r *RestoreObjectRequest) validate(ctx context.Context, objAPI ObjectLayer)
func postRestoreOpts(ctx context.Context, r *http.Request, bucket, object string) (opts ObjectOptions, err error) {
versioned := globalBucketVersioningSys.Enabled(bucket)
versionSuspended := globalBucketVersioningSys.Suspended(bucket)
vid := strings.TrimSpace(r.URL.Query().Get(xhttp.VersionID))
vid := strings.TrimSpace(r.Form.Get(xhttp.VersionID))
if vid != "" && vid != nullVersionID {
_, err := uuid.Parse(vid)
if err != nil {
@@ -559,7 +577,7 @@ func (fi FileInfo) IsRemote() bool {
// IsRemote returns true if this object version's contents are in its remote
// tier.
func (oi ObjectInfo) IsRemote() bool {
if oi.TransitionStatus != lifecycle.TransitionComplete {
if oi.TransitionedObject.Status != lifecycle.TransitionComplete {
return false
}
return !isRestoredObjectOnDisk(oi.UserDefined)
@@ -673,3 +691,21 @@ func isRestoredObjectOnDisk(meta map[string]string) (onDisk bool) {
}
return onDisk
}
// ToLifecycleOpts returns lifecycle.ObjectOpts value for oi.
func (oi ObjectInfo) ToLifecycleOpts() lifecycle.ObjectOpts {
return lifecycle.ObjectOpts{
Name: oi.Name,
UserTags: oi.UserTags,
VersionID: oi.VersionID,
ModTime: oi.ModTime,
IsLatest: oi.IsLatest,
NumVersions: oi.NumVersions,
DeleteMarker: oi.DeleteMarker,
SuccessorModTime: oi.SuccessorModTime,
RestoreOngoing: oi.RestoreOngoing,
RestoreExpires: oi.RestoreExpires,
TransitionStatus: oi.TransitionedObject.Status,
RemoteTiersImmediately: globalDebugRemoteTiersImmediately,
}
}
+1 -2
View File
@@ -19,7 +19,6 @@ package cmd
import (
"bytes"
"context"
"net/http"
"testing"
"time"
@@ -240,7 +239,7 @@ func TestValidateTransitionTier(t *testing.T) {
t.Fatalf("Test %d: Failed to parse lifecycle config %v", i+1, err)
}
err = validateTransitionTier(context.Background(), lc)
err = validateTransitionTier(lc)
if err != tc.expectedErr {
t.Fatalf("Test %d: Expected %v but got %v", i+1, tc.expectedErr, err)
}
+4 -4
View File
@@ -92,7 +92,7 @@ func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r
return
}
urlValues := r.URL.Query()
urlValues := r.Form
// Extract all the listBucketVersions query params to their native values.
prefix, marker, delimiter, maxkeys, encodingType, versionIDMarker, errCode := getListBucketObjectVersionsArgs(urlValues)
@@ -153,7 +153,7 @@ func (api objectAPIHandlers) ListObjectsV2MHandler(w http.ResponseWriter, r *htt
return
}
urlValues := r.URL.Query()
urlValues := r.Form
// Extract all the listObjectsV2 query params to their native values.
prefix, token, startAfter, delimiter, fetchOwner, maxKeys, encodingType, errCode := getListObjectsV2Args(urlValues)
@@ -220,7 +220,7 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
return
}
urlValues := r.URL.Query()
urlValues := r.Form
// Extract all the listObjectsV2 query params to their native values.
prefix, token, startAfter, delimiter, fetchOwner, maxKeys, encodingType, errCode := getListObjectsV2Args(urlValues)
@@ -329,7 +329,7 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
}
// Extract all the litsObjectsV1 query params to their native values.
prefix, marker, delimiter, maxKeys, encodingType, s3Error := getListObjectsV1Args(r.URL.Query())
prefix, marker, delimiter, maxKeys, encodingType, s3Error := getListObjectsV1Args(r.Form)
if s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
return
+5 -5
View File
@@ -175,7 +175,7 @@ func enforceRetentionBypassForDelete(ctx context.Context, r *http.Request, bucke
// For objects in "Governance" mode, overwrite is allowed if a) object retention date is past OR
// governance bypass headers are set and user has governance bypass permissions.
// Objects in compliance mode can be overwritten only if retention date is being extended. No mode change is permitted.
func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, objRetention *objectlock.ObjectRetention, cred auth.Credentials, owner bool, claims map[string]interface{}) (ObjectInfo, APIErrorCode) {
func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, objRetention *objectlock.ObjectRetention, cred auth.Credentials, owner bool) (ObjectInfo, APIErrorCode) {
byPassSet := objectlock.IsObjectLockGovernanceBypassSet(r.Header)
opts, err := getOpts(ctx, r, bucket, object)
if err != nil {
@@ -203,7 +203,7 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket,
perm := isPutRetentionAllowed(bucket, object,
days, objRetention.RetainUntilDate.Time,
objRetention.Mode, byPassSet, r, cred,
owner, claims)
owner)
return oi, perm
}
@@ -211,7 +211,7 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket,
case objectlock.RetGovernance:
govPerm := isPutRetentionAllowed(bucket, object, days,
objRetention.RetainUntilDate.Time, objRetention.Mode,
byPassSet, r, cred, owner, claims)
byPassSet, r, cred, owner)
// Governance mode retention period cannot be shortened, if x-amz-bypass-governance is not set.
if !byPassSet {
if objRetention.Mode != objectlock.RetGovernance || objRetention.RetainUntilDate.Before((ret.RetainUntilDate.Time)) {
@@ -227,7 +227,7 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket,
}
compliancePerm := isPutRetentionAllowed(bucket, object,
days, objRetention.RetainUntilDate.Time, objRetention.Mode,
false, r, cred, owner, claims)
false, r, cred, owner)
return oi, compliancePerm
}
return oi, ErrNone
@@ -235,7 +235,7 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket,
perm := isPutRetentionAllowed(bucket, object,
days, objRetention.RetainUntilDate.Time,
objRetention.Mode, byPassSet, r, cred, owner, claims)
objRetention.Mode, byPassSet, r, cred, owner)
return oi, perm
}
+47
View File
@@ -26,6 +26,7 @@ import (
"net/http/httptest"
"reflect"
"strings"
"sync"
"testing"
"github.com/minio/minio/internal/auth"
@@ -93,6 +94,52 @@ func getAnonWriteOnlyObjectPolicy(bucketName, prefix string) *policy.Policy {
}
}
// Wrapper for calling Create Bucket and ensure we get one and only one success.
func TestCreateBucket(t *testing.T) {
ExecObjectLayerAPITest(t, testCreateBucket, []string{"MakeBucketWithLocation"})
}
// testCreateBucket - Test for calling Create Bucket and ensure we get one and only one success.
func testCreateBucket(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
credentials auth.Credentials, t *testing.T) {
bucketName1 := fmt.Sprintf("%s-1", bucketName)
const n = 100
var start = make(chan struct{})
var ok, errs int
var wg sync.WaitGroup
var mu sync.Mutex
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
defer wg.Done()
// Sync start.
<-start
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName1, BucketOptions{}); err != nil {
if _, ok := err.(BucketExists); !ok {
t.Logf("unexpected error: %T: %v", err, err)
return
}
mu.Lock()
errs++
mu.Unlock()
return
}
mu.Lock()
ok++
mu.Unlock()
}()
}
close(start)
wg.Wait()
if ok != 1 {
t.Fatalf("want 1 ok, got %d", ok)
}
if errs != n-1 {
t.Fatalf("want %d errors, got %d", n-1, errs)
}
}
// Wrapper for calling Put Bucket Policy HTTP handler tests for both Erasure multiple disks and single node setup.
func TestPutBucketPolicyHandler(t *testing.T) {
ExecObjectLayerAPITest(t, testPutBucketPolicyHandler, []string{"PutBucketPolicy"})
+5 -5
View File
@@ -77,10 +77,10 @@ func getConditionValues(r *http.Request, lc string, username string, claims map[
}
}
vid := r.URL.Query().Get("versionId")
vid := r.Form.Get(xhttp.VersionID)
if vid == "" {
if u, err := url.Parse(r.Header.Get(xhttp.AmzCopySource)); err == nil {
vid = u.Query().Get("versionId")
vid = u.Query().Get(xhttp.VersionID)
}
}
@@ -143,8 +143,8 @@ func getConditionValues(r *http.Request, lc string, username string, claims map[
}
}
var cloneURLValues = url.Values{}
for k, v := range r.URL.Query() {
cloneURLValues := make(url.Values, len(r.Form))
for k, v := range r.Form {
cloneURLValues[k] = v
}
@@ -174,7 +174,7 @@ func getConditionValues(r *http.Request, lc string, username string, claims map[
// Special case for AD/LDAP STS users
if k == ldapUser {
args["user"] = []string{vStr}
} else if k == ldapUsername {
} else if k == ldapUserN {
args["username"] = []string{vStr}
} else {
args[k] = []string{vStr}
+37 -18
View File
@@ -43,6 +43,8 @@ import (
iampolicy "github.com/minio/pkg/iam/policy"
)
const throttleDeadline = 1 * time.Hour
// gets replication config associated to a given bucket name.
func getReplicationConfig(ctx context.Context, bucketName string) (rc *replication.Config, err error) {
if globalIsGateway {
@@ -792,15 +794,18 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
Bucket: objInfo.Bucket,
HeaderSize: headerSize,
}
newCtx, cancel := context.WithTimeout(ctx, globalOperationTimeout.Timeout())
defer cancel()
newCtx := ctx
if globalBucketMonitor.IsThrottled(bucket) {
var cancel context.CancelFunc
newCtx, cancel = context.WithTimeout(ctx, throttleDeadline)
defer cancel()
}
r := bandwidth.NewMonitoredReader(newCtx, globalBucketMonitor, gr, opts)
if len(objInfo.Parts) > 1 {
if uploadID, err := replicateObjectWithMultipart(ctx, c, dest.Bucket, object, r, objInfo, putOpts); err != nil {
if objInfo.Multipart {
if err := replicateObjectWithMultipart(ctx, c, dest.Bucket, object,
r, objInfo, putOpts); err != nil {
replicationStatus = replication.Failed
logger.LogIf(ctx, fmt.Errorf("Unable to replicate for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err))
// block and abort remote upload upon failure.
c.AbortMultipartUpload(ctx, dest.Bucket, object, uploadID)
}
} else {
if _, err = c.PutObject(ctx, dest.Bucket, object, r, size, "", "", putOpts); err != nil {
@@ -839,7 +844,7 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
for k, v := range objInfo.UserDefined {
popts.UserDefined[k] = v
}
popts.UserDefined[xhttp.AmzBucketReplicationStatus] = replication.Completed.String()
popts.UserDefined[xhttp.AmzBucketReplicationStatus] = replicationStatus.String()
if objInfo.UserTags != "" {
popts.UserDefined[xhttp.AmzObjectTagging] = objInfo.UserTags
}
@@ -868,38 +873,52 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
}
}
func replicateObjectWithMultipart(ctx context.Context, c *miniogo.Core, bucket, object string, r io.Reader, objInfo ObjectInfo, opts miniogo.PutObjectOptions) (uploadID string, err error) {
func replicateObjectWithMultipart(ctx context.Context, c *miniogo.Core, bucket, object string, r io.Reader, objInfo ObjectInfo, opts miniogo.PutObjectOptions) (err error) {
var uploadedParts []miniogo.CompletePart
uploadID, err = c.NewMultipartUpload(context.Background(), bucket, object, opts)
uploadID, err := c.NewMultipartUpload(context.Background(), bucket, object, opts)
if err != nil {
return
return err
}
defer func() {
if err != nil {
// block and abort remote upload upon failure.
if aerr := c.AbortMultipartUpload(ctx, bucket, object, uploadID); aerr != nil {
aerr = fmt.Errorf("Unable to cleanup failed multipart replication %s on remote %s/%s: %w", uploadID, bucket, object, aerr)
logger.LogIf(ctx, aerr)
}
}
}()
var (
hr *hash.Reader
pInfo miniogo.ObjectPart
)
for _, partInfo := range objInfo.Parts {
hr, err = hash.NewReader(r, partInfo.Size, "", "", partInfo.Size)
if err != nil {
return
return err
}
pInfo, err = c.PutObjectPart(ctx, bucket, object, uploadID, partInfo.Number, hr, partInfo.Size, "", "", opts.ServerSideEncryption)
if err != nil {
return
return err
}
if pInfo.Size != partInfo.Size {
return uploadID, fmt.Errorf("Part size mismatch: got %d, want %d", pInfo.Size, partInfo.Size)
return fmt.Errorf("Part size mismatch: got %d, want %d", pInfo.Size, partInfo.Size)
}
uploadedParts = append(uploadedParts, miniogo.CompletePart{
PartNumber: pInfo.PartNumber,
ETag: pInfo.ETag,
})
}
_, err = c.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, miniogo.PutObjectOptions{Internal: miniogo.AdvancedPutOptions{
SourceMTime: objInfo.ModTime,
ReplicationRequest: true, // always set this to distinguish between `mc mirror` replication and serverside
}})
return
_, err = c.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, miniogo.PutObjectOptions{
Internal: miniogo.AdvancedPutOptions{
SourceMTime: objInfo.ModTime,
// always set this to distinguish between `mc mirror` replication and serverside
ReplicationRequest: true,
}})
return err
}
// filterReplicationStatusMetadata filters replication status metadata for COPY
+1
View File
@@ -346,6 +346,7 @@ func (sys *BucketTargetSys) getRemoteTargetClient(tcfg *madmin.BucketTarget) (*T
getRemoteTargetInstanceTransportOnce.Do(func() {
getRemoteTargetInstanceTransport = NewRemoteTargetHTTPTransport()
})
api, err := minio.New(tcfg.Endpoint, &miniogo.Options{
Creds: creds,
Secure: tcfg.Secure,
+56 -38
View File
@@ -44,6 +44,7 @@ import (
"github.com/minio/console/restapi"
"github.com/minio/console/restapi/operations"
"github.com/minio/kes"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/minio/internal/auth"
@@ -70,7 +71,7 @@ func init() {
logger.Init(GOPATH, GOROOT)
logger.RegisterError(config.FmtError)
if IsKubernetes() || IsDocker() || IsBOSH() || IsDCOS() || IsKubernetesReplicaSet() || IsPCFTile() {
if IsKubernetes() || IsDocker() || IsBOSH() || IsDCOS() || IsPCFTile() {
// 30 seconds matches the orchestrator DNS TTLs, have
// a 5 second timeout to lookup from DNS servers.
globalDNSCache = xhttp.NewDNSCache(30*time.Second, 5*time.Second, logger.LogOnceIf)
@@ -91,8 +92,6 @@ func init() {
},
})
globalTransitionState = newTransitionState()
console.SetColor("Debug", fcolor.New())
gob.Register(StorageErr(""))
@@ -104,6 +103,10 @@ func init() {
},
},
}
// All minio-go API operations shall be performed only once,
// another way to look at this is we are turning off retries.
minio.MaxRetry = 1
}
const consolePrefix = "CONSOLE_"
@@ -111,7 +114,11 @@ const consolePrefix = "CONSOLE_"
func minioConfigToConsoleFeatures() {
os.Setenv("CONSOLE_PBKDF_SALT", globalDeploymentID)
os.Setenv("CONSOLE_PBKDF_PASSPHRASE", globalDeploymentID)
os.Setenv("CONSOLE_MINIO_SERVER", getAPIEndpoints()[0])
if globalMinioEndpoint != "" {
os.Setenv("CONSOLE_MINIO_SERVER", globalMinioEndpoint)
} else {
os.Setenv("CONSOLE_MINIO_SERVER", getAPIEndpoints()[0])
}
if value := env.Get("MINIO_LOG_QUERY_URL", ""); value != "" {
os.Setenv("CONSOLE_LOG_QUERY_URL", value)
if value := env.Get("MINIO_LOG_QUERY_AUTH_TOKEN", ""); value != "" {
@@ -145,6 +152,9 @@ func minioConfigToConsoleFeatures() {
}
os.Setenv("CONSOLE_MINIO_REGION", globalServerRegion)
os.Setenv("CONSOLE_CERT_PASSWD", env.Get("MINIO_CERT_PASSWD", ""))
if globalSubnetLicense != "" {
os.Setenv("CONSOLE_SUBNET_LICENSE", globalSubnetLicense)
}
}
func initConsoleServer() (*restapi.Server, error) {
@@ -169,14 +179,16 @@ func initConsoleServer() (*restapi.Server, error) {
return nil, err
}
noLog := func(string, ...interface{}) {
// nothing to log
}
// Initialize MinIO loggers
restapi.LogInfo = logger.Info
restapi.LogError = logger.Error
restapi.LogInfo = noLog
restapi.LogError = noLog
api := operations.NewConsoleAPI(swaggerSpec)
api.Logger = func(_ string, _ ...interface{}) {
// nothing to log.
}
api.Logger = noLog
server := restapi.NewServer(api)
// register all APIs
@@ -200,39 +212,10 @@ func initConsoleServer() (*restapi.Server, error) {
restapi.Hostname = globalMinioConsoleHost
}
// subnet license refresh process
go func() {
// start refreshing subnet license after 5 seconds..
time.Sleep(time.Second * 5)
failedAttempts := 0
for {
if err := restapi.RefreshLicense(); err != nil {
failedAttempts++
// end license refresh after 3 consecutive failed attempts
if failedAttempts >= 3 {
return
}
// wait 5 minutes and retry again
time.Sleep(time.Minute * 5)
continue
}
// if license refreshed successfully reset the counter
failedAttempts = 0
// try to refresh license every 24 hrs
time.Sleep(time.Hour * 24)
}
}()
return server, nil
}
func verifyObjectLayerFeatures(name string, objAPI ObjectLayer) {
if (GlobalKMS != nil) && !objAPI.IsEncryptionSupported() {
logger.Fatal(errInvalidArgument,
"Encryption support is requested but '%s' does not support encryption", name)
}
if strings.HasPrefix(name, "gateway") {
if GlobalGatewaySSE.IsSet() && GlobalKMS == nil {
uiErr := config.ErrInvalidGWSSEEnvValue(nil).Msg("MINIO_GATEWAY_SSE set but KMS is not configured")
@@ -415,6 +398,39 @@ func handleCommonEnvVars() {
if err != nil {
logger.Fatal(config.ErrInvalidBrowserValue(err), "Invalid MINIO_BROWSER value in environment variable")
}
if globalBrowserEnabled {
if redirectURL := env.Get(config.EnvMinIOBrowserRedirectURL, ""); redirectURL != "" {
u, err := xnet.ParseHTTPURL(redirectURL)
if err != nil {
logger.Fatal(err, "Invalid MINIO_BROWSER_REDIRECT_URL value in environment variable")
}
// Look for if URL has invalid values and return error.
if !((u.Scheme == "http" || u.Scheme == "https") &&
(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)
logger.Fatal(err, "Invalid MINIO_BROWSER_REDIRECT_URL value is environment variable")
}
u.Path = "" // remove any path component such as `/`
globalBrowserRedirectURL = u
}
}
if serverURL := env.Get(config.EnvMinIOServerURL, ""); serverURL != "" {
u, err := xnet.ParseHTTPURL(serverURL)
if err != nil {
logger.Fatal(err, "Invalid MINIO_SERVER_URL value in environment variable")
}
// Look for if URL has invalid values and return error.
if !((u.Scheme == "http" || u.Scheme == "https") &&
(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)
logger.Fatal(err, "Invalid MINIO_SERVER_URL value is environment variable")
}
u.Path = "" // remove any path component such as `/`
globalMinioEndpoint = u.String()
}
globalFSOSync, err = config.ParseBool(env.Get(config.EnvFSOSync, config.EnableOff))
if err != nil {
@@ -583,6 +599,8 @@ func handleCommonEnvVars() {
if tiers := env.Get("_MINIO_DEBUG_REMOTE_TIERS_IMMEDIATELY", ""); tiers != "" {
globalDebugRemoteTiersImmediately = strings.Split(tiers, ",")
}
globalSubnetLicense = env.Get(config.EnvMinIOSubnetLicense, "")
}
func logStartupMessage(msg string) {
+32 -26
View File
@@ -43,6 +43,7 @@ import (
"github.com/minio/minio/internal/kms"
"github.com/minio/minio/internal/logger"
"github.com/minio/minio/internal/logger/target/http"
"github.com/minio/minio/internal/logger/target/kafka"
"github.com/minio/pkg/env"
)
@@ -58,7 +59,8 @@ func initHelp() {
config.APISubSys: api.DefaultKVS,
config.CredentialsSubSys: config.DefaultCredentialKVS,
config.LoggerWebhookSubSys: logger.DefaultKVS,
config.AuditWebhookSubSys: logger.DefaultAuditKVS,
config.AuditWebhookSubSys: logger.DefaultAuditWebhookKVS,
config.AuditKafkaSubSys: logger.DefaultAuditKafkaKVS,
config.HealSubSys: heal.DefaultKVS,
config.ScannerSubSys: scanner.DefaultKVS,
}
@@ -122,6 +124,11 @@ func initHelp() {
Description: "send audit logs to webhook endpoints",
MultipleTargets: true,
},
config.HelpKV{
Key: config.AuditKafkaSubSys,
Description: "send audit logs to kafka endpoints",
MultipleTargets: true,
},
config.HelpKV{
Key: config.NotifyWebhookSubSys,
Description: "publish bucket notifications to webhook endpoints",
@@ -197,7 +204,8 @@ func initHelp() {
config.IdentityLDAPSubSys: xldap.Help,
config.PolicyOPASubSys: opa.Help,
config.LoggerWebhookSubSys: logger.Help,
config.AuditWebhookSubSys: logger.HelpAudit,
config.AuditWebhookSubSys: logger.HelpWebhook,
config.AuditKafkaSubSys: logger.HelpKafka,
config.NotifyAMQPSubSys: notify.HelpAMQP,
config.NotifyKafkaSubSys: notify.HelpKafka,
config.NotifyMQTTSubSys: notify.HelpMQTT,
@@ -478,42 +486,40 @@ func lookupConfigs(s config.Config, setDriveCounts []int) {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize logger: %w", err))
}
for k, l := range loggerCfg.HTTP {
for _, l := range loggerCfg.HTTP {
if l.Enabled {
l.LogOnce = logger.LogOnceIf
l.UserAgent = loggerUserAgent
l.Transport = NewGatewayHTTPTransportWithClientCerts(l.ClientCert, l.ClientKey)
// Enable http logging
if err = logger.AddTarget(
http.New(
http.WithTargetName(k),
http.WithEndpoint(l.Endpoint),
http.WithAuthToken(l.AuthToken),
http.WithUserAgent(loggerUserAgent),
http.WithLogKind(string(logger.All)),
http.WithTransport(NewGatewayHTTPTransport()),
),
); err != nil {
if err = logger.AddTarget(http.New(l)); err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize console HTTP target: %w", err))
}
}
}
for k, l := range loggerCfg.Audit {
for _, l := range loggerCfg.AuditWebhook {
if l.Enabled {
l.LogOnce = logger.LogOnceIf
l.UserAgent = loggerUserAgent
l.Transport = NewGatewayHTTPTransportWithClientCerts(l.ClientCert, l.ClientKey)
// Enable http audit logging
if err = logger.AddAuditTarget(
http.New(
http.WithTargetName(k),
http.WithEndpoint(l.Endpoint),
http.WithAuthToken(l.AuthToken),
http.WithUserAgent(loggerUserAgent),
http.WithLogKind(string(logger.All)),
http.WithTransport(NewGatewayHTTPTransportWithClientCerts(l.ClientCert, l.ClientKey)),
),
); err != nil {
if err = logger.AddAuditTarget(http.New(l)); err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize audit HTTP target: %w", err))
}
}
}
for _, l := range loggerCfg.AuditKafka {
if l.Enabled {
l.LogOnce = logger.LogOnceIf
// Enable Kafka audit logging
if err = logger.AddAuditTarget(kafka.New(l)); err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize audit Kafka target: %w", err))
}
}
}
globalConfigTargetList, err = notify.GetNotificationTargets(GlobalContext, s, NewGatewayHTTPTransport(), false)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize notification target(s): %w", err))
@@ -717,8 +723,8 @@ func loadConfig(objAPI ObjectLayer) error {
func getOpenIDValidators(cfg openid.Config) *openid.Validators {
validators := openid.NewValidators()
if cfg.JWKS.URL != nil {
validators.Add(openid.NewJWT(cfg))
if cfg.Enabled {
validators.Add(&cfg)
}
return validators
+4 -3
View File
@@ -41,6 +41,7 @@ import (
"github.com/minio/minio/internal/event/target"
"github.com/minio/minio/internal/kms"
"github.com/minio/minio/internal/logger"
"github.com/minio/minio/internal/logger/target/http"
xnet "github.com/minio/pkg/net"
"github.com/minio/pkg/quick"
)
@@ -2383,8 +2384,8 @@ func migrateV26ToV27() error {
// Enable console logging by default to avoid breaking users
// current deployments
srvConfig.Logger.Console.Enabled = true
srvConfig.Logger.HTTP = make(map[string]logger.HTTP)
srvConfig.Logger.HTTP["1"] = logger.HTTP{}
srvConfig.Logger.HTTP = make(map[string]http.Config)
srvConfig.Logger.HTTP["1"] = http.Config{}
if err = quick.SaveConfig(srvConfig, configFile, globalEtcdClient); err != nil {
return fmt.Errorf("Failed to migrate config from ‘26’ to ‘27’. %w", err)
@@ -2748,7 +2749,7 @@ func migrateMinioSysConfigToKV(objAPI ObjectLayer) error {
for k, loggerArgs := range cfg.Logger.HTTP {
logger.SetLoggerHTTP(newCfg, k, loggerArgs)
}
for k, auditArgs := range cfg.Logger.Audit {
for k, auditArgs := range cfg.Logger.AuditWebhook {
logger.SetLoggerHTTPAudit(newCfg, k, auditArgs)
}
+2 -2
View File
@@ -117,8 +117,8 @@ func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan interface{}, doneCh <-chan
sys.pubsub.Subscribe(subCh, doneCh, filter)
}
// Validate if HTTPConsoleLoggerSys is valid, always returns nil right now
func (sys *HTTPConsoleLoggerSys) Validate() error {
// Init if HTTPConsoleLoggerSys is valid, always returns nil right now
func (sys *HTTPConsoleLoggerSys) Init() error {
return nil
}
+1 -1
View File
@@ -28,7 +28,7 @@ import (
// Test cross domain xml handler.
func TestCrossXMLHandler(t *testing.T) {
// Server initialization.
router := mux.NewRouter().SkipClean(true)
router := mux.NewRouter().SkipClean(true).UseEncodedPath()
handler := setCrossDomainPolicy(router)
srv := httptest.NewServer(handler)
+24 -39
View File
@@ -525,12 +525,22 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
return
}
if !into.Compacted {
into.addChild(dataUsageHash(folder.name))
h := dataUsageHash(folder.name)
into.addChild(h)
// We scanned a folder, optionally send update.
f.updateCache.deleteRecursive(h)
f.updateCache.copyWithChildren(&f.newCache, h, folder.parent)
f.sendUpdate()
}
// We scanned a folder, optionally send update.
f.sendUpdate()
}
// Transfer existing
if !into.Compacted {
for _, folder := range existingFolders {
h := hashPath(folder.name)
f.updateCache.copyWithChildren(&f.oldCache, h, folder.parent)
}
}
// Scan new...
for _, folder := range newFolders {
h := hashPath(folder.name)
@@ -903,7 +913,7 @@ func (i *scannerItem) applyLifecycle(ctx context.Context, o ObjectLayer, meta ac
SuccessorModTime: meta.oi.SuccessorModTime,
RestoreOngoing: meta.oi.RestoreOngoing,
RestoreExpires: meta.oi.RestoreExpires,
TransitionStatus: meta.oi.TransitionStatus,
TransitionStatus: meta.oi.TransitionedObject.Status,
RemoteTiersImmediately: globalDebugRemoteTiersImmediately,
})
if i.debug {
@@ -966,7 +976,7 @@ func (i *scannerItem) applyLifecycle(ctx context.Context, o ObjectLayer, meta ac
// applyTierObjSweep removes remote object pending deletion and the free-version
// tracking this information.
func (i *scannerItem) applyTierObjSweep(ctx context.Context, o ObjectLayer, meta actionMeta) {
if !meta.oi.tierFreeVersion {
if !meta.oi.TransitionedObject.FreeVersion {
// nothing to be done
return
}
@@ -979,7 +989,7 @@ func (i *scannerItem) applyTierObjSweep(ctx context.Context, o ObjectLayer, meta
return err
}
// Remove the remote object
err := deleteObjectFromRemoteTier(ctx, meta.oi.transitionedObjName, meta.oi.transitionVersionID, meta.oi.TransitionTier)
err := deleteObjectFromRemoteTier(ctx, meta.oi.TransitionedObject.Name, meta.oi.TransitionedObject.VersionID, meta.oi.TransitionedObject.Tier)
if ignoreNotFoundErr(err) != nil {
logger.LogIf(ctx, err)
return
@@ -1020,22 +1030,7 @@ func (i *scannerItem) applyActions(ctx context.Context, o ObjectLayer, meta acti
}
func evalActionFromLifecycle(ctx context.Context, lc lifecycle.Lifecycle, obj ObjectInfo, debug bool) (action lifecycle.Action) {
lcOpts := lifecycle.ObjectOpts{
Name: obj.Name,
UserTags: obj.UserTags,
ModTime: obj.ModTime,
VersionID: obj.VersionID,
DeleteMarker: obj.DeleteMarker,
IsLatest: obj.IsLatest,
NumVersions: obj.NumVersions,
SuccessorModTime: obj.SuccessorModTime,
RestoreOngoing: obj.RestoreOngoing,
RestoreExpires: obj.RestoreExpires,
TransitionStatus: obj.TransitionStatus,
RemoteTiersImmediately: globalDebugRemoteTiersImmediately,
}
action = lc.ComputeAction(lcOpts)
action = lc.ComputeAction(obj.ToLifecycleOpts())
if debug {
console.Debugf(applyActionsLogPrefix+" lifecycle: Secondary scan: %v\n", action)
}
@@ -1070,7 +1065,7 @@ func evalActionFromLifecycle(ctx context.Context, lc lifecycle.Lifecycle, obj Ob
func applyTransitionAction(ctx context.Context, action lifecycle.Action, objLayer ObjectLayer, obj ObjectInfo) bool {
srcOpts := ObjectOptions{}
if obj.TransitionStatus == "" {
if obj.TransitionedObject.Status == "" {
srcOpts.Versioned = globalBucketVersioningSys.Enabled(obj.Bucket)
srcOpts.VersionID = obj.VersionID
// mark transition as pending
@@ -1086,25 +1081,11 @@ func applyTransitionAction(ctx context.Context, action lifecycle.Action, objLaye
}
func applyExpiryOnTransitionedObject(ctx context.Context, objLayer ObjectLayer, obj ObjectInfo, restoredObject bool) bool {
lcOpts := lifecycle.ObjectOpts{
Name: obj.Name,
UserTags: obj.UserTags,
ModTime: obj.ModTime,
VersionID: obj.VersionID,
DeleteMarker: obj.DeleteMarker,
IsLatest: obj.IsLatest,
NumVersions: obj.NumVersions,
SuccessorModTime: obj.SuccessorModTime,
RestoreOngoing: obj.RestoreOngoing,
RestoreExpires: obj.RestoreExpires,
TransitionStatus: obj.TransitionStatus,
}
action := expireObj
if restoredObject {
action = expireRestoredObj
}
if err := expireTransitionedObject(ctx, objLayer, &obj, lcOpts, action); err != nil {
if err := expireTransitionedObject(ctx, objLayer, &obj, obj.ToLifecycleOpts(), action); err != nil {
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
return false
}
@@ -1156,7 +1137,7 @@ func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLay
// Apply object, object version, restored object or restored object version action on the given object
func applyExpiryRule(ctx context.Context, objLayer ObjectLayer, obj ObjectInfo, restoredObject, applyOnVersion bool) bool {
if obj.TransitionStatus != "" {
if obj.TransitionedObject.Status != "" {
return applyExpiryOnTransitionedObject(ctx, objLayer, obj, restoredObject)
}
return applyExpiryOnNonTransitionedObjects(ctx, objLayer, obj, applyOnVersion)
@@ -1383,6 +1364,8 @@ const (
ILMExpiry = "ilm:expiry"
// ILMFreeVersionDelete - audit trail for ILM free-version delete
ILMFreeVersionDelete = "ilm:free-version-delete"
// ILMTransition - audit trail for ILM transitioning.
ILMTransition = " ilm:transition"
)
func auditLogLifecycle(ctx context.Context, oi ObjectInfo, trigger string) {
@@ -1392,6 +1375,8 @@ func auditLogLifecycle(ctx context.Context, oi ObjectInfo, trigger string) {
apiName = "ILMExpiry"
case ILMFreeVersionDelete:
apiName = "ILMFreeVersionDelete"
case ILMTransition:
apiName = "ILMTransition"
}
auditLogInternal(ctx, oi.Bucket, oi.Name, AuditLogOptions{
Trigger: trigger,
+2 -2
View File
@@ -46,7 +46,7 @@ const (
dataUpdateTrackerQueueSize = 0
dataUpdateTrackerFilename = dataUsageBucket + SlashSeparator + ".tracker.bin"
dataUpdateTrackerVersion = 5
dataUpdateTrackerVersion = 6
dataUpdateTrackerSaveInterval = 5 * time.Minute
)
@@ -397,7 +397,7 @@ func (d *dataUpdateTracker) deserialize(src io.Reader, newerThan time.Time) erro
return err
}
switch tmp[0] {
case 1, 2, 3, 4:
case 1, 2, 3, 4, 5:
if intDataUpdateTracker.debug {
console.Debugln(color.Green("dataUpdateTracker: ") + "deprecated data version, updating.")
}
+1 -1
View File
@@ -51,7 +51,7 @@ func (t *testingLogger) String() string {
return ""
}
func (t *testingLogger) Validate() error {
func (t *testingLogger) Init() error {
return nil
}
+16
View File
@@ -513,7 +513,11 @@ func (d *dataUsageCache) reduceChildrenOf(path dataUsageHash, limit int, compact
// StringAll returns a detailed string representation of all entries in the cache.
func (d *dataUsageCache) StringAll() string {
// Remove bloom filter from print.
bf := d.Info.BloomFilter
d.Info.BloomFilter = nil
s := fmt.Sprintf("info:%+v\n", d.Info)
d.Info.BloomFilter = bf
for k, v := range d.Cache {
s += fmt.Sprintf("\t%v: %+v\n", k, v)
}
@@ -530,6 +534,18 @@ func (h dataUsageHash) Key() string {
return string(h)
}
func (d *dataUsageCache) flattenChildrens(root dataUsageEntry) (m map[string]dataUsageEntry) {
m = make(map[string]dataUsageEntry)
for id := range root.Children {
e := d.Cache[id]
if len(e.Children) > 0 {
e = d.flatten(e)
}
m[id] = e
}
return m
}
// flatten all children of the root into the root element and return it.
func (d *dataUsageCache) flatten(root dataUsageEntry) dataUsageEntry {
for id := range root.Children {
+35
View File
@@ -21,6 +21,7 @@ import (
"bytes"
"context"
"net/http"
"strings"
jsoniter "github.com/json-iterator/go"
"github.com/minio/madmin-go"
@@ -59,6 +60,40 @@ func storeDataUsageInBackend(ctx context.Context, objAPI ObjectLayer, dui <-chan
}
}
// loadPrefixUsageFromBackend returns prefix usages found in passed buckets
// e.g.: /testbucket/prefix => 355601334
func loadPrefixUsageFromBackend(ctx context.Context, objAPI ObjectLayer, bucket string) (map[string]uint64, error) {
z, ok := objAPI.(*erasureServerPools)
if !ok {
// Prefix usage is empty
return map[string]uint64{}, nil
}
cache := dataUsageCache{}
m := make(map[string]uint64)
for _, pool := range z.serverPools {
for _, er := range pool.sets {
// Load bucket usage prefixes
if err := cache.load(ctx, er, bucket+slashSeparator+dataUsageCacheName); err == nil {
root := cache.find(bucket)
if root == nil {
// We dont have usage information for this bucket in this
// set, go to the next set
continue
}
for id, usageInfo := range cache.flattenChildrens(*root) {
prefix := strings.TrimPrefix(id, bucket+slashSeparator)
m[prefix] += uint64(usageInfo.Size)
}
}
}
}
return m, nil
}
func loadDataUsageFromBackend(ctx context.Context, objAPI ObjectLayer) (madmin.DataUsageInfo, error) {
r, err := objAPI.GetObjectNInfo(ctx, dataUsageBucket, dataUsageObjName, nil, http.Header{}, readLock, ObjectOptions{})
if err != nil {
@@ -1,3 +1,4 @@
//go:build windows
// +build windows
/*
+1
View File
@@ -1,3 +1,4 @@
//go:build !windows
// +build !windows
// Copyright (c) 2015-2021 MinIO, Inc.
+6 -72
View File
@@ -245,7 +245,7 @@ func (l *EndpointServerPools) Add(zeps PoolEndpoints) error {
func (l EndpointServerPools) Localhost() string {
for _, ep := range l {
for _, endpoint := range ep.Endpoints {
if endpoint.IsLocal {
if endpoint.IsLocal && endpoint.Host != "" {
u := &url.URL{
Scheme: endpoint.Scheme,
Host: endpoint.Host,
@@ -254,7 +254,11 @@ func (l EndpointServerPools) Localhost() string {
}
}
}
return ""
host := globalMinioHost
if host == "" {
host = sortIPs(localIP4.ToSlice())[0]
}
return fmt.Sprintf("%s://%s", getURLScheme(globalIsTLS), net.JoinHostPort(host, globalMinioPort))
}
// LocalDisksPaths returns the disk paths of the local disks
@@ -373,27 +377,6 @@ func (endpoints Endpoints) GetAllStrings() (all []string) {
return
}
func hostResolveToLocalhost(endpoint Endpoint) bool {
hostIPs, err := getHostIP(endpoint.Hostname())
if err != nil {
// Log the message to console about the host resolving
reqInfo := (&logger.ReqInfo{}).AppendTags(
"host",
endpoint.Hostname(),
)
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
logger.LogIf(ctx, err, logger.Application)
return false
}
var loopback int
for _, hostIP := range hostIPs.ToSlice() {
if net.ParseIP(hostIP).IsLoopback() {
loopback++
}
}
return loopback == len(hostIPs)
}
func (endpoints Endpoints) atleastOneEndpointLocal() bool {
for _, endpoint := range endpoints {
if endpoint.IsLocal {
@@ -406,7 +389,6 @@ func (endpoints Endpoints) atleastOneEndpointLocal() bool {
// UpdateIsLocal - resolves the host and discovers the local host.
func (endpoints Endpoints) UpdateIsLocal(foundPrevLocal bool) error {
orchestrated := IsDocker() || IsKubernetes()
k8sReplicaSet := IsKubernetesReplicaSet()
var epsResolved int
var foundLocal bool
@@ -439,24 +421,6 @@ func (endpoints Endpoints) UpdateIsLocal(foundPrevLocal bool) error {
endpoints[i].Hostname(),
)
if k8sReplicaSet && hostResolveToLocalhost(endpoints[i]) {
err := fmt.Errorf("host %s resolves to 127.*, DNS incorrectly configured retrying",
endpoints[i])
// time elapsed
timeElapsed := time.Since(startTime)
// log error only if more than 1s elapsed
if timeElapsed > time.Second {
reqInfo.AppendTags("elapsedTime",
humanize.RelTime(startTime,
startTime.Add(timeElapsed),
"elapsed",
""))
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
logger.LogIf(ctx, err, logger.Application)
}
continue
}
// return err if not Docker or Kubernetes
// We use IsDocker() to check for Docker environment
// We use IsKubernetes() to check for Kubernetes environment
@@ -485,36 +449,6 @@ func (endpoints Endpoints) UpdateIsLocal(foundPrevLocal bool) error {
} else {
resolvedList[i] = true
endpoints[i].IsLocal = isLocal
if k8sReplicaSet && !endpoints.atleastOneEndpointLocal() && !foundPrevLocal {
// In replicated set in k8s deployment, IPs might
// get resolved for older IPs, add this code
// to ensure that we wait for this server to
// participate atleast one disk and be local.
//
// In special cases for replica set with expanded
// pool setups we need to make sure to provide
// value of foundPrevLocal from pool1 if we already
// found a local setup. Only if we haven't found
// previous local we continue to wait to look for
// atleast one local.
resolvedList[i] = false
// time elapsed
err := fmt.Errorf("no endpoint is local to this host: %s", endpoints[i])
timeElapsed := time.Since(startTime)
// log error only if more than 1s elapsed
if timeElapsed > time.Second {
reqInfo.AppendTags("elapsedTime",
humanize.RelTime(startTime,
startTime.Add(timeElapsed),
"elapsed",
"",
))
ctx := logger.SetReqInfo(GlobalContext,
reqInfo)
logger.LogIf(ctx, err, logger.Application)
}
continue
}
epsResolved++
if !foundLocal {
foundLocal = isLocal
-65
View File
@@ -20,8 +20,6 @@ package cmd
import (
"context"
"sync"
"github.com/minio/minio/internal/sync/errgroup"
)
func (er erasureObjects) getLocalDisks() (localDisks []StorageAPI) {
@@ -47,38 +45,6 @@ func (er erasureObjects) getLoadBalancedLocalDisks() (newDisks []StorageAPI) {
return newDisks
}
func (er erasureObjects) getOnlineDisks() (newDisks []StorageAPI) {
disks := er.getDisks()
var wg sync.WaitGroup
var mu sync.Mutex
for _, i := range hashOrder(UTCNow().String(), len(disks)) {
i := i
wg.Add(1)
go func() {
defer wg.Done()
if disks[i-1] == nil {
return
}
di, err := disks[i-1].DiskInfo(context.Background())
if err != nil || di.Healing {
// - Do not consume disks which are not reachable
// unformatted or simply not accessible for some reason.
//
// - Do not consume disks which are being healed
//
// - Future: skip busy disks
return
}
mu.Lock()
newDisks = append(newDisks, disks[i-1])
mu.Unlock()
}()
}
wg.Wait()
return newDisks
}
// getLoadBalancedDisks - fetches load balanced (sufficiently randomized) disk slice.
// ensures to skip disks if they are not healing and online.
func (er erasureObjects) getLoadBalancedDisks(optimized bool) []StorageAPI {
@@ -133,34 +99,3 @@ func (er erasureObjects) getLoadBalancedDisks(optimized bool) []StorageAPI {
// Return disks which have maximum disk usage common.
return newDisks[max]
}
// This function does the following check, suppose
// object is "a/b/c/d", stat makes sure that objects
// - "a/b/c"
// - "a/b"
// - "a"
// do not exist on the namespace.
func (er erasureObjects) parentDirIsObject(ctx context.Context, bucket, parent string) bool {
storageDisks := er.getDisks()
g := errgroup.WithNErrs(len(storageDisks))
for index := range storageDisks {
index := index
g.Go(func() error {
if storageDisks[index] == nil {
return errDiskNotFound
}
// Check if 'prefix' is an object on this 'disk', else continue the check the next disk
return storageDisks[index].CheckFile(ctx, bucket, parent)
}, index)
}
// NOTE: Observe we are not trying to read `xl.meta` and figure out the actual
// quorum intentionally, but rely on the default case scenario. Actual quorum
// verification will happen by top layer by using getObjectInfo() and will be
// ignored if necessary.
readQuorum := getReadQuorum(len(storageDisks))
return reduceReadQuorumErrs(ctx, g.Wait(), objectOpIgnoredErrs, readQuorum) == nil
}
-83
View File
@@ -1,83 +0,0 @@
// Copyright (c) 2015-2021 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package cmd
import (
"bytes"
"context"
"os"
"testing"
)
// Tests for if parent directory is object
func TestErasureParentDirIsObject(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
obj, fsDisks, err := prepareErasureSets32(ctx)
if err != nil {
t.Fatalf("Unable to initialize 'Erasure' object layer.")
}
defer obj.Shutdown(context.Background())
// Remove all disks.
for _, disk := range fsDisks {
defer os.RemoveAll(disk)
}
bucketName := "testbucket"
objectName := "object"
if err = obj.MakeBucketWithLocation(GlobalContext, bucketName, BucketOptions{}); err != nil {
t.Fatal(err)
}
objectContent := "12345"
_, err = obj.PutObject(GlobalContext, bucketName, objectName,
mustGetPutObjReader(t, bytes.NewReader([]byte(objectContent)), int64(len(objectContent)), "", ""), ObjectOptions{})
if err != nil {
t.Fatal(err)
}
testCases := []struct {
expectedErr bool
objectName string
}{
{
expectedErr: true,
objectName: pathJoin(objectName, "parent-is-object"),
},
{
expectedErr: false,
objectName: pathJoin("no-parent", "object"),
},
}
for _, testCase := range testCases {
t.Run("", func(t *testing.T) {
_, err = obj.PutObject(GlobalContext, bucketName, testCase.objectName,
mustGetPutObjReader(t, bytes.NewReader([]byte(objectContent)), int64(len(objectContent)), "", ""), ObjectOptions{})
if testCase.expectedErr && err == nil {
t.Error("Expected error but got nil")
}
if !testCase.expectedErr && err != nil {
t.Errorf("Expected nil but got %v", err)
}
})
}
}
+32 -56
View File
@@ -189,35 +189,6 @@ func getLatestFileInfo(ctx context.Context, partsMetadata []FileInfo, errs []err
return latestFileInfo, nil
}
// fileInfoConsistent whether all fileinfos are consistent with each other.
// Will return false if any fileinfo mismatches.
func fileInfoConsistent(ctx context.Context, partsMetadata []FileInfo, errs []error) bool {
// There should be atleast half correct entries, if not return failure
if reducedErr := reduceReadQuorumErrs(ctx, errs, nil, len(partsMetadata)/2); reducedErr != nil {
return false
}
if len(partsMetadata) == 1 {
return true
}
// Reference
ref := partsMetadata[0]
if !ref.IsValid() {
return false
}
for _, meta := range partsMetadata[1:] {
if !meta.IsValid() {
return false
}
if !meta.ModTime.Equal(ref.ModTime) {
return false
}
if meta.DataDir != ref.DataDir {
return false
}
}
return true
}
// disksWithAllParts - This function needs to be called with
// []StorageAPI returned by listOnlineDisks. Returns,
//
@@ -225,8 +196,9 @@ func fileInfoConsistent(ctx context.Context, partsMetadata []FileInfo, errs []er
//
// - 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, bucket,
object string, scanMode madmin.HealScanMode) ([]StorageAPI, []error) {
func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetadata []FileInfo,
errs []error, bucket, object string, scanMode madmin.HealScanMode) ([]StorageAPI, []error) {
// List of disks having latest version of the object er.meta (by modtime)
_, modTime, dataDir := listOnlineDisks(onlineDisks, partsMetadata, errs)
@@ -239,15 +211,17 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
// consider the offline disks as consistent.
continue
}
if len(meta.Erasure.Distribution) != len(onlineDisks) {
// Erasure distribution seems to have lesser
// number of items than number of online disks.
inconsistent++
continue
}
if meta.Erasure.Distribution[i] != meta.Erasure.Index {
// Mismatch indexes with distribution order
inconsistent++
if !meta.Deleted {
if len(meta.Erasure.Distribution) != len(onlineDisks) {
// Erasure distribution seems to have lesser
// number of items than number of online disks.
inconsistent++
continue
}
if meta.Erasure.Distribution[i] != meta.Erasure.Index {
// Mismatch indexes with distribution order
inconsistent++
}
}
}
@@ -267,8 +241,8 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
dataErrs[i] = errDiskNotFound
continue
}
meta := partsMetadata[i]
meta := partsMetadata[i]
if !meta.ModTime.Equal(modTime) || meta.DataDir != dataDir {
dataErrs[i] = errFileCorrupt
partsMetadata[i] = FileInfo{}
@@ -280,20 +254,22 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
continue
}
if len(meta.Erasure.Distribution) != len(onlineDisks) {
// Erasure distribution is not the same as onlineDisks
// attempt a fix if possible, assuming other entries
// might have the right erasure distribution.
partsMetadata[i] = FileInfo{}
dataErrs[i] = errFileCorrupt
continue
}
if !meta.Deleted {
if len(meta.Erasure.Distribution) != len(onlineDisks) {
// Erasure distribution is not the same as onlineDisks
// attempt a fix if possible, assuming other entries
// might have the right erasure distribution.
partsMetadata[i] = FileInfo{}
dataErrs[i] = errFileCorrupt
continue
}
// Since erasure.Distribution is trustable we can fix the mismatching erasure.Index
if meta.Erasure.Distribution[i] != meta.Erasure.Index {
partsMetadata[i] = FileInfo{}
dataErrs[i] = errFileCorrupt
continue
// Since erasure.Distribution is trustable we can fix the mismatching erasure.Index
if meta.Erasure.Distribution[i] != meta.Erasure.Index {
partsMetadata[i] = FileInfo{}
dataErrs[i] = errFileCorrupt
continue
}
}
}
@@ -320,11 +296,11 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
// disk has a valid xl.meta but may not have all the
// parts. This is considered an outdated disk, since
// it needs healing too.
if !partsMetadata[i].IsRemote() {
if !partsMetadata[i].Deleted && !partsMetadata[i].IsRemote() {
dataErrs[i] = onlineDisk.VerifyFile(ctx, bucket, object, partsMetadata[i])
}
case madmin.HealNormalScan:
if !partsMetadata[i].IsRemote() {
if !partsMetadata[i].Deleted && !partsMetadata[i].IsRemote() {
dataErrs[i] = onlineDisk.CheckParts(ctx, bucket, object, partsMetadata[i])
}
}
+30 -46
View File
@@ -212,7 +212,7 @@ func shouldHealObjectOnDisk(erErr, dataErr error, meta FileInfo, quorumModTime t
return true
}
if erErr == nil {
if !meta.IsRemote() {
if !meta.Deleted && !meta.IsRemote() {
// If xl.meta was read fine but there may be problem with the part.N files.
if IsErr(dataErr, []error{
errFileNotFound,
@@ -267,10 +267,10 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
// Re-read when we have lock...
partsMetadata, errs := readAllFileInfo(ctx, storageDisks, bucket, object, versionID, true)
_, err = getLatestFileInfo(ctx, partsMetadata, errs)
if err != nil {
if _, err = getLatestFileInfo(ctx, partsMetadata, errs); err != nil {
return er.purgeObjectDangling(ctx, bucket, object, versionID, partsMetadata, errs, []error{}, opts)
}
// List of disks having latest version of the object er.meta
// (by modtime).
_, modTime, dataDir := listOnlineDisks(storageDisks, partsMetadata, errs)
@@ -292,7 +292,8 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
// 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, dataErrs := disksWithAllParts(ctx, storageDisks, partsMetadata, errs, bucket, object, scanMode)
availableDisks, dataErrs := disksWithAllParts(ctx, storageDisks, partsMetadata,
errs, bucket, object, scanMode)
// Loop to find number of disks with valid data, per-drive
// data state and a list of outdated disks on which data needs
@@ -357,7 +358,8 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
err = toObjectErr(errFileVersionNotFound, bucket, object, versionID)
}
// File is fully gone, fileInfo is empty.
return defaultHealResult(FileInfo{}, storageDisks, storageEndpoints, errs, bucket, object, versionID, er.defaultParityCount), err
return er.defaultHealResult(FileInfo{}, storageDisks, storageEndpoints, errs,
bucket, object, versionID), err
}
// If less than read quorum number of disks have all the parts
@@ -642,30 +644,25 @@ func (er erasureObjects) healObjectDir(ctx context.Context, bucket, object strin
// Populates default heal result item entries with possible values when we are returning prematurely.
// This is to ensure that in any circumstance we are not returning empty arrays with wrong values.
func defaultHealResult(lfi FileInfo, storageDisks []StorageAPI, storageEndpoints []string, errs []error, bucket, object, versionID string, defaultParityCount int) madmin.HealResultItem {
func (er erasureObjects) defaultHealResult(lfi FileInfo, storageDisks []StorageAPI, storageEndpoints []string, errs []error, bucket, object, versionID string) madmin.HealResultItem {
// Initialize heal result object
result := madmin.HealResultItem{
Type: madmin.HealItemObject,
Bucket: bucket,
Object: object,
VersionID: versionID,
DiskCount: len(storageDisks),
Type: madmin.HealItemObject,
Bucket: bucket,
Object: object,
ObjectSize: lfi.Size,
VersionID: versionID,
DiskCount: len(storageDisks),
}
if lfi.IsValid() {
result.ObjectSize = lfi.Size
result.ParityBlocks = lfi.Erasure.ParityBlocks
} else {
// Default to most common configuration for erasure blocks.
result.ParityBlocks = defaultParityCount
result.ParityBlocks = er.defaultParityCount
}
result.DataBlocks = len(storageDisks) - result.ParityBlocks
if errs == nil {
// No disks related errors are provided, quit
return result
}
for index, disk := range storageDisks {
if disk == nil {
result.Before.Drives = append(result.Before.Drives, madmin.HealDriveInfo{
@@ -684,6 +681,8 @@ func defaultHealResult(lfi FileInfo, storageDisks []StorageAPI, storageEndpoints
switch errs[index] {
case errFileNotFound, errVolumeNotFound:
driveState = madmin.DriveStateMissing
case nil:
driveState = madmin.DriveStateOk
}
result.Before.Drives = append(result.Before.Drives, madmin.HealDriveInfo{
UUID: "",
@@ -697,15 +696,6 @@ func defaultHealResult(lfi FileInfo, storageDisks []StorageAPI, storageEndpoints
})
}
if !lfi.IsValid() {
// Default to most common configuration for erasure blocks.
result.ParityBlocks = defaultParityCount
result.DataBlocks = len(storageDisks) - defaultParityCount
} else {
result.ParityBlocks = lfi.Erasure.ParityBlocks
result.DataBlocks = lfi.Erasure.DataBlocks
}
return result
}
@@ -793,11 +783,9 @@ func (er erasureObjects) purgeObjectDangling(ctx context.Context, bucket, object
var err error
var returnNotFound bool
if !opts.DryRun && opts.Remove {
if versionID == "" {
err = er.deleteObject(ctx, bucket, object, writeQuorum)
} else {
err = er.deleteObjectVersion(ctx, bucket, object, writeQuorum, FileInfo{VersionID: versionID}, false)
}
err = er.deleteObjectVersion(ctx, bucket, object, writeQuorum, FileInfo{
VersionID: versionID,
}, false)
// If Delete was successful, make sure to return the appropriate error
// and heal result appropriate with delete's error messages
@@ -820,15 +808,18 @@ func (er erasureObjects) purgeObjectDangling(ctx context.Context, bucket, object
if versionID != "" {
err = toObjectErr(errFileVersionNotFound, bucket, object, versionID)
}
return defaultHealResult(m, storageDisks, storageEndpoints, errs, bucket, object, versionID, er.defaultParityCount), err
return er.defaultHealResult(m, storageDisks, storageEndpoints,
errs, bucket, object, versionID), err
}
return defaultHealResult(m, storageDisks, storageEndpoints, errs, bucket, object, versionID, er.defaultParityCount), toObjectErr(err, bucket, object, versionID)
return er.defaultHealResult(m, storageDisks, storageEndpoints,
errs, bucket, object, versionID), toObjectErr(err, bucket, object, versionID)
}
readQuorum := len(storageDisks) - er.defaultParityCount
err := toObjectErr(reduceReadQuorumErrs(ctx, errs, objectOpIgnoredErrs, readQuorum), bucket, object, versionID)
return defaultHealResult(m, storageDisks, storageEndpoints, errs, bucket, object, versionID, er.defaultParityCount), err
err := toObjectErr(reduceReadQuorumErrs(ctx, errs, objectOpIgnoredErrs, readQuorum),
bucket, object, versionID)
return er.defaultHealResult(m, storageDisks, storageEndpoints, errs, bucket, object, versionID), err
}
// Object is considered dangling/corrupted if any only
@@ -914,22 +905,15 @@ func (er erasureObjects) HealObject(ctx context.Context, bucket, object, version
// Perform quick read without lock.
// This allows to quickly check if all is ok or all are missing.
partsMetadata, errs := readAllFileInfo(healCtx, storageDisks, bucket, object, versionID, false)
_, errs := readAllFileInfo(healCtx, storageDisks, bucket, object, versionID, false)
if isAllNotFound(errs) {
err = toObjectErr(errFileNotFound, bucket, object)
if versionID != "" {
err = toObjectErr(errFileVersionNotFound, bucket, object, versionID)
}
// Nothing to do, file is already gone.
return defaultHealResult(FileInfo{}, storageDisks, storageEndpoints, errs, bucket, object, versionID, er.defaultParityCount), err
}
// Return early if all ok and not deep scanning.
if opts.ScanMode == madmin.HealNormalScan && fileInfoConsistent(ctx, partsMetadata, errs) {
fi, err := getLatestFileInfo(ctx, partsMetadata, errs)
if err == nil && fi.VersionID == versionID {
return defaultHealResult(fi, storageDisks, storageEndpoints, errs, bucket, object, versionID, fi.Erasure.ParityBlocks), nil
}
return er.defaultHealResult(FileInfo{}, storageDisks, storageEndpoints,
errs, bucket, object, versionID), err
}
// Heal the object.
+2 -2
View File
@@ -221,7 +221,7 @@ func TestHealObjectCorrupted(t *testing.T) {
t.Fatalf("Failed to getLatestFileInfo - %v", err)
}
if err = firstDisk.CheckFile(context.Background(), bucket, object); err != nil {
if _, err = firstDisk.StatInfoFile(context.Background(), bucket, object+"/"+xlStorageFormatFile); err != nil {
t.Errorf("Expected er.meta file to be present but stat failed - %v", err)
}
@@ -365,7 +365,7 @@ func TestHealObjectErasure(t *testing.T) {
t.Fatalf("Failed to heal object - %v", err)
}
if err = firstDisk.CheckFile(context.Background(), bucket, object); err != nil {
if _, err = firstDisk.StatInfoFile(context.Background(), bucket, object+"/"+xlStorageFormatFile); err != nil {
t.Errorf("Expected er.meta file to be present but stat failed - %v", err)
}
+36 -5
View File
@@ -27,6 +27,8 @@ import (
"time"
"github.com/minio/minio/internal/bucket/replication"
"github.com/minio/minio/internal/crypto"
"github.com/minio/minio/internal/etag"
xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/logger"
"github.com/minio/minio/internal/sync/errgroup"
@@ -141,6 +143,13 @@ func (fi FileInfo) ToObjectInfo(bucket, object string) ObjectInfo {
// Extract etag from metadata.
objInfo.ETag = extractETag(fi.Metadata)
// Verify if Etag is parseable, if yes
// then check if its multipart etag.
et, e := etag.Parse(objInfo.ETag)
if e == nil {
objInfo.Multipart = et.IsMultipart()
}
// Add user tags to the object info
tags := fi.Metadata[xhttp.AmzObjectTagging]
if len(tags) != 0 {
@@ -153,11 +162,13 @@ func (fi FileInfo) ToObjectInfo(bucket, object string) ObjectInfo {
objInfo.ReplicationStatus = replication.StatusType(fi.DeleteMarkerReplicationStatus)
}
objInfo.TransitionStatus = fi.TransitionStatus
objInfo.transitionedObjName = fi.TransitionedObjName
objInfo.transitionVersionID = fi.TransitionVersionID
objInfo.tierFreeVersion = fi.TierFreeVersion()
objInfo.TransitionTier = fi.TransitionTier
objInfo.TransitionedObject = TransitionedObject{
Name: fi.TransitionedObjName,
VersionID: fi.TransitionVersionID,
Status: fi.TransitionStatus,
FreeVersion: fi.TierFreeVersion(),
Tier: fi.TransitionTier,
}
// etag/md5Sum has already been extracted. We need to
// remove to avoid it from appearing as part of
@@ -165,6 +176,14 @@ func (fi FileInfo) ToObjectInfo(bucket, object string) ObjectInfo {
// Tags have also been extracted, we remove that as well.
objInfo.UserDefined = cleanMetadata(fi.Metadata)
// Set multipart for encryption properly if
// not set already.
if !objInfo.Multipart {
if _, ok := crypto.IsEncrypted(objInfo.UserDefined); ok {
objInfo.Multipart = crypto.IsMultiPart(objInfo.UserDefined)
}
}
// All the parts per object.
objInfo.Parts = fi.Parts
@@ -259,6 +278,18 @@ func findFileInfoInQuorum(ctx context.Context, metaArr []FileInfo, modTime time.
h.Write([]byte(fmt.Sprintf("%v", meta.Erasure.Distribution)))
// make sure that length of Data is same
h.Write([]byte(fmt.Sprintf("%v", len(meta.Data))))
// ILM transition fields
h.Write([]byte(meta.TransitionStatus))
h.Write([]byte(meta.TransitionTier))
h.Write([]byte(meta.TransitionedObjName))
h.Write([]byte(meta.TransitionVersionID))
// Server-side replication fields
h.Write([]byte(fmt.Sprintf("%v", meta.MarkDeleted)))
h.Write([]byte(meta.DeleteMarkerReplicationStatus))
h.Write([]byte(meta.VersionPurgeStatus))
h.Write([]byte(meta.Metadata[xhttp.AmzBucketReplicationStatus]))
metaHashes[i] = hex.EncodeToString(h.Sum(nil))
h.Reset()
}
+4 -9
View File
@@ -122,7 +122,7 @@ func (er erasureObjects) renameAll(ctx context.Context, bucket, prefix string) {
wg.Add(1)
go func(disk StorageAPI) {
defer wg.Done()
disk.RenameFile(ctx, bucket, prefix, minioMetaTmpBucket, mustGetUUID())
disk.RenameFile(ctx, bucket, prefix, minioMetaTmpDeletedBucket, mustGetUUID())
}(disk)
}
wg.Wait()
@@ -347,8 +347,9 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
// Fill all the necessary metadata.
// Update `xl.meta` content on each disks.
for index := range partsMetadata {
partsMetadata[index].Metadata = opts.UserDefined
partsMetadata[index].Fresh = true
partsMetadata[index].ModTime = modTime
partsMetadata[index].Metadata = opts.UserDefined
}
uploadID := mustGetUUID()
@@ -782,12 +783,6 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
return oi, toObjectErr(err, bucket, object, uploadID)
}
// Check if an object is present as one of the parent dir.
// -- FIXME. (needs a new kind of lock).
if opts.ParentIsObject != nil && opts.ParentIsObject(rctx, bucket, path.Dir(object)) {
return oi, toObjectErr(errFileParentIsFile, bucket, object)
}
// Calculate s3 compatible md5sum for complete multipart.
s3MD5 := getCompleteMultipartMD5(parts)
@@ -945,7 +940,7 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
if disk != nil && disk.IsOnline() {
continue
}
er.addPartial(bucket, object, fi.VersionID)
er.addPartial(bucket, object, fi.VersionID, fi.Size)
break
}
+185 -55
View File
@@ -256,26 +256,6 @@ func (er erasureObjects) getObjectWithFileInfo(ctx context.Context, bucket, obje
return toObjectErr(err, bucket, object)
}
// This hack is needed to avoid a bug found when overwriting
// the inline data object with a un-inlined version, for the
// time being we need this as we have released inline-data
// version already and this bug is already present in newer
// releases.
//
// This mainly happens with objects < smallFileThreshold when
// they are overwritten with un-inlined objects >= smallFileThreshold,
// due to a bug in RenameData() the fi.Data is not niled leading to
// GetObject thinking that fi.Data is valid while fi.Size has
// changed already.
if _, ok := fi.Metadata[ReservedMetadataPrefixLower+"inline-data"]; ok {
shardFileSize := erasure.ShardFileSize(fi.Size)
if shardFileSize >= 0 && shardFileSize >= smallFileThreshold {
for i := range metaArr {
metaArr[i].Data = nil
}
}
}
var healOnce sync.Once
// once we have obtained a common FileInfo i.e latest, we should stick
@@ -442,7 +422,9 @@ func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object s
}
// if missing metadata can be reconstructed, attempt to reconstruct.
if missingBlocks > 0 && missingBlocks < readQuorum {
// additionally do not heal delete markers inline, let them be
// healed upon regular heal process.
if !fi.Deleted && missingBlocks > 0 && missingBlocks < readQuorum {
if _, healing := er.getOnlineDisksWithHealing(); !healing {
go healObject(bucket, object, fi.VersionID, madmin.HealNormalScan)
}
@@ -580,6 +562,142 @@ func rename(ctx context.Context, disks []StorageAPI, srcBucket, srcEntry, dstBuc
return evalDisks(disks, errs), err
}
func (er erasureObjects) putMetacacheObject(ctx context.Context, key string, r *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) {
data := r.Reader
// No metadata is set, allocate a new one.
if opts.UserDefined == nil {
opts.UserDefined = make(map[string]string)
}
storageDisks := er.getDisks()
// Get parity and data drive count based on storage class metadata
parityDrives := globalStorageClass.GetParityForSC(opts.UserDefined[xhttp.AmzStorageClass])
if parityDrives <= 0 {
parityDrives = er.defaultParityCount
}
dataDrives := len(storageDisks) - parityDrives
// we now know the number of blocks this object needs for data and parity.
// writeQuorum is dataBlocks + 1
writeQuorum := dataDrives
if dataDrives == parityDrives {
writeQuorum++
}
// Validate input data size and it can never be less than zero.
if data.Size() < -1 {
logger.LogIf(ctx, errInvalidArgument, logger.Application)
return ObjectInfo{}, toObjectErr(errInvalidArgument)
}
// Initialize parts metadata
partsMetadata := make([]FileInfo, len(storageDisks))
fi := newFileInfo(pathJoin(minioMetaBucket, key), dataDrives, parityDrives)
fi.DataDir = mustGetUUID()
// Initialize erasure metadata.
for index := range partsMetadata {
partsMetadata[index] = fi
}
// Order disks according to erasure distribution
var onlineDisks []StorageAPI
onlineDisks, partsMetadata = shuffleDisksAndPartsMetadata(storageDisks, partsMetadata, fi)
erasure, err := NewErasure(ctx, fi.Erasure.DataBlocks, fi.Erasure.ParityBlocks, fi.Erasure.BlockSize)
if err != nil {
return ObjectInfo{}, toObjectErr(err, minioMetaBucket, key)
}
// Fetch buffer for I/O, returns from the pool if not allocates a new one and returns.
var buffer []byte
switch size := data.Size(); {
case size == 0:
buffer = make([]byte, 1) // Allocate atleast a byte to reach EOF
case size >= fi.Erasure.BlockSize:
buffer = er.bp.Get()
defer er.bp.Put(buffer)
case size < fi.Erasure.BlockSize:
// No need to allocate fully blockSizeV1 buffer if the incoming data is smaller.
buffer = make([]byte, size, 2*size+int64(fi.Erasure.ParityBlocks+fi.Erasure.DataBlocks-1))
}
if len(buffer) > int(fi.Erasure.BlockSize) {
buffer = buffer[:fi.Erasure.BlockSize]
}
shardFileSize := erasure.ShardFileSize(data.Size())
writers := make([]io.Writer, len(onlineDisks))
inlineBuffers := make([]*bytes.Buffer, len(onlineDisks))
for i, disk := range onlineDisks {
if disk == nil {
continue
}
inlineBuffers[i] = bytes.NewBuffer(make([]byte, 0, shardFileSize))
writers[i] = newStreamingBitrotWriterBuffer(inlineBuffers[i], DefaultBitrotAlgorithm, erasure.ShardSize())
}
n, erasureErr := erasure.Encode(ctx, data, writers, buffer, writeQuorum)
closeBitrotWriters(writers)
if erasureErr != nil {
return ObjectInfo{}, toObjectErr(erasureErr, minioMetaBucket, key)
}
// Should return IncompleteBody{} error when reader has fewer bytes
// than specified in request header.
if n < data.Size() {
return ObjectInfo{}, IncompleteBody{Bucket: minioMetaBucket, Object: key}
}
for i, w := range writers {
if w == nil {
onlineDisks[i] = nil
continue
}
partsMetadata[i].Data = inlineBuffers[i].Bytes()
partsMetadata[i].AddObjectPart(1, "", n, data.ActualSize())
partsMetadata[i].Erasure.AddChecksumInfo(ChecksumInfo{
PartNumber: 1,
Algorithm: DefaultBitrotAlgorithm,
Hash: bitrotWriterSum(w),
})
}
modTime := UTCNow()
// Fill all the necessary metadata.
// Update `xl.meta` content on each disks.
for index := range partsMetadata {
partsMetadata[index].Size = n
partsMetadata[index].Fresh = true
partsMetadata[index].ModTime = modTime
partsMetadata[index].Metadata = opts.UserDefined
}
// Set an additional header when data is inlined.
for index := range partsMetadata {
partsMetadata[index].SetInlineData()
}
for i := 0; i < len(onlineDisks); i++ {
if onlineDisks[i] != nil && onlineDisks[i].IsOnline() {
// Object info is the same in all disks, so we can pick
// the first meta from online disk
fi = partsMetadata[i]
break
}
}
if _, err = writeUniqueFileInfo(ctx, onlineDisks, minioMetaBucket, key, partsMetadata, writeQuorum); err != nil {
return ObjectInfo{}, toObjectErr(err, minioMetaBucket, key)
}
return fi.ToObjectInfo(minioMetaBucket, key), nil
}
// PutObject - creates an object upon reading from the input stream
// until EOF, erasure codes the data across all disk and additionally
// writes `xl.meta` which carries the necessary metadata for future
@@ -658,13 +776,6 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
return ObjectInfo{}, toObjectErr(errInvalidArgument)
}
// Check if an object is present as one of the parent dir.
// -- FIXME. (needs a new kind of lock).
// -- FIXME (this also causes performance issue when disks are down).
if opts.ParentIsObject != nil && opts.ParentIsObject(ctx, bucket, path.Dir(object)) {
return ObjectInfo{}, toObjectErr(errFileParentIsFile, bucket, object)
}
// Initialize parts metadata
partsMetadata := make([]FileInfo, len(storageDisks))
@@ -738,6 +849,15 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
} else if shardFileSize < smallFileThreshold/8 {
inlineBuffers = make([]*bytes.Buffer, len(onlineDisks))
}
} else {
// If compressed, use actual size to determine.
if sz := erasure.ShardFileSize(data.ActualSize()); sz > 0 {
if !opts.Versioned && sz < smallFileThreshold {
inlineBuffers = make([]*bytes.Buffer, len(onlineDisks))
} else if sz < smallFileThreshold/8 {
inlineBuffers = make([]*bytes.Buffer, len(onlineDisks))
}
}
}
for i, disk := range onlineDisks {
if disk == nil {
@@ -745,7 +865,11 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
}
if len(inlineBuffers) > 0 {
inlineBuffers[i] = bytes.NewBuffer(make([]byte, 0, shardFileSize))
sz := shardFileSize
if sz < 0 {
sz = data.ActualSize()
}
inlineBuffers[i] = bytes.NewBuffer(make([]byte, 0, sz))
writers[i] = newStreamingBitrotWriterBuffer(inlineBuffers[i], DefaultBitrotAlgorithm, erasure.ShardSize())
continue
}
@@ -816,7 +940,7 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
if len(inlineBuffers) > 0 {
// Set an additional header when data is inlined.
for index := range partsMetadata {
partsMetadata[index].Metadata[ReservedMetadataPrefixLower+"inline-data"] = "true"
partsMetadata[index].SetInlineData()
}
}
@@ -826,16 +950,6 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
return ObjectInfo{}, toObjectErr(err, bucket, object)
}
// Whether a disk was initially or becomes offline
// during this upload, send it to the MRF list.
for i := 0; i < len(onlineDisks); i++ {
if onlineDisks[i] != nil && onlineDisks[i].IsOnline() {
continue
}
er.addPartial(bucket, object, fi.VersionID)
break
}
for i := 0; i < len(onlineDisks); i++ {
if onlineDisks[i] != nil && onlineDisks[i].IsOnline() {
// Object info is the same in all disks, so we can pick
@@ -844,6 +958,17 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
break
}
}
// Whether a disk was initially or becomes offline
// during this upload, send it to the MRF list.
for i := 0; i < len(onlineDisks); i++ {
if onlineDisks[i] != nil && onlineDisks[i].IsOnline() {
continue
}
er.addPartial(bucket, object, fi.VersionID, fi.Size)
break
}
online = countOnlineDisks(onlineDisks)
return fi.ToObjectInfo(bucket, object), nil
@@ -940,6 +1065,7 @@ func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objec
DeleteMarkerReplicationStatus: objects[i].DeleteMarkerReplicationStatus,
VersionPurgeStatus: objects[i].VersionPurgeStatus,
}
versions[i].SetTierFreeVersionID(mustGetUUID())
if opts.Versioned {
versions[i].VersionID = uuid
}
@@ -1029,7 +1155,7 @@ func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objec
// all other direct versionId references we should
// ensure no dangling file is left over.
er.addPartial(bucket, version.Name, version.VersionID)
er.addPartial(bucket, version.Name, version.VersionID, -1)
break
}
}
@@ -1177,7 +1303,7 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
if disk != nil && disk.IsOnline() {
continue
}
er.addPartial(bucket, object, opts.VersionID)
er.addPartial(bucket, object, opts.VersionID, -1)
break
}
@@ -1192,11 +1318,15 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
// Send the successful but partial upload/delete, however ignore
// if the channel is blocked by other items.
func (er erasureObjects) addPartial(bucket, object, versionID string) {
select {
case er.mrfOpCh <- partialOperation{bucket: bucket, object: object, versionID: versionID}:
default:
}
func (er erasureObjects) addPartial(bucket, object, versionID string, size int64) {
globalMRFState.addPartialOp(partialOperation{
bucket: bucket,
object: object,
versionID: versionID,
size: size,
setIndex: er.setIndex,
poolIndex: er.poolIndex,
})
}
func (er erasureObjects) PutObjectMetadata(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
@@ -1388,7 +1518,8 @@ func (er erasureObjects) TransitionObject(ctx context.Context, bucket, object st
return toObjectErr(err, bucket, object)
}
}
destObj, err := genTransitionObjName()
destObj, err := genTransitionObjName(bucket)
if err != nil {
return err
}
@@ -1421,19 +1552,18 @@ func (er erasureObjects) TransitionObject(ctx context.Context, bucket, object st
if disk != nil && disk.IsOnline() {
continue
}
er.addPartial(bucket, object, opts.VersionID)
er.addPartial(bucket, object, opts.VersionID, -1)
break
}
// Notify object deleted event.
objInfo := fi.ToObjectInfo(bucket, object)
sendEvent(eventArgs{
EventName: eventName,
BucketName: bucket,
Object: ObjectInfo{
Name: object,
VersionID: opts.VersionID,
},
Host: "Internal: [ILM-Transition]",
Object: objInfo,
Host: "Internal: [ILM-Transition]",
})
auditLogLifecycle(ctx, objInfo, ILMTransition)
return err
}
+137 -52
View File
@@ -147,6 +147,39 @@ func (z *erasureServerPools) GetDisksID(ids ...string) []StorageAPI {
return res
}
// GetRawData will return all files with a given raw path to the callback.
// Errors are ignored, only errors from the callback are returned.
// For now only direct file paths are supported.
func (z *erasureServerPools) GetRawData(ctx context.Context, volume, file string, fn func(r io.Reader, host string, disk string, filename string, size int64, modtime time.Time) error) error {
for _, s := range z.serverPools {
for _, disks := range s.erasureDisks {
for i, disk := range disks {
if disk == OfflineDisk {
continue
}
si, err := disk.StatInfoFile(ctx, volume, file)
if err != nil {
continue
}
r, err := disk.ReadFileStream(ctx, volume, file, 0, si.Size)
if err != nil {
continue
}
defer r.Close()
did, err := disk.GetDiskID()
if err != nil {
did = fmt.Sprintf("disk-%d", i)
}
err = fn(r, disk.Hostname(), did, pathJoin(volume, file), si.Size, si.ModTime)
if err != nil {
return err
}
}
}
}
return nil
}
func (z *erasureServerPools) SetDriveCounts() []int {
setDriveCounts := make([]int, len(z.serverPools))
for i := range z.serverPools {
@@ -240,12 +273,7 @@ type poolObjInfo struct {
Err error
}
// getPoolIdxExisting returns the (first) found object pool index containing an object.
// If the object exists, but the latest version is a delete marker, the index with it is still returned.
// If the object does not exist ObjectNotFound error is returned.
// If any other error is found, it is returned.
// The check is skipped if there is only one zone, and 0, nil is always returned in that case.
func (z *erasureServerPools) getPoolIdxExisting(ctx context.Context, bucket, object string) (idx int, err error) {
func (z *erasureServerPools) getPoolIdxExistingWithOpts(ctx context.Context, bucket, object string, opts ObjectOptions) (idx int, err error) {
if z.SinglePool() {
return 0, nil
}
@@ -261,7 +289,7 @@ func (z *erasureServerPools) getPoolIdxExisting(ctx context.Context, bucket, obj
pinfo := poolObjInfo{
PoolIndex: i,
}
pinfo.ObjInfo, pinfo.Err = pool.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
pinfo.ObjInfo, pinfo.Err = pool.GetObjectInfo(ctx, bucket, object, opts)
poolObjInfos[i] = pinfo
}(i, pool)
}
@@ -298,6 +326,19 @@ func (z *erasureServerPools) getPoolIdxExisting(ctx context.Context, bucket, obj
return -1, toObjectErr(errFileNotFound, bucket, object)
}
func (z *erasureServerPools) getPoolIdxExistingNoLock(ctx context.Context, bucket, object string) (idx int, err error) {
return z.getPoolIdxExistingWithOpts(ctx, bucket, object, ObjectOptions{NoLock: true})
}
// getPoolIdxExisting returns the (first) found object pool index containing an object.
// If the object exists, but the latest version is a delete marker, the index with it is still returned.
// If the object does not exist ObjectNotFound error is returned.
// If any other error is found, it is returned.
// The check is skipped if there is only one zone, and 0, nil is always returned in that case.
func (z *erasureServerPools) getPoolIdxExisting(ctx context.Context, bucket, object string) (idx int, err error) {
return z.getPoolIdxExistingWithOpts(ctx, bucket, object, ObjectOptions{})
}
// getPoolIdx returns the found previous object and its corresponding pool idx,
// if none are found falls back to most available space pool.
func (z *erasureServerPools) getPoolIdx(ctx context.Context, bucket, object string, size int64) (idx int, err error) {
@@ -541,6 +582,15 @@ func (z *erasureServerPools) NSScanner(ctx context.Context, bf *bloomFilter, upd
func (z *erasureServerPools) MakeBucketWithLocation(ctx context.Context, bucket string, opts BucketOptions) error {
g := errgroup.WithNErrs(len(z.serverPools))
// Lock the bucket name before creating.
lk := z.NewNSLock(minioMetaTmpBucket, bucket+".lck")
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
if err != nil {
return err
}
ctx = lkctx.Context()
defer lk.Unlock(lkctx.Cancel)
// Create buckets in parallel across all sets.
for index := range z.serverPools {
index := index
@@ -804,27 +854,6 @@ func (z *erasureServerPools) DeleteObjects(ctx context.Context, bucket string, o
objSets.Add(objects[i].ObjectName)
}
poolObjIdxMap := map[int][]ObjectToDelete{}
origIndexMap := map[int][]int{}
if !z.SinglePool() {
for j, obj := range objects {
idx, err := z.getPoolIdxExisting(ctx, bucket, obj.ObjectName)
if isErrObjectNotFound(err) {
derrs[j] = err
continue
}
if err != nil {
// Unhandled errors return right here.
for i := range derrs {
derrs[i] = err
}
return dobjects, derrs
}
poolObjIdxMap[idx] = append(poolObjIdxMap[idx], obj)
origIndexMap[idx] = append(origIndexMap[idx], j)
}
}
// Acquire a bulk write lock across 'objects'
multiDeleteLock := z.NewNSLock(bucket, objSets.ToSlice()...)
lkctx, err := multiDeleteLock.GetLock(ctx, globalOperationTimeout)
@@ -841,17 +870,65 @@ func (z *erasureServerPools) DeleteObjects(ctx context.Context, bucket string, o
return z.serverPools[0].DeleteObjects(ctx, bucket, objects, opts)
}
for idx, pool := range z.serverPools {
objs := poolObjIdxMap[idx]
orgIndexes := origIndexMap[idx]
deletedObjects, errs := pool.DeleteObjects(ctx, bucket, objs, opts)
for i, derr := range errs {
if derr != nil {
derrs[orgIndexes[i]] = derr
// Fetch location of up to 10 objects concurrently.
poolObjIdxMap := map[int][]ObjectToDelete{}
origIndexMap := map[int][]int{}
var mu sync.Mutex
eg := errgroup.WithNErrs(len(objects)).WithConcurrency(10)
cctx, cancel := eg.WithCancelOnError(ctx)
defer cancel()
for j, obj := range objects {
j := j
obj := obj
eg.Go(func() error {
idx, err := z.getPoolIdxExistingNoLock(cctx, bucket, obj.ObjectName)
if isErrObjectNotFound(err) {
derrs[j] = err
return nil
}
dobjects[orgIndexes[i]] = deletedObjects[i]
}
if err != nil {
// unhandled errors return right here.
return err
}
mu.Lock()
poolObjIdxMap[idx] = append(poolObjIdxMap[idx], obj)
origIndexMap[idx] = append(origIndexMap[idx], j)
mu.Unlock()
return nil
}, j)
}
if err := eg.WaitErr(); err != nil {
for i := range derrs {
derrs[i] = err
}
return dobjects, derrs
}
// Delete concurrently in all server pools.
var wg sync.WaitGroup
wg.Add(len(z.serverPools))
for idx, pool := range z.serverPools {
go func(idx int, pool *erasureSets) {
defer wg.Done()
objs := poolObjIdxMap[idx]
if len(objs) > 0 {
orgIndexes := origIndexMap[idx]
deletedObjects, errs := pool.DeleteObjects(ctx, bucket, objs, opts)
mu.Lock()
for i, derr := range errs {
if derr != nil {
derrs[orgIndexes[i]] = derr
}
dobjects[orgIndexes[i]] = deletedObjects[i]
}
mu.Unlock()
}
}(idx, pool)
}
wg.Wait()
return dobjects, derrs
}
@@ -951,6 +1028,7 @@ func (z *erasureServerPools) ListObjectVersions(ctx context.Context, bucket, pre
if err != nil && err != io.EOF {
return loi, err
}
defer merged.truncate(0) // Release when returning
if versionMarker == "" {
o := listPathOptions{Marker: marker}
// If we are not looking for a specific version skip it.
@@ -991,6 +1069,22 @@ func maxKeysPlusOne(maxKeys int, addOne bool) int {
func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsInfo, error) {
var loi ListObjectsInfo
if len(prefix) > 0 && maxKeys == 1 && delimiter == "" && marker == "" {
// Optimization for certain applications like
// - Cohesity
// - Actifio, Splunk etc.
// which send ListObjects requests where the actual object
// itself is the prefix and max-keys=1 in such scenarios
// we can simply verify locally if such an object exists
// to avoid the need for ListObjects().
objInfo, err := z.GetObjectInfo(ctx, bucket, prefix, ObjectOptions{NoLock: true})
if err == nil {
loi.Objects = append(loi.Objects, objInfo)
return loi, nil
}
}
opts := listPathOptions{
Bucket: bucket,
Prefix: prefix,
@@ -1007,6 +1101,7 @@ func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, ma
}
merged.forwardPast(opts.Marker)
defer merged.truncate(0) // Release when returning
// Default is recursive, if delimiter is set then list non recursive.
objects := merged.fileInfos(bucket, prefix, delimiter)
@@ -1335,20 +1430,8 @@ func (z *erasureServerPools) DeleteBucket(ctx context.Context, bucket string, fo
return nil
}
// deleteAll will delete a bucket+prefix unconditionally across all disks.
// Note that set distribution is ignored so it should only be used in cases where
// data is not distributed across sets.
// Errors are logged but individual disk failures are not returned.
func (z *erasureServerPools) deleteAll(ctx context.Context, bucket, prefix string) {
for _, servers := range z.serverPools {
for _, set := range servers.sets {
set.deleteAll(ctx, bucket, prefix)
}
}
}
// renameAll will rename bucket+prefix unconditionally across all disks to
// minioMetaTmpBucket + unique uuid,
// minioMetaTmpDeletedBucket + unique uuid,
// Note that set distribution is ignored so it should only be used in cases where
// data is not distributed across sets. Errors are logged but individual
// disk failures are not returned.
@@ -1611,7 +1694,7 @@ func (z *erasureServerPools) HealObjects(ctx context.Context, bucket, prefix str
path = prefix
}
if err := listPathRaw(ctx, listPathRawOptions{
lopts := listPathRawOptions{
disks: disks,
bucket: bucket,
path: path,
@@ -1627,7 +1710,9 @@ func (z *erasureServerPools) HealObjects(ctx context.Context, bucket, prefix str
}
},
finished: nil,
}); err != nil {
}
if err := listPathRaw(ctx, lopts); err != nil {
errCh <- fmt.Errorf("listPathRaw returned %w: opts(%#v)", err, lopts)
cancel()
return
}
+1 -90
View File
@@ -96,8 +96,6 @@ type erasureSets struct {
disksStorageInfoCache timedValue
mrfMU sync.Mutex
mrfOperations map[healSource]int
lastConnectDisksOpTime time.Time
}
@@ -268,20 +266,11 @@ func (s *erasureSets) connectDisks() {
wg.Wait()
go func() {
idler := time.NewTimer(100 * time.Millisecond)
defer idler.Stop()
for setIndex, justConnected := range setsJustConnected {
if !justConnected {
continue
}
// Send a new set connect event with a timeout
idler.Reset(100 * time.Millisecond)
select {
case s.setReconnectEvent <- setIndex:
case <-idler.C:
}
globalMRFState.newSetReconnected(setIndex, s.poolIndex)
}
}()
}
@@ -375,7 +364,6 @@ func newErasureSets(ctx context.Context, endpoints Endpoints, storageDisks []Sto
setReconnectEvent: make(chan int),
distributionAlgo: format.Erasure.DistributionAlgo,
deploymentID: uuid.MustParse(format.ID),
mrfOperations: make(map[healSource]int),
poolIndex: poolIdx,
}
@@ -446,7 +434,6 @@ func newErasureSets(ctx context.Context, endpoints Endpoints, storageDisks []Sto
nsMutex: mutex,
bp: bp,
bpOld: bpOld,
mrfOpCh: make(chan partialOperation, 10000),
}
}
@@ -466,8 +453,6 @@ func newErasureSets(ctx context.Context, endpoints Endpoints, storageDisks []Sto
// Start the disk monitoring and connect routine.
go s.monitorAndConnectEndpoints(ctx, defaultMonitorConnectEndpointInterval)
go s.maintainMRFList()
go s.healMRFRoutine()
return s, nil
}
@@ -893,18 +878,10 @@ func (s *erasureSets) GetObjectNInfo(ctx context.Context, bucket, object string,
return set.GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts)
}
func (s *erasureSets) parentDirIsObject(ctx context.Context, bucket, parent string) bool {
if parent == "." {
return false
}
return s.getHashedSet(parent).parentDirIsObject(ctx, bucket, parent)
}
// PutObject - writes an object to hashedSet based on the object name.
func (s *erasureSets) PutObject(ctx context.Context, bucket string, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) {
set := s.getHashedSet(object)
auditObjectErasureSet(ctx, object, set)
opts.ParentIsObject = s.parentDirIsObject
return set.PutObject(ctx, bucket, object, data, opts)
}
@@ -1089,7 +1066,6 @@ func (s *erasureSets) AbortMultipartUpload(ctx context.Context, bucket, object,
func (s *erasureSets) CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart, opts ObjectOptions) (objInfo ObjectInfo, err error) {
set := s.getHashedSet(object)
auditObjectErasureSet(ctx, object, set)
opts.ParentIsObject = s.parentDirIsObject
return set.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts)
}
@@ -1388,71 +1364,6 @@ func (s *erasureSets) GetObjectTags(ctx context.Context, bucket, object string,
return er.GetObjectTags(ctx, bucket, object, opts)
}
// maintainMRFList gathers the list of successful partial uploads
// from all underlying er.sets and puts them in a global map which
// should not have more than 10000 entries.
func (s *erasureSets) maintainMRFList() {
var agg = make(chan partialOperation, 10000)
for i, er := range s.sets {
go func(c <-chan partialOperation, setIndex int) {
for msg := range c {
msg.failedSet = setIndex
select {
case agg <- msg:
default:
}
}
}(er.mrfOpCh, i)
}
for fOp := range agg {
s.mrfMU.Lock()
if len(s.mrfOperations) > 10000 {
s.mrfMU.Unlock()
continue
}
s.mrfOperations[healSource{
bucket: fOp.bucket,
object: fOp.object,
versionID: fOp.versionID,
opts: &madmin.HealOpts{Remove: true},
}] = fOp.failedSet
s.mrfMU.Unlock()
}
}
// healMRFRoutine monitors new disks connection, sweep the MRF list
// to find objects related to the new disk that needs to be healed.
func (s *erasureSets) healMRFRoutine() {
// Wait until background heal state is initialized
bgSeq := mustGetHealSequence(GlobalContext)
for setIndex := range s.setReconnectEvent {
// Get the list of objects related the er.set
// to which the connected disk belongs.
var mrfOperations []healSource
s.mrfMU.Lock()
for k, v := range s.mrfOperations {
if v == setIndex {
mrfOperations = append(mrfOperations, k)
}
}
s.mrfMU.Unlock()
// Heal objects
for _, u := range mrfOperations {
waitForLowHTTPReq(globalHealConfig.IOCount, globalHealConfig.Sleep)
// Send an object to background heal
bgSeq.sourceCh <- u
s.mrfMU.Lock()
delete(s.mrfOperations, u)
s.mrfMU.Unlock()
}
}
}
// TransitionObject - transition object content to target tier.
func (s *erasureSets) TransitionObject(ctx context.Context, bucket, object string, opts ObjectOptions) error {
return s.getHashedSet(object).TransitionObject(ctx, bucket, object, opts)
+1 -13
View File
@@ -39,15 +39,6 @@ import (
// OfflineDisk represents an unavailable disk.
var OfflineDisk StorageAPI // zero value is nil
// partialOperation is a successful upload/delete of an object
// but not written in all disks (having quorum)
type partialOperation struct {
bucket string
object string
versionID string
failedSet int
}
// erasureObjects - Implements ER object layer.
type erasureObjects struct {
GatewayUnsupported
@@ -78,8 +69,6 @@ type erasureObjects struct {
// legacy objects.
bpOld *bpool.BytePoolCap
mrfOpCh chan partialOperation
deletedCleanupSleeper *dynamicSleeper
}
@@ -493,11 +482,10 @@ func (er erasureObjects) nsScanner(ctx context.Context, buckets []BucketInfo, bf
Entry: update,
}
if intDataUpdateTracker.debug {
console.Debugln("bucket", bucket.Name, "got update", update)
console.Debugln("z:", er.poolIndex, "s:", er.setIndex, "bucket", name, "got update", update)
}
}
}(cache.Info.Name)
// Calc usage
before := cache.Info.LastUpdate
var err error
+4 -2
View File
@@ -705,8 +705,10 @@ func saveUnformattedFormat(ctx context.Context, storageDisks []StorageAPI, forma
if format == nil {
continue
}
if err := saveFormatErasure(storageDisks[index], format, true); err != nil {
return err
if storageDisks[index] != nil && storageDisks[index].IsOnline() {
if err := saveFormatErasure(storageDisks[index], format, true); err != nil {
return err
}
}
}
return nil
+20
View File
@@ -24,6 +24,7 @@ import (
pathutil "path"
"runtime"
"strings"
"time"
"github.com/minio/minio/internal/lock"
"github.com/minio/minio/internal/logger"
@@ -168,6 +169,25 @@ func fsStat(ctx context.Context, statLoc string) (os.FileInfo, error) {
return fi, nil
}
// fsTouch updates a file access & modtime with current time
func fsTouch(ctx context.Context, statLoc string) error {
if statLoc == "" {
logger.LogIf(ctx, errInvalidArgument)
return errInvalidArgument
}
if err := checkPathLength(statLoc); err != nil {
logger.LogIf(ctx, err)
return err
}
now := time.Now()
if err := os.Chtimes(statLoc, now, now); err != nil {
logger.LogIf(ctx, err)
return err
}
return nil
}
// Lookup if volume exists, returns volume attributes upon success.
func fsStatVolume(ctx context.Context, volume string) (os.FileInfo, error) {
fi, err := fsStat(ctx, volume)
+16 -1
View File
@@ -29,6 +29,8 @@ import (
"time"
jsoniter "github.com/json-iterator/go"
"github.com/minio/minio/internal/crypto"
"github.com/minio/minio/internal/etag"
xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/lock"
"github.com/minio/minio/internal/logger"
@@ -167,6 +169,12 @@ func (m fsMetaV1) ToObjectInfo(bucket, object string, fi os.FileInfo) ObjectInfo
}
objInfo.ETag = extractETag(m.Meta)
et, e := etag.Parse(objInfo.ETag)
if e == nil {
objInfo.Multipart = et.IsMultipart()
}
objInfo.ContentType = m.Meta["content-type"]
objInfo.ContentEncoding = m.Meta["content-encoding"]
if storageClass, ok := m.Meta[xhttp.AmzStorageClass]; ok {
@@ -176,7 +184,6 @@ func (m fsMetaV1) ToObjectInfo(bucket, object string, fi os.FileInfo) ObjectInfo
}
var (
t time.Time
e error
)
if exp, ok := m.Meta["expires"]; ok {
if t, e = time.Parse(http.TimeFormat, exp); e == nil {
@@ -193,6 +200,14 @@ func (m fsMetaV1) ToObjectInfo(bucket, object string, fi os.FileInfo) ObjectInfo
// Tags have also been extracted, we remove that as well.
objInfo.UserDefined = cleanMetadata(m.Meta)
// Set multipart for encryption properly if
// not set already.
if !objInfo.Multipart {
if _, ok := crypto.IsEncrypted(objInfo.UserDefined); ok {
objInfo.Multipart = crypto.IsMultiPart(objInfo.UserDefined)
}
}
// All the parts per object.
objInfo.Parts = m.Parts
-6
View File
@@ -24,7 +24,6 @@ import (
"fmt"
"io/ioutil"
"os"
pathutil "path"
"sort"
"strconv"
"strings"
@@ -550,11 +549,6 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
return oi, toObjectErr(err)
}
// Check if an object is present as one of the parent dir.
if fs.parentDirIsObject(ctx, bucket, pathutil.Dir(object)) {
return oi, toObjectErr(errFileParentIsFile, bucket, object)
}
if _, err := fs.statBucketDir(ctx, bucket); err != nil {
return oi, toObjectErr(err, bucket)
}
+27 -32
View File
@@ -27,6 +27,7 @@ import (
"os"
"os/user"
"path"
"path/filepath"
"sort"
"strings"
"sync"
@@ -533,7 +534,7 @@ func (fs *FSObjects) ListBuckets(ctx context.Context) ([]BucketInfo, error) {
atomic.AddInt64(&fs.activeIOCount, -1)
}()
entries, err := readDir(fs.fsPath)
entries, err := readDirWithOpts(fs.fsPath, readDirOpts{count: -1, followDirSymlink: true})
if err != nil {
logger.LogIf(ctx, errDiskNotFound)
return nil, toObjectErr(errDiskNotFound)
@@ -681,8 +682,15 @@ func (fs *FSObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBu
return oi, toObjectErr(err, srcBucket, srcObject)
}
// Stat the file to get file size.
fi, err := fsStatFile(ctx, pathJoin(fs.fsPath, srcBucket, srcObject))
fsObjectPath := pathJoin(fs.fsPath, srcBucket, srcObject)
// Update object modtime
err = fsTouch(ctx, fsObjectPath)
if err != nil {
return oi, toObjectErr(err, srcBucket, srcObject)
}
// Stat the file to get object info
fi, err := fsStatFile(ctx, fsObjectPath)
if err != nil {
return oi, toObjectErr(err, srcBucket, srcObject)
}
@@ -998,26 +1006,6 @@ func (fs *FSObjects) GetObjectInfo(ctx context.Context, bucket, object string, o
return oi, toObjectErr(err, bucket, object)
}
// This function does the following check, suppose
// object is "a/b/c/d", stat makes sure that objects ""a/b/c""
// "a/b" and "a" do not exist.
func (fs *FSObjects) parentDirIsObject(ctx context.Context, bucket, parent string) bool {
var isParentDirObject func(string) bool
isParentDirObject = func(p string) bool {
if p == "." || p == SlashSeparator {
return false
}
if fsIsFile(ctx, pathJoin(fs.fsPath, bucket, p)) {
// If there is already a file at prefix "p", return true.
return true
}
// Check if there is a file as one of the parent paths.
return isParentDirObject(path.Dir(p))
}
return isParentDirObject(parent)
}
// PutObject - creates an object upon reading from the input stream
// until EOF, writes data directly to configured filesystem path.
// Additionally writes `fs.json` which carries the necessary metadata
@@ -1071,10 +1059,6 @@ func (fs *FSObjects) putObject(ctx context.Context, bucket string, object string
// with a slash separator, we treat it like a valid operation
// and return success.
if isObjectDir(object, data.Size()) {
// Check if an object is present as one of the parent dir.
if fs.parentDirIsObject(ctx, bucket, path.Dir(object)) {
return ObjectInfo{}, toObjectErr(errFileParentIsFile, bucket, object)
}
if err = mkdirAll(pathJoin(fs.fsPath, bucket, object), 0777); err != nil {
logger.LogIf(ctx, err)
return ObjectInfo{}, toObjectErr(err, bucket, object)
@@ -1086,11 +1070,6 @@ func (fs *FSObjects) putObject(ctx context.Context, bucket string, object string
return fsMeta.ToObjectInfo(bucket, object, fi), nil
}
// Check if an object is present as one of the parent dir.
if fs.parentDirIsObject(ctx, bucket, path.Dir(object)) {
return ObjectInfo{}, toObjectErr(errFileParentIsFile, bucket, object)
}
// Validate input data size and it can never be less than zero.
if data.Size() < -1 {
logger.LogIf(ctx, errInvalidArgument, logger.Application)
@@ -1509,3 +1488,19 @@ func (fs *FSObjects) TransitionObject(ctx context.Context, bucket, object string
func (fs *FSObjects) RestoreTransitionedObject(ctx context.Context, bucket, object string, opts ObjectOptions) error {
return NotImplemented{}
}
// GetRawData returns raw file data to the callback.
// Errors are ignored, only errors from the callback are returned.
// For now only direct file paths are supported.
func (fs *FSObjects) GetRawData(ctx context.Context, volume, file string, fn func(r io.Reader, host string, disk string, filename string, size int64, modtime time.Time) error) error {
f, err := os.Open(filepath.Join(fs.fsPath, volume, file))
if err != nil {
return nil
}
defer f.Close()
st, err := f.Stat()
if err != nil || st.IsDir() {
return nil
}
return fn(f, "fs", fs.fsUUID, file, st.Size(), st.ModTime())
}
-94
View File
@@ -26,71 +26,6 @@ import (
"github.com/minio/madmin-go"
)
// Tests for if parent directory is object
func TestFSParentDirIsObject(t *testing.T) {
obj, disk, err := prepareFS()
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(disk)
bucketName := "testbucket"
objectName := "object"
if err = obj.MakeBucketWithLocation(GlobalContext, bucketName, BucketOptions{}); err != nil {
t.Fatal(err)
}
objectContent := "12345"
objInfo, err := obj.PutObject(GlobalContext, bucketName, objectName,
mustGetPutObjReader(t, bytes.NewReader([]byte(objectContent)), int64(len(objectContent)), "", ""), ObjectOptions{})
if err != nil {
t.Fatal(err)
}
if objInfo.Name != objectName {
t.Fatalf("Unexpected object name returned got %s, expected %s", objInfo.Name, objectName)
}
fs := obj.(*FSObjects)
testCases := []struct {
parentIsObject bool
objectName string
}{
// parentIsObject is true if object is available.
{
parentIsObject: true,
objectName: objectName,
},
{
parentIsObject: false,
objectName: "",
},
{
parentIsObject: false,
objectName: ".",
},
// Should not cause infinite loop.
{
parentIsObject: false,
objectName: SlashSeparator,
},
{
parentIsObject: false,
objectName: "\\",
},
// Should not cause infinite loop with double forward slash.
{
parentIsObject: false,
objectName: "//",
},
}
for i, testCase := range testCases {
gotValue := fs.parentDirIsObject(GlobalContext, bucketName, testCase.objectName)
if testCase.parentIsObject != gotValue {
t.Errorf("Test %d: Unexpected value returned got %t, expected %t", i+1, gotValue, testCase.parentIsObject)
}
}
}
// TestNewFS - tests initialization of all input disks
// and constructs a valid `FS` object layer.
func TestNewFS(t *testing.T) {
@@ -226,35 +161,6 @@ func TestFSPutObject(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = obj.PutObject(GlobalContext, bucketName, objectName+"/1", mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), ObjectOptions{})
if err == nil {
t.Fatal("Unexpected should fail here, backend corruption occurred")
}
if nerr, ok := err.(ParentIsObject); !ok {
t.Fatalf("Expected ParentIsObject, got %#v", err)
} else {
if nerr.Bucket != "bucket" {
t.Fatalf("Expected 'bucket', got %s", nerr.Bucket)
}
if nerr.Object != "1/2/3/4/object/1" {
t.Fatalf("Expected '1/2/3/4/object/1', got %s", nerr.Object)
}
}
_, err = obj.PutObject(GlobalContext, bucketName, objectName+"/1/", mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), 0, "", ""), ObjectOptions{})
if err == nil {
t.Fatal("Unexpected should fail here, backned corruption occurred")
}
if nerr, ok := err.(ParentIsObject); !ok {
t.Fatalf("Expected ParentIsObject, got %#v", err)
} else {
if nerr.Bucket != "bucket" {
t.Fatalf("Expected 'bucket', got %s", nerr.Bucket)
}
if nerr.Object != "1/2/3/4/object/1/" {
t.Fatalf("Expected '1/2/3/4/object/1/', got %s", nerr.Object)
}
}
}
// TestFSDeleteObject - test fs.DeleteObject() with healthy and corrupted disks
+6 -20
View File
@@ -209,14 +209,6 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
// To avoid this error situation we check for port availability.
logger.FatalIf(checkPortAvailability(globalMinioHost, globalMinioPort), "Unable to start the gateway")
globalMinioEndpoint = func() string {
host := globalMinioHost
if host == "" {
host = sortIPs(localIP4.ToSlice())[0]
}
return fmt.Sprintf("%s://%s", getURLScheme(globalIsTLS), net.JoinHostPort(host, globalMinioPort))
}()
// Handle gateway specific env
gatewayHandleEnvVars()
@@ -248,16 +240,12 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
// avoid URL path encoding minio/minio#8950
router := mux.NewRouter().SkipClean(true).UseEncodedPath()
if globalEtcdClient != nil {
// Enable STS router if etcd is enabled.
registerSTSRouter(router)
}
enableIAMOps := globalEtcdClient != nil
// Enable STS router if etcd is enabled.
registerSTSRouter(router)
// Enable IAM admin APIs if etcd is enabled, if not just enable basic
// operations such as profiling, server info etc.
registerAdminRouter(router, enableConfigOps, enableIAMOps)
registerAdminRouter(router, enableConfigOps)
// Add healthcheck router
registerHealthCheckRouter(router)
@@ -315,12 +303,10 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
logger.FatalIf(globalNotificationSys.Init(GlobalContext, buckets, newObject), "Unable to initialize notification system")
}
if enableIAMOps {
// Initialize users credentials and policies in background.
globalIAMSys.InitStore(newObject)
// Initialize users credentials and policies in background.
globalIAMSys.InitStore(newObject)
go globalIAMSys.Init(GlobalContext, newObject)
}
go globalIAMSys.Init(GlobalContext, newObject)
if globalCacheConfig.Enabled {
// initialize the new disk cache objects.
+17 -59
View File
@@ -21,13 +21,13 @@ import (
"context"
"net"
"net/http"
"net/url"
"path"
"strings"
"sync/atomic"
"time"
"github.com/minio/minio-go/v7/pkg/set"
xnet "github.com/minio/pkg/net"
humanize "github.com/dustin/go-humanize"
"github.com/minio/minio/internal/config/dns"
@@ -154,15 +154,15 @@ func setRedirectHandler(h http.Handler) http.Handler {
func guessIsBrowserReq(r *http.Request) bool {
aType := getRequestAuthType(r)
ok := strings.Contains(r.Header.Get("User-Agent"), "Mozilla") && globalBrowserEnabled &&
(aType == authTypeJWT || aType == authTypeAnonymous)
return ok
return strings.Contains(r.Header.Get("User-Agent"), "Mozilla") &&
globalBrowserEnabled && aType == authTypeAnonymous
}
func setBrowserRedirectHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
read := r.Method == http.MethodGet || r.Method == http.MethodHead
// Re-direction is handled specifically for browser requests.
if guessIsBrowserReq(r) {
if guessIsBrowserReq(r) && read {
// Fetch the redirect location if any.
if u := getRedirectLocation(r); u != nil {
// Employ a temporary re-direct.
@@ -183,7 +183,7 @@ func shouldProxy() bool {
// redirectable, this is purely internal function and
// serves only limited purpose on redirect-handler for
// browser requests.
func getRedirectLocation(r *http.Request) *url.URL {
func getRedirectLocation(r *http.Request) *xnet.URL {
resource, err := getResource(r.URL.Path, r.Host, globalDomainNames)
if err != nil {
return nil
@@ -197,11 +197,14 @@ func getRedirectLocation(r *http.Request) *url.URL {
} {
bucket, _ := path2BucketObject(resource)
if path.Clean(bucket) == prefix || resource == slashSeparator {
if globalBrowserRedirectURL != nil {
return globalBrowserRedirectURL
}
hostname, _, _ := net.SplitHostPort(r.Host)
if hostname == "" {
hostname = r.Host
}
return &url.URL{
return &xnet.URL{
Host: net.JoinHostPort(hostname, globalMinioConsolePort),
Scheme: func() string {
scheme := "http"
@@ -403,7 +406,7 @@ func setRequestValidityHandler(h http.Handler) http.Handler {
return
}
// Check for bad components in URL query values.
for _, vv := range r.URL.Query() {
for _, vv := range r.Form {
for _, v := range vv {
if hasBadPathComponent(v) {
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInvalidResourceName), r.URL)
@@ -433,55 +436,6 @@ func setBucketForwardingHandler(h http.Handler) http.Handler {
return
}
// For browser requests, when federation is setup we need to
// specifically handle download and upload for browser requests.
if guessIsBrowserReq(r) {
var bucket, _ string
switch r.Method {
case http.MethodPut:
if getRequestAuthType(r) == authTypeJWT {
bucket, _ = path2BucketObjectWithBasePath(minioReservedBucketPath+"/upload", r.URL.Path)
}
case http.MethodGet:
if t := r.URL.Query().Get("token"); t != "" {
bucket, _ = path2BucketObjectWithBasePath(minioReservedBucketPath+"/download", r.URL.Path)
} else if getRequestAuthType(r) != authTypeJWT && !strings.HasPrefix(r.URL.Path, minioReservedBucketPath) {
bucket, _ = request2BucketObjectName(r)
}
}
if bucket == "" {
h.ServeHTTP(w, r)
return
}
sr, err := globalDNSConfig.Get(bucket)
if err != nil {
if err == dns.ErrNoEntriesFound {
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrNoSuchBucket),
r.URL)
} else {
writeErrorResponse(r.Context(), w, toAPIError(r.Context(), err),
r.URL)
}
return
}
if globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(sr)...)).IsEmpty() {
r.URL.Scheme = "http"
if globalIsTLS {
r.URL.Scheme = "https"
}
r.URL.Host = getHostFromSrv(sr)
// Make sure we remove any existing headers before
// proxying the request to another node.
for k := range w.Header() {
w.Header().Del(k)
}
globalForwarder.ServeHTTP(w, r)
return
}
h.ServeHTTP(w, r)
return
}
bucket, object := request2BucketObjectName(r)
// Requests in federated setups for STS type calls which are
@@ -550,11 +504,15 @@ func addCustomHeaders(h http.Handler) http.Handler {
})
}
// addSecurityHeaders adds various HTTP(S) response headers.
// Security Headers enable various security protections behaviors in the client's browser.
func addSecurityHeaders(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
header := w.Header()
header.Set("X-XSS-Protection", "1; mode=block") // Prevents against XSS attacks
header.Set("Content-Security-Policy", "block-all-mixed-content") // prevent mixed (HTTP / HTTPS content)
header.Set("X-XSS-Protection", "1; mode=block") // Prevents against XSS attacks
header.Set("Content-Security-Policy", "block-all-mixed-content") // prevent mixed (HTTP / HTTPS content)
header.Set("X-Content-Type-Options", "nosniff") // Prevent mime-sniff
header.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") // HSTS mitigates variants of MITM attacks
h.ServeHTTP(w, r)
})
}
+17 -5
View File
@@ -79,13 +79,20 @@ func getBackgroundHealStatus(ctx context.Context, o ObjectLayer) (madmin.BgHealS
return madmin.BgHealState{}, false
}
status := madmin.BgHealState{
ScannedItemsCount: bgSeq.getScannedItemsCount(),
}
if globalMRFState.initialized() {
status.MRF = map[string]madmin.MRFStatus{
globalLocalNodeName: globalMRFState.getCurrentMRFRoundInfo(),
}
}
var healDisksMap = map[string]struct{}{}
for _, ep := range getLocalDisksToHeal() {
healDisksMap[ep.String()] = struct{}{}
}
status := madmin.BgHealState{
ScannedItemsCount: bgSeq.getScannedItemsCount(),
}
if o == nil {
healing := globalBackgroundHealState.getLocalHealingDisks()
@@ -225,12 +232,12 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []BucketIn
ScanMode: madmin.HealNormalScan, Remove: healDeleteDangling}); err != nil {
if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
// If not deleted, assume they failed.
tracker.ObjectsFailed++
tracker.ItemsFailed++
tracker.BytesFailed += uint64(version.Size)
logger.LogIf(ctx, err)
}
} else {
tracker.ObjectsHealed++
tracker.ItemsHealed++
tracker.BytesDone += uint64(version.Size)
}
bgSeq.logHeal(madmin.HealItemObject)
@@ -265,6 +272,11 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []BucketIn
finished: nil,
})
if err != nil {
logger.LogIf(ctx, err)
return err
}
select {
// If context is canceled don't mark as done...
case <-ctx.Done():
+11 -1
View File
@@ -45,6 +45,7 @@ import (
"github.com/minio/minio/internal/event"
"github.com/minio/minio/internal/pubsub"
"github.com/minio/pkg/certs"
xnet "github.com/minio/pkg/net"
)
// minio configuration related constants.
@@ -106,7 +107,7 @@ const (
maxBucketSSEConfigSize = 1 * humanize.MiByte
// diskFillFraction is the fraction of a disk we allow to be filled.
diskFillFraction = 0.95
diskFillFraction = 0.99
// diskAssumeUnknownSize is the size to assume when an unknown size upload is requested.
diskAssumeUnknownSize = 1 << 30
@@ -137,6 +138,10 @@ var (
// This flag is set to 'true' by default
globalBrowserEnabled = true
// Custom browser redirect URL, not set by default
// and it is automatically deduced.
globalBrowserRedirectURL *xnet.URL
// This flag is set to 'true' when MINIO_UPDATE env is set to 'off'. Default is false.
globalInplaceUpdateDisabled = false
@@ -211,6 +216,9 @@ var (
// The name of this local node, fetched from arguments
globalLocalNodeName string
// The global subnet license
globalSubnetLicense string
globalRemoteEndpoints map[string]Endpoint
// Global server's network statistics
@@ -288,6 +296,8 @@ var (
globalBackgroundHealRoutine *healRoutine
globalBackgroundHealState *allHealState
globalMRFState mrfState
// If writes to FS backend should be O_SYNC.
globalFSOSync bool
+12
View File
@@ -39,6 +39,7 @@ type apiConfig struct {
totalDriveCount int
replicationWorkers int
replicationFailedWorkers int
transitionWorkers int
}
func (t *apiConfig) init(cfg api.Config, setDriveCounts []int) {
@@ -87,6 +88,10 @@ func (t *apiConfig) init(cfg api.Config, setDriveCounts []int) {
}
t.replicationFailedWorkers = cfg.ReplicationFailedWorkers
t.replicationWorkers = cfg.ReplicationWorkers
if globalTransitionState != nil && cfg.TransitionWorkers != t.transitionWorkers {
globalTransitionState.UpdateWorkers(cfg.TransitionWorkers)
}
t.transitionWorkers = cfg.TransitionWorkers
}
func (t *apiConfig) getListQuorum() int {
@@ -173,3 +178,10 @@ func (t *apiConfig) getReplicationWorkers() int {
return t.replicationWorkers
}
func (t *apiConfig) getTransitionWorkers() int {
t.mu.RLock()
defer t.mu.RUnlock()
return t.transitionWorkers
}
+1 -10
View File
@@ -112,7 +112,7 @@ var userMetadataKeyPrefixes = []string{
// extractMetadata extracts metadata from HTTP header and HTTP queryString.
func extractMetadata(ctx context.Context, r *http.Request) (metadata map[string]string, err error) {
query := r.URL.Query()
query := r.Form
header := r.Header
metadata = make(map[string]string)
// Extract all query values.
@@ -213,15 +213,6 @@ func getReqAccessCred(r *http.Request, region string) (cred auth.Credentials) {
if cred.AccessKey == "" {
cred, _, _ = getReqAccessKeyV2(r)
}
if cred.AccessKey == "" {
claims, owner, _ := webRequestAuthenticate(r)
if owner {
return globalActiveCred
}
if claims != nil {
cred, _ = globalIAMSys.GetUser(claims.AccessKey)
}
}
return cred
}
+1 -1
View File
@@ -42,7 +42,7 @@ func ClusterCheckHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(ctx, globalAPIConfig.getClusterDeadline())
defer cancel()
opts := HealthOptions{Maintenance: r.URL.Query().Get("maintenance") == "true"}
opts := HealthOptions{Maintenance: r.Form.Get("maintenance") == "true"}
result := objLayer.Health(ctx, opts)
if result.WriteQuorum > 0 {
w.Header().Set(xhttp.MinIOWriteQuorum, strconv.Itoa(result.WriteQuorum))
+1 -1
View File
@@ -106,7 +106,7 @@ func getDrivePerfInfos(ctx context.Context, addr string) madmin.DrivePerfInfos {
serialPerf := getDrivePerfInfo(ctx, false)
parallelPerf := getDrivePerfInfo(ctx, true)
return madmin.DrivePerfInfos{
Addr: addr,
NodeCommon: madmin.NodeCommon{Addr: addr},
SerialPerf: serialPerf,
ParallelPerf: parallelPerf,
}
+4 -4
View File
@@ -38,12 +38,12 @@ type ConnStats struct {
}
// Increase total input bytes
func (s *ConnStats) incInputBytes(n int) {
func (s *ConnStats) incInputBytes(n int64) {
atomic.AddUint64(&s.totalInputBytes, uint64(n))
}
// Increase total output bytes
func (s *ConnStats) incOutputBytes(n int) {
func (s *ConnStats) incOutputBytes(n int64) {
atomic.AddUint64(&s.totalOutputBytes, uint64(n))
}
@@ -58,12 +58,12 @@ func (s *ConnStats) getTotalOutputBytes() uint64 {
}
// Increase outbound input bytes
func (s *ConnStats) incS3InputBytes(n int) {
func (s *ConnStats) incS3InputBytes(n int64) {
atomic.AddUint64(&s.s3InputBytes, uint64(n))
}
// Increase outbound output bytes
func (s *ConnStats) incS3OutputBytes(n int) {
func (s *ConnStats) incS3OutputBytes(n int64) {
atomic.AddUint64(&s.s3OutputBytes, uint64(n))
}
-71
View File
@@ -30,11 +30,9 @@ import (
"strings"
"time"
"github.com/gorilla/mux"
"github.com/minio/madmin-go"
"github.com/minio/minio/internal/handlers"
"github.com/minio/minio/internal/logger"
jsonrpc "github.com/minio/rpc"
)
// recordRequest - records the first recLen bytes
@@ -110,75 +108,6 @@ func getOpName(name string) (op string) {
return op
}
// WebTrace gets trace of web request
func WebTrace(ri *jsonrpc.RequestInfo) madmin.TraceInfo {
r := ri.Request
w := ri.ResponseWriter
name := ri.Method
// Setup a http request body recorder
reqHeaders := r.Header.Clone()
reqHeaders.Set("Host", r.Host)
if len(r.TransferEncoding) == 0 {
reqHeaders.Set("Content-Length", strconv.Itoa(int(r.ContentLength)))
} else {
reqHeaders.Set("Transfer-Encoding", strings.Join(r.TransferEncoding, ","))
}
now := time.Now().UTC()
t := madmin.TraceInfo{TraceType: madmin.TraceHTTP, FuncName: name, Time: now}
t.NodeName = r.Host
if globalIsDistErasure {
t.NodeName = globalLocalNodeName
}
if t.NodeName == "" {
t.NodeName = globalLocalNodeName
}
// strip only standard port from the host address
if host, port, err := net.SplitHostPort(t.NodeName); err == nil {
if port == "443" || port == "80" {
t.NodeName = host
}
}
vars := mux.Vars(r)
rq := madmin.TraceRequestInfo{
Time: now,
Proto: r.Proto,
Method: r.Method,
Path: SlashSeparator + pathJoin(vars["bucket"], vars["object"]),
RawQuery: redactLDAPPwd(r.URL.RawQuery),
Client: handlers.GetSourceIP(r),
Headers: reqHeaders,
}
rw, ok := w.(*logger.ResponseWriter)
if ok {
rs := madmin.TraceResponseInfo{
Time: time.Now().UTC(),
Headers: rw.Header().Clone(),
StatusCode: rw.StatusCode,
Body: logger.BodyPlaceHolder,
}
if rs.StatusCode == 0 {
rs.StatusCode = http.StatusOK
}
t.RespInfo = rs
t.CallStats = madmin.TraceCallStats{
Latency: rs.Time.Sub(rw.StartTime),
InputBytes: int(r.ContentLength),
OutputBytes: rw.Size(),
TimeToFirstByte: rw.TimeToFirstByte,
}
}
t.ReqInfo = rq
return t
}
// Trace gets trace of http request
func Trace(f http.HandlerFunc, logBody bool, w http.ResponseWriter, r *http.Request) madmin.TraceInfo {
name := getOpName(runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name())
+133
View File
@@ -0,0 +1,133 @@
// Copyright (c) 2015-2021 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package cmd
import (
"context"
"sync"
"github.com/minio/minio/internal/auth"
iampolicy "github.com/minio/pkg/iam/policy"
)
type iamDummyStore struct {
sync.RWMutex
}
func (ids *iamDummyStore) lock() {
ids.Lock()
}
func (ids *iamDummyStore) unlock() {
ids.Unlock()
}
func (ids *iamDummyStore) rlock() {
ids.RLock()
}
func (ids *iamDummyStore) runlock() {
ids.RUnlock()
}
func (ids *iamDummyStore) migrateBackendFormat(context.Context) error {
return nil
}
func (ids *iamDummyStore) loadPolicyDoc(ctx context.Context, policy string, m map[string]iampolicy.Policy) error {
return nil
}
func (ids *iamDummyStore) loadPolicyDocs(ctx context.Context, m map[string]iampolicy.Policy) error {
return nil
}
func (ids *iamDummyStore) loadUser(ctx context.Context, user string, userType IAMUserType, m map[string]auth.Credentials) error {
return nil
}
func (ids *iamDummyStore) loadUsers(ctx context.Context, userType IAMUserType, m map[string]auth.Credentials) error {
return nil
}
func (ids *iamDummyStore) loadGroup(ctx context.Context, group string, m map[string]GroupInfo) error {
return nil
}
func (ids *iamDummyStore) loadGroups(ctx context.Context, m map[string]GroupInfo) error {
return nil
}
func (ids *iamDummyStore) loadMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool, m map[string]MappedPolicy) error {
return nil
}
func (ids *iamDummyStore) loadMappedPolicies(ctx context.Context, userType IAMUserType, isGroup bool, m map[string]MappedPolicy) error {
return nil
}
func (ids *iamDummyStore) loadAll(ctx context.Context, sys *IAMSys) error {
return sys.Load(ctx, ids)
}
func (ids *iamDummyStore) saveIAMConfig(ctx context.Context, item interface{}, path string, opts ...options) error {
return nil
}
func (ids *iamDummyStore) loadIAMConfig(ctx context.Context, item interface{}, path string) error {
return nil
}
func (ids *iamDummyStore) deleteIAMConfig(ctx context.Context, path string) error {
return nil
}
func (ids *iamDummyStore) savePolicyDoc(ctx context.Context, policyName string, p iampolicy.Policy) error {
return nil
}
func (ids *iamDummyStore) saveMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool, mp MappedPolicy, opts ...options) error {
return nil
}
func (ids *iamDummyStore) saveUserIdentity(ctx context.Context, name string, userType IAMUserType, u UserIdentity, opts ...options) error {
return nil
}
func (ids *iamDummyStore) saveGroupInfo(ctx context.Context, group string, gi GroupInfo) error {
return nil
}
func (ids *iamDummyStore) deletePolicyDoc(ctx context.Context, policyName string) error {
return nil
}
func (ids *iamDummyStore) deleteMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool) error {
return nil
}
func (ids *iamDummyStore) deleteUserIdentity(ctx context.Context, name string, userType IAMUserType) error {
return nil
}
func (ids *iamDummyStore) deleteGroupInfo(ctx context.Context, name string) error {
return nil
}
func (ids *iamDummyStore) watch(context.Context, *IAMSys) {
}
+7 -7
View File
@@ -162,7 +162,7 @@ func (ies *IAMEtcdStore) migrateUsersConfigToV1(ctx context.Context) error {
// 2. copy policy to new loc.
mp := newMappedPolicy(policyName)
userType := regularUser
userType := regUser
path := getMappedPolicyPath(user, userType, false)
if err := ies.saveIAMConfig(ctx, mp, path); err != nil {
return err
@@ -335,7 +335,7 @@ func (ies *IAMEtcdStore) loadUser(ctx context.Context, user string, userType IAM
func (ies *IAMEtcdStore) loadUsers(ctx context.Context, userType IAMUserType, m map[string]auth.Credentials) error {
var basePrefix string
switch userType {
case srvAccUser:
case svcUser:
basePrefix = iamConfigServiceAccountsPrefix
case stsUser:
basePrefix = iamConfigSTSPrefix
@@ -432,7 +432,7 @@ func (ies *IAMEtcdStore) loadMappedPolicies(ctx context.Context, userType IAMUse
basePrefix = iamConfigPolicyDBGroupsPrefix
} else {
switch userType {
case srvAccUser:
case svcUser:
basePrefix = iamConfigPolicyDBServiceAccountsPrefix
case stsUser:
basePrefix = iamConfigPolicyDBSTSUsersPrefix
@@ -567,7 +567,7 @@ func (ies *IAMEtcdStore) reloadFromEvent(sys *IAMSys, event *etcd.Event) {
case usersPrefix:
accessKey := path.Dir(strings.TrimPrefix(string(event.Kv.Key),
iamConfigUsersPrefix))
ies.loadUser(ctx, accessKey, regularUser, sys.iamUsersMap)
ies.loadUser(ctx, accessKey, regUser, sys.iamUsersMap)
case stsPrefix:
accessKey := path.Dir(strings.TrimPrefix(string(event.Kv.Key),
iamConfigSTSPrefix))
@@ -593,7 +593,7 @@ func (ies *IAMEtcdStore) reloadFromEvent(sys *IAMSys, event *etcd.Event) {
case svcPrefix:
accessKey := path.Dir(strings.TrimPrefix(string(event.Kv.Key),
iamConfigServiceAccountsPrefix))
ies.loadUser(ctx, accessKey, srvAccUser, sys.iamUsersMap)
ies.loadUser(ctx, accessKey, svcUser, sys.iamUsersMap)
case groupsPrefix:
group := path.Dir(strings.TrimPrefix(string(event.Kv.Key),
iamConfigGroupsPrefix))
@@ -609,7 +609,7 @@ func (ies *IAMEtcdStore) reloadFromEvent(sys *IAMSys, event *etcd.Event) {
policyMapFile := strings.TrimPrefix(string(event.Kv.Key),
iamConfigPolicyDBUsersPrefix)
user := strings.TrimSuffix(policyMapFile, ".json")
ies.loadMappedPolicy(ctx, user, regularUser, false, sys.iamUserPolicyMap)
ies.loadMappedPolicy(ctx, user, regUser, false, sys.iamUserPolicyMap)
case policyDBSTSUsersPrefix:
policyMapFile := strings.TrimPrefix(string(event.Kv.Key),
iamConfigPolicyDBSTSUsersPrefix)
@@ -619,7 +619,7 @@ func (ies *IAMEtcdStore) reloadFromEvent(sys *IAMSys, event *etcd.Event) {
policyMapFile := strings.TrimPrefix(string(event.Kv.Key),
iamConfigPolicyDBGroupsPrefix)
user := strings.TrimSuffix(policyMapFile, ".json")
ies.loadMappedPolicy(ctx, user, regularUser, true, sys.iamGroupPolicyMap)
ies.loadMappedPolicy(ctx, user, regUser, true, sys.iamGroupPolicyMap)
}
case eventDelete:
switch {
+3 -3
View File
@@ -104,7 +104,7 @@ func (iamOS *IAMObjectStore) migrateUsersConfigToV1(ctx context.Context) error {
// 2. copy policy file to new location.
mp := newMappedPolicy(policyName)
userType := regularUser
userType := regUser
if err := iamOS.saveMappedPolicy(ctx, user, userType, false, mp); err != nil {
return err
}
@@ -279,7 +279,7 @@ func (iamOS *IAMObjectStore) loadUser(ctx context.Context, user string, userType
func (iamOS *IAMObjectStore) loadUsers(ctx context.Context, userType IAMUserType, m map[string]auth.Credentials) error {
var basePrefix string
switch userType {
case srvAccUser:
case svcUser:
basePrefix = iamConfigServiceAccountsPrefix
case stsUser:
basePrefix = iamConfigSTSPrefix
@@ -348,7 +348,7 @@ func (iamOS *IAMObjectStore) loadMappedPolicies(ctx context.Context, userType IA
basePath = iamConfigPolicyDBGroupsPrefix
} else {
switch userType {
case srvAccUser:
case svcUser:
basePath = iamConfigPolicyDBServiceAccountsPrefix
case stsUser:
basePath = iamConfigPolicyDBSTSUsersPrefix
+315 -102
View File
@@ -112,7 +112,7 @@ func getIAMFormatFilePath() string {
func getUserIdentityPath(user string, userType IAMUserType) string {
var basePath string
switch userType {
case srvAccUser:
case svcUser:
basePath = iamConfigServiceAccountsPrefix
case stsUser:
basePath = iamConfigSTSPrefix
@@ -135,7 +135,7 @@ func getMappedPolicyPath(name string, userType IAMUserType, isGroup bool) string
return pathJoin(iamConfigPolicyDBGroupsPrefix, name+".json")
}
switch userType {
case srvAccUser:
case svcUser:
return pathJoin(iamConfigPolicyDBServiceAccountsPrefix, name+".json")
case stsUser:
return pathJoin(iamConfigPolicyDBSTSUsersPrefix, name+".json")
@@ -230,9 +230,9 @@ type IAMSys struct {
type IAMUserType int
const (
regularUser IAMUserType = iota
regUser IAMUserType = iota
stsUser
srvAccUser
svcUser
)
// key options
@@ -355,7 +355,7 @@ func (sys *IAMSys) LoadPolicyMapping(objAPI ObjectLayer, userOrGroup string, isG
if globalEtcdClient == nil {
var err error
userType := regularUser
userType := regUser
if sys.usersSysType == LDAPUsersSysType {
userType = stsUser
}
@@ -432,7 +432,7 @@ func (sys *IAMSys) LoadServiceAccount(accessKey string) error {
defer sys.store.unlock()
if globalEtcdClient == nil {
err := sys.store.loadUser(context.Background(), accessKey, srvAccUser, sys.iamUsersMap)
err := sys.store.loadUser(context.Background(), accessKey, svcUser, sys.iamUsersMap)
if err != nil {
return err
}
@@ -452,7 +452,11 @@ func (sys *IAMSys) InitStore(objAPI ObjectLayer) {
defer sys.Unlock()
if globalEtcdClient == nil {
sys.store = newIAMObjectStore(objAPI)
if globalIsGateway {
sys.store = &iamDummyStore{}
} else {
sys.store = newIAMObjectStore(objAPI)
}
} else {
sys.store = newIAMEtcdStore()
}
@@ -492,7 +496,7 @@ func (sys *IAMSys) Load(ctx context.Context, store IAMStorageAPI) error {
setDefaultCannedPolicies(iamPolicyDocsMap)
if isMinIOUsersSys {
if err := store.loadUsers(ctx, regularUser, iamUsersMap); err != nil {
if err := store.loadUsers(ctx, regUser, iamUsersMap); err != nil {
return err
}
if err := store.loadGroups(ctx, iamGroupsMap); err != nil {
@@ -501,16 +505,16 @@ func (sys *IAMSys) Load(ctx context.Context, store IAMStorageAPI) error {
}
// load polices mapped to users
if err := store.loadMappedPolicies(ctx, regularUser, false, iamUserPolicyMap); err != nil {
if err := store.loadMappedPolicies(ctx, regUser, false, iamUserPolicyMap); err != nil {
return err
}
// load policies mapped to groups
if err := store.loadMappedPolicies(ctx, regularUser, true, iamGroupPolicyMap); err != nil {
if err := store.loadMappedPolicies(ctx, regUser, true, iamGroupPolicyMap); err != nil {
return err
}
if err := store.loadUsers(ctx, srvAccUser, iamUsersMap); err != nil {
if err := store.loadUsers(ctx, svcUser, iamUsersMap); err != nil {
return err
}
@@ -545,33 +549,11 @@ func (sys *IAMSys) Load(ctx context.Context, store IAMStorageAPI) error {
}
// purge any expired entries which became expired now.
var expiredEntries []string
for k, v := range sys.iamUsersMap {
if v.IsExpired() {
delete(sys.iamUsersMap, k)
delete(sys.iamUserPolicyMap, k)
expiredEntries = append(expiredEntries, k)
// Deleting on the disk is taken care of in the next cycle
}
}
for _, v := range sys.iamUsersMap {
if v.IsServiceAccount() {
for _, accessKey := range expiredEntries {
if v.ParentUser == accessKey {
_ = store.deleteUserIdentity(ctx, v.AccessKey, srvAccUser)
delete(sys.iamUsersMap, v.AccessKey)
}
}
}
}
// purge any expired entries which became expired now.
for k, v := range sys.iamUsersMap {
if v.IsExpired() {
delete(sys.iamUsersMap, k)
delete(sys.iamUserPolicyMap, k)
// Deleting on the etcd is taken care of in the next cycle
// deleting will be done in the next cycle.
}
}
@@ -668,6 +650,25 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer) {
break
}
// Set up polling for expired accounts and credentials purging.
switch {
case globalOpenIDConfig.ProviderEnabled():
go func() {
for {
time.Sleep(globalRefreshIAMInterval)
sys.purgeExpiredCredentialsForExternalSSO(ctx)
}
}()
case globalLDAPConfig.EnabledWithLookupBind():
go func() {
for {
time.Sleep(globalRefreshIAMInterval)
sys.purgeExpiredCredentialsForLDAP(ctx)
sys.updateGroupMembershipsForLDAP(ctx)
}
}()
}
go sys.store.watch(ctx, sys)
}
@@ -708,7 +709,7 @@ func (sys *IAMSys) DeletePolicy(policyName string) error {
if cr.IsTemp() {
sys.policyDBSet(u, strings.Join(pset.ToSlice(), ","), stsUser, false)
} else {
sys.policyDBSet(u, strings.Join(pset.ToSlice(), ","), regularUser, false)
sys.policyDBSet(u, strings.Join(pset.ToSlice(), ","), regUser, false)
}
}
}
@@ -718,7 +719,7 @@ func (sys *IAMSys) DeletePolicy(policyName string) error {
pset := mp.policySet()
if pset.Contains(policyName) {
pset.Remove(policyName)
sys.policyDBSet(g, strings.Join(pset.ToSlice(), ","), regularUser, true)
sys.policyDBSet(g, strings.Join(pset.ToSlice(), ","), regUser, true)
}
}
@@ -823,7 +824,7 @@ func (sys *IAMSys) DeleteUser(accessKey string) error {
// Delete any service accounts if any first.
if u.IsServiceAccount() {
if u.ParentUser == accessKey {
_ = sys.store.deleteUserIdentity(context.Background(), u.AccessKey, srvAccUser)
_ = sys.store.deleteUserIdentity(context.Background(), u.AccessKey, svcUser)
delete(sys.iamUsersMap, u.AccessKey)
}
}
@@ -837,8 +838,8 @@ func (sys *IAMSys) DeleteUser(accessKey string) error {
}
// It is ok to ignore deletion error on the mapped policy
sys.store.deleteMappedPolicy(context.Background(), accessKey, regularUser, false)
err := sys.store.deleteUserIdentity(context.Background(), accessKey, regularUser)
sys.store.deleteMappedPolicy(context.Background(), accessKey, regUser, false)
err := sys.store.deleteUserIdentity(context.Background(), accessKey, regUser)
if err == errNoSuchUser {
// ignore if user is already deleted.
err = nil
@@ -1153,7 +1154,7 @@ func (sys *IAMSys) SetUserStatus(accessKey string, status madmin.AccountStatus)
}(),
})
if err := sys.store.saveUserIdentity(context.Background(), accessKey, regularUser, uinfo); err != nil {
if err := sys.store.saveUserIdentity(context.Background(), accessKey, regUser, uinfo); err != nil {
return err
}
@@ -1165,6 +1166,9 @@ type newServiceAccountOpts struct {
sessionPolicy *iampolicy.Policy
accessKey string
secretKey string
// LDAP username
ldapUsername string
}
// NewServiceAccount - create a new service account
@@ -1222,6 +1226,11 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro
m[iamPolicyClaimNameSA()] = "inherited-policy"
}
// For LDAP service account, save the ldap username in the metadata.
if opts.ldapUsername != "" {
m[ldapUserN] = opts.ldapUsername
}
var (
cred auth.Credentials
)
@@ -1240,7 +1249,7 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro
u := newUserIdentity(cred)
if err := sys.store.saveUserIdentity(context.Background(), u.Credentials.AccessKey, srvAccUser, u); err != nil {
if err := sys.store.saveUserIdentity(context.Background(), u.Credentials.AccessKey, svcUser, u); err != nil {
return auth.Credentials{}, err
}
@@ -1310,7 +1319,7 @@ func (sys *IAMSys) UpdateServiceAccount(ctx context.Context, accessKey string, o
}
u := newUserIdentity(cr)
if err := sys.store.saveUserIdentity(context.Background(), u.Credentials.AccessKey, srvAccUser, u); err != nil {
if err := sys.store.saveUserIdentity(context.Background(), u.Credentials.AccessKey, svcUser, u); err != nil {
return err
}
@@ -1397,12 +1406,8 @@ func (sys *IAMSys) DeleteServiceAccount(ctx context.Context, accessKey string) e
}
// It is ok to ignore deletion error on the mapped policy
err := sys.store.deleteUserIdentity(context.Background(), accessKey, srvAccUser)
if err != nil {
// ignore if user is already deleted.
if err == errNoSuchUser {
return nil
}
err := sys.store.deleteUserIdentity(context.Background(), accessKey, svcUser)
if err != nil && err != errNoSuchUser {
return err
}
@@ -1448,7 +1453,7 @@ func (sys *IAMSys) CreateUser(accessKey string, uinfo madmin.UserInfo) error {
}(),
})
if err := sys.store.saveUserIdentity(context.Background(), accessKey, regularUser, u); err != nil {
if err := sys.store.saveUserIdentity(context.Background(), accessKey, regUser, u); err != nil {
return err
}
@@ -1456,7 +1461,7 @@ func (sys *IAMSys) CreateUser(accessKey string, uinfo madmin.UserInfo) error {
// Set policy if specified.
if uinfo.PolicyName != "" {
return sys.policyDBSet(accessKey, uinfo.PolicyName, regularUser, false)
return sys.policyDBSet(accessKey, uinfo.PolicyName, regUser, false)
}
return nil
}
@@ -1489,7 +1494,7 @@ func (sys *IAMSys) SetUserSecretKey(accessKey string, secretKey string) error {
cred.SecretKey = secretKey
u := newUserIdentity(cred)
if err := sys.store.saveUserIdentity(context.Background(), accessKey, regularUser, u); err != nil {
if err := sys.store.saveUserIdentity(context.Background(), accessKey, regUser, u); err != nil {
return err
}
@@ -1501,18 +1506,18 @@ func (sys *IAMSys) loadUserFromStore(accessKey string) {
sys.store.lock()
// If user is already found proceed.
if _, found := sys.iamUsersMap[accessKey]; !found {
sys.store.loadUser(context.Background(), accessKey, regularUser, sys.iamUsersMap)
sys.store.loadUser(context.Background(), accessKey, regUser, sys.iamUsersMap)
if _, found = sys.iamUsersMap[accessKey]; found {
// found user, load its mapped policies
sys.store.loadMappedPolicy(context.Background(), accessKey, regularUser, false, sys.iamUserPolicyMap)
sys.store.loadMappedPolicy(context.Background(), accessKey, regUser, false, sys.iamUserPolicyMap)
} else {
sys.store.loadUser(context.Background(), accessKey, srvAccUser, sys.iamUsersMap)
sys.store.loadUser(context.Background(), accessKey, svcUser, sys.iamUsersMap)
if svc, found := sys.iamUsersMap[accessKey]; found {
// Found service account, load its parent user and its mapped policies.
if sys.usersSysType == MinIOUsersSysType {
sys.store.loadUser(context.Background(), svc.ParentUser, regularUser, sys.iamUsersMap)
sys.store.loadUser(context.Background(), svc.ParentUser, regUser, sys.iamUsersMap)
}
sys.store.loadMappedPolicy(context.Background(), svc.ParentUser, regularUser, false, sys.iamUserPolicyMap)
sys.store.loadMappedPolicy(context.Background(), svc.ParentUser, regUser, false, sys.iamUserPolicyMap)
} else {
// None found fall back to STS users.
sys.store.loadUser(context.Background(), accessKey, stsUser, sys.iamUsersMap)
@@ -1535,6 +1540,188 @@ func (sys *IAMSys) loadUserFromStore(accessKey string) {
sys.store.unlock()
}
// purgeExpiredCredentialsForExternalSSO - validates if local credentials are still valid
// by checking remote IDP if the relevant users are still active and present.
func (sys *IAMSys) purgeExpiredCredentialsForExternalSSO(ctx context.Context) {
sys.store.lock()
parentUsersMap := make(map[string][]auth.Credentials, len(sys.iamUsersMap))
for _, cred := range sys.iamUsersMap {
if cred.IsServiceAccount() || cred.IsTemp() {
userid, err := parseOpenIDParentUser(cred.ParentUser)
if err == errSkipFile {
continue
}
parentUsersMap[userid] = append(parentUsersMap[userid], cred)
}
}
sys.store.unlock()
expiredUsers := make([]auth.Credentials, 0, len(parentUsersMap))
for userid, creds := range parentUsersMap {
u, err := globalOpenIDConfig.LookupUser(userid)
if err != nil {
logger.LogIf(GlobalContext, err)
continue
}
// Disabled parentUser purge the entries locally
if !u.Enabled {
expiredUsers = append(expiredUsers, creds...)
}
}
for _, cred := range expiredUsers {
userType := regUser
if cred.IsServiceAccount() {
userType = svcUser
} else if cred.IsTemp() {
userType = stsUser
}
sys.store.deleteIAMConfig(ctx, getUserIdentityPath(cred.AccessKey, userType))
sys.store.deleteIAMConfig(ctx, getMappedPolicyPath(cred.AccessKey, userType, false))
}
sys.store.lock()
for _, cred := range expiredUsers {
delete(sys.iamUsersMap, cred.AccessKey)
delete(sys.iamUserPolicyMap, cred.AccessKey)
}
sys.store.unlock()
}
// purgeExpiredCredentialsForLDAP - validates if local credentials are still
// valid by checking LDAP server if the relevant users are still present.
func (sys *IAMSys) purgeExpiredCredentialsForLDAP(ctx context.Context) {
sys.store.lock()
parentUsersMap := make(map[string][]auth.Credentials, len(sys.iamUsersMap))
parentUsers := make([]string, 0, len(sys.iamUsersMap))
for _, cred := range sys.iamUsersMap {
if cred.IsServiceAccount() || cred.IsTemp() {
if globalLDAPConfig.IsLDAPUserDN(cred.ParentUser) {
if _, ok := parentUsersMap[cred.ParentUser]; !ok {
parentUsers = append(parentUsers, cred.ParentUser)
}
parentUsersMap[cred.ParentUser] = append(parentUsersMap[cred.ParentUser], cred)
}
}
}
sys.store.unlock()
expiredUsers, err := globalLDAPConfig.GetNonEligibleUserDistNames(parentUsers)
if err != nil {
// Log and return on error - perhaps it'll work the next time.
logger.LogIf(GlobalContext, err)
return
}
for _, expiredUser := range expiredUsers {
for _, cred := range parentUsersMap[expiredUser] {
userType := regUser
if cred.IsServiceAccount() {
userType = svcUser
} else if cred.IsTemp() {
userType = stsUser
}
sys.store.deleteIAMConfig(ctx, getUserIdentityPath(cred.AccessKey, userType))
sys.store.deleteIAMConfig(ctx, getMappedPolicyPath(cred.AccessKey, userType, false))
}
}
sys.store.lock()
for _, user := range expiredUsers {
for _, cred := range parentUsersMap[user] {
delete(sys.iamUsersMap, cred.AccessKey)
delete(sys.iamUserPolicyMap, cred.AccessKey)
}
}
sys.store.unlock()
}
// updateGroupMembershipsForLDAP - updates the list of groups associated with the credential.
func (sys *IAMSys) updateGroupMembershipsForLDAP(ctx context.Context) {
// 1. Collect all LDAP users with active creds.
sys.store.lock()
// List of unique LDAP (parent) user DNs that have active creds
parentUsers := make([]string, 0, len(sys.iamUsersMap))
// Map of LDAP user to list of active credential objects
parentUserToCredsMap := make(map[string][]auth.Credentials, len(sys.iamUsersMap))
// DN to ldap username mapping for each LDAP user
parentUserToLDAPUsernameMap := make(map[string]string, len(sys.iamUsersMap))
for _, cred := range sys.iamUsersMap {
if cred.IsServiceAccount() || cred.IsTemp() {
if globalLDAPConfig.IsLDAPUserDN(cred.ParentUser) {
// Check if this is the first time we are
// encountering this LDAP user.
if _, ok := parentUserToCredsMap[cred.ParentUser]; !ok {
// Try to find the ldapUsername for this
// parentUser by extracting JWT claims
jwtClaims, err := auth.ExtractClaims(cred.SessionToken, globalActiveCred.SecretKey)
if err != nil {
// skip this cred - session token seems
// invalid
continue
}
ldapUsername, ok := jwtClaims.Lookup(ldapUserN)
if !ok {
// skip this cred - we dont have the
// username info needed
continue
}
// Collect each new cred.ParentUser into parentUsers
parentUsers = append(parentUsers, cred.ParentUser)
// Update the ldapUsernameMap
parentUserToLDAPUsernameMap[cred.ParentUser] = ldapUsername
}
parentUserToCredsMap[cred.ParentUser] = append(parentUserToCredsMap[cred.ParentUser], cred)
}
}
}
sys.store.unlock()
// 2. Query LDAP server for groups of the LDAP users collected.
updatedGroups, err := globalLDAPConfig.LookupGroupMemberships(parentUsers, parentUserToLDAPUsernameMap)
if err != nil {
// Log and return on error - perhaps it'll work the next time.
logger.LogIf(GlobalContext, err)
return
}
// 3. Update creds for those users whose groups are changed
sys.store.lock()
defer sys.store.unlock()
for _, parentUser := range parentUsers {
currGroupsSet := updatedGroups[parentUser]
currGroups := currGroupsSet.ToSlice()
for _, cred := range parentUserToCredsMap[parentUser] {
gSet := set.CreateStringSet(cred.Groups...)
if gSet.Equals(currGroupsSet) {
// No change to groups memberships for this
// credential.
continue
}
cred.Groups = currGroups
userType := regUser
if cred.IsServiceAccount() {
userType = svcUser
} else if cred.IsTemp() {
userType = stsUser
}
// Overwrite the user identity here. As store should be
// atomic, it shouldn't cause any corruption.
if err := sys.store.saveUserIdentity(ctx, cred.AccessKey, userType, newUserIdentity(cred)); err != nil {
// Log and continue error - perhaps it'll work the next time.
logger.LogIf(GlobalContext, err)
continue
}
// If we wrote the updated creds to IAM storage, we can
// update the in memory map.
sys.iamUsersMap[cred.AccessKey] = cred
}
}
}
// GetUser - get user credentials
func (sys *IAMSys) GetUser(accessKey string) (cred auth.Credentials, ok bool) {
if !sys.Initialized() {
@@ -1693,7 +1880,7 @@ func (sys *IAMSys) RemoveUsersFromGroup(group string, members []string) error {
// Remove the group from storage. First delete the
// mapped policy. No-mapped-policy case is ignored.
if err := sys.store.deleteMappedPolicy(context.Background(), group, regularUser, true); err != nil && err != errNoSuchPolicy {
if err := sys.store.deleteMappedPolicy(context.Background(), group, regUser, true); err != nil && err != errNoSuchPolicy {
return err
}
if err := sys.store.deleteGroupInfo(context.Background(), group); err != nil && err != errNoSuchGroup {
@@ -1839,7 +2026,7 @@ func (sys *IAMSys) PolicyDBSet(name, policy string, isGroup bool) error {
return sys.policyDBSet(name, policy, stsUser, isGroup)
}
return sys.policyDBSet(name, policy, regularUser, isGroup)
return sys.policyDBSet(name, policy, regUser, isGroup)
}
// policyDBSet - sets a policy for user in the policy db. Assumes that caller
@@ -1867,7 +2054,7 @@ func (sys *IAMSys) policyDBSet(name, policyName string, userType IAMUserType, is
// Add a fallback removal towards previous content that may come back
// as a ghost user due to lack of delete, this change occurred
// introduced in PR #11840
sys.store.deleteMappedPolicy(context.Background(), name, regularUser, false)
sys.store.deleteMappedPolicy(context.Background(), name, regUser, false)
}
err := sys.store.deleteMappedPolicy(context.Background(), name, userType, isGroup)
if err != nil && err != errNoSuchPolicy {
@@ -2002,7 +2189,7 @@ func (sys *IAMSys) policyDBGet(name string, isGroup bool) (policies []string, er
// IsAllowedServiceAccount - checks if the given service account is allowed to perform
// actions. The permission of the parent user is checked first
func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parent string) bool {
func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parentUser string) bool {
// Now check if we have a subject claim
p, ok := args.Claims[parentClaim]
if ok {
@@ -2013,7 +2200,7 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parent string) b
}
// The parent claim in the session token should be equal
// to the parent detected in the backend
if parentInClaim != parent {
if parentInClaim != parentUser {
return false
}
} else {
@@ -2024,7 +2211,7 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parent string) b
}
// Check policy for this service account.
svcPolicies, err := sys.PolicyDBGet(parent, false, args.Groups...)
svcPolicies, err := sys.PolicyDBGet(parentUser, false, args.Groups...)
if err != nil {
logger.LogIf(GlobalContext, err)
return false
@@ -2057,7 +2244,10 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parent string) b
}
parentArgs := args
parentArgs.AccountName = parent
parentArgs.AccountName = parentUser
// These are dynamic values set them appropriately.
parentArgs.ConditionValues["username"] = []string{parentUser}
parentArgs.ConditionValues["userid"] = []string{parentUser}
saPolicyClaim, ok := args.Claims[iamPolicyClaimNameSA()]
if !ok {
@@ -2096,7 +2286,11 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parent string) b
return false
}
// Policy without Version string value reject it.
// This can only happen if policy was set but with an empty JSON.
if subPolicy.Version == "" && len(subPolicy.Statements) == 0 {
return combinedPolicy.IsAllowed(parentArgs)
}
if subPolicy.Version == "" {
return false
}
@@ -2106,20 +2300,15 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parent string) b
// IsAllowedLDAPSTS - checks for LDAP specific claims and values
func (sys *IAMSys) IsAllowedLDAPSTS(args iampolicy.Args, parentUser string) bool {
parentInClaimIface, ok := args.Claims[ldapUser]
if ok {
parentInClaim, ok := parentInClaimIface.(string)
if !ok {
// ldap parentInClaim name is not a string reject it.
return false
}
if parentInClaim != parentUser {
// ldap claim has been modified maliciously reject it.
return false
}
} else {
// no ldap parentInClaim claim present reject it.
// parentUser value must match the ldap user in the claim.
if parentInClaimIface, ok := args.Claims[ldapUser]; !ok {
// no ldapUser claim present reject it.
return false
} else if parentInClaim, ok := parentInClaimIface.(string); !ok {
// not the right type, reject it.
return false
} else if parentInClaim != parentUser {
// ldap claim has been modified maliciously reject it.
return false
}
@@ -2156,6 +2345,11 @@ func (sys *IAMSys) IsAllowedLDAPSTS(args iampolicy.Args, parentUser string) bool
availablePolicies[i].Statements...)
}
hasSessionPolicy, isAllowedSP := isAllowedBySessionPolicy(args)
if hasSessionPolicy {
return isAllowedSP && combinedPolicy.IsAllowed(args)
}
return combinedPolicy.IsAllowed(args)
}
@@ -2215,31 +2409,14 @@ func (sys *IAMSys) IsAllowedSTS(args iampolicy.Args, parentUser string) bool {
availablePolicies[i].Statements...)
}
// These are dynamic values set them appropriately.
args.ConditionValues["username"] = []string{parentUser}
args.ConditionValues["userid"] = []string{parentUser}
// Now check if we have a sessionPolicy.
spolicy, ok := args.Claims[iampolicy.SessionPolicyName]
if ok {
spolicyStr, ok := spolicy.(string)
if !ok {
// Sub policy if set, should be a string reject
// malformed/malicious requests.
return false
}
// Check if policy is parseable.
subPolicy, err := iampolicy.ParseConfig(bytes.NewReader([]byte(spolicyStr)))
if err != nil {
// Log any error in input session policy config.
logger.LogIf(GlobalContext, err)
return false
}
// Policy without Version string value reject it.
if subPolicy.Version == "" {
return false
}
// Sub policy is set and valid.
return combinedPolicy.IsAllowed(args) && subPolicy.IsAllowed(args)
hasSessionPolicy, isAllowedSP := isAllowedBySessionPolicy(args)
if hasSessionPolicy {
return isAllowedSP && combinedPolicy.IsAllowed(args)
}
// Sub policy not set, this is most common since subPolicy
@@ -2247,6 +2424,42 @@ func (sys *IAMSys) IsAllowedSTS(args iampolicy.Args, parentUser string) bool {
return combinedPolicy.IsAllowed(args)
}
func isAllowedBySessionPolicy(args iampolicy.Args) (hasSessionPolicy bool, isAllowed bool) {
hasSessionPolicy = false
isAllowed = false
// Now check if we have a sessionPolicy.
spolicy, ok := args.Claims[iampolicy.SessionPolicyName]
if !ok {
return
}
hasSessionPolicy = true
spolicyStr, ok := spolicy.(string)
if !ok {
// Sub policy if set, should be a string reject
// malformed/malicious requests.
return
}
// Check if policy is parseable.
subPolicy, err := iampolicy.ParseConfig(bytes.NewReader([]byte(spolicyStr)))
if err != nil {
// Log any error in input session policy config.
logger.LogIf(GlobalContext, err)
return
}
// Policy without Version string value reject it.
if subPolicy.Version == "" {
return
}
// Sub policy is set and valid.
return hasSessionPolicy, subPolicy.IsAllowed(args)
}
// GetCombinedPolicy returns a combined policy combining all policies
func (sys *IAMSys) GetCombinedPolicy(policies ...string) iampolicy.Policy {
// Policies were found, evaluate all of them.
+37 -25
View File
@@ -27,6 +27,7 @@ import (
"github.com/minio/minio/internal/auth"
xjwt "github.com/minio/minio/internal/jwt"
"github.com/minio/minio/internal/logger"
iampolicy "github.com/minio/pkg/iam/policy"
)
const (
@@ -97,29 +98,6 @@ func authenticateURL(accessKey, secretKey string) (string, error) {
return authenticateJWTUsers(accessKey, secretKey, defaultURLJWTExpiry)
}
// Callback function used for parsing
func webTokenCallback(claims *xjwt.MapClaims) ([]byte, error) {
if claims.AccessKey == globalActiveCred.AccessKey {
return []byte(globalActiveCred.SecretKey), nil
}
ok, _, err := globalIAMSys.IsTempUser(claims.AccessKey)
if err != nil {
if err == errNoSuchUser {
return nil, errInvalidAccessKeyID
}
return nil, err
}
if ok {
return []byte(globalActiveCred.SecretKey), nil
}
cred, ok := globalIAMSys.GetUser(claims.AccessKey)
if !ok {
return nil, errInvalidAccessKeyID
}
return []byte(cred.SecretKey), nil
}
// Check if the request is authenticated.
// Returns nil if the request is authenticated. errNoAuthToken if token missing.
// Returns errAuthentication for all other errors.
@@ -132,10 +110,44 @@ func webRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, bool, error) {
return nil, false, err
}
claims := xjwt.NewMapClaims()
if err := xjwt.ParseWithClaims(token, claims, webTokenCallback); err != nil {
if err := xjwt.ParseWithClaims(token, claims, func(claims *xjwt.MapClaims) ([]byte, error) {
if claims.AccessKey == globalActiveCred.AccessKey {
return []byte(globalActiveCred.SecretKey), nil
}
cred, ok := globalIAMSys.GetUser(claims.AccessKey)
if !ok {
return nil, errInvalidAccessKeyID
}
return []byte(cred.SecretKey), nil
}); err != nil {
return claims, false, errAuthentication
}
owner := claims.AccessKey == globalActiveCred.AccessKey
owner := true
if globalActiveCred.AccessKey != claims.AccessKey {
// Check if the access key is part of users credentials.
ucred, ok := globalIAMSys.GetUser(claims.AccessKey)
if !ok {
return nil, false, errInvalidAccessKeyID
}
// get embedded claims
eclaims, s3Err := checkClaimsFromToken(req, ucred)
if s3Err != ErrNone {
return nil, false, errAuthentication
}
for k, v := range eclaims {
claims.MapClaims[k] = v
}
// Now check if we have a sessionPolicy.
if _, ok = eclaims[iampolicy.SessionPolicyName]; ok {
owner = false
} else {
owner = globalActiveCred.AccessKey == ucred.ParentUser
}
}
return claims, owner, nil
}
+1 -1
View File
@@ -65,7 +65,7 @@ func (api objectAPIHandlers) ListenNotificationHandler(w http.ResponseWriter, r
}
}
values := r.URL.Query()
values := r.Form
var prefix string
if len(values[peerRESTListenPrefix]) > 1 {
+5 -4
View File
@@ -63,15 +63,16 @@ func (l *lockRESTServer) IsValid(w http.ResponseWriter, r *http.Request) bool {
}
func getLockArgs(r *http.Request) (args dsync.LockArgs, err error) {
quorum, err := strconv.Atoi(r.URL.Query().Get(lockRESTQuorum))
values := r.Form
quorum, err := strconv.Atoi(values.Get(lockRESTQuorum))
if err != nil {
return args, err
}
args = dsync.LockArgs{
Owner: r.URL.Query().Get(lockRESTOwner),
UID: r.URL.Query().Get(lockRESTUID),
Source: r.URL.Query().Get(lockRESTSource),
Owner: values.Get(lockRESTOwner),
UID: values.Get(lockRESTUID),
Source: values.Get(lockRESTSource),
Quorum: quorum,
}
+33 -1
View File
@@ -37,6 +37,9 @@ type metaCacheEntry struct {
// cached contains the metadata if decoded.
cached *FileInfo
// Indicates the entry can be reused and only one reference to metadata is expected.
reusable bool
}
// isDir returns if the entry is representing a prefix directory.
@@ -345,6 +348,8 @@ type metaCacheEntriesSorted struct {
o metaCacheEntries
// list id is not serialized
listID string
// Reuse buffers
reuse bool
}
// shallowClone will create a shallow clone of the array objects,
@@ -490,6 +495,15 @@ func (m *metaCacheEntriesSorted) forwardTo(s string) {
idx := sort.Search(len(m.o), func(i int) bool {
return m.o[i].name >= s
})
if m.reuse {
for i, entry := range m.o[:idx] {
if len(entry.metadata) >= metaDataReadDefault && len(entry.metadata) < metaDataReadDefault*4 {
metaDataPool.Put(entry.metadata)
}
m.o[i].metadata = nil
}
}
m.o = m.o[idx:]
}
@@ -501,6 +515,14 @@ func (m *metaCacheEntriesSorted) forwardPast(s string) {
idx := sort.Search(len(m.o), func(i int) bool {
return m.o[i].name > s
})
if m.reuse {
for i, entry := range m.o[:idx] {
if len(entry.metadata) >= metaDataReadDefault && len(entry.metadata) < metaDataReadDefault*4 {
metaDataPool.Put(entry.metadata)
}
m.o[i].metadata = nil
}
}
m.o = m.o[idx:]
}
@@ -600,7 +622,9 @@ func mergeEntryChannels(ctx context.Context, in []chan metaCacheEntry, out chan<
out <- *best
last = best.name
} else {
console.Debugln("mergeEntryChannels: discarding duplicate", best.name, "<=", last)
if serverDebugLog {
console.Debugln("mergeEntryChannels: discarding duplicate", best.name, "<=", last)
}
}
// Replace entry we just sent.
if err := selectFrom(bestIdx); err != nil {
@@ -713,6 +737,14 @@ func (m *metaCacheEntriesSorted) truncate(n int) {
return
}
if len(m.o) > n {
if m.reuse {
for i, entry := range m.o[n:] {
if len(entry.metadata) >= metaDataReadDefault && len(entry.metadata) < metaDataReadDefault*4 {
metaDataPool.Put(entry.metadata)
}
m.o[n+i].metadata = nil
}
}
m.o = m.o[:n]
}
}
+30 -5
View File
@@ -97,6 +97,7 @@ func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) (
o.parseMarker()
o.BaseDir = baseDirFromPrefix(o.Prefix)
o.Transient = o.Transient || isReservedOrInvalidBucket(o.Bucket, false)
o.SetFilter()
if o.Transient {
o.Create = false
}
@@ -162,6 +163,7 @@ func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) (
if o.pool < len(z.serverPools) && o.set < len(z.serverPools[o.pool].sets) {
o.debugln("Resuming", o)
entries, err = z.serverPools[o.pool].sets[o.set].streamMetadataParts(ctx, *o)
entries.reuse = true // We read from stream and are not sharing results.
if err == nil {
return entries, nil
}
@@ -185,9 +187,11 @@ func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) (
rpc := globalNotificationSys.restClientFromHash(o.Bucket)
ctx, cancel := context.WithTimeout(GlobalContext, 5*time.Second)
defer cancel()
if c, err := rpc.GetMetacacheListing(ctx, *o); err == nil {
c, err := rpc.GetMetacacheListing(ctx, *o)
if err == nil {
c.error = "no longer used"
c.status = scanStateError
rpc.UpdateMetacacheListing(ctx, *c)
}
}()
o.ID = ""
@@ -198,8 +202,8 @@ func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) (
// Create filter for results.
o.debugln("Raw List", o)
filterCh := make(chan metaCacheEntry, o.Limit)
filteredResults := o.gatherResults(filterCh)
listCtx, cancelList := context.WithCancel(ctx)
filteredResults := o.gatherResults(listCtx, filterCh)
var wg sync.WaitGroup
wg.Add(1)
var listErr error
@@ -217,6 +221,7 @@ func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) (
if listErr != nil && !errors.Is(listErr, context.Canceled) {
return entries, listErr
}
entries.reuse = true
truncated := entries.len() > o.Limit || err == nil
entries.truncate(o.Limit)
if !o.Transient && truncated {
@@ -341,7 +346,7 @@ func (z *erasureServerPools) listAndSave(ctx context.Context, o *listPathOptions
inCh := make(chan metaCacheEntry, metacacheBlockSize)
outCh := make(chan metaCacheEntry, o.Limit)
filteredResults := o.gatherResults(outCh)
filteredResults := o.gatherResults(ctx, outCh)
mc := o.newMetacache()
meta := metaCacheRPC{meta: &mc, cancel: cancel, rpc: globalNotificationSys.restClientFromHash(o.Bucket), o: *o}
@@ -363,13 +368,33 @@ func (z *erasureServerPools) listAndSave(ctx context.Context, o *listPathOptions
o.debugln("listAndSave: listing", o.ID, "finished with ", err)
}(*o)
// Keep track of when we return since we no longer have to send entries to output.
var funcReturned bool
var funcReturnedMu sync.Mutex
defer func() {
funcReturnedMu.Lock()
funcReturned = true
funcReturnedMu.Unlock()
}()
// Write listing to results and saver.
go func() {
var returned bool
for entry := range inCh {
outCh <- entry
if !returned {
funcReturnedMu.Lock()
returned = funcReturned
funcReturnedMu.Unlock()
outCh <- entry
if returned {
close(outCh)
}
}
entry.reusable = returned
saveCh <- entry
}
close(outCh)
if !returned {
close(outCh)
}
close(saveCh)
}()
+118 -55
View File
@@ -131,20 +131,31 @@ func (o *listPathOptions) debugln(data ...interface{}) {
// gatherResults will collect all results on the input channel and filter results according to the options.
// Caller should close the channel when done.
// The returned function will return the results once there is enough or input is closed.
func (o *listPathOptions) gatherResults(in <-chan metaCacheEntry) func() (metaCacheEntriesSorted, error) {
// The returned function will return the results once there is enough or input is closed,
// or the context is canceled.
func (o *listPathOptions) gatherResults(ctx context.Context, in <-chan metaCacheEntry) func() (metaCacheEntriesSorted, error) {
var resultsDone = make(chan metaCacheEntriesSorted)
// Copy so we can mutate
resCh := resultsDone
var done bool
var mu sync.Mutex
resErr := io.EOF
go func() {
var results metaCacheEntriesSorted
var returned bool
for entry := range in {
if resCh == nil {
if returned {
// past limit
continue
}
mu.Lock()
returned = done
mu.Unlock()
if returned {
resCh = nil
continue
}
if !o.IncludeDirectories && entry.isDir() {
continue
}
@@ -167,6 +178,7 @@ func (o *listPathOptions) gatherResults(in <-chan metaCacheEntry) func() (metaCa
resErr = nil
resCh <- results
resCh = nil
returned = true
}
continue
}
@@ -178,7 +190,15 @@ func (o *listPathOptions) gatherResults(in <-chan metaCacheEntry) func() (metaCa
}
}()
return func() (metaCacheEntriesSorted, error) {
return <-resultsDone, resErr
select {
case <-ctx.Done():
mu.Lock()
done = true
mu.Unlock()
return metaCacheEntriesSorted{}, ctx.Err()
case r := <-resultsDone:
return r, resErr
}
}
}
@@ -351,18 +371,18 @@ func (er *erasureObjects) streamMetadataParts(ctx context.Context, o listPathOpt
// All operations are performed without locks, so we must be careful and allow for failures.
// Read metadata associated with the object from a disk.
if retries > 0 {
disks := er.getOnlineDisks()
if len(disks) == 0 {
time.Sleep(retryDelay)
retries++
continue
}
_, err := disks[0].ReadVersion(ctx, minioMetaBucket, o.objectPath(0), "", false)
if err != nil {
time.Sleep(retryDelay)
retries++
continue
for _, disk := range er.getDisks() {
if disk == nil {
continue
}
_, err := disk.ReadVersion(ctx, minioMetaBucket,
o.objectPath(0), "", false)
if err != nil {
time.Sleep(retryDelay)
retries++
continue
}
break
}
}
@@ -421,20 +441,21 @@ func (er *erasureObjects) streamMetadataParts(ctx context.Context, o listPathOpt
if retries > 0 {
// Load from one disk only
disks := er.getOnlineDisks()
if len(disks) == 0 {
time.Sleep(retryDelay)
retries++
continue
}
_, err := disks[0].ReadVersion(ctx, minioMetaBucket, o.objectPath(partN), "", false)
if err != nil {
time.Sleep(retryDelay)
retries++
continue
for _, disk := range er.getDisks() {
if disk == nil {
continue
}
_, err := disk.ReadVersion(ctx, minioMetaBucket,
o.objectPath(partN), "", false)
if err != nil {
time.Sleep(retryDelay)
retries++
continue
}
break
}
}
// Load first part metadata...
fi, metaArr, onlineDisks, err = er.getObjectFileInfo(ctx, minioMetaBucket, o.objectPath(partN), ObjectOptions{}, true)
if err != nil {
@@ -512,7 +533,8 @@ func (er *erasureObjects) listPath(ctx context.Context, o listPathOptions, resul
askDisks := o.AskDisks
listingQuorum := o.AskDisks - 1
disks := er.getOnlineDisks()
disks := er.getDisks()
var fallbackDisks []StorageAPI
// Special case: ask all disks if the drive count is 4
if askDisks == -1 || er.setDriveCount == 4 {
@@ -527,6 +549,7 @@ func (er *erasureObjects) listPath(ctx context.Context, o listPathOptions, resul
rand.Shuffle(len(disks), func(i, j int) {
disks[i], disks[j] = disks[j], disks[i]
})
fallbackDisks = disks[askDisks:]
disks = disks[:askDisks]
}
@@ -539,13 +562,14 @@ func (er *erasureObjects) listPath(ctx context.Context, o listPathOptions, resul
ctxDone := ctx.Done()
return listPathRaw(ctx, listPathRawOptions{
disks: disks,
bucket: o.Bucket,
path: o.BaseDir,
recursive: o.Recursive,
filterPrefix: o.FilterPrefix,
minDisks: listingQuorum,
forwardTo: o.Marker,
disks: disks,
fallbackDisks: fallbackDisks,
bucket: o.Bucket,
path: o.BaseDir,
recursive: o.Recursive,
filterPrefix: o.FilterPrefix,
minDisks: listingQuorum,
forwardTo: o.Marker,
agreed: func(entry metaCacheEntry) {
select {
case <-ctxDone:
@@ -644,10 +668,8 @@ func (er *erasureObjects) saveMetaCacheStream(ctx context.Context, mc *metaCache
r, err := hash.NewReader(bytes.NewReader(b.data), int64(len(b.data)), "", "", int64(len(b.data)))
logger.LogIf(ctx, err)
custom := b.headerKV()
_, err = er.putObject(ctx, minioMetaBucket, o.objectPath(b.n), NewPutObjReader(r), ObjectOptions{
UserDefined: custom,
NoLock: true, // No need to hold namespace lock, each prefix caches uniquely.
ParentIsObject: nil,
_, err = er.putMetacacheObject(ctx, o.objectPath(b.n), NewPutObjReader(r), ObjectOptions{
UserDefined: custom,
})
if err != nil {
mc.setErr(err.Error())
@@ -710,9 +732,10 @@ func (er *erasureObjects) saveMetaCacheStream(ctx context.Context, mc *metaCache
}
type listPathRawOptions struct {
disks []StorageAPI
bucket, path string
recursive bool
disks []StorageAPI
fallbackDisks []StorageAPI
bucket, path string
recursive bool
// Only return results with this prefix.
filterPrefix string
@@ -752,10 +775,18 @@ func listPathRaw(ctx context.Context, opts listPathRawOptions) (err error) {
if len(disks) == 0 {
return fmt.Errorf("listPathRaw: 0 drives provided")
}
// Cancel upstream if we finish before we expect.
ctx, cancel := context.WithCancel(ctx)
defer cancel()
fallback := func(err error) bool {
if err == nil {
return false
}
return err.Error() == errUnformattedDisk.Error() ||
err.Error() == errVolumeNotFound.Error()
}
askDisks := len(disks)
readers := make([]*metacacheReader, askDisks)
for i := range disks {
@@ -768,15 +799,44 @@ func listPathRaw(ctx context.Context, opts listPathRawOptions) (err error) {
// Send request to each disk.
go func() {
werr := d.WalkDir(ctx, WalkDirOptions{
Bucket: opts.bucket,
BaseDir: opts.path,
Recursive: opts.recursive,
ReportNotFound: opts.reportNotFound,
FilterPrefix: opts.filterPrefix,
ForwardTo: opts.forwardTo,
}, w)
var werr error
if d == nil {
werr = errDiskNotFound
} else {
werr = d.WalkDir(ctx, WalkDirOptions{
Bucket: opts.bucket,
BaseDir: opts.path,
Recursive: opts.recursive,
ReportNotFound: opts.reportNotFound,
FilterPrefix: opts.filterPrefix,
ForwardTo: opts.forwardTo,
}, w)
}
// fallback only when set.
if len(opts.fallbackDisks) > 0 && fallback(werr) {
// This fallback is only set when
// askDisks is less than total
// number of disks per set.
for _, fd := range opts.fallbackDisks {
if fd == nil {
continue
}
werr = fd.WalkDir(ctx, WalkDirOptions{
Bucket: opts.bucket,
BaseDir: opts.path,
Recursive: opts.recursive,
ReportNotFound: opts.reportNotFound,
FilterPrefix: opts.filterPrefix,
ForwardTo: opts.forwardTo,
}, w)
if werr == nil {
break
}
}
}
w.CloseWithError(werr)
if werr != io.EOF && werr != nil &&
werr.Error() != errFileNotFound.Error() &&
werr.Error() != errVolumeNotFound.Error() &&
@@ -795,10 +855,8 @@ func listPathRaw(ctx context.Context, opts listPathRawOptions) (err error) {
for i := range topEntries {
topEntries[i] = metaCacheEntry{}
}
select {
case <-ctx.Done():
if contextCanceled(ctx) {
return ctx.Err()
default:
}
for i, r := range readers {
if errs[i] != nil {
@@ -866,7 +924,12 @@ func listPathRaw(ctx context.Context, opts listPathRawOptions) (err error) {
var combinedErr []string
for i, err := range errs {
if err != nil {
combinedErr = append(combinedErr, fmt.Sprintf("disk %d returned: %s", i, err))
if disks[i] != nil {
combinedErr = append(combinedErr,
fmt.Sprintf("disk %s returned: %s", disks[i], err))
} else {
combinedErr = append(combinedErr, err.Error())
}
}
}
return errors.New(strings.Join(combinedErr, ", "))
+25 -7
View File
@@ -56,10 +56,11 @@ const metacacheStreamVersion = 2
// metacacheWriter provides a serializer of metacache objects.
type metacacheWriter struct {
mw *msgp.Writer
creator func() error
closer func() error
blockSize int
mw *msgp.Writer
creator func() error
closer func() error
blockSize int
reuseBlocks bool
streamErr error
streamWg sync.WaitGroup
@@ -141,6 +142,11 @@ func (w *metacacheWriter) write(objs ...metaCacheEntry) error {
if err != nil {
return err
}
if w.reuseBlocks || o.reusable {
if cap(o.metadata) >= metaDataReadDefault && cap(o.metadata) < metaDataReadDefault*4 {
metaDataPool.Put(o.metadata)
}
}
}
return nil
@@ -354,10 +360,14 @@ func (r *metacacheReader) next() (metaCacheEntry, error) {
r.err = err
return m, err
}
m.metadata, err = r.mr.ReadBytes(nil)
m.metadata, err = r.mr.ReadBytes(metaDataPool.Get().([]byte)[:0])
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
if len(m.metadata) == 0 && cap(m.metadata) >= metaDataReadDefault {
metaDataPool.Put(m.metadata)
m.metadata = nil
}
r.err = err
return m, err
}
@@ -510,13 +520,17 @@ func (r *metacacheReader) readN(n int, inclDeleted, inclDirs bool, prefix string
r.mr.R.Skip(1)
return metaCacheEntriesSorted{o: res}, io.EOF
}
if meta.metadata, err = r.mr.ReadBytes(nil); err != nil {
if meta.metadata, err = r.mr.ReadBytes(metaDataPool.Get().([]byte)[:0]); err != nil {
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
r.err = err
return metaCacheEntriesSorted{o: res}, err
}
if len(meta.metadata) == 0 && cap(meta.metadata) >= metaDataReadDefault {
metaDataPool.Put(meta.metadata)
meta.metadata = nil
}
if !inclDirs && meta.isDir() {
continue
}
@@ -565,13 +579,17 @@ func (r *metacacheReader) readAll(ctx context.Context, dst chan<- metaCacheEntry
r.err = err
return err
}
if meta.metadata, err = r.mr.ReadBytes(nil); err != nil {
if meta.metadata, err = r.mr.ReadBytes(metaDataPool.Get().([]byte)[:0]); err != nil {
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
r.err = err
return err
}
if len(meta.metadata) == 0 && cap(meta.metadata) >= metaDataReadDefault {
metaDataPool.Put(meta.metadata)
meta.metadata = nil
}
select {
case <-ctx.Done():
r.err = ctx.Err()
+28 -11
View File
@@ -22,7 +22,6 @@ import (
"io"
"net/http"
"net/url"
"os"
"sort"
"strconv"
"strings"
@@ -66,8 +65,7 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
}
// Stat a volume entry.
_, err = os.Lstat(volumeDir)
if err != nil {
if err = Access(volumeDir); err != nil {
if osIsNotExist(err) {
return errVolumeNotFound
} else if isSysErrIO(err) {
@@ -78,6 +76,7 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
// Use a small block size to start sending quickly
w := newMetacacheWriter(wr, 16<<10)
w.reuseBlocks = true // We are not sharing results, so reuse buffers.
defer w.Close()
out, err := w.stream()
if err != nil {
@@ -100,7 +99,8 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
metadata: metadata,
}
} else {
if st, err := Lstat(pathJoin(volumeDir, opts.BaseDir, xlStorageFormatFile)); err == nil && st.Mode().IsRegular() {
st, sterr := Lstat(pathJoin(volumeDir, opts.BaseDir, xlStorageFormatFile))
if sterr == nil && st.Mode().IsRegular() {
return errFileNotFound
}
}
@@ -118,11 +118,12 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
forward = forward[:idx]
}
}
if contextCanceled(ctx) {
return ctx.Err()
}
s.walkMu.Lock()
entries, err := s.ListDir(ctx, opts.Bucket, current, -1)
s.walkMu.Unlock()
if err != nil {
// Folder could have gone away in-between
if err != errVolumeNotFound && err != errFileNotFound {
@@ -134,12 +135,21 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
// Forward some errors?
return nil
}
if len(entries) == 0 {
return nil
}
dirObjects := make(map[string]struct{})
for i, entry := range entries {
if len(prefix) > 0 && !strings.HasPrefix(entry, prefix) {
// Do do not retain the file, since it doesn't
// match the prefix.
entries[i] = ""
continue
}
if len(forward) > 0 && entry < forward {
// Do do not retain the file, since its
// lexially smaller than 'forward'
entries[i] = ""
continue
}
if strings.HasSuffix(entry, slashSeparator) {
@@ -163,7 +173,9 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
// If root was an object return it as such.
if HasSuffix(entry, xlStorageFormatFile) {
var meta metaCacheEntry
s.walkMu.Lock()
meta.metadata, err = s.readMetadata(pathJoin(volumeDir, current, entry))
s.walkMu.Unlock()
if err != nil {
logger.LogIf(ctx, err)
continue
@@ -178,7 +190,9 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
// Check legacy.
if HasSuffix(entry, xlStorageFormatFileV1) {
var meta metaCacheEntry
s.walkMu.Lock()
meta.metadata, err = xioutil.ReadFile(pathJoin(volumeDir, current, entry))
s.walkMu.Unlock()
if err != nil {
logger.LogIf(ctx, err)
continue
@@ -195,7 +209,7 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
// Process in sort order.
sort.Strings(entries)
dirStack := make([]string, 0, 5)
prefix = "" // Remove prefix after first level.
prefix = "" // Remove prefix after first level as we have already filtered the list.
if len(forward) > 0 {
idx := sort.SearchStrings(entries, forward)
if idx > 0 {
@@ -234,7 +248,9 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
meta.name = meta.name[:len(meta.name)-1] + globalDirSuffixWithSlash
}
s.walkMu.Lock()
meta.metadata, err = s.readMetadata(pathJoin(volumeDir, meta.name, xlStorageFormatFile))
s.walkMu.Unlock()
switch {
case err == nil:
// It was an object
@@ -243,10 +259,10 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
}
out <- meta
case osIsNotExist(err):
s.walkMu.Lock()
meta.metadata, err = xioutil.ReadFile(pathJoin(volumeDir, meta.name, xlStorageFormatFileV1))
s.walkMu.Unlock()
if err == nil {
// Maybe rename? Would make it inconsistent across disks though.
// os.Rename(pathJoin(volumeDir, meta.name, xlStorageFormatFileV1), pathJoin(volumeDir, meta.name, xlStorageFormatFile))
// It was an object
out <- meta
continue
@@ -265,6 +281,7 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
logger.LogIf(ctx, err)
}
}
// If directory entry left on stack, pop it now.
for len(dirStack) > 0 {
pop := dirStack[len(dirStack)-1]
@@ -324,7 +341,7 @@ func (s *storageRESTServer) WalkDirHandler(w http.ResponseWriter, r *http.Reques
}
var reportNotFound bool
if v := vars[storageRESTReportNotFound]; v != "" {
if v := r.Form.Get(storageRESTReportNotFound); v != "" {
reportNotFound, err = strconv.ParseBool(v)
if err != nil {
s.writeErrorResponse(w, err)
@@ -332,8 +349,8 @@ func (s *storageRESTServer) WalkDirHandler(w http.ResponseWriter, r *http.Reques
}
}
prefix := r.URL.Query().Get(storageRESTPrefixFilter)
forward := r.URL.Query().Get(storageRESTForwardFilter)
prefix := r.Form.Get(storageRESTPrefixFilter)
forward := r.Form.Get(storageRESTForwardFilter)
writer := streamHTTPResponse(w)
writer.CloseWithError(s.storage.WalkDir(r.Context(), WalkDirOptions{
Bucket: volume,
+1 -1
View File
@@ -149,5 +149,5 @@ func (m *metacache) delete(ctx context.Context) {
logger.LogIf(ctx, errors.New("metacache.delete: expected objAPI to be *erasureServerPools"))
return
}
ez.deleteAll(ctx, minioMetaBucket, metacachePrefixForID(m.bucket, m.id))
ez.renameAll(ctx, minioMetaBucket, metacachePrefixForID(m.bucket, m.id))
}
+66
View File
@@ -68,6 +68,7 @@ const (
softwareSubsystem MetricSubsystem = "software"
sysCallSubsystem MetricSubsystem = "syscall"
usageSubsystem MetricSubsystem = "usage"
ilmSubsystem MetricSubsystem = "ilm"
)
// MetricName are the individual names for the metric.
@@ -121,6 +122,10 @@ const (
upTime = "uptime_seconds"
memory = "resident_memory_bytes"
cpu = "cpu_total_seconds"
expiryPendingTasks MetricName = "expiry_pending_tasks"
transitionPendingTasks MetricName = "transition_pending_tasks"
transitionActiveTasks MetricName = "transition_active_tasks"
)
const (
@@ -249,6 +254,7 @@ func GetGeneratorsForPeer() []MetricsGenerator {
getMinioVersionMetrics,
getNetworkMetrics,
getS3TTFBMetric,
getILMNodeMetrics,
}
return g
}
@@ -1000,6 +1006,66 @@ func getS3TTFBMetric() MetricsGroup {
}
}
func getTransitionPendingTasksMD() MetricDescription {
return MetricDescription{
Namespace: nodeMetricNamespace,
Subsystem: ilmSubsystem,
Name: transitionPendingTasks,
Help: "Number of pending ILM transition tasks in the queue.",
Type: gaugeMetric,
}
}
func getTransitionActiveTasksMD() MetricDescription {
return MetricDescription{
Namespace: nodeMetricNamespace,
Subsystem: ilmSubsystem,
Name: transitionActiveTasks,
Help: "Number of active ILM transition tasks.",
Type: gaugeMetric,
}
}
func getExpiryPendingTasksMD() MetricDescription {
return MetricDescription{
Namespace: nodeMetricNamespace,
Subsystem: ilmSubsystem,
Name: expiryPendingTasks,
Help: "Number of pending ILM expiry tasks in the queue.",
Type: gaugeMetric,
}
}
func getILMNodeMetrics() MetricsGroup {
return MetricsGroup{
id: "ILMNodeMetrics",
cachedRead: cachedRead,
read: func(_ context.Context) []Metric {
expPendingTasks := Metric{
Description: getExpiryPendingTasksMD(),
}
trPendingTasks := Metric{
Description: getTransitionPendingTasksMD(),
}
trActiveTasks := Metric{
Description: getTransitionActiveTasksMD(),
}
if globalExpiryState != nil {
expPendingTasks.Value = float64(globalExpiryState.PendingTasks())
}
if globalTransitionState != nil {
trPendingTasks.Value = float64(globalTransitionState.PendingTasks())
trActiveTasks.Value = float64(globalTransitionState.ActiveTasks())
}
return []Metric{
expPendingTasks,
trPendingTasks,
trActiveTasks,
}
},
}
}
func getMinioVersionMetrics() MetricsGroup {
return MetricsGroup{
id: "MinioVersionMetrics",
+248
View File
@@ -0,0 +1,248 @@
// Copyright (c) 2015-2021 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package cmd
import (
"context"
"sync"
"sync/atomic"
"time"
"github.com/minio/madmin-go"
"github.com/minio/minio/internal/logger"
)
var mrfHealingOpts = madmin.HealOpts{ScanMode: madmin.HealNormalScan, Remove: healDeleteDangling}
const (
mrfInfoResetInterval = 10 * time.Second
mrfOpsQueueSize = 10000
)
// partialOperation is a successful upload/delete of an object
// but not written in all disks (having quorum)
type partialOperation struct {
bucket string
object string
versionID string
size int64
setIndex int
poolIndex int
}
type setInfo struct {
index, pool int
}
// mrfState sncapsulates all the information
// related to the global background MRF.
type mrfState struct {
ready int32
ctx context.Context
objectAPI ObjectLayer
mu sync.Mutex
opCh chan partialOperation
pendingOps map[partialOperation]setInfo
setReconnectEvent chan setInfo
itemsHealed uint64
bytesHealed uint64
pendingItems uint64
pendingBytes uint64
triggeredAt time.Time
}
// Initialize healing MRF subsystem
func (m *mrfState) init(ctx context.Context, objAPI ObjectLayer) {
m.mu.Lock()
defer m.mu.Unlock()
m.ctx = ctx
m.objectAPI = objAPI
m.opCh = make(chan partialOperation, mrfOpsQueueSize)
m.pendingOps = make(map[partialOperation]setInfo)
m.setReconnectEvent = make(chan setInfo)
go globalMRFState.maintainMRFList()
go globalMRFState.healRoutine()
atomic.StoreInt32(&m.ready, 1)
}
func (m *mrfState) initialized() bool {
return atomic.LoadInt32(&m.ready) != 0
}
// Add a partial S3 operation (put/delete) when one or more disks are offline.
func (m *mrfState) addPartialOp(op partialOperation) {
if !m.initialized() {
return
}
select {
case m.opCh <- op:
default:
}
}
// Receive the new set (disk) reconnection event
func (m *mrfState) newSetReconnected(pool, set int) {
if !m.initialized() {
return
}
idler := time.NewTimer(100 * time.Millisecond)
defer idler.Stop()
select {
case m.setReconnectEvent <- setInfo{index: set, pool: pool}:
case <-idler.C:
}
}
// Get current MRF stats of the last MRF activity
func (m *mrfState) getCurrentMRFRoundInfo() madmin.MRFStatus {
m.mu.Lock()
triggeredAt := m.triggeredAt
itemsHealed := m.itemsHealed
bytesHealed := m.bytesHealed
pendingItems := m.pendingItems
pendingBytes := m.pendingBytes
m.mu.Unlock()
if pendingItems == 0 {
return madmin.MRFStatus{}
}
return madmin.MRFStatus{
Started: triggeredAt,
ItemsHealed: itemsHealed,
BytesHealed: bytesHealed,
TotalItems: itemsHealed + pendingItems,
TotalBytes: bytesHealed + pendingBytes,
}
}
// maintainMRFList gathers the list of successful partial uploads
// from all underlying er.sets and puts them in a global map which
// should not have more than 10000 entries.
func (m *mrfState) maintainMRFList() {
for fOp := range m.opCh {
m.mu.Lock()
if len(m.pendingOps) > mrfOpsQueueSize {
m.mu.Unlock()
continue
}
m.pendingOps[fOp] = setInfo{index: fOp.setIndex, pool: fOp.poolIndex}
m.pendingItems++
if fOp.size > 0 {
m.pendingBytes += uint64(fOp.size)
}
m.mu.Unlock()
}
}
// Reset current MRF stats
func (m *mrfState) resetMRFInfoIfNoPendingOps() {
m.mu.Lock()
defer m.mu.Unlock()
if m.pendingItems > 0 {
return
}
m.itemsHealed = 0
m.bytesHealed = 0
m.pendingItems = 0
m.pendingBytes = 0
m.triggeredAt = time.Time{}
}
// healRoutine listens to new disks reconnection events and
// issues healing requests for queued objects belonging to the
// corresponding erasure set
func (m *mrfState) healRoutine() {
idler := time.NewTimer(mrfInfoResetInterval)
defer idler.Stop()
for {
idler.Reset(mrfInfoResetInterval)
select {
case <-m.ctx.Done():
return
case <-idler.C:
m.resetMRFInfoIfNoPendingOps()
case setInfo := <-m.setReconnectEvent:
// Get the list of objects related the er.set
// to which the connected disk belongs.
var mrfOperations []partialOperation
m.mu.Lock()
for k, v := range m.pendingOps {
if v == setInfo {
mrfOperations = append(mrfOperations, k)
}
}
m.mu.Unlock()
if len(mrfOperations) == 0 {
continue
}
m.mu.Lock()
m.triggeredAt = time.Now().UTC()
m.mu.Unlock()
// Heal objects
for _, u := range mrfOperations {
waitForLowHTTPReq(globalHealConfig.IOCount, globalHealConfig.Sleep)
if _, err := m.objectAPI.HealObject(m.ctx, u.bucket, u.object, u.versionID, mrfHealingOpts); err != nil {
if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
// If not deleted, assume they failed.
logger.LogIf(m.ctx, err)
} else {
m.mu.Lock()
m.itemsHealed++
m.pendingItems--
m.mu.Unlock()
}
} else {
m.mu.Lock()
m.itemsHealed++
m.pendingItems--
m.bytesHealed += uint64(u.size)
m.pendingBytes -= uint64(u.size)
m.mu.Unlock()
}
m.mu.Lock()
delete(m.pendingOps, u)
m.mu.Unlock()
}
}
}
}
// Initialize healing MRF
func initHealMRF(ctx context.Context, obj ObjectLayer) {
globalMRFState.init(ctx, obj)
}
+7 -7
View File
@@ -218,13 +218,6 @@ func (d *naughtyDisk) CheckParts(ctx context.Context, volume string, path string
return d.disk.CheckParts(ctx, volume, path, fi)
}
func (d *naughtyDisk) CheckFile(ctx context.Context, volume string, path string) (err error) {
if err := d.calcError(); err != nil {
return err
}
return d.disk.CheckFile(ctx, volume, path)
}
func (d *naughtyDisk) Delete(ctx context.Context, volume string, path string, recursive bool) (err error) {
if err := d.calcError(); err != nil {
return err
@@ -291,3 +284,10 @@ func (d *naughtyDisk) VerifyFile(ctx context.Context, volume, path string, fi Fi
}
return d.disk.VerifyFile(ctx, volume, path, fi)
}
func (d *naughtyDisk) StatInfoFile(ctx context.Context, volume, path string) (stat StatInfo, err error) {
if err := d.calcError(); err != nil {
return stat, err
}
return d.disk.StatInfoFile(ctx, volume, path)
}
+27 -10
View File
@@ -22,6 +22,7 @@ import (
"fmt"
"net"
"net/url"
"runtime"
"sort"
"strings"
@@ -46,20 +47,30 @@ func mustSplitHostPort(hostPort string) (host, port string) {
// mustGetLocalIP4 returns IPv4 addresses of localhost. It panics on error.
func mustGetLocalIP4() (ipList set.StringSet) {
ipList = set.NewStringSet()
addrs, err := net.InterfaceAddrs()
ifs, err := net.Interfaces()
logger.FatalIf(err, "Unable to get IP addresses of this host")
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
for _, interf := range ifs {
addrs, err := interf.Addrs()
if err != nil {
continue
}
if runtime.GOOS == "windows" && interf.Flags&net.FlagUp == 0 {
continue
}
if ip.To4() != nil {
ipList.Add(ip.String())
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip.To4() != nil {
ipList.Add(ip.String())
}
}
}
@@ -157,6 +168,9 @@ func sortIPs(ipList []string) []string {
}
func getConsoleEndpoints() (consoleEndpoints []string) {
if globalBrowserRedirectURL != nil {
return []string{globalBrowserRedirectURL.String()}
}
var ipList []string
if globalMinioConsoleHost == "" {
ipList = sortIPs(mustGetLocalIP4().ToSlice())
@@ -174,6 +188,9 @@ func getConsoleEndpoints() (consoleEndpoints []string) {
}
func getAPIEndpoints() (apiEndpoints []string) {
if globalMinioEndpoint != "" {
return []string{globalMinioEndpoint}
}
var ipList []string
if globalMinioHost == "" {
ipList = sortIPs(mustGetLocalIP4().ToSlice())
+119 -32
View File
@@ -860,7 +860,7 @@ func (sys *NotificationSys) GetNetPerfInfo(ctx context.Context) madmin.NetPerfIn
}
}
return madmin.NetPerfInfo{
Addr: globalLocalNodeName,
NodeCommon: madmin.NodeCommon{Addr: globalLocalNodeName},
RemotePeers: netInfos,
}
}
@@ -935,7 +935,7 @@ func (sys *NotificationSys) GetParallelNetPerfInfo(ctx context.Context) madmin.N
}
wg.Wait()
return madmin.NetPerfInfo{
Addr: globalLocalNodeName,
NodeCommon: madmin.NodeCommon{Addr: globalLocalNodeName},
RemotePeers: netInfos,
}
}
@@ -995,12 +995,7 @@ func (sys *NotificationSys) GetCPUs(ctx context.Context) []madmin.CPUs {
for index, err := range g.Wait() {
if err != nil {
addr := sys.peerClients[index].host.String()
reqInfo := (&logger.ReqInfo{}).AppendTags("remotePeer", addr)
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
logger.LogIf(ctx, err)
reply[index].Addr = addr
reply[index].Error = err.Error()
sys.addNodeErr(&reply[index], sys.peerClients[index], err)
}
}
return reply
@@ -1025,12 +1020,7 @@ func (sys *NotificationSys) GetPartitions(ctx context.Context) []madmin.Partitio
for index, err := range g.Wait() {
if err != nil {
addr := sys.peerClients[index].host.String()
reqInfo := (&logger.ReqInfo{}).AppendTags("remotePeer", addr)
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
logger.LogIf(ctx, err)
reply[index].Addr = addr
reply[index].Error = err.Error()
sys.addNodeErr(&reply[index], sys.peerClients[index], err)
}
}
return reply
@@ -1055,12 +1045,67 @@ func (sys *NotificationSys) GetOSInfo(ctx context.Context) []madmin.OSInfo {
for index, err := range g.Wait() {
if err != nil {
addr := sys.peerClients[index].host.String()
reqInfo := (&logger.ReqInfo{}).AppendTags("remotePeer", addr)
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
logger.LogIf(ctx, err)
reply[index].Addr = addr
reply[index].Error = err.Error()
sys.addNodeErr(&reply[index], sys.peerClients[index], err)
}
}
return reply
}
// GetSysServices - Get information about system services
// (only the services that are of concern to minio)
func (sys *NotificationSys) GetSysServices(ctx context.Context) []madmin.SysServices {
reply := make([]madmin.SysServices, len(sys.peerClients))
g := errgroup.WithNErrs(len(sys.peerClients))
for index, client := range sys.peerClients {
if client == nil {
continue
}
index := index
g.Go(func() error {
var err error
reply[index], err = sys.peerClients[index].GetSELinuxInfo(ctx)
return err
}, index)
}
for index, err := range g.Wait() {
if err != nil {
sys.addNodeErr(&reply[index], sys.peerClients[index], err)
}
}
return reply
}
func (sys *NotificationSys) addNodeErr(nodeInfo madmin.NodeInfo, peerClient *peerRESTClient, err error) {
addr := peerClient.host.String()
reqInfo := (&logger.ReqInfo{}).AppendTags("remotePeer", addr)
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
logger.LogIf(ctx, err)
nodeInfo.SetAddr(addr)
nodeInfo.SetError(err.Error())
}
// GetSysErrors - Memory information
func (sys *NotificationSys) GetSysErrors(ctx context.Context) []madmin.SysErrors {
reply := make([]madmin.SysErrors, len(sys.peerClients))
g := errgroup.WithNErrs(len(sys.peerClients))
for index, client := range sys.peerClients {
if client == nil {
continue
}
index := index
g.Go(func() error {
var err error
reply[index], err = sys.peerClients[index].GetSysErrors(ctx)
return err
}, index)
}
for index, err := range g.Wait() {
if err != nil {
sys.addNodeErr(&reply[index], sys.peerClients[index], err)
}
}
return reply
@@ -1085,12 +1130,7 @@ func (sys *NotificationSys) GetMemInfo(ctx context.Context) []madmin.MemInfo {
for index, err := range g.Wait() {
if err != nil {
addr := sys.peerClients[index].host.String()
reqInfo := (&logger.ReqInfo{}).AppendTags("remotePeer", addr)
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
logger.LogIf(ctx, err)
reply[index].Addr = addr
reply[index].Error = err.Error()
sys.addNodeErr(&reply[index], sys.peerClients[index], err)
}
}
return reply
@@ -1115,12 +1155,7 @@ func (sys *NotificationSys) GetProcInfo(ctx context.Context) []madmin.ProcInfo {
for index, err := range g.Wait() {
if err != nil {
addr := sys.peerClients[index].host.String()
reqInfo := (&logger.ReqInfo{}).AppendTags("remotePeer", addr)
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
logger.LogIf(ctx, err)
reply[index].Addr = addr
reply[index].Error = err.Error()
sys.addNodeErr(&reply[index], sys.peerClients[index], err)
}
}
return reply
@@ -1427,3 +1462,55 @@ func (sys *NotificationSys) GetClusterMetrics(ctx context.Context) chan Metric {
}(&wg, ch)
return ch
}
// Speedtest run GET/PUT tests at input concurrency for requested object size,
// optionally you can extend the tests longer with time.Duration.
func (sys *NotificationSys) Speedtest(ctx context.Context, size int, concurrent int, duration time.Duration) []madmin.SpeedtestResult {
results := make([]madmin.SpeedtestResult, len(sys.allPeerClients))
scheme := "http"
if globalIsTLS {
scheme = "https"
}
var wg sync.WaitGroup
for index := range sys.peerClients {
if sys.peerClients[index] == nil {
continue
}
wg.Add(1)
go func(index int) {
defer wg.Done()
r, err := sys.peerClients[index].Speedtest(ctx, size, concurrent, duration)
u := &url.URL{
Scheme: scheme,
Host: sys.peerClients[index].host.String(),
}
results[index].Endpoint = u.String()
results[index].Err = err
if err == nil {
results[index].Uploads = r.Uploads
results[index].Downloads = r.Downloads
}
}(index)
}
wg.Add(1)
go func() {
defer wg.Done()
r, err := selfSpeedtest(ctx, size, concurrent, duration)
u := &url.URL{
Scheme: scheme,
Host: globalLocalNodeName,
}
results[len(results)-1].Endpoint = u.String()
results[len(results)-1].Err = err
if err == nil {
results[len(results)-1].Uploads = r.Uploads
results[len(results)-1].Downloads = r.Downloads
}
}()
wg.Wait()
return results
}
+15 -11
View File
@@ -113,16 +113,8 @@ type ObjectInfo struct {
// to a delete marker on an object.
DeleteMarker bool
// tierFreeVersion is true if this is a free-version
tierFreeVersion bool
// TransitionStatus indicates if transition is complete/pending
TransitionStatus string
// Name of transitioned object on remote tier
transitionedObjName string
// VersionID on the the remote tier
transitionVersionID string
// Name of remote tier object has transitioned to
TransitionTier string
// Transitioned object information
TransitionedObject TransitionedObject
// RestoreExpires indicates date a restored object expires
RestoreExpires time.Time
@@ -173,6 +165,8 @@ type ObjectInfo struct {
Legacy bool // indicates object on disk is in legacy data format
Multipart bool // indicates if object is multipart object.
// backendType indicates which backend filled this structure
backendType BackendType
@@ -193,11 +187,12 @@ func (o ObjectInfo) Clone() (cinfo ObjectInfo) {
Size: o.Size,
IsDir: o.IsDir,
ETag: o.ETag,
Multipart: o.Multipart,
InnerETag: o.InnerETag,
VersionID: o.VersionID,
IsLatest: o.IsLatest,
DeleteMarker: o.DeleteMarker,
TransitionStatus: o.TransitionStatus,
TransitionedObject: o.TransitionedObject,
RestoreExpires: o.RestoreExpires,
RestoreOngoing: o.RestoreOngoing,
ContentType: o.ContentType,
@@ -351,6 +346,15 @@ type ListMultipartsInfo struct {
EncodingType string // Not supported yet.
}
// TransitionedObject transitioned object tier and status.
type TransitionedObject struct {
Name string
VersionID string
Tier string
FreeVersion bool
Status string
}
// DeletedObjectInfo - container for list objects versions deleted objects.
type DeletedObjectInfo struct {
// Name of the bucket.
-17
View File
@@ -22,7 +22,6 @@ import (
"errors"
"fmt"
"io"
"path"
)
// Converts underlying storage error. Convenience function written to
@@ -64,15 +63,6 @@ func toObjectErr(err error, params ...string) error {
apiErr.Object = decodeDirObject(params[1])
}
return apiErr
case errFileParentIsFile.Error():
apiErr := ParentIsObject{}
if len(params) >= 1 {
apiErr.Bucket = params[0]
}
if len(params) >= 2 {
apiErr.Object = decodeDirObject(params[1])
}
return apiErr
case errIsNotRegular.Error():
apiErr := ObjectExistsAsDirectory{}
if len(params) >= 1 {
@@ -322,13 +312,6 @@ func (e PrefixAccessDenied) Error() string {
return "Prefix access is denied: " + e.Bucket + SlashSeparator + e.Object
}
// ParentIsObject object access is denied.
type ParentIsObject GenericError
func (e ParentIsObject) Error() string {
return "Parent is object " + e.Bucket + SlashSeparator + path.Dir(e.Object)
}
// BucketExists bucket exists.
type BucketExists GenericError
+3 -4
View File
@@ -53,10 +53,9 @@ type ObjectOptions struct {
VersionPurgeStatus VersionPurgeStatusType // Is only set in DELETE operations for delete marker version to be permanently deleted.
Transition TransitionOptions
NoLock bool // indicates to lower layers if the caller is expecting to hold locks.
ProxyRequest bool // only set for GET/HEAD in active-active replication scenario
ProxyHeaderSet bool // only set for GET/HEAD in active-active replication scenario
ParentIsObject func(ctx context.Context, bucket, parent string) bool // Used to verify if parent is an object.
NoLock bool // indicates to lower layers if the caller is expecting to hold locks.
ProxyRequest bool // only set for GET/HEAD in active-active replication scenario
ProxyHeaderSet bool // only set for GET/HEAD in active-active replication scenario
DeletePrefix bool // set true to enforce a prefix deletion, only application for DeleteObject API,
+25
View File
@@ -84,6 +84,10 @@ func testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
{testBuckets[4], "file1/guidSplunk-aaaa/file", "content", nil},
{testBuckets[5], "dir/day_id=2017-10-10/issue", "content", nil},
{testBuckets[5], "dir/day_id=2017-10-11/issue", "content", nil},
{testBuckets[5], "foo/201910/1122", "content", nil},
{testBuckets[5], "foo/201910/1112", "content", nil},
{testBuckets[5], "foo/201910/2112", "content", nil},
{testBuckets[5], "foo/201910_txt", "content", nil},
}
for _, object := range testObjects {
md5Bytes := md5.Sum([]byte(object.content))
@@ -477,6 +481,24 @@ func testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
IsTruncated: true,
Prefixes: []string{"dir/day_id=2017-10-10/"},
},
// ListObjectsResult-37 list with prefix match 2 levels deep
{
IsTruncated: false,
Objects: []ObjectInfo{
{Name: "foo/201910/1112"},
{Name: "foo/201910/1122"},
},
},
// ListObjectsResult-38 list with prefix match 1 level deep
{
IsTruncated: false,
Objects: []ObjectInfo{
{Name: "foo/201910/1112"},
{Name: "foo/201910/1122"},
{Name: "foo/201910/2112"},
{Name: "foo/201910_txt"},
},
},
}
testCases := []struct {
@@ -602,6 +624,9 @@ func testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
{testBuckets[4], "file1/", "", "guidSplunk", 1000, resultCases[35], nil, true},
// Test listing at prefix with expected prefix markers
{testBuckets[5], "dir/", "", SlashSeparator, 1, resultCases[36], nil, true},
// Test listing with prefix match
{testBuckets[5], "foo/201910/11", "", "", 1000, resultCases[37], nil, true},
{testBuckets[5], "foo/201910", "", "", 1000, resultCases[38], nil, true},
}
for i, testCase := range testCases {
+8 -3
View File
@@ -83,7 +83,7 @@ func getOpts(ctx context.Context, r *http.Request, bucket, object string) (Objec
var partNumber int
var err error
if pn := r.URL.Query().Get(xhttp.PartNumber); pn != "" {
if pn := r.Form.Get(xhttp.PartNumber); pn != "" {
partNumber, err = strconv.Atoi(pn)
if err != nil {
return opts, err
@@ -93,7 +93,7 @@ func getOpts(ctx context.Context, r *http.Request, bucket, object string) (Objec
}
}
vid := strings.TrimSpace(r.URL.Query().Get(xhttp.VersionID))
vid := strings.TrimSpace(r.Form.Get(xhttp.VersionID))
if vid != "" && vid != nullVersionID {
_, err := uuid.Parse(vid)
if err != nil {
@@ -219,7 +219,8 @@ func delOpts(ctx context.Context, r *http.Request, bucket, object string) (opts
// get ObjectOptions for PUT calls from encryption headers and metadata
func putOpts(ctx context.Context, r *http.Request, bucket, object string, metadata map[string]string) (opts ObjectOptions, err error) {
versioned := globalBucketVersioningSys.Enabled(bucket)
vid := strings.TrimSpace(r.URL.Query().Get(xhttp.VersionID))
versionSuspended := globalBucketVersioningSys.Suspended(bucket)
vid := strings.TrimSpace(r.Form.Get(xhttp.VersionID))
if vid != "" && vid != nullVersionID {
_, err := uuid.Parse(vid)
if err != nil {
@@ -266,6 +267,7 @@ func putOpts(ctx context.Context, r *http.Request, bucket, object string, metada
UserDefined: metadata,
VersionID: vid,
Versioned: versioned,
VersionSuspended: versionSuspended,
MTime: mtime,
}, nil
}
@@ -273,6 +275,7 @@ func putOpts(ctx context.Context, r *http.Request, bucket, object string, metada
opts, err = getOpts(ctx, r, bucket, object)
opts.VersionID = vid
opts.Versioned = versioned
opts.VersionSuspended = versionSuspended
opts.UserDefined = metadata
return
}
@@ -290,6 +293,7 @@ func putOpts(ctx context.Context, r *http.Request, bucket, object string, metada
UserDefined: metadata,
VersionID: vid,
Versioned: versioned,
VersionSuspended: versionSuspended,
MTime: mtime,
}, nil
}
@@ -300,6 +304,7 @@ func putOpts(ctx context.Context, r *http.Request, bucket, object string, metada
}
opts.VersionID = vid
opts.Versioned = versioned
opts.VersionSuspended = versionSuspended
opts.MTime = mtime
return opts, nil
}
+14 -2
View File
@@ -61,7 +61,9 @@ const (
// MinIO tmp meta prefix.
minioMetaTmpBucket = minioMetaBucket + "/tmp"
// MinIO tmp meta prefix for deleted objects.
minioMetaTmpDeletedBucket = minioMetaTmpBucket + "/.trash"
minioMetaTmpDeletedBucket = minioMetaTmpBucket + "/.trash"
minioMetaSpeedTestBucket = minioMetaBucket + "/speedtest"
minioMetaSpeedTestBucketPrefix = "objects/"
// DNS separator (period), used for bucket name validation.
dnsDelimiter = "."
@@ -882,6 +884,16 @@ func CleanMinioInternalMetadataKeys(metadata map[string]string) map[string]strin
return newMeta
}
// compressOpts are the options for writing compressed data.
var compressOpts []s2.WriterOption
func init() {
if runtime.GOARCH == "amd64" {
// On amd64 we have assembly and can use stronger compression.
compressOpts = append(compressOpts, s2.WriterBetterCompression())
}
}
// newS2CompressReader will read data from r, compress it and return the compressed data as a Reader.
// Use Close to ensure resources are released on incomplete streams.
//
@@ -892,7 +904,7 @@ func newS2CompressReader(r io.Reader, on int64) io.ReadCloser {
pr, pw := io.Pipe()
// Copy input to compressor
go func() {
comp := s2.NewWriter(pw)
comp := s2.NewWriter(pw, compressOpts...)
cn, err := io.Copy(comp, r)
if err != nil {
comp.Close()
+1 -15
View File
@@ -19,13 +19,11 @@ package cmd
import (
"context"
"fmt"
"net/http"
"regexp"
"strconv"
"time"
"github.com/minio/minio/internal/bucket/lifecycle"
xhttp "github.com/minio/minio/internal/http"
)
@@ -262,19 +260,7 @@ func setPutObjHeaders(w http.ResponseWriter, objInfo ObjectInfo, delete bool) {
if objInfo.Bucket != "" && objInfo.Name != "" {
if lc, err := globalLifecycleSys.Get(objInfo.Bucket); err == nil && !delete {
ruleID, expiryTime := lc.PredictExpiryTime(lifecycle.ObjectOpts{
Name: objInfo.Name,
UserTags: objInfo.UserTags,
VersionID: objInfo.VersionID,
ModTime: objInfo.ModTime,
IsLatest: objInfo.IsLatest,
DeleteMarker: objInfo.DeleteMarker,
})
if !expiryTime.IsZero() {
w.Header()[xhttp.AmzExpiration] = []string{
fmt.Sprintf(`expiry-date="%s", rule-id="%s"`, expiryTime.Format(http.TimeFormat), ruleID),
}
}
lc.SetPredictionHeaders(w, objInfo.ToLifecycleOpts())
}
}
}

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