Compare commits

...

6 Commits

Author SHA1 Message Date
Lionel Nicolas d5ccc5ecd8
Merge b9e138e220 into e909be6380 2025-07-23 11:06:27 +08:00
Poorna e909be6380 send replication requests to correct pool (#1162)
VulnCheck / Analysis (push) Has been cancelled Details
Fixes incorrect application of ilm expiry rules on versioned objects
when replication is enabled.

Regression from https://github.com/minio/minio/pull/20441 which sends
DeleteObject calls to all pools. This is a problem for replication + ilm
scenario since replicated version can end up in a pool by itself instead of
pool where remaining object versions reside.

For example, if the delete marker is set on pool1 and object versions exist on
pool2, the second rule below will cause the delete marker to be expired by ilm
policy since it is the single version present in pool1
```
{
  "Rules": [
   {
    "ID": "cs6il1ri2hp48g71mdjg",
    "NoncurrentVersionExpiration": {
     "NoncurrentDays": 14
    },
    "Status": "Enabled"
   },
   {
    "Expiration": {
     "ExpiredObjectDeleteMarker": true
    },
    "ID": "cs6inj3i2hp4po19cil0",
    "Status": "Enabled"
   }
  ]
}
```
2025-07-19 13:27:52 -07:00
jiuker 83b2ad418b
fix: restrict SinglePool by the minimum free drive threshold (#21115)
VulnCheck / Analysis (push) Waiting to run Details
2025-07-18 23:25:44 -07:00
Loganaden Velvindron 7a64bb9766
Add support for X25519MLKEM768 (#21435)
Signed-off-by: Bhuvanesh Fokeer <fokeerbhuvanesh@cyberstorm.mu>
Signed-off-by: Nakul Baboolall <nkb@cyberstorm.mu>
Signed-off-by: Sehun Bissessur <sehun.bissessur@cyberstorm.mu>
2025-07-18 23:23:15 -07:00
Minio Trusted 34679befef Update yaml files to latest version RELEASE.2025-07-18T21-56-31Z
VulnCheck / Analysis (push) Waiting to run Details
2025-07-18 23:28:59 +00:00
Harshavardhana 4021d8c8e2
fix: lambda handler response to match the lambda return status (#21436) 2025-07-18 14:56:31 -07:00
12 changed files with 241 additions and 109 deletions

View File

@ -28,7 +28,7 @@ import (
"github.com/minio/madmin-go/v3/logger/log"
"github.com/minio/minio/internal/logger"
"github.com/minio/minio/internal/logger/target/console"
"github.com/minio/minio/internal/logger/target/types"
types "github.com/minio/minio/internal/logger/target/loggertypes"
"github.com/minio/minio/internal/pubsub"
xnet "github.com/minio/pkg/v3/net"
)

View File

@ -635,15 +635,18 @@ func (z *erasureServerPools) getPoolIdxNoLock(ctx context.Context, bucket, objec
// if none are found falls back to most available space pool, this function is
// designed to be only used by PutObject, CopyObject (newObject creation) and NewMultipartUpload.
func (z *erasureServerPools) getPoolIdx(ctx context.Context, bucket, object string, size int64) (idx int, err error) {
idx, err = z.getPoolIdxExistingWithOpts(ctx, bucket, object, ObjectOptions{
pinfo, _, err := z.getPoolInfoExistingWithOpts(ctx, bucket, object, ObjectOptions{
SkipDecommissioned: true,
SkipRebalancing: true,
})
if err != nil && !isErrObjectNotFound(err) {
return idx, err
return -1, err
}
if isErrObjectNotFound(err) {
idx = pinfo.Index
if isErrObjectNotFound(err) || pinfo.Err == nil {
// will generate a temp object
idx = z.getAvailablePoolIdx(ctx, bucket, object, size)
if idx < 0 {
return -1, toObjectErr(errDiskFull)
@ -1089,6 +1092,10 @@ func (z *erasureServerPools) PutObject(ctx context.Context, bucket string, objec
object = encodeDirObject(object)
if z.SinglePool() {
_, err := z.getPoolIdx(ctx, bucket, object, data.Size())
if err != nil {
return ObjectInfo{}, err
}
return z.serverPools[0].PutObject(ctx, bucket, object, data, opts)
}
@ -1178,6 +1185,13 @@ func (z *erasureServerPools) DeleteObject(ctx context.Context, bucket string, ob
return z.deleteObjectFromAllPools(ctx, bucket, object, opts, noReadQuorumPools)
}
// All replication requests needs to go to pool with the object.
if opts.ReplicationRequest {
objInfo, err = z.serverPools[pinfo.Index].DeleteObject(ctx, bucket, object, opts)
objInfo.Name = decodeDirObject(object)
return objInfo, err
}
for _, pool := range z.serverPools {
objInfo, err := pool.DeleteObject(ctx, bucket, object, opts)
if err != nil && !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
@ -1816,6 +1830,10 @@ func (z *erasureServerPools) PutObjectPart(ctx context.Context, bucket, object,
}
if z.SinglePool() {
_, err := z.getPoolIdx(ctx, bucket, object, data.Size())
if err != nil {
return PartInfo{}, err
}
return z.serverPools[0].PutObjectPart(ctx, bucket, object, uploadID, partID, data, opts)
}

View File

@ -23,6 +23,8 @@ import (
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/klauspost/compress/gzhttp"
@ -39,7 +41,7 @@ import (
"github.com/minio/minio/internal/logger"
)
func getLambdaEventData(bucket, object string, cred auth.Credentials, r *http.Request) (levent.Event, error) {
var getLambdaEventData = func(bucket, object string, cred auth.Credentials, r *http.Request) (levent.Event, error) {
host := globalLocalNodeName
secure := globalIsTLS
if globalMinioEndpointURL != nil {
@ -100,80 +102,6 @@ func getLambdaEventData(bucket, object string, cred auth.Credentials, r *http.Re
return eventData, nil
}
var statusTextToCode = map[string]int{
"Continue": http.StatusContinue,
"Switching Protocols": http.StatusSwitchingProtocols,
"Processing": http.StatusProcessing,
"Early Hints": http.StatusEarlyHints,
"OK": http.StatusOK,
"Created": http.StatusCreated,
"Accepted": http.StatusAccepted,
"Non-Authoritative Information": http.StatusNonAuthoritativeInfo,
"No Content": http.StatusNoContent,
"Reset Content": http.StatusResetContent,
"Partial Content": http.StatusPartialContent,
"Multi-Status": http.StatusMultiStatus,
"Already Reported": http.StatusAlreadyReported,
"IM Used": http.StatusIMUsed,
"Multiple Choices": http.StatusMultipleChoices,
"Moved Permanently": http.StatusMovedPermanently,
"Found": http.StatusFound,
"See Other": http.StatusSeeOther,
"Not Modified": http.StatusNotModified,
"Use Proxy": http.StatusUseProxy,
"Temporary Redirect": http.StatusTemporaryRedirect,
"Permanent Redirect": http.StatusPermanentRedirect,
"Bad Request": http.StatusBadRequest,
"Unauthorized": http.StatusUnauthorized,
"Payment Required": http.StatusPaymentRequired,
"Forbidden": http.StatusForbidden,
"Not Found": http.StatusNotFound,
"Method Not Allowed": http.StatusMethodNotAllowed,
"Not Acceptable": http.StatusNotAcceptable,
"Proxy Authentication Required": http.StatusProxyAuthRequired,
"Request Timeout": http.StatusRequestTimeout,
"Conflict": http.StatusConflict,
"Gone": http.StatusGone,
"Length Required": http.StatusLengthRequired,
"Precondition Failed": http.StatusPreconditionFailed,
"Request Entity Too Large": http.StatusRequestEntityTooLarge,
"Request URI Too Long": http.StatusRequestURITooLong,
"Unsupported Media Type": http.StatusUnsupportedMediaType,
"Requested Range Not Satisfiable": http.StatusRequestedRangeNotSatisfiable,
"Expectation Failed": http.StatusExpectationFailed,
"I'm a teapot": http.StatusTeapot,
"Misdirected Request": http.StatusMisdirectedRequest,
"Unprocessable Entity": http.StatusUnprocessableEntity,
"Locked": http.StatusLocked,
"Failed Dependency": http.StatusFailedDependency,
"Too Early": http.StatusTooEarly,
"Upgrade Required": http.StatusUpgradeRequired,
"Precondition Required": http.StatusPreconditionRequired,
"Too Many Requests": http.StatusTooManyRequests,
"Request Header Fields Too Large": http.StatusRequestHeaderFieldsTooLarge,
"Unavailable For Legal Reasons": http.StatusUnavailableForLegalReasons,
"Internal Server Error": http.StatusInternalServerError,
"Not Implemented": http.StatusNotImplemented,
"Bad Gateway": http.StatusBadGateway,
"Service Unavailable": http.StatusServiceUnavailable,
"Gateway Timeout": http.StatusGatewayTimeout,
"HTTP Version Not Supported": http.StatusHTTPVersionNotSupported,
"Variant Also Negotiates": http.StatusVariantAlsoNegotiates,
"Insufficient Storage": http.StatusInsufficientStorage,
"Loop Detected": http.StatusLoopDetected,
"Not Extended": http.StatusNotExtended,
"Network Authentication Required": http.StatusNetworkAuthenticationRequired,
}
// StatusCode returns a HTTP Status code for the HTTP text. It returns -1
// if the text is unknown.
func StatusCode(text string) int {
if code, ok := statusTextToCode[text]; ok {
return code
}
return -1
}
func fwdHeadersToS3(h http.Header, w http.ResponseWriter) {
const trim = "x-amz-fwd-header-"
for k, v := range h {
@ -183,19 +111,26 @@ func fwdHeadersToS3(h http.Header, w http.ResponseWriter) {
}
}
func fwdStatusToAPIError(resp *http.Response) *APIError {
if status := resp.Header.Get(xhttp.AmzFwdStatus); status != "" && StatusCode(status) > -1 {
apiErr := &APIError{
HTTPStatusCode: StatusCode(status),
Description: resp.Header.Get(xhttp.AmzFwdErrorMessage),
Code: resp.Header.Get(xhttp.AmzFwdErrorCode),
}
if apiErr.HTTPStatusCode == http.StatusOK {
return nil
}
return apiErr
func fwdStatusToAPIError(statusCode int, resp *http.Response) *APIError {
if statusCode < http.StatusBadRequest {
return nil
}
desc := resp.Header.Get(xhttp.AmzFwdErrorMessage)
if strings.TrimSpace(desc) == "" {
apiErr := errorCodes.ToAPIErr(ErrInvalidRequest)
return &apiErr
}
code := resp.Header.Get(xhttp.AmzFwdErrorCode)
if strings.TrimSpace(code) == "" {
apiErr := errorCodes.ToAPIErr(ErrInvalidRequest)
apiErr.Description = desc
return &apiErr
}
return &APIError{
HTTPStatusCode: statusCode,
Description: desc,
Code: code,
}
return nil
}
// GetObjectLambdaHandler - GET Object with transformed data via lambda functions
@ -262,26 +197,31 @@ func (api objectAPIHandlers) GetObjectLambdaHandler(w http.ResponseWriter, r *ht
return
}
statusCode := resp.StatusCode
if status := resp.Header.Get(xhttp.AmzFwdStatus); status != "" {
statusCode, err = strconv.Atoi(status)
if err != nil {
writeErrorResponse(ctx, w, APIError{
Code: "LambdaFunctionStatusError",
HTTPStatusCode: http.StatusBadRequest,
Description: err.Error(),
}, r.URL)
return
}
}
// Set all the relevant lambda forward headers if found.
fwdHeadersToS3(resp.Header, w)
if apiErr := fwdStatusToAPIError(resp); apiErr != nil {
if apiErr := fwdStatusToAPIError(statusCode, resp); apiErr != nil {
writeErrorResponse(ctx, w, *apiErr, r.URL)
return
}
if resp.StatusCode != http.StatusOK {
writeErrorResponse(ctx, w, APIError{
Code: "LambdaFunctionError",
HTTPStatusCode: resp.StatusCode,
Description: "unexpected failure reported from lambda function",
}, r.URL)
return
}
if !globalAPIConfig.shouldGzipObjects() {
w.Header().Set(gzhttp.HeaderNoCompression, "true")
}
w.WriteHeader(statusCode)
io.Copy(w, resp.Body)
}

View File

@ -0,0 +1,174 @@
// Copyright (c) 2015-2025 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"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"testing"
"time"
"github.com/minio/minio-go/v7/pkg/signer"
"github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/config"
"github.com/minio/minio/internal/config/lambda"
levent "github.com/minio/minio/internal/config/lambda/event"
xhttp "github.com/minio/minio/internal/http"
)
func TestGetObjectLambdaHandler(t *testing.T) {
testCases := []struct {
name string
statusCode int
body string
contentType string
expectStatus int
}{
{
name: "Success 206 Partial Content",
statusCode: 206,
body: "partial-object-data",
contentType: "text/plain",
expectStatus: 206,
},
{
name: "Success 200 OK",
statusCode: 200,
body: "full-object-data",
contentType: "application/json",
expectStatus: 200,
},
{
name: "Client Error 400",
statusCode: 400,
body: "bad-request",
contentType: "application/xml",
expectStatus: 400,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
runObjectLambdaTest(t, tc.statusCode, tc.body, tc.contentType, tc.expectStatus)
})
}
}
func runObjectLambdaTest(t *testing.T, lambdaStatus int, lambdaBody, contentType string, expectStatus int) {
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{
t: t,
objAPITest: func(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler, credentials auth.Credentials, t *testing.T) {
objectName := "dummy-object"
functionID := "lambda1"
functionToken := "token123"
// Lambda mock server
lambdaServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(xhttp.AmzRequestRoute, functionID)
w.Header().Set(xhttp.AmzRequestToken, functionToken)
w.Header().Set(xhttp.AmzFwdHeaderContentType, contentType)
w.Header().Set(xhttp.AmzFwdStatus, strconv.Itoa(lambdaStatus))
w.WriteHeader(lambdaStatus)
w.Write([]byte(lambdaBody))
}))
defer lambdaServer.Close()
lambdaARN := "arn:minio:s3-object-lambda::lambda1:webhook"
cfg := config.New()
cfg[config.LambdaWebhookSubSys] = map[string]config.KVS{
functionID: {
{Key: "endpoint", Value: lambdaServer.URL},
{Key: "enable", Value: config.EnableOn},
},
}
cfg[config.APISubSys] = map[string]config.KVS{
"api": {
{Key: "gzip", Value: config.EnableOff},
},
}
var err error
globalLambdaTargetList, err = lambda.FetchEnabledTargets(context.Background(), cfg, http.DefaultTransport.(*http.Transport))
if err != nil {
t.Fatalf("failed to load lambda targets: %v", err)
}
getLambdaEventData = func(_, _ string, _ auth.Credentials, _ *http.Request) (levent.Event, error) {
return levent.Event{
GetObjectContext: &levent.GetObjectContext{
OutputRoute: functionID,
OutputToken: functionToken,
InputS3URL: "http://localhost/dummy",
},
UserRequest: levent.UserRequest{
Headers: map[string][]string{},
},
UserIdentity: levent.Identity{
PrincipalID: "test-user",
},
}, nil
}
body := []byte{}
req := httptest.NewRequest("GET", "/objectlambda/"+bucketName+"/"+objectName+"?lambdaArn="+url.QueryEscape(lambdaARN), bytes.NewReader(body))
req.Form = url.Values{"lambdaArn": []string{lambdaARN}}
req.Header.Set("Host", "localhost")
req.Header.Set("X-Amz-Date", time.Now().UTC().Format("20060102T150405Z"))
sum := sha256.Sum256(body)
req.Header.Set("X-Amz-Content-Sha256", hex.EncodeToString(sum[:]))
req = signer.SignV4(*req, credentials.AccessKey, credentials.SecretKey, "", "us-east-1")
rec := httptest.NewRecorder()
api := objectAPIHandlers{
ObjectAPI: func() ObjectLayer {
return obj
},
}
api.GetObjectLambdaHandler(rec, req)
res := rec.Result()
defer res.Body.Close()
respBody, _ := io.ReadAll(res.Body)
if res.StatusCode != expectStatus {
t.Errorf("Expected status %d, got %d", expectStatus, res.StatusCode)
}
if contentType != "" {
if ct := res.Header.Get("Content-Type"); ct != contentType {
t.Errorf("Expected Content-Type %q, got %q", contentType, ct)
}
}
if res.StatusCode < 400 {
if string(respBody) != lambdaBody {
t.Errorf("Expected body %q, got %q", lambdaBody, string(respBody))
}
}
},
endpoints: []string{"GetObject"},
})
}

View File

@ -2,7 +2,7 @@ version: '3.7'
# Settings and configurations that are common for all containers
x-minio-common: &minio-common
image: quay.io/minio/minio:RELEASE.2025-06-13T11-33-47Z
image: quay.io/minio/minio:RELEASE.2025-07-18T21-56-31Z
command: server --console-address ":9001" http://minio{1...4}/data{1...2}
expose:
- "9000"

View File

@ -74,5 +74,5 @@ func TLSCiphersBackwardCompatible() []uint16 {
// TLSCurveIDs returns a list of supported elliptic curve IDs
// in preference order.
func TLSCurveIDs() []tls.CurveID {
return []tls.CurveID{tls.CurveP256, tls.X25519, tls.CurveP384, tls.CurveP521}
return []tls.CurveID{tls.X25519MLKEM768, tls.CurveP256, tls.X25519, tls.CurveP384, tls.CurveP521}
}

View File

@ -35,7 +35,7 @@ import (
jsoniter "github.com/json-iterator/go"
xhttp "github.com/minio/minio/internal/http"
xioutil "github.com/minio/minio/internal/ioutil"
"github.com/minio/minio/internal/logger/target/types"
types "github.com/minio/minio/internal/logger/target/loggertypes"
"github.com/minio/minio/internal/once"
"github.com/minio/minio/internal/store"
xnet "github.com/minio/pkg/v3/net"

View File

@ -35,7 +35,7 @@ import (
saramatls "github.com/IBM/sarama/tools/tls"
xioutil "github.com/minio/minio/internal/ioutil"
"github.com/minio/minio/internal/logger/target/types"
types "github.com/minio/minio/internal/logger/target/loggertypes"
"github.com/minio/minio/internal/once"
"github.com/minio/minio/internal/store"
xnet "github.com/minio/pkg/v3/net"

View File

@ -1,6 +1,6 @@
// Code generated by "stringer -type=TargetType -trimprefix=Target types.go"; DO NOT EDIT.
package types
package loggertypes
import "strconv"

View File

@ -15,7 +15,7 @@
// 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 types
package loggertypes
// TargetType indicates type of the target e.g. console, http, kafka
type TargetType uint8

View File

@ -36,7 +36,7 @@ import (
"github.com/minio/madmin-go/v3/logger/log"
"github.com/minio/minio/internal/logger"
"github.com/minio/minio/internal/logger/target/types"
types "github.com/minio/minio/internal/logger/target/loggertypes"
)
const (

View File

@ -25,7 +25,7 @@ import (
"github.com/minio/minio/internal/logger/target/http"
"github.com/minio/minio/internal/logger/target/kafka"
"github.com/minio/minio/internal/logger/target/types"
types "github.com/minio/minio/internal/logger/target/loggertypes"
)
// Target is the entity that we will receive