From 116e72f0244918fe9eeaa74620423649de08005f Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Tue, 10 Feb 2026 16:40:55 +0530 Subject: [PATCH 001/128] Amazon s3mounter code --- cos-csi-mounter/server/s3mounter.go | 73 ++++++++++ cos-csi-mounter/server/server.go | 2 +- cos-csi-mounter/server/utils.go | 10 ++ pkg/constants/constants.go | 2 + pkg/mounter/mounter.go | 2 + pkg/mounter/mounter_s3mounter.go | 207 ++++++++++++++++++++++++++++ 6 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 cos-csi-mounter/server/s3mounter.go create mode 100644 pkg/mounter/mounter_s3mounter.go diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go new file mode 100644 index 00000000..5553283b --- /dev/null +++ b/cos-csi-mounter/server/s3mounter.go @@ -0,0 +1,73 @@ +package main + +import ( + "encoding/json" + "fmt" + + "go.uber.org/zap" +) + +type s3MounterArgs struct { + ReadOnly string `json:"read-only,omitempty"` + AllowOther string `json:"allow-other,omitempty"` + UID string `json:"uid,omitempty"` + GID string `json:"gid,omitempty"` + UMask string `json:"umask,omitempty"` + LogLevel string `json:"log-level,omitempty"` +} + +func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string, error) { + // Marshal to JSON + raw, err := json.Marshal(args) + if err != nil { + return nil, err + } + + // Unmarshal into map[string]string + var m map[string]string + if err := json.Unmarshal(raw, &m); err != nil { + return nil, err + } + + // Convert to key=value slice + result := []string{"mount", bucket, targetPath} + for k, v := range m { + result = append(result, fmt.Sprintf("--%s=%v", k, v)) // --key=value + } + + return result, nil // [mount, bucket, path, --key1=value1, --key2=value2, ...] +} + +func (args s3MounterArgs) Validate(targetPath string) error { + if err := pathValidator(targetPath); err != nil { + return err + } + + // Check if value of allow-other parameter is boolean "true" or "false" + if args.AllowOther != "" { + if isBool := isBoolString(args.AllowOther); !isBool { + logger.Error("cannot convert value of allow-other into boolean", zap.Any("allow-other", args.AllowOther)) + return fmt.Errorf("cannot convert value of allow-other into boolean: %v", args.AllowOther) + } + } + + // // Check if rclone config file exists or not + // if exists, err := FileExists(args.ConfigPath); err != nil { + // logger.Error("error checking rclone config file existence") + // return fmt.Errorf("error checking rclone config file existence") + // } else if !exists { + // logger.Error("rclone config file not found") + // return fmt.Errorf("rclone config file not found") + // } + + + // Check if value of read-only parameter is boolean "true" or "false" + if args.ReadOnly != "" { + if isBool := isBoolString(args.ReadOnly); !isBool { + logger.Error("cannot convert value of read-only into boolean", zap.Any("read-only", args.ReadOnly)) + return fmt.Errorf("cannot convert value of read-only into boolean: %v", args.ReadOnly) + } + } + + return nil +} diff --git a/cos-csi-mounter/server/server.go b/cos-csi-mounter/server/server.go index 3c690dbe..05643607 100644 --- a/cos-csi-mounter/server/server.go +++ b/cos-csi-mounter/server/server.go @@ -169,7 +169,7 @@ func handleCosMount(mounter mounterUtils.MounterUtils, parser MounterArgsParser) logger.Info("New mount request with values:", zap.String("Bucket", request.Bucket), zap.String("Path", request.Path), zap.String("Mounter", request.Mounter), zap.Any("Args", request.Args)) - if request.Mounter != constants.S3FS && request.Mounter != constants.RClone { + if request.Mounter != constants.S3FS && request.Mounter != constants.RClone && request.Mounter != constants.S3MOUNTER { logger.Error("invalid mounter", zap.Any("mounter", request.Mounter)) c.JSON(http.StatusBadRequest, gin.H{"error": "invalid mounter"}) return diff --git a/cos-csi-mounter/server/utils.go b/cos-csi-mounter/server/utils.go index e2858b11..877dba35 100644 --- a/cos-csi-mounter/server/utils.go +++ b/cos-csi-mounter/server/utils.go @@ -85,6 +85,16 @@ func (req *MountRequest) ParseMounterArgs() ([]string, error) { return nil, fmt.Errorf("rclone args validation failed: %w", err) } return args.PopulateArgsSlice(req.Bucket, req.Path) + + case constants.S3MOUNTER: + var args s3MounterArgs + if err := strictDecodeForUnknownFields(req.Args, &args); err != nil { + return nil, fmt.Errorf("invalid rclone args decode error: %w", err) + } + if err := args.Validate(req.Path); err != nil { + return nil, fmt.Errorf("s3Mounter args validation failed: %w", err) + } + return args.PopulateArgsSlice(req.Bucket, req.Path) default: return nil, fmt.Errorf("unknown mounter: %s", req.Mounter) diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 054f035c..f458a2c5 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -16,6 +16,8 @@ const ( S3FS = "s3fs" RClone = "rclone" + S3MOUNTER = "s3mounter" + DefaultNamespace = "default" IAMEP = "https://private.iam.cloud.ibm.com/identity/token" diff --git a/pkg/mounter/mounter.go b/pkg/mounter/mounter.go index 47337fdc..ce15d2b9 100644 --- a/pkg/mounter/mounter.go +++ b/pkg/mounter/mounter.go @@ -104,6 +104,8 @@ func (s *CSIMounterFactory) NewMounter(params MounterParams) Mounter { Gid: params.Gid, ReadOnly: params.ReadOnly, }) + case constants.S3MOUNTER: + return NewMountpointS3Mounter(secretMap, mountFlags, mounterUtils) default: // default to s3fs return NewS3fsMounter(S3fsMounterParams{ diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go new file mode 100644 index 00000000..b48cce26 --- /dev/null +++ b/pkg/mounter/mounter_s3mounter.go @@ -0,0 +1,207 @@ +// pkg/mounter/mounter-mountpoint-s3.go +package mounter + +import ( + "encoding/json" + "fmt" + "strings" + "github.com/IBM/ibm-object-csi-driver/pkg/constants" + "github.com/IBM/ibm-object-csi-driver/pkg/mounter/utils" + "k8s.io/klog/v2" +) + +// type MountpointS3Mounter struct { +// Bucket string +// Endpoint string +// Region string +// Prefix string +// ReadOnly bool +// UID string +// GID string +// MounterUtils utils.MounterUtils +// } + +type MountpointS3Mounter struct { + BucketName string // from secret / SC + ObjPath string // optional prefix + Region string + Endpoint string + ReadOnly bool + UID string + GID string + MountOptions []string + + MounterUtils utils.MounterUtils +} +// const ( +// passFile = ".passwd-s3fs" // #nosec G101: not password +// maxRetries = 3 +// ) + +// var ( +// writePassWrap = writePass +// removeFile = removeS3FSCredFile +// ) + + +// func NewMountpointS3Mounter( +// secretMap map[string]string, +// mountFlags []string, +// mounterUtils utils.MounterUtils, +// ) Mounter { + +// return &MountpointS3Mounter{ +// Bucket: secretMap["bucketName"], +// Endpoint: secretMap["endpoint"], +// Region: secretMap["region"], +// Prefix: secretMap["prefix"], +// UID: secretMap["uid"], +// GID: secretMap["gid"], +// MounterUtils: mounterUtils, +// } +// } +// func (m *MountpointS3Mounter) Mount(source, target string) error { +// klog.Infof("MountpointS3 Mount target=%s", target) + +// args := map[string]string{ +// "endpoint": m.Endpoint, +// "region": m.Region, +// "uid": m.UID, +// "gid": m.GID, +// } + +// if m.Prefix != "" { +// args["prefix"] = m.Prefix +// } + +// payloadMap := map[string]interface{}{ +// "path": target, +// "bucket": m.Bucket, +// "mounter": constants.MountpointS3, +// "args": args, +// } + +// payload, _ := json.Marshal(payloadMap) + +// return mounterRequest( +// string(payload), +// "http://unix/api/cos/mount", +// ) +// } +// func (m *MountpointS3Mounter) Unmount(target string) error { +// klog.Infof("MountpointS3 Unmount target=%s", target) + +// payload := fmt.Sprintf(`{"path":"%s"}`, target) + +// return mounterRequest( +// payload, +// "http://unix/api/cos/unmount", +// ) +// } + +func NewMountpointS3Mounter( + secretMap map[string]string, + mountOptions []string, + mounterUtils utils.MounterUtils, +) Mounter { + + m := &MountpointS3Mounter{} + + if v, ok := secretMap["bucketName"]; ok { + m.BucketName = v + } + if v, ok := secretMap["objPath"]; ok { + m.ObjPath = v + } + if v, ok := secretMap["region"]; ok { + m.Region = v + } + if v, ok := secretMap["endpoint"]; ok { + m.Endpoint = v + } + if v, ok := secretMap["uid"]; ok { + m.UID = v + } + if v, ok := secretMap["gid"]; ok { + m.GID = v + } + if v, ok := secretMap["readOnly"]; ok && v == "true" { + m.ReadOnly = true + } + + m.MountOptions = mountOptions + m.MounterUtils = mounterUtils + + klog.Infof( + "newMountpointS3Mounter bucket=%s prefix=%s region=%s endpoint=%s readonly=%v", + m.BucketName, m.ObjPath, m.Region, m.Endpoint, m.ReadOnly, + ) + + return m +} +func (m *MountpointS3Mounter) Mount(source, target string) error { + klog.Info("-MountpointS3 Mount-") + klog.Infof("Mount target: %s", target) + + bucket := m.BucketName + if m.ObjPath != "" { + bucket = fmt.Sprintf("%s:%s", bucket, strings.TrimPrefix(m.ObjPath, "/")) + } + + // Arguments sent to mounter daemon + args := map[string]string{ + "region": m.Region, + } + + if m.Endpoint != "" { + args["endpoint-url"] = m.Endpoint + } + if m.ReadOnly { + args["read-only"] = "true" + } + if m.UID != "" { + args["uid"] = m.UID + } + if m.GID != "" { + args["gid"] = m.GID + } + + for _, opt := range m.MountOptions { + parts := strings.SplitN(opt, "=", 2) + if len(parts) == 2 { + args[parts[0]] = parts[1] + } else { + args[parts[0]] = "true" + } + } + + payload := map[string]interface{}{ + "path": target, + "bucket": bucket, + "mounter": constants.MountpointS3, + "args": args, + } + + data, err := json.Marshal(payload) + if err != nil { + return err + } + + klog.Infof("Worker Mounting Payload: %s", string(data)) + + return mounterRequest( + string(data), + "http://unix/api/cos/mount", + ) +} +func (m *MountpointS3Mounter) Unmount(target string) error { + klog.Info("-MountpointS3 Unmount-") + klog.Infof("Unmount target: %s", target) + + payload := fmt.Sprintf(`{"path":"%s"}`, target) + + return mounterRequest( + payload, + "http://unix/api/cos/unmount", + ) +} From 4fb847567eadf6ef4c2588bf889904d702c150cf Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 16 Feb 2026 00:47:06 +0530 Subject: [PATCH 002/128] Updated mounter-s3 code --- pkg/mounter/mounter_s3mounter.go | 554 +++++++++++++++++++++---------- 1 file changed, 385 insertions(+), 169 deletions(-) diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index b48cce26..d32a36ee 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -1,207 +1,423 @@ -// pkg/mounter/mounter-mountpoint-s3.go +// Package mounter package mounter import ( + "bufio" + "crypto/sha256" "encoding/json" "fmt" + "os" + "path" "strings" + "time" + "github.com/IBM/ibm-object-csi-driver/pkg/constants" "github.com/IBM/ibm-object-csi-driver/pkg/mounter/utils" "k8s.io/klog/v2" ) -// type MountpointS3Mounter struct { -// Bucket string -// Endpoint string -// Region string -// Prefix string -// ReadOnly bool -// UID string -// GID string -// MounterUtils utils.MounterUtils -// } - +// MountpointS3Mounter implements Mounter for AWS Mountpoint S3 type MountpointS3Mounter struct { - BucketName string // from secret / SC - ObjPath string // optional prefix - Region string - Endpoint string - ReadOnly bool - UID string - GID string - MountOptions []string - - MounterUtils utils.MounterUtils + BucketName string + ObjectPath string + EndPoint string + LocConstraint string + AuthType string + AccessKey string + SecretKey string + UID string + GID string + UMask string + LogLevel string + ReadOnly bool + MountOptions []string + MounterUtils utils.MounterUtils +} + +// s3MounterArgs holds the args passed to the worker for mount-s3 +type s3MounterArgs struct { + ReadOnly string `json:"read-only,omitempty"` + AllowOther string `json:"allow-other,omitempty"` + UID string `json:"uid,omitempty"` + GID string `json:"gid,omitempty"` + UMask string `json:"umask,omitempty"` + LogLevel string `json:"log-level,omitempty"` + // Config file path for AWS credentials + AwsConfigDir string `json:"aws-config-dir,omitempty"` + EndpointURL string `json:"endpoint-url,omitempty"` + Region string `json:"region,omitempty"` } -// const ( -// passFile = ".passwd-s3fs" // #nosec G101: not password -// maxRetries = 3 -// ) - -// var ( -// writePassWrap = writePass -// removeFile = removeS3FSCredFile -// ) - - -// func NewMountpointS3Mounter( -// secretMap map[string]string, -// mountFlags []string, -// mounterUtils utils.MounterUtils, -// ) Mounter { - -// return &MountpointS3Mounter{ -// Bucket: secretMap["bucketName"], -// Endpoint: secretMap["endpoint"], -// Region: secretMap["region"], -// Prefix: secretMap["prefix"], -// UID: secretMap["uid"], -// GID: secretMap["gid"], -// MounterUtils: mounterUtils, -// } -// } -// func (m *MountpointS3Mounter) Mount(source, target string) error { -// klog.Infof("MountpointS3 Mount target=%s", target) - -// args := map[string]string{ -// "endpoint": m.Endpoint, -// "region": m.Region, -// "uid": m.UID, -// "gid": m.GID, -// } - -// if m.Prefix != "" { -// args["prefix"] = m.Prefix -// } - -// payloadMap := map[string]interface{}{ -// "path": target, -// "bucket": m.Bucket, -// "mounter": constants.MountpointS3, -// "args": args, -// } - -// payload, _ := json.Marshal(payloadMap) - -// return mounterRequest( -// string(payload), -// "http://unix/api/cos/mount", -// ) -// } -// func (m *MountpointS3Mounter) Unmount(target string) error { -// klog.Infof("MountpointS3 Unmount target=%s", target) - -// payload := fmt.Sprintf(`{"path":"%s"}`, target) - -// return mounterRequest( -// payload, -// "http://unix/api/cos/unmount", -// ) -// } - -func NewMountpointS3Mounter( - secretMap map[string]string, - mountOptions []string, - mounterUtils utils.MounterUtils, -) Mounter { - - m := &MountpointS3Mounter{} - - if v, ok := secretMap["bucketName"]; ok { - m.BucketName = v - } - if v, ok := secretMap["objPath"]; ok { - m.ObjPath = v - } - if v, ok := secretMap["region"]; ok { - m.Region = v - } - if v, ok := secretMap["endpoint"]; ok { - m.Endpoint = v - } - if v, ok := secretMap["uid"]; ok { - m.UID = v - } - if v, ok := secretMap["gid"]; ok { - m.GID = v - } - if v, ok := secretMap["readOnly"]; ok && v == "true" { - m.ReadOnly = true - } - - m.MountOptions = mountOptions - m.MounterUtils = mounterUtils - - klog.Infof( - "newMountpointS3Mounter bucket=%s prefix=%s region=%s endpoint=%s readonly=%v", - m.BucketName, m.ObjPath, m.Region, m.Endpoint, m.ReadOnly, + +const ( + s3MountCredentialsFile = "credentials" + s3MountConfigFile = "config" + s3MountAwsProfile = "default" +) + +var ( + removeS3ConfigFile = removeS3MountConfigFile +) + +// NewMountpointS3Mounter creates a new MountpointS3Mounter from secretMap +func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounterUtils utils.MounterUtils) Mounter { + klog.Info("-newMountpointS3Mounter-") + + var ( + val string + check bool + mounter = &MountpointS3Mounter{} ) - return m + if val, check = secretMap["cosEndpoint"]; check { + mounter.EndPoint = val + } + if val, check = secretMap["locationConstraint"]; check { + mounter.LocConstraint = val + } + if val, check = secretMap["bucketName"]; check { + mounter.BucketName = val + } + if val, check = secretMap["objectPath"]; check { + mounter.ObjectPath = val + } + if val, check = secretMap["accessKey"]; check { + mounter.AccessKey = val + } + if val, check = secretMap["secretKey"]; check { + mounter.SecretKey = val + } + if val, check = secretMap["uid"]; check { + mounter.UID = val + } + // If gid set but uid not, use gid for uid (same as rclone behavior) + if secretMap["gid"] != "" && secretMap["uid"] == "" { + mounter.UID = secretMap["gid"] + } else if secretMap["uid"] != "" { + mounter.UID = secretMap["uid"] + } + if val, check = secretMap["gid"]; check { + mounter.GID = val + } + if val, check = secretMap["umask"]; check { + mounter.UMask = val + } + if val, check = secretMap["logLevel"]; check { + mounter.LogLevel = val + } + if val, check = secretMap["readOnly"]; check { + mounter.ReadOnly = val == "true" + } + + mounter.AuthType = "hmac" + + klog.Infof("newMountpointS3Mounter args:\n\tbucketName: [%s]\n\tobjectPath: [%s]\n\tendPoint: [%s]\n\tlocationConstraint: [%s]\n\tauthType: [%s]", + mounter.BucketName, mounter.ObjectPath, mounter.EndPoint, mounter.LocConstraint, mounter.AuthType) + + mounter.MountOptions = mountOptions + mounter.MounterUtils = mounterUtils + + return mounter } -func (m *MountpointS3Mounter) Mount(source, target string) error { - klog.Info("-MountpointS3 Mount-") - klog.Infof("Mount target: %s", target) - bucket := m.BucketName - if m.ObjPath != "" { - bucket = fmt.Sprintf("%s:%s", bucket, strings.TrimPrefix(m.ObjPath, "/")) +// Mount mounts the S3 bucket using mountpoint-s3 +func (s3 *MountpointS3Mounter) Mount(source string, target string) error { + klog.Info("-MountpointS3Mounter Mount-") + klog.Infof("Mount args:\n\tsource: <%s>\n\ttarget: <%s>", source, target) + + // Determine config path based on mode + var configPath string + if mountWorker { + configPath = constants.MounterConfigPathOnHost + } else { + configPath = constants.MounterConfigPathOnPodS3Mount } - // Arguments sent to mounter daemon - args := map[string]string{ - "region": m.Region, + // Create unique config dir per volume + configPathWithVolID := path.Join(configPath, fmt.Sprintf("%x", sha256.Sum256([]byte(target)))) + + configCreated := false + + // Cleanup on failure + defer func() { + if err != nil && configCreated { + klog.Warningf("Mount failed, cleaning up config dir: %s", configPathWithVolID) + removeS3ConfigFile(configPath, target) + } + }() + + // Write AWS credentials config file + if err := createS3MountConfig(configPathWithVolID, s3); err != nil { + klog.Errorf("MountpointS3Mounter Mount: Cannot create config file: %v", err) + return err } + + configCreated = true - if m.Endpoint != "" { - args["endpoint-url"] = m.Endpoint + // Build bucket path with optional object path prefix + bucketName := s3.BucketName + if s3.ObjectPath != "" { + trimmedPath := strings.TrimPrefix(s3.ObjectPath, "/") + bucketName = fmt.Sprintf("%s/%s", s3.BucketName, trimmedPath) } - if m.ReadOnly { - args["read-only"] = "true" + + args, wnOp := s3.formulateMountOptions(bucketName, target, configPathWithVolID) + + if mountWorker { + klog.Info("Mount on Worker started...") + + jsonData, err := json.Marshal(wnOp) + if err != nil { + klog.Errorf("Error marshalling mount args: %v", err) + return err + } + + payload := fmt.Sprintf(`{"path":"%s","bucket":"%s","mounter":"%s","args":%s}`, + target, bucketName, constants.MountpointS3, jsonData) + + err = mounterRequest(payload, "http://unix/api/cos/mount") + if err != nil { + klog.Errorf("failed to mount on worker: %v", err) + return err + } + return nil } - if m.UID != "" { - args["uid"] = m.UID + + klog.Info("NodeServer Mounting...") + return s3.MounterUtils.FuseMount(target, constants.MountpointS3BinaryPath, args) +} + +// Unmount unmounts the S3 bucket +func (s3 *MountpointS3Mounter) Unmount(target string) error { + klog.Info("-MountpointS3Mounter Unmount-") + + if mountWorker { + klog.Info("Unmount on Worker started...") + + payload := fmt.Sprintf(`{"path":"%s"}`, target) + + err := mounterRequest(payload, "http://unix/api/cos/unmount") + if err != nil { + klog.Errorf("failed to unmount on worker: %v", err) + return err + } + + removeS3ConfigFile(constants.MounterConfigPathOnHost, target) + return nil } - if m.GID != "" { - args["gid"] = m.GID + + klog.Info("NodeServer Unmounting...") + err := s3.MounterUtils.FuseUnmount(target) + if err != nil { + return err } - for _, opt := range m.MountOptions { - parts := strings.SplitN(opt, "=", 2) - if len(parts) == 2 { - args[parts[0]] = parts[1] - } else { - args[parts[0]] = "true" - } + removeS3ConfigFile(constants.MounterConfigPathOnPodS3Mount, target) + return nil +} + +// createS3MountConfig creates the AWS credentials and config files +// that mount-s3 reads via --aws-config-dir flag. +// Directory structure: +// +// / +// credentials <- AWS credentials (access/secret key) +// config <- AWS config (region, endpoint) +func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) error { + // Create the config directory + if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 + klog.Errorf("MountpointS3Mounter: Cannot create config dir %s: %v", configPathWithVolID, err) + return err } - payload := map[string]interface{}{ - "path": target, - "bucket": bucket, - "mounter": constants.MountpointS3, - "args": args, + // --- Write credentials file --- + // Format matches AWS credentials file: + // [default] + // aws_access_key_id = + // aws_secret_access_key = + credFile := path.Join(configPathWithVolID, s3MountCredentialsFile) + credParams := []string{ + "[" + s3MountAwsProfile + "]", + "aws_access_key_id = " + s3.AccessKey, + "aws_secret_access_key = " + s3.SecretKey, + } + if err := writeConfigFile(credFile, credParams); err != nil { + klog.Errorf("MountpointS3Mounter: Cannot write credentials file: %v", err) + return err } - data, err := json.Marshal(payload) - if err != nil { + // --- Write config file --- + // Format matches AWS config file: + // [default] + // region = + // endpoint_url = (for IBM COS compatibility) + region := convertLocationConstraintToRegion(s3.LocConstraint) + configParams := []string{ + "[" + s3MountAwsProfile + "]", + "region = " + region, + } + if s3.EndPoint != "" { + configParams = append(configParams, "endpoint_url = "+s3.EndPoint) + } + cfgFile := path.Join(configPathWithVolID, s3MountConfigFile) + if err := writeConfigFile(cfgFile, configParams); err != nil { + klog.Errorf("MountpointS3Mounter: Cannot write config file: %v", err) return err } - klog.Infof("Worker Mounting Payload: %s", string(data)) + klog.Infof("MountpointS3Mounter: Created config files at %s", configPathWithVolID) + return nil +} - return mounterRequest( - string(data), - "http://unix/api/cos/mount", - ) +// writeConfigFile writes lines to a file with 0600 permissions +func writeConfigFile(filePath string, lines []string) error { + f, err := CreateFile(filePath) // #nosec G304 + if err != nil { + return fmt.Errorf("cannot create file %s: %w", filePath, err) + } + defer func() { + if cerr := f.Close(); cerr != nil { + klog.Errorf("writeConfigFile: cannot close file %s: %v", filePath, cerr) + } + }() + + if err := Chmod(filePath, 0600); err != nil { // #nosec G302 + return fmt.Errorf("cannot chmod file %s: %w", filePath, err) + } + + w := bufio.NewWriter(f) + for _, line := range lines { + if _, err := w.WriteString(line + "\n"); err != nil { + return fmt.Errorf("cannot write to file %s: %w", filePath, err) + } + } + return w.Flush() } -func (m *MountpointS3Mounter) Unmount(target string) error { - klog.Info("-MountpointS3 Unmount-") - klog.Infof("Unmount target: %s", target) - payload := fmt.Sprintf(`{"path":"%s"}`, target) +// formulateMountOptions builds CLI args for mount-s3 and the worker payload +func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathWithVolID string) (nodeServerOp []string, workerNodeOp s3MounterArgs) { + // mount-s3 syntax: mount-s3 [flags] + nodeServerOp = []string{ + bucket, + target, + "--allow-other", + // Point mount-s3 to our config directory for credentials & config + "--aws-config-dir=" + configPathWithVolID, + } - return mounterRequest( - payload, - "http://unix/api/cos/unmount", - ) + workerNodeOp = s3MounterArgs{ + AllowOther: "true", + AwsConfigDir: configPathWithVolID, + } + + // Endpoint (IBM COS endpoint) + if s3.EndPoint != "" { + nodeServerOp = append(nodeServerOp, "--endpoint-url="+s3.EndPoint) + workerNodeOp.EndpointURL = s3.EndPoint + } + + // Region from location constraint + // Not rquired because already passed in config file, There is no issue in passing it with CLI but may create an issue if the regions are differnet.. + // if s3.LocConstraint != "" { + // region := convertLocationConstraintToRegion(s3.LocConstraint) + // nodeServerOp = append(nodeServerOp, "--region="+region) + // workerNodeOp.Region = region + // } + + // UID + if s3.UID != "" { + nodeServerOp = append(nodeServerOp, "--uid="+s3.UID) + workerNodeOp.UID = s3.UID + } + + // GID + if s3.GID != "" { + nodeServerOp = append(nodeServerOp, "--gid="+s3.GID) + workerNodeOp.GID = s3.GID + } + + // UMask + if s3.UMask != "" { + nodeServerOp = append(nodeServerOp, "--umask="+s3.UMask) + workerNodeOp.UMask = s3.UMask + } + + // Log level + logLevel := s3.LogLevel + if logLevel == "" { + logLevel = "warn" + } + nodeServerOp = append(nodeServerOp, "--log-level="+logLevel) + workerNodeOp.LogLevel = logLevel + + // Read-only + if s3.ReadOnly { + nodeServerOp = append(nodeServerOp, "--read-only") + workerNodeOp.ReadOnly = "true" + } + + // Extra mount options from StorageClass + nodeServerOp = append(nodeServerOp, s3.MountOptions...) + + klog.Infof("MountpointS3 nodeServerOp: %v", nodeServerOp) + klog.Infof("MountpointS3 workerNodeOp: %+v", workerNodeOp) + + return nodeServerOp, workerNodeOp +} + +// removeS3MountConfigFile removes the config directory for the volume (same pattern as rclone) +func removeS3MountConfigFile(configPath, target string) { + configPathWithVolID := path.Join(configPath, fmt.Sprintf("%x", sha256.Sum256([]byte(target)))) + + for retry := 1; retry <= maxRetries; retry++ { + _, err := Stat(configPathWithVolID) + if err != nil { + if os.IsNotExist(err) { + klog.Infof("removeS3MountConfigFile: Config dir does not exist: %s", configPathWithVolID) + return + } + klog.Errorf("removeS3MountConfigFile: Attempt %d - Failed to stat path %s: %v", retry, configPathWithVolID, err) + time.Sleep(constants.Interval) + continue + } + if err = RemoveAll(configPathWithVolID); err != nil { + klog.Errorf("removeS3MountConfigFile: Attempt %d - Failed to remove config dir %s: %v", retry, configPathWithVolID, err) + time.Sleep(constants.Interval) + continue + } + klog.Infof("removeS3MountConfigFile: Successfully removed config dir: %s", configPathWithVolID) + return + } + klog.Errorf("removeS3MountConfigFile: Failed to remove config dir after %d attempts", maxRetries) } + +// convertLocationConstraintToRegion converts IBM COS location constraints +// to AWS-style region strings that mountpoint-s3 understands +// e.g. "us-east-standard" -> "us-east-1" +func convertLocationConstraintToRegion(locationConstraint string) string { + tiers := []string{"-standard", "-smart", "-cold", "-flex", "-vault"} + region := locationConstraint + for _, tier := range tiers { + region = strings.TrimSuffix(region, tier) + } + + regionMap := map[string]string{ + "us-east": "us-east-1", + "us-south": "us-south", + "eu-de": "eu-de", + "eu-gb": "eu-gb", + "ap-north": "ap-north", + "ap-south": "ap-south", + "jp-tok": "jp-tok", + "au-syd": "au-syd", + "ca-tor": "ca-tor", + "br-sao": "br-sao", + "global": "global", + } + + if mapped, ok := regionMap[region]; ok { + return mapped + } + + klog.Warningf("No region mapping found for location constraint: %s, using as-is: %s", locationConstraint, region) + return region +} \ No newline at end of file From 836810f86d8ad7ac6b48cb5a8a10cdb8c07ce882 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 16 Feb 2026 01:16:14 +0530 Subject: [PATCH 003/128] Updated version --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a409aad1..03ca723e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v1.0.6 - name: v1.0.6 + tag_name: v0.12.4 + name: v0.12.4 body: | ## πŸš€ What’s New - Add support for AddMountParam to pass additional s3fs mount options From 2e54ee8a101224234b83a2788fa4b0ea353570e8 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Tue, 17 Feb 2026 11:38:05 +0530 Subject: [PATCH 004/128] Added constants and update release version & branch --- .github/workflows/release.yml | 6 +++--- pkg/constants/constants.go | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 03ca723e..1dd1bfed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,10 +3,10 @@ name: CI & Release on: push: branches: - - main + - aws-s3mounter pull_request: branches: - - main + - aws-s3mounter concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 1.0.6 + APP_VERSION: v0.12.4 steps: - name: Checkout Code diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index f458a2c5..64bca673 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -36,6 +36,11 @@ const ( COSCSIMounterSocketPathEnv = "COS_CSI_MOUNTER_SOCKET" MounterConfigPathOnHost = "/var/lib/coscsi-config" MounterConfigPathOnPodS3fs = "/var/lib/ibmc-s3fs" + MountpointS3 = "mount-s3" + MountpointS3BinaryPath = "/usr/bin/mount-s3" + + // Config paths for mountpoint-s3 (same pattern as rclone) + MounterConfigPathOnPodS3Mount = "/var/lib/coscsi-config/s3mount" MounterConfigPathOnPodRclone = "/root/.config/rclone" // Interval to wait till next loop Interval = 500 * time.Millisecond From 9b9df63640e55fd37cedc66126d88e963d5efc44 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Thu, 19 Feb 2026 01:10:02 +0530 Subject: [PATCH 005/128] Commented cleanup function publish image --- pkg/mounter/mounter_s3mounter.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index d32a36ee..8db1e117 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -135,23 +135,23 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { // Create unique config dir per volume configPathWithVolID := path.Join(configPath, fmt.Sprintf("%x", sha256.Sum256([]byte(target)))) - configCreated := false + // configCreated := false - // Cleanup on failure - defer func() { - if err != nil && configCreated { - klog.Warningf("Mount failed, cleaning up config dir: %s", configPathWithVolID) - removeS3ConfigFile(configPath, target) - } - }() + // // Cleanup on failure + // defer func() { + // if err != nil && configCreated { + // klog.Warningf("Mount failed, cleaning up config dir: %s", configPathWithVolID) + // removeS3ConfigFile(configPath, target) + // } + // }() // Write AWS credentials config file if err := createS3MountConfig(configPathWithVolID, s3); err != nil { klog.Errorf("MountpointS3Mounter Mount: Cannot create config file: %v", err) return err } - - configCreated = true + + // configCreated = true // Build bucket path with optional object path prefix bucketName := s3.BucketName From b815329055176941566ea2782e05480809f875d9 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Thu, 19 Feb 2026 12:02:56 +0530 Subject: [PATCH 006/128] publish v0.12.4 --- .github/workflows/release.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1dd1bfed..2f9bf941 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -143,8 +143,7 @@ jobs: name: v0.12.4 body: | ## πŸš€ What’s New - - Add support for AddMountParam to pass additional s3fs mount options - - AddMountParam allows passing s3fs mount options as a comma-separated string + - Testing aws-s3Mounter image prerelease: ${{ env.IS_LATEST_RELEASE != 'true' }} - name: Perform CodeQL Analysis From dfcf06f03d2efef5e96fe85bf499461d25c65cf2 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Thu, 19 Feb 2026 12:29:26 +0530 Subject: [PATCH 007/128] publish v0.12.5 --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2f9bf941..20b1fd17 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: v0.12.4 + APP_VERSION: v0.12.5 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.4 - name: v0.12.4 + tag_name: v0.12.5 + name: v0.12.5 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image From 4bedbf67889f0bb26ae1584d6ccaeb6091f1214f Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Thu, 19 Feb 2026 18:50:18 +0530 Subject: [PATCH 008/128] publish v0.12.6 --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 20b1fd17..33cc4391 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: v0.12.5 + APP_VERSION: v0.12.6 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.5 - name: v0.12.5 + tag_name: v0.12.6 + name: v0.12.6 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image From 871f13ed3340853fec481932a6b8d6213c03cad9 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Fri, 20 Feb 2026 11:32:21 +0530 Subject: [PATCH 009/128] publish v0.12.6 --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 33cc4391..61aed924 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: v0.12.6 + APP_VERSION: 0.12.6 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.6 - name: v0.12.6 + tag_name: 0.12.6 + name: 0.12.6 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image From c21b509370b5048eb219e967074c16c2183633d5 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 23 Feb 2026 11:45:24 +0530 Subject: [PATCH 010/128] Update version in makefile --- cos-csi-mounter/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index 66423c60..41d1ca73 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 1.0.6 +APP_VERSION := 0.12.6 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin From e4dc4411b5e188bdc65a480fa24fe051d8304ef0 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 23 Feb 2026 11:47:28 +0530 Subject: [PATCH 011/128] publish 0.12.6 --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 61aed924..dde679f3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -144,6 +144,7 @@ jobs: body: | ## πŸš€ What’s New - Testing aws-s3Mounter image + - Publishing new test version prerelease: ${{ env.IS_LATEST_RELEASE != 'true' }} - name: Perform CodeQL Analysis From 6e4565bb1186a72492ed550702bada5d09e30e89 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 23 Feb 2026 12:00:03 +0530 Subject: [PATCH 012/128] publish v0.12.7 --- .github/workflows/release.yml | 6 +++--- cos-csi-mounter/Makefile | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dde679f3..a368d9e6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.6 + APP_VERSION: 0.12.7 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: 0.12.6 - name: 0.12.6 + tag_name: v0.12.7 + name: v0.12.7 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index 41d1ca73..b1fba2df 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.6 +APP_VERSION := 0.12.7 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin From 49dd8935d58907195902516c728cab27769f6eda Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Mon, 23 Feb 2026 14:54:56 +0530 Subject: [PATCH 013/128] publish v0.12.7 --- pkg/mounter/mounter_s3mounter.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 8db1e117..71e81272 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -124,7 +124,7 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { klog.Info("-MountpointS3Mounter Mount-") klog.Infof("Mount args:\n\tsource: <%s>\n\ttarget: <%s>", source, target) - // Determine config path based on mode + // Determine config path based on mode var configPath string if mountWorker { configPath = constants.MounterConfigPathOnHost @@ -132,18 +132,18 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { configPath = constants.MounterConfigPathOnPodS3Mount } - // Create unique config dir per volume + // Create unique config dir per volume configPathWithVolID := path.Join(configPath, fmt.Sprintf("%x", sha256.Sum256([]byte(target)))) - + // configCreated := false - // // Cleanup on failure - // defer func() { - // if err != nil && configCreated { - // klog.Warningf("Mount failed, cleaning up config dir: %s", configPathWithVolID) - // removeS3ConfigFile(configPath, target) - // } - // }() + // // Cleanup on failure + // defer func() { + // if err != nil && configCreated { + // klog.Warningf("Mount failed, cleaning up config dir: %s", configPathWithVolID) + // removeS3ConfigFile(configPath, target) + // } + // }() // Write AWS credentials config file if err := createS3MountConfig(configPathWithVolID, s3); err != nil { @@ -224,7 +224,7 @@ func (s3 *MountpointS3Mounter) Unmount(target string) error { // config <- AWS config (region, endpoint) func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) error { // Create the config directory - if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 + if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 -- directory is created under a controlled path using SHA256 hash, not user input klog.Errorf("MountpointS3Mounter: Cannot create config dir %s: %v", configPathWithVolID, err) return err } @@ -270,7 +270,7 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er // writeConfigFile writes lines to a file with 0600 permissions func writeConfigFile(filePath string, lines []string) error { - f, err := CreateFile(filePath) // #nosec G304 + f, err := CreateFile(filePath) // #nosec G304 -- filePath is constructed internally from a SHA256 hash and constant config dir, not from user input if err != nil { return fmt.Errorf("cannot create file %s: %w", filePath, err) } @@ -280,7 +280,7 @@ func writeConfigFile(filePath string, lines []string) error { } }() - if err := Chmod(filePath, 0600); err != nil { // #nosec G302 + if err := Chmod(filePath, 0600); err != nil { // #nosec G302 -- restrictive permissions (0600) are intentionally set for credential files return fmt.Errorf("cannot chmod file %s: %w", filePath, err) } @@ -420,4 +420,4 @@ func convertLocationConstraintToRegion(locationConstraint string) string { klog.Warningf("No region mapping found for location constraint: %s, using as-is: %s", locationConstraint, region) return region -} \ No newline at end of file +} From 7133e9a218afc987c64edf2ea15cc50840216340 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 23 Feb 2026 16:14:17 +0530 Subject: [PATCH 014/128] publish tag 0.12.8 --- .github/workflows/release.yml | 6 +++--- cos-csi-mounter/Makefile | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a368d9e6..4aae3f9c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.7 + APP_VERSION: 0.12.8 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.7 - name: v0.12.7 + tag_name: v0.12.8 + name: v0.12.8 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index b1fba2df..3833dd55 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.7 +APP_VERSION := 0.12.8 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin From 221f345eeec7b5e0bd3b0db8f6ec40da9bc4ecc8 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Wed, 25 Feb 2026 13:10:44 +0530 Subject: [PATCH 015/128] publish v0.12.9 --- .github/workflows/release.yml | 6 +++--- cos-csi-mounter/Makefile | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4aae3f9c..15c18260 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.8 + APP_VERSION: 0.12.9 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.8 - name: v0.12.8 + tag_name: v0.12.9 + name: v0.12.9 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index 3833dd55..56e72505 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.8 +APP_VERSION := 0.12.9 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin From 6ad38e5e5cf0fe4ca57e67dac9c5d7f9d5b8fe59 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Tue, 3 Mar 2026 17:22:06 +0530 Subject: [PATCH 016/128] Updated name to mount-s3 --- cos-csi-mounter/server/server.go | 2 +- cos-csi-mounter/server/utils.go | 2 +- pkg/constants/constants.go | 3 +-- pkg/mounter/mounter_s3mounter.go | 43 +------------------------------- 4 files changed, 4 insertions(+), 46 deletions(-) diff --git a/cos-csi-mounter/server/server.go b/cos-csi-mounter/server/server.go index 05643607..467eabe3 100644 --- a/cos-csi-mounter/server/server.go +++ b/cos-csi-mounter/server/server.go @@ -169,7 +169,7 @@ func handleCosMount(mounter mounterUtils.MounterUtils, parser MounterArgsParser) logger.Info("New mount request with values:", zap.String("Bucket", request.Bucket), zap.String("Path", request.Path), zap.String("Mounter", request.Mounter), zap.Any("Args", request.Args)) - if request.Mounter != constants.S3FS && request.Mounter != constants.RClone && request.Mounter != constants.S3MOUNTER { + if request.Mounter != constants.S3FS && request.Mounter != constants.RClone && request.Mounter != constants.AMAZONS3MOUNTER { logger.Error("invalid mounter", zap.Any("mounter", request.Mounter)) c.JSON(http.StatusBadRequest, gin.H{"error": "invalid mounter"}) return diff --git a/cos-csi-mounter/server/utils.go b/cos-csi-mounter/server/utils.go index 877dba35..3807a1e7 100644 --- a/cos-csi-mounter/server/utils.go +++ b/cos-csi-mounter/server/utils.go @@ -86,7 +86,7 @@ func (req *MountRequest) ParseMounterArgs() ([]string, error) { } return args.PopulateArgsSlice(req.Bucket, req.Path) - case constants.S3MOUNTER: + case constants.AMAZONS3MOUNTER: var args s3MounterArgs if err := strictDecodeForUnknownFields(req.Args, &args); err != nil { return nil, fmt.Errorf("invalid rclone args decode error: %w", err) diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 64bca673..7f3234dd 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -16,7 +16,7 @@ const ( S3FS = "s3fs" RClone = "rclone" - S3MOUNTER = "s3mounter" + AMAZONS3MOUNTER = "mount-s3" DefaultNamespace = "default" @@ -36,7 +36,6 @@ const ( COSCSIMounterSocketPathEnv = "COS_CSI_MOUNTER_SOCKET" MounterConfigPathOnHost = "/var/lib/coscsi-config" MounterConfigPathOnPodS3fs = "/var/lib/ibmc-s3fs" - MountpointS3 = "mount-s3" MountpointS3BinaryPath = "/usr/bin/mount-s3" // Config paths for mountpoint-s3 (same pattern as rclone) diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 71e81272..259d84df 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -250,7 +250,7 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er // [default] // region = // endpoint_url = (for IBM COS compatibility) - region := convertLocationConstraintToRegion(s3.LocConstraint) + region := s3.LocConstraint configParams := []string{ "[" + s3MountAwsProfile + "]", "region = " + region, @@ -314,15 +314,6 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW nodeServerOp = append(nodeServerOp, "--endpoint-url="+s3.EndPoint) workerNodeOp.EndpointURL = s3.EndPoint } - - // Region from location constraint - // Not rquired because already passed in config file, There is no issue in passing it with CLI but may create an issue if the regions are differnet.. - // if s3.LocConstraint != "" { - // region := convertLocationConstraintToRegion(s3.LocConstraint) - // nodeServerOp = append(nodeServerOp, "--region="+region) - // workerNodeOp.Region = region - // } - // UID if s3.UID != "" { nodeServerOp = append(nodeServerOp, "--uid="+s3.UID) @@ -389,35 +380,3 @@ func removeS3MountConfigFile(configPath, target string) { } klog.Errorf("removeS3MountConfigFile: Failed to remove config dir after %d attempts", maxRetries) } - -// convertLocationConstraintToRegion converts IBM COS location constraints -// to AWS-style region strings that mountpoint-s3 understands -// e.g. "us-east-standard" -> "us-east-1" -func convertLocationConstraintToRegion(locationConstraint string) string { - tiers := []string{"-standard", "-smart", "-cold", "-flex", "-vault"} - region := locationConstraint - for _, tier := range tiers { - region = strings.TrimSuffix(region, tier) - } - - regionMap := map[string]string{ - "us-east": "us-east-1", - "us-south": "us-south", - "eu-de": "eu-de", - "eu-gb": "eu-gb", - "ap-north": "ap-north", - "ap-south": "ap-south", - "jp-tok": "jp-tok", - "au-syd": "au-syd", - "ca-tor": "ca-tor", - "br-sao": "br-sao", - "global": "global", - } - - if mapped, ok := regionMap[region]; ok { - return mapped - } - - klog.Warningf("No region mapping found for location constraint: %s, using as-is: %s", locationConstraint, region) - return region -} From 47e395c462b3cd1295ba231bfa1dcbedf180f50a Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Tue, 3 Mar 2026 17:24:16 +0530 Subject: [PATCH 017/128] publish 0.12.10 version --- .github/workflows/release.yml | 6 +++--- cos-csi-mounter/Makefile | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 15c18260..18eaad72 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.9 + APP_VERSION: 0.12.10 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.9 - name: v0.12.9 + tag_name: v0.12.10 + name: v0.12.10 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index 56e72505..5602219e 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.9 +APP_VERSION := 0.12.10 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin From a5c909acc6a5fec0074701cd85b16d4af361f40f Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Tue, 3 Mar 2026 23:30:26 +0530 Subject: [PATCH 018/128] publish 0.12.10 version --- pkg/mounter/mounter_s3mounter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 259d84df..bf204225 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -172,7 +172,7 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { } payload := fmt.Sprintf(`{"path":"%s","bucket":"%s","mounter":"%s","args":%s}`, - target, bucketName, constants.MountpointS3, jsonData) + target, bucketName, constants.AMAZONS3MOUNTER, jsonData) err = mounterRequest(payload, "http://unix/api/cos/mount") if err != nil { From 8e4f7cbe0fb846dd03b3e9df76861f5e6ac7d370 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Thu, 5 Mar 2026 09:29:27 +0530 Subject: [PATCH 019/128] Fix mount-s3 worker args in cos-csi-mounter --- cos-csi-mounter/server/s3mounter.go | 37 ++++++++++++++++++++++------- cos-csi-mounter/server/utils.go | 4 ++-- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go index 5553283b..cb0e309c 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/s3mounter.go @@ -8,12 +8,15 @@ import ( ) type s3MounterArgs struct { - ReadOnly string `json:"read-only,omitempty"` - AllowOther string `json:"allow-other,omitempty"` - UID string `json:"uid,omitempty"` - GID string `json:"gid,omitempty"` - UMask string `json:"umask,omitempty"` - LogLevel string `json:"log-level,omitempty"` + ReadOnly string `json:"read-only,omitempty"` + AllowOther string `json:"allow-other,omitempty"` + UID string `json:"uid,omitempty"` + GID string `json:"gid,omitempty"` + UMask string `json:"umask,omitempty"` + LogLevel string `json:"log-level,omitempty"` + AwsConfigDir string `json:"aws-config-dir,omitempty"` + EndpointURL string `json:"endpoint-url,omitempty"` + Region string `json:"region,omitempty"` } func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string, error) { @@ -30,7 +33,7 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string } // Convert to key=value slice - result := []string{"mount", bucket, targetPath} + result := []string{bucket, targetPath} for k, v := range m { result = append(result, fmt.Sprintf("--%s=%v", k, v)) // --key=value } @@ -51,6 +54,25 @@ func (args s3MounterArgs) Validate(targetPath string) error { } } + if args.AwsConfigDir == "" { + logger.Error("aws-config-dir is required for mount-s3") + return fmt.Errorf("aws-config-dir is required for mount-s3") + } + + if args.AllowOther != "" { + if isBool := isBoolString(args.AllowOther); !isBool { + logger.Error("cannot convert value of allow-other into boolean", zap.Any("allow-other", args.AllowOther)) + return fmt.Errorf("cannot convert value of allow-other into boolean: %v", args.AllowOther) + } + } + + if args.ReadOnly != "" { + if isBool := isBoolString(args.ReadOnly); !isBool { + logger.Error("cannot convert value of read-only into boolean", zap.Any("read-only", args.ReadOnly)) + return fmt.Errorf("cannot convert value of read-only into boolean: %v", args.ReadOnly) + } + } + // // Check if rclone config file exists or not // if exists, err := FileExists(args.ConfigPath); err != nil { // logger.Error("error checking rclone config file existence") @@ -60,7 +82,6 @@ func (args s3MounterArgs) Validate(targetPath string) error { // return fmt.Errorf("rclone config file not found") // } - // Check if value of read-only parameter is boolean "true" or "false" if args.ReadOnly != "" { if isBool := isBoolString(args.ReadOnly); !isBool { diff --git a/cos-csi-mounter/server/utils.go b/cos-csi-mounter/server/utils.go index 3807a1e7..1c63f5fa 100644 --- a/cos-csi-mounter/server/utils.go +++ b/cos-csi-mounter/server/utils.go @@ -85,11 +85,11 @@ func (req *MountRequest) ParseMounterArgs() ([]string, error) { return nil, fmt.Errorf("rclone args validation failed: %w", err) } return args.PopulateArgsSlice(req.Bucket, req.Path) - + case constants.AMAZONS3MOUNTER: var args s3MounterArgs if err := strictDecodeForUnknownFields(req.Args, &args); err != nil { - return nil, fmt.Errorf("invalid rclone args decode error: %w", err) + return nil, fmt.Errorf("invalid mount-s3 args decode error: %w", err) // ← fix: was saying rclone } if err := args.Validate(req.Path); err != nil { return nil, fmt.Errorf("s3Mounter args validation failed: %w", err) From 96064fbe523adf2f7fae214f2ca1191a5bbfc3fd Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Thu, 5 Mar 2026 09:30:35 +0530 Subject: [PATCH 020/128] Fix mount-s3 worker args in cos-csi-mounter --- cos-csi-mounter/server/s3mounter.go | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go index cb0e309c..199770ce 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/s3mounter.go @@ -59,29 +59,6 @@ func (args s3MounterArgs) Validate(targetPath string) error { return fmt.Errorf("aws-config-dir is required for mount-s3") } - if args.AllowOther != "" { - if isBool := isBoolString(args.AllowOther); !isBool { - logger.Error("cannot convert value of allow-other into boolean", zap.Any("allow-other", args.AllowOther)) - return fmt.Errorf("cannot convert value of allow-other into boolean: %v", args.AllowOther) - } - } - - if args.ReadOnly != "" { - if isBool := isBoolString(args.ReadOnly); !isBool { - logger.Error("cannot convert value of read-only into boolean", zap.Any("read-only", args.ReadOnly)) - return fmt.Errorf("cannot convert value of read-only into boolean: %v", args.ReadOnly) - } - } - - // // Check if rclone config file exists or not - // if exists, err := FileExists(args.ConfigPath); err != nil { - // logger.Error("error checking rclone config file existence") - // return fmt.Errorf("error checking rclone config file existence") - // } else if !exists { - // logger.Error("rclone config file not found") - // return fmt.Errorf("rclone config file not found") - // } - // Check if value of read-only parameter is boolean "true" or "false" if args.ReadOnly != "" { if isBool := isBoolString(args.ReadOnly); !isBool { From ea53d12a8b23e79277ab1f3ae11f27303e7b0429 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Thu, 5 Mar 2026 10:18:07 +0530 Subject: [PATCH 021/128] Fix mount-s3 worker args in cos-csi-mounter --- .github/workflows/release.yml | 2 +- cos-csi-mounter/server/utils.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 18eaad72..faaa2573 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.10 + APP_VERSION: 0.12.11 steps: - name: Checkout Code diff --git a/cos-csi-mounter/server/utils.go b/cos-csi-mounter/server/utils.go index 1c63f5fa..58c41f28 100644 --- a/cos-csi-mounter/server/utils.go +++ b/cos-csi-mounter/server/utils.go @@ -89,7 +89,7 @@ func (req *MountRequest) ParseMounterArgs() ([]string, error) { case constants.AMAZONS3MOUNTER: var args s3MounterArgs if err := strictDecodeForUnknownFields(req.Args, &args); err != nil { - return nil, fmt.Errorf("invalid mount-s3 args decode error: %w", err) // ← fix: was saying rclone + return nil, fmt.Errorf("invalid mount-s3 args decode error: %w", err) } if err := args.Validate(req.Path); err != nil { return nil, fmt.Errorf("s3Mounter args validation failed: %w", err) From 86f7434f96053843450dd9c36f47252e613e2ab0 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Thu, 5 Mar 2026 10:21:13 +0530 Subject: [PATCH 022/128] publish 0.12.11 version --- pkg/mounter/mounter_s3mounter.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index bf204225..68811912 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -45,7 +45,6 @@ type s3MounterArgs struct { // Config file path for AWS credentials AwsConfigDir string `json:"aws-config-dir,omitempty"` EndpointURL string `json:"endpoint-url,omitempty"` - Region string `json:"region,omitempty"` } const ( @@ -229,7 +228,6 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er return err } - // --- Write credentials file --- // Format matches AWS credentials file: // [default] // aws_access_key_id = From 61307014dfdb86d0dca7752cab07b02c84645ed4 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Thu, 5 Mar 2026 10:28:45 +0530 Subject: [PATCH 023/128] publish 0.12.11 version --- pkg/mounter/mounter_s3mounter.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 68811912..7d099066 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -228,6 +228,7 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er return err } + // --- Write credentials file --- // Format matches AWS credentials file: // [default] // aws_access_key_id = From 26133ec9841e358a88a4639ab633874b5d52e636 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Thu, 5 Mar 2026 11:02:18 +0530 Subject: [PATCH 024/128] publish 0.12.11 version --- .github/workflows/release.yml | 4 ++-- cos-csi-mounter/Makefile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index faaa2573..9c0d483b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.10 - name: v0.12.10 + tag_name: v0.12.11 + name: v0.12.11 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index 5602219e..36780a09 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.10 +APP_VERSION := 0.12.11 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin From cb32f2d98e1f88b2bdeeb45496277b041583edd8 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sat, 7 Mar 2026 00:37:43 +0530 Subject: [PATCH 025/128] publish 0.12.11 version --- cos-csi-mounter/server/s3mounter.go | 117 +++++++++++++++++++------- pkg/mounter/mounter_s3mounter.go | 125 ++++++++++++++++++---------- pkg/mounter/utils/mounter_utils.go | 32 +++++++ 3 files changed, 201 insertions(+), 73 deletions(-) diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go index 199770ce..84ddd2b8 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/s3mounter.go @@ -1,52 +1,105 @@ package main import ( - "encoding/json" "fmt" "go.uber.org/zap" ) type s3MounterArgs struct { - ReadOnly string `json:"read-only,omitempty"` - AllowOther string `json:"allow-other,omitempty"` - UID string `json:"uid,omitempty"` - GID string `json:"gid,omitempty"` - UMask string `json:"umask,omitempty"` - LogLevel string `json:"log-level,omitempty"` - AwsConfigDir string `json:"aws-config-dir,omitempty"` - EndpointURL string `json:"endpoint-url,omitempty"` - Region string `json:"region,omitempty"` + ReadOnly string `json:"read-only,omitempty"` + AllowOther string `json:"allow-other,omitempty"` + UID string `json:"uid,omitempty"` + GID string `json:"gid,omitempty"` + // Removed: UMask β€” mount-s3 has no --umask flag; use --dir-mode/--file-mode via MountOptions + // Removed: Region β€” mount-s3 auto-detects region from AWS_CONFIG_FILE + // Removed: AwsConfigDir β€” mount-s3 has no --aws-config-dir flag + LogLevel string `json:"log-level,omitempty"` // valid: "debug", "debug-crt", "no-log" + EndpointURL string `json:"endpoint-url,omitempty"` + // AWS credential file paths β€” NOT CLI flags. + // Worker sets these as AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE + // env vars on the mount-s3 subprocess before invoking it. + AwsCredentialsFile string `json:"aws-credentials-file,omitempty"` + AwsConfigFile string `json:"aws-config-file,omitempty"` } +// PopulateArgsSlice builds the CLI args slice for mount-s3. +// +// Key rules: +// - AwsCredentialsFile and AwsConfigFile are NOT CLI flags β€” use EnvVars() for those +// - Boolean flags (--allow-other, --read-only) are emitted without a value +// because mount-s3 does not accept --allow-other=true syntax +// - LogLevel maps to --debug / --debug-crt / --no-log (mount-s3 has no --log-level) func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string, error) { - // Marshal to JSON - raw, err := json.Marshal(args) - if err != nil { - return nil, err + result := []string{bucket, targetPath} + + // --allow-other: boolean flag, no value + if args.AllowOther == "true" { + result = append(result, "--allow-other") } - // Unmarshal into map[string]string - var m map[string]string - if err := json.Unmarshal(raw, &m); err != nil { - return nil, err + // --read-only: boolean flag, no value + if args.ReadOnly == "true" { + result = append(result, "--read-only") } - // Convert to key=value slice - result := []string{bucket, targetPath} - for k, v := range m { - result = append(result, fmt.Sprintf("--%s=%v", k, v)) // --key=value + // --uid + if args.UID != "" { + result = append(result, "--uid="+args.UID) + } + + // --gid + if args.GID != "" { + result = append(result, "--gid="+args.GID) } - return result, nil // [mount, bucket, path, --key1=value1, --key2=value2, ...] + // --endpoint-url + if args.EndpointURL != "" { + result = append(result, "--endpoint-url="+args.EndpointURL) + } + + // Log level: mount-s3 has no --log-level flag. + // Map to the supported flags only. + switch args.LogLevel { + case "debug": + result = append(result, "--debug") + case "debug-crt": + result = append(result, "--debug-crt") + case "no-log": + result = append(result, "--no-log") + default: + if args.LogLevel != "" { + logger.Warn("unsupported log level for mount-s3, ignoring", + zap.String("log-level", args.LogLevel), + zap.String("supported", "debug, debug-crt, no-log"), + ) + } + } + + return result, nil +} + +// EnvVars returns the environment variables that must be set on the mount-s3 +// subprocess so it can locate the AWS credentials and config files. +// The caller must add these to cmd.Env before cmd.Start(). +func (args s3MounterArgs) EnvVars() []string { + var envVars []string + if args.AwsCredentialsFile != "" { + envVars = append(envVars, "AWS_SHARED_CREDENTIALS_FILE="+args.AwsCredentialsFile) + } + if args.AwsConfigFile != "" { + envVars = append(envVars, "AWS_CONFIG_FILE="+args.AwsConfigFile) + } + return envVars } +// Validate checks that required fields are present and values are valid. func (args s3MounterArgs) Validate(targetPath string) error { if err := pathValidator(targetPath); err != nil { return err } - // Check if value of allow-other parameter is boolean "true" or "false" + // allow-other must be a boolean string if set if args.AllowOther != "" { if isBool := isBoolString(args.AllowOther); !isBool { logger.Error("cannot convert value of allow-other into boolean", zap.Any("allow-other", args.AllowOther)) @@ -54,12 +107,18 @@ func (args s3MounterArgs) Validate(targetPath string) error { } } - if args.AwsConfigDir == "" { - logger.Error("aws-config-dir is required for mount-s3") - return fmt.Errorf("aws-config-dir is required for mount-s3") + // AwsCredentialsFile and AwsConfigFile replace the old AwsConfigDir check + if args.AwsCredentialsFile == "" { + logger.Error("aws-credentials-file is required for mount-s3") + return fmt.Errorf("aws-credentials-file is required for mount-s3") + } + + if args.AwsConfigFile == "" { + logger.Error("aws-config-file is required for mount-s3") + return fmt.Errorf("aws-config-file is required for mount-s3") } - // Check if value of read-only parameter is boolean "true" or "false" + // read-only must be a boolean string if set if args.ReadOnly != "" { if isBool := isBoolString(args.ReadOnly); !isBool { logger.Error("cannot convert value of read-only into boolean", zap.Any("read-only", args.ReadOnly)) @@ -68,4 +127,4 @@ func (args s3MounterArgs) Validate(targetPath string) error { } return nil -} +} \ No newline at end of file diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 7d099066..0ab2e1d8 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -27,8 +27,8 @@ type MountpointS3Mounter struct { SecretKey string UID string GID string - UMask string - LogLevel string + UMask string // kept for API compatibility; mount-s3 has no --umask, use --dir-mode/--file-mode via MountOptions + LogLevel string // valid values: "debug", "debug-crt", "no-log"; mount-s3 has no --log-level flag ReadOnly bool MountOptions []string MounterUtils utils.MounterUtils @@ -40,11 +40,15 @@ type s3MounterArgs struct { AllowOther string `json:"allow-other,omitempty"` UID string `json:"uid,omitempty"` GID string `json:"gid,omitempty"` - UMask string `json:"umask,omitempty"` - LogLevel string `json:"log-level,omitempty"` - // Config file path for AWS credentials - AwsConfigDir string `json:"aws-config-dir,omitempty"` - EndpointURL string `json:"endpoint-url,omitempty"` + // Removed: UMask β€” mount-s3 has no --umask flag + // Removed: AwsConfigDir β€” mount-s3 has no --aws-config-dir flag + LogLevel string `json:"log-level,omitempty"` // valid: "debug", "debug-crt", "no-log" + EndpointURL string `json:"endpoint-url,omitempty"` + // AWS credential file paths β€” NOT CLI flags. + // Worker sets these as AWS_SHARED_CREDENTIALS_FILE / AWS_CONFIG_FILE env vars + // on the mount-s3 subprocess before invoking it. + AwsCredentialsFile string `json:"aws-credentials-file,omitempty"` + AwsConfigFile string `json:"aws-config-file,omitempty"` } const ( @@ -57,6 +61,13 @@ var ( removeS3ConfigFile = removeS3MountConfigFile ) +// envMounter is a local interface for type assertion β€” allows calling +// FuseMountWithEnv on the concrete MounterOptsUtils without changing the +// MounterUtils interface, so other mounters (rclone etc.) are unaffected. +type envMounter interface { + FuseMountWithEnv(path string, comm string, args []string, envVars []string) error +} + // NewMountpointS3Mounter creates a new MountpointS3Mounter from secretMap func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounterUtils utils.MounterUtils) Mounter { klog.Info("-newMountpointS3Mounter-") @@ -134,24 +145,12 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { // Create unique config dir per volume configPathWithVolID := path.Join(configPath, fmt.Sprintf("%x", sha256.Sum256([]byte(target)))) - // configCreated := false - - // // Cleanup on failure - // defer func() { - // if err != nil && configCreated { - // klog.Warningf("Mount failed, cleaning up config dir: %s", configPathWithVolID) - // removeS3ConfigFile(configPath, target) - // } - // }() - - // Write AWS credentials config file + // Write AWS credentials and config files if err := createS3MountConfig(configPathWithVolID, s3); err != nil { klog.Errorf("MountpointS3Mounter Mount: Cannot create config file: %v", err) return err } - // configCreated = true - // Build bucket path with optional object path prefix bucketName := s3.BucketName if s3.ObjectPath != "" { @@ -159,7 +158,7 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { bucketName = fmt.Sprintf("%s/%s", s3.BucketName, trimmedPath) } - args, wnOp := s3.formulateMountOptions(bucketName, target, configPathWithVolID) + args, envVars, wnOp := s3.formulateMountOptions(bucketName, target, configPathWithVolID) if mountWorker { klog.Info("Mount on Worker started...") @@ -182,6 +181,16 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { } klog.Info("NodeServer Mounting...") + + // Use FuseMountWithEnv if available on the concrete type (race-safe, per-subprocess env). + // Falls back to FuseMount if running under a mock or different implementation β€” the + // fallback will still work but credentials must be available via system env in that case. + if m, ok := s3.MounterUtils.(envMounter); ok { + return m.FuseMountWithEnv(target, constants.MountpointS3BinaryPath, args, envVars) + } + + klog.Warning("MounterUtils does not implement FuseMountWithEnv, falling back to FuseMount. " + + "Ensure AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE are set in the process environment.") return s3.MounterUtils.FuseMount(target, constants.MountpointS3BinaryPath, args) } @@ -214,13 +223,14 @@ func (s3 *MountpointS3Mounter) Unmount(target string) error { return nil } -// createS3MountConfig creates the AWS credentials and config files -// that mount-s3 reads via --aws-config-dir flag. +// createS3MountConfig creates the AWS credentials and config files. +// mount-s3 locates these via AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE env vars. +// // Directory structure: // // / // credentials <- AWS credentials (access/secret key) -// config <- AWS config (region, endpoint) +// config <- AWS config (region, endpoint_url) func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) error { // Create the config directory if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 -- directory is created under a controlled path using SHA256 hash, not user input @@ -292,20 +302,33 @@ func writeConfigFile(filePath string, lines []string) error { return w.Flush() } -// formulateMountOptions builds CLI args for mount-s3 and the worker payload -func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathWithVolID string) (nodeServerOp []string, workerNodeOp s3MounterArgs) { +// formulateMountOptions builds CLI args, env vars, and the worker payload for mount-s3. +// +// Removed flags (do not exist in mount-s3): +// - --aws-config-dir β†’ replaced by AWS_SHARED_CREDENTIALS_FILE / AWS_CONFIG_FILE env vars +// - --log-level β†’ mapped to --debug / --debug-crt / --no-log +// - --umask β†’ use --dir-mode / --file-mode via MountOptions in StorageClass +func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathWithVolID string) (nodeServerOp []string, envVars []string, workerNodeOp s3MounterArgs) { // mount-s3 syntax: mount-s3 [flags] nodeServerOp = []string{ bucket, target, "--allow-other", - // Point mount-s3 to our config directory for credentials & config - "--aws-config-dir=" + configPathWithVolID, + } + + // AWS credentials via env vars β€” mount-s3 and the underlying AWS SDK reads these + // to locate the credentials and config files written by createS3MountConfig. + credFilePath := path.Join(configPathWithVolID, s3MountCredentialsFile) + cfgFilePath := path.Join(configPathWithVolID, s3MountConfigFile) + envVars = []string{ + "AWS_SHARED_CREDENTIALS_FILE=" + credFilePath, + "AWS_CONFIG_FILE=" + cfgFilePath, } workerNodeOp = s3MounterArgs{ - AllowOther: "true", - AwsConfigDir: configPathWithVolID, + AllowOther: "true", + AwsCredentialsFile: credFilePath, + AwsConfigFile: cfgFilePath, } // Endpoint (IBM COS endpoint) @@ -313,6 +336,7 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW nodeServerOp = append(nodeServerOp, "--endpoint-url="+s3.EndPoint) workerNodeOp.EndpointURL = s3.EndPoint } + // UID if s3.UID != "" { nodeServerOp = append(nodeServerOp, "--uid="+s3.UID) @@ -325,19 +349,32 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW workerNodeOp.GID = s3.GID } - // UMask + // UMask: mount-s3 has no --umask flag. + // Use --dir-mode / --file-mode via MountOptions in the StorageClass instead. if s3.UMask != "" { - nodeServerOp = append(nodeServerOp, "--umask="+s3.UMask) - workerNodeOp.UMask = s3.UMask - } - - // Log level - logLevel := s3.LogLevel - if logLevel == "" { - logLevel = "warn" + klog.Warningf("MountpointS3Mounter: 'umask=%s' is set but mount-s3 does not support --umask. "+ + "Use --dir-mode / --file-mode via mountOptions in the StorageClass instead.", s3.UMask) + } + + // Log level: mount-s3 has no --log-level flag. + // Valid mappings: "debug" β†’ --debug, "debug-crt" β†’ --debug-crt, "no-log" β†’ --no-log + // Anything else (e.g. "warn", "info") is ignored β€” mount-s3 default logging applies. + switch s3.LogLevel { + case "debug": + nodeServerOp = append(nodeServerOp, "--debug") + workerNodeOp.LogLevel = "debug" + case "debug-crt": + nodeServerOp = append(nodeServerOp, "--debug-crt") + workerNodeOp.LogLevel = "debug-crt" + case "no-log": + nodeServerOp = append(nodeServerOp, "--no-log") + workerNodeOp.LogLevel = "no-log" + default: + if s3.LogLevel != "" { + klog.Warningf("MountpointS3Mounter: unsupported log level '%s' for mount-s3. "+ + "Supported values: 'debug', 'debug-crt', 'no-log'. Ignoring.", s3.LogLevel) + } } - nodeServerOp = append(nodeServerOp, "--log-level="+logLevel) - workerNodeOp.LogLevel = logLevel // Read-only if s3.ReadOnly { @@ -345,13 +382,13 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW workerNodeOp.ReadOnly = "true" } - // Extra mount options from StorageClass + // Extra mount options from StorageClass (e.g. --dir-mode=0755, --file-mode=0644) nodeServerOp = append(nodeServerOp, s3.MountOptions...) klog.Infof("MountpointS3 nodeServerOp: %v", nodeServerOp) klog.Infof("MountpointS3 workerNodeOp: %+v", workerNodeOp) - return nodeServerOp, workerNodeOp + return nodeServerOp, envVars, workerNodeOp } // removeS3MountConfigFile removes the config directory for the volume (same pattern as rclone) @@ -378,4 +415,4 @@ func removeS3MountConfigFile(configPath, target string) { return } klog.Errorf("removeS3MountConfigFile: Failed to remove config dir after %d attempts", maxRetries) -} +} \ No newline at end of file diff --git a/pkg/mounter/utils/mounter_utils.go b/pkg/mounter/utils/mounter_utils.go index d2e1f8c6..05e16f57 100644 --- a/pkg/mounter/utils/mounter_utils.go +++ b/pkg/mounter/utils/mounter_utils.go @@ -32,7 +32,18 @@ type MounterUtils interface { type MounterOptsUtils struct { } +// FuseMount β€” signature unchanged, delegates to fuseMountInternal with no extra env vars. func (su *MounterOptsUtils) FuseMount(path string, comm string, args []string) error { + return su.fuseMountInternal(path, comm, args, nil) +} + +// FuseMountWithEnv β€” on the concrete struct only, NOT on the interface. +// envVars are per-subprocess env vars (e.g. AWS_SHARED_CREDENTIALS_FILE) +func (su *MounterOptsUtils) FuseMountWithEnv(path string, comm string, args []string, envVars []string) error { + return su.fuseMountInternal(path, comm, args, envVars) +} + +func (su *MounterOptsUtils) fuseMountInternal(path string, comm string, args []string, envVars []string) error { klog.Info("-FuseMount-") klog.Infof("FuseMount: params:\n\tpath: <%s>\n\tcommand: <%s>\n\targs: <%v>", path, comm, args) @@ -45,6 +56,27 @@ func (su *MounterOptsUtils) FuseMount(path string, comm string, args []string) e }() cmd := commandWithCtx(ctx, comm, args...) + + // If env vars provided, merge them with the inherited process env. + if len(envVars) > 0 { + // Build set of keys we are overriding + overrides := make(map[string]bool, len(envVars)) + for _, e := range envVars { + key := strings.SplitN(e, "=", 2)[0] + overrides[key] = true + } + // Inherit all system env except keys we are overriding + baseEnv := os.Environ() + filteredEnv := make([]string, 0, len(baseEnv)+len(envVars)) + for _, e := range baseEnv { + key := strings.SplitN(e, "=", 2)[0] + if !overrides[key] { + filteredEnv = append(filteredEnv, e) + } + } + cmd.Env = append(filteredEnv, envVars...) + } + err := cmd.Start() if err != nil { klog.Errorf("FuseMount: command start failed: mounter=%s, args=%v, error=%v", comm, args, err) From 0b706d049e623863daa9a62bdc01359688d51555 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sat, 7 Mar 2026 00:43:10 +0530 Subject: [PATCH 026/128] publish 0.12.12 version --- .github/workflows/release.yml | 6 +++--- cos-csi-mounter/Makefile | 2 +- pkg/mounter/utils/mounter_utils.go | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9c0d483b..9dcb85da 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.11 + APP_VERSION: 0.12.12 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.11 - name: v0.12.11 + tag_name: v0.12.12 + name: v0.12.12 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index 36780a09..402d2ee5 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.11 +APP_VERSION := 0.12.12 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin diff --git a/pkg/mounter/utils/mounter_utils.go b/pkg/mounter/utils/mounter_utils.go index 05e16f57..777095c4 100644 --- a/pkg/mounter/utils/mounter_utils.go +++ b/pkg/mounter/utils/mounter_utils.go @@ -59,7 +59,6 @@ func (su *MounterOptsUtils) fuseMountInternal(path string, comm string, args []s // If env vars provided, merge them with the inherited process env. if len(envVars) > 0 { - // Build set of keys we are overriding overrides := make(map[string]bool, len(envVars)) for _, e := range envVars { key := strings.SplitN(e, "=", 2)[0] From 42d70327c6149f429d0f7b768effc8735337bea0 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Mon, 9 Mar 2026 10:32:20 +0530 Subject: [PATCH 027/128] publish 0.12.12 version --- cos-csi-mounter/server/server.go | 43 +++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/cos-csi-mounter/server/server.go b/cos-csi-mounter/server/server.go index 467eabe3..82b62af3 100644 --- a/cos-csi-mounter/server/server.go +++ b/cos-csi-mounter/server/server.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "flag" "fmt" "net" @@ -31,6 +32,13 @@ var ( GitCommit = "none" ) +// envMounter is a local interface for type assertion β€” allows calling +// FuseMountWithEnv on the concrete MounterOptsUtils without changing the +// MounterUtils interface, so other mounters (rclone, s3fs) are unaffected. +type envMounter interface { + FuseMountWithEnv(path string, comm string, args []string, envVars []string) error +} + func init() { _ = flag.Set("logtostderr", "true") // #nosec G104: Attempt to set flags for logging to stderr only on best-effort basis.Error cannot be usefully handled. logger = setUpLogger() @@ -167,7 +175,12 @@ func handleCosMount(mounter mounterUtils.MounterUtils, parser MounterArgsParser) return } - logger.Info("New mount request with values:", zap.String("Bucket", request.Bucket), zap.String("Path", request.Path), zap.String("Mounter", request.Mounter), zap.Any("Args", request.Args)) + logger.Info("New mount request with values:", + zap.String("Bucket", request.Bucket), + zap.String("Path", request.Path), + zap.String("Mounter", request.Mounter), + zap.Any("Args", request.Args), + ) if request.Mounter != constants.S3FS && request.Mounter != constants.RClone && request.Mounter != constants.AMAZONS3MOUNTER { logger.Error("invalid mounter", zap.Any("mounter", request.Mounter)) @@ -185,12 +198,18 @@ func handleCosMount(mounter mounterUtils.MounterUtils, parser MounterArgsParser) args, err := parser.Parse(request) if err != nil { logger.Error("failed to parse mounter args", zap.Any("mounter", request.Mounter), zap.Error(err)) - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid args for mounter: %v", err)}) return } - err = mounter.FuseMount(request.Path, request.Mounter, args) + // For mount-s3: pass AWS credential paths as env vars on the subprocess. + // FuseMountWithEnv is on the concrete MounterOptsUtils struct only (not the interface), + // so we type-assert β€” other mounters (rclone, s3fs) fall through to plain FuseMount. + if em, ok := mounter.(envMounter); ok && request.Mounter == constants.AMAZONS3MOUNTER { + err = em.FuseMountWithEnv(request.Path, request.Mounter, args, buildS3EnvVars(request.Args)) + } else { + err = mounter.FuseMount(request.Path, request.Mounter, args) + } if err != nil { logger.Error("mount failed: ", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("mount failed: %v", err)}) @@ -202,6 +221,24 @@ func handleCosMount(mounter mounterUtils.MounterUtils, parser MounterArgsParser) } } +// buildS3EnvVars extracts AWS credential file paths from the request args map +// and returns them as env var strings for the mount-s3 subprocess. +// mount-s3 uses the AWS SDK which reads these standard env vars to locate credentials. +func buildS3EnvVars(args json.RawMessage) []string { + var argsMap map[string]string + if err := json.Unmarshal(args, &argsMap); err != nil { + return nil + } + var envVars []string + if v, ok := argsMap["aws-credentials-file"]; ok && v != "" { + envVars = append(envVars, "AWS_SHARED_CREDENTIALS_FILE="+v) + } + if v, ok := argsMap["aws-config-file"]; ok && v != "" { + envVars = append(envVars, "AWS_CONFIG_FILE="+v) + } + return envVars +} + func handleCosUnmount(mounter mounterUtils.MounterUtils) gin.HandlerFunc { return func(c *gin.Context) { var request struct { From e17805fc6c749b4f03c1730366f10a9bcbfa442e Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Mon, 9 Mar 2026 11:07:54 +0530 Subject: [PATCH 028/128] publish 0.12.13 version --- .github/workflows/release.yml | 6 +++--- cos-csi-mounter/Makefile | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9dcb85da..6055092d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.12 + APP_VERSION: 0.12.13 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.12 - name: v0.12.12 + tag_name: v0.12.13 + name: v0.12.13 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index 402d2ee5..8347cf78 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.12 +APP_VERSION := 0.12.13 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin From a1bba307be3c248893e4f925e245eac91f292caa Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Wed, 11 Mar 2026 10:59:54 +0530 Subject: [PATCH 029/128] publish 0.12.14 version --- .github/workflows/release.yml | 6 +++--- cos-csi-mounter/Makefile | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6055092d..17bdfe05 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.13 + APP_VERSION: 0.12.14 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.13 - name: v0.12.13 + tag_name: v0.12.14 + name: v0.12.14 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index 8347cf78..10018624 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.13 +APP_VERSION := 0.12.14 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin From e8c96175e1f03f9f7ed498a2a8a2dbfc86a453e8 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Tue, 17 Mar 2026 11:42:16 +0530 Subject: [PATCH 030/128] publish 0.12.15 version --- .github/workflows/release.yml | 6 +- cos-csi-mounter/Makefile | 2 +- cos-csi-mounter/server/s3mounter.go | 81 +++++++++++++++- pkg/mounter/mounter_s3mounter.go | 139 +++++++++++++++++++++++++++- 4 files changed, 217 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 17bdfe05..eb548c7f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.14 + APP_VERSION: 0.12.15 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.14 - name: v0.12.14 + tag_name: v0.12.15 + name: v0.12.15 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index 10018624..a4fcf9f8 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.14 +APP_VERSION := 0.12.15 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go index 84ddd2b8..2340a3f9 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/s3mounter.go @@ -12,15 +12,29 @@ type s3MounterArgs struct { UID string `json:"uid,omitempty"` GID string `json:"gid,omitempty"` // Removed: UMask β€” mount-s3 has no --umask flag; use --dir-mode/--file-mode via MountOptions - // Removed: Region β€” mount-s3 auto-detects region from AWS_CONFIG_FILE // Removed: AwsConfigDir β€” mount-s3 has no --aws-config-dir flag LogLevel string `json:"log-level,omitempty"` // valid: "debug", "debug-crt", "no-log" EndpointURL string `json:"endpoint-url,omitempty"` + // Region is passed explicitly as --region CLI flag in addition to being written + // in the AWS config file, to ensure it is always set even if AWS_CONFIG_FILE + // env var is not propagated correctly to the mount-s3 subprocess. + Region string `json:"region,omitempty"` // AWS credential file paths β€” NOT CLI flags. // Worker sets these as AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE // env vars on the mount-s3 subprocess before invoking it. AwsCredentialsFile string `json:"aws-credentials-file,omitempty"` AwsConfigFile string `json:"aws-config-file,omitempty"` + + MaxThreads string `json:"max-threads,omitempty"` + ReadPartSize string `json:"read-part-size,omitempty"` + WritePartSize string `json:"write-part-size,omitempty"` + MaxThroughputGbps string `json:"maximum-throughput-gbps,omitempty"` + UploadChecksums string `json:"upload-checksums,omitempty"` + CacheDir string `json:"cache,omitempty"` + MaxCacheSize string `json:"max-cache-size,omitempty"` + MetadataTTL string `json:"metadata-ttl,omitempty"` + NegativeMetadataTTL string `json:"negative-metadata-ttl,omitempty"` + LogMetrics bool `json:"log-metrics,omitempty"` } // PopulateArgsSlice builds the CLI args slice for mount-s3. @@ -58,6 +72,11 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string result = append(result, "--endpoint-url="+args.EndpointURL) } + // --region + if args.Region != "" { + result = append(result, "--region="+args.Region) + } + // Log level: mount-s3 has no --log-level flag. // Map to the supported flags only. switch args.LogLevel { @@ -76,6 +95,57 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string } } + // --- Performance tuning options --- + // Only appended when explicitly set. mount-s3 built-in defaults apply otherwise. + + // --max-threads (default: 16) + if args.MaxThreads != "" { + result = append(result, "--max-threads="+args.MaxThreads) + } + + // --read-part-size (default: 8 MiB) + if args.ReadPartSize != "" { + result = append(result, "--read-part-size="+args.ReadPartSize) + } + + // --write-part-size (default: 8 MiB) + if args.WritePartSize != "" { + result = append(result, "--write-part-size="+args.WritePartSize) + } + + // --maximum-throughput-gbps (default: auto-detect on EC2, 10 Gbps elsewhere) + if args.MaxThroughputGbps != "" { + result = append(result, "--maximum-throughput-gbps="+args.MaxThroughputGbps) + } + + // --upload-checksums (default: crc32c) + if args.UploadChecksums != "" { + result = append(result, "--upload-checksums="+args.UploadChecksums) + } + + // --cache + --max-cache-size (default: disabled) + if args.CacheDir != "" { + result = append(result, "--cache="+args.CacheDir) + if args.MaxCacheSize != "" { + result = append(result, "--max-cache-size="+args.MaxCacheSize) + } + } + + // --metadata-ttl (default: minimal, or 60s if --cache is set) + if args.MetadataTTL != "" { + result = append(result, "--metadata-ttl="+args.MetadataTTL) + } + + // --negative-metadata-ttl (default: same as metadata-ttl) + if args.NegativeMetadataTTL != "" { + result = append(result, "--negative-metadata-ttl="+args.NegativeMetadataTTL) + } + + // --log-metrics (default: false) β€” boolean flag, no value + if args.LogMetrics { + result = append(result, "--log-metrics") + } + return result, nil } @@ -126,5 +196,12 @@ func (args s3MounterArgs) Validate(targetPath string) error { } } + // upload-checksums must be a valid value if set. + // Validated early here so users get a clear error rather than a cryptic mount-s3 failure. + if args.UploadChecksums != "" && args.UploadChecksums != "crc32c" && args.UploadChecksums != "off" { + logger.Error("invalid upload-checksums value", zap.String("value", args.UploadChecksums)) + return fmt.Errorf("invalid upload-checksums value '%s': must be 'crc32c' or 'off'", args.UploadChecksums) + } + return nil -} \ No newline at end of file +} diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 0ab2e1d8..f08dea4d 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -32,6 +32,17 @@ type MountpointS3Mounter struct { ReadOnly bool MountOptions []string MounterUtils utils.MounterUtils + + MaxThreads string // --max-threads , default: 16 + ReadPartSize string // --read-part-size , default: 8388608 (8 MiB). + WritePartSize string // --write-part-size , default: 8388608 (8 MiB). Same rules as ReadPartSize. + MaxThroughputGbps string // --maximum-throughput-gbps , default: auto-detect on EC2, 10 Gbps elsewhere + UploadChecksums string // --upload-checksums , default: crc32c + CacheDir string // --cache , enables local disk read cache, default: disabled + MaxCacheSize string // --max-cache-size , only effective if CacheDir is set, default: preserve 5% of available space + MetadataTTL string // --metadata-ttl , default: minimal (or 60s if --cache is set) + NegativeMetadataTTL string // --negative-metadata-ttl , default: same as MetadataTTL + LogMetrics bool // --log-metrics, enables summarized performance metrics in logs, default: false } // s3MounterArgs holds the args passed to the worker for mount-s3 @@ -44,11 +55,24 @@ type s3MounterArgs struct { // Removed: AwsConfigDir β€” mount-s3 has no --aws-config-dir flag LogLevel string `json:"log-level,omitempty"` // valid: "debug", "debug-crt", "no-log" EndpointURL string `json:"endpoint-url,omitempty"` - // AWS credential file paths β€” NOT CLI flags. - // Worker sets these as AWS_SHARED_CREDENTIALS_FILE / AWS_CONFIG_FILE env vars - // on the mount-s3 subprocess before invoking it. + // Region is passed explicitly as --region CLI flag in addition to being written + // in the AWS config file, to ensure it is always set even if AWS_CONFIG_FILE + // env var is not propagated correctly to the mount-s3 subprocess. + Region string `json:"region,omitempty"` AwsCredentialsFile string `json:"aws-credentials-file,omitempty"` AwsConfigFile string `json:"aws-config-file,omitempty"` + + // Performance tuning fields β€” all optional, mirrors MountpointS3Mounter fields. + MaxThreads string `json:"max-threads,omitempty"` + ReadPartSize string `json:"read-part-size,omitempty"` + WritePartSize string `json:"write-part-size,omitempty"` + MaxThroughputGbps string `json:"maximum-throughput-gbps,omitempty"` + UploadChecksums string `json:"upload-checksums,omitempty"` + CacheDir string `json:"cache,omitempty"` + MaxCacheSize string `json:"max-cache-size,omitempty"` + MetadataTTL string `json:"metadata-ttl,omitempty"` + NegativeMetadataTTL string `json:"negative-metadata-ttl,omitempty"` + LogMetrics bool `json:"log-metrics,omitempty"` } const ( @@ -118,6 +142,40 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounter.ReadOnly = val == "true" } + // Performance tuning β€” all optional. + // If not set here, mount-s3 built-in defaults apply. + // StorageClass mountOptions override these if the same flag appears there. + if val, check = secretMap["maxThreads"]; check { + mounter.MaxThreads = val + } + if val, check = secretMap["readPartSize"]; check { + mounter.ReadPartSize = val + } + if val, check = secretMap["writePartSize"]; check { + mounter.WritePartSize = val + } + if val, check = secretMap["maximumThroughputGbps"]; check { + mounter.MaxThroughputGbps = val + } + if val, check = secretMap["uploadChecksums"]; check { + mounter.UploadChecksums = val + } + if val, check = secretMap["cacheDir"]; check { + mounter.CacheDir = val + } + if val, check = secretMap["maxCacheSize"]; check { + mounter.MaxCacheSize = val + } + if val, check = secretMap["metadataTTL"]; check { + mounter.MetadataTTL = val + } + if val, check = secretMap["negativeMetadataTTL"]; check { + mounter.NegativeMetadataTTL = val + } + if val, check = secretMap["logMetrics"]; check { + mounter.LogMetrics = val == "true" + } + mounter.AuthType = "hmac" klog.Infof("newMountpointS3Mounter args:\n\tbucketName: [%s]\n\tobjectPath: [%s]\n\tendPoint: [%s]\n\tlocationConstraint: [%s]\n\tauthType: [%s]", @@ -337,6 +395,12 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW workerNodeOp.EndpointURL = s3.EndPoint } + // Region β€” passed as CLI flag AND written to config file for redundancy. + if s3.LocConstraint != "" { + nodeServerOp = append(nodeServerOp, "--region="+s3.LocConstraint) + workerNodeOp.Region = s3.LocConstraint + } + // UID if s3.UID != "" { nodeServerOp = append(nodeServerOp, "--uid="+s3.UID) @@ -382,7 +446,72 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW workerNodeOp.ReadOnly = "true" } - // Extra mount options from StorageClass (e.g. --dir-mode=0755, --file-mode=0644) + // --- Performance tuning options --- + if s3.ReadPartSize != "" { + nodeServerOp = append(nodeServerOp, "--read-part-size="+s3.ReadPartSize) + workerNodeOp.ReadPartSize = s3.ReadPartSize + } + + // --write-part-size (default: 8388608 = 8 MiB). Same rules as read-part-size. + if s3.WritePartSize != "" { + nodeServerOp = append(nodeServerOp, "--write-part-size="+s3.WritePartSize) + workerNodeOp.WritePartSize = s3.WritePartSize + } + + // --max-threads (default: 16) + // Increase for workloads with many concurrent file operations. + if s3.MaxThreads != "" { + nodeServerOp = append(nodeServerOp, "--max-threads="+s3.MaxThreads) + workerNodeOp.MaxThreads = s3.MaxThreads + } + + // --maximum-throughput-gbps (default: auto-detect on EC2, 10 Gbps elsewhere) + // Not on EC2 so won't auto-detect correctly β€” set explicitly if you know your node bandwidth. + if s3.MaxThroughputGbps != "" { + nodeServerOp = append(nodeServerOp, "--maximum-throughput-gbps="+s3.MaxThroughputGbps) + workerNodeOp.MaxThroughputGbps = s3.MaxThroughputGbps + } + + // --upload-checksums (default: crc32c) + // Set to "off" to reduce CPU overhead on write-heavy workloads at the cost of integrity checks. + if s3.UploadChecksums != "" { + nodeServerOp = append(nodeServerOp, "--upload-checksums="+s3.UploadChecksums) + workerNodeOp.UploadChecksums = s3.UploadChecksums + } + + // --cache + --max-cache-size (default: disabled) + // Enables local disk read cache β€” improves repeated read performance. + // When cache is set, metadata-ttl defaults to 60s automatically. + if s3.CacheDir != "" { + nodeServerOp = append(nodeServerOp, "--cache="+s3.CacheDir) + workerNodeOp.CacheDir = s3.CacheDir + if s3.MaxCacheSize != "" { + nodeServerOp = append(nodeServerOp, "--max-cache-size="+s3.MaxCacheSize) + workerNodeOp.MaxCacheSize = s3.MaxCacheSize + } + } + + // --metadata-ttl (default: minimal, or 60s if --cache is set) + // Set higher to reduce ListObjects API calls on stable buckets. + if s3.MetadataTTL != "" { + nodeServerOp = append(nodeServerOp, "--metadata-ttl="+s3.MetadataTTL) + workerNodeOp.MetadataTTL = s3.MetadataTTL + } + + // --negative-metadata-ttl (default: same as metadata-ttl) + if s3.NegativeMetadataTTL != "" { + nodeServerOp = append(nodeServerOp, "--negative-metadata-ttl="+s3.NegativeMetadataTTL) + workerNodeOp.NegativeMetadataTTL = s3.NegativeMetadataTTL + } + + // --log-metrics (default: false) + // Logs summarized throughput metrics β€” zero performance cost, useful for monitoring. + if s3.LogMetrics { + nodeServerOp = append(nodeServerOp, "--log-metrics") + workerNodeOp.LogMetrics = true + } + + // StorageClass mountOptions are appended LAST β€” always take final precedence over everything above. nodeServerOp = append(nodeServerOp, s3.MountOptions...) klog.Infof("MountpointS3 nodeServerOp: %v", nodeServerOp) @@ -415,4 +544,4 @@ func removeS3MountConfigFile(configPath, target string) { return } klog.Errorf("removeS3MountConfigFile: Failed to remove config dir after %d attempts", maxRetries) -} \ No newline at end of file +} From cda3f1790fa872b0764ef6557808abf07342d199 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sat, 28 Mar 2026 15:40:06 +0530 Subject: [PATCH 031/128] publish 0.12.16 version --- .github/workflows/release.yml | 6 +- cos-csi-mounter/Makefile | 2 +- cos-csi-mounter/server/s3mounter.go | 62 ++++- cos-csi-mounter/server/utils.go | 26 ++ pkg/mounter/mounter_s3mounter.go | 367 +++++++++++++++++++--------- 5 files changed, 332 insertions(+), 131 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eb548c7f..40deaa16 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.15 + APP_VERSION: 0.12.16 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.15 - name: v0.12.15 + tag_name: v0.12.16 + name: v0.12.16 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index a4fcf9f8..ac8faeed 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.15 +APP_VERSION := 0.12.16 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go index 2340a3f9..f512160b 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/s3mounter.go @@ -7,14 +7,17 @@ import ( ) type s3MounterArgs struct { - ReadOnly string `json:"read-only,omitempty"` - AllowOther string `json:"allow-other,omitempty"` - UID string `json:"uid,omitempty"` - GID string `json:"gid,omitempty"` + ReadOnly string `json:"read-only,omitempty"` + AllowOther string `json:"allow-other,omitempty"` + AllowDelete string `json:"allow-delete,omitempty"` + AllowOverwrite string `json:"allow-overwrite,omitempty"` + UID string `json:"uid,omitempty"` + GID string `json:"gid,omitempty"` // Removed: UMask β€” mount-s3 has no --umask flag; use --dir-mode/--file-mode via MountOptions // Removed: AwsConfigDir β€” mount-s3 has no --aws-config-dir flag - LogLevel string `json:"log-level,omitempty"` // valid: "debug", "debug-crt", "no-log" - EndpointURL string `json:"endpoint-url,omitempty"` + LogLevel string `json:"log-level,omitempty"` // valid: "debug", "debug-crt", "no-log" + LogDirectory string `json:"log-directory,omitempty"` + EndpointURL string `json:"endpoint-url,omitempty"` // Region is passed explicitly as --region CLI flag in addition to being written // in the AWS config file, to ensure it is always set even if AWS_CONFIG_FILE // env var is not propagated correctly to the mount-s3 subprocess. @@ -57,6 +60,16 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string result = append(result, "--read-only") } + // --allow-delete: boolean flag, no value + if args.AllowDelete == "true" { + result = append(result, "--allow-delete") + } + + // --allow-overwrite: boolean flag, no value + if args.AllowOverwrite == "true" { + result = append(result, "--allow-overwrite") + } + // --uid if args.UID != "" { result = append(result, "--uid="+args.UID) @@ -95,6 +108,11 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string } } + // --log-directory + if args.LogDirectory != "" { + result = append(result, "--log-directory="+args.LogDirectory) + } + // --- Performance tuning options --- // Only appended when explicitly set. mount-s3 built-in defaults apply otherwise. @@ -196,6 +214,22 @@ func (args s3MounterArgs) Validate(targetPath string) error { } } + // allow-delete must be a boolean string if set + if args.AllowDelete != "" { + if isBool := isBoolString(args.AllowDelete); !isBool { + logger.Error("cannot convert value of allow-delete into boolean", zap.Any("allow-delete", args.AllowDelete)) + return fmt.Errorf("cannot convert value of allow-delete into boolean: %v", args.AllowDelete) + } + } + + // allow-overwrite must be a boolean string if set + if args.AllowOverwrite != "" { + if isBool := isBoolString(args.AllowOverwrite); !isBool { + logger.Error("cannot convert value of allow-overwrite into boolean", zap.Any("allow-overwrite", args.AllowOverwrite)) + return fmt.Errorf("cannot convert value of allow-overwrite into boolean: %v", args.AllowOverwrite) + } + } + // upload-checksums must be a valid value if set. // Validated early here so users get a clear error rather than a cryptic mount-s3 failure. if args.UploadChecksums != "" && args.UploadChecksums != "crc32c" && args.UploadChecksums != "off" { @@ -203,5 +237,21 @@ func (args s3MounterArgs) Validate(targetPath string) error { return fmt.Errorf("invalid upload-checksums value '%s': must be 'crc32c' or 'off'", args.UploadChecksums) } + // Ensure cache directory exists if specified + if args.CacheDir != "" { + if err := ensureDir(args.CacheDir); err != nil { + logger.Error("failed to create cache directory", zap.String("cache-dir", args.CacheDir), zap.Error(err)) + return fmt.Errorf("failed to create cache directory '%s': %w", args.CacheDir, err) + } + } + + // Ensure log directory exists if specified + if args.LogDirectory != "" { + if err := ensureDir(args.LogDirectory); err != nil { + logger.Error("failed to create log directory", zap.String("log-dir", args.LogDirectory), zap.Error(err)) + return fmt.Errorf("failed to create log directory '%s': %w", args.LogDirectory, err) + } + } + return nil } diff --git a/cos-csi-mounter/server/utils.go b/cos-csi-mounter/server/utils.go index 58c41f28..88a45f6f 100644 --- a/cos-csi-mounter/server/utils.go +++ b/cos-csi-mounter/server/utils.go @@ -125,3 +125,29 @@ func fileExists(path string) (bool, error) { } return !info.IsDir(), nil } + +// ensureDir creates the cache directory if it doesn't exist. +// It creates the directory with 0755 permissions (rwxr-xr-x). +func ensureDir(path string) error { + // Check if directory already exists + info, err := os.Stat(path) + if err == nil { + // Path exists, verify it's a directory + if !info.IsDir() { + return fmt.Errorf("cache path exists but is not a directory: %s", path) + } + return nil + } + + // If error is not "not exist", return it + if !os.IsNotExist(err) { + return fmt.Errorf("failed to stat cache directory: %w", err) + } + + // Directory doesn't exist, create it with parent directories + if err := os.MkdirAll(path, 0755); err != nil { + return fmt.Errorf("failed to create cache directory: %w", err) + } + + return nil +} diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index f08dea4d..5d97a6f9 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -16,26 +16,28 @@ import ( "k8s.io/klog/v2" ) -// MountpointS3Mounter implements Mounter for AWS Mountpoint S3 +// MountpointS3Mounter implements Mounter for AWS Mountpoint S3. type MountpointS3Mounter struct { - BucketName string - ObjectPath string - EndPoint string - LocConstraint string - AuthType string - AccessKey string - SecretKey string - UID string - GID string - UMask string // kept for API compatibility; mount-s3 has no --umask, use --dir-mode/--file-mode via MountOptions - LogLevel string // valid values: "debug", "debug-crt", "no-log"; mount-s3 has no --log-level flag - ReadOnly bool - MountOptions []string - MounterUtils utils.MounterUtils + BucketName string + ObjectPath string + EndPoint string + LocConstraint string + AuthType string + AccessKey string + SecretKey string + UID string + GID string + LogLevel string // valid values: "debug", "debug-crt", "no-log"; mount-s3 has no --log-level flag + LogDirectory string // --log-directory , write logs to directory instead of syslog + ReadOnly bool + AllowDelete bool // --allow-delete, enable delete operations + AllowOverwrite bool // --allow-overwrite, enable overwrite operations + MountOptions []string + MounterUtils utils.MounterUtils MaxThreads string // --max-threads , default: 16 - ReadPartSize string // --read-part-size , default: 8388608 (8 MiB). - WritePartSize string // --write-part-size , default: 8388608 (8 MiB). Same rules as ReadPartSize. + ReadPartSize string // --read-part-size , default: 8388608 (8 MiB) + WritePartSize string // --write-part-size , default: 8388608 (8 MiB) MaxThroughputGbps string // --maximum-throughput-gbps , default: auto-detect on EC2, 10 Gbps elsewhere UploadChecksums string // --upload-checksums , default: crc32c CacheDir string // --cache , enables local disk read cache, default: disabled @@ -45,16 +47,19 @@ type MountpointS3Mounter struct { LogMetrics bool // --log-metrics, enables summarized performance metrics in logs, default: false } -// s3MounterArgs holds the args passed to the worker for mount-s3 +// s3MounterArgs holds the args passed to the worker for mount-s3. type s3MounterArgs struct { ReadOnly string `json:"read-only,omitempty"` AllowOther string `json:"allow-other,omitempty"` UID string `json:"uid,omitempty"` GID string `json:"gid,omitempty"` - // Removed: UMask β€” mount-s3 has no --umask flag + // Removed: UMask β€” mount-s3 has no --umask flag; use --dir-mode/--file-mode via MountOptions // Removed: AwsConfigDir β€” mount-s3 has no --aws-config-dir flag - LogLevel string `json:"log-level,omitempty"` // valid: "debug", "debug-crt", "no-log" - EndpointURL string `json:"endpoint-url,omitempty"` + LogLevel string `json:"log-level,omitempty"` // valid: "debug", "debug-crt", "no-log" + EndpointURL string `json:"endpoint-url,omitempty"` + AllowDelete string `json:"allow-delete,omitempty"` // --allow-delete flag + AllowOverwrite string `json:"allow-overwrite,omitempty"` // --allow-overwrite flag + LogDirectory string `json:"log-directory,omitempty"` // --log-directory flag // Region is passed explicitly as --region CLI flag in addition to being written // in the AWS config file, to ensure it is always set even if AWS_CONFIG_FILE // env var is not propagated correctly to the mount-s3 subprocess. @@ -92,107 +97,215 @@ type envMounter interface { FuseMountWithEnv(path string, comm string, args []string, envVars []string) error } -// NewMountpointS3Mounter creates a new MountpointS3Mounter from secretMap +// NewMountpointS3Mounter creates a new MountpointS3Mounter from the unified +// secret format. +// +// Source mapping: +// +// secretMap β€” credentials and bucket identity only: +// accessKey, secretKey (data, base64-decoded by k8s) +// bucketName, objectPath, cosEndpoint, locationConstraint, +// uid, gid (stringData) +// +// mountOptions β€” all mount-s3 behavioral and performance flags, +// sourced from the secret's `mountOptions` field +// and/or the StorageClass mountOptions. +// Known flags are consumed into struct fields; +// unrecognised flags pass through to mount-s3 as-is. +// mountOptions uid/gid override secretMap uid/gid. func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounterUtils utils.MounterUtils) Mounter { klog.Info("-newMountpointS3Mounter-") - var ( - val string - check bool - mounter = &MountpointS3Mounter{} - ) + mounter := &MountpointS3Mounter{} - if val, check = secretMap["cosEndpoint"]; check { - mounter.EndPoint = val + // --- Credentials (unified secret `data` fields, base64-decoded by k8s) --- + if val, ok := secretMap["accessKey"]; ok { + mounter.AccessKey = val } - if val, check = secretMap["locationConstraint"]; check { - mounter.LocConstraint = val + if val, ok := secretMap["secretKey"]; ok { + mounter.SecretKey = val } - if val, check = secretMap["bucketName"]; check { + + // --- Bucket / endpoint identity (`stringData` fields) --- + if val, ok := secretMap["bucketName"]; ok { mounter.BucketName = val } - if val, check = secretMap["objectPath"]; check { + if val, ok := secretMap["objectPath"]; ok { mounter.ObjectPath = val } - if val, check = secretMap["accessKey"]; check { - mounter.AccessKey = val - } - if val, check = secretMap["secretKey"]; check { - mounter.SecretKey = val + // cosEndpoint and locationConstraint are IBM COS-specific. + // They are not part of the upstream unified secret schema but remain + // supported as secretMap keys for IBM COS compatibility. + if val, ok := secretMap["cosEndpoint"]; ok { + mounter.EndPoint = val } - if val, check = secretMap["uid"]; check { - mounter.UID = val + if val, ok := secretMap["locationConstraint"]; ok { + mounter.LocConstraint = val } - // If gid set but uid not, use gid for uid (same as rclone behavior) + + // --- UID / GID from secretMap --- + // If gid is set but uid is not, mirror gid β†’ uid (matches rclone behavior). + // mountOptions values (parsed below) override these if also present there. if secretMap["gid"] != "" && secretMap["uid"] == "" { mounter.UID = secretMap["gid"] - } else if secretMap["uid"] != "" { - mounter.UID = secretMap["uid"] + } else if val := secretMap["uid"]; val != "" { + mounter.UID = val } - if val, check = secretMap["gid"]; check { + if val := secretMap["gid"]; val != "" { mounter.GID = val } - if val, check = secretMap["umask"]; check { - mounter.UMask = val - } - if val, check = secretMap["logLevel"]; check { - mounter.LogLevel = val - } - if val, check = secretMap["readOnly"]; check { - mounter.ReadOnly = val == "true" - } - - // Performance tuning β€” all optional. - // If not set here, mount-s3 built-in defaults apply. - // StorageClass mountOptions override these if the same flag appears there. - if val, check = secretMap["maxThreads"]; check { - mounter.MaxThreads = val - } - if val, check = secretMap["readPartSize"]; check { - mounter.ReadPartSize = val - } - if val, check = secretMap["writePartSize"]; check { - mounter.WritePartSize = val - } - if val, check = secretMap["maximumThroughputGbps"]; check { - mounter.MaxThroughputGbps = val - } - if val, check = secretMap["uploadChecksums"]; check { - mounter.UploadChecksums = val - } - if val, check = secretMap["cacheDir"]; check { - mounter.CacheDir = val - } - if val, check = secretMap["maxCacheSize"]; check { - mounter.MaxCacheSize = val - } - if val, check = secretMap["metadataTTL"]; check { - mounter.MetadataTTL = val - } - if val, check = secretMap["negativeMetadataTTL"]; check { - mounter.NegativeMetadataTTL = val - } - if val, check = secretMap["logMetrics"]; check { - mounter.LogMetrics = val == "true" - } mounter.AuthType = "hmac" - klog.Infof("newMountpointS3Mounter args:\n\tbucketName: [%s]\n\tobjectPath: [%s]\n\tendPoint: [%s]\n\tlocationConstraint: [%s]\n\tauthType: [%s]", - mounter.BucketName, mounter.ObjectPath, mounter.EndPoint, mounter.LocConstraint, mounter.AuthType) - - mounter.MountOptions = mountOptions + // --- Parse mountOptions into struct fields --- + // Known mount-s3 flags are consumed; unrecognised flags are kept in + // MountOptions for pass-through. This runs last so that mountOptions + // values win over secretMap values for overlapping fields (uid, gid). + mounter.MountOptions = parseMountpointS3Options(mounter, mountOptions) mounter.MounterUtils = mounterUtils + klog.Infof("newMountpointS3Mounter args:\n\tbucketName: [%s]\n\tobjectPath: [%s]\n\tendPoint: [%s]\n\tlocationConstraint: [%s]\n\tauthType: [%s] \n\tmountOptions: [%s]", + mounter.BucketName, mounter.ObjectPath, mounter.EndPoint, mounter.LocConstraint, mounter.AuthType, mounter.MountOptions) + return mounter } -// Mount mounts the S3 bucket using mountpoint-s3 +// parseMountpointS3Options parses mount-s3 options from the provided slice and +// populates recognised flags into the mounter struct. +// +// Rules: +// - Flags may be provided with or without a leading "--" prefix (both accepted). +// - Boolean flags (--read-only, --allow-delete, etc.) need no value. +// - Value flags must be in "flag=value" or "--flag=value" form. +// - "allow-other" is silently consumed β€” formulateMountOptions always adds it. +// - "umask" is consumed with a warning β€” mount-s3 has no --umask flag. +// - Unrecognised flags are returned unchanged for pass-through to mount-s3. +// - mountOptions uid/gid override secretMap uid/gid (caller sets secretMap +// values first; this function runs second). +func parseMountpointS3Options(mounter *MountpointS3Mounter, opts []string) []string { + var remaining []string + + for _, opt := range opts { + // Normalise: strip optional leading "--" so comparisons are uniform. + trimmed := strings.TrimPrefix(opt, "--") + + key, value, hasValue := strings.Cut(trimmed, "=") + + switch key { + + // ------------------------------------------------------------------ + // Always emitted by formulateMountOptions β€” consume to avoid duplicates. + // ------------------------------------------------------------------ + case "allow-other": + // formulateMountOptions unconditionally adds --allow-other; drop here. + + // ------------------------------------------------------------------ + // Unsupported by mount-s3 β€” warn and drop so mount-s3 doesn't fail. + // ------------------------------------------------------------------ + case "umask": + klog.Warningf("parseMountpointS3Options: 'umask' is not supported by mount-s3. " + + "Use --dir-mode / --file-mode via mountOptions in the StorageClass instead. Ignoring.") + + // ------------------------------------------------------------------ + // Boolean mount flags β€” presence means enabled, no value needed. + // ------------------------------------------------------------------ + case "read-only": + mounter.ReadOnly = true + case "allow-delete": + mounter.AllowDelete = true + case "allow-overwrite": + mounter.AllowOverwrite = true + case "log-metrics": + mounter.LogMetrics = true + + // ------------------------------------------------------------------ + // Identity β€” mountOptions values override secretMap values. + // ------------------------------------------------------------------ + case "uid": + if hasValue { + mounter.UID = value + } + case "gid": + if hasValue { + mounter.GID = value + } + + // ------------------------------------------------------------------ + // Logging + // ------------------------------------------------------------------ + case "log-level": + // mount-s3 has no --log-level flag. + // formulateMountOptions maps: "debug" β†’ --debug, + // "debug-crt" β†’ --debug-crt, "no-log" β†’ --no-log. + if hasValue { + mounter.LogLevel = value + } + case "log-directory": + if hasValue { + mounter.LogDirectory = value + } + + // ------------------------------------------------------------------ + // Performance tuning + // ------------------------------------------------------------------ + case "max-threads": + if hasValue { + mounter.MaxThreads = value + } + case "read-part-size": + if hasValue { + mounter.ReadPartSize = value + } + case "write-part-size": + if hasValue { + mounter.WritePartSize = value + } + case "maximum-throughput-gbps": + if hasValue { + mounter.MaxThroughputGbps = value + } + case "upload-checksums": + if hasValue { + mounter.UploadChecksums = value + } + + // ------------------------------------------------------------------ + // Caching + // ------------------------------------------------------------------ + case "cache": + if hasValue { + mounter.CacheDir = value + } + case "max-cache-size": + if hasValue { + mounter.MaxCacheSize = value + } + case "metadata-ttl": + if hasValue { + mounter.MetadataTTL = value + } + case "negative-metadata-ttl": + if hasValue { + mounter.NegativeMetadataTTL = value + } + + // ------------------------------------------------------------------ + // Unrecognised β€” pass through to mount-s3 unchanged. + // ------------------------------------------------------------------ + default: + remaining = append(remaining, opt) + } + } + + return remaining +} + +// Mount mounts the S3 bucket using mountpoint-s3. func (s3 *MountpointS3Mounter) Mount(source string, target string) error { klog.Info("-MountpointS3Mounter Mount-") klog.Infof("Mount args:\n\tsource: <%s>\n\ttarget: <%s>", source, target) - // Determine config path based on mode + // Determine config path based on mode. var configPath string if mountWorker { configPath = constants.MounterConfigPathOnHost @@ -200,16 +313,16 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { configPath = constants.MounterConfigPathOnPodS3Mount } - // Create unique config dir per volume + // Create unique config dir per volume. configPathWithVolID := path.Join(configPath, fmt.Sprintf("%x", sha256.Sum256([]byte(target)))) - // Write AWS credentials and config files + // Write AWS credentials and config files. if err := createS3MountConfig(configPathWithVolID, s3); err != nil { klog.Errorf("MountpointS3Mounter Mount: Cannot create config file: %v", err) return err } - // Build bucket path with optional object path prefix + // Build bucket path with optional object path prefix. bucketName := s3.BucketName if s3.ObjectPath != "" { trimmedPath := strings.TrimPrefix(s3.ObjectPath, "/") @@ -252,7 +365,7 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { return s3.MounterUtils.FuseMount(target, constants.MountpointS3BinaryPath, args) } -// Unmount unmounts the S3 bucket +// Unmount unmounts the S3 bucket. func (s3 *MountpointS3Mounter) Unmount(target string) error { klog.Info("-MountpointS3Mounter Unmount-") @@ -290,7 +403,7 @@ func (s3 *MountpointS3Mounter) Unmount(target string) error { // credentials <- AWS credentials (access/secret key) // config <- AWS config (region, endpoint_url) func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) error { - // Create the config directory + // Create the config directory. if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 -- directory is created under a controlled path using SHA256 hash, not user input klog.Errorf("MountpointS3Mounter: Cannot create config dir %s: %v", configPathWithVolID, err) return err @@ -335,7 +448,7 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er return nil } -// writeConfigFile writes lines to a file with 0600 permissions +// writeConfigFile writes lines to a file with 0600 permissions. func writeConfigFile(filePath string, lines []string) error { f, err := CreateFile(filePath) // #nosec G304 -- filePath is constructed internally from a SHA256 hash and constant config dir, not from user input if err != nil { @@ -389,7 +502,7 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW AwsConfigFile: cfgFilePath, } - // Endpoint (IBM COS endpoint) + // Endpoint (IBM COS endpoint). if s3.EndPoint != "" { nodeServerOp = append(nodeServerOp, "--endpoint-url="+s3.EndPoint) workerNodeOp.EndpointURL = s3.EndPoint @@ -401,27 +514,20 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW workerNodeOp.Region = s3.LocConstraint } - // UID + // UID. if s3.UID != "" { nodeServerOp = append(nodeServerOp, "--uid="+s3.UID) workerNodeOp.UID = s3.UID } - // GID + // GID. if s3.GID != "" { nodeServerOp = append(nodeServerOp, "--gid="+s3.GID) workerNodeOp.GID = s3.GID } - // UMask: mount-s3 has no --umask flag. - // Use --dir-mode / --file-mode via MountOptions in the StorageClass instead. - if s3.UMask != "" { - klog.Warningf("MountpointS3Mounter: 'umask=%s' is set but mount-s3 does not support --umask. "+ - "Use --dir-mode / --file-mode via mountOptions in the StorageClass instead.", s3.UMask) - } - // Log level: mount-s3 has no --log-level flag. - // Valid mappings: "debug" β†’ --debug, "debug-crt" β†’ --debug-crt, "no-log" β†’ --no-log + // Valid mappings: "debug" β†’ --debug, "debug-crt" β†’ --debug-crt, "no-log" β†’ --no-log. // Anything else (e.g. "warn", "info") is ignored β€” mount-s3 default logging applies. switch s3.LogLevel { case "debug": @@ -440,46 +546,65 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW } } - // Read-only + // Read-only. if s3.ReadOnly { nodeServerOp = append(nodeServerOp, "--read-only") workerNodeOp.ReadOnly = "true" } + // Allow delete. + if s3.AllowDelete { + nodeServerOp = append(nodeServerOp, "--allow-delete") + workerNodeOp.AllowDelete = "true" + } + + // Allow overwrite. + if s3.AllowOverwrite { + nodeServerOp = append(nodeServerOp, "--allow-overwrite") + workerNodeOp.AllowOverwrite = "true" + } + + // Log directory. + if s3.LogDirectory != "" { + nodeServerOp = append(nodeServerOp, "--log-directory="+s3.LogDirectory) + workerNodeOp.LogDirectory = s3.LogDirectory + } + // --- Performance tuning options --- + + // --read-part-size (default: 8 MiB). if s3.ReadPartSize != "" { nodeServerOp = append(nodeServerOp, "--read-part-size="+s3.ReadPartSize) workerNodeOp.ReadPartSize = s3.ReadPartSize } - // --write-part-size (default: 8388608 = 8 MiB). Same rules as read-part-size. + // --write-part-size (default: 8 MiB). if s3.WritePartSize != "" { nodeServerOp = append(nodeServerOp, "--write-part-size="+s3.WritePartSize) workerNodeOp.WritePartSize = s3.WritePartSize } - // --max-threads (default: 16) - // Increase for workloads with many concurrent file operations. + // --max-threads (default: 16). if s3.MaxThreads != "" { nodeServerOp = append(nodeServerOp, "--max-threads="+s3.MaxThreads) workerNodeOp.MaxThreads = s3.MaxThreads } - // --maximum-throughput-gbps (default: auto-detect on EC2, 10 Gbps elsewhere) + // --maximum-throughput-gbps (default: auto-detect on EC2, 10 Gbps elsewhere). // Not on EC2 so won't auto-detect correctly β€” set explicitly if you know your node bandwidth. if s3.MaxThroughputGbps != "" { nodeServerOp = append(nodeServerOp, "--maximum-throughput-gbps="+s3.MaxThroughputGbps) workerNodeOp.MaxThroughputGbps = s3.MaxThroughputGbps } - // --upload-checksums (default: crc32c) + // --upload-checksums (default: crc32c). // Set to "off" to reduce CPU overhead on write-heavy workloads at the cost of integrity checks. if s3.UploadChecksums != "" { nodeServerOp = append(nodeServerOp, "--upload-checksums="+s3.UploadChecksums) workerNodeOp.UploadChecksums = s3.UploadChecksums } - // --cache + --max-cache-size (default: disabled) + // --cache + --max-cache-size (default: disabled). // Enables local disk read cache β€” improves repeated read performance. // When cache is set, metadata-ttl defaults to 60s automatically. if s3.CacheDir != "" { @@ -491,20 +616,20 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW } } - // --metadata-ttl (default: minimal, or 60s if --cache is set) + // --metadata-ttl (default: minimal, or 60s if --cache is set). // Set higher to reduce ListObjects API calls on stable buckets. if s3.MetadataTTL != "" { nodeServerOp = append(nodeServerOp, "--metadata-ttl="+s3.MetadataTTL) workerNodeOp.MetadataTTL = s3.MetadataTTL } - // --negative-metadata-ttl (default: same as metadata-ttl) + // --negative-metadata-ttl (default: same as metadata-ttl). if s3.NegativeMetadataTTL != "" { nodeServerOp = append(nodeServerOp, "--negative-metadata-ttl="+s3.NegativeMetadataTTL) workerNodeOp.NegativeMetadataTTL = s3.NegativeMetadataTTL } - // --log-metrics (default: false) + // --log-metrics (default: false). // Logs summarized throughput metrics β€” zero performance cost, useful for monitoring. if s3.LogMetrics { nodeServerOp = append(nodeServerOp, "--log-metrics") @@ -520,7 +645,7 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW return nodeServerOp, envVars, workerNodeOp } -// removeS3MountConfigFile removes the config directory for the volume (same pattern as rclone) +// removeS3MountConfigFile removes the config directory for the volume (same pattern as rclone). func removeS3MountConfigFile(configPath, target string) { configPathWithVolID := path.Join(configPath, fmt.Sprintf("%x", sha256.Sum256([]byte(target)))) From 70a978dec20d11961f2256bf916f823b2cda088f Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sat, 28 Mar 2026 16:14:33 +0530 Subject: [PATCH 032/128] publish 0.12.16 version --- cos-csi-mounter/server/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cos-csi-mounter/server/utils.go b/cos-csi-mounter/server/utils.go index 88a45f6f..48915dbc 100644 --- a/cos-csi-mounter/server/utils.go +++ b/cos-csi-mounter/server/utils.go @@ -145,7 +145,7 @@ func ensureDir(path string) error { } // Directory doesn't exist, create it with parent directories - if err := os.MkdirAll(path, 0755); err != nil { + if err := os.MkdirAll(path, 0750); err != nil { // #nosec G301 -- cache directory permissions set to 0750 for security return fmt.Errorf("failed to create cache directory: %w", err) } From 6057cb87a592bdf767d9c2f0e5b26b9ccfc678f0 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sun, 29 Mar 2026 11:43:07 +0530 Subject: [PATCH 033/128] publish 0.12.17 version --- .github/workflows/release.yml | 220 +++++++++++++++---------------- cos-csi-mounter/Makefile | 2 +- pkg/mounter/mounter_s3mounter.go | 74 ++++++++--- 3 files changed, 165 insertions(+), 131 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 40deaa16..239307d1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,10 +3,10 @@ name: CI & Release on: push: branches: - - aws-s3mounter + - aws-s3mounter pull_request: branches: - - aws-s3mounter + - aws-s3mounter concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -17,88 +17,88 @@ jobs: runs-on: ubuntu-latest outputs: should_release: ${{ steps.check_release.outputs.should_release }} - + steps: - - name: Checkout Code - uses: actions/checkout@v5 - - - name: Print Go Version - run: go version - - - name: Get last commit message - id: check_commit - run: | - message=$(git log -1 --pretty=%B) - message="${message//'%'/'%25'}" # Escape '%' - message="${message//$'\n'/'%0A'}" # Escape newlines - message="${message//$'\r'/'%0D'}" # Escape carriage returns - echo "message=$message" >> "$GITHUB_OUTPUT" - shell: bash - - - name: Check Commit Message - run: | - echo "Commit Message: ${{ steps.check_commit.outputs.message }}" - - - name: Check if release should be triggered - id: check_release - run: | - if [[ "${{ steps.check_commit.outputs.message }}" == *"publish"* ]]; then - echo "should_release=true" >> "$GITHUB_OUTPUT" - echo "Debian/RPM packages will be created" - else - echo "should_release=false" >> "$GITHUB_OUTPUT" - echo "Debian/RPM packages will not be created" - fi + - name: Checkout Code + uses: actions/checkout@v5 + + - name: Print Go Version + run: go version + + - name: Get last commit message + id: check_commit + run: | + message=$(git log -1 --pretty=%B) + message="${message//'%'/'%25'}" # Escape '%' + message="${message//$'\n'/'%0A'}" # Escape newlines + message="${message//$'\r'/'%0D'}" # Escape carriage returns + echo "message=$message" >> "$GITHUB_OUTPUT" + shell: bash + + - name: Check Commit Message + run: | + echo "Commit Message: ${{ steps.check_commit.outputs.message }}" + + - name: Check if release should be triggered + id: check_release + run: | + if [[ "${{ steps.check_commit.outputs.message }}" == *"publish"* ]]; then + echo "should_release=true" >> "$GITHUB_OUTPUT" + echo "Debian/RPM packages will be created" + else + echo "should_release=false" >> "$GITHUB_OUTPUT" + echo "Debian/RPM packages will not be created" + fi build-and-test: needs: common-setup runs-on: ubuntu-latest permissions: - contents: write # needed to push coverage to gh-pages + contents: write # needed to push coverage to gh-pages pull-requests: write # needed to comment on PRs steps: - - name: Checkout code - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Install system dependencies (bc + docker) - run: | - sudo apt-get update -qq - sudo apt-get install -y bc docker-ce-cli - - - name: Prepare environment - run: | - export GO111MODULE=on - go mod tidy - make deps - - - name: Check code formatting - run: make fmt - - - name: Run unit tests - run: make test - - - name: Generate coverage - run: | - make coverage - ./scripts/calculateCoverage.sh - touch "Passing" || touch "Failed" - - - name: Build driver image - run: | - make driver - env: - RHSM_USER: ${{ secrets.RHSM_USER }} - RHSM_PASS: ${{ secrets.RHSM_PASS }} - - - name: Publish coverage - if: success() - env: - GHE_TOKEN: ${{ secrets.GHE_TOKEN }} - run: | - ./scripts/publishCoverage.sh + - name: Checkout code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Install system dependencies (bc + docker) + run: | + sudo apt-get update -qq + sudo apt-get install -y bc docker-ce-cli + + - name: Prepare environment + run: | + export GO111MODULE=on + go mod tidy + make deps + + - name: Check code formatting + run: make fmt + + - name: Run unit tests + run: make test + + - name: Generate coverage + run: | + make coverage + ./scripts/calculateCoverage.sh + touch "Passing" || touch "Failed" + + - name: Build driver image + run: | + make driver + env: + RHSM_USER: ${{ secrets.RHSM_USER }} + RHSM_PASS: ${{ secrets.RHSM_PASS }} + + - name: Publish coverage + if: success() + env: + GHE_TOKEN: ${{ secrets.GHE_TOKEN }} + run: | + ./scripts/publishCoverage.sh release: needs: common-setup @@ -109,43 +109,43 @@ jobs: strategy: matrix: package_dir: - - cos-csi-mounter + - cos-csi-mounter env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.16 + APP_VERSION: 0.12.17 steps: - - name: Checkout Code - uses: actions/checkout@v5 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: "go" - - - name: Build Debian and RPM packages for cos-csi-mounter systemd service - run: | - cd ${{ matrix.package_dir }} - make packages - - - name: Latest Version (Tag and Release) - if: success() - id: release - uses: softprops/action-gh-release@v2 - with: - files: | - /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz - /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 - /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz - /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.16 - name: v0.12.16 - body: | - ## πŸš€ What’s New - - Testing aws-s3Mounter image - - Publishing new test version - prerelease: ${{ env.IS_LATEST_RELEASE != 'true' }} - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + - name: Checkout Code + uses: actions/checkout@v5 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: "go" + + - name: Build Debian and RPM packages for cos-csi-mounter systemd service + run: | + cd ${{ matrix.package_dir }} + make packages + + - name: Latest Version (Tag and Release) + if: success() + id: release + uses: softprops/action-gh-release@v2 + with: + files: | + /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz + /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 + /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz + /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 + tag_name: v0.12.17 + name: v0.12.17 + body: | + ## πŸš€ What’s New + - Testing aws-s3Mounter image + - Publishing new test version + prerelease: ${{ env.IS_LATEST_RELEASE != 'true' }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index ac8faeed..95169d3c 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.16 +APP_VERSION := 0.12.17 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 5d97a6f9..afdaf438 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -100,19 +100,17 @@ type envMounter interface { // NewMountpointS3Mounter creates a new MountpointS3Mounter from the unified // secret format. // -// Source mapping: +// Source mapping and precedence (lowest β†’ highest): // -// secretMap β€” credentials and bucket identity only: -// accessKey, secretKey (data, base64-decoded by k8s) -// bucketName, objectPath, cosEndpoint, locationConstraint, -// uid, gid (stringData) +// 1. secretMap identity keys β€” accessKey, secretKey, bucketName, objectPath, +// cosEndpoint, locationConstraint, uid, gid +// 2. StorageClass mountOptions (mountOptions []string arg) β€” parsed first +// 3. Secret mountOptions (secretMap["mountOptions"] multiline string) β€” parsed +// second and therefore wins over StorageClass on any overlapping flag // -// mountOptions β€” all mount-s3 behavioral and performance flags, -// sourced from the secret's `mountOptions` field -// and/or the StorageClass mountOptions. -// Known flags are consumed into struct fields; -// unrecognised flags pass through to mount-s3 as-is. -// mountOptions uid/gid override secretMap uid/gid. +// For passthrough flags (unrecognised by the parser), the same precedence +// applies: secret passthroughs are appended after SC passthroughs, so +// mount-s3 sees the secret value last (last-value-wins semantics). func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounterUtils utils.MounterUtils) Mounter { klog.Info("-newMountpointS3Mounter-") @@ -143,9 +141,9 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounter.LocConstraint = val } - // --- UID / GID from secretMap --- + // --- UID / GID from secretMap (lowest precedence for these fields) --- // If gid is set but uid is not, mirror gid β†’ uid (matches rclone behavior). - // mountOptions values (parsed below) override these if also present there. + // Both can be overridden by mountOptions below. if secretMap["gid"] != "" && secretMap["uid"] == "" { mounter.UID = secretMap["gid"] } else if val := secretMap["uid"]; val != "" { @@ -157,19 +155,55 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounter.AuthType = "hmac" - // --- Parse mountOptions into struct fields --- - // Known mount-s3 flags are consumed; unrecognised flags are kept in - // MountOptions for pass-through. This runs last so that mountOptions - // values win over secretMap values for overlapping fields (uid, gid). - mounter.MountOptions = parseMountpointS3Options(mounter, mountOptions) + // --- Merge and parse mountOptions --- + // + // Step 1: Parse StorageClass mountOptions (the []string arg). + // These come in already split by the kubelet. + scRemainder := parseMountpointS3Options(mounter, mountOptions) + + // Step 2: Parse the secret's mountOptions multiline string. + // splitSecretMountOptions handles blank lines and # comments. + // Secret options are parsed AFTER SC options so they win on overlap. + secretOpts := splitSecretMountOptions(secretMap["mountOptions"]) + secretRemainder := parseMountpointS3Options(mounter, secretOpts) + + // Step 3: Build the final passthrough slice. + // SC passthroughs come first; secret passthroughs come last so + // mount-s3 sees the secret value last (last-value-wins). + mounter.MountOptions = append(scRemainder, secretRemainder...) mounter.MounterUtils = mounterUtils - klog.Infof("newMountpointS3Mounter args:\n\tbucketName: [%s]\n\tobjectPath: [%s]\n\tendPoint: [%s]\n\tlocationConstraint: [%s]\n\tauthType: [%s] \n\tmountOptions: [%s]", - mounter.BucketName, mounter.ObjectPath, mounter.EndPoint, mounter.LocConstraint, mounter.AuthType, mounter.MountOptions) + klog.Infof("newMountpointS3Mounter args:\n\tbucketName: [%s]\n\tobjectPath: [%s]\n\tendPoint: [%s]\n\tlocationConstraint: [%s]\n\tauthType: [%s]", + mounter.BucketName, mounter.ObjectPath, mounter.EndPoint, mounter.LocConstraint, mounter.AuthType) + klog.Infof("newMountpointS3Mounter: SC mountOptions=%v secretMountOptions=%v passthrough=%v", + mountOptions, secretOpts, mounter.MountOptions) return mounter } +// splitSecretMountOptions splits the multiline mountOptions string from the +// secret into a []string suitable for parseMountpointS3Options. +// +// Rules: +// - Each non-empty line is one flag (same as StorageClass mountOptions). +// - Lines beginning with '#' (after trimming) are comments β€” skipped. +// - Leading/trailing whitespace on each line is trimmed. +// - Blank lines are skipped. +func splitSecretMountOptions(raw string) []string { + if raw == "" { + return nil + } + var opts []string + for _, line := range strings.Split(raw, "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + opts = append(opts, line) + } + return opts +} + // parseMountpointS3Options parses mount-s3 options from the provided slice and // populates recognised flags into the mounter struct. // From ff176c60ee743c1159be7b1760936b0c4c21d0ad Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sun, 29 Mar 2026 22:34:36 +0530 Subject: [PATCH 034/128] publish 0.12.18 version --- .github/workflows/release.yml | 6 +++--- cos-csi-mounter/Makefile | 2 +- cos-csi-mounter/server/server.go | 30 ++++++++++++++++++++---------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 239307d1..b6a9fbee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.17 + APP_VERSION: 0.12.18 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.17 - name: v0.12.17 + tag_name: v0.12.18 + name: v0.12.18 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index 95169d3c..a085bbbf 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.17 +APP_VERSION := 0.12.18 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin diff --git a/cos-csi-mounter/server/server.go b/cos-csi-mounter/server/server.go index 82b62af3..ff39cb94 100644 --- a/cos-csi-mounter/server/server.go +++ b/cos-csi-mounter/server/server.go @@ -106,7 +106,6 @@ func handleSignals() { <-signals socketPath := filepath.Join(constants.SocketDir, constants.SocketFile) if err := os.Remove(socketPath); err != nil { - // Handle it properly: log it, retry, return, etc. logger.Warn("Failed to remove socket on exit", zap.String("path", socketPath), zap.Error(err)) } os.Exit(0) @@ -130,7 +129,6 @@ func startService(setupSocketFunc func() (net.Listener, error), router http.Hand logger.Error("Failed to create socket", zap.Error(err)) return err } - // Close the listener at the end defer func() { if err := listener.Close(); err != nil { fmt.Fprintf(os.Stderr, "failed to close listener: %v\n", err) @@ -141,7 +139,6 @@ func startService(setupSocketFunc func() (net.Listener, error), router http.Hand logger.Info("Starting cos-csi-mounter service...") - // Serve HTTP requests over Unix socket server := &http.Server{ Handler: router, ReadHeaderTimeout: 3 * time.Second, @@ -194,7 +191,7 @@ func handleCosMount(mounter mounterUtils.MounterUtils, parser MounterArgsParser) return } - // validate mounter args + // validate and parse mounter args args, err := parser.Parse(request) if err != nil { logger.Error("failed to parse mounter args", zap.Any("mounter", request.Mounter), zap.Error(err)) @@ -224,17 +221,30 @@ func handleCosMount(mounter mounterUtils.MounterUtils, parser MounterArgsParser) // buildS3EnvVars extracts AWS credential file paths from the request args map // and returns them as env var strings for the mount-s3 subprocess. // mount-s3 uses the AWS SDK which reads these standard env vars to locate credentials. +// +// IMPORTANT: must use map[string]interface{} β€” NOT map[string]string. +// The args payload contains mixed types (e.g. "log-metrics": true is a bool, +// not a string). Unmarshalling into map[string]string silently fails on any +// bool or numeric field, returning nil and dropping both credential env vars, +// which causes mount-s3 to fail with NoSigningCredentials. func buildS3EnvVars(args json.RawMessage) []string { - var argsMap map[string]string + var argsMap map[string]interface{} if err := json.Unmarshal(args, &argsMap); err != nil { + logger.Error("buildS3EnvVars: failed to unmarshal args, credential env vars will be missing", + zap.Error(err)) return nil } + var envVars []string - if v, ok := argsMap["aws-credentials-file"]; ok && v != "" { - envVars = append(envVars, "AWS_SHARED_CREDENTIALS_FILE="+v) + if v, ok := argsMap["aws-credentials-file"]; ok { + if s, ok := v.(string); ok && s != "" { + envVars = append(envVars, "AWS_SHARED_CREDENTIALS_FILE="+s) + } } - if v, ok := argsMap["aws-config-file"]; ok && v != "" { - envVars = append(envVars, "AWS_CONFIG_FILE="+v) + if v, ok := argsMap["aws-config-file"]; ok { + if s, ok := v.(string); ok && s != "" { + envVars = append(envVars, "AWS_CONFIG_FILE="+s) + } } return envVars } @@ -263,4 +273,4 @@ func handleCosUnmount(mounter mounterUtils.MounterUtils) gin.HandlerFunc { logger.Info("bucket unmount is successful", zap.Any("path", request.Path)) c.JSON(http.StatusOK, gin.H{"status": "success"}) } -} +} \ No newline at end of file From 4163f7353758159aa94c4e625f6dd0a13990f184 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sun, 5 Apr 2026 18:23:43 +0530 Subject: [PATCH 035/128] publish 0.12.19 version --- .github/workflows/release.yml | 6 +- cos-csi-mounter/Makefile | 2 +- cos-csi-mounter/server/s3mounter.go | 144 +++----- pkg/mounter/mounter_s3mounter.go | 495 +++++++++++++--------------- 4 files changed, 284 insertions(+), 363 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b6a9fbee..70d76a2a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.18 + APP_VERSION: 0.12.19 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.18 - name: v0.12.18 + tag_name: v0.12.19 + name: v0.12.19 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index a085bbbf..45d2435e 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.18 +APP_VERSION := 0.12.19 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go index f512160b..77bf07ec 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/s3mounter.go @@ -6,38 +6,33 @@ import ( "go.uber.org/zap" ) +// s3MounterArgs holds the args sent from the node server for mount-s3. +// This struct must match the s3MounterArgs in pkg/mounter/mounter_s3mounter.go type s3MounterArgs struct { - ReadOnly string `json:"read-only,omitempty"` - AllowOther string `json:"allow-other,omitempty"` - AllowDelete string `json:"allow-delete,omitempty"` - AllowOverwrite string `json:"allow-overwrite,omitempty"` - UID string `json:"uid,omitempty"` - GID string `json:"gid,omitempty"` - // Removed: UMask β€” mount-s3 has no --umask flag; use --dir-mode/--file-mode via MountOptions - // Removed: AwsConfigDir β€” mount-s3 has no --aws-config-dir flag - LogLevel string `json:"log-level,omitempty"` // valid: "debug", "debug-crt", "no-log" - LogDirectory string `json:"log-directory,omitempty"` - EndpointURL string `json:"endpoint-url,omitempty"` - // Region is passed explicitly as --region CLI flag in addition to being written - // in the AWS config file, to ensure it is always set even if AWS_CONFIG_FILE - // env var is not propagated correctly to the mount-s3 subprocess. - Region string `json:"region,omitempty"` - // AWS credential file paths β€” NOT CLI flags. - // Worker sets these as AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE - // env vars on the mount-s3 subprocess before invoking it. + // Always set. + AllowOther string `json:"allow-other,omitempty"` AwsCredentialsFile string `json:"aws-credentials-file,omitempty"` AwsConfigFile string `json:"aws-config-file,omitempty"` - MaxThreads string `json:"max-threads,omitempty"` - ReadPartSize string `json:"read-part-size,omitempty"` - WritePartSize string `json:"write-part-size,omitempty"` - MaxThroughputGbps string `json:"maximum-throughput-gbps,omitempty"` - UploadChecksums string `json:"upload-checksums,omitempty"` - CacheDir string `json:"cache,omitempty"` - MaxCacheSize string `json:"max-cache-size,omitempty"` - MetadataTTL string `json:"metadata-ttl,omitempty"` - NegativeMetadataTTL string `json:"negative-metadata-ttl,omitempty"` - LogMetrics bool `json:"log-metrics,omitempty"` + // SecretMap-sourced. + EndpointURL string `json:"endpoint-url,omitempty"` + Region string `json:"region,omitempty"` + + // Identity. + UID string `json:"uid,omitempty"` + GID string `json:"gid,omitempty"` + + // Logging (translated from LogLevel). + LogLevel string `json:"log-level,omitempty"` + LogDirectory string `json:"log-directory,omitempty"` + + // Cache. + CacheDir string `json:"cache,omitempty"` + + // Write operations - conflict resolution applied when read-only is set. + ReadOnly string `json:"read-only,omitempty"` + AllowOverwrite string `json:"allow-overwrite,omitempty"` + IncrementalUpload string `json:"incremental-upload,omitempty"` } // PopulateArgsSlice builds the CLI args slice for mount-s3. @@ -50,6 +45,20 @@ type s3MounterArgs struct { func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string, error) { result := []string{bucket, targetPath} + // --- Read-only priority resolution --- + // If read-only is set, clear all write-related flags to ensure clean read-only mount. + // This prevents conflicts where both read-only and write flags are set. + if args.ReadOnly == "true" { + if args.AllowOverwrite == "true" { + logger.Warn("read-only is set, clearing allow-overwrite") + args.AllowOverwrite = "" + } + if args.IncrementalUpload == "true" { + logger.Warn("read-only is set, clearing incremental-upload") + args.IncrementalUpload = "" + } + } + // --allow-other: boolean flag, no value if args.AllowOther == "true" { result = append(result, "--allow-other") @@ -60,16 +69,16 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string result = append(result, "--read-only") } - // --allow-delete: boolean flag, no value - if args.AllowDelete == "true" { - result = append(result, "--allow-delete") - } - - // --allow-overwrite: boolean flag, no value + // --allow-overwrite: boolean flag, no value (cleared if read-only is set) if args.AllowOverwrite == "true" { result = append(result, "--allow-overwrite") } + // --incremental-upload: boolean flag, no value (cleared if read-only is set) + if args.IncrementalUpload == "true" { + result = append(result, "--incremental-upload") + } + // --uid if args.UID != "" { result = append(result, "--uid="+args.UID) @@ -113,55 +122,9 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string result = append(result, "--log-directory="+args.LogDirectory) } - // --- Performance tuning options --- - // Only appended when explicitly set. mount-s3 built-in defaults apply otherwise. - - // --max-threads (default: 16) - if args.MaxThreads != "" { - result = append(result, "--max-threads="+args.MaxThreads) - } - - // --read-part-size (default: 8 MiB) - if args.ReadPartSize != "" { - result = append(result, "--read-part-size="+args.ReadPartSize) - } - - // --write-part-size (default: 8 MiB) - if args.WritePartSize != "" { - result = append(result, "--write-part-size="+args.WritePartSize) - } - - // --maximum-throughput-gbps (default: auto-detect on EC2, 10 Gbps elsewhere) - if args.MaxThroughputGbps != "" { - result = append(result, "--maximum-throughput-gbps="+args.MaxThroughputGbps) - } - - // --upload-checksums (default: crc32c) - if args.UploadChecksums != "" { - result = append(result, "--upload-checksums="+args.UploadChecksums) - } - - // --cache + --max-cache-size (default: disabled) + // --cache if args.CacheDir != "" { result = append(result, "--cache="+args.CacheDir) - if args.MaxCacheSize != "" { - result = append(result, "--max-cache-size="+args.MaxCacheSize) - } - } - - // --metadata-ttl (default: minimal, or 60s if --cache is set) - if args.MetadataTTL != "" { - result = append(result, "--metadata-ttl="+args.MetadataTTL) - } - - // --negative-metadata-ttl (default: same as metadata-ttl) - if args.NegativeMetadataTTL != "" { - result = append(result, "--negative-metadata-ttl="+args.NegativeMetadataTTL) - } - - // --log-metrics (default: false) β€” boolean flag, no value - if args.LogMetrics { - result = append(result, "--log-metrics") } return result, nil @@ -214,14 +177,6 @@ func (args s3MounterArgs) Validate(targetPath string) error { } } - // allow-delete must be a boolean string if set - if args.AllowDelete != "" { - if isBool := isBoolString(args.AllowDelete); !isBool { - logger.Error("cannot convert value of allow-delete into boolean", zap.Any("allow-delete", args.AllowDelete)) - return fmt.Errorf("cannot convert value of allow-delete into boolean: %v", args.AllowDelete) - } - } - // allow-overwrite must be a boolean string if set if args.AllowOverwrite != "" { if isBool := isBoolString(args.AllowOverwrite); !isBool { @@ -230,11 +185,12 @@ func (args s3MounterArgs) Validate(targetPath string) error { } } - // upload-checksums must be a valid value if set. - // Validated early here so users get a clear error rather than a cryptic mount-s3 failure. - if args.UploadChecksums != "" && args.UploadChecksums != "crc32c" && args.UploadChecksums != "off" { - logger.Error("invalid upload-checksums value", zap.String("value", args.UploadChecksums)) - return fmt.Errorf("invalid upload-checksums value '%s': must be 'crc32c' or 'off'", args.UploadChecksums) + // incremental-upload must be a boolean string if set + if args.IncrementalUpload != "" { + if isBool := isBoolString(args.IncrementalUpload); !isBool { + logger.Error("cannot convert value of incremental-upload into boolean", zap.Any("incremental-upload", args.IncrementalUpload)) + return fmt.Errorf("cannot convert value of incremental-upload into boolean: %v", args.IncrementalUpload) + } } // Ensure cache directory exists if specified diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index afdaf438..c526e335 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -17,67 +17,91 @@ import ( ) // MountpointS3Mounter implements Mounter for AWS Mountpoint S3. +// +// Only flags that require code-level handling are modelled as struct fields: +// - Conflict resolution : ReadOnly takes priority β€” clears AllowOverwrite / IncrementalUpload +// - Directory validation: LogDirectory / CacheDir (must exist before mount) +// - Name translation : LogLevel ("debug" β†’ --debug, not --log-level=debug) +// - Special precedence : UID / GID (secretMap < mountOptions) +// - Always-emitted : allow-other (deduplicated here) +// - Invalid flag : umask (warn + drop) +// - SecretMap-sourced : EndPoint / LocConstraint (injected from secret fields) +// +// All other flags are the user's responsibility and pass through unchanged to +// mount-s3 via MountOptions. The parser ensures every passthrough entry has a +// "--" prefix so mount-s3 always receives syntactically valid flags. type MountpointS3Mounter struct { - BucketName string - ObjectPath string - EndPoint string - LocConstraint string - AuthType string - AccessKey string - SecretKey string - UID string - GID string - LogLevel string // valid values: "debug", "debug-crt", "no-log"; mount-s3 has no --log-level flag - LogDirectory string // --log-directory , write logs to directory instead of syslog - ReadOnly bool - AllowDelete bool // --allow-delete, enable delete operations - AllowOverwrite bool // --allow-overwrite, enable overwrite operations - MountOptions []string - MounterUtils utils.MounterUtils - - MaxThreads string // --max-threads , default: 16 - ReadPartSize string // --read-part-size , default: 8388608 (8 MiB) - WritePartSize string // --write-part-size , default: 8388608 (8 MiB) - MaxThroughputGbps string // --maximum-throughput-gbps , default: auto-detect on EC2, 10 Gbps elsewhere - UploadChecksums string // --upload-checksums , default: crc32c - CacheDir string // --cache , enables local disk read cache, default: disabled - MaxCacheSize string // --max-cache-size , only effective if CacheDir is set, default: preserve 5% of available space - MetadataTTL string // --metadata-ttl , default: minimal (or 60s if --cache is set) - NegativeMetadataTTL string // --negative-metadata-ttl , default: same as MetadataTTL - LogMetrics bool // --log-metrics, enables summarized performance metrics in logs, default: false + // Identity β€” sourced from secretMap, not mountOptions. + BucketName string + ObjectPath string + EndPoint string + LocConstraint string + AuthType string + AccessKey string + SecretKey string + + // UID / GID β€” secretMap sets initial value; mountOptions can override. + UID string + GID string + + // LogLevel requires name translation: + // "debug" β†’ --debug + // "debug-crt" β†’ --debug-crt + // "no-log" β†’ --no-log + // mount-s3 has no --log-level flag. + LogLevel string + + // LogDirectory must exist on the node before mount-s3 starts. + // Validated in Mount() before calling formulateMountOptions. + LogDirectory string + + // CacheDir must exist on the node before mount-s3 starts. + // Validated in Mount() before calling formulateMountOptions. + CacheDir string + + // Conflict resolution β€” ReadOnly takes priority over write flags. + // If ReadOnly is set, AllowOverwrite and IncrementalUpload are cleared + // in Mount() before formulateMountOptions is called. This allows SC to + // set allow-overwrite and incremental-upload as safe defaults while still + // letting users request read-only via their secret without errors. + ReadOnly bool // --read-only + AllowOverwrite bool // --allow-overwrite + IncrementalUpload bool // --incremental-upload + + // MountOptions holds all remaining flags that pass through unchanged. + // The parser ensures every entry has a "--" prefix so mount-s3 sees valid flags. + MountOptions []string + MounterUtils utils.MounterUtils } -// s3MounterArgs holds the args passed to the worker for mount-s3. +// s3MounterArgs holds the args sent to the worker node daemon for mount-s3. +// Only structured fields are represented here; passthrough flags are embedded +// in the Args slice at the call site. type s3MounterArgs struct { - ReadOnly string `json:"read-only,omitempty"` - AllowOther string `json:"allow-other,omitempty"` - UID string `json:"uid,omitempty"` - GID string `json:"gid,omitempty"` - // Removed: UMask β€” mount-s3 has no --umask flag; use --dir-mode/--file-mode via MountOptions - // Removed: AwsConfigDir β€” mount-s3 has no --aws-config-dir flag - LogLevel string `json:"log-level,omitempty"` // valid: "debug", "debug-crt", "no-log" - EndpointURL string `json:"endpoint-url,omitempty"` - AllowDelete string `json:"allow-delete,omitempty"` // --allow-delete flag - AllowOverwrite string `json:"allow-overwrite,omitempty"` // --allow-overwrite flag - LogDirectory string `json:"log-directory,omitempty"` // --log-directory flag - // Region is passed explicitly as --region CLI flag in addition to being written - // in the AWS config file, to ensure it is always set even if AWS_CONFIG_FILE - // env var is not propagated correctly to the mount-s3 subprocess. - Region string `json:"region,omitempty"` + // Always set. + AllowOther string `json:"allow-other,omitempty"` AwsCredentialsFile string `json:"aws-credentials-file,omitempty"` AwsConfigFile string `json:"aws-config-file,omitempty"` - // Performance tuning fields β€” all optional, mirrors MountpointS3Mounter fields. - MaxThreads string `json:"max-threads,omitempty"` - ReadPartSize string `json:"read-part-size,omitempty"` - WritePartSize string `json:"write-part-size,omitempty"` - MaxThroughputGbps string `json:"maximum-throughput-gbps,omitempty"` - UploadChecksums string `json:"upload-checksums,omitempty"` - CacheDir string `json:"cache,omitempty"` - MaxCacheSize string `json:"max-cache-size,omitempty"` - MetadataTTL string `json:"metadata-ttl,omitempty"` - NegativeMetadataTTL string `json:"negative-metadata-ttl,omitempty"` - LogMetrics bool `json:"log-metrics,omitempty"` + // SecretMap-sourced. + EndpointURL string `json:"endpoint-url,omitempty"` + Region string `json:"region,omitempty"` + + // Identity. + UID string `json:"uid,omitempty"` + GID string `json:"gid,omitempty"` + + // Logging (translated from LogLevel). + LogLevel string `json:"log-level,omitempty"` + LogDirectory string `json:"log-directory,omitempty"` + + // Cache. + CacheDir string `json:"cache,omitempty"` + + // Conflict trio. + ReadOnly string `json:"read-only,omitempty"` + AllowOverwrite string `json:"allow-overwrite,omitempty"` + IncrementalUpload string `json:"incremental-upload,omitempty"` } const ( @@ -108,15 +132,18 @@ type envMounter interface { // 3. Secret mountOptions (secretMap["mountOptions"] multiline string) β€” parsed // second and therefore wins over StorageClass on any overlapping flag // -// For passthrough flags (unrecognised by the parser), the same precedence -// applies: secret passthroughs are appended after SC passthroughs, so -// mount-s3 sees the secret value last (last-value-wins semantics). +// Special case: if read-only is present in either source, it takes priority and +// clears allow-overwrite and incremental-upload regardless of where they came from. +// +// For passthrough flags (unrecognised by the structured parser), the same +// precedence applies: secret passthroughs are appended after SC passthroughs, +// so mount-s3 sees the secret value last (last-value-wins semantics). func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounterUtils utils.MounterUtils) Mounter { klog.Info("-newMountpointS3Mounter-") mounter := &MountpointS3Mounter{} - // --- Credentials (unified secret `data` fields, base64-decoded by k8s) --- + // --- Credentials --- if val, ok := secretMap["accessKey"]; ok { mounter.AccessKey = val } @@ -124,16 +151,13 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounter.SecretKey = val } - // --- Bucket / endpoint identity (`stringData` fields) --- + // --- Bucket / endpoint identity --- if val, ok := secretMap["bucketName"]; ok { mounter.BucketName = val } if val, ok := secretMap["objectPath"]; ok { mounter.ObjectPath = val } - // cosEndpoint and locationConstraint are IBM COS-specific. - // They are not part of the upstream unified secret schema but remain - // supported as secretMap keys for IBM COS compatibility. if val, ok := secretMap["cosEndpoint"]; ok { mounter.EndPoint = val } @@ -141,9 +165,7 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounter.LocConstraint = val } - // --- UID / GID from secretMap (lowest precedence for these fields) --- - // If gid is set but uid is not, mirror gid β†’ uid (matches rclone behavior). - // Both can be overridden by mountOptions below. + // --- UID / GID (lowest precedence β€” mountOptions overrides below) --- if secretMap["gid"] != "" && secretMap["uid"] == "" { mounter.UID = secretMap["gid"] } else if val := secretMap["uid"]; val != "" { @@ -156,20 +178,14 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounter.AuthType = "hmac" // --- Merge and parse mountOptions --- - // - // Step 1: Parse StorageClass mountOptions (the []string arg). - // These come in already split by the kubelet. + // Step 1: StorageClass mountOptions (already split by kubelet). scRemainder := parseMountpointS3Options(mounter, mountOptions) - // Step 2: Parse the secret's mountOptions multiline string. - // splitSecretMountOptions handles blank lines and # comments. - // Secret options are parsed AFTER SC options so they win on overlap. + // Step 2: Secret mountOptions (multiline string, parsed after SC so it wins). secretOpts := splitSecretMountOptions(secretMap["mountOptions"]) secretRemainder := parseMountpointS3Options(mounter, secretOpts) - // Step 3: Build the final passthrough slice. - // SC passthroughs come first; secret passthroughs come last so - // mount-s3 sees the secret value last (last-value-wins). + // Step 3: SC passthroughs first, secret passthroughs last (last-value-wins). mounter.MountOptions = append(scRemainder, secretRemainder...) mounter.MounterUtils = mounterUtils @@ -185,7 +201,7 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, // secret into a []string suitable for parseMountpointS3Options. // // Rules: -// - Each non-empty line is one flag (same as StorageClass mountOptions). +// - Each non-empty line is one flag. // - Lines beginning with '#' (after trimming) are comments β€” skipped. // - Leading/trailing whitespace on each line is trimmed. // - Blank lines are skipped. @@ -204,53 +220,46 @@ func splitSecretMountOptions(raw string) []string { return opts } -// parseMountpointS3Options parses mount-s3 options from the provided slice and -// populates recognised flags into the mounter struct. +// parseMountpointS3Options parses mount-s3 options from the provided slice. +// +// Only flags that require code-level handling are matched by name β€” everything +// else is forwarded unchanged to mount-s3 via the returned remainder slice. +// +// The remainder always has a "--" prefix added when missing, so mount-s3 +// receives syntactically valid flags regardless of how the user wrote them. +// This handles both boolean flags ("allow-delete" β†’ "--allow-delete") and +// key-value pairs ("max-threads=32" β†’ "--max-threads=32") correctly because +// the original opt string is preserved and only the prefix is fixed. // // Rules: -// - Flags may be provided with or without a leading "--" prefix (both accepted). -// - Boolean flags (--read-only, --allow-delete, etc.) need no value. +// - Flags may be provided with or without a leading "--" prefix. +// - Boolean flags need no value. // - Value flags must be in "flag=value" or "--flag=value" form. // - "allow-other" is silently consumed β€” formulateMountOptions always adds it. // - "umask" is consumed with a warning β€” mount-s3 has no --umask flag. -// - Unrecognised flags are returned unchanged for pass-through to mount-s3. -// - mountOptions uid/gid override secretMap uid/gid (caller sets secretMap -// values first; this function runs second). +// - uid/gid override secretMap values (caller sets secretMap values first). func parseMountpointS3Options(mounter *MountpointS3Mounter, opts []string) []string { var remaining []string for _, opt := range opts { - // Normalise: strip optional leading "--" so comparisons are uniform. + // Normalise: strip optional leading "--" so switch cases are uniform. trimmed := strings.TrimPrefix(opt, "--") - key, value, hasValue := strings.Cut(trimmed, "=") switch key { // ------------------------------------------------------------------ - // Always emitted by formulateMountOptions β€” consume to avoid duplicates. + // Always emitted β€” consume to avoid duplicates. // ------------------------------------------------------------------ case "allow-other": - // formulateMountOptions unconditionally adds --allow-other; drop here. + // formulateMountOptions unconditionally adds --allow-other. // ------------------------------------------------------------------ - // Unsupported by mount-s3 β€” warn and drop so mount-s3 doesn't fail. + // Invalid for mount-s3 β€” warn and drop so mount-s3 doesn't fail. // ------------------------------------------------------------------ case "umask": klog.Warningf("parseMountpointS3Options: 'umask' is not supported by mount-s3. " + - "Use --dir-mode / --file-mode via mountOptions in the StorageClass instead. Ignoring.") - - // ------------------------------------------------------------------ - // Boolean mount flags β€” presence means enabled, no value needed. - // ------------------------------------------------------------------ - case "read-only": - mounter.ReadOnly = true - case "allow-delete": - mounter.AllowDelete = true - case "allow-overwrite": - mounter.AllowOverwrite = true - case "log-metrics": - mounter.LogMetrics = true + "Use --dir-mode / --file-mode instead. Ignoring.") // ------------------------------------------------------------------ // Identity β€” mountOptions values override secretMap values. @@ -265,69 +274,52 @@ func parseMountpointS3Options(mounter *MountpointS3Mounter, opts []string) []str } // ------------------------------------------------------------------ - // Logging + // Log level β€” requires name translation in formulateMountOptions. + // mount-s3 has no --log-level flag; valid values map to separate flags: + // "debug" β†’ --debug, "debug-crt" β†’ --debug-crt, "no-log" β†’ --no-log // ------------------------------------------------------------------ case "log-level": - // mount-s3 has no --log-level flag. - // formulateMountOptions maps: "debug" β†’ --debug, - // "debug-crt" β†’ --debug-crt, "no-log" β†’ --no-log. if hasValue { mounter.LogLevel = value } - case "log-directory": - if hasValue { - mounter.LogDirectory = value - } // ------------------------------------------------------------------ - // Performance tuning + // Directory flags β€” validated for existence before mount-s3 starts. // ------------------------------------------------------------------ - case "max-threads": - if hasValue { - mounter.MaxThreads = value - } - case "read-part-size": - if hasValue { - mounter.ReadPartSize = value - } - case "write-part-size": - if hasValue { - mounter.WritePartSize = value - } - case "maximum-throughput-gbps": + case "log-directory": if hasValue { - mounter.MaxThroughputGbps = value + mounter.LogDirectory = value } - case "upload-checksums": + case "cache": if hasValue { - mounter.UploadChecksums = value + mounter.CacheDir = value } // ------------------------------------------------------------------ - // Caching + // Conflict trio β€” tracked for read-only priority resolution in Mount(). + // read-only clears allow-overwrite and incremental-upload if all three + // are set, so SC defaults don't block users from requesting read-only. // ------------------------------------------------------------------ - case "cache": - if hasValue { - mounter.CacheDir = value - } - case "max-cache-size": - if hasValue { - mounter.MaxCacheSize = value - } - case "metadata-ttl": - if hasValue { - mounter.MetadataTTL = value - } - case "negative-metadata-ttl": - if hasValue { - mounter.NegativeMetadataTTL = value - } + case "read-only": + mounter.ReadOnly = true + case "allow-overwrite": + mounter.AllowOverwrite = true + case "incremental-upload": + mounter.IncrementalUpload = true // ------------------------------------------------------------------ - // Unrecognised β€” pass through to mount-s3 unchanged. + // Everything else β€” pass through to mount-s3 unchanged. + // Ensure "--" prefix so mount-s3 always receives a valid flag. + // Works for both boolean flags and key-value pairs: + // "allow-delete" β†’ "--allow-delete" + // "max-threads=32" β†’ "--max-threads=32" // ------------------------------------------------------------------ default: - remaining = append(remaining, opt) + if strings.HasPrefix(opt, "--") { + remaining = append(remaining, opt) + } else { + remaining = append(remaining, "--"+opt) + } } } @@ -339,7 +331,52 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { klog.Info("-MountpointS3Mounter Mount-") klog.Infof("Mount args:\n\tsource: <%s>\n\ttarget: <%s>", source, target) - // Determine config path based on mode. + // --- Read-only priority resolution --- + // If read-only is set, it takes priority over all write-related flags. + // AllowOverwrite and IncrementalUpload are cleared here, and allow-delete + // is filtered from MountOptions, so the mount succeeds cleanly in read-only mode. + // This means users can always request read-only via their secret even when + // the StorageClass has write flags as defaults. + if s3.ReadOnly { + if s3.AllowOverwrite { + klog.Infof("MountpointS3Mounter: read-only set β€” clearing allow-overwrite") + s3.AllowOverwrite = false + } + if s3.IncrementalUpload { + klog.Infof("MountpointS3Mounter: read-only set β€” clearing incremental-upload") + s3.IncrementalUpload = false + } + + // Filter out allow-delete from passthrough options + var filtered []string + for _, opt := range s3.MountOptions { + trimmed := strings.TrimPrefix(opt, "--") + if trimmed != "allow-delete" { + filtered = append(filtered, opt) + } else { + klog.Infof("MountpointS3Mounter: read-only set β€” removing allow-delete from passthrough options") + } + } + s3.MountOptions = filtered + } + + // --- Directory validation --- + // mount-s3 fails with a confusing error if these directories don't exist. + // Validate here to surface a clear error message before spawning the process. + if s3.LogDirectory != "" { + if _, err := Stat(s3.LogDirectory); err != nil { + return fmt.Errorf("MountpointS3Mounter: log-directory %q does not exist or is not accessible: %w", + s3.LogDirectory, err) + } + } + if s3.CacheDir != "" { + if _, err := Stat(s3.CacheDir); err != nil { + return fmt.Errorf("MountpointS3Mounter: cache directory %q does not exist or is not accessible: %w", + s3.CacheDir, err) + } + } + + // --- Config path --- var configPath string if mountWorker { configPath = constants.MounterConfigPathOnHost @@ -347,10 +384,8 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { configPath = constants.MounterConfigPathOnPodS3Mount } - // Create unique config dir per volume. configPathWithVolID := path.Join(configPath, fmt.Sprintf("%x", sha256.Sum256([]byte(target)))) - // Write AWS credentials and config files. if err := createS3MountConfig(configPathWithVolID, s3); err != nil { klog.Errorf("MountpointS3Mounter Mount: Cannot create config file: %v", err) return err @@ -387,9 +422,6 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { klog.Info("NodeServer Mounting...") - // Use FuseMountWithEnv if available on the concrete type (race-safe, per-subprocess env). - // Falls back to FuseMount if running under a mock or different implementation β€” the - // fallback will still work but credentials must be available via system env in that case. if m, ok := s3.MounterUtils.(envMounter); ok { return m.FuseMountWithEnv(target, constants.MountpointS3BinaryPath, args, envVars) } @@ -437,17 +469,11 @@ func (s3 *MountpointS3Mounter) Unmount(target string) error { // credentials <- AWS credentials (access/secret key) // config <- AWS config (region, endpoint_url) func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) error { - // Create the config directory. - if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 -- directory is created under a controlled path using SHA256 hash, not user input + if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 klog.Errorf("MountpointS3Mounter: Cannot create config dir %s: %v", configPathWithVolID, err) return err } - // --- Write credentials file --- - // Format matches AWS credentials file: - // [default] - // aws_access_key_id = - // aws_secret_access_key = credFile := path.Join(configPathWithVolID, s3MountCredentialsFile) credParams := []string{ "[" + s3MountAwsProfile + "]", @@ -459,11 +485,6 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er return err } - // --- Write config file --- - // Format matches AWS config file: - // [default] - // region = - // endpoint_url = (for IBM COS compatibility) region := s3.LocConstraint configParams := []string{ "[" + s3MountAwsProfile + "]", @@ -484,7 +505,7 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er // writeConfigFile writes lines to a file with 0600 permissions. func writeConfigFile(filePath string, lines []string) error { - f, err := CreateFile(filePath) // #nosec G304 -- filePath is constructed internally from a SHA256 hash and constant config dir, not from user input + f, err := CreateFile(filePath) // #nosec G304 if err != nil { return fmt.Errorf("cannot create file %s: %w", filePath, err) } @@ -494,7 +515,7 @@ func writeConfigFile(filePath string, lines []string) error { } }() - if err := Chmod(filePath, 0600); err != nil { // #nosec G302 -- restrictive permissions (0600) are intentionally set for credential files + if err := Chmod(filePath, 0600); err != nil { // #nosec G302 return fmt.Errorf("cannot chmod file %s: %w", filePath, err) } @@ -507,22 +528,28 @@ func writeConfigFile(filePath string, lines []string) error { return w.Flush() } -// formulateMountOptions builds CLI args, env vars, and the worker payload for mount-s3. +// formulateMountOptions builds the CLI args, env vars, and worker payload for mount-s3. // -// Removed flags (do not exist in mount-s3): -// - --aws-config-dir β†’ replaced by AWS_SHARED_CREDENTIALS_FILE / AWS_CONFIG_FILE env vars -// - --log-level β†’ mapped to --debug / --debug-crt / --no-log -// - --umask β†’ use --dir-mode / --file-mode via MountOptions in StorageClass +// Only structured fields are emitted here. All other user-supplied flags arrive +// pre-formatted in s3.MountOptions and are appended at the end. +// +// By the time this is called, Mount() has already: +// - Cleared AllowOverwrite / IncrementalUpload if ReadOnly is set +// - Validated LogDirectory and CacheDir existence +// +// Flag ordering: +// 1. Positional args : bucket, mountpoint +// 2. Always-on : --allow-other +// 3. SecretMap-sourced : --endpoint-url, --region +// 4. Identity : --uid, --gid +// 5. Logging : --debug/--debug-crt/--no-log, --log-directory +// 6. Cache : --cache +// 7. Write flags : --allow-overwrite, --incremental-upload +// 8. Read-only : --read-only +// 9. User passthrough : s3.MountOptions (appended last β€” highest precedence) func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathWithVolID string) (nodeServerOp []string, envVars []string, workerNodeOp s3MounterArgs) { - // mount-s3 syntax: mount-s3 [flags] - nodeServerOp = []string{ - bucket, - target, - "--allow-other", - } + nodeServerOp = []string{bucket, target, "--allow-other"} - // AWS credentials via env vars β€” mount-s3 and the underlying AWS SDK reads these - // to locate the credentials and config files written by createS3MountConfig. credFilePath := path.Join(configPathWithVolID, s3MountCredentialsFile) cfgFilePath := path.Join(configPathWithVolID, s3MountConfigFile) envVars = []string{ @@ -536,33 +563,28 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW AwsConfigFile: cfgFilePath, } - // Endpoint (IBM COS endpoint). + // --- SecretMap-sourced --- if s3.EndPoint != "" { nodeServerOp = append(nodeServerOp, "--endpoint-url="+s3.EndPoint) workerNodeOp.EndpointURL = s3.EndPoint } - - // Region β€” passed as CLI flag AND written to config file for redundancy. if s3.LocConstraint != "" { nodeServerOp = append(nodeServerOp, "--region="+s3.LocConstraint) workerNodeOp.Region = s3.LocConstraint } - // UID. + // --- Identity --- if s3.UID != "" { nodeServerOp = append(nodeServerOp, "--uid="+s3.UID) workerNodeOp.UID = s3.UID } - - // GID. if s3.GID != "" { nodeServerOp = append(nodeServerOp, "--gid="+s3.GID) workerNodeOp.GID = s3.GID } - // Log level: mount-s3 has no --log-level flag. - // Valid mappings: "debug" β†’ --debug, "debug-crt" β†’ --debug-crt, "no-log" β†’ --no-log. - // Anything else (e.g. "warn", "info") is ignored β€” mount-s3 default logging applies. + // --- Log level (name translation) --- + // mount-s3 has no --log-level flag; each level maps to its own flag. switch s3.LogLevel { case "debug": nodeServerOp = append(nodeServerOp, "--debug") @@ -575,102 +597,45 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW workerNodeOp.LogLevel = "no-log" default: if s3.LogLevel != "" { - klog.Warningf("MountpointS3Mounter: unsupported log level '%s' for mount-s3. "+ - "Supported values: 'debug', 'debug-crt', 'no-log'. Ignoring.", s3.LogLevel) + klog.Warningf("MountpointS3Mounter: unsupported log-level %q. "+ + "Supported: 'debug', 'debug-crt', 'no-log'. Ignoring.", s3.LogLevel) } } - // Read-only. - if s3.ReadOnly { - nodeServerOp = append(nodeServerOp, "--read-only") - workerNodeOp.ReadOnly = "true" - } - - // Allow delete. - if s3.AllowDelete { - nodeServerOp = append(nodeServerOp, "--allow-delete") - workerNodeOp.AllowDelete = "true" - } - - // Allow overwrite. - if s3.AllowOverwrite { - nodeServerOp = append(nodeServerOp, "--allow-overwrite") - workerNodeOp.AllowOverwrite = "true" - } - - // Log directory. + // --- Log directory (existence validated in Mount()) --- if s3.LogDirectory != "" { nodeServerOp = append(nodeServerOp, "--log-directory="+s3.LogDirectory) workerNodeOp.LogDirectory = s3.LogDirectory } - // --- Performance tuning options --- - - // --read-part-size (default: 8 MiB). - if s3.ReadPartSize != "" { - nodeServerOp = append(nodeServerOp, "--read-part-size="+s3.ReadPartSize) - workerNodeOp.ReadPartSize = s3.ReadPartSize - } - - // --write-part-size (default: 8 MiB). - if s3.WritePartSize != "" { - nodeServerOp = append(nodeServerOp, "--write-part-size="+s3.WritePartSize) - workerNodeOp.WritePartSize = s3.WritePartSize - } - - // --max-threads (default: 16). - if s3.MaxThreads != "" { - nodeServerOp = append(nodeServerOp, "--max-threads="+s3.MaxThreads) - workerNodeOp.MaxThreads = s3.MaxThreads - } - - // --maximum-throughput-gbps (default: auto-detect on EC2, 10 Gbps elsewhere). - // Not on EC2 so won't auto-detect correctly β€” set explicitly if you know your node bandwidth. - if s3.MaxThroughputGbps != "" { - nodeServerOp = append(nodeServerOp, "--maximum-throughput-gbps="+s3.MaxThroughputGbps) - workerNodeOp.MaxThroughputGbps = s3.MaxThroughputGbps - } - - // --upload-checksums (default: crc32c). - // Set to "off" to reduce CPU overhead on write-heavy workloads at the cost of integrity checks. - if s3.UploadChecksums != "" { - nodeServerOp = append(nodeServerOp, "--upload-checksums="+s3.UploadChecksums) - workerNodeOp.UploadChecksums = s3.UploadChecksums - } - - // --cache + --max-cache-size (default: disabled). - // Enables local disk read cache β€” improves repeated read performance. - // When cache is set, metadata-ttl defaults to 60s automatically. + // --- Cache directory (existence validated in Mount()) --- if s3.CacheDir != "" { nodeServerOp = append(nodeServerOp, "--cache="+s3.CacheDir) workerNodeOp.CacheDir = s3.CacheDir - if s3.MaxCacheSize != "" { - nodeServerOp = append(nodeServerOp, "--max-cache-size="+s3.MaxCacheSize) - workerNodeOp.MaxCacheSize = s3.MaxCacheSize - } } - // --metadata-ttl (default: minimal, or 60s if --cache is set). - // Set higher to reduce ListObjects API calls on stable buckets. - if s3.MetadataTTL != "" { - nodeServerOp = append(nodeServerOp, "--metadata-ttl="+s3.MetadataTTL) - workerNodeOp.MetadataTTL = s3.MetadataTTL + // --- Write flags --- + // These are only emitted if ReadOnly is false. + // Mount() clears these when ReadOnly is set, so no conflict is possible here. + if s3.AllowOverwrite { + nodeServerOp = append(nodeServerOp, "--allow-overwrite") + workerNodeOp.AllowOverwrite = "true" } - - // --negative-metadata-ttl (default: same as metadata-ttl). - if s3.NegativeMetadataTTL != "" { - nodeServerOp = append(nodeServerOp, "--negative-metadata-ttl="+s3.NegativeMetadataTTL) - workerNodeOp.NegativeMetadataTTL = s3.NegativeMetadataTTL + if s3.IncrementalUpload { + nodeServerOp = append(nodeServerOp, "--incremental-upload") + workerNodeOp.IncrementalUpload = "true" } - // --log-metrics (default: false). - // Logs summarized throughput metrics β€” zero performance cost, useful for monitoring. - if s3.LogMetrics { - nodeServerOp = append(nodeServerOp, "--log-metrics") - workerNodeOp.LogMetrics = true + // --- Read-only --- + // Safe to emit here β€” Mount() already cleared AllowOverwrite and + // IncrementalUpload above, so no conflicting flags will be present. + if s3.ReadOnly { + nodeServerOp = append(nodeServerOp, "--read-only") + workerNodeOp.ReadOnly = "true" } - // StorageClass mountOptions are appended LAST β€” always take final precedence over everything above. + // --- User passthrough (appended last β€” highest precedence) --- + // All entries already have "--" prefix (enforced by parseMountpointS3Options). nodeServerOp = append(nodeServerOp, s3.MountOptions...) klog.Infof("MountpointS3 nodeServerOp: %v", nodeServerOp) @@ -679,7 +644,7 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW return nodeServerOp, envVars, workerNodeOp } -// removeS3MountConfigFile removes the config directory for the volume (same pattern as rclone). +// removeS3MountConfigFile removes the config directory for the given volume target. func removeS3MountConfigFile(configPath, target string) { configPathWithVolID := path.Join(configPath, fmt.Sprintf("%x", sha256.Sum256([]byte(target)))) From 633e5859f49d5c575ecce1301d1c30abfd2afc6f Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 30 Mar 2026 12:37:17 +0530 Subject: [PATCH 036/128] Unit-test for s3mounter --- pkg/mounter/mounter-s3mounter_test.go | 613 ++++++++++++++++++++++++++ 1 file changed, 613 insertions(+) create mode 100644 pkg/mounter/mounter-s3mounter_test.go diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go new file mode 100644 index 00000000..89266bad --- /dev/null +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -0,0 +1,613 @@ +package mounter + +import ( + "errors" + "os" + "testing" + + "github.com/IBM/ibm-object-csi-driver/pkg/constants" + mounterUtils "github.com/IBM/ibm-object-csi-driver/pkg/mounter/utils" + "github.com/stretchr/testify/assert" +) + +var ( + s3MounterSecretMap = map[string]string{ + "cosEndpoint": "https://s3.us-east.cloud-object-storage.appdomain.cloud", + "locationConstraint": "us-east", + "bucketName": "test-bucket", + "objectPath": "test-path", + "accessKey": "test-access-key", + "secretKey": "test-secret-key", + "uid": "1000", + "gid": "1000", + "umask": "0022", + "logLevel": "debug", + "readOnly": "true", + "maxThreads": "32", + "readPartSize": "16777216", + "writePartSize": "16777216", + "maximumThroughputGbps": "20", + "uploadChecksums": "crc32c", + "cacheDir": "/tmp/cache", + "maxCacheSize": "1024", + "metadataTTL": "60", + "negativeMetadataTTL": "30", + "logMetrics": "true", + } + + s3MounterMountOptions = []string{"--dir-mode=0755", "--file-mode=0644"} + source = "test-source" + target = "/mnt/test-target" +) + +func TestNewMountpointS3Mounter_Success(t *testing.T) { + mounter := NewMountpointS3Mounter(s3MounterSecretMap, s3MounterMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + + s3Mounter, ok := mounter.(*MountpointS3Mounter) + assert.True(t, ok) + + assert.Equal(t, s3MounterSecretMap["bucketName"], s3Mounter.BucketName) + assert.Equal(t, s3MounterSecretMap["objectPath"], s3Mounter.ObjectPath) + assert.Equal(t, s3MounterSecretMap["cosEndpoint"], s3Mounter.EndPoint) + assert.Equal(t, s3MounterSecretMap["locationConstraint"], s3Mounter.LocConstraint) + assert.Equal(t, s3MounterSecretMap["accessKey"], s3Mounter.AccessKey) + assert.Equal(t, s3MounterSecretMap["secretKey"], s3Mounter.SecretKey) + assert.Equal(t, s3MounterSecretMap["uid"], s3Mounter.UID) + assert.Equal(t, s3MounterSecretMap["gid"], s3Mounter.GID) + assert.Equal(t, s3MounterSecretMap["umask"], s3Mounter.UMask) + assert.Equal(t, s3MounterSecretMap["logLevel"], s3Mounter.LogLevel) + assert.True(t, s3Mounter.ReadOnly) + assert.Equal(t, "hmac", s3Mounter.AuthType) +} + +func TestNewMountpointS3Mounter_PerformanceTuning(t *testing.T) { + mounter := NewMountpointS3Mounter(s3MounterSecretMap, s3MounterMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + + s3Mounter, ok := mounter.(*MountpointS3Mounter) + assert.True(t, ok) + + assert.Equal(t, s3MounterSecretMap["maxThreads"], s3Mounter.MaxThreads) + assert.Equal(t, s3MounterSecretMap["readPartSize"], s3Mounter.ReadPartSize) + assert.Equal(t, s3MounterSecretMap["writePartSize"], s3Mounter.WritePartSize) + assert.Equal(t, s3MounterSecretMap["maximumThroughputGbps"], s3Mounter.MaxThroughputGbps) + assert.Equal(t, s3MounterSecretMap["uploadChecksums"], s3Mounter.UploadChecksums) + assert.Equal(t, s3MounterSecretMap["cacheDir"], s3Mounter.CacheDir) + assert.Equal(t, s3MounterSecretMap["maxCacheSize"], s3Mounter.MaxCacheSize) + assert.Equal(t, s3MounterSecretMap["metadataTTL"], s3Mounter.MetadataTTL) + assert.Equal(t, s3MounterSecretMap["negativeMetadataTTL"], s3Mounter.NegativeMetadataTTL) + assert.True(t, s3Mounter.LogMetrics) +} + +func TestNewMountpointS3Mounter_GidWithoutUid(t *testing.T) { + secretMap := map[string]string{ + "gid": "2000", + } + + mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + + s3Mounter, ok := mounter.(*MountpointS3Mounter) + assert.True(t, ok) + assert.Equal(t, "2000", s3Mounter.UID) + assert.Equal(t, "2000", s3Mounter.GID) +} + +func TestNewMountpointS3Mounter_UidOverridesGid(t *testing.T) { + secretMap := map[string]string{ + "uid": "1000", + "gid": "2000", + } + + mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + + s3Mounter, ok := mounter.(*MountpointS3Mounter) + assert.True(t, ok) + assert.Equal(t, "1000", s3Mounter.UID) + assert.Equal(t, "2000", s3Mounter.GID) +} + +func TestMountpointS3Mount_NodeServer_Positive(t *testing.T) { + mountWorker = false + + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return &os.File{}, nil + } + Chmod = func(path string, perm os.FileMode) error { + return nil + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseMountFn: func(path, comm string, args []string) error { + return nil + }, + }), + BucketName: "test-bucket", + ObjectPath: "test-path", + EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", + LocConstraint: "us-east", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + UID: "1000", + GID: "1000", + LogLevel: "debug", + ReadOnly: true, + MountOptions: s3MounterMountOptions, + } + + err := s3Mounter.Mount(source, target) + assert.NoError(t, err) +} + +func TestMountpointS3Mount_NodeServer_WithEnvMounter(t *testing.T) { + mountWorker = false + + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return &os.File{}, nil + } + Chmod = func(path string, perm os.FileMode) error { + return nil + } + + // Create a mock that implements envMounter interface + mockUtils := &mockEnvMounter{ + FakeMounterUtilsImpl: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mockUtils, + BucketName: "test-bucket", + EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", + LocConstraint: "us-east", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + MountOptions: s3MounterMountOptions, + } + + err := s3Mounter.Mount(source, target) + assert.NoError(t, err) +} + +func TestMountpointS3Mount_WorkerNode_Positive(t *testing.T) { + mountWorker = true + + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return &os.File{}, nil + } + Chmod = func(path string, perm os.FileMode) error { + return nil + } + mounterRequest = func(payload, url string) error { + return nil + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), + BucketName: "test-bucket", + EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", + LocConstraint: "us-east", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := s3Mounter.Mount(source, target) + assert.NoError(t, err) +} + +func TestMountpointS3Mount_CreateDirFails_Negative(t *testing.T) { + mountWorker = false + + MakeDir = func(path string, perm os.FileMode) error { + return errors.New("failed to create directory") + } + + s3Mounter := &MountpointS3Mounter{ + BucketName: "test-bucket", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := s3Mounter.Mount(source, target) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Cannot create config dir") +} + +func TestMountpointS3Mount_CreateCredFileFails_Negative(t *testing.T) { + mountWorker = false + + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return nil, errors.New("failed to create file") + } + + s3Mounter := &MountpointS3Mounter{ + BucketName: "test-bucket", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := s3Mounter.Mount(source, target) + assert.Error(t, err) + assert.Contains(t, err.Error(), "cannot create file") +} + +func TestMountpointS3Mount_WorkerNode_Negative(t *testing.T) { + mountWorker = true + + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return &os.File{}, nil + } + Chmod = func(path string, perm os.FileMode) error { + return nil + } + mounterRequest = func(payload, url string) error { + return errors.New("failed to perform http request") + } + + s3Mounter := &MountpointS3Mounter{ + BucketName: "test-bucket", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := s3Mounter.Mount(source, target) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to perform http request") +} + +func TestMountpointS3Unmount_NodeServer(t *testing.T) { + mountWorker = false + + removeS3ConfigFile = func(configPath, target string) {} + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseUnmountFn: func(path string) error { + return nil + }, + }), + } + + err := s3Mounter.Unmount(target) + assert.NoError(t, err) +} + +func TestMountpointS3Unmount_WorkerNode(t *testing.T) { + mountWorker = true + + removeS3ConfigFile = func(configPath, target string) {} + + mounterRequest = func(payload, url string) error { + return nil + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseUnmountFn: func(path string) error { + return nil + }, + }), + } + + err := s3Mounter.Unmount(target) + assert.NoError(t, err) +} + +func TestMountpointS3Unmount_WorkerNode_Negative(t *testing.T) { + mountWorker = true + + mounterRequest = func(payload, url string) error { + return errors.New("failed to create http request") + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseUnmountFn: func(path string) error { + return nil + }, + }), + } + + err := s3Mounter.Unmount(target) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to create http request") +} + +func TestMountpointS3Unmount_NodeServer_Negative(t *testing.T) { + mountWorker = false + + removeS3ConfigFile = func(configPath, target string) {} + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseUnmountFn: func(path string) error { + return errors.New("failed to unmount") + }, + }), + } + + err := s3Mounter.Unmount(target) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to unmount") +} + +func TestRemoveS3MountConfigFile_PathNotExists(t *testing.T) { + Stat = func(path string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } + removeS3MountConfigFile("/test", target) +} + +func TestRemoveS3MountConfigFile_StatRetryThenSuccess(t *testing.T) { + attempt := 0 + Stat = func(_ string) (os.FileInfo, error) { + if attempt == 0 { + attempt++ + return nil, errors.New("stat error") + } + return nil, nil + } + RemoveAll = func(_ string) error { + return nil + } + + removeS3MountConfigFile("/test", target) +} + +func TestRemoveS3MountConfigFile_RemoveRetryThenSuccess(t *testing.T) { + Stat = func(_ string) (os.FileInfo, error) { + return nil, nil + } + attempt := 0 + RemoveAll = func(_ string) error { + if attempt == 0 { + attempt++ + return errors.New("remove error") + } + return nil + } + + removeS3MountConfigFile("/test", target) +} + +func TestRemoveS3MountConfigFile_Negative(t *testing.T) { + called := 0 + Stat = func(_ string) (os.FileInfo, error) { + return nil, nil + } + RemoveAll = func(_ string) error { + called++ + return errors.New("remove failed") + } + + removeS3MountConfigFile("/test", target) + assert.Equal(t, maxRetries, called) +} + +func TestFormulateMountOptions_AllOptions(t *testing.T) { + s3Mounter := &MountpointS3Mounter{ + BucketName: "test-bucket", + ObjectPath: "test-path", + EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", + LocConstraint: "us-east", + UID: "1000", + GID: "2000", + UMask: "0022", + LogLevel: "debug", + ReadOnly: true, + MaxThreads: "32", + ReadPartSize: "16777216", + WritePartSize: "16777216", + MaxThroughputGbps: "20", + UploadChecksums: "crc32c", + CacheDir: "/tmp/cache", + MaxCacheSize: "1024", + MetadataTTL: "60", + NegativeMetadataTTL: "30", + LogMetrics: true, + MountOptions: []string{"--dir-mode=0755"}, + } + + nodeServerOp, envVars, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket/test-path", target, "/config/path") + + // Check node server options + assert.Contains(t, nodeServerOp, "test-bucket/test-path") + assert.Contains(t, nodeServerOp, target) + assert.Contains(t, nodeServerOp, "--allow-other") + assert.Contains(t, nodeServerOp, "--endpoint-url=https://s3.us-east.cloud-object-storage.appdomain.cloud") + assert.Contains(t, nodeServerOp, "--region=us-east") + assert.Contains(t, nodeServerOp, "--uid=1000") + assert.Contains(t, nodeServerOp, "--gid=2000") + assert.Contains(t, nodeServerOp, "--debug") + assert.Contains(t, nodeServerOp, "--read-only") + assert.Contains(t, nodeServerOp, "--max-threads=32") + assert.Contains(t, nodeServerOp, "--read-part-size=16777216") + assert.Contains(t, nodeServerOp, "--write-part-size=16777216") + assert.Contains(t, nodeServerOp, "--maximum-throughput-gbps=20") + assert.Contains(t, nodeServerOp, "--upload-checksums=crc32c") + assert.Contains(t, nodeServerOp, "--cache=/tmp/cache") + assert.Contains(t, nodeServerOp, "--max-cache-size=1024") + assert.Contains(t, nodeServerOp, "--metadata-ttl=60") + assert.Contains(t, nodeServerOp, "--negative-metadata-ttl=30") + assert.Contains(t, nodeServerOp, "--log-metrics") + assert.Contains(t, nodeServerOp, "--dir-mode=0755") + + // Check env vars + assert.Len(t, envVars, 2) + assert.Contains(t, envVars[0], "AWS_SHARED_CREDENTIALS_FILE=") + assert.Contains(t, envVars[1], "AWS_CONFIG_FILE=") + + // Check worker node options + assert.Equal(t, "true", workerNodeOp.AllowOther) + assert.Equal(t, "https://s3.us-east.cloud-object-storage.appdomain.cloud", workerNodeOp.EndpointURL) + assert.Equal(t, "us-east", workerNodeOp.Region) + assert.Equal(t, "1000", workerNodeOp.UID) + assert.Equal(t, "2000", workerNodeOp.GID) + assert.Equal(t, "debug", workerNodeOp.LogLevel) + assert.Equal(t, "true", workerNodeOp.ReadOnly) + assert.Equal(t, "32", workerNodeOp.MaxThreads) + assert.Equal(t, "16777216", workerNodeOp.ReadPartSize) + assert.Equal(t, "16777216", workerNodeOp.WritePartSize) + assert.Equal(t, "20", workerNodeOp.MaxThroughputGbps) + assert.Equal(t, "crc32c", workerNodeOp.UploadChecksums) + assert.Equal(t, "/tmp/cache", workerNodeOp.CacheDir) + assert.Equal(t, "1024", workerNodeOp.MaxCacheSize) + assert.Equal(t, "60", workerNodeOp.MetadataTTL) + assert.Equal(t, "30", workerNodeOp.NegativeMetadataTTL) + assert.True(t, workerNodeOp.LogMetrics) +} + +func TestFormulateMountOptions_LogLevelDebugCrt(t *testing.T) { + s3Mounter := &MountpointS3Mounter{ + LogLevel: "debug-crt", + } + + nodeServerOp, _, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") + + assert.Contains(t, nodeServerOp, "--debug-crt") + assert.Equal(t, "debug-crt", workerNodeOp.LogLevel) +} + +func TestFormulateMountOptions_LogLevelNoLog(t *testing.T) { + s3Mounter := &MountpointS3Mounter{ + LogLevel: "no-log", + } + + nodeServerOp, _, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") + + assert.Contains(t, nodeServerOp, "--no-log") + assert.Equal(t, "no-log", workerNodeOp.LogLevel) +} + +func TestFormulateMountOptions_InvalidLogLevel(t *testing.T) { + s3Mounter := &MountpointS3Mounter{ + LogLevel: "invalid-level", + } + + nodeServerOp, _, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") + + // Should not contain any log level flag + assert.NotContains(t, nodeServerOp, "--debug") + assert.NotContains(t, nodeServerOp, "--debug-crt") + assert.NotContains(t, nodeServerOp, "--no-log") + assert.Empty(t, workerNodeOp.LogLevel) +} + +func TestFormulateMountOptions_MinimalConfig(t *testing.T) { + s3Mounter := &MountpointS3Mounter{ + MountOptions: []string{}, + } + + nodeServerOp, envVars, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") + + // Check basic options are present + assert.Contains(t, nodeServerOp, "test-bucket") + assert.Contains(t, nodeServerOp, target) + assert.Contains(t, nodeServerOp, "--allow-other") + + // Check env vars + assert.Len(t, envVars, 2) + + // Check worker node options + assert.Equal(t, "true", workerNodeOp.AllowOther) +} + +func TestCreateS3MountConfig_Success(t *testing.T) { + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return &os.File{}, nil + } + Chmod = func(path string, perm os.FileMode) error { + return nil + } + + s3Mounter := &MountpointS3Mounter{ + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + LocConstraint: "us-east", + EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", + } + + err := createS3MountConfig("/config/path", s3Mounter) + assert.NoError(t, err) +} + +func TestCreateS3MountConfig_MakeDirFails(t *testing.T) { + MakeDir = func(path string, perm os.FileMode) error { + return errors.New("failed to create directory") + } + + s3Mounter := &MountpointS3Mounter{ + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := createS3MountConfig("/config/path", s3Mounter) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Cannot create config dir") +} + +func TestCreateS3MountConfig_CreateFileFails(t *testing.T) { + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return nil, errors.New("failed to create file") + } + + s3Mounter := &MountpointS3Mounter{ + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := createS3MountConfig("/config/path", s3Mounter) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Cannot write credentials file") +} + +func TestCreateS3MountConfig_ChmodFails(t *testing.T) { + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return &os.File{}, nil + } + Chmod = func(path string, perm os.FileMode) error { + return errors.New("failed to chmod") + } + + s3Mounter := &MountpointS3Mounter{ + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := createS3MountConfig("/config/path", s3Mounter) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Cannot write credentials file") +} + +// mockEnvMounter implements the envMounter interface for testing +type mockEnvMounter struct { + *mounterUtils.FakeMounterUtilsImpl +} + +func (m *mockEnvMounter) FuseMountWithEnv(path string, comm string, args []string, envVars []string) error { + return nil +} + +// Made with Bob From 5149e3263e75b63cc998b1a700f63b1e4b6c101b Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 30 Mar 2026 13:00:18 +0530 Subject: [PATCH 037/128] Updated test-code --- pkg/mounter/mounter-s3mounter_test.go | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go index 89266bad..41bc8253 100644 --- a/pkg/mounter/mounter-s3mounter_test.go +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -4,8 +4,6 @@ import ( "errors" "os" "testing" - - "github.com/IBM/ibm-object-csi-driver/pkg/constants" mounterUtils "github.com/IBM/ibm-object-csi-driver/pkg/mounter/utils" "github.com/stretchr/testify/assert" ) @@ -20,7 +18,6 @@ var ( "secretKey": "test-secret-key", "uid": "1000", "gid": "1000", - "umask": "0022", "logLevel": "debug", "readOnly": "true", "maxThreads": "32", @@ -36,8 +33,6 @@ var ( } s3MounterMountOptions = []string{"--dir-mode=0755", "--file-mode=0644"} - source = "test-source" - target = "/mnt/test-target" ) func TestNewMountpointS3Mounter_Success(t *testing.T) { @@ -54,7 +49,6 @@ func TestNewMountpointS3Mounter_Success(t *testing.T) { assert.Equal(t, s3MounterSecretMap["secretKey"], s3Mounter.SecretKey) assert.Equal(t, s3MounterSecretMap["uid"], s3Mounter.UID) assert.Equal(t, s3MounterSecretMap["gid"], s3Mounter.GID) - assert.Equal(t, s3MounterSecretMap["umask"], s3Mounter.UMask) assert.Equal(t, s3MounterSecretMap["logLevel"], s3Mounter.LogLevel) assert.True(t, s3Mounter.ReadOnly) assert.Equal(t, "hmac", s3Mounter.AuthType) @@ -405,7 +399,6 @@ func TestFormulateMountOptions_AllOptions(t *testing.T) { LocConstraint: "us-east", UID: "1000", GID: "2000", - UMask: "0022", LogLevel: "debug", ReadOnly: true, MaxThreads: "32", @@ -603,11 +596,9 @@ func TestCreateS3MountConfig_ChmodFails(t *testing.T) { // mockEnvMounter implements the envMounter interface for testing type mockEnvMounter struct { - *mounterUtils.FakeMounterUtilsImpl + mounterUtils.MounterUtils } func (m *mockEnvMounter) FuseMountWithEnv(path string, comm string, args []string, envVars []string) error { return nil } - -// Made with Bob From 6d720e18da3a91ccebf75518b853968623a2971a Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 30 Mar 2026 13:08:36 +0530 Subject: [PATCH 038/128] updated code --- pkg/mounter/mounter-s3mounter_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go index 41bc8253..6ccd3435 100644 --- a/pkg/mounter/mounter-s3mounter_test.go +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -150,7 +150,7 @@ func TestMountpointS3Mount_NodeServer_WithEnvMounter(t *testing.T) { // Create a mock that implements envMounter interface mockUtils := &mockEnvMounter{ - FakeMounterUtilsImpl: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), } s3Mounter := &MountpointS3Mounter{ From b303b95c66deee3511302f5ef01a3cafbf04af02 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 30 Mar 2026 13:41:39 +0530 Subject: [PATCH 039/128] Change os file to temp file creation --- pkg/mounter/mounter-s3mounter_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go index 6ccd3435..07a125d2 100644 --- a/pkg/mounter/mounter-s3mounter_test.go +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -106,7 +106,7 @@ func TestMountpointS3Mount_NodeServer_Positive(t *testing.T) { return nil } CreateFile = func(path string) (*os.File, error) { - return &os.File{}, nil + return os.CreateTemp("", "test") } Chmod = func(path string, perm os.FileMode) error { return nil @@ -142,7 +142,7 @@ func TestMountpointS3Mount_NodeServer_WithEnvMounter(t *testing.T) { return nil } CreateFile = func(path string) (*os.File, error) { - return &os.File{}, nil + return os.CreateTemp("", "test") } Chmod = func(path string, perm os.FileMode) error { return nil @@ -174,7 +174,7 @@ func TestMountpointS3Mount_WorkerNode_Positive(t *testing.T) { return nil } CreateFile = func(path string) (*os.File, error) { - return &os.File{}, nil + return os.CreateTemp("", "test") } Chmod = func(path string, perm os.FileMode) error { return nil @@ -242,7 +242,7 @@ func TestMountpointS3Mount_WorkerNode_Negative(t *testing.T) { return nil } CreateFile = func(path string) (*os.File, error) { - return &os.File{}, nil + return os.CreateTemp("", "test") } Chmod = func(path string, perm os.FileMode) error { return nil @@ -523,7 +523,7 @@ func TestCreateS3MountConfig_Success(t *testing.T) { return nil } CreateFile = func(path string) (*os.File, error) { - return &os.File{}, nil + return os.CreateTemp("", "test") } Chmod = func(path string, perm os.FileMode) error { return nil @@ -578,7 +578,7 @@ func TestCreateS3MountConfig_ChmodFails(t *testing.T) { return nil } CreateFile = func(path string) (*os.File, error) { - return &os.File{}, nil + return os.CreateTemp("", "test") } Chmod = func(path string, perm os.FileMode) error { return errors.New("failed to chmod") From 6e379425194a7320d8d5d9a7967b9486486973f8 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 30 Mar 2026 13:49:14 +0530 Subject: [PATCH 040/128] Update variables detail --- pkg/mounter/mounter-s3mounter_test.go | 40 +++++++++++++-------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go index 07a125d2..b0d8e56c 100644 --- a/pkg/mounter/mounter-s3mounter_test.go +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -10,26 +10,26 @@ import ( var ( s3MounterSecretMap = map[string]string{ - "cosEndpoint": "https://s3.us-east.cloud-object-storage.appdomain.cloud", - "locationConstraint": "us-east", - "bucketName": "test-bucket", - "objectPath": "test-path", - "accessKey": "test-access-key", - "secretKey": "test-secret-key", - "uid": "1000", - "gid": "1000", - "logLevel": "debug", - "readOnly": "true", - "maxThreads": "32", - "readPartSize": "16777216", - "writePartSize": "16777216", - "maximumThroughputGbps": "20", - "uploadChecksums": "crc32c", - "cacheDir": "/tmp/cache", - "maxCacheSize": "1024", - "metadataTTL": "60", - "negativeMetadataTTL": "30", - "logMetrics": "true", + "cosEndpoint": "https://s3.us-east.cloud-object-storage.appdomain.cloud", + "locationConstraint": "us-east", + "bucketName": "test-bucket", + "objectPath": "test-path", + "accessKey": "test-access-key", + "secretKey": "test-secret-key", + "uid": "1000", + "gid": "1000", + "mountOptions": `log-level=debug +read-only +max-threads=32 +read-part-size=16777216 +write-part-size=16777216 +maximum-throughput-gbps=20 +upload-checksums=crc32c +cache=/tmp/cache +max-cache-size=1024 +metadata-ttl=60 +negative-metadata-ttl=30 +log-metrics`, } s3MounterMountOptions = []string{"--dir-mode=0755", "--file-mode=0644"} From e0f158bc8247068b6cd6d524d5c93ae5a4833654 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 30 Mar 2026 14:03:29 +0530 Subject: [PATCH 041/128] Remove assertion checks for some map variables --- pkg/mounter/mounter-s3mounter_test.go | 35 +++++++++++++++------------ 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go index b0d8e56c..9f84c834 100644 --- a/pkg/mounter/mounter-s3mounter_test.go +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -49,7 +49,7 @@ func TestNewMountpointS3Mounter_Success(t *testing.T) { assert.Equal(t, s3MounterSecretMap["secretKey"], s3Mounter.SecretKey) assert.Equal(t, s3MounterSecretMap["uid"], s3Mounter.UID) assert.Equal(t, s3MounterSecretMap["gid"], s3Mounter.GID) - assert.Equal(t, s3MounterSecretMap["logLevel"], s3Mounter.LogLevel) + assert.Equal(t, "debug", s3Mounter.LogLevel) assert.True(t, s3Mounter.ReadOnly) assert.Equal(t, "hmac", s3Mounter.AuthType) } @@ -60,15 +60,15 @@ func TestNewMountpointS3Mounter_PerformanceTuning(t *testing.T) { s3Mounter, ok := mounter.(*MountpointS3Mounter) assert.True(t, ok) - assert.Equal(t, s3MounterSecretMap["maxThreads"], s3Mounter.MaxThreads) - assert.Equal(t, s3MounterSecretMap["readPartSize"], s3Mounter.ReadPartSize) - assert.Equal(t, s3MounterSecretMap["writePartSize"], s3Mounter.WritePartSize) - assert.Equal(t, s3MounterSecretMap["maximumThroughputGbps"], s3Mounter.MaxThroughputGbps) - assert.Equal(t, s3MounterSecretMap["uploadChecksums"], s3Mounter.UploadChecksums) - assert.Equal(t, s3MounterSecretMap["cacheDir"], s3Mounter.CacheDir) - assert.Equal(t, s3MounterSecretMap["maxCacheSize"], s3Mounter.MaxCacheSize) - assert.Equal(t, s3MounterSecretMap["metadataTTL"], s3Mounter.MetadataTTL) - assert.Equal(t, s3MounterSecretMap["negativeMetadataTTL"], s3Mounter.NegativeMetadataTTL) + assert.Equal(t, "32", s3Mounter.MaxThreads) + assert.Equal(t, "16777216", s3Mounter.ReadPartSize) + assert.Equal(t, "16777216", s3Mounter.WritePartSize) + assert.Equal(t, "20", s3Mounter.MaxThroughputGbps) + assert.Equal(t, "crc32c", s3Mounter.UploadChecksums) + assert.Equal(t, "/tmp/cache", s3Mounter.CacheDir) + assert.Equal(t, "1024", s3Mounter.MaxCacheSize) + assert.Equal(t, "60", s3Mounter.MetadataTTL) + assert.Equal(t, "30", s3Mounter.NegativeMetadataTTL) assert.True(t, s3Mounter.LogMetrics) } @@ -112,12 +112,17 @@ func TestMountpointS3Mount_NodeServer_Positive(t *testing.T) { return nil } - s3Mounter := &MountpointS3Mounter{ + // Create a mock that implements envMounter interface + mockUtils := &mockEnvMounter{ MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ FuseMountFn: func(path, comm string, args []string) error { return nil }, }), + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mockUtils, BucketName: "test-bucket", ObjectPath: "test-path", EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", @@ -211,7 +216,7 @@ func TestMountpointS3Mount_CreateDirFails_Negative(t *testing.T) { err := s3Mounter.Mount(source, target) assert.Error(t, err) - assert.Contains(t, err.Error(), "Cannot create config dir") + assert.Contains(t, err.Error(), "failed to create directory") } func TestMountpointS3Mount_CreateCredFileFails_Negative(t *testing.T) { @@ -552,7 +557,7 @@ func TestCreateS3MountConfig_MakeDirFails(t *testing.T) { err := createS3MountConfig("/config/path", s3Mounter) assert.Error(t, err) - assert.Contains(t, err.Error(), "Cannot create config dir") + assert.Contains(t, err.Error(), "failed to create directory") } func TestCreateS3MountConfig_CreateFileFails(t *testing.T) { @@ -570,7 +575,7 @@ func TestCreateS3MountConfig_CreateFileFails(t *testing.T) { err := createS3MountConfig("/config/path", s3Mounter) assert.Error(t, err) - assert.Contains(t, err.Error(), "Cannot write credentials file") + assert.Contains(t, err.Error(), "failed to create file") } func TestCreateS3MountConfig_ChmodFails(t *testing.T) { @@ -591,7 +596,7 @@ func TestCreateS3MountConfig_ChmodFails(t *testing.T) { err := createS3MountConfig("/config/path", s3Mounter) assert.Error(t, err) - assert.Contains(t, err.Error(), "Cannot write credentials file") + assert.Contains(t, err.Error(), "failed to chmod") } // mockEnvMounter implements the envMounter interface for testing From 013e5e04047e3edf3947ba23a6ce7211d2324e7b Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Mon, 6 Apr 2026 00:47:40 +0530 Subject: [PATCH 042/128] publish 0.12.19 version --- pkg/mounter/mounter-s3mounter_test.go | 113 ++++++++++++-------------- 1 file changed, 51 insertions(+), 62 deletions(-) diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go index 9f84c834..b5643c3d 100644 --- a/pkg/mounter/mounter-s3mounter_test.go +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -54,22 +54,25 @@ func TestNewMountpointS3Mounter_Success(t *testing.T) { assert.Equal(t, "hmac", s3Mounter.AuthType) } -func TestNewMountpointS3Mounter_PerformanceTuning(t *testing.T) { +func TestNewMountpointS3Mounter_PassthroughOptions(t *testing.T) { mounter := NewMountpointS3Mounter(s3MounterSecretMap, s3MounterMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) s3Mounter, ok := mounter.(*MountpointS3Mounter) assert.True(t, ok) - assert.Equal(t, "32", s3Mounter.MaxThreads) - assert.Equal(t, "16777216", s3Mounter.ReadPartSize) - assert.Equal(t, "16777216", s3Mounter.WritePartSize) - assert.Equal(t, "20", s3Mounter.MaxThroughputGbps) - assert.Equal(t, "crc32c", s3Mounter.UploadChecksums) + // Verify passthrough options are preserved with -- prefix + assert.Contains(t, s3Mounter.MountOptions, "--max-threads=32") + assert.Contains(t, s3Mounter.MountOptions, "--read-part-size=16777216") + assert.Contains(t, s3Mounter.MountOptions, "--write-part-size=16777216") + assert.Contains(t, s3Mounter.MountOptions, "--maximum-throughput-gbps=20") + assert.Contains(t, s3Mounter.MountOptions, "--upload-checksums=crc32c") + assert.Contains(t, s3Mounter.MountOptions, "--max-cache-size=1024") + assert.Contains(t, s3Mounter.MountOptions, "--metadata-ttl=60") + assert.Contains(t, s3Mounter.MountOptions, "--negative-metadata-ttl=30") + assert.Contains(t, s3Mounter.MountOptions, "--log-metrics") + + // Verify structured fields are set correctly assert.Equal(t, "/tmp/cache", s3Mounter.CacheDir) - assert.Equal(t, "1024", s3Mounter.MaxCacheSize) - assert.Equal(t, "60", s3Mounter.MetadataTTL) - assert.Equal(t, "30", s3Mounter.NegativeMetadataTTL) - assert.True(t, s3Mounter.LogMetrics) } func TestNewMountpointS3Mounter_GidWithoutUid(t *testing.T) { @@ -111,18 +114,16 @@ func TestMountpointS3Mount_NodeServer_Positive(t *testing.T) { Chmod = func(path string, perm os.FileMode) error { return nil } + Stat = func(path string) (os.FileInfo, error) { + return nil, nil + } - // Create a mock that implements envMounter interface - mockUtils := &mockEnvMounter{ + s3Mounter := &MountpointS3Mounter{ MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ FuseMountFn: func(path, comm string, args []string) error { return nil }, }), - } - - s3Mounter := &MountpointS3Mounter{ - MounterUtils: mockUtils, BucketName: "test-bucket", ObjectPath: "test-path", EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", @@ -152,14 +153,12 @@ func TestMountpointS3Mount_NodeServer_WithEnvMounter(t *testing.T) { Chmod = func(path string, perm os.FileMode) error { return nil } - - // Create a mock that implements envMounter interface - mockUtils := &mockEnvMounter{ - MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), + Stat = func(path string) (os.FileInfo, error) { + return nil, nil } s3Mounter := &MountpointS3Mounter{ - MounterUtils: mockUtils, + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), BucketName: "test-bucket", EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", LocConstraint: "us-east", @@ -398,30 +397,33 @@ func TestRemoveS3MountConfigFile_Negative(t *testing.T) { func TestFormulateMountOptions_AllOptions(t *testing.T) { s3Mounter := &MountpointS3Mounter{ - BucketName: "test-bucket", - ObjectPath: "test-path", - EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", - LocConstraint: "us-east", - UID: "1000", - GID: "2000", - LogLevel: "debug", - ReadOnly: true, - MaxThreads: "32", - ReadPartSize: "16777216", - WritePartSize: "16777216", - MaxThroughputGbps: "20", - UploadChecksums: "crc32c", - CacheDir: "/tmp/cache", - MaxCacheSize: "1024", - MetadataTTL: "60", - NegativeMetadataTTL: "30", - LogMetrics: true, - MountOptions: []string{"--dir-mode=0755"}, + BucketName: "test-bucket", + ObjectPath: "test-path", + EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", + LocConstraint: "us-east", + UID: "1000", + GID: "2000", + LogLevel: "debug", + LogDirectory: "/var/log/mount-s3", + CacheDir: "/tmp/cache", + ReadOnly: true, + MountOptions: []string{ + "--dir-mode=0755", + "--max-threads=32", + "--read-part-size=16777216", + "--write-part-size=16777216", + "--maximum-throughput-gbps=20", + "--upload-checksums=crc32c", + "--max-cache-size=1024", + "--metadata-ttl=60", + "--negative-metadata-ttl=30", + "--log-metrics", + }, } nodeServerOp, envVars, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket/test-path", target, "/config/path") - // Check node server options + // Check node server options - structured fields assert.Contains(t, nodeServerOp, "test-bucket/test-path") assert.Contains(t, nodeServerOp, target) assert.Contains(t, nodeServerOp, "--allow-other") @@ -430,42 +432,37 @@ func TestFormulateMountOptions_AllOptions(t *testing.T) { assert.Contains(t, nodeServerOp, "--uid=1000") assert.Contains(t, nodeServerOp, "--gid=2000") assert.Contains(t, nodeServerOp, "--debug") + assert.Contains(t, nodeServerOp, "--log-directory=/var/log/mount-s3") + assert.Contains(t, nodeServerOp, "--cache=/tmp/cache") assert.Contains(t, nodeServerOp, "--read-only") + + // Check node server options - passthrough fields + assert.Contains(t, nodeServerOp, "--dir-mode=0755") assert.Contains(t, nodeServerOp, "--max-threads=32") assert.Contains(t, nodeServerOp, "--read-part-size=16777216") assert.Contains(t, nodeServerOp, "--write-part-size=16777216") assert.Contains(t, nodeServerOp, "--maximum-throughput-gbps=20") assert.Contains(t, nodeServerOp, "--upload-checksums=crc32c") - assert.Contains(t, nodeServerOp, "--cache=/tmp/cache") assert.Contains(t, nodeServerOp, "--max-cache-size=1024") assert.Contains(t, nodeServerOp, "--metadata-ttl=60") assert.Contains(t, nodeServerOp, "--negative-metadata-ttl=30") assert.Contains(t, nodeServerOp, "--log-metrics") - assert.Contains(t, nodeServerOp, "--dir-mode=0755") // Check env vars assert.Len(t, envVars, 2) assert.Contains(t, envVars[0], "AWS_SHARED_CREDENTIALS_FILE=") assert.Contains(t, envVars[1], "AWS_CONFIG_FILE=") - // Check worker node options + // Check worker node options - only structured fields are in the struct assert.Equal(t, "true", workerNodeOp.AllowOther) assert.Equal(t, "https://s3.us-east.cloud-object-storage.appdomain.cloud", workerNodeOp.EndpointURL) assert.Equal(t, "us-east", workerNodeOp.Region) assert.Equal(t, "1000", workerNodeOp.UID) assert.Equal(t, "2000", workerNodeOp.GID) assert.Equal(t, "debug", workerNodeOp.LogLevel) - assert.Equal(t, "true", workerNodeOp.ReadOnly) - assert.Equal(t, "32", workerNodeOp.MaxThreads) - assert.Equal(t, "16777216", workerNodeOp.ReadPartSize) - assert.Equal(t, "16777216", workerNodeOp.WritePartSize) - assert.Equal(t, "20", workerNodeOp.MaxThroughputGbps) - assert.Equal(t, "crc32c", workerNodeOp.UploadChecksums) + assert.Equal(t, "/var/log/mount-s3", workerNodeOp.LogDirectory) assert.Equal(t, "/tmp/cache", workerNodeOp.CacheDir) - assert.Equal(t, "1024", workerNodeOp.MaxCacheSize) - assert.Equal(t, "60", workerNodeOp.MetadataTTL) - assert.Equal(t, "30", workerNodeOp.NegativeMetadataTTL) - assert.True(t, workerNodeOp.LogMetrics) + assert.Equal(t, "true", workerNodeOp.ReadOnly) } func TestFormulateMountOptions_LogLevelDebugCrt(t *testing.T) { @@ -599,11 +596,3 @@ func TestCreateS3MountConfig_ChmodFails(t *testing.T) { assert.Contains(t, err.Error(), "failed to chmod") } -// mockEnvMounter implements the envMounter interface for testing -type mockEnvMounter struct { - mounterUtils.MounterUtils -} - -func (m *mockEnvMounter) FuseMountWithEnv(path string, comm string, args []string, envVars []string) error { - return nil -} From 088ea349157555eef068e9065782f5ab1d3a862d Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Mon, 6 Apr 2026 00:59:12 +0530 Subject: [PATCH 043/128] publish 0.12.19 version --- pkg/mounter/mounter-s3mounter_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go index b5643c3d..69560655 100644 --- a/pkg/mounter/mounter-s3mounter_test.go +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -121,6 +121,7 @@ func TestMountpointS3Mount_NodeServer_Positive(t *testing.T) { s3Mounter := &MountpointS3Mounter{ MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ FuseMountFn: func(path, comm string, args []string) error { + // Mock successful mount return nil }, }), @@ -158,7 +159,12 @@ func TestMountpointS3Mount_NodeServer_WithEnvMounter(t *testing.T) { } s3Mounter := &MountpointS3Mounter{ - MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseMountFn: func(path, comm string, args []string) error { + // Mock successful mount with env vars + return nil + }, + }), BucketName: "test-bucket", EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", LocConstraint: "us-east", From 00ad6440cf513f97d8bfa5d596dba1a4720a71bb Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Mon, 6 Apr 2026 10:28:15 +0530 Subject: [PATCH 044/128] publish 0.12.19 version --- pkg/mounter/mounter-s3mounter_test.go | 604 -------------------------- 1 file changed, 604 deletions(-) delete mode 100644 pkg/mounter/mounter-s3mounter_test.go diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go deleted file mode 100644 index 69560655..00000000 --- a/pkg/mounter/mounter-s3mounter_test.go +++ /dev/null @@ -1,604 +0,0 @@ -package mounter - -import ( - "errors" - "os" - "testing" - mounterUtils "github.com/IBM/ibm-object-csi-driver/pkg/mounter/utils" - "github.com/stretchr/testify/assert" -) - -var ( - s3MounterSecretMap = map[string]string{ - "cosEndpoint": "https://s3.us-east.cloud-object-storage.appdomain.cloud", - "locationConstraint": "us-east", - "bucketName": "test-bucket", - "objectPath": "test-path", - "accessKey": "test-access-key", - "secretKey": "test-secret-key", - "uid": "1000", - "gid": "1000", - "mountOptions": `log-level=debug -read-only -max-threads=32 -read-part-size=16777216 -write-part-size=16777216 -maximum-throughput-gbps=20 -upload-checksums=crc32c -cache=/tmp/cache -max-cache-size=1024 -metadata-ttl=60 -negative-metadata-ttl=30 -log-metrics`, - } - - s3MounterMountOptions = []string{"--dir-mode=0755", "--file-mode=0644"} -) - -func TestNewMountpointS3Mounter_Success(t *testing.T) { - mounter := NewMountpointS3Mounter(s3MounterSecretMap, s3MounterMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) - - s3Mounter, ok := mounter.(*MountpointS3Mounter) - assert.True(t, ok) - - assert.Equal(t, s3MounterSecretMap["bucketName"], s3Mounter.BucketName) - assert.Equal(t, s3MounterSecretMap["objectPath"], s3Mounter.ObjectPath) - assert.Equal(t, s3MounterSecretMap["cosEndpoint"], s3Mounter.EndPoint) - assert.Equal(t, s3MounterSecretMap["locationConstraint"], s3Mounter.LocConstraint) - assert.Equal(t, s3MounterSecretMap["accessKey"], s3Mounter.AccessKey) - assert.Equal(t, s3MounterSecretMap["secretKey"], s3Mounter.SecretKey) - assert.Equal(t, s3MounterSecretMap["uid"], s3Mounter.UID) - assert.Equal(t, s3MounterSecretMap["gid"], s3Mounter.GID) - assert.Equal(t, "debug", s3Mounter.LogLevel) - assert.True(t, s3Mounter.ReadOnly) - assert.Equal(t, "hmac", s3Mounter.AuthType) -} - -func TestNewMountpointS3Mounter_PassthroughOptions(t *testing.T) { - mounter := NewMountpointS3Mounter(s3MounterSecretMap, s3MounterMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) - - s3Mounter, ok := mounter.(*MountpointS3Mounter) - assert.True(t, ok) - - // Verify passthrough options are preserved with -- prefix - assert.Contains(t, s3Mounter.MountOptions, "--max-threads=32") - assert.Contains(t, s3Mounter.MountOptions, "--read-part-size=16777216") - assert.Contains(t, s3Mounter.MountOptions, "--write-part-size=16777216") - assert.Contains(t, s3Mounter.MountOptions, "--maximum-throughput-gbps=20") - assert.Contains(t, s3Mounter.MountOptions, "--upload-checksums=crc32c") - assert.Contains(t, s3Mounter.MountOptions, "--max-cache-size=1024") - assert.Contains(t, s3Mounter.MountOptions, "--metadata-ttl=60") - assert.Contains(t, s3Mounter.MountOptions, "--negative-metadata-ttl=30") - assert.Contains(t, s3Mounter.MountOptions, "--log-metrics") - - // Verify structured fields are set correctly - assert.Equal(t, "/tmp/cache", s3Mounter.CacheDir) -} - -func TestNewMountpointS3Mounter_GidWithoutUid(t *testing.T) { - secretMap := map[string]string{ - "gid": "2000", - } - - mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) - - s3Mounter, ok := mounter.(*MountpointS3Mounter) - assert.True(t, ok) - assert.Equal(t, "2000", s3Mounter.UID) - assert.Equal(t, "2000", s3Mounter.GID) -} - -func TestNewMountpointS3Mounter_UidOverridesGid(t *testing.T) { - secretMap := map[string]string{ - "uid": "1000", - "gid": "2000", - } - - mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) - - s3Mounter, ok := mounter.(*MountpointS3Mounter) - assert.True(t, ok) - assert.Equal(t, "1000", s3Mounter.UID) - assert.Equal(t, "2000", s3Mounter.GID) -} - -func TestMountpointS3Mount_NodeServer_Positive(t *testing.T) { - mountWorker = false - - MakeDir = func(path string, perm os.FileMode) error { - return nil - } - CreateFile = func(path string) (*os.File, error) { - return os.CreateTemp("", "test") - } - Chmod = func(path string, perm os.FileMode) error { - return nil - } - Stat = func(path string) (os.FileInfo, error) { - return nil, nil - } - - s3Mounter := &MountpointS3Mounter{ - MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ - FuseMountFn: func(path, comm string, args []string) error { - // Mock successful mount - return nil - }, - }), - BucketName: "test-bucket", - ObjectPath: "test-path", - EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", - LocConstraint: "us-east", - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - UID: "1000", - GID: "1000", - LogLevel: "debug", - ReadOnly: true, - MountOptions: s3MounterMountOptions, - } - - err := s3Mounter.Mount(source, target) - assert.NoError(t, err) -} - -func TestMountpointS3Mount_NodeServer_WithEnvMounter(t *testing.T) { - mountWorker = false - - MakeDir = func(path string, perm os.FileMode) error { - return nil - } - CreateFile = func(path string) (*os.File, error) { - return os.CreateTemp("", "test") - } - Chmod = func(path string, perm os.FileMode) error { - return nil - } - Stat = func(path string) (os.FileInfo, error) { - return nil, nil - } - - s3Mounter := &MountpointS3Mounter{ - MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ - FuseMountFn: func(path, comm string, args []string) error { - // Mock successful mount with env vars - return nil - }, - }), - BucketName: "test-bucket", - EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", - LocConstraint: "us-east", - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - MountOptions: s3MounterMountOptions, - } - - err := s3Mounter.Mount(source, target) - assert.NoError(t, err) -} - -func TestMountpointS3Mount_WorkerNode_Positive(t *testing.T) { - mountWorker = true - - MakeDir = func(path string, perm os.FileMode) error { - return nil - } - CreateFile = func(path string) (*os.File, error) { - return os.CreateTemp("", "test") - } - Chmod = func(path string, perm os.FileMode) error { - return nil - } - mounterRequest = func(payload, url string) error { - return nil - } - - s3Mounter := &MountpointS3Mounter{ - MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), - BucketName: "test-bucket", - EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", - LocConstraint: "us-east", - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - } - - err := s3Mounter.Mount(source, target) - assert.NoError(t, err) -} - -func TestMountpointS3Mount_CreateDirFails_Negative(t *testing.T) { - mountWorker = false - - MakeDir = func(path string, perm os.FileMode) error { - return errors.New("failed to create directory") - } - - s3Mounter := &MountpointS3Mounter{ - BucketName: "test-bucket", - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - } - - err := s3Mounter.Mount(source, target) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to create directory") -} - -func TestMountpointS3Mount_CreateCredFileFails_Negative(t *testing.T) { - mountWorker = false - - MakeDir = func(path string, perm os.FileMode) error { - return nil - } - CreateFile = func(path string) (*os.File, error) { - return nil, errors.New("failed to create file") - } - - s3Mounter := &MountpointS3Mounter{ - BucketName: "test-bucket", - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - } - - err := s3Mounter.Mount(source, target) - assert.Error(t, err) - assert.Contains(t, err.Error(), "cannot create file") -} - -func TestMountpointS3Mount_WorkerNode_Negative(t *testing.T) { - mountWorker = true - - MakeDir = func(path string, perm os.FileMode) error { - return nil - } - CreateFile = func(path string) (*os.File, error) { - return os.CreateTemp("", "test") - } - Chmod = func(path string, perm os.FileMode) error { - return nil - } - mounterRequest = func(payload, url string) error { - return errors.New("failed to perform http request") - } - - s3Mounter := &MountpointS3Mounter{ - BucketName: "test-bucket", - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - } - - err := s3Mounter.Mount(source, target) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to perform http request") -} - -func TestMountpointS3Unmount_NodeServer(t *testing.T) { - mountWorker = false - - removeS3ConfigFile = func(configPath, target string) {} - - s3Mounter := &MountpointS3Mounter{ - MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ - FuseUnmountFn: func(path string) error { - return nil - }, - }), - } - - err := s3Mounter.Unmount(target) - assert.NoError(t, err) -} - -func TestMountpointS3Unmount_WorkerNode(t *testing.T) { - mountWorker = true - - removeS3ConfigFile = func(configPath, target string) {} - - mounterRequest = func(payload, url string) error { - return nil - } - - s3Mounter := &MountpointS3Mounter{ - MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ - FuseUnmountFn: func(path string) error { - return nil - }, - }), - } - - err := s3Mounter.Unmount(target) - assert.NoError(t, err) -} - -func TestMountpointS3Unmount_WorkerNode_Negative(t *testing.T) { - mountWorker = true - - mounterRequest = func(payload, url string) error { - return errors.New("failed to create http request") - } - - s3Mounter := &MountpointS3Mounter{ - MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ - FuseUnmountFn: func(path string) error { - return nil - }, - }), - } - - err := s3Mounter.Unmount(target) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to create http request") -} - -func TestMountpointS3Unmount_NodeServer_Negative(t *testing.T) { - mountWorker = false - - removeS3ConfigFile = func(configPath, target string) {} - - s3Mounter := &MountpointS3Mounter{ - MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ - FuseUnmountFn: func(path string) error { - return errors.New("failed to unmount") - }, - }), - } - - err := s3Mounter.Unmount(target) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to unmount") -} - -func TestRemoveS3MountConfigFile_PathNotExists(t *testing.T) { - Stat = func(path string) (os.FileInfo, error) { - return nil, os.ErrNotExist - } - removeS3MountConfigFile("/test", target) -} - -func TestRemoveS3MountConfigFile_StatRetryThenSuccess(t *testing.T) { - attempt := 0 - Stat = func(_ string) (os.FileInfo, error) { - if attempt == 0 { - attempt++ - return nil, errors.New("stat error") - } - return nil, nil - } - RemoveAll = func(_ string) error { - return nil - } - - removeS3MountConfigFile("/test", target) -} - -func TestRemoveS3MountConfigFile_RemoveRetryThenSuccess(t *testing.T) { - Stat = func(_ string) (os.FileInfo, error) { - return nil, nil - } - attempt := 0 - RemoveAll = func(_ string) error { - if attempt == 0 { - attempt++ - return errors.New("remove error") - } - return nil - } - - removeS3MountConfigFile("/test", target) -} - -func TestRemoveS3MountConfigFile_Negative(t *testing.T) { - called := 0 - Stat = func(_ string) (os.FileInfo, error) { - return nil, nil - } - RemoveAll = func(_ string) error { - called++ - return errors.New("remove failed") - } - - removeS3MountConfigFile("/test", target) - assert.Equal(t, maxRetries, called) -} - -func TestFormulateMountOptions_AllOptions(t *testing.T) { - s3Mounter := &MountpointS3Mounter{ - BucketName: "test-bucket", - ObjectPath: "test-path", - EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", - LocConstraint: "us-east", - UID: "1000", - GID: "2000", - LogLevel: "debug", - LogDirectory: "/var/log/mount-s3", - CacheDir: "/tmp/cache", - ReadOnly: true, - MountOptions: []string{ - "--dir-mode=0755", - "--max-threads=32", - "--read-part-size=16777216", - "--write-part-size=16777216", - "--maximum-throughput-gbps=20", - "--upload-checksums=crc32c", - "--max-cache-size=1024", - "--metadata-ttl=60", - "--negative-metadata-ttl=30", - "--log-metrics", - }, - } - - nodeServerOp, envVars, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket/test-path", target, "/config/path") - - // Check node server options - structured fields - assert.Contains(t, nodeServerOp, "test-bucket/test-path") - assert.Contains(t, nodeServerOp, target) - assert.Contains(t, nodeServerOp, "--allow-other") - assert.Contains(t, nodeServerOp, "--endpoint-url=https://s3.us-east.cloud-object-storage.appdomain.cloud") - assert.Contains(t, nodeServerOp, "--region=us-east") - assert.Contains(t, nodeServerOp, "--uid=1000") - assert.Contains(t, nodeServerOp, "--gid=2000") - assert.Contains(t, nodeServerOp, "--debug") - assert.Contains(t, nodeServerOp, "--log-directory=/var/log/mount-s3") - assert.Contains(t, nodeServerOp, "--cache=/tmp/cache") - assert.Contains(t, nodeServerOp, "--read-only") - - // Check node server options - passthrough fields - assert.Contains(t, nodeServerOp, "--dir-mode=0755") - assert.Contains(t, nodeServerOp, "--max-threads=32") - assert.Contains(t, nodeServerOp, "--read-part-size=16777216") - assert.Contains(t, nodeServerOp, "--write-part-size=16777216") - assert.Contains(t, nodeServerOp, "--maximum-throughput-gbps=20") - assert.Contains(t, nodeServerOp, "--upload-checksums=crc32c") - assert.Contains(t, nodeServerOp, "--max-cache-size=1024") - assert.Contains(t, nodeServerOp, "--metadata-ttl=60") - assert.Contains(t, nodeServerOp, "--negative-metadata-ttl=30") - assert.Contains(t, nodeServerOp, "--log-metrics") - - // Check env vars - assert.Len(t, envVars, 2) - assert.Contains(t, envVars[0], "AWS_SHARED_CREDENTIALS_FILE=") - assert.Contains(t, envVars[1], "AWS_CONFIG_FILE=") - - // Check worker node options - only structured fields are in the struct - assert.Equal(t, "true", workerNodeOp.AllowOther) - assert.Equal(t, "https://s3.us-east.cloud-object-storage.appdomain.cloud", workerNodeOp.EndpointURL) - assert.Equal(t, "us-east", workerNodeOp.Region) - assert.Equal(t, "1000", workerNodeOp.UID) - assert.Equal(t, "2000", workerNodeOp.GID) - assert.Equal(t, "debug", workerNodeOp.LogLevel) - assert.Equal(t, "/var/log/mount-s3", workerNodeOp.LogDirectory) - assert.Equal(t, "/tmp/cache", workerNodeOp.CacheDir) - assert.Equal(t, "true", workerNodeOp.ReadOnly) -} - -func TestFormulateMountOptions_LogLevelDebugCrt(t *testing.T) { - s3Mounter := &MountpointS3Mounter{ - LogLevel: "debug-crt", - } - - nodeServerOp, _, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") - - assert.Contains(t, nodeServerOp, "--debug-crt") - assert.Equal(t, "debug-crt", workerNodeOp.LogLevel) -} - -func TestFormulateMountOptions_LogLevelNoLog(t *testing.T) { - s3Mounter := &MountpointS3Mounter{ - LogLevel: "no-log", - } - - nodeServerOp, _, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") - - assert.Contains(t, nodeServerOp, "--no-log") - assert.Equal(t, "no-log", workerNodeOp.LogLevel) -} - -func TestFormulateMountOptions_InvalidLogLevel(t *testing.T) { - s3Mounter := &MountpointS3Mounter{ - LogLevel: "invalid-level", - } - - nodeServerOp, _, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") - - // Should not contain any log level flag - assert.NotContains(t, nodeServerOp, "--debug") - assert.NotContains(t, nodeServerOp, "--debug-crt") - assert.NotContains(t, nodeServerOp, "--no-log") - assert.Empty(t, workerNodeOp.LogLevel) -} - -func TestFormulateMountOptions_MinimalConfig(t *testing.T) { - s3Mounter := &MountpointS3Mounter{ - MountOptions: []string{}, - } - - nodeServerOp, envVars, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") - - // Check basic options are present - assert.Contains(t, nodeServerOp, "test-bucket") - assert.Contains(t, nodeServerOp, target) - assert.Contains(t, nodeServerOp, "--allow-other") - - // Check env vars - assert.Len(t, envVars, 2) - - // Check worker node options - assert.Equal(t, "true", workerNodeOp.AllowOther) -} - -func TestCreateS3MountConfig_Success(t *testing.T) { - MakeDir = func(path string, perm os.FileMode) error { - return nil - } - CreateFile = func(path string) (*os.File, error) { - return os.CreateTemp("", "test") - } - Chmod = func(path string, perm os.FileMode) error { - return nil - } - - s3Mounter := &MountpointS3Mounter{ - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - LocConstraint: "us-east", - EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", - } - - err := createS3MountConfig("/config/path", s3Mounter) - assert.NoError(t, err) -} - -func TestCreateS3MountConfig_MakeDirFails(t *testing.T) { - MakeDir = func(path string, perm os.FileMode) error { - return errors.New("failed to create directory") - } - - s3Mounter := &MountpointS3Mounter{ - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - } - - err := createS3MountConfig("/config/path", s3Mounter) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to create directory") -} - -func TestCreateS3MountConfig_CreateFileFails(t *testing.T) { - MakeDir = func(path string, perm os.FileMode) error { - return nil - } - CreateFile = func(path string) (*os.File, error) { - return nil, errors.New("failed to create file") - } - - s3Mounter := &MountpointS3Mounter{ - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - } - - err := createS3MountConfig("/config/path", s3Mounter) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to create file") -} - -func TestCreateS3MountConfig_ChmodFails(t *testing.T) { - MakeDir = func(path string, perm os.FileMode) error { - return nil - } - CreateFile = func(path string) (*os.File, error) { - return os.CreateTemp("", "test") - } - Chmod = func(path string, perm os.FileMode) error { - return errors.New("failed to chmod") - } - - s3Mounter := &MountpointS3Mounter{ - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - } - - err := createS3MountConfig("/config/path", s3Mounter) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to chmod") -} - From c9694ef7e8edc80096d7bc033f13bcdd6bfc0561 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Tue, 7 Apr 2026 00:55:00 +0530 Subject: [PATCH 045/128] publish 0.12.20 version --- .github/workflows/release.yml | 6 +++--- cos-csi-mounter/Makefile | 2 +- pkg/mounter/mounter_s3mounter.go | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 70d76a2a..e9032832 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.19 + APP_VERSION: 0.12.20 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.19 - name: v0.12.19 + tag_name: v0.12.20 + name: v0.12.20 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index 45d2435e..f0cacf4e 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.19 +APP_VERSION := 0.12.20 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index c526e335..815d42c9 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -469,7 +469,7 @@ func (s3 *MountpointS3Mounter) Unmount(target string) error { // credentials <- AWS credentials (access/secret key) // config <- AWS config (region, endpoint_url) func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) error { - if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 + if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 -- 0755 permissions required for AWS config directory access klog.Errorf("MountpointS3Mounter: Cannot create config dir %s: %v", configPathWithVolID, err) return err } @@ -505,7 +505,7 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er // writeConfigFile writes lines to a file with 0600 permissions. func writeConfigFile(filePath string, lines []string) error { - f, err := CreateFile(filePath) // #nosec G304 + f, err := CreateFile(filePath) // #nosec G304 -- filePath is constructed from validated volume ID and config directory if err != nil { return fmt.Errorf("cannot create file %s: %w", filePath, err) } @@ -515,7 +515,7 @@ func writeConfigFile(filePath string, lines []string) error { } }() - if err := Chmod(filePath, 0600); err != nil { // #nosec G302 + if err := Chmod(filePath, 0600); err != nil { // #nosec G302 -- 0600 permissions required to protect AWS credentials file return fmt.Errorf("cannot chmod file %s: %w", filePath, err) } From b67313178ac4993aee616d659c010d0414edf802 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sat, 11 Apr 2026 03:32:12 +0530 Subject: [PATCH 046/128] publish 0.12.21 version --- .github/workflows/release.yml | 6 +- cos-csi-mounter/Makefile | 2 +- pkg/mounter/mounter_s3mounter.go | 174 +++++-------------------------- 3 files changed, 29 insertions(+), 153 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e9032832..c8c7c9e9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.20 + APP_VERSION: 0.12.21 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.20 - name: v0.12.20 + tag_name: v0.12.21 + name: v0.12.21 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index f0cacf4e..0f85b45a 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.20 +APP_VERSION := 0.12.21 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 815d42c9..07ee66fe 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -20,7 +20,7 @@ import ( // // Only flags that require code-level handling are modelled as struct fields: // - Conflict resolution : ReadOnly takes priority β€” clears AllowOverwrite / IncrementalUpload -// - Directory validation: LogDirectory / CacheDir (must exist before mount) +// - Directory creation : LogDirectory / CacheDir are created by the worker's Validate() // - Name translation : LogLevel ("debug" β†’ --debug, not --log-level=debug) // - Special precedence : UID / GID (secretMap < mountOptions) // - Always-emitted : allow-other (deduplicated here) @@ -51,12 +51,10 @@ type MountpointS3Mounter struct { // mount-s3 has no --log-level flag. LogLevel string - // LogDirectory must exist on the node before mount-s3 starts. - // Validated in Mount() before calling formulateMountOptions. + // LogDirectory is forwarded to the worker; worker's Validate() creates it via ensureDir. LogDirectory string - // CacheDir must exist on the node before mount-s3 starts. - // Validated in Mount() before calling formulateMountOptions. + // CacheDir is forwarded to the worker; worker's Validate() creates it via ensureDir. CacheDir string // Conflict resolution β€” ReadOnly takes priority over write flags. @@ -75,8 +73,9 @@ type MountpointS3Mounter struct { } // s3MounterArgs holds the args sent to the worker node daemon for mount-s3. -// Only structured fields are represented here; passthrough flags are embedded -// in the Args slice at the call site. +// Structured fields cover all named flags; Args carries any remaining +// passthrough flags (e.g. --allow-delete, --hello, --never=true). +// This struct must stay in sync with s3MounterArgs in the worker (cos-csi-mounter). type s3MounterArgs struct { // Always set. AllowOther string `json:"allow-other,omitempty"` @@ -102,6 +101,11 @@ type s3MounterArgs struct { ReadOnly string `json:"read-only,omitempty"` AllowOverwrite string `json:"allow-overwrite,omitempty"` IncrementalUpload string `json:"incremental-upload,omitempty"` + + // Passthrough flags β€” user-supplied flags not handled by structured fields + // (e.g. --allow-delete, --hello, --never=true). + // Worker's PopulateArgsSlice appends these last so they have highest precedence. + Args []string `json:"args,omitempty"` } const ( @@ -123,21 +127,6 @@ type envMounter interface { // NewMountpointS3Mounter creates a new MountpointS3Mounter from the unified // secret format. -// -// Source mapping and precedence (lowest β†’ highest): -// -// 1. secretMap identity keys β€” accessKey, secretKey, bucketName, objectPath, -// cosEndpoint, locationConstraint, uid, gid -// 2. StorageClass mountOptions (mountOptions []string arg) β€” parsed first -// 3. Secret mountOptions (secretMap["mountOptions"] multiline string) β€” parsed -// second and therefore wins over StorageClass on any overlapping flag -// -// Special case: if read-only is present in either source, it takes priority and -// clears allow-overwrite and incremental-upload regardless of where they came from. -// -// For passthrough flags (unrecognised by the structured parser), the same -// precedence applies: secret passthroughs are appended after SC passthroughs, -// so mount-s3 sees the secret value last (last-value-wins semantics). func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounterUtils utils.MounterUtils) Mounter { klog.Info("-newMountpointS3Mounter-") @@ -178,14 +167,9 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounter.AuthType = "hmac" // --- Merge and parse mountOptions --- - // Step 1: StorageClass mountOptions (already split by kubelet). scRemainder := parseMountpointS3Options(mounter, mountOptions) - - // Step 2: Secret mountOptions (multiline string, parsed after SC so it wins). secretOpts := splitSecretMountOptions(secretMap["mountOptions"]) secretRemainder := parseMountpointS3Options(mounter, secretOpts) - - // Step 3: SC passthroughs first, secret passthroughs last (last-value-wins). mounter.MountOptions = append(scRemainder, secretRemainder...) mounter.MounterUtils = mounterUtils @@ -197,14 +181,7 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, return mounter } -// splitSecretMountOptions splits the multiline mountOptions string from the -// secret into a []string suitable for parseMountpointS3Options. -// -// Rules: -// - Each non-empty line is one flag. -// - Lines beginning with '#' (after trimming) are comments β€” skipped. -// - Leading/trailing whitespace on each line is trimmed. -// - Blank lines are skipped. +// splitSecretMountOptions splits the multiline mountOptions string from the secret. func splitSecretMountOptions(raw string) []string { if raw == "" { return nil @@ -221,49 +198,19 @@ func splitSecretMountOptions(raw string) []string { } // parseMountpointS3Options parses mount-s3 options from the provided slice. -// -// Only flags that require code-level handling are matched by name β€” everything -// else is forwarded unchanged to mount-s3 via the returned remainder slice. -// -// The remainder always has a "--" prefix added when missing, so mount-s3 -// receives syntactically valid flags regardless of how the user wrote them. -// This handles both boolean flags ("allow-delete" β†’ "--allow-delete") and -// key-value pairs ("max-threads=32" β†’ "--max-threads=32") correctly because -// the original opt string is preserved and only the prefix is fixed. -// -// Rules: -// - Flags may be provided with or without a leading "--" prefix. -// - Boolean flags need no value. -// - Value flags must be in "flag=value" or "--flag=value" form. -// - "allow-other" is silently consumed β€” formulateMountOptions always adds it. -// - "umask" is consumed with a warning β€” mount-s3 has no --umask flag. -// - uid/gid override secretMap values (caller sets secretMap values first). func parseMountpointS3Options(mounter *MountpointS3Mounter, opts []string) []string { var remaining []string for _, opt := range opts { - // Normalise: strip optional leading "--" so switch cases are uniform. trimmed := strings.TrimPrefix(opt, "--") key, value, hasValue := strings.Cut(trimmed, "=") switch key { - - // ------------------------------------------------------------------ - // Always emitted β€” consume to avoid duplicates. - // ------------------------------------------------------------------ case "allow-other": // formulateMountOptions unconditionally adds --allow-other. - - // ------------------------------------------------------------------ - // Invalid for mount-s3 β€” warn and drop so mount-s3 doesn't fail. - // ------------------------------------------------------------------ case "umask": klog.Warningf("parseMountpointS3Options: 'umask' is not supported by mount-s3. " + "Use --dir-mode / --file-mode instead. Ignoring.") - - // ------------------------------------------------------------------ - // Identity β€” mountOptions values override secretMap values. - // ------------------------------------------------------------------ case "uid": if hasValue { mounter.UID = value @@ -272,20 +219,10 @@ func parseMountpointS3Options(mounter *MountpointS3Mounter, opts []string) []str if hasValue { mounter.GID = value } - - // ------------------------------------------------------------------ - // Log level β€” requires name translation in formulateMountOptions. - // mount-s3 has no --log-level flag; valid values map to separate flags: - // "debug" β†’ --debug, "debug-crt" β†’ --debug-crt, "no-log" β†’ --no-log - // ------------------------------------------------------------------ case "log-level": if hasValue { mounter.LogLevel = value } - - // ------------------------------------------------------------------ - // Directory flags β€” validated for existence before mount-s3 starts. - // ------------------------------------------------------------------ case "log-directory": if hasValue { mounter.LogDirectory = value @@ -294,26 +231,12 @@ func parseMountpointS3Options(mounter *MountpointS3Mounter, opts []string) []str if hasValue { mounter.CacheDir = value } - - // ------------------------------------------------------------------ - // Conflict trio β€” tracked for read-only priority resolution in Mount(). - // read-only clears allow-overwrite and incremental-upload if all three - // are set, so SC defaults don't block users from requesting read-only. - // ------------------------------------------------------------------ case "read-only": mounter.ReadOnly = true case "allow-overwrite": mounter.AllowOverwrite = true case "incremental-upload": mounter.IncrementalUpload = true - - // ------------------------------------------------------------------ - // Everything else β€” pass through to mount-s3 unchanged. - // Ensure "--" prefix so mount-s3 always receives a valid flag. - // Works for both boolean flags and key-value pairs: - // "allow-delete" β†’ "--allow-delete" - // "max-threads=32" β†’ "--max-threads=32" - // ------------------------------------------------------------------ default: if strings.HasPrefix(opt, "--") { remaining = append(remaining, opt) @@ -332,11 +255,6 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { klog.Infof("Mount args:\n\tsource: <%s>\n\ttarget: <%s>", source, target) // --- Read-only priority resolution --- - // If read-only is set, it takes priority over all write-related flags. - // AllowOverwrite and IncrementalUpload are cleared here, and allow-delete - // is filtered from MountOptions, so the mount succeeds cleanly in read-only mode. - // This means users can always request read-only via their secret even when - // the StorageClass has write flags as defaults. if s3.ReadOnly { if s3.AllowOverwrite { klog.Infof("MountpointS3Mounter: read-only set β€” clearing allow-overwrite") @@ -347,7 +265,6 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { s3.IncrementalUpload = false } - // Filter out allow-delete from passthrough options var filtered []string for _, opt := range s3.MountOptions { trimmed := strings.TrimPrefix(opt, "--") @@ -360,21 +277,9 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { s3.MountOptions = filtered } - // --- Directory validation --- - // mount-s3 fails with a confusing error if these directories don't exist. - // Validate here to surface a clear error message before spawning the process. - if s3.LogDirectory != "" { - if _, err := Stat(s3.LogDirectory); err != nil { - return fmt.Errorf("MountpointS3Mounter: log-directory %q does not exist or is not accessible: %w", - s3.LogDirectory, err) - } - } - if s3.CacheDir != "" { - if _, err := Stat(s3.CacheDir); err != nil { - return fmt.Errorf("MountpointS3Mounter: cache directory %q does not exist or is not accessible: %w", - s3.CacheDir, err) - } - } + // NOTE: No directory validation or creation here. + // LogDirectory and CacheDir are forwarded to the worker via workerNodeOp. + // The worker's Validate() calls ensureDir() which creates them if missing. // --- Config path --- var configPath string @@ -391,7 +296,6 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { return err } - // Build bucket path with optional object path prefix. bucketName := s3.BucketName if s3.ObjectPath != "" { trimmedPath := strings.TrimPrefix(s3.ObjectPath, "/") @@ -412,6 +316,8 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { payload := fmt.Sprintf(`{"path":"%s","bucket":"%s","mounter":"%s","args":%s}`, target, bucketName, constants.AMAZONS3MOUNTER, jsonData) + klog.Infof("MountpointS3Mounter: worker payload: %s", payload) + err = mounterRequest(payload, "http://unix/api/cos/mount") if err != nil { klog.Errorf("failed to mount on worker: %v", err) @@ -426,8 +332,7 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { return m.FuseMountWithEnv(target, constants.MountpointS3BinaryPath, args, envVars) } - klog.Warning("MounterUtils does not implement FuseMountWithEnv, falling back to FuseMount. " + - "Ensure AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE are set in the process environment.") + klog.Warning("MounterUtils does not implement FuseMountWithEnv, falling back to FuseMount.") return s3.MounterUtils.FuseMount(target, constants.MountpointS3BinaryPath, args) } @@ -461,15 +366,8 @@ func (s3 *MountpointS3Mounter) Unmount(target string) error { } // createS3MountConfig creates the AWS credentials and config files. -// mount-s3 locates these via AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE env vars. -// -// Directory structure: -// -// / -// credentials <- AWS credentials (access/secret key) -// config <- AWS config (region, endpoint_url) func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) error { - if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 -- 0755 permissions required for AWS config directory access + if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 -- config directory needs to be readable by mount-s3 process klog.Errorf("MountpointS3Mounter: Cannot create config dir %s: %v", configPathWithVolID, err) return err } @@ -505,7 +403,7 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er // writeConfigFile writes lines to a file with 0600 permissions. func writeConfigFile(filePath string, lines []string) error { - f, err := CreateFile(filePath) // #nosec G304 -- filePath is constructed from validated volume ID and config directory + f, err := CreateFile(filePath) // #nosec G304 -- filePath is constructed from validated volume ID and predefined config file names if err != nil { return fmt.Errorf("cannot create file %s: %w", filePath, err) } @@ -515,7 +413,7 @@ func writeConfigFile(filePath string, lines []string) error { } }() - if err := Chmod(filePath, 0600); err != nil { // #nosec G302 -- 0600 permissions required to protect AWS credentials file + if err := Chmod(filePath, 0600); err != nil { // #nosec G302 -- credentials file must be readable only by owner for security return fmt.Errorf("cannot chmod file %s: %w", filePath, err) } @@ -529,24 +427,6 @@ func writeConfigFile(filePath string, lines []string) error { } // formulateMountOptions builds the CLI args, env vars, and worker payload for mount-s3. -// -// Only structured fields are emitted here. All other user-supplied flags arrive -// pre-formatted in s3.MountOptions and are appended at the end. -// -// By the time this is called, Mount() has already: -// - Cleared AllowOverwrite / IncrementalUpload if ReadOnly is set -// - Validated LogDirectory and CacheDir existence -// -// Flag ordering: -// 1. Positional args : bucket, mountpoint -// 2. Always-on : --allow-other -// 3. SecretMap-sourced : --endpoint-url, --region -// 4. Identity : --uid, --gid -// 5. Logging : --debug/--debug-crt/--no-log, --log-directory -// 6. Cache : --cache -// 7. Write flags : --allow-overwrite, --incremental-upload -// 8. Read-only : --read-only -// 9. User passthrough : s3.MountOptions (appended last β€” highest precedence) func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathWithVolID string) (nodeServerOp []string, envVars []string, workerNodeOp s3MounterArgs) { nodeServerOp = []string{bucket, target, "--allow-other"} @@ -584,7 +464,6 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW } // --- Log level (name translation) --- - // mount-s3 has no --log-level flag; each level maps to its own flag. switch s3.LogLevel { case "debug": nodeServerOp = append(nodeServerOp, "--debug") @@ -602,21 +481,19 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW } } - // --- Log directory (existence validated in Mount()) --- + // --- Log directory --- if s3.LogDirectory != "" { nodeServerOp = append(nodeServerOp, "--log-directory="+s3.LogDirectory) workerNodeOp.LogDirectory = s3.LogDirectory } - // --- Cache directory (existence validated in Mount()) --- + // --- Cache directory --- if s3.CacheDir != "" { nodeServerOp = append(nodeServerOp, "--cache="+s3.CacheDir) workerNodeOp.CacheDir = s3.CacheDir } // --- Write flags --- - // These are only emitted if ReadOnly is false. - // Mount() clears these when ReadOnly is set, so no conflict is possible here. if s3.AllowOverwrite { nodeServerOp = append(nodeServerOp, "--allow-overwrite") workerNodeOp.AllowOverwrite = "true" @@ -627,16 +504,15 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW } // --- Read-only --- - // Safe to emit here β€” Mount() already cleared AllowOverwrite and - // IncrementalUpload above, so no conflicting flags will be present. if s3.ReadOnly { nodeServerOp = append(nodeServerOp, "--read-only") workerNodeOp.ReadOnly = "true" } // --- User passthrough (appended last β€” highest precedence) --- - // All entries already have "--" prefix (enforced by parseMountpointS3Options). + // Set on BOTH paths so the worker daemon receives passthrough flags too. nodeServerOp = append(nodeServerOp, s3.MountOptions...) + workerNodeOp.Args = s3.MountOptions klog.Infof("MountpointS3 nodeServerOp: %v", nodeServerOp) klog.Infof("MountpointS3 workerNodeOp: %+v", workerNodeOp) From 472b02a232c465fdadbff498c9541c0387a085c4 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sat, 11 Apr 2026 15:07:38 +0530 Subject: [PATCH 047/128] publish 0.12.22 version --- .github/workflows/release.yml | 6 +++--- cos-csi-mounter/Makefile | 2 +- cos-csi-mounter/server/s3mounter.go | 10 ++++++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c8c7c9e9..bb450ea4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.21 + APP_VERSION: 0.12.22 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.21 - name: v0.12.21 + tag_name: v0.12.22 + name: v0.12.22 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index 0f85b45a..a893f059 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.21 +APP_VERSION := 0.12.22 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go index 77bf07ec..191bfe56 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/s3mounter.go @@ -33,6 +33,11 @@ type s3MounterArgs struct { ReadOnly string `json:"read-only,omitempty"` AllowOverwrite string `json:"allow-overwrite,omitempty"` IncrementalUpload string `json:"incremental-upload,omitempty"` + + // Passthrough flags β€” user-supplied flags not handled by structured fields + // (e.g. --allow-delete, --hello, --never=true). + // PopulateArgsSlice appends these last so they have highest precedence. + Args []string `json:"args,omitempty"` } // PopulateArgsSlice builds the CLI args slice for mount-s3. @@ -127,6 +132,11 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string result = append(result, "--cache="+args.CacheDir) } + // --- User passthrough flags (appended last β€” highest precedence) --- + // These are flags like --allow-delete, --hello, --never=true that aren't + // handled by the structured fields above. + result = append(result, args.Args...) + return result, nil } From 9e2a22896735b8450b6e2e5a80421c2f5b5299c5 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Mon, 13 Apr 2026 00:29:52 +0530 Subject: [PATCH 048/128] publish 0.12.23 version --- .github/workflows/release.yml | 6 +- cos-csi-mounter/Makefile | 2 +- cos-csi-mounter/server/s3mounter.go | 66 +++--------- pkg/mounter/mounter_s3mounter.go | 159 ++++++++++++++-------------- 4 files changed, 102 insertions(+), 131 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bb450ea4..88dd624a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.22 + APP_VERSION: 0.12.23 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.22 - name: v0.12.22 + tag_name: v0.12.23 + name: v0.12.23 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index a893f059..2525755a 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.22 +APP_VERSION := 0.12.23 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go index 191bfe56..613e3a66 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/s3mounter.go @@ -34,25 +34,25 @@ type s3MounterArgs struct { AllowOverwrite string `json:"allow-overwrite,omitempty"` IncrementalUpload string `json:"incremental-upload,omitempty"` - // Passthrough flags β€” user-supplied flags not handled by structured fields - // (e.g. --allow-delete, --hello, --never=true). - // PopulateArgsSlice appends these last so they have highest precedence. + // Passthrough flags β€” user-supplied flags not handled by structured fields. + // Secret flags override SC flags when the same key appears in both. + // These are already deduplicated by the node server before being sent here. Args []string `json:"args,omitempty"` } // PopulateArgsSlice builds the CLI args slice for mount-s3. -// -// Key rules: -// - AwsCredentialsFile and AwsConfigFile are NOT CLI flags β€” use EnvVars() for those -// - Boolean flags (--allow-other, --read-only) are emitted without a value -// because mount-s3 does not accept --allow-other=true syntax -// - LogLevel maps to --debug / --debug-crt / --no-log (mount-s3 has no --log-level) func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string, error) { result := []string{bucket, targetPath} - // --- Read-only priority resolution --- - // If read-only is set, clear all write-related flags to ensure clean read-only mount. - // This prevents conflicts where both read-only and write flags are set. + // Block incremental-upload flag (causes data loss with IBM COS). + // This flag uses multiple PutObject calls that overwrite each other, + // resulting in ~75% data loss. + if args.IncrementalUpload == "true" { + logger.Warn("incremental-upload is not supported with IBM Cloud Object Storage, disabling") + args.IncrementalUpload = "" + } + + // Read-only priority resolution. if args.ReadOnly == "true" { if args.AllowOverwrite == "true" { logger.Warn("read-only is set, clearing allow-overwrite") @@ -64,48 +64,31 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string } } - // --allow-other: boolean flag, no value if args.AllowOther == "true" { result = append(result, "--allow-other") } - - // --read-only: boolean flag, no value if args.ReadOnly == "true" { result = append(result, "--read-only") } - - // --allow-overwrite: boolean flag, no value (cleared if read-only is set) if args.AllowOverwrite == "true" { result = append(result, "--allow-overwrite") } - - // --incremental-upload: boolean flag, no value (cleared if read-only is set) if args.IncrementalUpload == "true" { result = append(result, "--incremental-upload") } - - // --uid if args.UID != "" { result = append(result, "--uid="+args.UID) } - - // --gid if args.GID != "" { result = append(result, "--gid="+args.GID) } - - // --endpoint-url if args.EndpointURL != "" { result = append(result, "--endpoint-url="+args.EndpointURL) } - - // --region if args.Region != "" { result = append(result, "--region="+args.Region) } - // Log level: mount-s3 has no --log-level flag. - // Map to the supported flags only. switch args.LogLevel { case "debug": result = append(result, "--debug") @@ -122,27 +105,20 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string } } - // --log-directory if args.LogDirectory != "" { result = append(result, "--log-directory="+args.LogDirectory) } - - // --cache if args.CacheDir != "" { result = append(result, "--cache="+args.CacheDir) } - // --- User passthrough flags (appended last β€” highest precedence) --- - // These are flags like --allow-delete, --hello, --never=true that aren't - // handled by the structured fields above. + // Passthrough flags appended last β€” already deduplicated by node server. result = append(result, args.Args...) return result, nil } -// EnvVars returns the environment variables that must be set on the mount-s3 -// subprocess so it can locate the AWS credentials and config files. -// The caller must add these to cmd.Env before cmd.Start(). +// EnvVars returns the environment variables for the mount-s3 subprocess. func (args s3MounterArgs) EnvVars() []string { var envVars []string if args.AwsCredentialsFile != "" { @@ -160,7 +136,6 @@ func (args s3MounterArgs) Validate(targetPath string) error { return err } - // allow-other must be a boolean string if set if args.AllowOther != "" { if isBool := isBoolString(args.AllowOther); !isBool { logger.Error("cannot convert value of allow-other into boolean", zap.Any("allow-other", args.AllowOther)) @@ -168,34 +143,27 @@ func (args s3MounterArgs) Validate(targetPath string) error { } } - // AwsCredentialsFile and AwsConfigFile replace the old AwsConfigDir check if args.AwsCredentialsFile == "" { logger.Error("aws-credentials-file is required for mount-s3") return fmt.Errorf("aws-credentials-file is required for mount-s3") } - if args.AwsConfigFile == "" { logger.Error("aws-config-file is required for mount-s3") return fmt.Errorf("aws-config-file is required for mount-s3") } - // read-only must be a boolean string if set if args.ReadOnly != "" { if isBool := isBoolString(args.ReadOnly); !isBool { logger.Error("cannot convert value of read-only into boolean", zap.Any("read-only", args.ReadOnly)) return fmt.Errorf("cannot convert value of read-only into boolean: %v", args.ReadOnly) } } - - // allow-overwrite must be a boolean string if set if args.AllowOverwrite != "" { if isBool := isBoolString(args.AllowOverwrite); !isBool { logger.Error("cannot convert value of allow-overwrite into boolean", zap.Any("allow-overwrite", args.AllowOverwrite)) return fmt.Errorf("cannot convert value of allow-overwrite into boolean: %v", args.AllowOverwrite) } } - - // incremental-upload must be a boolean string if set if args.IncrementalUpload != "" { if isBool := isBoolString(args.IncrementalUpload); !isBool { logger.Error("cannot convert value of incremental-upload into boolean", zap.Any("incremental-upload", args.IncrementalUpload)) @@ -203,15 +171,13 @@ func (args s3MounterArgs) Validate(targetPath string) error { } } - // Ensure cache directory exists if specified + // ensureDir creates the directory if it does not exist. if args.CacheDir != "" { if err := ensureDir(args.CacheDir); err != nil { logger.Error("failed to create cache directory", zap.String("cache-dir", args.CacheDir), zap.Error(err)) return fmt.Errorf("failed to create cache directory '%s': %w", args.CacheDir, err) } } - - // Ensure log directory exists if specified if args.LogDirectory != "" { if err := ensureDir(args.LogDirectory); err != nil { logger.Error("failed to create log directory", zap.String("log-dir", args.LogDirectory), zap.Error(err)) @@ -220,4 +186,4 @@ func (args s3MounterArgs) Validate(targetPath string) error { } return nil -} +} \ No newline at end of file diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 07ee66fe..fd2cc4e8 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -17,21 +17,7 @@ import ( ) // MountpointS3Mounter implements Mounter for AWS Mountpoint S3. -// -// Only flags that require code-level handling are modelled as struct fields: -// - Conflict resolution : ReadOnly takes priority β€” clears AllowOverwrite / IncrementalUpload -// - Directory creation : LogDirectory / CacheDir are created by the worker's Validate() -// - Name translation : LogLevel ("debug" β†’ --debug, not --log-level=debug) -// - Special precedence : UID / GID (secretMap < mountOptions) -// - Always-emitted : allow-other (deduplicated here) -// - Invalid flag : umask (warn + drop) -// - SecretMap-sourced : EndPoint / LocConstraint (injected from secret fields) -// -// All other flags are the user's responsibility and pass through unchanged to -// mount-s3 via MountOptions. The parser ensures every passthrough entry has a -// "--" prefix so mount-s3 always receives syntactically valid flags. type MountpointS3Mounter struct { - // Identity β€” sourced from secretMap, not mountOptions. BucketName string ObjectPath string EndPoint string @@ -40,71 +26,47 @@ type MountpointS3Mounter struct { AccessKey string SecretKey string - // UID / GID β€” secretMap sets initial value; mountOptions can override. UID string GID string - // LogLevel requires name translation: - // "debug" β†’ --debug - // "debug-crt" β†’ --debug-crt - // "no-log" β†’ --no-log - // mount-s3 has no --log-level flag. - LogLevel string - - // LogDirectory is forwarded to the worker; worker's Validate() creates it via ensureDir. + LogLevel string LogDirectory string + CacheDir string - // CacheDir is forwarded to the worker; worker's Validate() creates it via ensureDir. - CacheDir string - - // Conflict resolution β€” ReadOnly takes priority over write flags. - // If ReadOnly is set, AllowOverwrite and IncrementalUpload are cleared - // in Mount() before formulateMountOptions is called. This allows SC to - // set allow-overwrite and incremental-upload as safe defaults while still - // letting users request read-only via their secret without errors. - ReadOnly bool // --read-only - AllowOverwrite bool // --allow-overwrite - IncrementalUpload bool // --incremental-upload + ReadOnly bool + AllowOverwrite bool + IncrementalUpload bool - // MountOptions holds all remaining flags that pass through unchanged. - // The parser ensures every entry has a "--" prefix so mount-s3 sees valid flags. + // MountOptions holds deduplicated passthrough flags. + // SC flags are overridden by secret flags when the same key appears in both. MountOptions []string MounterUtils utils.MounterUtils } // s3MounterArgs holds the args sent to the worker node daemon for mount-s3. -// Structured fields cover all named flags; Args carries any remaining -// passthrough flags (e.g. --allow-delete, --hello, --never=true). // This struct must stay in sync with s3MounterArgs in the worker (cos-csi-mounter). type s3MounterArgs struct { - // Always set. AllowOther string `json:"allow-other,omitempty"` AwsCredentialsFile string `json:"aws-credentials-file,omitempty"` AwsConfigFile string `json:"aws-config-file,omitempty"` - // SecretMap-sourced. EndpointURL string `json:"endpoint-url,omitempty"` Region string `json:"region,omitempty"` - // Identity. UID string `json:"uid,omitempty"` GID string `json:"gid,omitempty"` - // Logging (translated from LogLevel). LogLevel string `json:"log-level,omitempty"` LogDirectory string `json:"log-directory,omitempty"` - // Cache. CacheDir string `json:"cache,omitempty"` - // Conflict trio. ReadOnly string `json:"read-only,omitempty"` AllowOverwrite string `json:"allow-overwrite,omitempty"` IncrementalUpload string `json:"incremental-upload,omitempty"` - // Passthrough flags β€” user-supplied flags not handled by structured fields - // (e.g. --allow-delete, --hello, --never=true). - // Worker's PopulateArgsSlice appends these last so they have highest precedence. + // Passthrough flags β€” user-supplied flags not handled by structured fields. + // Secret flags override SC flags when the same key appears in both. Args []string `json:"args,omitempty"` } @@ -118,29 +80,31 @@ var ( removeS3ConfigFile = removeS3MountConfigFile ) -// envMounter is a local interface for type assertion β€” allows calling -// FuseMountWithEnv on the concrete MounterOptsUtils without changing the -// MounterUtils interface, so other mounters (rclone etc.) are unaffected. type envMounter interface { FuseMountWithEnv(path string, comm string, args []string, envVars []string) error } // NewMountpointS3Mounter creates a new MountpointS3Mounter from the unified // secret format. +// +// Precedence (lowest β†’ highest): +// 1. secretMap identity keys +// 2. StorageClass mountOptions β€” parsed first +// 3. Secret mountOptions β€” parsed second, wins over SC on any overlapping flag +// +// For passthrough flags, deduplication is applied: if the same flag key appears +// in both SC and secret, the SC entry is dropped and the secret entry is kept. func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounterUtils utils.MounterUtils) Mounter { klog.Info("-newMountpointS3Mounter-") mounter := &MountpointS3Mounter{} - // --- Credentials --- if val, ok := secretMap["accessKey"]; ok { mounter.AccessKey = val } if val, ok := secretMap["secretKey"]; ok { mounter.SecretKey = val } - - // --- Bucket / endpoint identity --- if val, ok := secretMap["bucketName"]; ok { mounter.BucketName = val } @@ -154,7 +118,6 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounter.LocConstraint = val } - // --- UID / GID (lowest precedence β€” mountOptions overrides below) --- if secretMap["gid"] != "" && secretMap["uid"] == "" { mounter.UID = secretMap["gid"] } else if val := secretMap["uid"]; val != "" { @@ -166,21 +129,76 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounter.AuthType = "hmac" - // --- Merge and parse mountOptions --- + // Step 1: parse SC options. scRemainder := parseMountpointS3Options(mounter, mountOptions) + + // Step 2: parse secret options (structured fields set by secret override SC). secretOpts := splitSecretMountOptions(secretMap["mountOptions"]) secretRemainder := parseMountpointS3Options(mounter, secretOpts) - mounter.MountOptions = append(scRemainder, secretRemainder...) + + // Step 3: deduplicate passthroughs β€” secret wins over SC on same key. + mounter.MountOptions = deduplicatePassthrough(scRemainder, secretRemainder) + + // Step 4: Block incremental-upload flag (causes data loss with IBM COS). + // This flag uses multiple PutObject calls that overwrite each other instead of + // using proper multipart upload, resulting in ~75% data loss. + if mounter.IncrementalUpload { + klog.Info("mount-s3: --incremental-upload is not supported ") + mounter.IncrementalUpload = false + } + mounter.MounterUtils = mounterUtils klog.Infof("newMountpointS3Mounter args:\n\tbucketName: [%s]\n\tobjectPath: [%s]\n\tendPoint: [%s]\n\tlocationConstraint: [%s]\n\tauthType: [%s]", mounter.BucketName, mounter.ObjectPath, mounter.EndPoint, mounter.LocConstraint, mounter.AuthType) - klog.Infof("newMountpointS3Mounter: SC mountOptions=%v secretMountOptions=%v passthrough=%v", + klog.Infof("newMountpointS3Mounter: SC mountOptions=%v secretMountOptions=%v deduplicated passthrough=%v", mountOptions, secretOpts, mounter.MountOptions) return mounter } +// deduplicatePassthrough merges SC and secret passthrough flags. +// If the same flag key appears in both, the SC entry is dropped and the +// secret entry is kept (secret wins). +// +// Examples: +// +// SC: [--max-threads=32, --maximum-throughput-gbps=10, --allow-delete] +// Secret: [--max-threads=128, --maximum-throughput-gbps=25] +// Result: [--allow-delete, --max-threads=128, --maximum-throughput-gbps=25] +func deduplicatePassthrough(scFlags, secretFlags []string) []string { + // Build a set of keys present in secret flags. + secretKeys := make(map[string]struct{}, len(secretFlags)) + for _, flag := range secretFlags { + key := flagKey(flag) + secretKeys[key] = struct{}{} + } + + // Keep SC flags whose key does NOT appear in secret flags. + var result []string + for _, flag := range scFlags { + key := flagKey(flag) + if _, overridden := secretKeys[key]; overridden { + klog.Infof("deduplicatePassthrough: SC flag %q overridden by secret", flag) + } else { + result = append(result, flag) + } + } + + // Append all secret flags last. + result = append(result, secretFlags...) + return result +} + +// flagKey extracts the key portion from a passthrough flag. +// "--max-threads=128" β†’ "max-threads" +// "--allow-delete" β†’ "allow-delete" +func flagKey(flag string) string { + trimmed := strings.TrimPrefix(flag, "--") + key, _, _ := strings.Cut(trimmed, "=") + return key +} + // splitSecretMountOptions splits the multiline mountOptions string from the secret. func splitSecretMountOptions(raw string) []string { if raw == "" { @@ -189,6 +207,7 @@ func splitSecretMountOptions(raw string) []string { var opts []string for _, line := range strings.Split(raw, "\n") { line = strings.TrimSpace(line) + line = strings.TrimRight(line, ",") // ← ADD THIS if line == "" || strings.HasPrefix(line, "#") { continue } @@ -207,7 +226,6 @@ func parseMountpointS3Options(mounter *MountpointS3Mounter, opts []string) []str switch key { case "allow-other": - // formulateMountOptions unconditionally adds --allow-other. case "umask": klog.Warningf("parseMountpointS3Options: 'umask' is not supported by mount-s3. " + "Use --dir-mode / --file-mode instead. Ignoring.") @@ -254,7 +272,7 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { klog.Info("-MountpointS3Mounter Mount-") klog.Infof("Mount args:\n\tsource: <%s>\n\ttarget: <%s>", source, target) - // --- Read-only priority resolution --- + // Read-only priority resolution. if s3.ReadOnly { if s3.AllowOverwrite { klog.Infof("MountpointS3Mounter: read-only set β€” clearing allow-overwrite") @@ -264,7 +282,6 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { klog.Infof("MountpointS3Mounter: read-only set β€” clearing incremental-upload") s3.IncrementalUpload = false } - var filtered []string for _, opt := range s3.MountOptions { trimmed := strings.TrimPrefix(opt, "--") @@ -281,7 +298,6 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { // LogDirectory and CacheDir are forwarded to the worker via workerNodeOp. // The worker's Validate() calls ensureDir() which creates them if missing. - // --- Config path --- var configPath string if mountWorker { configPath = constants.MounterConfigPathOnHost @@ -367,7 +383,7 @@ func (s3 *MountpointS3Mounter) Unmount(target string) error { // createS3MountConfig creates the AWS credentials and config files. func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) error { - if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 -- config directory needs to be readable by mount-s3 process + if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 klog.Errorf("MountpointS3Mounter: Cannot create config dir %s: %v", configPathWithVolID, err) return err } @@ -403,7 +419,7 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er // writeConfigFile writes lines to a file with 0600 permissions. func writeConfigFile(filePath string, lines []string) error { - f, err := CreateFile(filePath) // #nosec G304 -- filePath is constructed from validated volume ID and predefined config file names + f, err := CreateFile(filePath) // #nosec G304 if err != nil { return fmt.Errorf("cannot create file %s: %w", filePath, err) } @@ -413,7 +429,7 @@ func writeConfigFile(filePath string, lines []string) error { } }() - if err := Chmod(filePath, 0600); err != nil { // #nosec G302 -- credentials file must be readable only by owner for security + if err := Chmod(filePath, 0600); err != nil { // #nosec G302 return fmt.Errorf("cannot chmod file %s: %w", filePath, err) } @@ -443,7 +459,6 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW AwsConfigFile: cfgFilePath, } - // --- SecretMap-sourced --- if s3.EndPoint != "" { nodeServerOp = append(nodeServerOp, "--endpoint-url="+s3.EndPoint) workerNodeOp.EndpointURL = s3.EndPoint @@ -452,8 +467,6 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW nodeServerOp = append(nodeServerOp, "--region="+s3.LocConstraint) workerNodeOp.Region = s3.LocConstraint } - - // --- Identity --- if s3.UID != "" { nodeServerOp = append(nodeServerOp, "--uid="+s3.UID) workerNodeOp.UID = s3.UID @@ -463,7 +476,6 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW workerNodeOp.GID = s3.GID } - // --- Log level (name translation) --- switch s3.LogLevel { case "debug": nodeServerOp = append(nodeServerOp, "--debug") @@ -481,19 +493,14 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW } } - // --- Log directory --- if s3.LogDirectory != "" { nodeServerOp = append(nodeServerOp, "--log-directory="+s3.LogDirectory) workerNodeOp.LogDirectory = s3.LogDirectory } - - // --- Cache directory --- if s3.CacheDir != "" { nodeServerOp = append(nodeServerOp, "--cache="+s3.CacheDir) workerNodeOp.CacheDir = s3.CacheDir } - - // --- Write flags --- if s3.AllowOverwrite { nodeServerOp = append(nodeServerOp, "--allow-overwrite") workerNodeOp.AllowOverwrite = "true" @@ -502,15 +509,13 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW nodeServerOp = append(nodeServerOp, "--incremental-upload") workerNodeOp.IncrementalUpload = "true" } - - // --- Read-only --- if s3.ReadOnly { nodeServerOp = append(nodeServerOp, "--read-only") workerNodeOp.ReadOnly = "true" } - // --- User passthrough (appended last β€” highest precedence) --- - // Set on BOTH paths so the worker daemon receives passthrough flags too. + // Passthrough β€” deduplicated so secret wins over SC. + // Set on BOTH paths so the worker daemon receives them too. nodeServerOp = append(nodeServerOp, s3.MountOptions...) workerNodeOp.Args = s3.MountOptions From df640fab7715af8a5af888f7b570204d7a1a3446 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Mon, 13 Apr 2026 10:20:28 +0530 Subject: [PATCH 049/128] publish 0.12.24 version --- .github/workflows/release.yml | 6 +++--- cos-csi-mounter/Makefile | 2 +- pkg/mounter/mounter_s3mounter.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 88dd624a..9ccb99fd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.23 + APP_VERSION: 0.12.24 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.23 - name: v0.12.23 + tag_name: v0.12.24 + name: v0.12.24 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index 2525755a..162a8498 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.23 +APP_VERSION := 0.12.24 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index fd2cc4e8..dfa49c0c 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -383,7 +383,7 @@ func (s3 *MountpointS3Mounter) Unmount(target string) error { // createS3MountConfig creates the AWS credentials and config files. func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) error { - if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 + if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 -- config directory needs to be readable by mount-s3 process klog.Errorf("MountpointS3Mounter: Cannot create config dir %s: %v", configPathWithVolID, err) return err } From 479fa1c74c1e69cbf767d504e24ef7c5eaa0cf64 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Mon, 13 Apr 2026 11:39:33 +0530 Subject: [PATCH 050/128] publish 0.12.25 version --- .github/workflows/release.yml | 6 +++--- cos-csi-mounter/Makefile | 2 +- pkg/mounter/mounter_s3mounter.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9ccb99fd..d87a94df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.24 + APP_VERSION: 0.12.25 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.24 - name: v0.12.24 + tag_name: v0.12.25 + name: v0.12.25 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index 162a8498..e266797c 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.24 +APP_VERSION := 0.12.25 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index dfa49c0c..903fcd7a 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -419,7 +419,7 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er // writeConfigFile writes lines to a file with 0600 permissions. func writeConfigFile(filePath string, lines []string) error { - f, err := CreateFile(filePath) // #nosec G304 + f, err := CreateFile(filePath) // #nosec G304 -- file path is constructed from controlled configPathWithVolID and constant filename if err != nil { return fmt.Errorf("cannot create file %s: %w", filePath, err) } @@ -429,7 +429,7 @@ func writeConfigFile(filePath string, lines []string) error { } }() - if err := Chmod(filePath, 0600); err != nil { // #nosec G302 + if err := Chmod(filePath, 0600); err != nil { // #nosec G302 -- credentials file requires restrictive permissions (owner read/write only) return fmt.Errorf("cannot chmod file %s: %w", filePath, err) } From eb74885350d7a403796213d4487b9d0c7f1a1fed Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sat, 2 May 2026 20:19:29 +0530 Subject: [PATCH 051/128] publish 0.12.26 version --- .github/workflows/release.yml | 6 +++--- cos-csi-mounter/Makefile | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d87a94df..4c2ffcdd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.25 + APP_VERSION: 0.12.26 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.25 - name: v0.12.25 + tag_name: v0.12.26 + name: v0.12.26 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index e266797c..57dc8fde 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.25 +APP_VERSION := 0.12.26 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin From 08e19f1ce34576bcffcbb40ee443e96b89a5ed4d Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Tue, 12 May 2026 16:44:16 +0530 Subject: [PATCH 052/128] Updated testcases --- .github/workflows/release.yml | 6 +- pkg/mounter/mounter-s3mounter_test.go | 890 ++++++++++++++++++++++++++ 2 files changed, 893 insertions(+), 3 deletions(-) create mode 100644 pkg/mounter/mounter-s3mounter_test.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4c2ffcdd..a858c440 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.26 + APP_VERSION: 0.12.27 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.26 - name: v0.12.26 + tag_name: v0.12.27 + name: v0.12.27 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go new file mode 100644 index 00000000..7e7efb5f --- /dev/null +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -0,0 +1,890 @@ +package mounter + +import ( + "errors" + "os" + "testing" + mounterUtils "github.com/IBM/ibm-object-csi-driver/pkg/mounter/utils" + "github.com/stretchr/testify/assert" +) + +var ( + s3MounterSecretMap = map[string]string{ + "cosEndpoint": "https://s3.us-east.cloud-object-storage.appdomain.cloud", + "locationConstraint": "us-east", + "bucketName": "test-bucket", + "objectPath": "test-path", + "accessKey": "test-access-key", + "secretKey": "test-secret-key", + "uid": "1000", + "gid": "1000", + "mountOptions": `log-level=debug +read-only +cache=/tmp/cache`, + } + + s3MounterMountOptions = []string{"--dir-mode=0755", "--file-mode=0644"} +) + +func TestNewMountpointS3Mounter_Success(t *testing.T) { + mounter := NewMountpointS3Mounter(s3MounterSecretMap, s3MounterMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + + s3Mounter, ok := mounter.(*MountpointS3Mounter) + assert.True(t, ok) + + assert.Equal(t, s3MounterSecretMap["bucketName"], s3Mounter.BucketName) + assert.Equal(t, s3MounterSecretMap["objectPath"], s3Mounter.ObjectPath) + assert.Equal(t, s3MounterSecretMap["cosEndpoint"], s3Mounter.EndPoint) + assert.Equal(t, s3MounterSecretMap["locationConstraint"], s3Mounter.LocConstraint) + assert.Equal(t, s3MounterSecretMap["accessKey"], s3Mounter.AccessKey) + assert.Equal(t, s3MounterSecretMap["secretKey"], s3Mounter.SecretKey) + assert.Equal(t, s3MounterSecretMap["uid"], s3Mounter.UID) + assert.Equal(t, s3MounterSecretMap["gid"], s3Mounter.GID) + assert.Equal(t, "debug", s3Mounter.LogLevel) + assert.True(t, s3Mounter.ReadOnly) + assert.Equal(t, "/tmp/cache", s3Mounter.CacheDir) + assert.Equal(t, "hmac", s3Mounter.AuthType) + // MountOptions should contain SC options (dir-mode, file-mode) + assert.Contains(t, s3Mounter.MountOptions, "--dir-mode=0755") + assert.Contains(t, s3Mounter.MountOptions, "--file-mode=0644") +} + +func TestNewMountpointS3Mounter_GidWithoutUid(t *testing.T) { + secretMap := map[string]string{ + "gid": "2000", + } + + mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + + s3Mounter, ok := mounter.(*MountpointS3Mounter) + assert.True(t, ok) + assert.Equal(t, "2000", s3Mounter.UID) + assert.Equal(t, "2000", s3Mounter.GID) +} + +func TestNewMountpointS3Mounter_UidOverridesGid(t *testing.T) { + secretMap := map[string]string{ + "uid": "1000", + "gid": "2000", + } + + mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + + s3Mounter, ok := mounter.(*MountpointS3Mounter) + assert.True(t, ok) + assert.Equal(t, "1000", s3Mounter.UID) + assert.Equal(t, "2000", s3Mounter.GID) +} + +func TestMountpointS3Mount_NodeServer_Positive(t *testing.T) { + mountWorker = false + + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return os.CreateTemp("", "test") + } + Chmod = func(path string, perm os.FileMode) error { + return nil + } + + // Create a mock that implements envMounter interface + mockUtils := &mockEnvMounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseMountFn: func(path, comm string, args []string) error { + return nil + }, + }), + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mockUtils, + BucketName: "test-bucket", + ObjectPath: "test-path", + EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", + LocConstraint: "us-east", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + UID: "1000", + GID: "1000", + LogLevel: "debug", + ReadOnly: true, + MountOptions: s3MounterMountOptions, + } + + err := s3Mounter.Mount(source, target) + assert.NoError(t, err) +} + +func TestMountpointS3Mount_NodeServer_WithEnvMounter(t *testing.T) { + mountWorker = false + + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return os.CreateTemp("", "test") + } + Chmod = func(path string, perm os.FileMode) error { + return nil + } + + // Create a mock that implements envMounter interface + mockUtils := &mockEnvMounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mockUtils, + BucketName: "test-bucket", + EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", + LocConstraint: "us-east", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + MountOptions: s3MounterMountOptions, + } + + err := s3Mounter.Mount(source, target) + assert.NoError(t, err) +} + +func TestMountpointS3Mount_WorkerNode_Positive(t *testing.T) { + mountWorker = true + + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return os.CreateTemp("", "test") + } + Chmod = func(path string, perm os.FileMode) error { + return nil + } + mounterRequest = func(payload, url string) error { + return nil + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), + BucketName: "test-bucket", + EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", + LocConstraint: "us-east", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := s3Mounter.Mount(source, target) + assert.NoError(t, err) +} + +func TestMountpointS3Mount_CreateDirFails_Negative(t *testing.T) { + mountWorker = false + + MakeDir = func(path string, perm os.FileMode) error { + return errors.New("failed to create directory") + } + + s3Mounter := &MountpointS3Mounter{ + BucketName: "test-bucket", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := s3Mounter.Mount(source, target) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to create directory") +} + +func TestMountpointS3Mount_CreateCredFileFails_Negative(t *testing.T) { + mountWorker = false + + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return nil, errors.New("failed to create file") + } + + s3Mounter := &MountpointS3Mounter{ + BucketName: "test-bucket", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := s3Mounter.Mount(source, target) + assert.Error(t, err) + assert.Contains(t, err.Error(), "cannot create file") +} + +func TestMountpointS3Mount_WorkerNode_Negative(t *testing.T) { + mountWorker = true + + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return os.CreateTemp("", "test") + } + Chmod = func(path string, perm os.FileMode) error { + return nil + } + mounterRequest = func(payload, url string) error { + return errors.New("failed to perform http request") + } + + s3Mounter := &MountpointS3Mounter{ + BucketName: "test-bucket", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := s3Mounter.Mount(source, target) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to perform http request") +} + +func TestMountpointS3Unmount_NodeServer(t *testing.T) { + mountWorker = false + + removeS3ConfigFile = func(configPath, target string) {} + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseUnmountFn: func(path string) error { + return nil + }, + }), + } + + err := s3Mounter.Unmount(target) + assert.NoError(t, err) +} + +func TestMountpointS3Unmount_WorkerNode(t *testing.T) { + mountWorker = true + + removeS3ConfigFile = func(configPath, target string) {} + + mounterRequest = func(payload, url string) error { + return nil + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseUnmountFn: func(path string) error { + return nil + }, + }), + } + + err := s3Mounter.Unmount(target) + assert.NoError(t, err) +} + +func TestMountpointS3Unmount_WorkerNode_Negative(t *testing.T) { + mountWorker = true + + mounterRequest = func(payload, url string) error { + return errors.New("failed to create http request") + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseUnmountFn: func(path string) error { + return nil + }, + }), + } + + err := s3Mounter.Unmount(target) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to create http request") +} + +func TestMountpointS3Unmount_NodeServer_Negative(t *testing.T) { + mountWorker = false + + removeS3ConfigFile = func(configPath, target string) {} + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseUnmountFn: func(path string) error { + return errors.New("failed to unmount") + }, + }), + } + + err := s3Mounter.Unmount(target) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to unmount") +} + +func TestRemoveS3MountConfigFile_PathNotExists(t *testing.T) { + Stat = func(path string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } + removeS3MountConfigFile("/test", target) +} + +func TestRemoveS3MountConfigFile_StatRetryThenSuccess(t *testing.T) { + attempt := 0 + Stat = func(_ string) (os.FileInfo, error) { + if attempt == 0 { + attempt++ + return nil, errors.New("stat error") + } + return nil, nil + } + RemoveAll = func(_ string) error { + return nil + } + + removeS3MountConfigFile("/test", target) +} + +func TestRemoveS3MountConfigFile_RemoveRetryThenSuccess(t *testing.T) { + Stat = func(_ string) (os.FileInfo, error) { + return nil, nil + } + attempt := 0 + RemoveAll = func(_ string) error { + if attempt == 0 { + attempt++ + return errors.New("remove error") + } + return nil + } + + removeS3MountConfigFile("/test", target) +} + +func TestRemoveS3MountConfigFile_Negative(t *testing.T) { + called := 0 + Stat = func(_ string) (os.FileInfo, error) { + return nil, nil + } + RemoveAll = func(_ string) error { + called++ + return errors.New("remove failed") + } + + removeS3MountConfigFile("/test", target) + assert.Equal(t, maxRetries, called) +} + +func TestFormulateMountOptions_AllOptions(t *testing.T) { + s3Mounter := &MountpointS3Mounter{ + BucketName: "test-bucket", + ObjectPath: "test-path", + EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", + LocConstraint: "us-east", + UID: "1000", + GID: "2000", + LogLevel: "debug", + LogDirectory: "/var/log/mount-s3", + CacheDir: "/tmp/cache", + ReadOnly: true, + MountOptions: []string{"--dir-mode=0755", "--max-threads=32"}, + } + + nodeServerOp, envVars, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket/test-path", target, "/config/path") + + // Check node server options + assert.Contains(t, nodeServerOp, "test-bucket/test-path") + assert.Contains(t, nodeServerOp, target) + assert.Contains(t, nodeServerOp, "--allow-other") + assert.Contains(t, nodeServerOp, "--endpoint-url=https://s3.us-east.cloud-object-storage.appdomain.cloud") + assert.Contains(t, nodeServerOp, "--region=us-east") + assert.Contains(t, nodeServerOp, "--uid=1000") + assert.Contains(t, nodeServerOp, "--gid=2000") + assert.Contains(t, nodeServerOp, "--debug") + assert.Contains(t, nodeServerOp, "--log-directory=/var/log/mount-s3") + assert.Contains(t, nodeServerOp, "--cache=/tmp/cache") + assert.Contains(t, nodeServerOp, "--read-only") + assert.Contains(t, nodeServerOp, "--dir-mode=0755") + assert.Contains(t, nodeServerOp, "--max-threads=32") + + // Check env vars + assert.Len(t, envVars, 2) + assert.Contains(t, envVars[0], "AWS_SHARED_CREDENTIALS_FILE=") + assert.Contains(t, envVars[1], "AWS_CONFIG_FILE=") + + // Check worker node options + assert.Equal(t, "true", workerNodeOp.AllowOther) + assert.Equal(t, "https://s3.us-east.cloud-object-storage.appdomain.cloud", workerNodeOp.EndpointURL) + assert.Equal(t, "us-east", workerNodeOp.Region) + assert.Equal(t, "1000", workerNodeOp.UID) + assert.Equal(t, "2000", workerNodeOp.GID) + assert.Equal(t, "debug", workerNodeOp.LogLevel) + assert.Equal(t, "/var/log/mount-s3", workerNodeOp.LogDirectory) + assert.Equal(t, "/tmp/cache", workerNodeOp.CacheDir) + assert.Equal(t, "true", workerNodeOp.ReadOnly) + assert.Contains(t, workerNodeOp.Args, "--dir-mode=0755") + assert.Contains(t, workerNodeOp.Args, "--max-threads=32") +} + +func TestFormulateMountOptions_LogLevelDebugCrt(t *testing.T) { + s3Mounter := &MountpointS3Mounter{ + LogLevel: "debug-crt", + } + + nodeServerOp, _, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") + + assert.Contains(t, nodeServerOp, "--debug-crt") + assert.Equal(t, "debug-crt", workerNodeOp.LogLevel) +} + +func TestFormulateMountOptions_LogLevelNoLog(t *testing.T) { + s3Mounter := &MountpointS3Mounter{ + LogLevel: "no-log", + } + + nodeServerOp, _, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") + + assert.Contains(t, nodeServerOp, "--no-log") + assert.Equal(t, "no-log", workerNodeOp.LogLevel) +} + +func TestFormulateMountOptions_InvalidLogLevel(t *testing.T) { + s3Mounter := &MountpointS3Mounter{ + LogLevel: "invalid-level", + } + + nodeServerOp, _, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") + + // Should not contain any log level flag + assert.NotContains(t, nodeServerOp, "--debug") + assert.NotContains(t, nodeServerOp, "--debug-crt") + assert.NotContains(t, nodeServerOp, "--no-log") + assert.Empty(t, workerNodeOp.LogLevel) +} + +func TestFormulateMountOptions_MinimalConfig(t *testing.T) { + s3Mounter := &MountpointS3Mounter{ + MountOptions: []string{}, + } + + nodeServerOp, envVars, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") + + // Check basic options are present + assert.Contains(t, nodeServerOp, "test-bucket") + assert.Contains(t, nodeServerOp, target) + assert.Contains(t, nodeServerOp, "--allow-other") + + // Check env vars + assert.Len(t, envVars, 2) + + // Check worker node options + assert.Equal(t, "true", workerNodeOp.AllowOther) +} + +func TestCreateS3MountConfig_Success(t *testing.T) { + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return os.CreateTemp("", "test") + } + Chmod = func(path string, perm os.FileMode) error { + return nil + } + + s3Mounter := &MountpointS3Mounter{ + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + LocConstraint: "us-east", + EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", + } + + err := createS3MountConfig("/config/path", s3Mounter) + assert.NoError(t, err) +} + +func TestCreateS3MountConfig_MakeDirFails(t *testing.T) { + MakeDir = func(path string, perm os.FileMode) error { + return errors.New("failed to create directory") + } + + s3Mounter := &MountpointS3Mounter{ + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := createS3MountConfig("/config/path", s3Mounter) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to create directory") +} + +func TestCreateS3MountConfig_CreateFileFails(t *testing.T) { + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return nil, errors.New("failed to create file") + } + + s3Mounter := &MountpointS3Mounter{ + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := createS3MountConfig("/config/path", s3Mounter) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to create file") +} + +func TestCreateS3MountConfig_ChmodFails(t *testing.T) { + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return os.CreateTemp("", "test") + } + Chmod = func(path string, perm os.FileMode) error { + return errors.New("failed to chmod") + } + + s3Mounter := &MountpointS3Mounter{ + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := createS3MountConfig("/config/path", s3Mounter) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to chmod") +} + +// mockEnvMounter implements the envMounter interface for testing +type mockEnvMounter struct { + mounterUtils.MounterUtils +} + +func (m *mockEnvMounter) FuseMountWithEnv(path string, comm string, args []string, envVars []string) error { + return nil +} + + +// Test deduplicatePassthrough function +func TestDeduplicatePassthrough(t *testing.T) { + tests := []struct { + name string + scFlags []string + secretFlags []string + expected []string + }{ + { + name: "No overlap - all flags kept", + scFlags: []string{"--allow-delete", "--dir-mode=0755"}, + secretFlags: []string{"--max-threads=128"}, + expected: []string{"--allow-delete", "--dir-mode=0755", "--max-threads=128"}, + }, + { + name: "Complete overlap - secret wins", + scFlags: []string{"--max-threads=32", "--maximum-throughput-gbps=10"}, + secretFlags: []string{"--max-threads=128", "--maximum-throughput-gbps=25"}, + expected: []string{"--max-threads=128", "--maximum-throughput-gbps=25"}, + }, + { + name: "Partial overlap - secret wins on duplicates", + scFlags: []string{"--max-threads=32", "--maximum-throughput-gbps=10", "--allow-delete"}, + secretFlags: []string{"--max-threads=128", "--maximum-throughput-gbps=25"}, + expected: []string{"--allow-delete", "--max-threads=128", "--maximum-throughput-gbps=25"}, + }, + { + name: "Empty SC flags", + scFlags: []string{}, + secretFlags: []string{"--max-threads=128"}, + expected: []string{"--max-threads=128"}, + }, + { + name: "Empty secret flags", + scFlags: []string{"--max-threads=32"}, + secretFlags: []string{}, + expected: []string{"--max-threads=32"}, + }, + { + name: "Both empty", + scFlags: []string{}, + secretFlags: []string{}, + expected: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := deduplicatePassthrough(tt.scFlags, tt.secretFlags) + assert.ElementsMatch(t, tt.expected, result) + }) + } +} + +// Test flagKey function +func TestFlagKey(t *testing.T) { + tests := []struct { + name string + flag string + expected string + }{ + { + name: "Flag with value", + flag: "--max-threads=128", + expected: "max-threads", + }, + { + name: "Flag without value", + flag: "--allow-delete", + expected: "allow-delete", + }, + { + name: "Flag without double dash", + flag: "max-threads=128", + expected: "max-threads", + }, + { + name: "Complex flag with multiple equals", + flag: "--endpoint-url=https://s3.example.com", + expected: "endpoint-url", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := flagKey(tt.flag) + assert.Equal(t, tt.expected, result) + }) + } +} + +// Test splitSecretMountOptions function +func TestSplitSecretMountOptions(t *testing.T) { + tests := []struct { + name string + raw string + expected []string + }{ + { + name: "Basic multiline options", + raw: `log-level=debug +read-only +cache=/tmp/cache`, + expected: []string{"log-level=debug", "read-only", "cache=/tmp/cache"}, + }, + { + name: "Options with trailing commas", + raw: `log-level=debug, +read-only, +cache=/tmp/cache,`, + expected: []string{"log-level=debug", "read-only", "cache=/tmp/cache"}, + }, + { + name: "Options with comments", + raw: `log-level=debug +# This is a comment +read-only +cache=/tmp/cache`, + expected: []string{"log-level=debug", "read-only", "cache=/tmp/cache"}, + }, + { + name: "Options with empty lines", + raw: `log-level=debug + +read-only + +cache=/tmp/cache`, + expected: []string{"log-level=debug", "read-only", "cache=/tmp/cache"}, + }, + { + name: "Options with whitespace", + raw: ` log-level=debug + read-only + cache=/tmp/cache `, + expected: []string{"log-level=debug", "read-only", "cache=/tmp/cache"}, + }, + { + name: "Empty string", + raw: "", + expected: nil, + }, + { + name: "Only comments and empty lines", + raw: `# Comment 1 + +# Comment 2`, + expected: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := splitSecretMountOptions(tt.raw) + assert.Equal(t, tt.expected, result) + }) + } +} + +// Test parseMountpointS3Options function +func TestParseMountpointS3Options(t *testing.T) { + tests := []struct { + name string + opts []string + expectedMounter *MountpointS3Mounter + expectedRemaining []string + }{ + { + name: "Parse structured options", + opts: []string{"--uid=1000", "--gid=2000", "--log-level=debug", "--cache=/tmp/cache", "--read-only"}, + expectedMounter: &MountpointS3Mounter{ + UID: "1000", + GID: "2000", + LogLevel: "debug", + CacheDir: "/tmp/cache", + ReadOnly: true, + }, + expectedRemaining: []string{}, + }, + { + name: "Parse with passthrough options", + opts: []string{"--uid=1000", "--max-threads=32", "--dir-mode=0755"}, + expectedMounter: &MountpointS3Mounter{ + UID: "1000", + }, + expectedRemaining: []string{"--max-threads=32", "--dir-mode=0755"}, + }, + { + name: "Parse allow-overwrite and incremental-upload", + opts: []string{"--allow-overwrite", "--incremental-upload"}, + expectedMounter: &MountpointS3Mounter{ + AllowOverwrite: true, + IncrementalUpload: true, + }, + expectedRemaining: []string{}, + }, + { + name: "Parse log-directory", + opts: []string{"--log-directory=/var/log/mount-s3"}, + expectedMounter: &MountpointS3Mounter{ + LogDirectory: "/var/log/mount-s3", + }, + expectedRemaining: []string{}, + }, + { + name: "Ignore allow-other", + opts: []string{"--allow-other", "--uid=1000"}, + expectedMounter: &MountpointS3Mounter{ + UID: "1000", + }, + expectedRemaining: []string{}, + }, + { + name: "Add double dash to options without it", + opts: []string{"max-threads=32", "dir-mode=0755"}, + expectedMounter: &MountpointS3Mounter{}, + expectedRemaining: []string{"--max-threads=32", "--dir-mode=0755"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mounter := &MountpointS3Mounter{} + remaining := parseMountpointS3Options(mounter, tt.opts) + + assert.Equal(t, tt.expectedMounter.UID, mounter.UID) + assert.Equal(t, tt.expectedMounter.GID, mounter.GID) + assert.Equal(t, tt.expectedMounter.LogLevel, mounter.LogLevel) + assert.Equal(t, tt.expectedMounter.LogDirectory, mounter.LogDirectory) + assert.Equal(t, tt.expectedMounter.CacheDir, mounter.CacheDir) + assert.Equal(t, tt.expectedMounter.ReadOnly, mounter.ReadOnly) + assert.Equal(t, tt.expectedMounter.AllowOverwrite, mounter.AllowOverwrite) + assert.Equal(t, tt.expectedMounter.IncrementalUpload, mounter.IncrementalUpload) + assert.ElementsMatch(t, tt.expectedRemaining, remaining) + }) + } +} + +// Test incremental-upload blocking +func TestNewMountpointS3Mounter_IncrementalUploadBlocked(t *testing.T) { + secretMap := map[string]string{ + "bucketName": "test-bucket", + "accessKey": "test-access-key", + "secretKey": "test-secret-key", + "mountOptions": `incremental-upload +allow-overwrite`, + } + + mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + + s3Mounter, ok := mounter.(*MountpointS3Mounter) + assert.True(t, ok) + + // incremental-upload should be blocked (set to false) + assert.False(t, s3Mounter.IncrementalUpload, "incremental-upload should be blocked due to data loss issues") + // allow-overwrite should still be set + assert.True(t, s3Mounter.AllowOverwrite) +} + +// Test SC and secret mount options precedence +func TestNewMountpointS3Mounter_MountOptionsPrecedence(t *testing.T) { + secretMap := map[string]string{ + "bucketName": "test-bucket", + "accessKey": "test-access-key", + "secretKey": "test-secret-key", + "mountOptions": `max-threads=128 +maximum-throughput-gbps=25`, + } + + scMountOptions := []string{"--max-threads=32", "--maximum-throughput-gbps=10", "--allow-delete"} + + mounter := NewMountpointS3Mounter(secretMap, scMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + + s3Mounter, ok := mounter.(*MountpointS3Mounter) + assert.True(t, ok) + + // Secret options should override SC options for same keys + assert.Contains(t, s3Mounter.MountOptions, "--max-threads=128") + assert.Contains(t, s3Mounter.MountOptions, "--maximum-throughput-gbps=25") + assert.NotContains(t, s3Mounter.MountOptions, "--max-threads=32") + assert.NotContains(t, s3Mounter.MountOptions, "--maximum-throughput-gbps=10") + + // SC-only options should be preserved + assert.Contains(t, s3Mounter.MountOptions, "--allow-delete") +} + +// Test read-only priority resolution in Mount +func TestMountpointS3Mount_ReadOnlyPriority(t *testing.T) { + mountWorker = false + + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return os.CreateTemp("", "test") + } + Chmod = func(path string, perm os.FileMode) error { + return nil + } + + mockUtils := &mockEnvMounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mockUtils, + BucketName: "test-bucket", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + ReadOnly: true, + AllowOverwrite: true, // Should be cleared + IncrementalUpload: true, // Should be cleared + MountOptions: []string{"--allow-delete", "--max-threads=32"}, // allow-delete should be removed + } + + err := s3Mounter.Mount(source, target) + assert.NoError(t, err) + + // Verify read-only priority cleared conflicting options + assert.False(t, s3Mounter.AllowOverwrite) + assert.False(t, s3Mounter.IncrementalUpload) + assert.NotContains(t, s3Mounter.MountOptions, "--allow-delete") + assert.Contains(t, s3Mounter.MountOptions, "--max-threads=32") +} From 287b71d280d492b9650bb553e0a306041576d8b5 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Tue, 12 May 2026 17:13:39 +0530 Subject: [PATCH 053/128] Updated test-cases --- .github/workflows/release.yml | 1 + pkg/mounter/mounter-s3mounter_test.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a858c440..486266de 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -114,6 +114,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' APP_VERSION: 0.12.27 + steps: - name: Checkout Code diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go index 7e7efb5f..01046e45 100644 --- a/pkg/mounter/mounter-s3mounter_test.go +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -711,7 +711,7 @@ cache=/tmp/cache`, raw: `# Comment 1 # Comment 2`, - expected: []string{}, + expected: nil, }, } From 70c44563c19c5b9bc04e33c30dbf074bf3c0267e Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Tue, 12 May 2026 18:41:36 +0530 Subject: [PATCH 054/128] updated test case --- .github/workflows/release.yml | 6 +++--- pkg/mounter/mounter_test.go | 37 +++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 486266de..f9025aa5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.27 + APP_VERSION: 0.12.28 steps: @@ -140,8 +140,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.27 - name: v0.12.27 + tag_name: v0.12.28 + name: v0.12.28 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/pkg/mounter/mounter_test.go b/pkg/mounter/mounter_test.go index 0ae7af4d..ab3bb71f 100644 --- a/pkg/mounter/mounter_test.go +++ b/pkg/mounter/mounter_test.go @@ -90,6 +90,35 @@ func TestNewMounter(t *testing.T) { }, expectedErr: nil, }, + { + name: "MountpointS3 Mounter", + attrib: map[string]string{"mounter": constants.AMAZONS3MOUNTER}, + secretMap: map[string]string{ + "cosEndpoint": "test-endpoint", + "locationConstraint": "test-loc-constraint", + "bucketName": "test-bucket-name", + "objectPath": "test-obj-path", + "accessKey": "test-access-key", + "secretKey": "test-secret-key", + "uid": "1000", + "gid": "2000", + }, + mountOptions: []string{"--dir-mode=0755", "--file-mode=0644"}, + expected: &MountpointS3Mounter{ + BucketName: "test-bucket-name", + ObjectPath: "test-obj-path", + EndPoint: "test-endpoint", + LocConstraint: "test-loc-constraint", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + AuthType: "hmac", + UID: "1000", + GID: "2000", + MountOptions: []string{"--dir-mode=0755", "--file-mode=0644"}, + MounterUtils: &mounterUtils.MounterOptsUtils{}, + }, + expectedErr: nil, + }, { name: "Default Mounter", attrib: map[string]string{}, @@ -145,6 +174,14 @@ func TestNewMounter(t *testing.T) { rclone.MountOptions = nil expected.MountOptions = nil } + if s3mounter, ok := result.(*MountpointS3Mounter); ok { + expected := test.expected.(*MountpointS3Mounter) + if !stringSlicesEqualIgnoreOrder(s3mounter.MountOptions, expected.MountOptions) { + t.Errorf("MountOptions mismatch.\nGot: %v\nWant: %v", s3mounter.MountOptions, expected.MountOptions) + } + s3mounter.MountOptions = nil + expected.MountOptions = nil + } assert.Equal(t, result, test.expected) From d9a4041fe5a197f375bb3f99096c71cf43c29569 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Tue, 14 Jul 2026 23:41:16 +0530 Subject: [PATCH 055/128] publish 0.12.28 version --- .github/workflows/release.yml | 1 - cos-csi-mounter/Makefile | 2 +- cos-csi-mounter/server/s3mounter.go | 22 ++++++++- pkg/mounter/mounter_s3mounter.go | 69 ++++++++++++++++++++--------- 4 files changed, 69 insertions(+), 25 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f9025aa5..8e32fcaf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -114,7 +114,6 @@ jobs: env: IS_LATEST_RELEASE: 'true' APP_VERSION: 0.12.28 - steps: - name: Checkout Code diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index 57dc8fde..00e487f5 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.26 +APP_VERSION := 0.12.27 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go index 613e3a66..bb8c040b 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/s3mounter.go @@ -31,9 +31,13 @@ type s3MounterArgs struct { // Write operations - conflict resolution applied when read-only is set. ReadOnly string `json:"read-only,omitempty"` + AllowDelete string `json:"allow-delete,omitempty"` AllowOverwrite string `json:"allow-overwrite,omitempty"` IncrementalUpload string `json:"incremental-upload,omitempty"` + // ForcePathStyle is always true for IBM COS compatibility. + ForcePathStyle string `json:"force-path-style,omitempty"` + // Passthrough flags β€” user-supplied flags not handled by structured fields. // Secret flags override SC flags when the same key appears in both. // These are already deduplicated by the node server before being sent here. @@ -52,8 +56,12 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string args.IncrementalUpload = "" } - // Read-only priority resolution. + // Read-only priority resolution: suppress write-enabling flags. if args.ReadOnly == "true" { + if args.AllowDelete == "true" { + logger.Warn("read-only is set, clearing allow-delete") + args.AllowDelete = "" + } if args.AllowOverwrite == "true" { logger.Warn("read-only is set, clearing allow-overwrite") args.AllowOverwrite = "" @@ -70,6 +78,9 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string if args.ReadOnly == "true" { result = append(result, "--read-only") } + if args.AllowDelete == "true" { + result = append(result, "--allow-delete") + } if args.AllowOverwrite == "true" { result = append(result, "--allow-overwrite") } @@ -111,6 +122,9 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string if args.CacheDir != "" { result = append(result, "--cache="+args.CacheDir) } + if args.ForcePathStyle == "true" { + result = append(result, "--force-path-style") + } // Passthrough flags appended last β€” already deduplicated by node server. result = append(result, args.Args...) @@ -158,6 +172,12 @@ func (args s3MounterArgs) Validate(targetPath string) error { return fmt.Errorf("cannot convert value of read-only into boolean: %v", args.ReadOnly) } } + if args.AllowDelete != "" { + if isBool := isBoolString(args.AllowDelete); !isBool { + logger.Error("cannot convert value of allow-delete into boolean", zap.Any("allow-delete", args.AllowDelete)) + return fmt.Errorf("cannot convert value of allow-delete into boolean: %v", args.AllowDelete) + } + } if args.AllowOverwrite != "" { if isBool := isBoolString(args.AllowOverwrite); !isBool { logger.Error("cannot convert value of allow-overwrite into boolean", zap.Any("allow-overwrite", args.AllowOverwrite)) diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 903fcd7a..6290c8a1 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -29,11 +29,11 @@ type MountpointS3Mounter struct { UID string GID string - LogLevel string - LogDirectory string - CacheDir string + LogLevel string + CacheDir string ReadOnly bool + AllowDelete bool AllowOverwrite bool IncrementalUpload bool @@ -43,6 +43,12 @@ type MountpointS3Mounter struct { MounterUtils utils.MounterUtils } +const ( + // s3MountLogDirectory is the hardcoded log directory for mount-s3, matching + // the same convention as rclone's hardcoded --log-file=/var/log/rclone.log. + s3MountLogDirectory = "/var/log/mount-s3" +) + // s3MounterArgs holds the args sent to the worker node daemon for mount-s3. // This struct must stay in sync with s3MounterArgs in the worker (cos-csi-mounter). type s3MounterArgs struct { @@ -62,9 +68,13 @@ type s3MounterArgs struct { CacheDir string `json:"cache,omitempty"` ReadOnly string `json:"read-only,omitempty"` + AllowDelete string `json:"allow-delete,omitempty"` AllowOverwrite string `json:"allow-overwrite,omitempty"` IncrementalUpload string `json:"incremental-upload,omitempty"` + // ForcePathStyle is always true for IBM COS compatibility. + ForcePathStyle string `json:"force-path-style,omitempty"` + // Passthrough flags β€” user-supplied flags not handled by structured fields. // Secret flags override SC flags when the same key appears in both. Args []string `json:"args,omitempty"` @@ -147,6 +157,11 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounter.IncrementalUpload = false } + // Step 5: allow-delete and allow-overwrite are hardcoded defaults. + // They are suppressed later in Mount() when read-only is set. + mounter.AllowDelete = true + mounter.AllowOverwrite = true + mounter.MounterUtils = mounterUtils klog.Infof("newMountpointS3Mounter args:\n\tbucketName: [%s]\n\tobjectPath: [%s]\n\tendPoint: [%s]\n\tlocationConstraint: [%s]\n\tauthType: [%s]", @@ -242,17 +257,23 @@ func parseMountpointS3Options(mounter *MountpointS3Mounter, opts []string) []str mounter.LogLevel = value } case "log-directory": - if hasValue { - mounter.LogDirectory = value - } + // Hardcoded β€” user-supplied value is ignored. + klog.Warningf("parseMountpointS3Options: 'log-directory' is hardcoded to %q and cannot be overridden. Ignoring.", s3MountLogDirectory) + case "force-path-style": + // Hardcoded β€” always enabled for IBM COS compatibility. + klog.Warningf("parseMountpointS3Options: 'force-path-style' is hardcoded and cannot be overridden. Ignoring.") case "cache": if hasValue { mounter.CacheDir = value } case "read-only": mounter.ReadOnly = true + case "allow-delete": + // Hardcoded β€” always enabled unless read-only. User override ignored. + klog.Warningf("parseMountpointS3Options: 'allow-delete' is hardcoded and cannot be overridden. Ignoring.") case "allow-overwrite": - mounter.AllowOverwrite = true + // Hardcoded β€” always enabled unless read-only. User override ignored. + klog.Warningf("parseMountpointS3Options: 'allow-overwrite' is hardcoded and cannot be overridden. Ignoring.") case "incremental-upload": mounter.IncrementalUpload = true default: @@ -272,8 +293,12 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { klog.Info("-MountpointS3Mounter Mount-") klog.Infof("Mount args:\n\tsource: <%s>\n\ttarget: <%s>", source, target) - // Read-only priority resolution. + // Read-only priority resolution: suppress write-enabling flags. if s3.ReadOnly { + if s3.AllowDelete { + klog.Infof("MountpointS3Mounter: read-only set β€” clearing allow-delete") + s3.AllowDelete = false + } if s3.AllowOverwrite { klog.Infof("MountpointS3Mounter: read-only set β€” clearing allow-overwrite") s3.AllowOverwrite = false @@ -282,16 +307,6 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { klog.Infof("MountpointS3Mounter: read-only set β€” clearing incremental-upload") s3.IncrementalUpload = false } - var filtered []string - for _, opt := range s3.MountOptions { - trimmed := strings.TrimPrefix(opt, "--") - if trimmed != "allow-delete" { - filtered = append(filtered, opt) - } else { - klog.Infof("MountpointS3Mounter: read-only set β€” removing allow-delete from passthrough options") - } - } - s3.MountOptions = filtered } // NOTE: No directory validation or creation here. @@ -493,14 +508,24 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW } } - if s3.LogDirectory != "" { - nodeServerOp = append(nodeServerOp, "--log-directory="+s3.LogDirectory) - workerNodeOp.LogDirectory = s3.LogDirectory - } + // log-directory is hardcoded β€” always passed, never user-configurable. + nodeServerOp = append(nodeServerOp, "--log-directory="+s3MountLogDirectory) + workerNodeOp.LogDirectory = s3MountLogDirectory + if s3.CacheDir != "" { nodeServerOp = append(nodeServerOp, "--cache="+s3.CacheDir) workerNodeOp.CacheDir = s3.CacheDir } + + // force-path-style is hardcoded β€” required for IBM COS compatibility. + nodeServerOp = append(nodeServerOp, "--force-path-style") + workerNodeOp.ForcePathStyle = "true" + + // allow-delete and allow-overwrite are hardcoded defaults, suppressed when read-only. + if s3.AllowDelete { + nodeServerOp = append(nodeServerOp, "--allow-delete") + workerNodeOp.AllowDelete = "true" + } if s3.AllowOverwrite { nodeServerOp = append(nodeServerOp, "--allow-overwrite") workerNodeOp.AllowOverwrite = "true" From ca82ac23d38e9588a5cf3b2bb286c5972b67ff88 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Tue, 14 Jul 2026 23:47:26 +0530 Subject: [PATCH 056/128] publish 0.12.28 version --- cos-csi-mounter/Makefile | 2 +- pkg/mounter/mounter_s3mounter.go | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index 00e487f5..09774920 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.27 +APP_VERSION := 0.12.28 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 6290c8a1..ee1acf4b 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -29,8 +29,9 @@ type MountpointS3Mounter struct { UID string GID string - LogLevel string - CacheDir string + LogLevel string + LogDirectory string + CacheDir string ReadOnly bool AllowDelete bool @@ -257,8 +258,10 @@ func parseMountpointS3Options(mounter *MountpointS3Mounter, opts []string) []str mounter.LogLevel = value } case "log-directory": - // Hardcoded β€” user-supplied value is ignored. + // Hardcoded β€” user-supplied value is ignored; the struct field is set + // to the fixed path so callers can read it back consistently. klog.Warningf("parseMountpointS3Options: 'log-directory' is hardcoded to %q and cannot be overridden. Ignoring.", s3MountLogDirectory) + mounter.LogDirectory = s3MountLogDirectory case "force-path-style": // Hardcoded β€” always enabled for IBM COS compatibility. klog.Warningf("parseMountpointS3Options: 'force-path-style' is hardcoded and cannot be overridden. Ignoring.") From 09e1c7e6bed4367de38083f548489ea989fe9f6d Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Wed, 15 Jul 2026 00:00:59 +0530 Subject: [PATCH 057/128] publish 0.12.28 version --- pkg/mounter/mounter-s3mounter_test.go | 38 ++++++++++++++++----------- pkg/mounter/mounter_test.go | 24 +++++++++-------- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go index 01046e45..f862583f 100644 --- a/pkg/mounter/mounter-s3mounter_test.go +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -755,16 +755,19 @@ func TestParseMountpointS3Options(t *testing.T) { name: "Parse allow-overwrite and incremental-upload", opts: []string{"--allow-overwrite", "--incremental-upload"}, expectedMounter: &MountpointS3Mounter{ - AllowOverwrite: true, + // allow-overwrite is now hardcoded β€” parsed value is ignored. + AllowOverwrite: false, IncrementalUpload: true, }, expectedRemaining: []string{}, }, { name: "Parse log-directory", - opts: []string{"--log-directory=/var/log/mount-s3"}, + // log-directory is now hardcoded β€” any user-supplied value is ignored and + // the struct is always set to the fixed path s3MountLogDirectory. + opts: []string{"--log-directory=/some/custom/path"}, expectedMounter: &MountpointS3Mounter{ - LogDirectory: "/var/log/mount-s3", + LogDirectory: s3MountLogDirectory, }, expectedRemaining: []string{}, }, @@ -832,22 +835,25 @@ func TestNewMountpointS3Mounter_MountOptionsPrecedence(t *testing.T) { "mountOptions": `max-threads=128 maximum-throughput-gbps=25`, } - + + // --allow-delete is now hardcoded β€” it must not appear in passthrough MountOptions. scMountOptions := []string{"--max-threads=32", "--maximum-throughput-gbps=10", "--allow-delete"} mounter := NewMountpointS3Mounter(secretMap, scMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) s3Mounter, ok := mounter.(*MountpointS3Mounter) assert.True(t, ok) - - // Secret options should override SC options for same keys + + // Secret options should override SC options for same keys. assert.Contains(t, s3Mounter.MountOptions, "--max-threads=128") assert.Contains(t, s3Mounter.MountOptions, "--maximum-throughput-gbps=25") assert.NotContains(t, s3Mounter.MountOptions, "--max-threads=32") assert.NotContains(t, s3Mounter.MountOptions, "--maximum-throughput-gbps=10") - - // SC-only options should be preserved - assert.Contains(t, s3Mounter.MountOptions, "--allow-delete") + + // allow-delete is now hardcoded β€” it must NOT appear in the passthrough slice. + assert.NotContains(t, s3Mounter.MountOptions, "--allow-delete") + // But it is always enabled via the struct field. + assert.True(t, s3Mounter.AllowDelete) } // Test read-only priority resolution in Mount @@ -874,17 +880,19 @@ func TestMountpointS3Mount_ReadOnlyPriority(t *testing.T) { AccessKey: "test-access-key", SecretKey: "test-secret-key", ReadOnly: true, - AllowOverwrite: true, // Should be cleared - IncrementalUpload: true, // Should be cleared - MountOptions: []string{"--allow-delete", "--max-threads=32"}, // allow-delete should be removed + AllowDelete: true, // Should be cleared by read-only + AllowOverwrite: true, // Should be cleared by read-only + IncrementalUpload: true, // Should be cleared by read-only + MountOptions: []string{"--max-threads=32"}, } err := s3Mounter.Mount(source, target) assert.NoError(t, err) - - // Verify read-only priority cleared conflicting options + + // Verify read-only priority cleared all write-enabling flags. + assert.False(t, s3Mounter.AllowDelete) assert.False(t, s3Mounter.AllowOverwrite) assert.False(t, s3Mounter.IncrementalUpload) - assert.NotContains(t, s3Mounter.MountOptions, "--allow-delete") + // Passthrough options unrelated to write-access are preserved. assert.Contains(t, s3Mounter.MountOptions, "--max-threads=32") } diff --git a/pkg/mounter/mounter_test.go b/pkg/mounter/mounter_test.go index ab3bb71f..4e382d7a 100644 --- a/pkg/mounter/mounter_test.go +++ b/pkg/mounter/mounter_test.go @@ -105,17 +105,19 @@ func TestNewMounter(t *testing.T) { }, mountOptions: []string{"--dir-mode=0755", "--file-mode=0644"}, expected: &MountpointS3Mounter{ - BucketName: "test-bucket-name", - ObjectPath: "test-obj-path", - EndPoint: "test-endpoint", - LocConstraint: "test-loc-constraint", - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - AuthType: "hmac", - UID: "1000", - GID: "2000", - MountOptions: []string{"--dir-mode=0755", "--file-mode=0644"}, - MounterUtils: &mounterUtils.MounterOptsUtils{}, + BucketName: "test-bucket-name", + ObjectPath: "test-obj-path", + EndPoint: "test-endpoint", + LocConstraint: "test-loc-constraint", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + AuthType: "hmac", + UID: "1000", + GID: "2000", + AllowDelete: true, + AllowOverwrite: true, + MountOptions: []string{"--dir-mode=0755", "--file-mode=0644"}, + MounterUtils: &mounterUtils.MounterOptsUtils{}, }, expectedErr: nil, }, From 4238902ed5616ae26a1e62e3da49f91a23bb868e Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 20 Jul 2026 00:19:14 +0530 Subject: [PATCH 058/128] Added readonly and gid variable to Mounter function --- .github/workflows/release.yml | 4 +- .secrets.baseline | 156 ++++++++++++++++++++++---- pkg/mounter/mounter-s3mounter_test.go | 24 ++-- pkg/mounter/mounter.go | 2 +- pkg/mounter/mounter_s3mounter.go | 15 +-- 5 files changed, 159 insertions(+), 42 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8e32fcaf..69f62aae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.28 - name: v0.12.28 + tag_name: v0.12.29 + name: v0.12.29 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/.secrets.baseline b/.secrets.baseline index 675075a7..bdfccc18 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "go.sum|^.secrets.baseline$", "lines": null }, - "generated_at": "2025-12-18T07:17:56Z", + "generated_at": "2026-07-19T18:48:39Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -80,6 +80,7 @@ "README.md": [ { "hashed_secret": "325778ab9d49a6df7bc13a83563bec2de2a84c95", + "is_secret": true, "is_verified": false, "line_number": 72, "type": "Secret Keyword", @@ -89,6 +90,7 @@ "examples/kubernetes/cos-s3-csi-rclone-pvc.yaml": [ { "hashed_secret": "ccae38d9a6159915fbbf9bf72dbf6760e0c63c35", + "is_secret": true, "is_verified": false, "line_number": 7, "type": "Secret Keyword", @@ -98,6 +100,7 @@ "examples/kubernetes/cos-s3-csi-rclone-secret.yaml": [ { "hashed_secret": "d08f88df745fa7950b104e4a707a31cfce7b5841", + "is_secret": true, "is_verified": false, "line_number": 9, "type": "Secret Keyword", @@ -107,6 +110,7 @@ "examples/kubernetes/cos-s3-csi-s3fs-pvc.yaml": [ { "hashed_secret": "f5691ce6d36f7c4434f9f927ddc09db95c0df745", + "is_secret": true, "is_verified": false, "line_number": 7, "type": "Secret Keyword", @@ -116,31 +120,53 @@ "examples/kubernetes/cos-s3-csi-s3fs-secret.yaml": [ { "hashed_secret": "d08f88df745fa7950b104e4a707a31cfce7b5841", + "is_secret": true, "is_verified": false, "line_number": 10, "type": "Secret Keyword", "verified_result": null } ], + "pkg/constants/constants.go": [ + { + "hashed_secret": "84ad41837ea9d0e48a7186f627442db291621455", + "is_secret": true, + "is_verified": false, + "line_number": 54, + "type": "Secret Keyword", + "verified_result": null + } + ], "pkg/driver/controllerserver.go": [ + { + "hashed_secret": "4e1e3828599786b2713ed5a843d6e0bcf854a2f6", + "is_secret": true, + "is_verified": false, + "line_number": 138, + "type": "Secret Keyword", + "verified_result": null + }, { "hashed_secret": "a1f0e99af8b76b514ef9e6a174e9c79970332082", + "is_secret": true, "is_verified": false, - "line_number": 176, + "line_number": 207, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "b732fb611fd46a38e8667f9972e0cde777fbe37f", + "is_secret": true, "is_verified": false, - "line_number": 179, + "line_number": 210, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "62d6314bd936033553442d72fe8ce3b93ba8802d", + "is_secret": true, "is_verified": false, - "line_number": 324, + "line_number": 389, "type": "Secret Keyword", "verified_result": null }, @@ -148,7 +174,7 @@ "hashed_secret": "39f69c278f46165447f30d10acf54277aaa3d5fc", "is_secret": false, "is_verified": false, - "line_number": 460, + "line_number": 525, "type": "Secret Keyword", "verified_result": null }, @@ -156,14 +182,23 @@ "hashed_secret": "f75b33f87ffeacb3a4f793a09693e672e07449ff", "is_secret": false, "is_verified": false, - "line_number": 479, + "line_number": 544, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "d4c3d66fd0c38547a3c7a4c6bdc29c36911bc030", + "is_secret": true, "is_verified": false, - "line_number": 544, + "line_number": 623, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "89fe47e5881e37bdb626d8354632f80baa3fe54f", + "is_secret": true, + "is_verified": false, + "line_number": 632, "type": "Secret Keyword", "verified_result": null } @@ -171,8 +206,9 @@ "pkg/driver/controllerserver_test.go": [ { "hashed_secret": "7e6a3680012346b94b54731e13d8a9ffa3790645", + "is_secret": true, "is_verified": false, - "line_number": 248, + "line_number": 249, "type": "Secret Keyword", "verified_result": null } @@ -182,15 +218,39 @@ "hashed_secret": "39f69c278f46165447f30d10acf54277aaa3d5fc", "is_secret": false, "is_verified": false, - "line_number": 98, + "line_number": 111, "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "2ace62c1befa19e3ea37dd52be9f6d508c5163e6", - "is_secret": false, + "hashed_secret": "919673f52800c82484f1c9ab638faa1201c18477", + "is_secret": true, + "is_verified": false, + "line_number": 303, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "769044d35edae0d0d6e0b8c24d68e456d61a7ac1", + "is_secret": true, + "is_verified": false, + "line_number": 307, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "828564546c10e1da3568d3b502cd13ceada5fb93", + "is_secret": true, + "is_verified": false, + "line_number": 311, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "6c9dcae2cd186e569853c911d0799b23f1bc9b95", + "is_secret": true, "is_verified": false, - "line_number": 264, + "line_number": 312, "type": "Secret Keyword", "verified_result": null } @@ -198,8 +258,33 @@ "pkg/mounter/mounter-rclone_test.go": [ { "hashed_secret": "2e7a7ee14caebf378fc32d6cf6f557f347c96773", + "is_secret": true, + "is_verified": false, + "line_number": 115, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "8e2d2b7918839eef755ecb9181235cc83934befb", + "is_secret": true, + "is_verified": false, + "line_number": 367, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "724ba0f0a4c4226b95478a7f304efd7951250ed3", + "is_secret": true, "is_verified": false, - "line_number": 77, + "line_number": 368, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "a56dba7ed82ba76c3b271ce5302214cafc85d1c7", + "is_secret": true, + "is_verified": false, + "line_number": 369, "type": "Secret Keyword", "verified_result": null } @@ -209,7 +294,7 @@ "hashed_secret": "39f69c278f46165447f30d10acf54277aaa3d5fc", "is_secret": false, "is_verified": false, - "line_number": 86, + "line_number": 103, "type": "Secret Keyword", "verified_result": null } @@ -217,8 +302,27 @@ "pkg/mounter/mounter-s3fs_test.go": [ { "hashed_secret": "2e7a7ee14caebf378fc32d6cf6f557f347c96773", + "is_secret": true, "is_verified": false, - "line_number": 21, + "line_number": 22, + "type": "Secret Keyword", + "verified_result": null + } + ], + "pkg/mounter/mounter_s3mounter.go": [ + { + "hashed_secret": "a244263a4d970da788fefed5992903a119375616", + "is_secret": true, + "is_verified": false, + "line_number": 183, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "2ace62c1befa19e3ea37dd52be9f6d508c5163e6", + "is_secret": true, + "is_verified": false, + "line_number": 414, "type": "Secret Keyword", "verified_result": null } @@ -226,6 +330,7 @@ "pkg/mounter/mounter_test.go": [ { "hashed_secret": "2e7a7ee14caebf378fc32d6cf6f557f347c96773", + "is_secret": true, "is_verified": false, "line_number": 45, "type": "Secret Keyword", @@ -237,7 +342,7 @@ "hashed_secret": "096aacba084fa9ee67efa56625495beaeba085a1", "is_secret": false, "is_verified": false, - "line_number": 221, + "line_number": 243, "type": "Secret Keyword", "verified_result": null }, @@ -245,7 +350,15 @@ "hashed_secret": "ccba5450a9a31dd78751224042f7598b6ac2f04f", "is_secret": false, "is_verified": false, - "line_number": 226, + "line_number": 248, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "f75b33f87ffeacb3a4f793a09693e672e07449ff", + "is_secret": true, + "is_verified": false, + "line_number": 270, "type": "Secret Keyword", "verified_result": null } @@ -253,15 +366,17 @@ "pkg/s3client/s3client_test.go": [ { "hashed_secret": "d4c3d66fd0c38547a3c7a4c6bdc29c36911bc030", + "is_secret": true, "is_verified": false, - "line_number": 35, + "line_number": 57, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "2fa0ceed0ed702c459dd69734dfa1bc54e0bdd70", + "is_secret": true, "is_verified": false, - "line_number": 93, + "line_number": 123, "type": "Secret Keyword", "verified_result": null } @@ -269,6 +384,7 @@ "pkg/utils/driver_utils.go": [ { "hashed_secret": "c7c6508b19455e3e8040e60e9833fbede92e5d8e", + "is_secret": true, "is_verified": false, "line_number": 376, "type": "Secret Keyword", @@ -278,6 +394,7 @@ "tests/e2e/s3fs_test.go": [ { "hashed_secret": "d5cd5ec53cf11ad72f24451e6c077d94d0481046", + "is_secret": true, "is_verified": false, "line_number": 147, "type": "Secret Keyword", @@ -287,6 +404,7 @@ "tests/e2e/testsuites/basespecs.go": [ { "hashed_secret": "7519d22977923ba306715e7048cede9b5ee60d0b", + "is_secret": true, "is_verified": false, "line_number": 117, "type": "Secret Keyword", diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go index f862583f..b7807dcd 100644 --- a/pkg/mounter/mounter-s3mounter_test.go +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -17,9 +17,7 @@ var ( "accessKey": "test-access-key", "secretKey": "test-secret-key", "uid": "1000", - "gid": "1000", "mountOptions": `log-level=debug -read-only cache=/tmp/cache`, } @@ -27,7 +25,7 @@ cache=/tmp/cache`, ) func TestNewMountpointS3Mounter_Success(t *testing.T) { - mounter := NewMountpointS3Mounter(s3MounterSecretMap, s3MounterMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + mounter := NewMountpointS3Mounter(s3MounterSecretMap, s3MounterMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), "1000", true) s3Mounter, ok := mounter.(*MountpointS3Mounter) assert.True(t, ok) @@ -39,7 +37,7 @@ func TestNewMountpointS3Mounter_Success(t *testing.T) { assert.Equal(t, s3MounterSecretMap["accessKey"], s3Mounter.AccessKey) assert.Equal(t, s3MounterSecretMap["secretKey"], s3Mounter.SecretKey) assert.Equal(t, s3MounterSecretMap["uid"], s3Mounter.UID) - assert.Equal(t, s3MounterSecretMap["gid"], s3Mounter.GID) + assert.Equal(t, "1000", s3Mounter.GID) assert.Equal(t, "debug", s3Mounter.LogLevel) assert.True(t, s3Mounter.ReadOnly) assert.Equal(t, "/tmp/cache", s3Mounter.CacheDir) @@ -50,14 +48,13 @@ func TestNewMountpointS3Mounter_Success(t *testing.T) { } func TestNewMountpointS3Mounter_GidWithoutUid(t *testing.T) { - secretMap := map[string]string{ - "gid": "2000", - } + secretMap := map[string]string{} - mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), "2000", false) s3Mounter, ok := mounter.(*MountpointS3Mounter) assert.True(t, ok) + // When gid is set and no uid in secretMap, UID is also set to gid value. assert.Equal(t, "2000", s3Mounter.UID) assert.Equal(t, "2000", s3Mounter.GID) } @@ -65,13 +62,13 @@ func TestNewMountpointS3Mounter_GidWithoutUid(t *testing.T) { func TestNewMountpointS3Mounter_UidOverridesGid(t *testing.T) { secretMap := map[string]string{ "uid": "1000", - "gid": "2000", } - mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), "2000", false) s3Mounter, ok := mounter.(*MountpointS3Mounter) assert.True(t, ok) + // uid from secretMap takes precedence over gid param for the UID field. assert.Equal(t, "1000", s3Mounter.UID) assert.Equal(t, "2000", s3Mounter.GID) } @@ -739,7 +736,8 @@ func TestParseMountpointS3Options(t *testing.T) { GID: "2000", LogLevel: "debug", CacheDir: "/tmp/cache", - ReadOnly: true, + // read-only is now set via MounterParams.ReadOnly, not parsed from flags. + ReadOnly: false, }, expectedRemaining: []string{}, }, @@ -815,7 +813,7 @@ func TestNewMountpointS3Mounter_IncrementalUploadBlocked(t *testing.T) { allow-overwrite`, } - mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), "", false) s3Mounter, ok := mounter.(*MountpointS3Mounter) assert.True(t, ok) @@ -839,7 +837,7 @@ maximum-throughput-gbps=25`, // --allow-delete is now hardcoded β€” it must not appear in passthrough MountOptions. scMountOptions := []string{"--max-threads=32", "--maximum-throughput-gbps=10", "--allow-delete"} - mounter := NewMountpointS3Mounter(secretMap, scMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + mounter := NewMountpointS3Mounter(secretMap, scMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), "", false) s3Mounter, ok := mounter.(*MountpointS3Mounter) assert.True(t, ok) diff --git a/pkg/mounter/mounter.go b/pkg/mounter/mounter.go index ce15d2b9..fe8d8a5f 100644 --- a/pkg/mounter/mounter.go +++ b/pkg/mounter/mounter.go @@ -105,7 +105,7 @@ func (s *CSIMounterFactory) NewMounter(params MounterParams) Mounter { ReadOnly: params.ReadOnly, }) case constants.S3MOUNTER: - return NewMountpointS3Mounter(secretMap, mountFlags, mounterUtils) + return NewMountpointS3Mounter(secretMap, mountFlags, mounterUtils, params.Gid, params.ReadOnly) default: // default to s3fs return NewS3fsMounter(S3fsMounterParams{ diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index ee1acf4b..0c209e47 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -105,7 +105,7 @@ type envMounter interface { // // For passthrough flags, deduplication is applied: if the same flag key appears // in both SC and secret, the SC entry is dropped and the secret entry is kept. -func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounterUtils utils.MounterUtils) Mounter { +func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounterUtils utils.MounterUtils, gid string, readOnly bool) Mounter { klog.Info("-newMountpointS3Mounter-") mounter := &MountpointS3Mounter{} @@ -129,14 +129,14 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounter.LocConstraint = val } - if secretMap["gid"] != "" && secretMap["uid"] == "" { - mounter.UID = secretMap["gid"] + if gid != "" && secretMap["uid"] == "" { + mounter.UID = gid } else if val := secretMap["uid"]; val != "" { mounter.UID = val } - if val := secretMap["gid"]; val != "" { - mounter.GID = val - } + mounter.GID = gid + + mounter.ReadOnly = readOnly mounter.AuthType = "hmac" @@ -270,7 +270,8 @@ func parseMountpointS3Options(mounter *MountpointS3Mounter, opts []string) []str mounter.CacheDir = value } case "read-only": - mounter.ReadOnly = true + // read-only is now set directly from MounterParams.ReadOnly β€” ignore if passed via flags. + klog.Warningf("parseMountpointS3Options: 'read-only' is set via MounterParams and cannot be overridden via mount options. Ignoring.") case "allow-delete": // Hardcoded β€” always enabled unless read-only. User override ignored. klog.Warningf("parseMountpointS3Options: 'allow-delete' is hardcoded and cannot be overridden. Ignoring.") From b3ae9a4a3f247a0b0e61efce1a84e06bf8e754d7 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 20 Jul 2026 00:28:06 +0530 Subject: [PATCH 059/128] publish 0.12.29 version --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 69f62aae..fb14a9f8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -144,7 +144,7 @@ jobs: body: | ## πŸš€ What’s New - Testing aws-s3Mounter image - - Publishing new test version + - Publishing new testing version prerelease: ${{ env.IS_LATEST_RELEASE != 'true' }} - name: Perform CodeQL Analysis From 20fe1cdb89714d3fc35b5b1c121b410115a40d04 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 20 Jul 2026 00:38:13 +0530 Subject: [PATCH 060/128] publish 0.12.29 version --- pkg/mounter/mounter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/mounter/mounter.go b/pkg/mounter/mounter.go index fe8d8a5f..2025cd00 100644 --- a/pkg/mounter/mounter.go +++ b/pkg/mounter/mounter.go @@ -104,7 +104,7 @@ func (s *CSIMounterFactory) NewMounter(params MounterParams) Mounter { Gid: params.Gid, ReadOnly: params.ReadOnly, }) - case constants.S3MOUNTER: + case constants.AMAZONS3MOUNTER: return NewMountpointS3Mounter(secretMap, mountFlags, mounterUtils, params.Gid, params.ReadOnly) default: // default to s3fs From 7d1aa10772aa39143f7feee7a6fff43fca90fe5b Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 20 Jul 2026 00:41:31 +0530 Subject: [PATCH 061/128] publish 0.12.49 version --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fb14a9f8..95bcc921 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.29 - name: v0.12.29 + tag_name: v0.12.49 + name: v0.12.49 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image From f2069612891cd3c2f475ddaf710ff08a29001b83 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 20 Jul 2026 00:51:08 +0530 Subject: [PATCH 062/128] publish 0.12.49 version --- .secrets.baseline | 6 +++--- pkg/mounter/mounter_s3mounter.go | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index bdfccc18..cfdd49cc 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "go.sum|^.secrets.baseline$", "lines": null }, - "generated_at": "2026-07-19T18:48:39Z", + "generated_at": "2026-07-19T19:20:49Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -314,7 +314,7 @@ "hashed_secret": "a244263a4d970da788fefed5992903a119375616", "is_secret": true, "is_verified": false, - "line_number": 183, + "line_number": 187, "type": "Secret Keyword", "verified_result": null }, @@ -322,7 +322,7 @@ "hashed_secret": "2ace62c1befa19e3ea37dd52be9f6d508c5163e6", "is_secret": true, "is_verified": false, - "line_number": 414, + "line_number": 418, "type": "Secret Keyword", "verified_result": null } diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 0c209e47..b33ce091 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -134,7 +134,11 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, } else if val := secretMap["uid"]; val != "" { mounter.UID = val } - mounter.GID = gid + if val := secretMap["gid"]; val != "" { + mounter.GID = val + } else { + mounter.GID = gid + } mounter.ReadOnly = readOnly From 0798211076234532b2869641c51e8293da11d221 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Tue, 21 Jul 2026 10:13:29 +0530 Subject: [PATCH 063/128] publish 0.12.50 version --- .github/workflows/release.yml | 6 +++--- cos-csi-mounter/Makefile | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 95bcc921..a7d84aa0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.28 + APP_VERSION: 0.12.50 steps: - name: Checkout Code @@ -139,8 +139,8 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.49 - name: v0.12.49 + tag_name: v0.12.50 + name: v0.12.50 body: | ## πŸš€ What’s New - Testing aws-s3Mounter image diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index 09774920..210cdc13 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.28 +APP_VERSION := 0.12.50 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin From 143bec166f3473ad678fa23866381e321359549b Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Tue, 18 Aug 2026 19:22:18 +0530 Subject: [PATCH 064/128] publish 0.12.50 version --- cos-csi-mounter/server/s3mounter.go | 13 +++++-- cos-csi-mounter/server/utils.go | 25 +++++++++----- cos-csi-mounter/server/utils_test.go | 51 ++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 11 deletions(-) diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go index bb8c040b..ffbc6c4a 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/s3mounter.go @@ -6,6 +6,13 @@ import ( "go.uber.org/zap" ) +var ( + // Safe base directory for mount-s3 log files. + s3MounterSafeLogDir = "/var/log/mount-s3" + // Safe base directory for mount-s3 cache files. + s3MounterSafeCacheDir = "/tmp" +) + // s3MounterArgs holds the args sent from the node server for mount-s3. // This struct must match the s3MounterArgs in pkg/mounter/mounter_s3mounter.go type s3MounterArgs struct { @@ -191,15 +198,15 @@ func (args s3MounterArgs) Validate(targetPath string) error { } } - // ensureDir creates the directory if it does not exist. + // ensureDir creates the directory if it does not exist, confined to its safe base. if args.CacheDir != "" { - if err := ensureDir(args.CacheDir); err != nil { + if err := ensureDir(args.CacheDir, s3MounterSafeCacheDir); err != nil { logger.Error("failed to create cache directory", zap.String("cache-dir", args.CacheDir), zap.Error(err)) return fmt.Errorf("failed to create cache directory '%s': %w", args.CacheDir, err) } } if args.LogDirectory != "" { - if err := ensureDir(args.LogDirectory); err != nil { + if err := ensureDir(args.LogDirectory, s3MounterSafeLogDir); err != nil { logger.Error("failed to create log directory", zap.String("log-dir", args.LogDirectory), zap.Error(err)) return fmt.Errorf("failed to create log directory '%s': %w", args.LogDirectory, err) } diff --git a/cos-csi-mounter/server/utils.go b/cos-csi-mounter/server/utils.go index 48915dbc..a927998f 100644 --- a/cos-csi-mounter/server/utils.go +++ b/cos-csi-mounter/server/utils.go @@ -126,27 +126,36 @@ func fileExists(path string) (bool, error) { return !info.IsDir(), nil } -// ensureDir creates the cache directory if it doesn't exist. -// It creates the directory with 0755 permissions (rwxr-xr-x). -func ensureDir(path string) error { +// ensureDir creates the directory at path if it doesn't exist. +// It first resolves the absolute path and verifies it is within safeBase +// to prevent path-traversal attacks before creating the directory. +func ensureDir(path, safeBase string) error { + absPath, err := absPathResolver(path) + if err != nil { + return fmt.Errorf("failed to resolve absolute path: %v", err) + } + if !strings.HasPrefix(absPath, safeBase) { + return fmt.Errorf("path %v is outside the safe directory", absPath) + } + // Check if directory already exists - info, err := os.Stat(path) + info, err := os.Stat(absPath) if err == nil { // Path exists, verify it's a directory if !info.IsDir() { - return fmt.Errorf("cache path exists but is not a directory: %s", path) + return fmt.Errorf("path exists but is not a directory: %s", absPath) } return nil } // If error is not "not exist", return it if !os.IsNotExist(err) { - return fmt.Errorf("failed to stat cache directory: %w", err) + return fmt.Errorf("failed to stat directory: %w", err) } // Directory doesn't exist, create it with parent directories - if err := os.MkdirAll(path, 0750); err != nil { // #nosec G301 -- cache directory permissions set to 0750 for security - return fmt.Errorf("failed to create cache directory: %w", err) + if err := os.MkdirAll(absPath, 0750); err != nil { // #nosec G301 -- directory permissions set to 0750 for security + return fmt.Errorf("failed to create directory: %w", err) } return nil diff --git a/cos-csi-mounter/server/utils_test.go b/cos-csi-mounter/server/utils_test.go index f111b021..8fc5f86a 100644 --- a/cos-csi-mounter/server/utils_test.go +++ b/cos-csi-mounter/server/utils_test.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "errors" + "os" "path/filepath" "testing" @@ -148,3 +149,53 @@ func TestFileExists_AbsFails(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "failed to resolve absolute path") } + +func TestEnsureDir_OutsideSafeDirectory(t *testing.T) { + err := ensureDir("/etc/passwd", "/tmp") + assert.Error(t, err) + assert.Contains(t, err.Error(), "outside the safe directory") +} + +func TestEnsureDir_PathTraversal(t *testing.T) { + err := ensureDir("/tmp/../../etc", "/tmp") + assert.Error(t, err) + assert.Contains(t, err.Error(), "outside the safe directory") +} + +func TestEnsureDir_CreatesDirectory(t *testing.T) { + dir := filepath.Join(t.TempDir(), "subdir") + safeBase := filepath.Dir(dir) + err := ensureDir(dir, safeBase) + assert.NoError(t, err) + info, statErr := os.Stat(dir) + assert.NoError(t, statErr) + assert.True(t, info.IsDir()) +} + +func TestEnsureDir_ExistingDirectory(t *testing.T) { + dir := t.TempDir() + err := ensureDir(dir, filepath.Dir(dir)) + assert.NoError(t, err) +} + +func TestEnsureDir_PathIsFile(t *testing.T) { + dir := t.TempDir() + file := filepath.Join(dir, "file.txt") + assert.NoError(t, os.WriteFile(file, []byte("x"), 0600)) + err := ensureDir(file, dir) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not a directory") +} + +func TestEnsureDir_AbsFails(t *testing.T) { + originalAbs := absPathResolver + defer func() { absPathResolver = originalAbs }() + + absPathResolver = func(path string) (string, error) { + return "", errors.New("abs error") + } + + err := ensureDir("some-path", "/tmp") + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to resolve absolute path") +} From c804af948236af79705a4b7a5f762ed1e7607c85 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Tue, 10 Feb 2026 16:40:55 +0530 Subject: [PATCH 065/128] Amazon s3mounter code --- cos-csi-mounter/server/s3mounter.go | 73 ++++++++++ cos-csi-mounter/server/server.go | 2 +- cos-csi-mounter/server/utils.go | 10 ++ pkg/constants/constants.go | 2 + pkg/mounter/mounter.go | 2 + pkg/mounter/mounter_s3mounter.go | 207 ++++++++++++++++++++++++++++ 6 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 cos-csi-mounter/server/s3mounter.go create mode 100644 pkg/mounter/mounter_s3mounter.go diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go new file mode 100644 index 00000000..5553283b --- /dev/null +++ b/cos-csi-mounter/server/s3mounter.go @@ -0,0 +1,73 @@ +package main + +import ( + "encoding/json" + "fmt" + + "go.uber.org/zap" +) + +type s3MounterArgs struct { + ReadOnly string `json:"read-only,omitempty"` + AllowOther string `json:"allow-other,omitempty"` + UID string `json:"uid,omitempty"` + GID string `json:"gid,omitempty"` + UMask string `json:"umask,omitempty"` + LogLevel string `json:"log-level,omitempty"` +} + +func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string, error) { + // Marshal to JSON + raw, err := json.Marshal(args) + if err != nil { + return nil, err + } + + // Unmarshal into map[string]string + var m map[string]string + if err := json.Unmarshal(raw, &m); err != nil { + return nil, err + } + + // Convert to key=value slice + result := []string{"mount", bucket, targetPath} + for k, v := range m { + result = append(result, fmt.Sprintf("--%s=%v", k, v)) // --key=value + } + + return result, nil // [mount, bucket, path, --key1=value1, --key2=value2, ...] +} + +func (args s3MounterArgs) Validate(targetPath string) error { + if err := pathValidator(targetPath); err != nil { + return err + } + + // Check if value of allow-other parameter is boolean "true" or "false" + if args.AllowOther != "" { + if isBool := isBoolString(args.AllowOther); !isBool { + logger.Error("cannot convert value of allow-other into boolean", zap.Any("allow-other", args.AllowOther)) + return fmt.Errorf("cannot convert value of allow-other into boolean: %v", args.AllowOther) + } + } + + // // Check if rclone config file exists or not + // if exists, err := FileExists(args.ConfigPath); err != nil { + // logger.Error("error checking rclone config file existence") + // return fmt.Errorf("error checking rclone config file existence") + // } else if !exists { + // logger.Error("rclone config file not found") + // return fmt.Errorf("rclone config file not found") + // } + + + // Check if value of read-only parameter is boolean "true" or "false" + if args.ReadOnly != "" { + if isBool := isBoolString(args.ReadOnly); !isBool { + logger.Error("cannot convert value of read-only into boolean", zap.Any("read-only", args.ReadOnly)) + return fmt.Errorf("cannot convert value of read-only into boolean: %v", args.ReadOnly) + } + } + + return nil +} diff --git a/cos-csi-mounter/server/server.go b/cos-csi-mounter/server/server.go index 3c690dbe..05643607 100644 --- a/cos-csi-mounter/server/server.go +++ b/cos-csi-mounter/server/server.go @@ -169,7 +169,7 @@ func handleCosMount(mounter mounterUtils.MounterUtils, parser MounterArgsParser) logger.Info("New mount request with values:", zap.String("Bucket", request.Bucket), zap.String("Path", request.Path), zap.String("Mounter", request.Mounter), zap.Any("Args", request.Args)) - if request.Mounter != constants.S3FS && request.Mounter != constants.RClone { + if request.Mounter != constants.S3FS && request.Mounter != constants.RClone && request.Mounter != constants.S3MOUNTER { logger.Error("invalid mounter", zap.Any("mounter", request.Mounter)) c.JSON(http.StatusBadRequest, gin.H{"error": "invalid mounter"}) return diff --git a/cos-csi-mounter/server/utils.go b/cos-csi-mounter/server/utils.go index e2858b11..877dba35 100644 --- a/cos-csi-mounter/server/utils.go +++ b/cos-csi-mounter/server/utils.go @@ -85,6 +85,16 @@ func (req *MountRequest) ParseMounterArgs() ([]string, error) { return nil, fmt.Errorf("rclone args validation failed: %w", err) } return args.PopulateArgsSlice(req.Bucket, req.Path) + + case constants.S3MOUNTER: + var args s3MounterArgs + if err := strictDecodeForUnknownFields(req.Args, &args); err != nil { + return nil, fmt.Errorf("invalid rclone args decode error: %w", err) + } + if err := args.Validate(req.Path); err != nil { + return nil, fmt.Errorf("s3Mounter args validation failed: %w", err) + } + return args.PopulateArgsSlice(req.Bucket, req.Path) default: return nil, fmt.Errorf("unknown mounter: %s", req.Mounter) diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 054f035c..f458a2c5 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -16,6 +16,8 @@ const ( S3FS = "s3fs" RClone = "rclone" + S3MOUNTER = "s3mounter" + DefaultNamespace = "default" IAMEP = "https://private.iam.cloud.ibm.com/identity/token" diff --git a/pkg/mounter/mounter.go b/pkg/mounter/mounter.go index 47337fdc..ce15d2b9 100644 --- a/pkg/mounter/mounter.go +++ b/pkg/mounter/mounter.go @@ -104,6 +104,8 @@ func (s *CSIMounterFactory) NewMounter(params MounterParams) Mounter { Gid: params.Gid, ReadOnly: params.ReadOnly, }) + case constants.S3MOUNTER: + return NewMountpointS3Mounter(secretMap, mountFlags, mounterUtils) default: // default to s3fs return NewS3fsMounter(S3fsMounterParams{ diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go new file mode 100644 index 00000000..b48cce26 --- /dev/null +++ b/pkg/mounter/mounter_s3mounter.go @@ -0,0 +1,207 @@ +// pkg/mounter/mounter-mountpoint-s3.go +package mounter + +import ( + "encoding/json" + "fmt" + "strings" + "github.com/IBM/ibm-object-csi-driver/pkg/constants" + "github.com/IBM/ibm-object-csi-driver/pkg/mounter/utils" + "k8s.io/klog/v2" +) + +// type MountpointS3Mounter struct { +// Bucket string +// Endpoint string +// Region string +// Prefix string +// ReadOnly bool +// UID string +// GID string +// MounterUtils utils.MounterUtils +// } + +type MountpointS3Mounter struct { + BucketName string // from secret / SC + ObjPath string // optional prefix + Region string + Endpoint string + ReadOnly bool + UID string + GID string + MountOptions []string + + MounterUtils utils.MounterUtils +} +// const ( +// passFile = ".passwd-s3fs" // #nosec G101: not password +// maxRetries = 3 +// ) + +// var ( +// writePassWrap = writePass +// removeFile = removeS3FSCredFile +// ) + + +// func NewMountpointS3Mounter( +// secretMap map[string]string, +// mountFlags []string, +// mounterUtils utils.MounterUtils, +// ) Mounter { + +// return &MountpointS3Mounter{ +// Bucket: secretMap["bucketName"], +// Endpoint: secretMap["endpoint"], +// Region: secretMap["region"], +// Prefix: secretMap["prefix"], +// UID: secretMap["uid"], +// GID: secretMap["gid"], +// MounterUtils: mounterUtils, +// } +// } +// func (m *MountpointS3Mounter) Mount(source, target string) error { +// klog.Infof("MountpointS3 Mount target=%s", target) + +// args := map[string]string{ +// "endpoint": m.Endpoint, +// "region": m.Region, +// "uid": m.UID, +// "gid": m.GID, +// } + +// if m.Prefix != "" { +// args["prefix"] = m.Prefix +// } + +// payloadMap := map[string]interface{}{ +// "path": target, +// "bucket": m.Bucket, +// "mounter": constants.MountpointS3, +// "args": args, +// } + +// payload, _ := json.Marshal(payloadMap) + +// return mounterRequest( +// string(payload), +// "http://unix/api/cos/mount", +// ) +// } +// func (m *MountpointS3Mounter) Unmount(target string) error { +// klog.Infof("MountpointS3 Unmount target=%s", target) + +// payload := fmt.Sprintf(`{"path":"%s"}`, target) + +// return mounterRequest( +// payload, +// "http://unix/api/cos/unmount", +// ) +// } + +func NewMountpointS3Mounter( + secretMap map[string]string, + mountOptions []string, + mounterUtils utils.MounterUtils, +) Mounter { + + m := &MountpointS3Mounter{} + + if v, ok := secretMap["bucketName"]; ok { + m.BucketName = v + } + if v, ok := secretMap["objPath"]; ok { + m.ObjPath = v + } + if v, ok := secretMap["region"]; ok { + m.Region = v + } + if v, ok := secretMap["endpoint"]; ok { + m.Endpoint = v + } + if v, ok := secretMap["uid"]; ok { + m.UID = v + } + if v, ok := secretMap["gid"]; ok { + m.GID = v + } + if v, ok := secretMap["readOnly"]; ok && v == "true" { + m.ReadOnly = true + } + + m.MountOptions = mountOptions + m.MounterUtils = mounterUtils + + klog.Infof( + "newMountpointS3Mounter bucket=%s prefix=%s region=%s endpoint=%s readonly=%v", + m.BucketName, m.ObjPath, m.Region, m.Endpoint, m.ReadOnly, + ) + + return m +} +func (m *MountpointS3Mounter) Mount(source, target string) error { + klog.Info("-MountpointS3 Mount-") + klog.Infof("Mount target: %s", target) + + bucket := m.BucketName + if m.ObjPath != "" { + bucket = fmt.Sprintf("%s:%s", bucket, strings.TrimPrefix(m.ObjPath, "/")) + } + + // Arguments sent to mounter daemon + args := map[string]string{ + "region": m.Region, + } + + if m.Endpoint != "" { + args["endpoint-url"] = m.Endpoint + } + if m.ReadOnly { + args["read-only"] = "true" + } + if m.UID != "" { + args["uid"] = m.UID + } + if m.GID != "" { + args["gid"] = m.GID + } + + for _, opt := range m.MountOptions { + parts := strings.SplitN(opt, "=", 2) + if len(parts) == 2 { + args[parts[0]] = parts[1] + } else { + args[parts[0]] = "true" + } + } + + payload := map[string]interface{}{ + "path": target, + "bucket": bucket, + "mounter": constants.MountpointS3, + "args": args, + } + + data, err := json.Marshal(payload) + if err != nil { + return err + } + + klog.Infof("Worker Mounting Payload: %s", string(data)) + + return mounterRequest( + string(data), + "http://unix/api/cos/mount", + ) +} +func (m *MountpointS3Mounter) Unmount(target string) error { + klog.Info("-MountpointS3 Unmount-") + klog.Infof("Unmount target: %s", target) + + payload := fmt.Sprintf(`{"path":"%s"}`, target) + + return mounterRequest( + payload, + "http://unix/api/cos/unmount", + ) +} From e14a6eaddc779b404d27da13749af9a647333b3a Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 16 Feb 2026 00:47:06 +0530 Subject: [PATCH 066/128] Updated mounter-s3 code --- pkg/mounter/mounter_s3mounter.go | 554 +++++++++++++++++++++---------- 1 file changed, 385 insertions(+), 169 deletions(-) diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index b48cce26..d32a36ee 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -1,207 +1,423 @@ -// pkg/mounter/mounter-mountpoint-s3.go +// Package mounter package mounter import ( + "bufio" + "crypto/sha256" "encoding/json" "fmt" + "os" + "path" "strings" + "time" + "github.com/IBM/ibm-object-csi-driver/pkg/constants" "github.com/IBM/ibm-object-csi-driver/pkg/mounter/utils" "k8s.io/klog/v2" ) -// type MountpointS3Mounter struct { -// Bucket string -// Endpoint string -// Region string -// Prefix string -// ReadOnly bool -// UID string -// GID string -// MounterUtils utils.MounterUtils -// } - +// MountpointS3Mounter implements Mounter for AWS Mountpoint S3 type MountpointS3Mounter struct { - BucketName string // from secret / SC - ObjPath string // optional prefix - Region string - Endpoint string - ReadOnly bool - UID string - GID string - MountOptions []string - - MounterUtils utils.MounterUtils + BucketName string + ObjectPath string + EndPoint string + LocConstraint string + AuthType string + AccessKey string + SecretKey string + UID string + GID string + UMask string + LogLevel string + ReadOnly bool + MountOptions []string + MounterUtils utils.MounterUtils +} + +// s3MounterArgs holds the args passed to the worker for mount-s3 +type s3MounterArgs struct { + ReadOnly string `json:"read-only,omitempty"` + AllowOther string `json:"allow-other,omitempty"` + UID string `json:"uid,omitempty"` + GID string `json:"gid,omitempty"` + UMask string `json:"umask,omitempty"` + LogLevel string `json:"log-level,omitempty"` + // Config file path for AWS credentials + AwsConfigDir string `json:"aws-config-dir,omitempty"` + EndpointURL string `json:"endpoint-url,omitempty"` + Region string `json:"region,omitempty"` } -// const ( -// passFile = ".passwd-s3fs" // #nosec G101: not password -// maxRetries = 3 -// ) - -// var ( -// writePassWrap = writePass -// removeFile = removeS3FSCredFile -// ) - - -// func NewMountpointS3Mounter( -// secretMap map[string]string, -// mountFlags []string, -// mounterUtils utils.MounterUtils, -// ) Mounter { - -// return &MountpointS3Mounter{ -// Bucket: secretMap["bucketName"], -// Endpoint: secretMap["endpoint"], -// Region: secretMap["region"], -// Prefix: secretMap["prefix"], -// UID: secretMap["uid"], -// GID: secretMap["gid"], -// MounterUtils: mounterUtils, -// } -// } -// func (m *MountpointS3Mounter) Mount(source, target string) error { -// klog.Infof("MountpointS3 Mount target=%s", target) - -// args := map[string]string{ -// "endpoint": m.Endpoint, -// "region": m.Region, -// "uid": m.UID, -// "gid": m.GID, -// } - -// if m.Prefix != "" { -// args["prefix"] = m.Prefix -// } - -// payloadMap := map[string]interface{}{ -// "path": target, -// "bucket": m.Bucket, -// "mounter": constants.MountpointS3, -// "args": args, -// } - -// payload, _ := json.Marshal(payloadMap) - -// return mounterRequest( -// string(payload), -// "http://unix/api/cos/mount", -// ) -// } -// func (m *MountpointS3Mounter) Unmount(target string) error { -// klog.Infof("MountpointS3 Unmount target=%s", target) - -// payload := fmt.Sprintf(`{"path":"%s"}`, target) - -// return mounterRequest( -// payload, -// "http://unix/api/cos/unmount", -// ) -// } - -func NewMountpointS3Mounter( - secretMap map[string]string, - mountOptions []string, - mounterUtils utils.MounterUtils, -) Mounter { - - m := &MountpointS3Mounter{} - - if v, ok := secretMap["bucketName"]; ok { - m.BucketName = v - } - if v, ok := secretMap["objPath"]; ok { - m.ObjPath = v - } - if v, ok := secretMap["region"]; ok { - m.Region = v - } - if v, ok := secretMap["endpoint"]; ok { - m.Endpoint = v - } - if v, ok := secretMap["uid"]; ok { - m.UID = v - } - if v, ok := secretMap["gid"]; ok { - m.GID = v - } - if v, ok := secretMap["readOnly"]; ok && v == "true" { - m.ReadOnly = true - } - - m.MountOptions = mountOptions - m.MounterUtils = mounterUtils - - klog.Infof( - "newMountpointS3Mounter bucket=%s prefix=%s region=%s endpoint=%s readonly=%v", - m.BucketName, m.ObjPath, m.Region, m.Endpoint, m.ReadOnly, + +const ( + s3MountCredentialsFile = "credentials" + s3MountConfigFile = "config" + s3MountAwsProfile = "default" +) + +var ( + removeS3ConfigFile = removeS3MountConfigFile +) + +// NewMountpointS3Mounter creates a new MountpointS3Mounter from secretMap +func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounterUtils utils.MounterUtils) Mounter { + klog.Info("-newMountpointS3Mounter-") + + var ( + val string + check bool + mounter = &MountpointS3Mounter{} ) - return m + if val, check = secretMap["cosEndpoint"]; check { + mounter.EndPoint = val + } + if val, check = secretMap["locationConstraint"]; check { + mounter.LocConstraint = val + } + if val, check = secretMap["bucketName"]; check { + mounter.BucketName = val + } + if val, check = secretMap["objectPath"]; check { + mounter.ObjectPath = val + } + if val, check = secretMap["accessKey"]; check { + mounter.AccessKey = val + } + if val, check = secretMap["secretKey"]; check { + mounter.SecretKey = val + } + if val, check = secretMap["uid"]; check { + mounter.UID = val + } + // If gid set but uid not, use gid for uid (same as rclone behavior) + if secretMap["gid"] != "" && secretMap["uid"] == "" { + mounter.UID = secretMap["gid"] + } else if secretMap["uid"] != "" { + mounter.UID = secretMap["uid"] + } + if val, check = secretMap["gid"]; check { + mounter.GID = val + } + if val, check = secretMap["umask"]; check { + mounter.UMask = val + } + if val, check = secretMap["logLevel"]; check { + mounter.LogLevel = val + } + if val, check = secretMap["readOnly"]; check { + mounter.ReadOnly = val == "true" + } + + mounter.AuthType = "hmac" + + klog.Infof("newMountpointS3Mounter args:\n\tbucketName: [%s]\n\tobjectPath: [%s]\n\tendPoint: [%s]\n\tlocationConstraint: [%s]\n\tauthType: [%s]", + mounter.BucketName, mounter.ObjectPath, mounter.EndPoint, mounter.LocConstraint, mounter.AuthType) + + mounter.MountOptions = mountOptions + mounter.MounterUtils = mounterUtils + + return mounter } -func (m *MountpointS3Mounter) Mount(source, target string) error { - klog.Info("-MountpointS3 Mount-") - klog.Infof("Mount target: %s", target) - bucket := m.BucketName - if m.ObjPath != "" { - bucket = fmt.Sprintf("%s:%s", bucket, strings.TrimPrefix(m.ObjPath, "/")) +// Mount mounts the S3 bucket using mountpoint-s3 +func (s3 *MountpointS3Mounter) Mount(source string, target string) error { + klog.Info("-MountpointS3Mounter Mount-") + klog.Infof("Mount args:\n\tsource: <%s>\n\ttarget: <%s>", source, target) + + // Determine config path based on mode + var configPath string + if mountWorker { + configPath = constants.MounterConfigPathOnHost + } else { + configPath = constants.MounterConfigPathOnPodS3Mount } - // Arguments sent to mounter daemon - args := map[string]string{ - "region": m.Region, + // Create unique config dir per volume + configPathWithVolID := path.Join(configPath, fmt.Sprintf("%x", sha256.Sum256([]byte(target)))) + + configCreated := false + + // Cleanup on failure + defer func() { + if err != nil && configCreated { + klog.Warningf("Mount failed, cleaning up config dir: %s", configPathWithVolID) + removeS3ConfigFile(configPath, target) + } + }() + + // Write AWS credentials config file + if err := createS3MountConfig(configPathWithVolID, s3); err != nil { + klog.Errorf("MountpointS3Mounter Mount: Cannot create config file: %v", err) + return err } + + configCreated = true - if m.Endpoint != "" { - args["endpoint-url"] = m.Endpoint + // Build bucket path with optional object path prefix + bucketName := s3.BucketName + if s3.ObjectPath != "" { + trimmedPath := strings.TrimPrefix(s3.ObjectPath, "/") + bucketName = fmt.Sprintf("%s/%s", s3.BucketName, trimmedPath) } - if m.ReadOnly { - args["read-only"] = "true" + + args, wnOp := s3.formulateMountOptions(bucketName, target, configPathWithVolID) + + if mountWorker { + klog.Info("Mount on Worker started...") + + jsonData, err := json.Marshal(wnOp) + if err != nil { + klog.Errorf("Error marshalling mount args: %v", err) + return err + } + + payload := fmt.Sprintf(`{"path":"%s","bucket":"%s","mounter":"%s","args":%s}`, + target, bucketName, constants.MountpointS3, jsonData) + + err = mounterRequest(payload, "http://unix/api/cos/mount") + if err != nil { + klog.Errorf("failed to mount on worker: %v", err) + return err + } + return nil } - if m.UID != "" { - args["uid"] = m.UID + + klog.Info("NodeServer Mounting...") + return s3.MounterUtils.FuseMount(target, constants.MountpointS3BinaryPath, args) +} + +// Unmount unmounts the S3 bucket +func (s3 *MountpointS3Mounter) Unmount(target string) error { + klog.Info("-MountpointS3Mounter Unmount-") + + if mountWorker { + klog.Info("Unmount on Worker started...") + + payload := fmt.Sprintf(`{"path":"%s"}`, target) + + err := mounterRequest(payload, "http://unix/api/cos/unmount") + if err != nil { + klog.Errorf("failed to unmount on worker: %v", err) + return err + } + + removeS3ConfigFile(constants.MounterConfigPathOnHost, target) + return nil } - if m.GID != "" { - args["gid"] = m.GID + + klog.Info("NodeServer Unmounting...") + err := s3.MounterUtils.FuseUnmount(target) + if err != nil { + return err } - for _, opt := range m.MountOptions { - parts := strings.SplitN(opt, "=", 2) - if len(parts) == 2 { - args[parts[0]] = parts[1] - } else { - args[parts[0]] = "true" - } + removeS3ConfigFile(constants.MounterConfigPathOnPodS3Mount, target) + return nil +} + +// createS3MountConfig creates the AWS credentials and config files +// that mount-s3 reads via --aws-config-dir flag. +// Directory structure: +// +// / +// credentials <- AWS credentials (access/secret key) +// config <- AWS config (region, endpoint) +func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) error { + // Create the config directory + if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 + klog.Errorf("MountpointS3Mounter: Cannot create config dir %s: %v", configPathWithVolID, err) + return err } - payload := map[string]interface{}{ - "path": target, - "bucket": bucket, - "mounter": constants.MountpointS3, - "args": args, + // --- Write credentials file --- + // Format matches AWS credentials file: + // [default] + // aws_access_key_id = + // aws_secret_access_key = + credFile := path.Join(configPathWithVolID, s3MountCredentialsFile) + credParams := []string{ + "[" + s3MountAwsProfile + "]", + "aws_access_key_id = " + s3.AccessKey, + "aws_secret_access_key = " + s3.SecretKey, + } + if err := writeConfigFile(credFile, credParams); err != nil { + klog.Errorf("MountpointS3Mounter: Cannot write credentials file: %v", err) + return err } - data, err := json.Marshal(payload) - if err != nil { + // --- Write config file --- + // Format matches AWS config file: + // [default] + // region = + // endpoint_url = (for IBM COS compatibility) + region := convertLocationConstraintToRegion(s3.LocConstraint) + configParams := []string{ + "[" + s3MountAwsProfile + "]", + "region = " + region, + } + if s3.EndPoint != "" { + configParams = append(configParams, "endpoint_url = "+s3.EndPoint) + } + cfgFile := path.Join(configPathWithVolID, s3MountConfigFile) + if err := writeConfigFile(cfgFile, configParams); err != nil { + klog.Errorf("MountpointS3Mounter: Cannot write config file: %v", err) return err } - klog.Infof("Worker Mounting Payload: %s", string(data)) + klog.Infof("MountpointS3Mounter: Created config files at %s", configPathWithVolID) + return nil +} - return mounterRequest( - string(data), - "http://unix/api/cos/mount", - ) +// writeConfigFile writes lines to a file with 0600 permissions +func writeConfigFile(filePath string, lines []string) error { + f, err := CreateFile(filePath) // #nosec G304 + if err != nil { + return fmt.Errorf("cannot create file %s: %w", filePath, err) + } + defer func() { + if cerr := f.Close(); cerr != nil { + klog.Errorf("writeConfigFile: cannot close file %s: %v", filePath, cerr) + } + }() + + if err := Chmod(filePath, 0600); err != nil { // #nosec G302 + return fmt.Errorf("cannot chmod file %s: %w", filePath, err) + } + + w := bufio.NewWriter(f) + for _, line := range lines { + if _, err := w.WriteString(line + "\n"); err != nil { + return fmt.Errorf("cannot write to file %s: %w", filePath, err) + } + } + return w.Flush() } -func (m *MountpointS3Mounter) Unmount(target string) error { - klog.Info("-MountpointS3 Unmount-") - klog.Infof("Unmount target: %s", target) - payload := fmt.Sprintf(`{"path":"%s"}`, target) +// formulateMountOptions builds CLI args for mount-s3 and the worker payload +func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathWithVolID string) (nodeServerOp []string, workerNodeOp s3MounterArgs) { + // mount-s3 syntax: mount-s3 [flags] + nodeServerOp = []string{ + bucket, + target, + "--allow-other", + // Point mount-s3 to our config directory for credentials & config + "--aws-config-dir=" + configPathWithVolID, + } - return mounterRequest( - payload, - "http://unix/api/cos/unmount", - ) + workerNodeOp = s3MounterArgs{ + AllowOther: "true", + AwsConfigDir: configPathWithVolID, + } + + // Endpoint (IBM COS endpoint) + if s3.EndPoint != "" { + nodeServerOp = append(nodeServerOp, "--endpoint-url="+s3.EndPoint) + workerNodeOp.EndpointURL = s3.EndPoint + } + + // Region from location constraint + // Not rquired because already passed in config file, There is no issue in passing it with CLI but may create an issue if the regions are differnet.. + // if s3.LocConstraint != "" { + // region := convertLocationConstraintToRegion(s3.LocConstraint) + // nodeServerOp = append(nodeServerOp, "--region="+region) + // workerNodeOp.Region = region + // } + + // UID + if s3.UID != "" { + nodeServerOp = append(nodeServerOp, "--uid="+s3.UID) + workerNodeOp.UID = s3.UID + } + + // GID + if s3.GID != "" { + nodeServerOp = append(nodeServerOp, "--gid="+s3.GID) + workerNodeOp.GID = s3.GID + } + + // UMask + if s3.UMask != "" { + nodeServerOp = append(nodeServerOp, "--umask="+s3.UMask) + workerNodeOp.UMask = s3.UMask + } + + // Log level + logLevel := s3.LogLevel + if logLevel == "" { + logLevel = "warn" + } + nodeServerOp = append(nodeServerOp, "--log-level="+logLevel) + workerNodeOp.LogLevel = logLevel + + // Read-only + if s3.ReadOnly { + nodeServerOp = append(nodeServerOp, "--read-only") + workerNodeOp.ReadOnly = "true" + } + + // Extra mount options from StorageClass + nodeServerOp = append(nodeServerOp, s3.MountOptions...) + + klog.Infof("MountpointS3 nodeServerOp: %v", nodeServerOp) + klog.Infof("MountpointS3 workerNodeOp: %+v", workerNodeOp) + + return nodeServerOp, workerNodeOp +} + +// removeS3MountConfigFile removes the config directory for the volume (same pattern as rclone) +func removeS3MountConfigFile(configPath, target string) { + configPathWithVolID := path.Join(configPath, fmt.Sprintf("%x", sha256.Sum256([]byte(target)))) + + for retry := 1; retry <= maxRetries; retry++ { + _, err := Stat(configPathWithVolID) + if err != nil { + if os.IsNotExist(err) { + klog.Infof("removeS3MountConfigFile: Config dir does not exist: %s", configPathWithVolID) + return + } + klog.Errorf("removeS3MountConfigFile: Attempt %d - Failed to stat path %s: %v", retry, configPathWithVolID, err) + time.Sleep(constants.Interval) + continue + } + if err = RemoveAll(configPathWithVolID); err != nil { + klog.Errorf("removeS3MountConfigFile: Attempt %d - Failed to remove config dir %s: %v", retry, configPathWithVolID, err) + time.Sleep(constants.Interval) + continue + } + klog.Infof("removeS3MountConfigFile: Successfully removed config dir: %s", configPathWithVolID) + return + } + klog.Errorf("removeS3MountConfigFile: Failed to remove config dir after %d attempts", maxRetries) } + +// convertLocationConstraintToRegion converts IBM COS location constraints +// to AWS-style region strings that mountpoint-s3 understands +// e.g. "us-east-standard" -> "us-east-1" +func convertLocationConstraintToRegion(locationConstraint string) string { + tiers := []string{"-standard", "-smart", "-cold", "-flex", "-vault"} + region := locationConstraint + for _, tier := range tiers { + region = strings.TrimSuffix(region, tier) + } + + regionMap := map[string]string{ + "us-east": "us-east-1", + "us-south": "us-south", + "eu-de": "eu-de", + "eu-gb": "eu-gb", + "ap-north": "ap-north", + "ap-south": "ap-south", + "jp-tok": "jp-tok", + "au-syd": "au-syd", + "ca-tor": "ca-tor", + "br-sao": "br-sao", + "global": "global", + } + + if mapped, ok := regionMap[region]; ok { + return mapped + } + + klog.Warningf("No region mapping found for location constraint: %s, using as-is: %s", locationConstraint, region) + return region +} \ No newline at end of file From 61ef029efbcd83e6b27972cf4be03322ad2e97da Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Tue, 17 Feb 2026 11:38:05 +0530 Subject: [PATCH 067/128] Added constants and update release version & branch --- .github/workflows/release.yml | 4 ++-- pkg/constants/constants.go | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 57d7d395..ad271cf1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,10 +3,10 @@ name: CI & Release on: push: branches: - - main + - aws-s3mounter pull_request: branches: - - main + - aws-s3mounter concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index f458a2c5..64bca673 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -36,6 +36,11 @@ const ( COSCSIMounterSocketPathEnv = "COS_CSI_MOUNTER_SOCKET" MounterConfigPathOnHost = "/var/lib/coscsi-config" MounterConfigPathOnPodS3fs = "/var/lib/ibmc-s3fs" + MountpointS3 = "mount-s3" + MountpointS3BinaryPath = "/usr/bin/mount-s3" + + // Config paths for mountpoint-s3 (same pattern as rclone) + MounterConfigPathOnPodS3Mount = "/var/lib/coscsi-config/s3mount" MounterConfigPathOnPodRclone = "/root/.config/rclone" // Interval to wait till next loop Interval = 500 * time.Millisecond From 8be9dc60ed958adfe4f897eeb0ed8a1b526e5fdf Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Thu, 19 Feb 2026 01:10:02 +0530 Subject: [PATCH 068/128] Commented cleanup function publish image --- pkg/mounter/mounter_s3mounter.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index d32a36ee..8db1e117 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -135,23 +135,23 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { // Create unique config dir per volume configPathWithVolID := path.Join(configPath, fmt.Sprintf("%x", sha256.Sum256([]byte(target)))) - configCreated := false + // configCreated := false - // Cleanup on failure - defer func() { - if err != nil && configCreated { - klog.Warningf("Mount failed, cleaning up config dir: %s", configPathWithVolID) - removeS3ConfigFile(configPath, target) - } - }() + // // Cleanup on failure + // defer func() { + // if err != nil && configCreated { + // klog.Warningf("Mount failed, cleaning up config dir: %s", configPathWithVolID) + // removeS3ConfigFile(configPath, target) + // } + // }() // Write AWS credentials config file if err := createS3MountConfig(configPathWithVolID, s3); err != nil { klog.Errorf("MountpointS3Mounter Mount: Cannot create config file: %v", err) return err } - - configCreated = true + + // configCreated = true // Build bucket path with optional object path prefix bucketName := s3.BucketName From d75a4bb8b4318bce23d58d3cf0e457ff57a639ed Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Thu, 19 Feb 2026 12:02:56 +0530 Subject: [PATCH 069/128] publish v0.12.4 --- .github/workflows/release.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ad271cf1..1829dc07 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -143,8 +143,7 @@ jobs: name: v1.0.7 body: | ## πŸš€ What’s New - - Add support for AddMountParam to pass additional s3fs mount options - - AddMountParam allows passing s3fs mount options as a comma-separated string + - Testing aws-s3Mounter image prerelease: ${{ env.IS_LATEST_RELEASE != 'true' }} - name: Perform CodeQL Analysis From 70bfa6807564239cf4fd962b98c976b921e2d480 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 23 Feb 2026 11:47:28 +0530 Subject: [PATCH 070/128] publish 0.12.6 --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1829dc07..8e94097b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -144,6 +144,7 @@ jobs: body: | ## πŸš€ What’s New - Testing aws-s3Mounter image + - Publishing new test version prerelease: ${{ env.IS_LATEST_RELEASE != 'true' }} - name: Perform CodeQL Analysis From e2b094771b0e1708406236de6a838b33b2754643 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Mon, 23 Feb 2026 14:54:56 +0530 Subject: [PATCH 071/128] publish v0.12.7 --- pkg/mounter/mounter_s3mounter.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 8db1e117..71e81272 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -124,7 +124,7 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { klog.Info("-MountpointS3Mounter Mount-") klog.Infof("Mount args:\n\tsource: <%s>\n\ttarget: <%s>", source, target) - // Determine config path based on mode + // Determine config path based on mode var configPath string if mountWorker { configPath = constants.MounterConfigPathOnHost @@ -132,18 +132,18 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { configPath = constants.MounterConfigPathOnPodS3Mount } - // Create unique config dir per volume + // Create unique config dir per volume configPathWithVolID := path.Join(configPath, fmt.Sprintf("%x", sha256.Sum256([]byte(target)))) - + // configCreated := false - // // Cleanup on failure - // defer func() { - // if err != nil && configCreated { - // klog.Warningf("Mount failed, cleaning up config dir: %s", configPathWithVolID) - // removeS3ConfigFile(configPath, target) - // } - // }() + // // Cleanup on failure + // defer func() { + // if err != nil && configCreated { + // klog.Warningf("Mount failed, cleaning up config dir: %s", configPathWithVolID) + // removeS3ConfigFile(configPath, target) + // } + // }() // Write AWS credentials config file if err := createS3MountConfig(configPathWithVolID, s3); err != nil { @@ -224,7 +224,7 @@ func (s3 *MountpointS3Mounter) Unmount(target string) error { // config <- AWS config (region, endpoint) func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) error { // Create the config directory - if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 + if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 -- directory is created under a controlled path using SHA256 hash, not user input klog.Errorf("MountpointS3Mounter: Cannot create config dir %s: %v", configPathWithVolID, err) return err } @@ -270,7 +270,7 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er // writeConfigFile writes lines to a file with 0600 permissions func writeConfigFile(filePath string, lines []string) error { - f, err := CreateFile(filePath) // #nosec G304 + f, err := CreateFile(filePath) // #nosec G304 -- filePath is constructed internally from a SHA256 hash and constant config dir, not from user input if err != nil { return fmt.Errorf("cannot create file %s: %w", filePath, err) } @@ -280,7 +280,7 @@ func writeConfigFile(filePath string, lines []string) error { } }() - if err := Chmod(filePath, 0600); err != nil { // #nosec G302 + if err := Chmod(filePath, 0600); err != nil { // #nosec G302 -- restrictive permissions (0600) are intentionally set for credential files return fmt.Errorf("cannot chmod file %s: %w", filePath, err) } @@ -420,4 +420,4 @@ func convertLocationConstraintToRegion(locationConstraint string) string { klog.Warningf("No region mapping found for location constraint: %s, using as-is: %s", locationConstraint, region) return region -} \ No newline at end of file +} From 0f722f302768e85ad3e7cae1257e9a7f080c2991 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Tue, 3 Mar 2026 17:22:06 +0530 Subject: [PATCH 072/128] Updated name to mount-s3 --- cos-csi-mounter/server/server.go | 2 +- cos-csi-mounter/server/utils.go | 2 +- pkg/constants/constants.go | 3 +-- pkg/mounter/mounter_s3mounter.go | 43 +------------------------------- 4 files changed, 4 insertions(+), 46 deletions(-) diff --git a/cos-csi-mounter/server/server.go b/cos-csi-mounter/server/server.go index 05643607..467eabe3 100644 --- a/cos-csi-mounter/server/server.go +++ b/cos-csi-mounter/server/server.go @@ -169,7 +169,7 @@ func handleCosMount(mounter mounterUtils.MounterUtils, parser MounterArgsParser) logger.Info("New mount request with values:", zap.String("Bucket", request.Bucket), zap.String("Path", request.Path), zap.String("Mounter", request.Mounter), zap.Any("Args", request.Args)) - if request.Mounter != constants.S3FS && request.Mounter != constants.RClone && request.Mounter != constants.S3MOUNTER { + if request.Mounter != constants.S3FS && request.Mounter != constants.RClone && request.Mounter != constants.AMAZONS3MOUNTER { logger.Error("invalid mounter", zap.Any("mounter", request.Mounter)) c.JSON(http.StatusBadRequest, gin.H{"error": "invalid mounter"}) return diff --git a/cos-csi-mounter/server/utils.go b/cos-csi-mounter/server/utils.go index 877dba35..3807a1e7 100644 --- a/cos-csi-mounter/server/utils.go +++ b/cos-csi-mounter/server/utils.go @@ -86,7 +86,7 @@ func (req *MountRequest) ParseMounterArgs() ([]string, error) { } return args.PopulateArgsSlice(req.Bucket, req.Path) - case constants.S3MOUNTER: + case constants.AMAZONS3MOUNTER: var args s3MounterArgs if err := strictDecodeForUnknownFields(req.Args, &args); err != nil { return nil, fmt.Errorf("invalid rclone args decode error: %w", err) diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 64bca673..7f3234dd 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -16,7 +16,7 @@ const ( S3FS = "s3fs" RClone = "rclone" - S3MOUNTER = "s3mounter" + AMAZONS3MOUNTER = "mount-s3" DefaultNamespace = "default" @@ -36,7 +36,6 @@ const ( COSCSIMounterSocketPathEnv = "COS_CSI_MOUNTER_SOCKET" MounterConfigPathOnHost = "/var/lib/coscsi-config" MounterConfigPathOnPodS3fs = "/var/lib/ibmc-s3fs" - MountpointS3 = "mount-s3" MountpointS3BinaryPath = "/usr/bin/mount-s3" // Config paths for mountpoint-s3 (same pattern as rclone) diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 71e81272..259d84df 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -250,7 +250,7 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er // [default] // region = // endpoint_url = (for IBM COS compatibility) - region := convertLocationConstraintToRegion(s3.LocConstraint) + region := s3.LocConstraint configParams := []string{ "[" + s3MountAwsProfile + "]", "region = " + region, @@ -314,15 +314,6 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW nodeServerOp = append(nodeServerOp, "--endpoint-url="+s3.EndPoint) workerNodeOp.EndpointURL = s3.EndPoint } - - // Region from location constraint - // Not rquired because already passed in config file, There is no issue in passing it with CLI but may create an issue if the regions are differnet.. - // if s3.LocConstraint != "" { - // region := convertLocationConstraintToRegion(s3.LocConstraint) - // nodeServerOp = append(nodeServerOp, "--region="+region) - // workerNodeOp.Region = region - // } - // UID if s3.UID != "" { nodeServerOp = append(nodeServerOp, "--uid="+s3.UID) @@ -389,35 +380,3 @@ func removeS3MountConfigFile(configPath, target string) { } klog.Errorf("removeS3MountConfigFile: Failed to remove config dir after %d attempts", maxRetries) } - -// convertLocationConstraintToRegion converts IBM COS location constraints -// to AWS-style region strings that mountpoint-s3 understands -// e.g. "us-east-standard" -> "us-east-1" -func convertLocationConstraintToRegion(locationConstraint string) string { - tiers := []string{"-standard", "-smart", "-cold", "-flex", "-vault"} - region := locationConstraint - for _, tier := range tiers { - region = strings.TrimSuffix(region, tier) - } - - regionMap := map[string]string{ - "us-east": "us-east-1", - "us-south": "us-south", - "eu-de": "eu-de", - "eu-gb": "eu-gb", - "ap-north": "ap-north", - "ap-south": "ap-south", - "jp-tok": "jp-tok", - "au-syd": "au-syd", - "ca-tor": "ca-tor", - "br-sao": "br-sao", - "global": "global", - } - - if mapped, ok := regionMap[region]; ok { - return mapped - } - - klog.Warningf("No region mapping found for location constraint: %s, using as-is: %s", locationConstraint, region) - return region -} From de3312392d730bb3294a4fecf05f84586e6a4e02 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Tue, 3 Mar 2026 23:30:26 +0530 Subject: [PATCH 073/128] publish 0.12.10 version --- pkg/mounter/mounter_s3mounter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 259d84df..bf204225 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -172,7 +172,7 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { } payload := fmt.Sprintf(`{"path":"%s","bucket":"%s","mounter":"%s","args":%s}`, - target, bucketName, constants.MountpointS3, jsonData) + target, bucketName, constants.AMAZONS3MOUNTER, jsonData) err = mounterRequest(payload, "http://unix/api/cos/mount") if err != nil { From 4f5a910ceb66455eed44337158a04331b7105200 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Thu, 5 Mar 2026 09:29:27 +0530 Subject: [PATCH 074/128] Fix mount-s3 worker args in cos-csi-mounter --- cos-csi-mounter/server/s3mounter.go | 37 ++++++++++++++++++++++------- cos-csi-mounter/server/utils.go | 4 ++-- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go index 5553283b..cb0e309c 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/s3mounter.go @@ -8,12 +8,15 @@ import ( ) type s3MounterArgs struct { - ReadOnly string `json:"read-only,omitempty"` - AllowOther string `json:"allow-other,omitempty"` - UID string `json:"uid,omitempty"` - GID string `json:"gid,omitempty"` - UMask string `json:"umask,omitempty"` - LogLevel string `json:"log-level,omitempty"` + ReadOnly string `json:"read-only,omitempty"` + AllowOther string `json:"allow-other,omitempty"` + UID string `json:"uid,omitempty"` + GID string `json:"gid,omitempty"` + UMask string `json:"umask,omitempty"` + LogLevel string `json:"log-level,omitempty"` + AwsConfigDir string `json:"aws-config-dir,omitempty"` + EndpointURL string `json:"endpoint-url,omitempty"` + Region string `json:"region,omitempty"` } func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string, error) { @@ -30,7 +33,7 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string } // Convert to key=value slice - result := []string{"mount", bucket, targetPath} + result := []string{bucket, targetPath} for k, v := range m { result = append(result, fmt.Sprintf("--%s=%v", k, v)) // --key=value } @@ -51,6 +54,25 @@ func (args s3MounterArgs) Validate(targetPath string) error { } } + if args.AwsConfigDir == "" { + logger.Error("aws-config-dir is required for mount-s3") + return fmt.Errorf("aws-config-dir is required for mount-s3") + } + + if args.AllowOther != "" { + if isBool := isBoolString(args.AllowOther); !isBool { + logger.Error("cannot convert value of allow-other into boolean", zap.Any("allow-other", args.AllowOther)) + return fmt.Errorf("cannot convert value of allow-other into boolean: %v", args.AllowOther) + } + } + + if args.ReadOnly != "" { + if isBool := isBoolString(args.ReadOnly); !isBool { + logger.Error("cannot convert value of read-only into boolean", zap.Any("read-only", args.ReadOnly)) + return fmt.Errorf("cannot convert value of read-only into boolean: %v", args.ReadOnly) + } + } + // // Check if rclone config file exists or not // if exists, err := FileExists(args.ConfigPath); err != nil { // logger.Error("error checking rclone config file existence") @@ -60,7 +82,6 @@ func (args s3MounterArgs) Validate(targetPath string) error { // return fmt.Errorf("rclone config file not found") // } - // Check if value of read-only parameter is boolean "true" or "false" if args.ReadOnly != "" { if isBool := isBoolString(args.ReadOnly); !isBool { diff --git a/cos-csi-mounter/server/utils.go b/cos-csi-mounter/server/utils.go index 3807a1e7..1c63f5fa 100644 --- a/cos-csi-mounter/server/utils.go +++ b/cos-csi-mounter/server/utils.go @@ -85,11 +85,11 @@ func (req *MountRequest) ParseMounterArgs() ([]string, error) { return nil, fmt.Errorf("rclone args validation failed: %w", err) } return args.PopulateArgsSlice(req.Bucket, req.Path) - + case constants.AMAZONS3MOUNTER: var args s3MounterArgs if err := strictDecodeForUnknownFields(req.Args, &args); err != nil { - return nil, fmt.Errorf("invalid rclone args decode error: %w", err) + return nil, fmt.Errorf("invalid mount-s3 args decode error: %w", err) // ← fix: was saying rclone } if err := args.Validate(req.Path); err != nil { return nil, fmt.Errorf("s3Mounter args validation failed: %w", err) From 5279b7576ceb72c9223936dabdc61b251b91c05d Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Thu, 5 Mar 2026 09:30:35 +0530 Subject: [PATCH 075/128] Fix mount-s3 worker args in cos-csi-mounter --- cos-csi-mounter/server/s3mounter.go | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go index cb0e309c..199770ce 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/s3mounter.go @@ -59,29 +59,6 @@ func (args s3MounterArgs) Validate(targetPath string) error { return fmt.Errorf("aws-config-dir is required for mount-s3") } - if args.AllowOther != "" { - if isBool := isBoolString(args.AllowOther); !isBool { - logger.Error("cannot convert value of allow-other into boolean", zap.Any("allow-other", args.AllowOther)) - return fmt.Errorf("cannot convert value of allow-other into boolean: %v", args.AllowOther) - } - } - - if args.ReadOnly != "" { - if isBool := isBoolString(args.ReadOnly); !isBool { - logger.Error("cannot convert value of read-only into boolean", zap.Any("read-only", args.ReadOnly)) - return fmt.Errorf("cannot convert value of read-only into boolean: %v", args.ReadOnly) - } - } - - // // Check if rclone config file exists or not - // if exists, err := FileExists(args.ConfigPath); err != nil { - // logger.Error("error checking rclone config file existence") - // return fmt.Errorf("error checking rclone config file existence") - // } else if !exists { - // logger.Error("rclone config file not found") - // return fmt.Errorf("rclone config file not found") - // } - // Check if value of read-only parameter is boolean "true" or "false" if args.ReadOnly != "" { if isBool := isBoolString(args.ReadOnly); !isBool { From 1afb1373a3f6f62980429e2bf0a7d8c4e635b3f2 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Thu, 5 Mar 2026 10:18:07 +0530 Subject: [PATCH 076/128] Fix mount-s3 worker args in cos-csi-mounter --- cos-csi-mounter/server/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cos-csi-mounter/server/utils.go b/cos-csi-mounter/server/utils.go index 1c63f5fa..58c41f28 100644 --- a/cos-csi-mounter/server/utils.go +++ b/cos-csi-mounter/server/utils.go @@ -89,7 +89,7 @@ func (req *MountRequest) ParseMounterArgs() ([]string, error) { case constants.AMAZONS3MOUNTER: var args s3MounterArgs if err := strictDecodeForUnknownFields(req.Args, &args); err != nil { - return nil, fmt.Errorf("invalid mount-s3 args decode error: %w", err) // ← fix: was saying rclone + return nil, fmt.Errorf("invalid mount-s3 args decode error: %w", err) } if err := args.Validate(req.Path); err != nil { return nil, fmt.Errorf("s3Mounter args validation failed: %w", err) From b7f76f3b0e91a7c18741275c3584194608bfd196 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Thu, 5 Mar 2026 10:21:13 +0530 Subject: [PATCH 077/128] publish 0.12.11 version --- pkg/mounter/mounter_s3mounter.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index bf204225..68811912 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -45,7 +45,6 @@ type s3MounterArgs struct { // Config file path for AWS credentials AwsConfigDir string `json:"aws-config-dir,omitempty"` EndpointURL string `json:"endpoint-url,omitempty"` - Region string `json:"region,omitempty"` } const ( @@ -229,7 +228,6 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er return err } - // --- Write credentials file --- // Format matches AWS credentials file: // [default] // aws_access_key_id = From 8d55d7fc59222684cbc39e398c3ae860a1c87a88 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Thu, 5 Mar 2026 10:28:45 +0530 Subject: [PATCH 078/128] publish 0.12.11 version --- pkg/mounter/mounter_s3mounter.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 68811912..7d099066 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -228,6 +228,7 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er return err } + // --- Write credentials file --- // Format matches AWS credentials file: // [default] // aws_access_key_id = From d6901d015b455a5a874547e88366ba24169f4ed1 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sat, 7 Mar 2026 00:37:43 +0530 Subject: [PATCH 079/128] publish 0.12.11 version --- cos-csi-mounter/server/s3mounter.go | 117 +++++++++++++++++++------- pkg/mounter/mounter_s3mounter.go | 125 ++++++++++++++++++---------- pkg/mounter/utils/mounter_utils.go | 32 +++++++ 3 files changed, 201 insertions(+), 73 deletions(-) diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go index 199770ce..84ddd2b8 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/s3mounter.go @@ -1,52 +1,105 @@ package main import ( - "encoding/json" "fmt" "go.uber.org/zap" ) type s3MounterArgs struct { - ReadOnly string `json:"read-only,omitempty"` - AllowOther string `json:"allow-other,omitempty"` - UID string `json:"uid,omitempty"` - GID string `json:"gid,omitempty"` - UMask string `json:"umask,omitempty"` - LogLevel string `json:"log-level,omitempty"` - AwsConfigDir string `json:"aws-config-dir,omitempty"` - EndpointURL string `json:"endpoint-url,omitempty"` - Region string `json:"region,omitempty"` + ReadOnly string `json:"read-only,omitempty"` + AllowOther string `json:"allow-other,omitempty"` + UID string `json:"uid,omitempty"` + GID string `json:"gid,omitempty"` + // Removed: UMask β€” mount-s3 has no --umask flag; use --dir-mode/--file-mode via MountOptions + // Removed: Region β€” mount-s3 auto-detects region from AWS_CONFIG_FILE + // Removed: AwsConfigDir β€” mount-s3 has no --aws-config-dir flag + LogLevel string `json:"log-level,omitempty"` // valid: "debug", "debug-crt", "no-log" + EndpointURL string `json:"endpoint-url,omitempty"` + // AWS credential file paths β€” NOT CLI flags. + // Worker sets these as AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE + // env vars on the mount-s3 subprocess before invoking it. + AwsCredentialsFile string `json:"aws-credentials-file,omitempty"` + AwsConfigFile string `json:"aws-config-file,omitempty"` } +// PopulateArgsSlice builds the CLI args slice for mount-s3. +// +// Key rules: +// - AwsCredentialsFile and AwsConfigFile are NOT CLI flags β€” use EnvVars() for those +// - Boolean flags (--allow-other, --read-only) are emitted without a value +// because mount-s3 does not accept --allow-other=true syntax +// - LogLevel maps to --debug / --debug-crt / --no-log (mount-s3 has no --log-level) func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string, error) { - // Marshal to JSON - raw, err := json.Marshal(args) - if err != nil { - return nil, err + result := []string{bucket, targetPath} + + // --allow-other: boolean flag, no value + if args.AllowOther == "true" { + result = append(result, "--allow-other") } - // Unmarshal into map[string]string - var m map[string]string - if err := json.Unmarshal(raw, &m); err != nil { - return nil, err + // --read-only: boolean flag, no value + if args.ReadOnly == "true" { + result = append(result, "--read-only") } - // Convert to key=value slice - result := []string{bucket, targetPath} - for k, v := range m { - result = append(result, fmt.Sprintf("--%s=%v", k, v)) // --key=value + // --uid + if args.UID != "" { + result = append(result, "--uid="+args.UID) + } + + // --gid + if args.GID != "" { + result = append(result, "--gid="+args.GID) } - return result, nil // [mount, bucket, path, --key1=value1, --key2=value2, ...] + // --endpoint-url + if args.EndpointURL != "" { + result = append(result, "--endpoint-url="+args.EndpointURL) + } + + // Log level: mount-s3 has no --log-level flag. + // Map to the supported flags only. + switch args.LogLevel { + case "debug": + result = append(result, "--debug") + case "debug-crt": + result = append(result, "--debug-crt") + case "no-log": + result = append(result, "--no-log") + default: + if args.LogLevel != "" { + logger.Warn("unsupported log level for mount-s3, ignoring", + zap.String("log-level", args.LogLevel), + zap.String("supported", "debug, debug-crt, no-log"), + ) + } + } + + return result, nil +} + +// EnvVars returns the environment variables that must be set on the mount-s3 +// subprocess so it can locate the AWS credentials and config files. +// The caller must add these to cmd.Env before cmd.Start(). +func (args s3MounterArgs) EnvVars() []string { + var envVars []string + if args.AwsCredentialsFile != "" { + envVars = append(envVars, "AWS_SHARED_CREDENTIALS_FILE="+args.AwsCredentialsFile) + } + if args.AwsConfigFile != "" { + envVars = append(envVars, "AWS_CONFIG_FILE="+args.AwsConfigFile) + } + return envVars } +// Validate checks that required fields are present and values are valid. func (args s3MounterArgs) Validate(targetPath string) error { if err := pathValidator(targetPath); err != nil { return err } - // Check if value of allow-other parameter is boolean "true" or "false" + // allow-other must be a boolean string if set if args.AllowOther != "" { if isBool := isBoolString(args.AllowOther); !isBool { logger.Error("cannot convert value of allow-other into boolean", zap.Any("allow-other", args.AllowOther)) @@ -54,12 +107,18 @@ func (args s3MounterArgs) Validate(targetPath string) error { } } - if args.AwsConfigDir == "" { - logger.Error("aws-config-dir is required for mount-s3") - return fmt.Errorf("aws-config-dir is required for mount-s3") + // AwsCredentialsFile and AwsConfigFile replace the old AwsConfigDir check + if args.AwsCredentialsFile == "" { + logger.Error("aws-credentials-file is required for mount-s3") + return fmt.Errorf("aws-credentials-file is required for mount-s3") + } + + if args.AwsConfigFile == "" { + logger.Error("aws-config-file is required for mount-s3") + return fmt.Errorf("aws-config-file is required for mount-s3") } - // Check if value of read-only parameter is boolean "true" or "false" + // read-only must be a boolean string if set if args.ReadOnly != "" { if isBool := isBoolString(args.ReadOnly); !isBool { logger.Error("cannot convert value of read-only into boolean", zap.Any("read-only", args.ReadOnly)) @@ -68,4 +127,4 @@ func (args s3MounterArgs) Validate(targetPath string) error { } return nil -} +} \ No newline at end of file diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 7d099066..0ab2e1d8 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -27,8 +27,8 @@ type MountpointS3Mounter struct { SecretKey string UID string GID string - UMask string - LogLevel string + UMask string // kept for API compatibility; mount-s3 has no --umask, use --dir-mode/--file-mode via MountOptions + LogLevel string // valid values: "debug", "debug-crt", "no-log"; mount-s3 has no --log-level flag ReadOnly bool MountOptions []string MounterUtils utils.MounterUtils @@ -40,11 +40,15 @@ type s3MounterArgs struct { AllowOther string `json:"allow-other,omitempty"` UID string `json:"uid,omitempty"` GID string `json:"gid,omitempty"` - UMask string `json:"umask,omitempty"` - LogLevel string `json:"log-level,omitempty"` - // Config file path for AWS credentials - AwsConfigDir string `json:"aws-config-dir,omitempty"` - EndpointURL string `json:"endpoint-url,omitempty"` + // Removed: UMask β€” mount-s3 has no --umask flag + // Removed: AwsConfigDir β€” mount-s3 has no --aws-config-dir flag + LogLevel string `json:"log-level,omitempty"` // valid: "debug", "debug-crt", "no-log" + EndpointURL string `json:"endpoint-url,omitempty"` + // AWS credential file paths β€” NOT CLI flags. + // Worker sets these as AWS_SHARED_CREDENTIALS_FILE / AWS_CONFIG_FILE env vars + // on the mount-s3 subprocess before invoking it. + AwsCredentialsFile string `json:"aws-credentials-file,omitempty"` + AwsConfigFile string `json:"aws-config-file,omitempty"` } const ( @@ -57,6 +61,13 @@ var ( removeS3ConfigFile = removeS3MountConfigFile ) +// envMounter is a local interface for type assertion β€” allows calling +// FuseMountWithEnv on the concrete MounterOptsUtils without changing the +// MounterUtils interface, so other mounters (rclone etc.) are unaffected. +type envMounter interface { + FuseMountWithEnv(path string, comm string, args []string, envVars []string) error +} + // NewMountpointS3Mounter creates a new MountpointS3Mounter from secretMap func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounterUtils utils.MounterUtils) Mounter { klog.Info("-newMountpointS3Mounter-") @@ -134,24 +145,12 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { // Create unique config dir per volume configPathWithVolID := path.Join(configPath, fmt.Sprintf("%x", sha256.Sum256([]byte(target)))) - // configCreated := false - - // // Cleanup on failure - // defer func() { - // if err != nil && configCreated { - // klog.Warningf("Mount failed, cleaning up config dir: %s", configPathWithVolID) - // removeS3ConfigFile(configPath, target) - // } - // }() - - // Write AWS credentials config file + // Write AWS credentials and config files if err := createS3MountConfig(configPathWithVolID, s3); err != nil { klog.Errorf("MountpointS3Mounter Mount: Cannot create config file: %v", err) return err } - // configCreated = true - // Build bucket path with optional object path prefix bucketName := s3.BucketName if s3.ObjectPath != "" { @@ -159,7 +158,7 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { bucketName = fmt.Sprintf("%s/%s", s3.BucketName, trimmedPath) } - args, wnOp := s3.formulateMountOptions(bucketName, target, configPathWithVolID) + args, envVars, wnOp := s3.formulateMountOptions(bucketName, target, configPathWithVolID) if mountWorker { klog.Info("Mount on Worker started...") @@ -182,6 +181,16 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { } klog.Info("NodeServer Mounting...") + + // Use FuseMountWithEnv if available on the concrete type (race-safe, per-subprocess env). + // Falls back to FuseMount if running under a mock or different implementation β€” the + // fallback will still work but credentials must be available via system env in that case. + if m, ok := s3.MounterUtils.(envMounter); ok { + return m.FuseMountWithEnv(target, constants.MountpointS3BinaryPath, args, envVars) + } + + klog.Warning("MounterUtils does not implement FuseMountWithEnv, falling back to FuseMount. " + + "Ensure AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE are set in the process environment.") return s3.MounterUtils.FuseMount(target, constants.MountpointS3BinaryPath, args) } @@ -214,13 +223,14 @@ func (s3 *MountpointS3Mounter) Unmount(target string) error { return nil } -// createS3MountConfig creates the AWS credentials and config files -// that mount-s3 reads via --aws-config-dir flag. +// createS3MountConfig creates the AWS credentials and config files. +// mount-s3 locates these via AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE env vars. +// // Directory structure: // // / // credentials <- AWS credentials (access/secret key) -// config <- AWS config (region, endpoint) +// config <- AWS config (region, endpoint_url) func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) error { // Create the config directory if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 -- directory is created under a controlled path using SHA256 hash, not user input @@ -292,20 +302,33 @@ func writeConfigFile(filePath string, lines []string) error { return w.Flush() } -// formulateMountOptions builds CLI args for mount-s3 and the worker payload -func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathWithVolID string) (nodeServerOp []string, workerNodeOp s3MounterArgs) { +// formulateMountOptions builds CLI args, env vars, and the worker payload for mount-s3. +// +// Removed flags (do not exist in mount-s3): +// - --aws-config-dir β†’ replaced by AWS_SHARED_CREDENTIALS_FILE / AWS_CONFIG_FILE env vars +// - --log-level β†’ mapped to --debug / --debug-crt / --no-log +// - --umask β†’ use --dir-mode / --file-mode via MountOptions in StorageClass +func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathWithVolID string) (nodeServerOp []string, envVars []string, workerNodeOp s3MounterArgs) { // mount-s3 syntax: mount-s3 [flags] nodeServerOp = []string{ bucket, target, "--allow-other", - // Point mount-s3 to our config directory for credentials & config - "--aws-config-dir=" + configPathWithVolID, + } + + // AWS credentials via env vars β€” mount-s3 and the underlying AWS SDK reads these + // to locate the credentials and config files written by createS3MountConfig. + credFilePath := path.Join(configPathWithVolID, s3MountCredentialsFile) + cfgFilePath := path.Join(configPathWithVolID, s3MountConfigFile) + envVars = []string{ + "AWS_SHARED_CREDENTIALS_FILE=" + credFilePath, + "AWS_CONFIG_FILE=" + cfgFilePath, } workerNodeOp = s3MounterArgs{ - AllowOther: "true", - AwsConfigDir: configPathWithVolID, + AllowOther: "true", + AwsCredentialsFile: credFilePath, + AwsConfigFile: cfgFilePath, } // Endpoint (IBM COS endpoint) @@ -313,6 +336,7 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW nodeServerOp = append(nodeServerOp, "--endpoint-url="+s3.EndPoint) workerNodeOp.EndpointURL = s3.EndPoint } + // UID if s3.UID != "" { nodeServerOp = append(nodeServerOp, "--uid="+s3.UID) @@ -325,19 +349,32 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW workerNodeOp.GID = s3.GID } - // UMask + // UMask: mount-s3 has no --umask flag. + // Use --dir-mode / --file-mode via MountOptions in the StorageClass instead. if s3.UMask != "" { - nodeServerOp = append(nodeServerOp, "--umask="+s3.UMask) - workerNodeOp.UMask = s3.UMask - } - - // Log level - logLevel := s3.LogLevel - if logLevel == "" { - logLevel = "warn" + klog.Warningf("MountpointS3Mounter: 'umask=%s' is set but mount-s3 does not support --umask. "+ + "Use --dir-mode / --file-mode via mountOptions in the StorageClass instead.", s3.UMask) + } + + // Log level: mount-s3 has no --log-level flag. + // Valid mappings: "debug" β†’ --debug, "debug-crt" β†’ --debug-crt, "no-log" β†’ --no-log + // Anything else (e.g. "warn", "info") is ignored β€” mount-s3 default logging applies. + switch s3.LogLevel { + case "debug": + nodeServerOp = append(nodeServerOp, "--debug") + workerNodeOp.LogLevel = "debug" + case "debug-crt": + nodeServerOp = append(nodeServerOp, "--debug-crt") + workerNodeOp.LogLevel = "debug-crt" + case "no-log": + nodeServerOp = append(nodeServerOp, "--no-log") + workerNodeOp.LogLevel = "no-log" + default: + if s3.LogLevel != "" { + klog.Warningf("MountpointS3Mounter: unsupported log level '%s' for mount-s3. "+ + "Supported values: 'debug', 'debug-crt', 'no-log'. Ignoring.", s3.LogLevel) + } } - nodeServerOp = append(nodeServerOp, "--log-level="+logLevel) - workerNodeOp.LogLevel = logLevel // Read-only if s3.ReadOnly { @@ -345,13 +382,13 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW workerNodeOp.ReadOnly = "true" } - // Extra mount options from StorageClass + // Extra mount options from StorageClass (e.g. --dir-mode=0755, --file-mode=0644) nodeServerOp = append(nodeServerOp, s3.MountOptions...) klog.Infof("MountpointS3 nodeServerOp: %v", nodeServerOp) klog.Infof("MountpointS3 workerNodeOp: %+v", workerNodeOp) - return nodeServerOp, workerNodeOp + return nodeServerOp, envVars, workerNodeOp } // removeS3MountConfigFile removes the config directory for the volume (same pattern as rclone) @@ -378,4 +415,4 @@ func removeS3MountConfigFile(configPath, target string) { return } klog.Errorf("removeS3MountConfigFile: Failed to remove config dir after %d attempts", maxRetries) -} +} \ No newline at end of file diff --git a/pkg/mounter/utils/mounter_utils.go b/pkg/mounter/utils/mounter_utils.go index d2e1f8c6..05e16f57 100644 --- a/pkg/mounter/utils/mounter_utils.go +++ b/pkg/mounter/utils/mounter_utils.go @@ -32,7 +32,18 @@ type MounterUtils interface { type MounterOptsUtils struct { } +// FuseMount β€” signature unchanged, delegates to fuseMountInternal with no extra env vars. func (su *MounterOptsUtils) FuseMount(path string, comm string, args []string) error { + return su.fuseMountInternal(path, comm, args, nil) +} + +// FuseMountWithEnv β€” on the concrete struct only, NOT on the interface. +// envVars are per-subprocess env vars (e.g. AWS_SHARED_CREDENTIALS_FILE) +func (su *MounterOptsUtils) FuseMountWithEnv(path string, comm string, args []string, envVars []string) error { + return su.fuseMountInternal(path, comm, args, envVars) +} + +func (su *MounterOptsUtils) fuseMountInternal(path string, comm string, args []string, envVars []string) error { klog.Info("-FuseMount-") klog.Infof("FuseMount: params:\n\tpath: <%s>\n\tcommand: <%s>\n\targs: <%v>", path, comm, args) @@ -45,6 +56,27 @@ func (su *MounterOptsUtils) FuseMount(path string, comm string, args []string) e }() cmd := commandWithCtx(ctx, comm, args...) + + // If env vars provided, merge them with the inherited process env. + if len(envVars) > 0 { + // Build set of keys we are overriding + overrides := make(map[string]bool, len(envVars)) + for _, e := range envVars { + key := strings.SplitN(e, "=", 2)[0] + overrides[key] = true + } + // Inherit all system env except keys we are overriding + baseEnv := os.Environ() + filteredEnv := make([]string, 0, len(baseEnv)+len(envVars)) + for _, e := range baseEnv { + key := strings.SplitN(e, "=", 2)[0] + if !overrides[key] { + filteredEnv = append(filteredEnv, e) + } + } + cmd.Env = append(filteredEnv, envVars...) + } + err := cmd.Start() if err != nil { klog.Errorf("FuseMount: command start failed: mounter=%s, args=%v, error=%v", comm, args, err) From d275531121b895177760fb07be73307325aea1f5 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sat, 7 Mar 2026 00:43:10 +0530 Subject: [PATCH 080/128] publish 0.12.12 version --- pkg/mounter/utils/mounter_utils.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/mounter/utils/mounter_utils.go b/pkg/mounter/utils/mounter_utils.go index 05e16f57..777095c4 100644 --- a/pkg/mounter/utils/mounter_utils.go +++ b/pkg/mounter/utils/mounter_utils.go @@ -59,7 +59,6 @@ func (su *MounterOptsUtils) fuseMountInternal(path string, comm string, args []s // If env vars provided, merge them with the inherited process env. if len(envVars) > 0 { - // Build set of keys we are overriding overrides := make(map[string]bool, len(envVars)) for _, e := range envVars { key := strings.SplitN(e, "=", 2)[0] From 4ccc2c4c70e9017562dd4f9ff0a6efcd771c1e1d Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Mon, 9 Mar 2026 10:32:20 +0530 Subject: [PATCH 081/128] publish 0.12.12 version --- cos-csi-mounter/server/server.go | 43 +++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/cos-csi-mounter/server/server.go b/cos-csi-mounter/server/server.go index 467eabe3..82b62af3 100644 --- a/cos-csi-mounter/server/server.go +++ b/cos-csi-mounter/server/server.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "flag" "fmt" "net" @@ -31,6 +32,13 @@ var ( GitCommit = "none" ) +// envMounter is a local interface for type assertion β€” allows calling +// FuseMountWithEnv on the concrete MounterOptsUtils without changing the +// MounterUtils interface, so other mounters (rclone, s3fs) are unaffected. +type envMounter interface { + FuseMountWithEnv(path string, comm string, args []string, envVars []string) error +} + func init() { _ = flag.Set("logtostderr", "true") // #nosec G104: Attempt to set flags for logging to stderr only on best-effort basis.Error cannot be usefully handled. logger = setUpLogger() @@ -167,7 +175,12 @@ func handleCosMount(mounter mounterUtils.MounterUtils, parser MounterArgsParser) return } - logger.Info("New mount request with values:", zap.String("Bucket", request.Bucket), zap.String("Path", request.Path), zap.String("Mounter", request.Mounter), zap.Any("Args", request.Args)) + logger.Info("New mount request with values:", + zap.String("Bucket", request.Bucket), + zap.String("Path", request.Path), + zap.String("Mounter", request.Mounter), + zap.Any("Args", request.Args), + ) if request.Mounter != constants.S3FS && request.Mounter != constants.RClone && request.Mounter != constants.AMAZONS3MOUNTER { logger.Error("invalid mounter", zap.Any("mounter", request.Mounter)) @@ -185,12 +198,18 @@ func handleCosMount(mounter mounterUtils.MounterUtils, parser MounterArgsParser) args, err := parser.Parse(request) if err != nil { logger.Error("failed to parse mounter args", zap.Any("mounter", request.Mounter), zap.Error(err)) - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid args for mounter: %v", err)}) return } - err = mounter.FuseMount(request.Path, request.Mounter, args) + // For mount-s3: pass AWS credential paths as env vars on the subprocess. + // FuseMountWithEnv is on the concrete MounterOptsUtils struct only (not the interface), + // so we type-assert β€” other mounters (rclone, s3fs) fall through to plain FuseMount. + if em, ok := mounter.(envMounter); ok && request.Mounter == constants.AMAZONS3MOUNTER { + err = em.FuseMountWithEnv(request.Path, request.Mounter, args, buildS3EnvVars(request.Args)) + } else { + err = mounter.FuseMount(request.Path, request.Mounter, args) + } if err != nil { logger.Error("mount failed: ", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("mount failed: %v", err)}) @@ -202,6 +221,24 @@ func handleCosMount(mounter mounterUtils.MounterUtils, parser MounterArgsParser) } } +// buildS3EnvVars extracts AWS credential file paths from the request args map +// and returns them as env var strings for the mount-s3 subprocess. +// mount-s3 uses the AWS SDK which reads these standard env vars to locate credentials. +func buildS3EnvVars(args json.RawMessage) []string { + var argsMap map[string]string + if err := json.Unmarshal(args, &argsMap); err != nil { + return nil + } + var envVars []string + if v, ok := argsMap["aws-credentials-file"]; ok && v != "" { + envVars = append(envVars, "AWS_SHARED_CREDENTIALS_FILE="+v) + } + if v, ok := argsMap["aws-config-file"]; ok && v != "" { + envVars = append(envVars, "AWS_CONFIG_FILE="+v) + } + return envVars +} + func handleCosUnmount(mounter mounterUtils.MounterUtils) gin.HandlerFunc { return func(c *gin.Context) { var request struct { From 103135bc14cb078a40d8773518a8d5449de44a57 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Tue, 17 Mar 2026 11:42:16 +0530 Subject: [PATCH 082/128] publish 0.12.15 version --- cos-csi-mounter/server/s3mounter.go | 81 +++++++++++++++- pkg/mounter/mounter_s3mounter.go | 139 +++++++++++++++++++++++++++- 2 files changed, 213 insertions(+), 7 deletions(-) diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go index 84ddd2b8..2340a3f9 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/s3mounter.go @@ -12,15 +12,29 @@ type s3MounterArgs struct { UID string `json:"uid,omitempty"` GID string `json:"gid,omitempty"` // Removed: UMask β€” mount-s3 has no --umask flag; use --dir-mode/--file-mode via MountOptions - // Removed: Region β€” mount-s3 auto-detects region from AWS_CONFIG_FILE // Removed: AwsConfigDir β€” mount-s3 has no --aws-config-dir flag LogLevel string `json:"log-level,omitempty"` // valid: "debug", "debug-crt", "no-log" EndpointURL string `json:"endpoint-url,omitempty"` + // Region is passed explicitly as --region CLI flag in addition to being written + // in the AWS config file, to ensure it is always set even if AWS_CONFIG_FILE + // env var is not propagated correctly to the mount-s3 subprocess. + Region string `json:"region,omitempty"` // AWS credential file paths β€” NOT CLI flags. // Worker sets these as AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE // env vars on the mount-s3 subprocess before invoking it. AwsCredentialsFile string `json:"aws-credentials-file,omitempty"` AwsConfigFile string `json:"aws-config-file,omitempty"` + + MaxThreads string `json:"max-threads,omitempty"` + ReadPartSize string `json:"read-part-size,omitempty"` + WritePartSize string `json:"write-part-size,omitempty"` + MaxThroughputGbps string `json:"maximum-throughput-gbps,omitempty"` + UploadChecksums string `json:"upload-checksums,omitempty"` + CacheDir string `json:"cache,omitempty"` + MaxCacheSize string `json:"max-cache-size,omitempty"` + MetadataTTL string `json:"metadata-ttl,omitempty"` + NegativeMetadataTTL string `json:"negative-metadata-ttl,omitempty"` + LogMetrics bool `json:"log-metrics,omitempty"` } // PopulateArgsSlice builds the CLI args slice for mount-s3. @@ -58,6 +72,11 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string result = append(result, "--endpoint-url="+args.EndpointURL) } + // --region + if args.Region != "" { + result = append(result, "--region="+args.Region) + } + // Log level: mount-s3 has no --log-level flag. // Map to the supported flags only. switch args.LogLevel { @@ -76,6 +95,57 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string } } + // --- Performance tuning options --- + // Only appended when explicitly set. mount-s3 built-in defaults apply otherwise. + + // --max-threads (default: 16) + if args.MaxThreads != "" { + result = append(result, "--max-threads="+args.MaxThreads) + } + + // --read-part-size (default: 8 MiB) + if args.ReadPartSize != "" { + result = append(result, "--read-part-size="+args.ReadPartSize) + } + + // --write-part-size (default: 8 MiB) + if args.WritePartSize != "" { + result = append(result, "--write-part-size="+args.WritePartSize) + } + + // --maximum-throughput-gbps (default: auto-detect on EC2, 10 Gbps elsewhere) + if args.MaxThroughputGbps != "" { + result = append(result, "--maximum-throughput-gbps="+args.MaxThroughputGbps) + } + + // --upload-checksums (default: crc32c) + if args.UploadChecksums != "" { + result = append(result, "--upload-checksums="+args.UploadChecksums) + } + + // --cache + --max-cache-size (default: disabled) + if args.CacheDir != "" { + result = append(result, "--cache="+args.CacheDir) + if args.MaxCacheSize != "" { + result = append(result, "--max-cache-size="+args.MaxCacheSize) + } + } + + // --metadata-ttl (default: minimal, or 60s if --cache is set) + if args.MetadataTTL != "" { + result = append(result, "--metadata-ttl="+args.MetadataTTL) + } + + // --negative-metadata-ttl (default: same as metadata-ttl) + if args.NegativeMetadataTTL != "" { + result = append(result, "--negative-metadata-ttl="+args.NegativeMetadataTTL) + } + + // --log-metrics (default: false) β€” boolean flag, no value + if args.LogMetrics { + result = append(result, "--log-metrics") + } + return result, nil } @@ -126,5 +196,12 @@ func (args s3MounterArgs) Validate(targetPath string) error { } } + // upload-checksums must be a valid value if set. + // Validated early here so users get a clear error rather than a cryptic mount-s3 failure. + if args.UploadChecksums != "" && args.UploadChecksums != "crc32c" && args.UploadChecksums != "off" { + logger.Error("invalid upload-checksums value", zap.String("value", args.UploadChecksums)) + return fmt.Errorf("invalid upload-checksums value '%s': must be 'crc32c' or 'off'", args.UploadChecksums) + } + return nil -} \ No newline at end of file +} diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 0ab2e1d8..f08dea4d 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -32,6 +32,17 @@ type MountpointS3Mounter struct { ReadOnly bool MountOptions []string MounterUtils utils.MounterUtils + + MaxThreads string // --max-threads , default: 16 + ReadPartSize string // --read-part-size , default: 8388608 (8 MiB). + WritePartSize string // --write-part-size , default: 8388608 (8 MiB). Same rules as ReadPartSize. + MaxThroughputGbps string // --maximum-throughput-gbps , default: auto-detect on EC2, 10 Gbps elsewhere + UploadChecksums string // --upload-checksums , default: crc32c + CacheDir string // --cache , enables local disk read cache, default: disabled + MaxCacheSize string // --max-cache-size , only effective if CacheDir is set, default: preserve 5% of available space + MetadataTTL string // --metadata-ttl , default: minimal (or 60s if --cache is set) + NegativeMetadataTTL string // --negative-metadata-ttl , default: same as MetadataTTL + LogMetrics bool // --log-metrics, enables summarized performance metrics in logs, default: false } // s3MounterArgs holds the args passed to the worker for mount-s3 @@ -44,11 +55,24 @@ type s3MounterArgs struct { // Removed: AwsConfigDir β€” mount-s3 has no --aws-config-dir flag LogLevel string `json:"log-level,omitempty"` // valid: "debug", "debug-crt", "no-log" EndpointURL string `json:"endpoint-url,omitempty"` - // AWS credential file paths β€” NOT CLI flags. - // Worker sets these as AWS_SHARED_CREDENTIALS_FILE / AWS_CONFIG_FILE env vars - // on the mount-s3 subprocess before invoking it. + // Region is passed explicitly as --region CLI flag in addition to being written + // in the AWS config file, to ensure it is always set even if AWS_CONFIG_FILE + // env var is not propagated correctly to the mount-s3 subprocess. + Region string `json:"region,omitempty"` AwsCredentialsFile string `json:"aws-credentials-file,omitempty"` AwsConfigFile string `json:"aws-config-file,omitempty"` + + // Performance tuning fields β€” all optional, mirrors MountpointS3Mounter fields. + MaxThreads string `json:"max-threads,omitempty"` + ReadPartSize string `json:"read-part-size,omitempty"` + WritePartSize string `json:"write-part-size,omitempty"` + MaxThroughputGbps string `json:"maximum-throughput-gbps,omitempty"` + UploadChecksums string `json:"upload-checksums,omitempty"` + CacheDir string `json:"cache,omitempty"` + MaxCacheSize string `json:"max-cache-size,omitempty"` + MetadataTTL string `json:"metadata-ttl,omitempty"` + NegativeMetadataTTL string `json:"negative-metadata-ttl,omitempty"` + LogMetrics bool `json:"log-metrics,omitempty"` } const ( @@ -118,6 +142,40 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounter.ReadOnly = val == "true" } + // Performance tuning β€” all optional. + // If not set here, mount-s3 built-in defaults apply. + // StorageClass mountOptions override these if the same flag appears there. + if val, check = secretMap["maxThreads"]; check { + mounter.MaxThreads = val + } + if val, check = secretMap["readPartSize"]; check { + mounter.ReadPartSize = val + } + if val, check = secretMap["writePartSize"]; check { + mounter.WritePartSize = val + } + if val, check = secretMap["maximumThroughputGbps"]; check { + mounter.MaxThroughputGbps = val + } + if val, check = secretMap["uploadChecksums"]; check { + mounter.UploadChecksums = val + } + if val, check = secretMap["cacheDir"]; check { + mounter.CacheDir = val + } + if val, check = secretMap["maxCacheSize"]; check { + mounter.MaxCacheSize = val + } + if val, check = secretMap["metadataTTL"]; check { + mounter.MetadataTTL = val + } + if val, check = secretMap["negativeMetadataTTL"]; check { + mounter.NegativeMetadataTTL = val + } + if val, check = secretMap["logMetrics"]; check { + mounter.LogMetrics = val == "true" + } + mounter.AuthType = "hmac" klog.Infof("newMountpointS3Mounter args:\n\tbucketName: [%s]\n\tobjectPath: [%s]\n\tendPoint: [%s]\n\tlocationConstraint: [%s]\n\tauthType: [%s]", @@ -337,6 +395,12 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW workerNodeOp.EndpointURL = s3.EndPoint } + // Region β€” passed as CLI flag AND written to config file for redundancy. + if s3.LocConstraint != "" { + nodeServerOp = append(nodeServerOp, "--region="+s3.LocConstraint) + workerNodeOp.Region = s3.LocConstraint + } + // UID if s3.UID != "" { nodeServerOp = append(nodeServerOp, "--uid="+s3.UID) @@ -382,7 +446,72 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW workerNodeOp.ReadOnly = "true" } - // Extra mount options from StorageClass (e.g. --dir-mode=0755, --file-mode=0644) + // --- Performance tuning options --- + if s3.ReadPartSize != "" { + nodeServerOp = append(nodeServerOp, "--read-part-size="+s3.ReadPartSize) + workerNodeOp.ReadPartSize = s3.ReadPartSize + } + + // --write-part-size (default: 8388608 = 8 MiB). Same rules as read-part-size. + if s3.WritePartSize != "" { + nodeServerOp = append(nodeServerOp, "--write-part-size="+s3.WritePartSize) + workerNodeOp.WritePartSize = s3.WritePartSize + } + + // --max-threads (default: 16) + // Increase for workloads with many concurrent file operations. + if s3.MaxThreads != "" { + nodeServerOp = append(nodeServerOp, "--max-threads="+s3.MaxThreads) + workerNodeOp.MaxThreads = s3.MaxThreads + } + + // --maximum-throughput-gbps (default: auto-detect on EC2, 10 Gbps elsewhere) + // Not on EC2 so won't auto-detect correctly β€” set explicitly if you know your node bandwidth. + if s3.MaxThroughputGbps != "" { + nodeServerOp = append(nodeServerOp, "--maximum-throughput-gbps="+s3.MaxThroughputGbps) + workerNodeOp.MaxThroughputGbps = s3.MaxThroughputGbps + } + + // --upload-checksums (default: crc32c) + // Set to "off" to reduce CPU overhead on write-heavy workloads at the cost of integrity checks. + if s3.UploadChecksums != "" { + nodeServerOp = append(nodeServerOp, "--upload-checksums="+s3.UploadChecksums) + workerNodeOp.UploadChecksums = s3.UploadChecksums + } + + // --cache + --max-cache-size (default: disabled) + // Enables local disk read cache β€” improves repeated read performance. + // When cache is set, metadata-ttl defaults to 60s automatically. + if s3.CacheDir != "" { + nodeServerOp = append(nodeServerOp, "--cache="+s3.CacheDir) + workerNodeOp.CacheDir = s3.CacheDir + if s3.MaxCacheSize != "" { + nodeServerOp = append(nodeServerOp, "--max-cache-size="+s3.MaxCacheSize) + workerNodeOp.MaxCacheSize = s3.MaxCacheSize + } + } + + // --metadata-ttl (default: minimal, or 60s if --cache is set) + // Set higher to reduce ListObjects API calls on stable buckets. + if s3.MetadataTTL != "" { + nodeServerOp = append(nodeServerOp, "--metadata-ttl="+s3.MetadataTTL) + workerNodeOp.MetadataTTL = s3.MetadataTTL + } + + // --negative-metadata-ttl (default: same as metadata-ttl) + if s3.NegativeMetadataTTL != "" { + nodeServerOp = append(nodeServerOp, "--negative-metadata-ttl="+s3.NegativeMetadataTTL) + workerNodeOp.NegativeMetadataTTL = s3.NegativeMetadataTTL + } + + // --log-metrics (default: false) + // Logs summarized throughput metrics β€” zero performance cost, useful for monitoring. + if s3.LogMetrics { + nodeServerOp = append(nodeServerOp, "--log-metrics") + workerNodeOp.LogMetrics = true + } + + // StorageClass mountOptions are appended LAST β€” always take final precedence over everything above. nodeServerOp = append(nodeServerOp, s3.MountOptions...) klog.Infof("MountpointS3 nodeServerOp: %v", nodeServerOp) @@ -415,4 +544,4 @@ func removeS3MountConfigFile(configPath, target string) { return } klog.Errorf("removeS3MountConfigFile: Failed to remove config dir after %d attempts", maxRetries) -} \ No newline at end of file +} From eddef9652529ecaaf519d7c912714eeb0ae2cb96 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sat, 28 Mar 2026 15:40:06 +0530 Subject: [PATCH 083/128] publish 0.12.16 version --- cos-csi-mounter/server/s3mounter.go | 62 ++++- cos-csi-mounter/server/utils.go | 26 ++ pkg/mounter/mounter_s3mounter.go | 367 +++++++++++++++++++--------- 3 files changed, 328 insertions(+), 127 deletions(-) diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go index 2340a3f9..f512160b 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/s3mounter.go @@ -7,14 +7,17 @@ import ( ) type s3MounterArgs struct { - ReadOnly string `json:"read-only,omitempty"` - AllowOther string `json:"allow-other,omitempty"` - UID string `json:"uid,omitempty"` - GID string `json:"gid,omitempty"` + ReadOnly string `json:"read-only,omitempty"` + AllowOther string `json:"allow-other,omitempty"` + AllowDelete string `json:"allow-delete,omitempty"` + AllowOverwrite string `json:"allow-overwrite,omitempty"` + UID string `json:"uid,omitempty"` + GID string `json:"gid,omitempty"` // Removed: UMask β€” mount-s3 has no --umask flag; use --dir-mode/--file-mode via MountOptions // Removed: AwsConfigDir β€” mount-s3 has no --aws-config-dir flag - LogLevel string `json:"log-level,omitempty"` // valid: "debug", "debug-crt", "no-log" - EndpointURL string `json:"endpoint-url,omitempty"` + LogLevel string `json:"log-level,omitempty"` // valid: "debug", "debug-crt", "no-log" + LogDirectory string `json:"log-directory,omitempty"` + EndpointURL string `json:"endpoint-url,omitempty"` // Region is passed explicitly as --region CLI flag in addition to being written // in the AWS config file, to ensure it is always set even if AWS_CONFIG_FILE // env var is not propagated correctly to the mount-s3 subprocess. @@ -57,6 +60,16 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string result = append(result, "--read-only") } + // --allow-delete: boolean flag, no value + if args.AllowDelete == "true" { + result = append(result, "--allow-delete") + } + + // --allow-overwrite: boolean flag, no value + if args.AllowOverwrite == "true" { + result = append(result, "--allow-overwrite") + } + // --uid if args.UID != "" { result = append(result, "--uid="+args.UID) @@ -95,6 +108,11 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string } } + // --log-directory + if args.LogDirectory != "" { + result = append(result, "--log-directory="+args.LogDirectory) + } + // --- Performance tuning options --- // Only appended when explicitly set. mount-s3 built-in defaults apply otherwise. @@ -196,6 +214,22 @@ func (args s3MounterArgs) Validate(targetPath string) error { } } + // allow-delete must be a boolean string if set + if args.AllowDelete != "" { + if isBool := isBoolString(args.AllowDelete); !isBool { + logger.Error("cannot convert value of allow-delete into boolean", zap.Any("allow-delete", args.AllowDelete)) + return fmt.Errorf("cannot convert value of allow-delete into boolean: %v", args.AllowDelete) + } + } + + // allow-overwrite must be a boolean string if set + if args.AllowOverwrite != "" { + if isBool := isBoolString(args.AllowOverwrite); !isBool { + logger.Error("cannot convert value of allow-overwrite into boolean", zap.Any("allow-overwrite", args.AllowOverwrite)) + return fmt.Errorf("cannot convert value of allow-overwrite into boolean: %v", args.AllowOverwrite) + } + } + // upload-checksums must be a valid value if set. // Validated early here so users get a clear error rather than a cryptic mount-s3 failure. if args.UploadChecksums != "" && args.UploadChecksums != "crc32c" && args.UploadChecksums != "off" { @@ -203,5 +237,21 @@ func (args s3MounterArgs) Validate(targetPath string) error { return fmt.Errorf("invalid upload-checksums value '%s': must be 'crc32c' or 'off'", args.UploadChecksums) } + // Ensure cache directory exists if specified + if args.CacheDir != "" { + if err := ensureDir(args.CacheDir); err != nil { + logger.Error("failed to create cache directory", zap.String("cache-dir", args.CacheDir), zap.Error(err)) + return fmt.Errorf("failed to create cache directory '%s': %w", args.CacheDir, err) + } + } + + // Ensure log directory exists if specified + if args.LogDirectory != "" { + if err := ensureDir(args.LogDirectory); err != nil { + logger.Error("failed to create log directory", zap.String("log-dir", args.LogDirectory), zap.Error(err)) + return fmt.Errorf("failed to create log directory '%s': %w", args.LogDirectory, err) + } + } + return nil } diff --git a/cos-csi-mounter/server/utils.go b/cos-csi-mounter/server/utils.go index 58c41f28..88a45f6f 100644 --- a/cos-csi-mounter/server/utils.go +++ b/cos-csi-mounter/server/utils.go @@ -125,3 +125,29 @@ func fileExists(path string) (bool, error) { } return !info.IsDir(), nil } + +// ensureDir creates the cache directory if it doesn't exist. +// It creates the directory with 0755 permissions (rwxr-xr-x). +func ensureDir(path string) error { + // Check if directory already exists + info, err := os.Stat(path) + if err == nil { + // Path exists, verify it's a directory + if !info.IsDir() { + return fmt.Errorf("cache path exists but is not a directory: %s", path) + } + return nil + } + + // If error is not "not exist", return it + if !os.IsNotExist(err) { + return fmt.Errorf("failed to stat cache directory: %w", err) + } + + // Directory doesn't exist, create it with parent directories + if err := os.MkdirAll(path, 0755); err != nil { + return fmt.Errorf("failed to create cache directory: %w", err) + } + + return nil +} diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index f08dea4d..5d97a6f9 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -16,26 +16,28 @@ import ( "k8s.io/klog/v2" ) -// MountpointS3Mounter implements Mounter for AWS Mountpoint S3 +// MountpointS3Mounter implements Mounter for AWS Mountpoint S3. type MountpointS3Mounter struct { - BucketName string - ObjectPath string - EndPoint string - LocConstraint string - AuthType string - AccessKey string - SecretKey string - UID string - GID string - UMask string // kept for API compatibility; mount-s3 has no --umask, use --dir-mode/--file-mode via MountOptions - LogLevel string // valid values: "debug", "debug-crt", "no-log"; mount-s3 has no --log-level flag - ReadOnly bool - MountOptions []string - MounterUtils utils.MounterUtils + BucketName string + ObjectPath string + EndPoint string + LocConstraint string + AuthType string + AccessKey string + SecretKey string + UID string + GID string + LogLevel string // valid values: "debug", "debug-crt", "no-log"; mount-s3 has no --log-level flag + LogDirectory string // --log-directory , write logs to directory instead of syslog + ReadOnly bool + AllowDelete bool // --allow-delete, enable delete operations + AllowOverwrite bool // --allow-overwrite, enable overwrite operations + MountOptions []string + MounterUtils utils.MounterUtils MaxThreads string // --max-threads , default: 16 - ReadPartSize string // --read-part-size , default: 8388608 (8 MiB). - WritePartSize string // --write-part-size , default: 8388608 (8 MiB). Same rules as ReadPartSize. + ReadPartSize string // --read-part-size , default: 8388608 (8 MiB) + WritePartSize string // --write-part-size , default: 8388608 (8 MiB) MaxThroughputGbps string // --maximum-throughput-gbps , default: auto-detect on EC2, 10 Gbps elsewhere UploadChecksums string // --upload-checksums , default: crc32c CacheDir string // --cache , enables local disk read cache, default: disabled @@ -45,16 +47,19 @@ type MountpointS3Mounter struct { LogMetrics bool // --log-metrics, enables summarized performance metrics in logs, default: false } -// s3MounterArgs holds the args passed to the worker for mount-s3 +// s3MounterArgs holds the args passed to the worker for mount-s3. type s3MounterArgs struct { ReadOnly string `json:"read-only,omitempty"` AllowOther string `json:"allow-other,omitempty"` UID string `json:"uid,omitempty"` GID string `json:"gid,omitempty"` - // Removed: UMask β€” mount-s3 has no --umask flag + // Removed: UMask β€” mount-s3 has no --umask flag; use --dir-mode/--file-mode via MountOptions // Removed: AwsConfigDir β€” mount-s3 has no --aws-config-dir flag - LogLevel string `json:"log-level,omitempty"` // valid: "debug", "debug-crt", "no-log" - EndpointURL string `json:"endpoint-url,omitempty"` + LogLevel string `json:"log-level,omitempty"` // valid: "debug", "debug-crt", "no-log" + EndpointURL string `json:"endpoint-url,omitempty"` + AllowDelete string `json:"allow-delete,omitempty"` // --allow-delete flag + AllowOverwrite string `json:"allow-overwrite,omitempty"` // --allow-overwrite flag + LogDirectory string `json:"log-directory,omitempty"` // --log-directory flag // Region is passed explicitly as --region CLI flag in addition to being written // in the AWS config file, to ensure it is always set even if AWS_CONFIG_FILE // env var is not propagated correctly to the mount-s3 subprocess. @@ -92,107 +97,215 @@ type envMounter interface { FuseMountWithEnv(path string, comm string, args []string, envVars []string) error } -// NewMountpointS3Mounter creates a new MountpointS3Mounter from secretMap +// NewMountpointS3Mounter creates a new MountpointS3Mounter from the unified +// secret format. +// +// Source mapping: +// +// secretMap β€” credentials and bucket identity only: +// accessKey, secretKey (data, base64-decoded by k8s) +// bucketName, objectPath, cosEndpoint, locationConstraint, +// uid, gid (stringData) +// +// mountOptions β€” all mount-s3 behavioral and performance flags, +// sourced from the secret's `mountOptions` field +// and/or the StorageClass mountOptions. +// Known flags are consumed into struct fields; +// unrecognised flags pass through to mount-s3 as-is. +// mountOptions uid/gid override secretMap uid/gid. func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounterUtils utils.MounterUtils) Mounter { klog.Info("-newMountpointS3Mounter-") - var ( - val string - check bool - mounter = &MountpointS3Mounter{} - ) + mounter := &MountpointS3Mounter{} - if val, check = secretMap["cosEndpoint"]; check { - mounter.EndPoint = val + // --- Credentials (unified secret `data` fields, base64-decoded by k8s) --- + if val, ok := secretMap["accessKey"]; ok { + mounter.AccessKey = val } - if val, check = secretMap["locationConstraint"]; check { - mounter.LocConstraint = val + if val, ok := secretMap["secretKey"]; ok { + mounter.SecretKey = val } - if val, check = secretMap["bucketName"]; check { + + // --- Bucket / endpoint identity (`stringData` fields) --- + if val, ok := secretMap["bucketName"]; ok { mounter.BucketName = val } - if val, check = secretMap["objectPath"]; check { + if val, ok := secretMap["objectPath"]; ok { mounter.ObjectPath = val } - if val, check = secretMap["accessKey"]; check { - mounter.AccessKey = val - } - if val, check = secretMap["secretKey"]; check { - mounter.SecretKey = val + // cosEndpoint and locationConstraint are IBM COS-specific. + // They are not part of the upstream unified secret schema but remain + // supported as secretMap keys for IBM COS compatibility. + if val, ok := secretMap["cosEndpoint"]; ok { + mounter.EndPoint = val } - if val, check = secretMap["uid"]; check { - mounter.UID = val + if val, ok := secretMap["locationConstraint"]; ok { + mounter.LocConstraint = val } - // If gid set but uid not, use gid for uid (same as rclone behavior) + + // --- UID / GID from secretMap --- + // If gid is set but uid is not, mirror gid β†’ uid (matches rclone behavior). + // mountOptions values (parsed below) override these if also present there. if secretMap["gid"] != "" && secretMap["uid"] == "" { mounter.UID = secretMap["gid"] - } else if secretMap["uid"] != "" { - mounter.UID = secretMap["uid"] + } else if val := secretMap["uid"]; val != "" { + mounter.UID = val } - if val, check = secretMap["gid"]; check { + if val := secretMap["gid"]; val != "" { mounter.GID = val } - if val, check = secretMap["umask"]; check { - mounter.UMask = val - } - if val, check = secretMap["logLevel"]; check { - mounter.LogLevel = val - } - if val, check = secretMap["readOnly"]; check { - mounter.ReadOnly = val == "true" - } - - // Performance tuning β€” all optional. - // If not set here, mount-s3 built-in defaults apply. - // StorageClass mountOptions override these if the same flag appears there. - if val, check = secretMap["maxThreads"]; check { - mounter.MaxThreads = val - } - if val, check = secretMap["readPartSize"]; check { - mounter.ReadPartSize = val - } - if val, check = secretMap["writePartSize"]; check { - mounter.WritePartSize = val - } - if val, check = secretMap["maximumThroughputGbps"]; check { - mounter.MaxThroughputGbps = val - } - if val, check = secretMap["uploadChecksums"]; check { - mounter.UploadChecksums = val - } - if val, check = secretMap["cacheDir"]; check { - mounter.CacheDir = val - } - if val, check = secretMap["maxCacheSize"]; check { - mounter.MaxCacheSize = val - } - if val, check = secretMap["metadataTTL"]; check { - mounter.MetadataTTL = val - } - if val, check = secretMap["negativeMetadataTTL"]; check { - mounter.NegativeMetadataTTL = val - } - if val, check = secretMap["logMetrics"]; check { - mounter.LogMetrics = val == "true" - } mounter.AuthType = "hmac" - klog.Infof("newMountpointS3Mounter args:\n\tbucketName: [%s]\n\tobjectPath: [%s]\n\tendPoint: [%s]\n\tlocationConstraint: [%s]\n\tauthType: [%s]", - mounter.BucketName, mounter.ObjectPath, mounter.EndPoint, mounter.LocConstraint, mounter.AuthType) - - mounter.MountOptions = mountOptions + // --- Parse mountOptions into struct fields --- + // Known mount-s3 flags are consumed; unrecognised flags are kept in + // MountOptions for pass-through. This runs last so that mountOptions + // values win over secretMap values for overlapping fields (uid, gid). + mounter.MountOptions = parseMountpointS3Options(mounter, mountOptions) mounter.MounterUtils = mounterUtils + klog.Infof("newMountpointS3Mounter args:\n\tbucketName: [%s]\n\tobjectPath: [%s]\n\tendPoint: [%s]\n\tlocationConstraint: [%s]\n\tauthType: [%s] \n\tmountOptions: [%s]", + mounter.BucketName, mounter.ObjectPath, mounter.EndPoint, mounter.LocConstraint, mounter.AuthType, mounter.MountOptions) + return mounter } -// Mount mounts the S3 bucket using mountpoint-s3 +// parseMountpointS3Options parses mount-s3 options from the provided slice and +// populates recognised flags into the mounter struct. +// +// Rules: +// - Flags may be provided with or without a leading "--" prefix (both accepted). +// - Boolean flags (--read-only, --allow-delete, etc.) need no value. +// - Value flags must be in "flag=value" or "--flag=value" form. +// - "allow-other" is silently consumed β€” formulateMountOptions always adds it. +// - "umask" is consumed with a warning β€” mount-s3 has no --umask flag. +// - Unrecognised flags are returned unchanged for pass-through to mount-s3. +// - mountOptions uid/gid override secretMap uid/gid (caller sets secretMap +// values first; this function runs second). +func parseMountpointS3Options(mounter *MountpointS3Mounter, opts []string) []string { + var remaining []string + + for _, opt := range opts { + // Normalise: strip optional leading "--" so comparisons are uniform. + trimmed := strings.TrimPrefix(opt, "--") + + key, value, hasValue := strings.Cut(trimmed, "=") + + switch key { + + // ------------------------------------------------------------------ + // Always emitted by formulateMountOptions β€” consume to avoid duplicates. + // ------------------------------------------------------------------ + case "allow-other": + // formulateMountOptions unconditionally adds --allow-other; drop here. + + // ------------------------------------------------------------------ + // Unsupported by mount-s3 β€” warn and drop so mount-s3 doesn't fail. + // ------------------------------------------------------------------ + case "umask": + klog.Warningf("parseMountpointS3Options: 'umask' is not supported by mount-s3. " + + "Use --dir-mode / --file-mode via mountOptions in the StorageClass instead. Ignoring.") + + // ------------------------------------------------------------------ + // Boolean mount flags β€” presence means enabled, no value needed. + // ------------------------------------------------------------------ + case "read-only": + mounter.ReadOnly = true + case "allow-delete": + mounter.AllowDelete = true + case "allow-overwrite": + mounter.AllowOverwrite = true + case "log-metrics": + mounter.LogMetrics = true + + // ------------------------------------------------------------------ + // Identity β€” mountOptions values override secretMap values. + // ------------------------------------------------------------------ + case "uid": + if hasValue { + mounter.UID = value + } + case "gid": + if hasValue { + mounter.GID = value + } + + // ------------------------------------------------------------------ + // Logging + // ------------------------------------------------------------------ + case "log-level": + // mount-s3 has no --log-level flag. + // formulateMountOptions maps: "debug" β†’ --debug, + // "debug-crt" β†’ --debug-crt, "no-log" β†’ --no-log. + if hasValue { + mounter.LogLevel = value + } + case "log-directory": + if hasValue { + mounter.LogDirectory = value + } + + // ------------------------------------------------------------------ + // Performance tuning + // ------------------------------------------------------------------ + case "max-threads": + if hasValue { + mounter.MaxThreads = value + } + case "read-part-size": + if hasValue { + mounter.ReadPartSize = value + } + case "write-part-size": + if hasValue { + mounter.WritePartSize = value + } + case "maximum-throughput-gbps": + if hasValue { + mounter.MaxThroughputGbps = value + } + case "upload-checksums": + if hasValue { + mounter.UploadChecksums = value + } + + // ------------------------------------------------------------------ + // Caching + // ------------------------------------------------------------------ + case "cache": + if hasValue { + mounter.CacheDir = value + } + case "max-cache-size": + if hasValue { + mounter.MaxCacheSize = value + } + case "metadata-ttl": + if hasValue { + mounter.MetadataTTL = value + } + case "negative-metadata-ttl": + if hasValue { + mounter.NegativeMetadataTTL = value + } + + // ------------------------------------------------------------------ + // Unrecognised β€” pass through to mount-s3 unchanged. + // ------------------------------------------------------------------ + default: + remaining = append(remaining, opt) + } + } + + return remaining +} + +// Mount mounts the S3 bucket using mountpoint-s3. func (s3 *MountpointS3Mounter) Mount(source string, target string) error { klog.Info("-MountpointS3Mounter Mount-") klog.Infof("Mount args:\n\tsource: <%s>\n\ttarget: <%s>", source, target) - // Determine config path based on mode + // Determine config path based on mode. var configPath string if mountWorker { configPath = constants.MounterConfigPathOnHost @@ -200,16 +313,16 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { configPath = constants.MounterConfigPathOnPodS3Mount } - // Create unique config dir per volume + // Create unique config dir per volume. configPathWithVolID := path.Join(configPath, fmt.Sprintf("%x", sha256.Sum256([]byte(target)))) - // Write AWS credentials and config files + // Write AWS credentials and config files. if err := createS3MountConfig(configPathWithVolID, s3); err != nil { klog.Errorf("MountpointS3Mounter Mount: Cannot create config file: %v", err) return err } - // Build bucket path with optional object path prefix + // Build bucket path with optional object path prefix. bucketName := s3.BucketName if s3.ObjectPath != "" { trimmedPath := strings.TrimPrefix(s3.ObjectPath, "/") @@ -252,7 +365,7 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { return s3.MounterUtils.FuseMount(target, constants.MountpointS3BinaryPath, args) } -// Unmount unmounts the S3 bucket +// Unmount unmounts the S3 bucket. func (s3 *MountpointS3Mounter) Unmount(target string) error { klog.Info("-MountpointS3Mounter Unmount-") @@ -290,7 +403,7 @@ func (s3 *MountpointS3Mounter) Unmount(target string) error { // credentials <- AWS credentials (access/secret key) // config <- AWS config (region, endpoint_url) func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) error { - // Create the config directory + // Create the config directory. if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 -- directory is created under a controlled path using SHA256 hash, not user input klog.Errorf("MountpointS3Mounter: Cannot create config dir %s: %v", configPathWithVolID, err) return err @@ -335,7 +448,7 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er return nil } -// writeConfigFile writes lines to a file with 0600 permissions +// writeConfigFile writes lines to a file with 0600 permissions. func writeConfigFile(filePath string, lines []string) error { f, err := CreateFile(filePath) // #nosec G304 -- filePath is constructed internally from a SHA256 hash and constant config dir, not from user input if err != nil { @@ -389,7 +502,7 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW AwsConfigFile: cfgFilePath, } - // Endpoint (IBM COS endpoint) + // Endpoint (IBM COS endpoint). if s3.EndPoint != "" { nodeServerOp = append(nodeServerOp, "--endpoint-url="+s3.EndPoint) workerNodeOp.EndpointURL = s3.EndPoint @@ -401,27 +514,20 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW workerNodeOp.Region = s3.LocConstraint } - // UID + // UID. if s3.UID != "" { nodeServerOp = append(nodeServerOp, "--uid="+s3.UID) workerNodeOp.UID = s3.UID } - // GID + // GID. if s3.GID != "" { nodeServerOp = append(nodeServerOp, "--gid="+s3.GID) workerNodeOp.GID = s3.GID } - // UMask: mount-s3 has no --umask flag. - // Use --dir-mode / --file-mode via MountOptions in the StorageClass instead. - if s3.UMask != "" { - klog.Warningf("MountpointS3Mounter: 'umask=%s' is set but mount-s3 does not support --umask. "+ - "Use --dir-mode / --file-mode via mountOptions in the StorageClass instead.", s3.UMask) - } - // Log level: mount-s3 has no --log-level flag. - // Valid mappings: "debug" β†’ --debug, "debug-crt" β†’ --debug-crt, "no-log" β†’ --no-log + // Valid mappings: "debug" β†’ --debug, "debug-crt" β†’ --debug-crt, "no-log" β†’ --no-log. // Anything else (e.g. "warn", "info") is ignored β€” mount-s3 default logging applies. switch s3.LogLevel { case "debug": @@ -440,46 +546,65 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW } } - // Read-only + // Read-only. if s3.ReadOnly { nodeServerOp = append(nodeServerOp, "--read-only") workerNodeOp.ReadOnly = "true" } + // Allow delete. + if s3.AllowDelete { + nodeServerOp = append(nodeServerOp, "--allow-delete") + workerNodeOp.AllowDelete = "true" + } + + // Allow overwrite. + if s3.AllowOverwrite { + nodeServerOp = append(nodeServerOp, "--allow-overwrite") + workerNodeOp.AllowOverwrite = "true" + } + + // Log directory. + if s3.LogDirectory != "" { + nodeServerOp = append(nodeServerOp, "--log-directory="+s3.LogDirectory) + workerNodeOp.LogDirectory = s3.LogDirectory + } + // --- Performance tuning options --- + + // --read-part-size (default: 8 MiB). if s3.ReadPartSize != "" { nodeServerOp = append(nodeServerOp, "--read-part-size="+s3.ReadPartSize) workerNodeOp.ReadPartSize = s3.ReadPartSize } - // --write-part-size (default: 8388608 = 8 MiB). Same rules as read-part-size. + // --write-part-size (default: 8 MiB). if s3.WritePartSize != "" { nodeServerOp = append(nodeServerOp, "--write-part-size="+s3.WritePartSize) workerNodeOp.WritePartSize = s3.WritePartSize } - // --max-threads (default: 16) - // Increase for workloads with many concurrent file operations. + // --max-threads (default: 16). if s3.MaxThreads != "" { nodeServerOp = append(nodeServerOp, "--max-threads="+s3.MaxThreads) workerNodeOp.MaxThreads = s3.MaxThreads } - // --maximum-throughput-gbps (default: auto-detect on EC2, 10 Gbps elsewhere) + // --maximum-throughput-gbps (default: auto-detect on EC2, 10 Gbps elsewhere). // Not on EC2 so won't auto-detect correctly β€” set explicitly if you know your node bandwidth. if s3.MaxThroughputGbps != "" { nodeServerOp = append(nodeServerOp, "--maximum-throughput-gbps="+s3.MaxThroughputGbps) workerNodeOp.MaxThroughputGbps = s3.MaxThroughputGbps } - // --upload-checksums (default: crc32c) + // --upload-checksums (default: crc32c). // Set to "off" to reduce CPU overhead on write-heavy workloads at the cost of integrity checks. if s3.UploadChecksums != "" { nodeServerOp = append(nodeServerOp, "--upload-checksums="+s3.UploadChecksums) workerNodeOp.UploadChecksums = s3.UploadChecksums } - // --cache + --max-cache-size (default: disabled) + // --cache + --max-cache-size (default: disabled). // Enables local disk read cache β€” improves repeated read performance. // When cache is set, metadata-ttl defaults to 60s automatically. if s3.CacheDir != "" { @@ -491,20 +616,20 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW } } - // --metadata-ttl (default: minimal, or 60s if --cache is set) + // --metadata-ttl (default: minimal, or 60s if --cache is set). // Set higher to reduce ListObjects API calls on stable buckets. if s3.MetadataTTL != "" { nodeServerOp = append(nodeServerOp, "--metadata-ttl="+s3.MetadataTTL) workerNodeOp.MetadataTTL = s3.MetadataTTL } - // --negative-metadata-ttl (default: same as metadata-ttl) + // --negative-metadata-ttl (default: same as metadata-ttl). if s3.NegativeMetadataTTL != "" { nodeServerOp = append(nodeServerOp, "--negative-metadata-ttl="+s3.NegativeMetadataTTL) workerNodeOp.NegativeMetadataTTL = s3.NegativeMetadataTTL } - // --log-metrics (default: false) + // --log-metrics (default: false). // Logs summarized throughput metrics β€” zero performance cost, useful for monitoring. if s3.LogMetrics { nodeServerOp = append(nodeServerOp, "--log-metrics") @@ -520,7 +645,7 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW return nodeServerOp, envVars, workerNodeOp } -// removeS3MountConfigFile removes the config directory for the volume (same pattern as rclone) +// removeS3MountConfigFile removes the config directory for the volume (same pattern as rclone). func removeS3MountConfigFile(configPath, target string) { configPathWithVolID := path.Join(configPath, fmt.Sprintf("%x", sha256.Sum256([]byte(target)))) From 31e4eaed9975eacbb12779ab61642906e91f347c Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sat, 28 Mar 2026 16:14:33 +0530 Subject: [PATCH 084/128] publish 0.12.16 version --- cos-csi-mounter/server/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cos-csi-mounter/server/utils.go b/cos-csi-mounter/server/utils.go index 88a45f6f..48915dbc 100644 --- a/cos-csi-mounter/server/utils.go +++ b/cos-csi-mounter/server/utils.go @@ -145,7 +145,7 @@ func ensureDir(path string) error { } // Directory doesn't exist, create it with parent directories - if err := os.MkdirAll(path, 0755); err != nil { + if err := os.MkdirAll(path, 0750); err != nil { // #nosec G301 -- cache directory permissions set to 0750 for security return fmt.Errorf("failed to create cache directory: %w", err) } From f3f7c61222a0a33907ab76dfc6bac965a4eb14fb Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sun, 29 Mar 2026 11:43:07 +0530 Subject: [PATCH 085/128] publish 0.12.17 version --- .github/workflows/release.yml | 133 ++++++++++++++++--------------- pkg/mounter/mounter_s3mounter.go | 74 ++++++++++++----- 2 files changed, 122 insertions(+), 85 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8e94097b..8ae6bb43 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,10 +3,10 @@ name: CI & Release on: push: branches: - - aws-s3mounter + - aws-s3mounter pull_request: branches: - - aws-s3mounter + - aws-s3mounter concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest outputs: should_release: ${{ steps.check_release.outputs.should_release }} - + steps: - name: Checkout Code uses: actions/checkout@v7 @@ -25,36 +25,39 @@ jobs: - name: Print Go Version run: go version - - name: Get last commit message - id: check_commit - run: | - message=$(git log -1 --pretty=%B) - message="${message//'%'/'%25'}" # Escape '%' - message="${message//$'\n'/'%0A'}" # Escape newlines - message="${message//$'\r'/'%0D'}" # Escape carriage returns - echo "message=$message" >> "$GITHUB_OUTPUT" - shell: bash - - - name: Check Commit Message - run: | - echo "Commit Message: ${{ steps.check_commit.outputs.message }}" - - - name: Check if release should be triggered - id: check_release - run: | - if [[ "${{ steps.check_commit.outputs.message }}" == *"publish"* ]]; then - echo "should_release=true" >> "$GITHUB_OUTPUT" - echo "Debian/RPM packages will be created" - else - echo "should_release=false" >> "$GITHUB_OUTPUT" - echo "Debian/RPM packages will not be created" - fi + - name: Print Go Version + run: go version + + - name: Get last commit message + id: check_commit + run: | + message=$(git log -1 --pretty=%B) + message="${message//'%'/'%25'}" # Escape '%' + message="${message//$'\n'/'%0A'}" # Escape newlines + message="${message//$'\r'/'%0D'}" # Escape carriage returns + echo "message=$message" >> "$GITHUB_OUTPUT" + shell: bash + + - name: Check Commit Message + run: | + echo "Commit Message: ${{ steps.check_commit.outputs.message }}" + + - name: Check if release should be triggered + id: check_release + run: | + if [[ "${{ steps.check_commit.outputs.message }}" == *"publish"* ]]; then + echo "should_release=true" >> "$GITHUB_OUTPUT" + echo "Debian/RPM packages will be created" + else + echo "should_release=false" >> "$GITHUB_OUTPUT" + echo "Debian/RPM packages will not be created" + fi build-and-test: needs: common-setup runs-on: ubuntu-latest permissions: - contents: write # needed to push coverage to gh-pages + contents: write # needed to push coverage to gh-pages pull-requests: write # needed to comment on PRs steps: @@ -63,42 +66,42 @@ jobs: with: fetch-depth: 0 - - name: Install system dependencies (bc + docker) - run: | - sudo apt-get update -qq - sudo apt-get install -y bc docker-ce-cli - - - name: Prepare environment - run: | - export GO111MODULE=on - go mod tidy - make deps - - - name: Check code formatting - run: make fmt - - - name: Run unit tests - run: make test - - - name: Generate coverage - run: | - make coverage - ./scripts/calculateCoverage.sh - touch "Passing" || touch "Failed" - - - name: Build driver image - run: | - make driver - env: - RHSM_USER: ${{ secrets.RHSM_USER }} - RHSM_PASS: ${{ secrets.RHSM_PASS }} - - - name: Publish coverage - if: success() - env: - GHE_TOKEN: ${{ secrets.GHE_TOKEN }} - run: | - ./scripts/publishCoverage.sh + - name: Install system dependencies (bc + docker) + run: | + sudo apt-get update -qq + sudo apt-get install -y bc docker-ce-cli + + - name: Prepare environment + run: | + export GO111MODULE=on + go mod tidy + make deps + + - name: Check code formatting + run: make fmt + + - name: Run unit tests + run: make test + + - name: Generate coverage + run: | + make coverage + ./scripts/calculateCoverage.sh + touch "Passing" || touch "Failed" + + - name: Build driver image + run: | + make driver + env: + RHSM_USER: ${{ secrets.RHSM_USER }} + RHSM_PASS: ${{ secrets.RHSM_PASS }} + + - name: Publish coverage + if: success() + env: + GHE_TOKEN: ${{ secrets.GHE_TOKEN }} + run: | + ./scripts/publishCoverage.sh release: needs: common-setup @@ -109,7 +112,7 @@ jobs: strategy: matrix: package_dir: - - cos-csi-mounter + - cos-csi-mounter env: IS_LATEST_RELEASE: 'true' diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 5d97a6f9..afdaf438 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -100,19 +100,17 @@ type envMounter interface { // NewMountpointS3Mounter creates a new MountpointS3Mounter from the unified // secret format. // -// Source mapping: +// Source mapping and precedence (lowest β†’ highest): // -// secretMap β€” credentials and bucket identity only: -// accessKey, secretKey (data, base64-decoded by k8s) -// bucketName, objectPath, cosEndpoint, locationConstraint, -// uid, gid (stringData) +// 1. secretMap identity keys β€” accessKey, secretKey, bucketName, objectPath, +// cosEndpoint, locationConstraint, uid, gid +// 2. StorageClass mountOptions (mountOptions []string arg) β€” parsed first +// 3. Secret mountOptions (secretMap["mountOptions"] multiline string) β€” parsed +// second and therefore wins over StorageClass on any overlapping flag // -// mountOptions β€” all mount-s3 behavioral and performance flags, -// sourced from the secret's `mountOptions` field -// and/or the StorageClass mountOptions. -// Known flags are consumed into struct fields; -// unrecognised flags pass through to mount-s3 as-is. -// mountOptions uid/gid override secretMap uid/gid. +// For passthrough flags (unrecognised by the parser), the same precedence +// applies: secret passthroughs are appended after SC passthroughs, so +// mount-s3 sees the secret value last (last-value-wins semantics). func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounterUtils utils.MounterUtils) Mounter { klog.Info("-newMountpointS3Mounter-") @@ -143,9 +141,9 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounter.LocConstraint = val } - // --- UID / GID from secretMap --- + // --- UID / GID from secretMap (lowest precedence for these fields) --- // If gid is set but uid is not, mirror gid β†’ uid (matches rclone behavior). - // mountOptions values (parsed below) override these if also present there. + // Both can be overridden by mountOptions below. if secretMap["gid"] != "" && secretMap["uid"] == "" { mounter.UID = secretMap["gid"] } else if val := secretMap["uid"]; val != "" { @@ -157,19 +155,55 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounter.AuthType = "hmac" - // --- Parse mountOptions into struct fields --- - // Known mount-s3 flags are consumed; unrecognised flags are kept in - // MountOptions for pass-through. This runs last so that mountOptions - // values win over secretMap values for overlapping fields (uid, gid). - mounter.MountOptions = parseMountpointS3Options(mounter, mountOptions) + // --- Merge and parse mountOptions --- + // + // Step 1: Parse StorageClass mountOptions (the []string arg). + // These come in already split by the kubelet. + scRemainder := parseMountpointS3Options(mounter, mountOptions) + + // Step 2: Parse the secret's mountOptions multiline string. + // splitSecretMountOptions handles blank lines and # comments. + // Secret options are parsed AFTER SC options so they win on overlap. + secretOpts := splitSecretMountOptions(secretMap["mountOptions"]) + secretRemainder := parseMountpointS3Options(mounter, secretOpts) + + // Step 3: Build the final passthrough slice. + // SC passthroughs come first; secret passthroughs come last so + // mount-s3 sees the secret value last (last-value-wins). + mounter.MountOptions = append(scRemainder, secretRemainder...) mounter.MounterUtils = mounterUtils - klog.Infof("newMountpointS3Mounter args:\n\tbucketName: [%s]\n\tobjectPath: [%s]\n\tendPoint: [%s]\n\tlocationConstraint: [%s]\n\tauthType: [%s] \n\tmountOptions: [%s]", - mounter.BucketName, mounter.ObjectPath, mounter.EndPoint, mounter.LocConstraint, mounter.AuthType, mounter.MountOptions) + klog.Infof("newMountpointS3Mounter args:\n\tbucketName: [%s]\n\tobjectPath: [%s]\n\tendPoint: [%s]\n\tlocationConstraint: [%s]\n\tauthType: [%s]", + mounter.BucketName, mounter.ObjectPath, mounter.EndPoint, mounter.LocConstraint, mounter.AuthType) + klog.Infof("newMountpointS3Mounter: SC mountOptions=%v secretMountOptions=%v passthrough=%v", + mountOptions, secretOpts, mounter.MountOptions) return mounter } +// splitSecretMountOptions splits the multiline mountOptions string from the +// secret into a []string suitable for parseMountpointS3Options. +// +// Rules: +// - Each non-empty line is one flag (same as StorageClass mountOptions). +// - Lines beginning with '#' (after trimming) are comments β€” skipped. +// - Leading/trailing whitespace on each line is trimmed. +// - Blank lines are skipped. +func splitSecretMountOptions(raw string) []string { + if raw == "" { + return nil + } + var opts []string + for _, line := range strings.Split(raw, "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + opts = append(opts, line) + } + return opts +} + // parseMountpointS3Options parses mount-s3 options from the provided slice and // populates recognised flags into the mounter struct. // From 0c812b8e6f8657c0225da222bc342b8b2b4ef3e5 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sun, 29 Mar 2026 22:34:36 +0530 Subject: [PATCH 086/128] publish 0.12.18 version --- cos-csi-mounter/server/server.go | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/cos-csi-mounter/server/server.go b/cos-csi-mounter/server/server.go index 82b62af3..ff39cb94 100644 --- a/cos-csi-mounter/server/server.go +++ b/cos-csi-mounter/server/server.go @@ -106,7 +106,6 @@ func handleSignals() { <-signals socketPath := filepath.Join(constants.SocketDir, constants.SocketFile) if err := os.Remove(socketPath); err != nil { - // Handle it properly: log it, retry, return, etc. logger.Warn("Failed to remove socket on exit", zap.String("path", socketPath), zap.Error(err)) } os.Exit(0) @@ -130,7 +129,6 @@ func startService(setupSocketFunc func() (net.Listener, error), router http.Hand logger.Error("Failed to create socket", zap.Error(err)) return err } - // Close the listener at the end defer func() { if err := listener.Close(); err != nil { fmt.Fprintf(os.Stderr, "failed to close listener: %v\n", err) @@ -141,7 +139,6 @@ func startService(setupSocketFunc func() (net.Listener, error), router http.Hand logger.Info("Starting cos-csi-mounter service...") - // Serve HTTP requests over Unix socket server := &http.Server{ Handler: router, ReadHeaderTimeout: 3 * time.Second, @@ -194,7 +191,7 @@ func handleCosMount(mounter mounterUtils.MounterUtils, parser MounterArgsParser) return } - // validate mounter args + // validate and parse mounter args args, err := parser.Parse(request) if err != nil { logger.Error("failed to parse mounter args", zap.Any("mounter", request.Mounter), zap.Error(err)) @@ -224,17 +221,30 @@ func handleCosMount(mounter mounterUtils.MounterUtils, parser MounterArgsParser) // buildS3EnvVars extracts AWS credential file paths from the request args map // and returns them as env var strings for the mount-s3 subprocess. // mount-s3 uses the AWS SDK which reads these standard env vars to locate credentials. +// +// IMPORTANT: must use map[string]interface{} β€” NOT map[string]string. +// The args payload contains mixed types (e.g. "log-metrics": true is a bool, +// not a string). Unmarshalling into map[string]string silently fails on any +// bool or numeric field, returning nil and dropping both credential env vars, +// which causes mount-s3 to fail with NoSigningCredentials. func buildS3EnvVars(args json.RawMessage) []string { - var argsMap map[string]string + var argsMap map[string]interface{} if err := json.Unmarshal(args, &argsMap); err != nil { + logger.Error("buildS3EnvVars: failed to unmarshal args, credential env vars will be missing", + zap.Error(err)) return nil } + var envVars []string - if v, ok := argsMap["aws-credentials-file"]; ok && v != "" { - envVars = append(envVars, "AWS_SHARED_CREDENTIALS_FILE="+v) + if v, ok := argsMap["aws-credentials-file"]; ok { + if s, ok := v.(string); ok && s != "" { + envVars = append(envVars, "AWS_SHARED_CREDENTIALS_FILE="+s) + } } - if v, ok := argsMap["aws-config-file"]; ok && v != "" { - envVars = append(envVars, "AWS_CONFIG_FILE="+v) + if v, ok := argsMap["aws-config-file"]; ok { + if s, ok := v.(string); ok && s != "" { + envVars = append(envVars, "AWS_CONFIG_FILE="+s) + } } return envVars } @@ -263,4 +273,4 @@ func handleCosUnmount(mounter mounterUtils.MounterUtils) gin.HandlerFunc { logger.Info("bucket unmount is successful", zap.Any("path", request.Path)) c.JSON(http.StatusOK, gin.H{"status": "success"}) } -} +} \ No newline at end of file From fe4b7a6fb5c2fee1b800a7d3cba7c3ed9ef6d4f1 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sun, 5 Apr 2026 18:23:43 +0530 Subject: [PATCH 087/128] publish 0.12.19 version --- cos-csi-mounter/server/s3mounter.go | 144 +++----- pkg/mounter/mounter_s3mounter.go | 495 +++++++++++++--------------- 2 files changed, 280 insertions(+), 359 deletions(-) diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go index f512160b..77bf07ec 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/s3mounter.go @@ -6,38 +6,33 @@ import ( "go.uber.org/zap" ) +// s3MounterArgs holds the args sent from the node server for mount-s3. +// This struct must match the s3MounterArgs in pkg/mounter/mounter_s3mounter.go type s3MounterArgs struct { - ReadOnly string `json:"read-only,omitempty"` - AllowOther string `json:"allow-other,omitempty"` - AllowDelete string `json:"allow-delete,omitempty"` - AllowOverwrite string `json:"allow-overwrite,omitempty"` - UID string `json:"uid,omitempty"` - GID string `json:"gid,omitempty"` - // Removed: UMask β€” mount-s3 has no --umask flag; use --dir-mode/--file-mode via MountOptions - // Removed: AwsConfigDir β€” mount-s3 has no --aws-config-dir flag - LogLevel string `json:"log-level,omitempty"` // valid: "debug", "debug-crt", "no-log" - LogDirectory string `json:"log-directory,omitempty"` - EndpointURL string `json:"endpoint-url,omitempty"` - // Region is passed explicitly as --region CLI flag in addition to being written - // in the AWS config file, to ensure it is always set even if AWS_CONFIG_FILE - // env var is not propagated correctly to the mount-s3 subprocess. - Region string `json:"region,omitempty"` - // AWS credential file paths β€” NOT CLI flags. - // Worker sets these as AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE - // env vars on the mount-s3 subprocess before invoking it. + // Always set. + AllowOther string `json:"allow-other,omitempty"` AwsCredentialsFile string `json:"aws-credentials-file,omitempty"` AwsConfigFile string `json:"aws-config-file,omitempty"` - MaxThreads string `json:"max-threads,omitempty"` - ReadPartSize string `json:"read-part-size,omitempty"` - WritePartSize string `json:"write-part-size,omitempty"` - MaxThroughputGbps string `json:"maximum-throughput-gbps,omitempty"` - UploadChecksums string `json:"upload-checksums,omitempty"` - CacheDir string `json:"cache,omitempty"` - MaxCacheSize string `json:"max-cache-size,omitempty"` - MetadataTTL string `json:"metadata-ttl,omitempty"` - NegativeMetadataTTL string `json:"negative-metadata-ttl,omitempty"` - LogMetrics bool `json:"log-metrics,omitempty"` + // SecretMap-sourced. + EndpointURL string `json:"endpoint-url,omitempty"` + Region string `json:"region,omitempty"` + + // Identity. + UID string `json:"uid,omitempty"` + GID string `json:"gid,omitempty"` + + // Logging (translated from LogLevel). + LogLevel string `json:"log-level,omitempty"` + LogDirectory string `json:"log-directory,omitempty"` + + // Cache. + CacheDir string `json:"cache,omitempty"` + + // Write operations - conflict resolution applied when read-only is set. + ReadOnly string `json:"read-only,omitempty"` + AllowOverwrite string `json:"allow-overwrite,omitempty"` + IncrementalUpload string `json:"incremental-upload,omitempty"` } // PopulateArgsSlice builds the CLI args slice for mount-s3. @@ -50,6 +45,20 @@ type s3MounterArgs struct { func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string, error) { result := []string{bucket, targetPath} + // --- Read-only priority resolution --- + // If read-only is set, clear all write-related flags to ensure clean read-only mount. + // This prevents conflicts where both read-only and write flags are set. + if args.ReadOnly == "true" { + if args.AllowOverwrite == "true" { + logger.Warn("read-only is set, clearing allow-overwrite") + args.AllowOverwrite = "" + } + if args.IncrementalUpload == "true" { + logger.Warn("read-only is set, clearing incremental-upload") + args.IncrementalUpload = "" + } + } + // --allow-other: boolean flag, no value if args.AllowOther == "true" { result = append(result, "--allow-other") @@ -60,16 +69,16 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string result = append(result, "--read-only") } - // --allow-delete: boolean flag, no value - if args.AllowDelete == "true" { - result = append(result, "--allow-delete") - } - - // --allow-overwrite: boolean flag, no value + // --allow-overwrite: boolean flag, no value (cleared if read-only is set) if args.AllowOverwrite == "true" { result = append(result, "--allow-overwrite") } + // --incremental-upload: boolean flag, no value (cleared if read-only is set) + if args.IncrementalUpload == "true" { + result = append(result, "--incremental-upload") + } + // --uid if args.UID != "" { result = append(result, "--uid="+args.UID) @@ -113,55 +122,9 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string result = append(result, "--log-directory="+args.LogDirectory) } - // --- Performance tuning options --- - // Only appended when explicitly set. mount-s3 built-in defaults apply otherwise. - - // --max-threads (default: 16) - if args.MaxThreads != "" { - result = append(result, "--max-threads="+args.MaxThreads) - } - - // --read-part-size (default: 8 MiB) - if args.ReadPartSize != "" { - result = append(result, "--read-part-size="+args.ReadPartSize) - } - - // --write-part-size (default: 8 MiB) - if args.WritePartSize != "" { - result = append(result, "--write-part-size="+args.WritePartSize) - } - - // --maximum-throughput-gbps (default: auto-detect on EC2, 10 Gbps elsewhere) - if args.MaxThroughputGbps != "" { - result = append(result, "--maximum-throughput-gbps="+args.MaxThroughputGbps) - } - - // --upload-checksums (default: crc32c) - if args.UploadChecksums != "" { - result = append(result, "--upload-checksums="+args.UploadChecksums) - } - - // --cache + --max-cache-size (default: disabled) + // --cache if args.CacheDir != "" { result = append(result, "--cache="+args.CacheDir) - if args.MaxCacheSize != "" { - result = append(result, "--max-cache-size="+args.MaxCacheSize) - } - } - - // --metadata-ttl (default: minimal, or 60s if --cache is set) - if args.MetadataTTL != "" { - result = append(result, "--metadata-ttl="+args.MetadataTTL) - } - - // --negative-metadata-ttl (default: same as metadata-ttl) - if args.NegativeMetadataTTL != "" { - result = append(result, "--negative-metadata-ttl="+args.NegativeMetadataTTL) - } - - // --log-metrics (default: false) β€” boolean flag, no value - if args.LogMetrics { - result = append(result, "--log-metrics") } return result, nil @@ -214,14 +177,6 @@ func (args s3MounterArgs) Validate(targetPath string) error { } } - // allow-delete must be a boolean string if set - if args.AllowDelete != "" { - if isBool := isBoolString(args.AllowDelete); !isBool { - logger.Error("cannot convert value of allow-delete into boolean", zap.Any("allow-delete", args.AllowDelete)) - return fmt.Errorf("cannot convert value of allow-delete into boolean: %v", args.AllowDelete) - } - } - // allow-overwrite must be a boolean string if set if args.AllowOverwrite != "" { if isBool := isBoolString(args.AllowOverwrite); !isBool { @@ -230,11 +185,12 @@ func (args s3MounterArgs) Validate(targetPath string) error { } } - // upload-checksums must be a valid value if set. - // Validated early here so users get a clear error rather than a cryptic mount-s3 failure. - if args.UploadChecksums != "" && args.UploadChecksums != "crc32c" && args.UploadChecksums != "off" { - logger.Error("invalid upload-checksums value", zap.String("value", args.UploadChecksums)) - return fmt.Errorf("invalid upload-checksums value '%s': must be 'crc32c' or 'off'", args.UploadChecksums) + // incremental-upload must be a boolean string if set + if args.IncrementalUpload != "" { + if isBool := isBoolString(args.IncrementalUpload); !isBool { + logger.Error("cannot convert value of incremental-upload into boolean", zap.Any("incremental-upload", args.IncrementalUpload)) + return fmt.Errorf("cannot convert value of incremental-upload into boolean: %v", args.IncrementalUpload) + } } // Ensure cache directory exists if specified diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index afdaf438..c526e335 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -17,67 +17,91 @@ import ( ) // MountpointS3Mounter implements Mounter for AWS Mountpoint S3. +// +// Only flags that require code-level handling are modelled as struct fields: +// - Conflict resolution : ReadOnly takes priority β€” clears AllowOverwrite / IncrementalUpload +// - Directory validation: LogDirectory / CacheDir (must exist before mount) +// - Name translation : LogLevel ("debug" β†’ --debug, not --log-level=debug) +// - Special precedence : UID / GID (secretMap < mountOptions) +// - Always-emitted : allow-other (deduplicated here) +// - Invalid flag : umask (warn + drop) +// - SecretMap-sourced : EndPoint / LocConstraint (injected from secret fields) +// +// All other flags are the user's responsibility and pass through unchanged to +// mount-s3 via MountOptions. The parser ensures every passthrough entry has a +// "--" prefix so mount-s3 always receives syntactically valid flags. type MountpointS3Mounter struct { - BucketName string - ObjectPath string - EndPoint string - LocConstraint string - AuthType string - AccessKey string - SecretKey string - UID string - GID string - LogLevel string // valid values: "debug", "debug-crt", "no-log"; mount-s3 has no --log-level flag - LogDirectory string // --log-directory , write logs to directory instead of syslog - ReadOnly bool - AllowDelete bool // --allow-delete, enable delete operations - AllowOverwrite bool // --allow-overwrite, enable overwrite operations - MountOptions []string - MounterUtils utils.MounterUtils - - MaxThreads string // --max-threads , default: 16 - ReadPartSize string // --read-part-size , default: 8388608 (8 MiB) - WritePartSize string // --write-part-size , default: 8388608 (8 MiB) - MaxThroughputGbps string // --maximum-throughput-gbps , default: auto-detect on EC2, 10 Gbps elsewhere - UploadChecksums string // --upload-checksums , default: crc32c - CacheDir string // --cache , enables local disk read cache, default: disabled - MaxCacheSize string // --max-cache-size , only effective if CacheDir is set, default: preserve 5% of available space - MetadataTTL string // --metadata-ttl , default: minimal (or 60s if --cache is set) - NegativeMetadataTTL string // --negative-metadata-ttl , default: same as MetadataTTL - LogMetrics bool // --log-metrics, enables summarized performance metrics in logs, default: false + // Identity β€” sourced from secretMap, not mountOptions. + BucketName string + ObjectPath string + EndPoint string + LocConstraint string + AuthType string + AccessKey string + SecretKey string + + // UID / GID β€” secretMap sets initial value; mountOptions can override. + UID string + GID string + + // LogLevel requires name translation: + // "debug" β†’ --debug + // "debug-crt" β†’ --debug-crt + // "no-log" β†’ --no-log + // mount-s3 has no --log-level flag. + LogLevel string + + // LogDirectory must exist on the node before mount-s3 starts. + // Validated in Mount() before calling formulateMountOptions. + LogDirectory string + + // CacheDir must exist on the node before mount-s3 starts. + // Validated in Mount() before calling formulateMountOptions. + CacheDir string + + // Conflict resolution β€” ReadOnly takes priority over write flags. + // If ReadOnly is set, AllowOverwrite and IncrementalUpload are cleared + // in Mount() before formulateMountOptions is called. This allows SC to + // set allow-overwrite and incremental-upload as safe defaults while still + // letting users request read-only via their secret without errors. + ReadOnly bool // --read-only + AllowOverwrite bool // --allow-overwrite + IncrementalUpload bool // --incremental-upload + + // MountOptions holds all remaining flags that pass through unchanged. + // The parser ensures every entry has a "--" prefix so mount-s3 sees valid flags. + MountOptions []string + MounterUtils utils.MounterUtils } -// s3MounterArgs holds the args passed to the worker for mount-s3. +// s3MounterArgs holds the args sent to the worker node daemon for mount-s3. +// Only structured fields are represented here; passthrough flags are embedded +// in the Args slice at the call site. type s3MounterArgs struct { - ReadOnly string `json:"read-only,omitempty"` - AllowOther string `json:"allow-other,omitempty"` - UID string `json:"uid,omitempty"` - GID string `json:"gid,omitempty"` - // Removed: UMask β€” mount-s3 has no --umask flag; use --dir-mode/--file-mode via MountOptions - // Removed: AwsConfigDir β€” mount-s3 has no --aws-config-dir flag - LogLevel string `json:"log-level,omitempty"` // valid: "debug", "debug-crt", "no-log" - EndpointURL string `json:"endpoint-url,omitempty"` - AllowDelete string `json:"allow-delete,omitempty"` // --allow-delete flag - AllowOverwrite string `json:"allow-overwrite,omitempty"` // --allow-overwrite flag - LogDirectory string `json:"log-directory,omitempty"` // --log-directory flag - // Region is passed explicitly as --region CLI flag in addition to being written - // in the AWS config file, to ensure it is always set even if AWS_CONFIG_FILE - // env var is not propagated correctly to the mount-s3 subprocess. - Region string `json:"region,omitempty"` + // Always set. + AllowOther string `json:"allow-other,omitempty"` AwsCredentialsFile string `json:"aws-credentials-file,omitempty"` AwsConfigFile string `json:"aws-config-file,omitempty"` - // Performance tuning fields β€” all optional, mirrors MountpointS3Mounter fields. - MaxThreads string `json:"max-threads,omitempty"` - ReadPartSize string `json:"read-part-size,omitempty"` - WritePartSize string `json:"write-part-size,omitempty"` - MaxThroughputGbps string `json:"maximum-throughput-gbps,omitempty"` - UploadChecksums string `json:"upload-checksums,omitempty"` - CacheDir string `json:"cache,omitempty"` - MaxCacheSize string `json:"max-cache-size,omitempty"` - MetadataTTL string `json:"metadata-ttl,omitempty"` - NegativeMetadataTTL string `json:"negative-metadata-ttl,omitempty"` - LogMetrics bool `json:"log-metrics,omitempty"` + // SecretMap-sourced. + EndpointURL string `json:"endpoint-url,omitempty"` + Region string `json:"region,omitempty"` + + // Identity. + UID string `json:"uid,omitempty"` + GID string `json:"gid,omitempty"` + + // Logging (translated from LogLevel). + LogLevel string `json:"log-level,omitempty"` + LogDirectory string `json:"log-directory,omitempty"` + + // Cache. + CacheDir string `json:"cache,omitempty"` + + // Conflict trio. + ReadOnly string `json:"read-only,omitempty"` + AllowOverwrite string `json:"allow-overwrite,omitempty"` + IncrementalUpload string `json:"incremental-upload,omitempty"` } const ( @@ -108,15 +132,18 @@ type envMounter interface { // 3. Secret mountOptions (secretMap["mountOptions"] multiline string) β€” parsed // second and therefore wins over StorageClass on any overlapping flag // -// For passthrough flags (unrecognised by the parser), the same precedence -// applies: secret passthroughs are appended after SC passthroughs, so -// mount-s3 sees the secret value last (last-value-wins semantics). +// Special case: if read-only is present in either source, it takes priority and +// clears allow-overwrite and incremental-upload regardless of where they came from. +// +// For passthrough flags (unrecognised by the structured parser), the same +// precedence applies: secret passthroughs are appended after SC passthroughs, +// so mount-s3 sees the secret value last (last-value-wins semantics). func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounterUtils utils.MounterUtils) Mounter { klog.Info("-newMountpointS3Mounter-") mounter := &MountpointS3Mounter{} - // --- Credentials (unified secret `data` fields, base64-decoded by k8s) --- + // --- Credentials --- if val, ok := secretMap["accessKey"]; ok { mounter.AccessKey = val } @@ -124,16 +151,13 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounter.SecretKey = val } - // --- Bucket / endpoint identity (`stringData` fields) --- + // --- Bucket / endpoint identity --- if val, ok := secretMap["bucketName"]; ok { mounter.BucketName = val } if val, ok := secretMap["objectPath"]; ok { mounter.ObjectPath = val } - // cosEndpoint and locationConstraint are IBM COS-specific. - // They are not part of the upstream unified secret schema but remain - // supported as secretMap keys for IBM COS compatibility. if val, ok := secretMap["cosEndpoint"]; ok { mounter.EndPoint = val } @@ -141,9 +165,7 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounter.LocConstraint = val } - // --- UID / GID from secretMap (lowest precedence for these fields) --- - // If gid is set but uid is not, mirror gid β†’ uid (matches rclone behavior). - // Both can be overridden by mountOptions below. + // --- UID / GID (lowest precedence β€” mountOptions overrides below) --- if secretMap["gid"] != "" && secretMap["uid"] == "" { mounter.UID = secretMap["gid"] } else if val := secretMap["uid"]; val != "" { @@ -156,20 +178,14 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounter.AuthType = "hmac" // --- Merge and parse mountOptions --- - // - // Step 1: Parse StorageClass mountOptions (the []string arg). - // These come in already split by the kubelet. + // Step 1: StorageClass mountOptions (already split by kubelet). scRemainder := parseMountpointS3Options(mounter, mountOptions) - // Step 2: Parse the secret's mountOptions multiline string. - // splitSecretMountOptions handles blank lines and # comments. - // Secret options are parsed AFTER SC options so they win on overlap. + // Step 2: Secret mountOptions (multiline string, parsed after SC so it wins). secretOpts := splitSecretMountOptions(secretMap["mountOptions"]) secretRemainder := parseMountpointS3Options(mounter, secretOpts) - // Step 3: Build the final passthrough slice. - // SC passthroughs come first; secret passthroughs come last so - // mount-s3 sees the secret value last (last-value-wins). + // Step 3: SC passthroughs first, secret passthroughs last (last-value-wins). mounter.MountOptions = append(scRemainder, secretRemainder...) mounter.MounterUtils = mounterUtils @@ -185,7 +201,7 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, // secret into a []string suitable for parseMountpointS3Options. // // Rules: -// - Each non-empty line is one flag (same as StorageClass mountOptions). +// - Each non-empty line is one flag. // - Lines beginning with '#' (after trimming) are comments β€” skipped. // - Leading/trailing whitespace on each line is trimmed. // - Blank lines are skipped. @@ -204,53 +220,46 @@ func splitSecretMountOptions(raw string) []string { return opts } -// parseMountpointS3Options parses mount-s3 options from the provided slice and -// populates recognised flags into the mounter struct. +// parseMountpointS3Options parses mount-s3 options from the provided slice. +// +// Only flags that require code-level handling are matched by name β€” everything +// else is forwarded unchanged to mount-s3 via the returned remainder slice. +// +// The remainder always has a "--" prefix added when missing, so mount-s3 +// receives syntactically valid flags regardless of how the user wrote them. +// This handles both boolean flags ("allow-delete" β†’ "--allow-delete") and +// key-value pairs ("max-threads=32" β†’ "--max-threads=32") correctly because +// the original opt string is preserved and only the prefix is fixed. // // Rules: -// - Flags may be provided with or without a leading "--" prefix (both accepted). -// - Boolean flags (--read-only, --allow-delete, etc.) need no value. +// - Flags may be provided with or without a leading "--" prefix. +// - Boolean flags need no value. // - Value flags must be in "flag=value" or "--flag=value" form. // - "allow-other" is silently consumed β€” formulateMountOptions always adds it. // - "umask" is consumed with a warning β€” mount-s3 has no --umask flag. -// - Unrecognised flags are returned unchanged for pass-through to mount-s3. -// - mountOptions uid/gid override secretMap uid/gid (caller sets secretMap -// values first; this function runs second). +// - uid/gid override secretMap values (caller sets secretMap values first). func parseMountpointS3Options(mounter *MountpointS3Mounter, opts []string) []string { var remaining []string for _, opt := range opts { - // Normalise: strip optional leading "--" so comparisons are uniform. + // Normalise: strip optional leading "--" so switch cases are uniform. trimmed := strings.TrimPrefix(opt, "--") - key, value, hasValue := strings.Cut(trimmed, "=") switch key { // ------------------------------------------------------------------ - // Always emitted by formulateMountOptions β€” consume to avoid duplicates. + // Always emitted β€” consume to avoid duplicates. // ------------------------------------------------------------------ case "allow-other": - // formulateMountOptions unconditionally adds --allow-other; drop here. + // formulateMountOptions unconditionally adds --allow-other. // ------------------------------------------------------------------ - // Unsupported by mount-s3 β€” warn and drop so mount-s3 doesn't fail. + // Invalid for mount-s3 β€” warn and drop so mount-s3 doesn't fail. // ------------------------------------------------------------------ case "umask": klog.Warningf("parseMountpointS3Options: 'umask' is not supported by mount-s3. " + - "Use --dir-mode / --file-mode via mountOptions in the StorageClass instead. Ignoring.") - - // ------------------------------------------------------------------ - // Boolean mount flags β€” presence means enabled, no value needed. - // ------------------------------------------------------------------ - case "read-only": - mounter.ReadOnly = true - case "allow-delete": - mounter.AllowDelete = true - case "allow-overwrite": - mounter.AllowOverwrite = true - case "log-metrics": - mounter.LogMetrics = true + "Use --dir-mode / --file-mode instead. Ignoring.") // ------------------------------------------------------------------ // Identity β€” mountOptions values override secretMap values. @@ -265,69 +274,52 @@ func parseMountpointS3Options(mounter *MountpointS3Mounter, opts []string) []str } // ------------------------------------------------------------------ - // Logging + // Log level β€” requires name translation in formulateMountOptions. + // mount-s3 has no --log-level flag; valid values map to separate flags: + // "debug" β†’ --debug, "debug-crt" β†’ --debug-crt, "no-log" β†’ --no-log // ------------------------------------------------------------------ case "log-level": - // mount-s3 has no --log-level flag. - // formulateMountOptions maps: "debug" β†’ --debug, - // "debug-crt" β†’ --debug-crt, "no-log" β†’ --no-log. if hasValue { mounter.LogLevel = value } - case "log-directory": - if hasValue { - mounter.LogDirectory = value - } // ------------------------------------------------------------------ - // Performance tuning + // Directory flags β€” validated for existence before mount-s3 starts. // ------------------------------------------------------------------ - case "max-threads": - if hasValue { - mounter.MaxThreads = value - } - case "read-part-size": - if hasValue { - mounter.ReadPartSize = value - } - case "write-part-size": - if hasValue { - mounter.WritePartSize = value - } - case "maximum-throughput-gbps": + case "log-directory": if hasValue { - mounter.MaxThroughputGbps = value + mounter.LogDirectory = value } - case "upload-checksums": + case "cache": if hasValue { - mounter.UploadChecksums = value + mounter.CacheDir = value } // ------------------------------------------------------------------ - // Caching + // Conflict trio β€” tracked for read-only priority resolution in Mount(). + // read-only clears allow-overwrite and incremental-upload if all three + // are set, so SC defaults don't block users from requesting read-only. // ------------------------------------------------------------------ - case "cache": - if hasValue { - mounter.CacheDir = value - } - case "max-cache-size": - if hasValue { - mounter.MaxCacheSize = value - } - case "metadata-ttl": - if hasValue { - mounter.MetadataTTL = value - } - case "negative-metadata-ttl": - if hasValue { - mounter.NegativeMetadataTTL = value - } + case "read-only": + mounter.ReadOnly = true + case "allow-overwrite": + mounter.AllowOverwrite = true + case "incremental-upload": + mounter.IncrementalUpload = true // ------------------------------------------------------------------ - // Unrecognised β€” pass through to mount-s3 unchanged. + // Everything else β€” pass through to mount-s3 unchanged. + // Ensure "--" prefix so mount-s3 always receives a valid flag. + // Works for both boolean flags and key-value pairs: + // "allow-delete" β†’ "--allow-delete" + // "max-threads=32" β†’ "--max-threads=32" // ------------------------------------------------------------------ default: - remaining = append(remaining, opt) + if strings.HasPrefix(opt, "--") { + remaining = append(remaining, opt) + } else { + remaining = append(remaining, "--"+opt) + } } } @@ -339,7 +331,52 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { klog.Info("-MountpointS3Mounter Mount-") klog.Infof("Mount args:\n\tsource: <%s>\n\ttarget: <%s>", source, target) - // Determine config path based on mode. + // --- Read-only priority resolution --- + // If read-only is set, it takes priority over all write-related flags. + // AllowOverwrite and IncrementalUpload are cleared here, and allow-delete + // is filtered from MountOptions, so the mount succeeds cleanly in read-only mode. + // This means users can always request read-only via their secret even when + // the StorageClass has write flags as defaults. + if s3.ReadOnly { + if s3.AllowOverwrite { + klog.Infof("MountpointS3Mounter: read-only set β€” clearing allow-overwrite") + s3.AllowOverwrite = false + } + if s3.IncrementalUpload { + klog.Infof("MountpointS3Mounter: read-only set β€” clearing incremental-upload") + s3.IncrementalUpload = false + } + + // Filter out allow-delete from passthrough options + var filtered []string + for _, opt := range s3.MountOptions { + trimmed := strings.TrimPrefix(opt, "--") + if trimmed != "allow-delete" { + filtered = append(filtered, opt) + } else { + klog.Infof("MountpointS3Mounter: read-only set β€” removing allow-delete from passthrough options") + } + } + s3.MountOptions = filtered + } + + // --- Directory validation --- + // mount-s3 fails with a confusing error if these directories don't exist. + // Validate here to surface a clear error message before spawning the process. + if s3.LogDirectory != "" { + if _, err := Stat(s3.LogDirectory); err != nil { + return fmt.Errorf("MountpointS3Mounter: log-directory %q does not exist or is not accessible: %w", + s3.LogDirectory, err) + } + } + if s3.CacheDir != "" { + if _, err := Stat(s3.CacheDir); err != nil { + return fmt.Errorf("MountpointS3Mounter: cache directory %q does not exist or is not accessible: %w", + s3.CacheDir, err) + } + } + + // --- Config path --- var configPath string if mountWorker { configPath = constants.MounterConfigPathOnHost @@ -347,10 +384,8 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { configPath = constants.MounterConfigPathOnPodS3Mount } - // Create unique config dir per volume. configPathWithVolID := path.Join(configPath, fmt.Sprintf("%x", sha256.Sum256([]byte(target)))) - // Write AWS credentials and config files. if err := createS3MountConfig(configPathWithVolID, s3); err != nil { klog.Errorf("MountpointS3Mounter Mount: Cannot create config file: %v", err) return err @@ -387,9 +422,6 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { klog.Info("NodeServer Mounting...") - // Use FuseMountWithEnv if available on the concrete type (race-safe, per-subprocess env). - // Falls back to FuseMount if running under a mock or different implementation β€” the - // fallback will still work but credentials must be available via system env in that case. if m, ok := s3.MounterUtils.(envMounter); ok { return m.FuseMountWithEnv(target, constants.MountpointS3BinaryPath, args, envVars) } @@ -437,17 +469,11 @@ func (s3 *MountpointS3Mounter) Unmount(target string) error { // credentials <- AWS credentials (access/secret key) // config <- AWS config (region, endpoint_url) func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) error { - // Create the config directory. - if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 -- directory is created under a controlled path using SHA256 hash, not user input + if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 klog.Errorf("MountpointS3Mounter: Cannot create config dir %s: %v", configPathWithVolID, err) return err } - // --- Write credentials file --- - // Format matches AWS credentials file: - // [default] - // aws_access_key_id = - // aws_secret_access_key = credFile := path.Join(configPathWithVolID, s3MountCredentialsFile) credParams := []string{ "[" + s3MountAwsProfile + "]", @@ -459,11 +485,6 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er return err } - // --- Write config file --- - // Format matches AWS config file: - // [default] - // region = - // endpoint_url = (for IBM COS compatibility) region := s3.LocConstraint configParams := []string{ "[" + s3MountAwsProfile + "]", @@ -484,7 +505,7 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er // writeConfigFile writes lines to a file with 0600 permissions. func writeConfigFile(filePath string, lines []string) error { - f, err := CreateFile(filePath) // #nosec G304 -- filePath is constructed internally from a SHA256 hash and constant config dir, not from user input + f, err := CreateFile(filePath) // #nosec G304 if err != nil { return fmt.Errorf("cannot create file %s: %w", filePath, err) } @@ -494,7 +515,7 @@ func writeConfigFile(filePath string, lines []string) error { } }() - if err := Chmod(filePath, 0600); err != nil { // #nosec G302 -- restrictive permissions (0600) are intentionally set for credential files + if err := Chmod(filePath, 0600); err != nil { // #nosec G302 return fmt.Errorf("cannot chmod file %s: %w", filePath, err) } @@ -507,22 +528,28 @@ func writeConfigFile(filePath string, lines []string) error { return w.Flush() } -// formulateMountOptions builds CLI args, env vars, and the worker payload for mount-s3. +// formulateMountOptions builds the CLI args, env vars, and worker payload for mount-s3. // -// Removed flags (do not exist in mount-s3): -// - --aws-config-dir β†’ replaced by AWS_SHARED_CREDENTIALS_FILE / AWS_CONFIG_FILE env vars -// - --log-level β†’ mapped to --debug / --debug-crt / --no-log -// - --umask β†’ use --dir-mode / --file-mode via MountOptions in StorageClass +// Only structured fields are emitted here. All other user-supplied flags arrive +// pre-formatted in s3.MountOptions and are appended at the end. +// +// By the time this is called, Mount() has already: +// - Cleared AllowOverwrite / IncrementalUpload if ReadOnly is set +// - Validated LogDirectory and CacheDir existence +// +// Flag ordering: +// 1. Positional args : bucket, mountpoint +// 2. Always-on : --allow-other +// 3. SecretMap-sourced : --endpoint-url, --region +// 4. Identity : --uid, --gid +// 5. Logging : --debug/--debug-crt/--no-log, --log-directory +// 6. Cache : --cache +// 7. Write flags : --allow-overwrite, --incremental-upload +// 8. Read-only : --read-only +// 9. User passthrough : s3.MountOptions (appended last β€” highest precedence) func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathWithVolID string) (nodeServerOp []string, envVars []string, workerNodeOp s3MounterArgs) { - // mount-s3 syntax: mount-s3 [flags] - nodeServerOp = []string{ - bucket, - target, - "--allow-other", - } + nodeServerOp = []string{bucket, target, "--allow-other"} - // AWS credentials via env vars β€” mount-s3 and the underlying AWS SDK reads these - // to locate the credentials and config files written by createS3MountConfig. credFilePath := path.Join(configPathWithVolID, s3MountCredentialsFile) cfgFilePath := path.Join(configPathWithVolID, s3MountConfigFile) envVars = []string{ @@ -536,33 +563,28 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW AwsConfigFile: cfgFilePath, } - // Endpoint (IBM COS endpoint). + // --- SecretMap-sourced --- if s3.EndPoint != "" { nodeServerOp = append(nodeServerOp, "--endpoint-url="+s3.EndPoint) workerNodeOp.EndpointURL = s3.EndPoint } - - // Region β€” passed as CLI flag AND written to config file for redundancy. if s3.LocConstraint != "" { nodeServerOp = append(nodeServerOp, "--region="+s3.LocConstraint) workerNodeOp.Region = s3.LocConstraint } - // UID. + // --- Identity --- if s3.UID != "" { nodeServerOp = append(nodeServerOp, "--uid="+s3.UID) workerNodeOp.UID = s3.UID } - - // GID. if s3.GID != "" { nodeServerOp = append(nodeServerOp, "--gid="+s3.GID) workerNodeOp.GID = s3.GID } - // Log level: mount-s3 has no --log-level flag. - // Valid mappings: "debug" β†’ --debug, "debug-crt" β†’ --debug-crt, "no-log" β†’ --no-log. - // Anything else (e.g. "warn", "info") is ignored β€” mount-s3 default logging applies. + // --- Log level (name translation) --- + // mount-s3 has no --log-level flag; each level maps to its own flag. switch s3.LogLevel { case "debug": nodeServerOp = append(nodeServerOp, "--debug") @@ -575,102 +597,45 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW workerNodeOp.LogLevel = "no-log" default: if s3.LogLevel != "" { - klog.Warningf("MountpointS3Mounter: unsupported log level '%s' for mount-s3. "+ - "Supported values: 'debug', 'debug-crt', 'no-log'. Ignoring.", s3.LogLevel) + klog.Warningf("MountpointS3Mounter: unsupported log-level %q. "+ + "Supported: 'debug', 'debug-crt', 'no-log'. Ignoring.", s3.LogLevel) } } - // Read-only. - if s3.ReadOnly { - nodeServerOp = append(nodeServerOp, "--read-only") - workerNodeOp.ReadOnly = "true" - } - - // Allow delete. - if s3.AllowDelete { - nodeServerOp = append(nodeServerOp, "--allow-delete") - workerNodeOp.AllowDelete = "true" - } - - // Allow overwrite. - if s3.AllowOverwrite { - nodeServerOp = append(nodeServerOp, "--allow-overwrite") - workerNodeOp.AllowOverwrite = "true" - } - - // Log directory. + // --- Log directory (existence validated in Mount()) --- if s3.LogDirectory != "" { nodeServerOp = append(nodeServerOp, "--log-directory="+s3.LogDirectory) workerNodeOp.LogDirectory = s3.LogDirectory } - // --- Performance tuning options --- - - // --read-part-size (default: 8 MiB). - if s3.ReadPartSize != "" { - nodeServerOp = append(nodeServerOp, "--read-part-size="+s3.ReadPartSize) - workerNodeOp.ReadPartSize = s3.ReadPartSize - } - - // --write-part-size (default: 8 MiB). - if s3.WritePartSize != "" { - nodeServerOp = append(nodeServerOp, "--write-part-size="+s3.WritePartSize) - workerNodeOp.WritePartSize = s3.WritePartSize - } - - // --max-threads (default: 16). - if s3.MaxThreads != "" { - nodeServerOp = append(nodeServerOp, "--max-threads="+s3.MaxThreads) - workerNodeOp.MaxThreads = s3.MaxThreads - } - - // --maximum-throughput-gbps (default: auto-detect on EC2, 10 Gbps elsewhere). - // Not on EC2 so won't auto-detect correctly β€” set explicitly if you know your node bandwidth. - if s3.MaxThroughputGbps != "" { - nodeServerOp = append(nodeServerOp, "--maximum-throughput-gbps="+s3.MaxThroughputGbps) - workerNodeOp.MaxThroughputGbps = s3.MaxThroughputGbps - } - - // --upload-checksums (default: crc32c). - // Set to "off" to reduce CPU overhead on write-heavy workloads at the cost of integrity checks. - if s3.UploadChecksums != "" { - nodeServerOp = append(nodeServerOp, "--upload-checksums="+s3.UploadChecksums) - workerNodeOp.UploadChecksums = s3.UploadChecksums - } - - // --cache + --max-cache-size (default: disabled). - // Enables local disk read cache β€” improves repeated read performance. - // When cache is set, metadata-ttl defaults to 60s automatically. + // --- Cache directory (existence validated in Mount()) --- if s3.CacheDir != "" { nodeServerOp = append(nodeServerOp, "--cache="+s3.CacheDir) workerNodeOp.CacheDir = s3.CacheDir - if s3.MaxCacheSize != "" { - nodeServerOp = append(nodeServerOp, "--max-cache-size="+s3.MaxCacheSize) - workerNodeOp.MaxCacheSize = s3.MaxCacheSize - } } - // --metadata-ttl (default: minimal, or 60s if --cache is set). - // Set higher to reduce ListObjects API calls on stable buckets. - if s3.MetadataTTL != "" { - nodeServerOp = append(nodeServerOp, "--metadata-ttl="+s3.MetadataTTL) - workerNodeOp.MetadataTTL = s3.MetadataTTL + // --- Write flags --- + // These are only emitted if ReadOnly is false. + // Mount() clears these when ReadOnly is set, so no conflict is possible here. + if s3.AllowOverwrite { + nodeServerOp = append(nodeServerOp, "--allow-overwrite") + workerNodeOp.AllowOverwrite = "true" } - - // --negative-metadata-ttl (default: same as metadata-ttl). - if s3.NegativeMetadataTTL != "" { - nodeServerOp = append(nodeServerOp, "--negative-metadata-ttl="+s3.NegativeMetadataTTL) - workerNodeOp.NegativeMetadataTTL = s3.NegativeMetadataTTL + if s3.IncrementalUpload { + nodeServerOp = append(nodeServerOp, "--incremental-upload") + workerNodeOp.IncrementalUpload = "true" } - // --log-metrics (default: false). - // Logs summarized throughput metrics β€” zero performance cost, useful for monitoring. - if s3.LogMetrics { - nodeServerOp = append(nodeServerOp, "--log-metrics") - workerNodeOp.LogMetrics = true + // --- Read-only --- + // Safe to emit here β€” Mount() already cleared AllowOverwrite and + // IncrementalUpload above, so no conflicting flags will be present. + if s3.ReadOnly { + nodeServerOp = append(nodeServerOp, "--read-only") + workerNodeOp.ReadOnly = "true" } - // StorageClass mountOptions are appended LAST β€” always take final precedence over everything above. + // --- User passthrough (appended last β€” highest precedence) --- + // All entries already have "--" prefix (enforced by parseMountpointS3Options). nodeServerOp = append(nodeServerOp, s3.MountOptions...) klog.Infof("MountpointS3 nodeServerOp: %v", nodeServerOp) @@ -679,7 +644,7 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW return nodeServerOp, envVars, workerNodeOp } -// removeS3MountConfigFile removes the config directory for the volume (same pattern as rclone). +// removeS3MountConfigFile removes the config directory for the given volume target. func removeS3MountConfigFile(configPath, target string) { configPathWithVolID := path.Join(configPath, fmt.Sprintf("%x", sha256.Sum256([]byte(target)))) From 393a48fd87c00188cf99c3a86ae0a6afa4590a64 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 30 Mar 2026 12:37:17 +0530 Subject: [PATCH 088/128] Unit-test for s3mounter --- pkg/mounter/mounter-s3mounter_test.go | 613 ++++++++++++++++++++++++++ 1 file changed, 613 insertions(+) create mode 100644 pkg/mounter/mounter-s3mounter_test.go diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go new file mode 100644 index 00000000..89266bad --- /dev/null +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -0,0 +1,613 @@ +package mounter + +import ( + "errors" + "os" + "testing" + + "github.com/IBM/ibm-object-csi-driver/pkg/constants" + mounterUtils "github.com/IBM/ibm-object-csi-driver/pkg/mounter/utils" + "github.com/stretchr/testify/assert" +) + +var ( + s3MounterSecretMap = map[string]string{ + "cosEndpoint": "https://s3.us-east.cloud-object-storage.appdomain.cloud", + "locationConstraint": "us-east", + "bucketName": "test-bucket", + "objectPath": "test-path", + "accessKey": "test-access-key", + "secretKey": "test-secret-key", + "uid": "1000", + "gid": "1000", + "umask": "0022", + "logLevel": "debug", + "readOnly": "true", + "maxThreads": "32", + "readPartSize": "16777216", + "writePartSize": "16777216", + "maximumThroughputGbps": "20", + "uploadChecksums": "crc32c", + "cacheDir": "/tmp/cache", + "maxCacheSize": "1024", + "metadataTTL": "60", + "negativeMetadataTTL": "30", + "logMetrics": "true", + } + + s3MounterMountOptions = []string{"--dir-mode=0755", "--file-mode=0644"} + source = "test-source" + target = "/mnt/test-target" +) + +func TestNewMountpointS3Mounter_Success(t *testing.T) { + mounter := NewMountpointS3Mounter(s3MounterSecretMap, s3MounterMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + + s3Mounter, ok := mounter.(*MountpointS3Mounter) + assert.True(t, ok) + + assert.Equal(t, s3MounterSecretMap["bucketName"], s3Mounter.BucketName) + assert.Equal(t, s3MounterSecretMap["objectPath"], s3Mounter.ObjectPath) + assert.Equal(t, s3MounterSecretMap["cosEndpoint"], s3Mounter.EndPoint) + assert.Equal(t, s3MounterSecretMap["locationConstraint"], s3Mounter.LocConstraint) + assert.Equal(t, s3MounterSecretMap["accessKey"], s3Mounter.AccessKey) + assert.Equal(t, s3MounterSecretMap["secretKey"], s3Mounter.SecretKey) + assert.Equal(t, s3MounterSecretMap["uid"], s3Mounter.UID) + assert.Equal(t, s3MounterSecretMap["gid"], s3Mounter.GID) + assert.Equal(t, s3MounterSecretMap["umask"], s3Mounter.UMask) + assert.Equal(t, s3MounterSecretMap["logLevel"], s3Mounter.LogLevel) + assert.True(t, s3Mounter.ReadOnly) + assert.Equal(t, "hmac", s3Mounter.AuthType) +} + +func TestNewMountpointS3Mounter_PerformanceTuning(t *testing.T) { + mounter := NewMountpointS3Mounter(s3MounterSecretMap, s3MounterMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + + s3Mounter, ok := mounter.(*MountpointS3Mounter) + assert.True(t, ok) + + assert.Equal(t, s3MounterSecretMap["maxThreads"], s3Mounter.MaxThreads) + assert.Equal(t, s3MounterSecretMap["readPartSize"], s3Mounter.ReadPartSize) + assert.Equal(t, s3MounterSecretMap["writePartSize"], s3Mounter.WritePartSize) + assert.Equal(t, s3MounterSecretMap["maximumThroughputGbps"], s3Mounter.MaxThroughputGbps) + assert.Equal(t, s3MounterSecretMap["uploadChecksums"], s3Mounter.UploadChecksums) + assert.Equal(t, s3MounterSecretMap["cacheDir"], s3Mounter.CacheDir) + assert.Equal(t, s3MounterSecretMap["maxCacheSize"], s3Mounter.MaxCacheSize) + assert.Equal(t, s3MounterSecretMap["metadataTTL"], s3Mounter.MetadataTTL) + assert.Equal(t, s3MounterSecretMap["negativeMetadataTTL"], s3Mounter.NegativeMetadataTTL) + assert.True(t, s3Mounter.LogMetrics) +} + +func TestNewMountpointS3Mounter_GidWithoutUid(t *testing.T) { + secretMap := map[string]string{ + "gid": "2000", + } + + mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + + s3Mounter, ok := mounter.(*MountpointS3Mounter) + assert.True(t, ok) + assert.Equal(t, "2000", s3Mounter.UID) + assert.Equal(t, "2000", s3Mounter.GID) +} + +func TestNewMountpointS3Mounter_UidOverridesGid(t *testing.T) { + secretMap := map[string]string{ + "uid": "1000", + "gid": "2000", + } + + mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + + s3Mounter, ok := mounter.(*MountpointS3Mounter) + assert.True(t, ok) + assert.Equal(t, "1000", s3Mounter.UID) + assert.Equal(t, "2000", s3Mounter.GID) +} + +func TestMountpointS3Mount_NodeServer_Positive(t *testing.T) { + mountWorker = false + + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return &os.File{}, nil + } + Chmod = func(path string, perm os.FileMode) error { + return nil + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseMountFn: func(path, comm string, args []string) error { + return nil + }, + }), + BucketName: "test-bucket", + ObjectPath: "test-path", + EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", + LocConstraint: "us-east", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + UID: "1000", + GID: "1000", + LogLevel: "debug", + ReadOnly: true, + MountOptions: s3MounterMountOptions, + } + + err := s3Mounter.Mount(source, target) + assert.NoError(t, err) +} + +func TestMountpointS3Mount_NodeServer_WithEnvMounter(t *testing.T) { + mountWorker = false + + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return &os.File{}, nil + } + Chmod = func(path string, perm os.FileMode) error { + return nil + } + + // Create a mock that implements envMounter interface + mockUtils := &mockEnvMounter{ + FakeMounterUtilsImpl: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mockUtils, + BucketName: "test-bucket", + EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", + LocConstraint: "us-east", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + MountOptions: s3MounterMountOptions, + } + + err := s3Mounter.Mount(source, target) + assert.NoError(t, err) +} + +func TestMountpointS3Mount_WorkerNode_Positive(t *testing.T) { + mountWorker = true + + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return &os.File{}, nil + } + Chmod = func(path string, perm os.FileMode) error { + return nil + } + mounterRequest = func(payload, url string) error { + return nil + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), + BucketName: "test-bucket", + EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", + LocConstraint: "us-east", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := s3Mounter.Mount(source, target) + assert.NoError(t, err) +} + +func TestMountpointS3Mount_CreateDirFails_Negative(t *testing.T) { + mountWorker = false + + MakeDir = func(path string, perm os.FileMode) error { + return errors.New("failed to create directory") + } + + s3Mounter := &MountpointS3Mounter{ + BucketName: "test-bucket", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := s3Mounter.Mount(source, target) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Cannot create config dir") +} + +func TestMountpointS3Mount_CreateCredFileFails_Negative(t *testing.T) { + mountWorker = false + + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return nil, errors.New("failed to create file") + } + + s3Mounter := &MountpointS3Mounter{ + BucketName: "test-bucket", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := s3Mounter.Mount(source, target) + assert.Error(t, err) + assert.Contains(t, err.Error(), "cannot create file") +} + +func TestMountpointS3Mount_WorkerNode_Negative(t *testing.T) { + mountWorker = true + + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return &os.File{}, nil + } + Chmod = func(path string, perm os.FileMode) error { + return nil + } + mounterRequest = func(payload, url string) error { + return errors.New("failed to perform http request") + } + + s3Mounter := &MountpointS3Mounter{ + BucketName: "test-bucket", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := s3Mounter.Mount(source, target) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to perform http request") +} + +func TestMountpointS3Unmount_NodeServer(t *testing.T) { + mountWorker = false + + removeS3ConfigFile = func(configPath, target string) {} + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseUnmountFn: func(path string) error { + return nil + }, + }), + } + + err := s3Mounter.Unmount(target) + assert.NoError(t, err) +} + +func TestMountpointS3Unmount_WorkerNode(t *testing.T) { + mountWorker = true + + removeS3ConfigFile = func(configPath, target string) {} + + mounterRequest = func(payload, url string) error { + return nil + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseUnmountFn: func(path string) error { + return nil + }, + }), + } + + err := s3Mounter.Unmount(target) + assert.NoError(t, err) +} + +func TestMountpointS3Unmount_WorkerNode_Negative(t *testing.T) { + mountWorker = true + + mounterRequest = func(payload, url string) error { + return errors.New("failed to create http request") + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseUnmountFn: func(path string) error { + return nil + }, + }), + } + + err := s3Mounter.Unmount(target) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to create http request") +} + +func TestMountpointS3Unmount_NodeServer_Negative(t *testing.T) { + mountWorker = false + + removeS3ConfigFile = func(configPath, target string) {} + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseUnmountFn: func(path string) error { + return errors.New("failed to unmount") + }, + }), + } + + err := s3Mounter.Unmount(target) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to unmount") +} + +func TestRemoveS3MountConfigFile_PathNotExists(t *testing.T) { + Stat = func(path string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } + removeS3MountConfigFile("/test", target) +} + +func TestRemoveS3MountConfigFile_StatRetryThenSuccess(t *testing.T) { + attempt := 0 + Stat = func(_ string) (os.FileInfo, error) { + if attempt == 0 { + attempt++ + return nil, errors.New("stat error") + } + return nil, nil + } + RemoveAll = func(_ string) error { + return nil + } + + removeS3MountConfigFile("/test", target) +} + +func TestRemoveS3MountConfigFile_RemoveRetryThenSuccess(t *testing.T) { + Stat = func(_ string) (os.FileInfo, error) { + return nil, nil + } + attempt := 0 + RemoveAll = func(_ string) error { + if attempt == 0 { + attempt++ + return errors.New("remove error") + } + return nil + } + + removeS3MountConfigFile("/test", target) +} + +func TestRemoveS3MountConfigFile_Negative(t *testing.T) { + called := 0 + Stat = func(_ string) (os.FileInfo, error) { + return nil, nil + } + RemoveAll = func(_ string) error { + called++ + return errors.New("remove failed") + } + + removeS3MountConfigFile("/test", target) + assert.Equal(t, maxRetries, called) +} + +func TestFormulateMountOptions_AllOptions(t *testing.T) { + s3Mounter := &MountpointS3Mounter{ + BucketName: "test-bucket", + ObjectPath: "test-path", + EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", + LocConstraint: "us-east", + UID: "1000", + GID: "2000", + UMask: "0022", + LogLevel: "debug", + ReadOnly: true, + MaxThreads: "32", + ReadPartSize: "16777216", + WritePartSize: "16777216", + MaxThroughputGbps: "20", + UploadChecksums: "crc32c", + CacheDir: "/tmp/cache", + MaxCacheSize: "1024", + MetadataTTL: "60", + NegativeMetadataTTL: "30", + LogMetrics: true, + MountOptions: []string{"--dir-mode=0755"}, + } + + nodeServerOp, envVars, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket/test-path", target, "/config/path") + + // Check node server options + assert.Contains(t, nodeServerOp, "test-bucket/test-path") + assert.Contains(t, nodeServerOp, target) + assert.Contains(t, nodeServerOp, "--allow-other") + assert.Contains(t, nodeServerOp, "--endpoint-url=https://s3.us-east.cloud-object-storage.appdomain.cloud") + assert.Contains(t, nodeServerOp, "--region=us-east") + assert.Contains(t, nodeServerOp, "--uid=1000") + assert.Contains(t, nodeServerOp, "--gid=2000") + assert.Contains(t, nodeServerOp, "--debug") + assert.Contains(t, nodeServerOp, "--read-only") + assert.Contains(t, nodeServerOp, "--max-threads=32") + assert.Contains(t, nodeServerOp, "--read-part-size=16777216") + assert.Contains(t, nodeServerOp, "--write-part-size=16777216") + assert.Contains(t, nodeServerOp, "--maximum-throughput-gbps=20") + assert.Contains(t, nodeServerOp, "--upload-checksums=crc32c") + assert.Contains(t, nodeServerOp, "--cache=/tmp/cache") + assert.Contains(t, nodeServerOp, "--max-cache-size=1024") + assert.Contains(t, nodeServerOp, "--metadata-ttl=60") + assert.Contains(t, nodeServerOp, "--negative-metadata-ttl=30") + assert.Contains(t, nodeServerOp, "--log-metrics") + assert.Contains(t, nodeServerOp, "--dir-mode=0755") + + // Check env vars + assert.Len(t, envVars, 2) + assert.Contains(t, envVars[0], "AWS_SHARED_CREDENTIALS_FILE=") + assert.Contains(t, envVars[1], "AWS_CONFIG_FILE=") + + // Check worker node options + assert.Equal(t, "true", workerNodeOp.AllowOther) + assert.Equal(t, "https://s3.us-east.cloud-object-storage.appdomain.cloud", workerNodeOp.EndpointURL) + assert.Equal(t, "us-east", workerNodeOp.Region) + assert.Equal(t, "1000", workerNodeOp.UID) + assert.Equal(t, "2000", workerNodeOp.GID) + assert.Equal(t, "debug", workerNodeOp.LogLevel) + assert.Equal(t, "true", workerNodeOp.ReadOnly) + assert.Equal(t, "32", workerNodeOp.MaxThreads) + assert.Equal(t, "16777216", workerNodeOp.ReadPartSize) + assert.Equal(t, "16777216", workerNodeOp.WritePartSize) + assert.Equal(t, "20", workerNodeOp.MaxThroughputGbps) + assert.Equal(t, "crc32c", workerNodeOp.UploadChecksums) + assert.Equal(t, "/tmp/cache", workerNodeOp.CacheDir) + assert.Equal(t, "1024", workerNodeOp.MaxCacheSize) + assert.Equal(t, "60", workerNodeOp.MetadataTTL) + assert.Equal(t, "30", workerNodeOp.NegativeMetadataTTL) + assert.True(t, workerNodeOp.LogMetrics) +} + +func TestFormulateMountOptions_LogLevelDebugCrt(t *testing.T) { + s3Mounter := &MountpointS3Mounter{ + LogLevel: "debug-crt", + } + + nodeServerOp, _, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") + + assert.Contains(t, nodeServerOp, "--debug-crt") + assert.Equal(t, "debug-crt", workerNodeOp.LogLevel) +} + +func TestFormulateMountOptions_LogLevelNoLog(t *testing.T) { + s3Mounter := &MountpointS3Mounter{ + LogLevel: "no-log", + } + + nodeServerOp, _, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") + + assert.Contains(t, nodeServerOp, "--no-log") + assert.Equal(t, "no-log", workerNodeOp.LogLevel) +} + +func TestFormulateMountOptions_InvalidLogLevel(t *testing.T) { + s3Mounter := &MountpointS3Mounter{ + LogLevel: "invalid-level", + } + + nodeServerOp, _, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") + + // Should not contain any log level flag + assert.NotContains(t, nodeServerOp, "--debug") + assert.NotContains(t, nodeServerOp, "--debug-crt") + assert.NotContains(t, nodeServerOp, "--no-log") + assert.Empty(t, workerNodeOp.LogLevel) +} + +func TestFormulateMountOptions_MinimalConfig(t *testing.T) { + s3Mounter := &MountpointS3Mounter{ + MountOptions: []string{}, + } + + nodeServerOp, envVars, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") + + // Check basic options are present + assert.Contains(t, nodeServerOp, "test-bucket") + assert.Contains(t, nodeServerOp, target) + assert.Contains(t, nodeServerOp, "--allow-other") + + // Check env vars + assert.Len(t, envVars, 2) + + // Check worker node options + assert.Equal(t, "true", workerNodeOp.AllowOther) +} + +func TestCreateS3MountConfig_Success(t *testing.T) { + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return &os.File{}, nil + } + Chmod = func(path string, perm os.FileMode) error { + return nil + } + + s3Mounter := &MountpointS3Mounter{ + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + LocConstraint: "us-east", + EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", + } + + err := createS3MountConfig("/config/path", s3Mounter) + assert.NoError(t, err) +} + +func TestCreateS3MountConfig_MakeDirFails(t *testing.T) { + MakeDir = func(path string, perm os.FileMode) error { + return errors.New("failed to create directory") + } + + s3Mounter := &MountpointS3Mounter{ + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := createS3MountConfig("/config/path", s3Mounter) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Cannot create config dir") +} + +func TestCreateS3MountConfig_CreateFileFails(t *testing.T) { + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return nil, errors.New("failed to create file") + } + + s3Mounter := &MountpointS3Mounter{ + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := createS3MountConfig("/config/path", s3Mounter) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Cannot write credentials file") +} + +func TestCreateS3MountConfig_ChmodFails(t *testing.T) { + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return &os.File{}, nil + } + Chmod = func(path string, perm os.FileMode) error { + return errors.New("failed to chmod") + } + + s3Mounter := &MountpointS3Mounter{ + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := createS3MountConfig("/config/path", s3Mounter) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Cannot write credentials file") +} + +// mockEnvMounter implements the envMounter interface for testing +type mockEnvMounter struct { + *mounterUtils.FakeMounterUtilsImpl +} + +func (m *mockEnvMounter) FuseMountWithEnv(path string, comm string, args []string, envVars []string) error { + return nil +} + +// Made with Bob From 1858e9e9e2afc4c077b3d9cdd6aba128ee51ba36 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 30 Mar 2026 13:00:18 +0530 Subject: [PATCH 089/128] Updated test-code --- pkg/mounter/mounter-s3mounter_test.go | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go index 89266bad..41bc8253 100644 --- a/pkg/mounter/mounter-s3mounter_test.go +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -4,8 +4,6 @@ import ( "errors" "os" "testing" - - "github.com/IBM/ibm-object-csi-driver/pkg/constants" mounterUtils "github.com/IBM/ibm-object-csi-driver/pkg/mounter/utils" "github.com/stretchr/testify/assert" ) @@ -20,7 +18,6 @@ var ( "secretKey": "test-secret-key", "uid": "1000", "gid": "1000", - "umask": "0022", "logLevel": "debug", "readOnly": "true", "maxThreads": "32", @@ -36,8 +33,6 @@ var ( } s3MounterMountOptions = []string{"--dir-mode=0755", "--file-mode=0644"} - source = "test-source" - target = "/mnt/test-target" ) func TestNewMountpointS3Mounter_Success(t *testing.T) { @@ -54,7 +49,6 @@ func TestNewMountpointS3Mounter_Success(t *testing.T) { assert.Equal(t, s3MounterSecretMap["secretKey"], s3Mounter.SecretKey) assert.Equal(t, s3MounterSecretMap["uid"], s3Mounter.UID) assert.Equal(t, s3MounterSecretMap["gid"], s3Mounter.GID) - assert.Equal(t, s3MounterSecretMap["umask"], s3Mounter.UMask) assert.Equal(t, s3MounterSecretMap["logLevel"], s3Mounter.LogLevel) assert.True(t, s3Mounter.ReadOnly) assert.Equal(t, "hmac", s3Mounter.AuthType) @@ -405,7 +399,6 @@ func TestFormulateMountOptions_AllOptions(t *testing.T) { LocConstraint: "us-east", UID: "1000", GID: "2000", - UMask: "0022", LogLevel: "debug", ReadOnly: true, MaxThreads: "32", @@ -603,11 +596,9 @@ func TestCreateS3MountConfig_ChmodFails(t *testing.T) { // mockEnvMounter implements the envMounter interface for testing type mockEnvMounter struct { - *mounterUtils.FakeMounterUtilsImpl + mounterUtils.MounterUtils } func (m *mockEnvMounter) FuseMountWithEnv(path string, comm string, args []string, envVars []string) error { return nil } - -// Made with Bob From 9383ee1ffe1c3262b716529115207f02723e2ee3 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 30 Mar 2026 13:08:36 +0530 Subject: [PATCH 090/128] updated code --- pkg/mounter/mounter-s3mounter_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go index 41bc8253..6ccd3435 100644 --- a/pkg/mounter/mounter-s3mounter_test.go +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -150,7 +150,7 @@ func TestMountpointS3Mount_NodeServer_WithEnvMounter(t *testing.T) { // Create a mock that implements envMounter interface mockUtils := &mockEnvMounter{ - FakeMounterUtilsImpl: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), } s3Mounter := &MountpointS3Mounter{ From 4a6a2e1f014cba4efaa8cc012d875558b203afb2 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 30 Mar 2026 13:41:39 +0530 Subject: [PATCH 091/128] Change os file to temp file creation --- pkg/mounter/mounter-s3mounter_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go index 6ccd3435..07a125d2 100644 --- a/pkg/mounter/mounter-s3mounter_test.go +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -106,7 +106,7 @@ func TestMountpointS3Mount_NodeServer_Positive(t *testing.T) { return nil } CreateFile = func(path string) (*os.File, error) { - return &os.File{}, nil + return os.CreateTemp("", "test") } Chmod = func(path string, perm os.FileMode) error { return nil @@ -142,7 +142,7 @@ func TestMountpointS3Mount_NodeServer_WithEnvMounter(t *testing.T) { return nil } CreateFile = func(path string) (*os.File, error) { - return &os.File{}, nil + return os.CreateTemp("", "test") } Chmod = func(path string, perm os.FileMode) error { return nil @@ -174,7 +174,7 @@ func TestMountpointS3Mount_WorkerNode_Positive(t *testing.T) { return nil } CreateFile = func(path string) (*os.File, error) { - return &os.File{}, nil + return os.CreateTemp("", "test") } Chmod = func(path string, perm os.FileMode) error { return nil @@ -242,7 +242,7 @@ func TestMountpointS3Mount_WorkerNode_Negative(t *testing.T) { return nil } CreateFile = func(path string) (*os.File, error) { - return &os.File{}, nil + return os.CreateTemp("", "test") } Chmod = func(path string, perm os.FileMode) error { return nil @@ -523,7 +523,7 @@ func TestCreateS3MountConfig_Success(t *testing.T) { return nil } CreateFile = func(path string) (*os.File, error) { - return &os.File{}, nil + return os.CreateTemp("", "test") } Chmod = func(path string, perm os.FileMode) error { return nil @@ -578,7 +578,7 @@ func TestCreateS3MountConfig_ChmodFails(t *testing.T) { return nil } CreateFile = func(path string) (*os.File, error) { - return &os.File{}, nil + return os.CreateTemp("", "test") } Chmod = func(path string, perm os.FileMode) error { return errors.New("failed to chmod") From 5162f2434e6c10bdf517f17c8f154ba74dc99f41 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 30 Mar 2026 13:49:14 +0530 Subject: [PATCH 092/128] Update variables detail --- pkg/mounter/mounter-s3mounter_test.go | 40 +++++++++++++-------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go index 07a125d2..b0d8e56c 100644 --- a/pkg/mounter/mounter-s3mounter_test.go +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -10,26 +10,26 @@ import ( var ( s3MounterSecretMap = map[string]string{ - "cosEndpoint": "https://s3.us-east.cloud-object-storage.appdomain.cloud", - "locationConstraint": "us-east", - "bucketName": "test-bucket", - "objectPath": "test-path", - "accessKey": "test-access-key", - "secretKey": "test-secret-key", - "uid": "1000", - "gid": "1000", - "logLevel": "debug", - "readOnly": "true", - "maxThreads": "32", - "readPartSize": "16777216", - "writePartSize": "16777216", - "maximumThroughputGbps": "20", - "uploadChecksums": "crc32c", - "cacheDir": "/tmp/cache", - "maxCacheSize": "1024", - "metadataTTL": "60", - "negativeMetadataTTL": "30", - "logMetrics": "true", + "cosEndpoint": "https://s3.us-east.cloud-object-storage.appdomain.cloud", + "locationConstraint": "us-east", + "bucketName": "test-bucket", + "objectPath": "test-path", + "accessKey": "test-access-key", + "secretKey": "test-secret-key", + "uid": "1000", + "gid": "1000", + "mountOptions": `log-level=debug +read-only +max-threads=32 +read-part-size=16777216 +write-part-size=16777216 +maximum-throughput-gbps=20 +upload-checksums=crc32c +cache=/tmp/cache +max-cache-size=1024 +metadata-ttl=60 +negative-metadata-ttl=30 +log-metrics`, } s3MounterMountOptions = []string{"--dir-mode=0755", "--file-mode=0644"} From 7c9891795abe3f2401d1a46f498201944816d4c0 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 30 Mar 2026 14:03:29 +0530 Subject: [PATCH 093/128] Remove assertion checks for some map variables --- pkg/mounter/mounter-s3mounter_test.go | 35 +++++++++++++++------------ 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go index b0d8e56c..9f84c834 100644 --- a/pkg/mounter/mounter-s3mounter_test.go +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -49,7 +49,7 @@ func TestNewMountpointS3Mounter_Success(t *testing.T) { assert.Equal(t, s3MounterSecretMap["secretKey"], s3Mounter.SecretKey) assert.Equal(t, s3MounterSecretMap["uid"], s3Mounter.UID) assert.Equal(t, s3MounterSecretMap["gid"], s3Mounter.GID) - assert.Equal(t, s3MounterSecretMap["logLevel"], s3Mounter.LogLevel) + assert.Equal(t, "debug", s3Mounter.LogLevel) assert.True(t, s3Mounter.ReadOnly) assert.Equal(t, "hmac", s3Mounter.AuthType) } @@ -60,15 +60,15 @@ func TestNewMountpointS3Mounter_PerformanceTuning(t *testing.T) { s3Mounter, ok := mounter.(*MountpointS3Mounter) assert.True(t, ok) - assert.Equal(t, s3MounterSecretMap["maxThreads"], s3Mounter.MaxThreads) - assert.Equal(t, s3MounterSecretMap["readPartSize"], s3Mounter.ReadPartSize) - assert.Equal(t, s3MounterSecretMap["writePartSize"], s3Mounter.WritePartSize) - assert.Equal(t, s3MounterSecretMap["maximumThroughputGbps"], s3Mounter.MaxThroughputGbps) - assert.Equal(t, s3MounterSecretMap["uploadChecksums"], s3Mounter.UploadChecksums) - assert.Equal(t, s3MounterSecretMap["cacheDir"], s3Mounter.CacheDir) - assert.Equal(t, s3MounterSecretMap["maxCacheSize"], s3Mounter.MaxCacheSize) - assert.Equal(t, s3MounterSecretMap["metadataTTL"], s3Mounter.MetadataTTL) - assert.Equal(t, s3MounterSecretMap["negativeMetadataTTL"], s3Mounter.NegativeMetadataTTL) + assert.Equal(t, "32", s3Mounter.MaxThreads) + assert.Equal(t, "16777216", s3Mounter.ReadPartSize) + assert.Equal(t, "16777216", s3Mounter.WritePartSize) + assert.Equal(t, "20", s3Mounter.MaxThroughputGbps) + assert.Equal(t, "crc32c", s3Mounter.UploadChecksums) + assert.Equal(t, "/tmp/cache", s3Mounter.CacheDir) + assert.Equal(t, "1024", s3Mounter.MaxCacheSize) + assert.Equal(t, "60", s3Mounter.MetadataTTL) + assert.Equal(t, "30", s3Mounter.NegativeMetadataTTL) assert.True(t, s3Mounter.LogMetrics) } @@ -112,12 +112,17 @@ func TestMountpointS3Mount_NodeServer_Positive(t *testing.T) { return nil } - s3Mounter := &MountpointS3Mounter{ + // Create a mock that implements envMounter interface + mockUtils := &mockEnvMounter{ MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ FuseMountFn: func(path, comm string, args []string) error { return nil }, }), + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mockUtils, BucketName: "test-bucket", ObjectPath: "test-path", EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", @@ -211,7 +216,7 @@ func TestMountpointS3Mount_CreateDirFails_Negative(t *testing.T) { err := s3Mounter.Mount(source, target) assert.Error(t, err) - assert.Contains(t, err.Error(), "Cannot create config dir") + assert.Contains(t, err.Error(), "failed to create directory") } func TestMountpointS3Mount_CreateCredFileFails_Negative(t *testing.T) { @@ -552,7 +557,7 @@ func TestCreateS3MountConfig_MakeDirFails(t *testing.T) { err := createS3MountConfig("/config/path", s3Mounter) assert.Error(t, err) - assert.Contains(t, err.Error(), "Cannot create config dir") + assert.Contains(t, err.Error(), "failed to create directory") } func TestCreateS3MountConfig_CreateFileFails(t *testing.T) { @@ -570,7 +575,7 @@ func TestCreateS3MountConfig_CreateFileFails(t *testing.T) { err := createS3MountConfig("/config/path", s3Mounter) assert.Error(t, err) - assert.Contains(t, err.Error(), "Cannot write credentials file") + assert.Contains(t, err.Error(), "failed to create file") } func TestCreateS3MountConfig_ChmodFails(t *testing.T) { @@ -591,7 +596,7 @@ func TestCreateS3MountConfig_ChmodFails(t *testing.T) { err := createS3MountConfig("/config/path", s3Mounter) assert.Error(t, err) - assert.Contains(t, err.Error(), "Cannot write credentials file") + assert.Contains(t, err.Error(), "failed to chmod") } // mockEnvMounter implements the envMounter interface for testing From 4279e68242543b01ad9063bd67741be193fbce5c Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Mon, 6 Apr 2026 00:47:40 +0530 Subject: [PATCH 094/128] publish 0.12.19 version --- pkg/mounter/mounter-s3mounter_test.go | 113 ++++++++++++-------------- 1 file changed, 51 insertions(+), 62 deletions(-) diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go index 9f84c834..b5643c3d 100644 --- a/pkg/mounter/mounter-s3mounter_test.go +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -54,22 +54,25 @@ func TestNewMountpointS3Mounter_Success(t *testing.T) { assert.Equal(t, "hmac", s3Mounter.AuthType) } -func TestNewMountpointS3Mounter_PerformanceTuning(t *testing.T) { +func TestNewMountpointS3Mounter_PassthroughOptions(t *testing.T) { mounter := NewMountpointS3Mounter(s3MounterSecretMap, s3MounterMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) s3Mounter, ok := mounter.(*MountpointS3Mounter) assert.True(t, ok) - assert.Equal(t, "32", s3Mounter.MaxThreads) - assert.Equal(t, "16777216", s3Mounter.ReadPartSize) - assert.Equal(t, "16777216", s3Mounter.WritePartSize) - assert.Equal(t, "20", s3Mounter.MaxThroughputGbps) - assert.Equal(t, "crc32c", s3Mounter.UploadChecksums) + // Verify passthrough options are preserved with -- prefix + assert.Contains(t, s3Mounter.MountOptions, "--max-threads=32") + assert.Contains(t, s3Mounter.MountOptions, "--read-part-size=16777216") + assert.Contains(t, s3Mounter.MountOptions, "--write-part-size=16777216") + assert.Contains(t, s3Mounter.MountOptions, "--maximum-throughput-gbps=20") + assert.Contains(t, s3Mounter.MountOptions, "--upload-checksums=crc32c") + assert.Contains(t, s3Mounter.MountOptions, "--max-cache-size=1024") + assert.Contains(t, s3Mounter.MountOptions, "--metadata-ttl=60") + assert.Contains(t, s3Mounter.MountOptions, "--negative-metadata-ttl=30") + assert.Contains(t, s3Mounter.MountOptions, "--log-metrics") + + // Verify structured fields are set correctly assert.Equal(t, "/tmp/cache", s3Mounter.CacheDir) - assert.Equal(t, "1024", s3Mounter.MaxCacheSize) - assert.Equal(t, "60", s3Mounter.MetadataTTL) - assert.Equal(t, "30", s3Mounter.NegativeMetadataTTL) - assert.True(t, s3Mounter.LogMetrics) } func TestNewMountpointS3Mounter_GidWithoutUid(t *testing.T) { @@ -111,18 +114,16 @@ func TestMountpointS3Mount_NodeServer_Positive(t *testing.T) { Chmod = func(path string, perm os.FileMode) error { return nil } + Stat = func(path string) (os.FileInfo, error) { + return nil, nil + } - // Create a mock that implements envMounter interface - mockUtils := &mockEnvMounter{ + s3Mounter := &MountpointS3Mounter{ MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ FuseMountFn: func(path, comm string, args []string) error { return nil }, }), - } - - s3Mounter := &MountpointS3Mounter{ - MounterUtils: mockUtils, BucketName: "test-bucket", ObjectPath: "test-path", EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", @@ -152,14 +153,12 @@ func TestMountpointS3Mount_NodeServer_WithEnvMounter(t *testing.T) { Chmod = func(path string, perm os.FileMode) error { return nil } - - // Create a mock that implements envMounter interface - mockUtils := &mockEnvMounter{ - MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), + Stat = func(path string) (os.FileInfo, error) { + return nil, nil } s3Mounter := &MountpointS3Mounter{ - MounterUtils: mockUtils, + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), BucketName: "test-bucket", EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", LocConstraint: "us-east", @@ -398,30 +397,33 @@ func TestRemoveS3MountConfigFile_Negative(t *testing.T) { func TestFormulateMountOptions_AllOptions(t *testing.T) { s3Mounter := &MountpointS3Mounter{ - BucketName: "test-bucket", - ObjectPath: "test-path", - EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", - LocConstraint: "us-east", - UID: "1000", - GID: "2000", - LogLevel: "debug", - ReadOnly: true, - MaxThreads: "32", - ReadPartSize: "16777216", - WritePartSize: "16777216", - MaxThroughputGbps: "20", - UploadChecksums: "crc32c", - CacheDir: "/tmp/cache", - MaxCacheSize: "1024", - MetadataTTL: "60", - NegativeMetadataTTL: "30", - LogMetrics: true, - MountOptions: []string{"--dir-mode=0755"}, + BucketName: "test-bucket", + ObjectPath: "test-path", + EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", + LocConstraint: "us-east", + UID: "1000", + GID: "2000", + LogLevel: "debug", + LogDirectory: "/var/log/mount-s3", + CacheDir: "/tmp/cache", + ReadOnly: true, + MountOptions: []string{ + "--dir-mode=0755", + "--max-threads=32", + "--read-part-size=16777216", + "--write-part-size=16777216", + "--maximum-throughput-gbps=20", + "--upload-checksums=crc32c", + "--max-cache-size=1024", + "--metadata-ttl=60", + "--negative-metadata-ttl=30", + "--log-metrics", + }, } nodeServerOp, envVars, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket/test-path", target, "/config/path") - // Check node server options + // Check node server options - structured fields assert.Contains(t, nodeServerOp, "test-bucket/test-path") assert.Contains(t, nodeServerOp, target) assert.Contains(t, nodeServerOp, "--allow-other") @@ -430,42 +432,37 @@ func TestFormulateMountOptions_AllOptions(t *testing.T) { assert.Contains(t, nodeServerOp, "--uid=1000") assert.Contains(t, nodeServerOp, "--gid=2000") assert.Contains(t, nodeServerOp, "--debug") + assert.Contains(t, nodeServerOp, "--log-directory=/var/log/mount-s3") + assert.Contains(t, nodeServerOp, "--cache=/tmp/cache") assert.Contains(t, nodeServerOp, "--read-only") + + // Check node server options - passthrough fields + assert.Contains(t, nodeServerOp, "--dir-mode=0755") assert.Contains(t, nodeServerOp, "--max-threads=32") assert.Contains(t, nodeServerOp, "--read-part-size=16777216") assert.Contains(t, nodeServerOp, "--write-part-size=16777216") assert.Contains(t, nodeServerOp, "--maximum-throughput-gbps=20") assert.Contains(t, nodeServerOp, "--upload-checksums=crc32c") - assert.Contains(t, nodeServerOp, "--cache=/tmp/cache") assert.Contains(t, nodeServerOp, "--max-cache-size=1024") assert.Contains(t, nodeServerOp, "--metadata-ttl=60") assert.Contains(t, nodeServerOp, "--negative-metadata-ttl=30") assert.Contains(t, nodeServerOp, "--log-metrics") - assert.Contains(t, nodeServerOp, "--dir-mode=0755") // Check env vars assert.Len(t, envVars, 2) assert.Contains(t, envVars[0], "AWS_SHARED_CREDENTIALS_FILE=") assert.Contains(t, envVars[1], "AWS_CONFIG_FILE=") - // Check worker node options + // Check worker node options - only structured fields are in the struct assert.Equal(t, "true", workerNodeOp.AllowOther) assert.Equal(t, "https://s3.us-east.cloud-object-storage.appdomain.cloud", workerNodeOp.EndpointURL) assert.Equal(t, "us-east", workerNodeOp.Region) assert.Equal(t, "1000", workerNodeOp.UID) assert.Equal(t, "2000", workerNodeOp.GID) assert.Equal(t, "debug", workerNodeOp.LogLevel) - assert.Equal(t, "true", workerNodeOp.ReadOnly) - assert.Equal(t, "32", workerNodeOp.MaxThreads) - assert.Equal(t, "16777216", workerNodeOp.ReadPartSize) - assert.Equal(t, "16777216", workerNodeOp.WritePartSize) - assert.Equal(t, "20", workerNodeOp.MaxThroughputGbps) - assert.Equal(t, "crc32c", workerNodeOp.UploadChecksums) + assert.Equal(t, "/var/log/mount-s3", workerNodeOp.LogDirectory) assert.Equal(t, "/tmp/cache", workerNodeOp.CacheDir) - assert.Equal(t, "1024", workerNodeOp.MaxCacheSize) - assert.Equal(t, "60", workerNodeOp.MetadataTTL) - assert.Equal(t, "30", workerNodeOp.NegativeMetadataTTL) - assert.True(t, workerNodeOp.LogMetrics) + assert.Equal(t, "true", workerNodeOp.ReadOnly) } func TestFormulateMountOptions_LogLevelDebugCrt(t *testing.T) { @@ -599,11 +596,3 @@ func TestCreateS3MountConfig_ChmodFails(t *testing.T) { assert.Contains(t, err.Error(), "failed to chmod") } -// mockEnvMounter implements the envMounter interface for testing -type mockEnvMounter struct { - mounterUtils.MounterUtils -} - -func (m *mockEnvMounter) FuseMountWithEnv(path string, comm string, args []string, envVars []string) error { - return nil -} From 31525355750173e16c60acdfb866c30ec25073cf Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Mon, 6 Apr 2026 00:59:12 +0530 Subject: [PATCH 095/128] publish 0.12.19 version --- pkg/mounter/mounter-s3mounter_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go index b5643c3d..69560655 100644 --- a/pkg/mounter/mounter-s3mounter_test.go +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -121,6 +121,7 @@ func TestMountpointS3Mount_NodeServer_Positive(t *testing.T) { s3Mounter := &MountpointS3Mounter{ MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ FuseMountFn: func(path, comm string, args []string) error { + // Mock successful mount return nil }, }), @@ -158,7 +159,12 @@ func TestMountpointS3Mount_NodeServer_WithEnvMounter(t *testing.T) { } s3Mounter := &MountpointS3Mounter{ - MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseMountFn: func(path, comm string, args []string) error { + // Mock successful mount with env vars + return nil + }, + }), BucketName: "test-bucket", EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", LocConstraint: "us-east", From d2046c2d45aaaab32321a6406285085e75ebd4c7 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Mon, 6 Apr 2026 10:28:15 +0530 Subject: [PATCH 096/128] publish 0.12.19 version --- pkg/mounter/mounter-s3mounter_test.go | 604 -------------------------- 1 file changed, 604 deletions(-) delete mode 100644 pkg/mounter/mounter-s3mounter_test.go diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go deleted file mode 100644 index 69560655..00000000 --- a/pkg/mounter/mounter-s3mounter_test.go +++ /dev/null @@ -1,604 +0,0 @@ -package mounter - -import ( - "errors" - "os" - "testing" - mounterUtils "github.com/IBM/ibm-object-csi-driver/pkg/mounter/utils" - "github.com/stretchr/testify/assert" -) - -var ( - s3MounterSecretMap = map[string]string{ - "cosEndpoint": "https://s3.us-east.cloud-object-storage.appdomain.cloud", - "locationConstraint": "us-east", - "bucketName": "test-bucket", - "objectPath": "test-path", - "accessKey": "test-access-key", - "secretKey": "test-secret-key", - "uid": "1000", - "gid": "1000", - "mountOptions": `log-level=debug -read-only -max-threads=32 -read-part-size=16777216 -write-part-size=16777216 -maximum-throughput-gbps=20 -upload-checksums=crc32c -cache=/tmp/cache -max-cache-size=1024 -metadata-ttl=60 -negative-metadata-ttl=30 -log-metrics`, - } - - s3MounterMountOptions = []string{"--dir-mode=0755", "--file-mode=0644"} -) - -func TestNewMountpointS3Mounter_Success(t *testing.T) { - mounter := NewMountpointS3Mounter(s3MounterSecretMap, s3MounterMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) - - s3Mounter, ok := mounter.(*MountpointS3Mounter) - assert.True(t, ok) - - assert.Equal(t, s3MounterSecretMap["bucketName"], s3Mounter.BucketName) - assert.Equal(t, s3MounterSecretMap["objectPath"], s3Mounter.ObjectPath) - assert.Equal(t, s3MounterSecretMap["cosEndpoint"], s3Mounter.EndPoint) - assert.Equal(t, s3MounterSecretMap["locationConstraint"], s3Mounter.LocConstraint) - assert.Equal(t, s3MounterSecretMap["accessKey"], s3Mounter.AccessKey) - assert.Equal(t, s3MounterSecretMap["secretKey"], s3Mounter.SecretKey) - assert.Equal(t, s3MounterSecretMap["uid"], s3Mounter.UID) - assert.Equal(t, s3MounterSecretMap["gid"], s3Mounter.GID) - assert.Equal(t, "debug", s3Mounter.LogLevel) - assert.True(t, s3Mounter.ReadOnly) - assert.Equal(t, "hmac", s3Mounter.AuthType) -} - -func TestNewMountpointS3Mounter_PassthroughOptions(t *testing.T) { - mounter := NewMountpointS3Mounter(s3MounterSecretMap, s3MounterMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) - - s3Mounter, ok := mounter.(*MountpointS3Mounter) - assert.True(t, ok) - - // Verify passthrough options are preserved with -- prefix - assert.Contains(t, s3Mounter.MountOptions, "--max-threads=32") - assert.Contains(t, s3Mounter.MountOptions, "--read-part-size=16777216") - assert.Contains(t, s3Mounter.MountOptions, "--write-part-size=16777216") - assert.Contains(t, s3Mounter.MountOptions, "--maximum-throughput-gbps=20") - assert.Contains(t, s3Mounter.MountOptions, "--upload-checksums=crc32c") - assert.Contains(t, s3Mounter.MountOptions, "--max-cache-size=1024") - assert.Contains(t, s3Mounter.MountOptions, "--metadata-ttl=60") - assert.Contains(t, s3Mounter.MountOptions, "--negative-metadata-ttl=30") - assert.Contains(t, s3Mounter.MountOptions, "--log-metrics") - - // Verify structured fields are set correctly - assert.Equal(t, "/tmp/cache", s3Mounter.CacheDir) -} - -func TestNewMountpointS3Mounter_GidWithoutUid(t *testing.T) { - secretMap := map[string]string{ - "gid": "2000", - } - - mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) - - s3Mounter, ok := mounter.(*MountpointS3Mounter) - assert.True(t, ok) - assert.Equal(t, "2000", s3Mounter.UID) - assert.Equal(t, "2000", s3Mounter.GID) -} - -func TestNewMountpointS3Mounter_UidOverridesGid(t *testing.T) { - secretMap := map[string]string{ - "uid": "1000", - "gid": "2000", - } - - mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) - - s3Mounter, ok := mounter.(*MountpointS3Mounter) - assert.True(t, ok) - assert.Equal(t, "1000", s3Mounter.UID) - assert.Equal(t, "2000", s3Mounter.GID) -} - -func TestMountpointS3Mount_NodeServer_Positive(t *testing.T) { - mountWorker = false - - MakeDir = func(path string, perm os.FileMode) error { - return nil - } - CreateFile = func(path string) (*os.File, error) { - return os.CreateTemp("", "test") - } - Chmod = func(path string, perm os.FileMode) error { - return nil - } - Stat = func(path string) (os.FileInfo, error) { - return nil, nil - } - - s3Mounter := &MountpointS3Mounter{ - MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ - FuseMountFn: func(path, comm string, args []string) error { - // Mock successful mount - return nil - }, - }), - BucketName: "test-bucket", - ObjectPath: "test-path", - EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", - LocConstraint: "us-east", - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - UID: "1000", - GID: "1000", - LogLevel: "debug", - ReadOnly: true, - MountOptions: s3MounterMountOptions, - } - - err := s3Mounter.Mount(source, target) - assert.NoError(t, err) -} - -func TestMountpointS3Mount_NodeServer_WithEnvMounter(t *testing.T) { - mountWorker = false - - MakeDir = func(path string, perm os.FileMode) error { - return nil - } - CreateFile = func(path string) (*os.File, error) { - return os.CreateTemp("", "test") - } - Chmod = func(path string, perm os.FileMode) error { - return nil - } - Stat = func(path string) (os.FileInfo, error) { - return nil, nil - } - - s3Mounter := &MountpointS3Mounter{ - MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ - FuseMountFn: func(path, comm string, args []string) error { - // Mock successful mount with env vars - return nil - }, - }), - BucketName: "test-bucket", - EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", - LocConstraint: "us-east", - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - MountOptions: s3MounterMountOptions, - } - - err := s3Mounter.Mount(source, target) - assert.NoError(t, err) -} - -func TestMountpointS3Mount_WorkerNode_Positive(t *testing.T) { - mountWorker = true - - MakeDir = func(path string, perm os.FileMode) error { - return nil - } - CreateFile = func(path string) (*os.File, error) { - return os.CreateTemp("", "test") - } - Chmod = func(path string, perm os.FileMode) error { - return nil - } - mounterRequest = func(payload, url string) error { - return nil - } - - s3Mounter := &MountpointS3Mounter{ - MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), - BucketName: "test-bucket", - EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", - LocConstraint: "us-east", - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - } - - err := s3Mounter.Mount(source, target) - assert.NoError(t, err) -} - -func TestMountpointS3Mount_CreateDirFails_Negative(t *testing.T) { - mountWorker = false - - MakeDir = func(path string, perm os.FileMode) error { - return errors.New("failed to create directory") - } - - s3Mounter := &MountpointS3Mounter{ - BucketName: "test-bucket", - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - } - - err := s3Mounter.Mount(source, target) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to create directory") -} - -func TestMountpointS3Mount_CreateCredFileFails_Negative(t *testing.T) { - mountWorker = false - - MakeDir = func(path string, perm os.FileMode) error { - return nil - } - CreateFile = func(path string) (*os.File, error) { - return nil, errors.New("failed to create file") - } - - s3Mounter := &MountpointS3Mounter{ - BucketName: "test-bucket", - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - } - - err := s3Mounter.Mount(source, target) - assert.Error(t, err) - assert.Contains(t, err.Error(), "cannot create file") -} - -func TestMountpointS3Mount_WorkerNode_Negative(t *testing.T) { - mountWorker = true - - MakeDir = func(path string, perm os.FileMode) error { - return nil - } - CreateFile = func(path string) (*os.File, error) { - return os.CreateTemp("", "test") - } - Chmod = func(path string, perm os.FileMode) error { - return nil - } - mounterRequest = func(payload, url string) error { - return errors.New("failed to perform http request") - } - - s3Mounter := &MountpointS3Mounter{ - BucketName: "test-bucket", - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - } - - err := s3Mounter.Mount(source, target) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to perform http request") -} - -func TestMountpointS3Unmount_NodeServer(t *testing.T) { - mountWorker = false - - removeS3ConfigFile = func(configPath, target string) {} - - s3Mounter := &MountpointS3Mounter{ - MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ - FuseUnmountFn: func(path string) error { - return nil - }, - }), - } - - err := s3Mounter.Unmount(target) - assert.NoError(t, err) -} - -func TestMountpointS3Unmount_WorkerNode(t *testing.T) { - mountWorker = true - - removeS3ConfigFile = func(configPath, target string) {} - - mounterRequest = func(payload, url string) error { - return nil - } - - s3Mounter := &MountpointS3Mounter{ - MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ - FuseUnmountFn: func(path string) error { - return nil - }, - }), - } - - err := s3Mounter.Unmount(target) - assert.NoError(t, err) -} - -func TestMountpointS3Unmount_WorkerNode_Negative(t *testing.T) { - mountWorker = true - - mounterRequest = func(payload, url string) error { - return errors.New("failed to create http request") - } - - s3Mounter := &MountpointS3Mounter{ - MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ - FuseUnmountFn: func(path string) error { - return nil - }, - }), - } - - err := s3Mounter.Unmount(target) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to create http request") -} - -func TestMountpointS3Unmount_NodeServer_Negative(t *testing.T) { - mountWorker = false - - removeS3ConfigFile = func(configPath, target string) {} - - s3Mounter := &MountpointS3Mounter{ - MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ - FuseUnmountFn: func(path string) error { - return errors.New("failed to unmount") - }, - }), - } - - err := s3Mounter.Unmount(target) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to unmount") -} - -func TestRemoveS3MountConfigFile_PathNotExists(t *testing.T) { - Stat = func(path string) (os.FileInfo, error) { - return nil, os.ErrNotExist - } - removeS3MountConfigFile("/test", target) -} - -func TestRemoveS3MountConfigFile_StatRetryThenSuccess(t *testing.T) { - attempt := 0 - Stat = func(_ string) (os.FileInfo, error) { - if attempt == 0 { - attempt++ - return nil, errors.New("stat error") - } - return nil, nil - } - RemoveAll = func(_ string) error { - return nil - } - - removeS3MountConfigFile("/test", target) -} - -func TestRemoveS3MountConfigFile_RemoveRetryThenSuccess(t *testing.T) { - Stat = func(_ string) (os.FileInfo, error) { - return nil, nil - } - attempt := 0 - RemoveAll = func(_ string) error { - if attempt == 0 { - attempt++ - return errors.New("remove error") - } - return nil - } - - removeS3MountConfigFile("/test", target) -} - -func TestRemoveS3MountConfigFile_Negative(t *testing.T) { - called := 0 - Stat = func(_ string) (os.FileInfo, error) { - return nil, nil - } - RemoveAll = func(_ string) error { - called++ - return errors.New("remove failed") - } - - removeS3MountConfigFile("/test", target) - assert.Equal(t, maxRetries, called) -} - -func TestFormulateMountOptions_AllOptions(t *testing.T) { - s3Mounter := &MountpointS3Mounter{ - BucketName: "test-bucket", - ObjectPath: "test-path", - EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", - LocConstraint: "us-east", - UID: "1000", - GID: "2000", - LogLevel: "debug", - LogDirectory: "/var/log/mount-s3", - CacheDir: "/tmp/cache", - ReadOnly: true, - MountOptions: []string{ - "--dir-mode=0755", - "--max-threads=32", - "--read-part-size=16777216", - "--write-part-size=16777216", - "--maximum-throughput-gbps=20", - "--upload-checksums=crc32c", - "--max-cache-size=1024", - "--metadata-ttl=60", - "--negative-metadata-ttl=30", - "--log-metrics", - }, - } - - nodeServerOp, envVars, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket/test-path", target, "/config/path") - - // Check node server options - structured fields - assert.Contains(t, nodeServerOp, "test-bucket/test-path") - assert.Contains(t, nodeServerOp, target) - assert.Contains(t, nodeServerOp, "--allow-other") - assert.Contains(t, nodeServerOp, "--endpoint-url=https://s3.us-east.cloud-object-storage.appdomain.cloud") - assert.Contains(t, nodeServerOp, "--region=us-east") - assert.Contains(t, nodeServerOp, "--uid=1000") - assert.Contains(t, nodeServerOp, "--gid=2000") - assert.Contains(t, nodeServerOp, "--debug") - assert.Contains(t, nodeServerOp, "--log-directory=/var/log/mount-s3") - assert.Contains(t, nodeServerOp, "--cache=/tmp/cache") - assert.Contains(t, nodeServerOp, "--read-only") - - // Check node server options - passthrough fields - assert.Contains(t, nodeServerOp, "--dir-mode=0755") - assert.Contains(t, nodeServerOp, "--max-threads=32") - assert.Contains(t, nodeServerOp, "--read-part-size=16777216") - assert.Contains(t, nodeServerOp, "--write-part-size=16777216") - assert.Contains(t, nodeServerOp, "--maximum-throughput-gbps=20") - assert.Contains(t, nodeServerOp, "--upload-checksums=crc32c") - assert.Contains(t, nodeServerOp, "--max-cache-size=1024") - assert.Contains(t, nodeServerOp, "--metadata-ttl=60") - assert.Contains(t, nodeServerOp, "--negative-metadata-ttl=30") - assert.Contains(t, nodeServerOp, "--log-metrics") - - // Check env vars - assert.Len(t, envVars, 2) - assert.Contains(t, envVars[0], "AWS_SHARED_CREDENTIALS_FILE=") - assert.Contains(t, envVars[1], "AWS_CONFIG_FILE=") - - // Check worker node options - only structured fields are in the struct - assert.Equal(t, "true", workerNodeOp.AllowOther) - assert.Equal(t, "https://s3.us-east.cloud-object-storage.appdomain.cloud", workerNodeOp.EndpointURL) - assert.Equal(t, "us-east", workerNodeOp.Region) - assert.Equal(t, "1000", workerNodeOp.UID) - assert.Equal(t, "2000", workerNodeOp.GID) - assert.Equal(t, "debug", workerNodeOp.LogLevel) - assert.Equal(t, "/var/log/mount-s3", workerNodeOp.LogDirectory) - assert.Equal(t, "/tmp/cache", workerNodeOp.CacheDir) - assert.Equal(t, "true", workerNodeOp.ReadOnly) -} - -func TestFormulateMountOptions_LogLevelDebugCrt(t *testing.T) { - s3Mounter := &MountpointS3Mounter{ - LogLevel: "debug-crt", - } - - nodeServerOp, _, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") - - assert.Contains(t, nodeServerOp, "--debug-crt") - assert.Equal(t, "debug-crt", workerNodeOp.LogLevel) -} - -func TestFormulateMountOptions_LogLevelNoLog(t *testing.T) { - s3Mounter := &MountpointS3Mounter{ - LogLevel: "no-log", - } - - nodeServerOp, _, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") - - assert.Contains(t, nodeServerOp, "--no-log") - assert.Equal(t, "no-log", workerNodeOp.LogLevel) -} - -func TestFormulateMountOptions_InvalidLogLevel(t *testing.T) { - s3Mounter := &MountpointS3Mounter{ - LogLevel: "invalid-level", - } - - nodeServerOp, _, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") - - // Should not contain any log level flag - assert.NotContains(t, nodeServerOp, "--debug") - assert.NotContains(t, nodeServerOp, "--debug-crt") - assert.NotContains(t, nodeServerOp, "--no-log") - assert.Empty(t, workerNodeOp.LogLevel) -} - -func TestFormulateMountOptions_MinimalConfig(t *testing.T) { - s3Mounter := &MountpointS3Mounter{ - MountOptions: []string{}, - } - - nodeServerOp, envVars, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") - - // Check basic options are present - assert.Contains(t, nodeServerOp, "test-bucket") - assert.Contains(t, nodeServerOp, target) - assert.Contains(t, nodeServerOp, "--allow-other") - - // Check env vars - assert.Len(t, envVars, 2) - - // Check worker node options - assert.Equal(t, "true", workerNodeOp.AllowOther) -} - -func TestCreateS3MountConfig_Success(t *testing.T) { - MakeDir = func(path string, perm os.FileMode) error { - return nil - } - CreateFile = func(path string) (*os.File, error) { - return os.CreateTemp("", "test") - } - Chmod = func(path string, perm os.FileMode) error { - return nil - } - - s3Mounter := &MountpointS3Mounter{ - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - LocConstraint: "us-east", - EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", - } - - err := createS3MountConfig("/config/path", s3Mounter) - assert.NoError(t, err) -} - -func TestCreateS3MountConfig_MakeDirFails(t *testing.T) { - MakeDir = func(path string, perm os.FileMode) error { - return errors.New("failed to create directory") - } - - s3Mounter := &MountpointS3Mounter{ - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - } - - err := createS3MountConfig("/config/path", s3Mounter) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to create directory") -} - -func TestCreateS3MountConfig_CreateFileFails(t *testing.T) { - MakeDir = func(path string, perm os.FileMode) error { - return nil - } - CreateFile = func(path string) (*os.File, error) { - return nil, errors.New("failed to create file") - } - - s3Mounter := &MountpointS3Mounter{ - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - } - - err := createS3MountConfig("/config/path", s3Mounter) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to create file") -} - -func TestCreateS3MountConfig_ChmodFails(t *testing.T) { - MakeDir = func(path string, perm os.FileMode) error { - return nil - } - CreateFile = func(path string) (*os.File, error) { - return os.CreateTemp("", "test") - } - Chmod = func(path string, perm os.FileMode) error { - return errors.New("failed to chmod") - } - - s3Mounter := &MountpointS3Mounter{ - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - } - - err := createS3MountConfig("/config/path", s3Mounter) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to chmod") -} - From 6a8a19c7f389e0b095cdac9ad431fee2eface79d Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Tue, 7 Apr 2026 00:55:00 +0530 Subject: [PATCH 097/128] publish 0.12.20 version --- pkg/mounter/mounter_s3mounter.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index c526e335..815d42c9 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -469,7 +469,7 @@ func (s3 *MountpointS3Mounter) Unmount(target string) error { // credentials <- AWS credentials (access/secret key) // config <- AWS config (region, endpoint_url) func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) error { - if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 + if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 -- 0755 permissions required for AWS config directory access klog.Errorf("MountpointS3Mounter: Cannot create config dir %s: %v", configPathWithVolID, err) return err } @@ -505,7 +505,7 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er // writeConfigFile writes lines to a file with 0600 permissions. func writeConfigFile(filePath string, lines []string) error { - f, err := CreateFile(filePath) // #nosec G304 + f, err := CreateFile(filePath) // #nosec G304 -- filePath is constructed from validated volume ID and config directory if err != nil { return fmt.Errorf("cannot create file %s: %w", filePath, err) } @@ -515,7 +515,7 @@ func writeConfigFile(filePath string, lines []string) error { } }() - if err := Chmod(filePath, 0600); err != nil { // #nosec G302 + if err := Chmod(filePath, 0600); err != nil { // #nosec G302 -- 0600 permissions required to protect AWS credentials file return fmt.Errorf("cannot chmod file %s: %w", filePath, err) } From 3ee69fdbfeb5dceda3181e08e76e58133eece354 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sat, 11 Apr 2026 03:32:12 +0530 Subject: [PATCH 098/128] publish 0.12.21 version --- pkg/mounter/mounter_s3mounter.go | 174 +++++-------------------------- 1 file changed, 25 insertions(+), 149 deletions(-) diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 815d42c9..07ee66fe 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -20,7 +20,7 @@ import ( // // Only flags that require code-level handling are modelled as struct fields: // - Conflict resolution : ReadOnly takes priority β€” clears AllowOverwrite / IncrementalUpload -// - Directory validation: LogDirectory / CacheDir (must exist before mount) +// - Directory creation : LogDirectory / CacheDir are created by the worker's Validate() // - Name translation : LogLevel ("debug" β†’ --debug, not --log-level=debug) // - Special precedence : UID / GID (secretMap < mountOptions) // - Always-emitted : allow-other (deduplicated here) @@ -51,12 +51,10 @@ type MountpointS3Mounter struct { // mount-s3 has no --log-level flag. LogLevel string - // LogDirectory must exist on the node before mount-s3 starts. - // Validated in Mount() before calling formulateMountOptions. + // LogDirectory is forwarded to the worker; worker's Validate() creates it via ensureDir. LogDirectory string - // CacheDir must exist on the node before mount-s3 starts. - // Validated in Mount() before calling formulateMountOptions. + // CacheDir is forwarded to the worker; worker's Validate() creates it via ensureDir. CacheDir string // Conflict resolution β€” ReadOnly takes priority over write flags. @@ -75,8 +73,9 @@ type MountpointS3Mounter struct { } // s3MounterArgs holds the args sent to the worker node daemon for mount-s3. -// Only structured fields are represented here; passthrough flags are embedded -// in the Args slice at the call site. +// Structured fields cover all named flags; Args carries any remaining +// passthrough flags (e.g. --allow-delete, --hello, --never=true). +// This struct must stay in sync with s3MounterArgs in the worker (cos-csi-mounter). type s3MounterArgs struct { // Always set. AllowOther string `json:"allow-other,omitempty"` @@ -102,6 +101,11 @@ type s3MounterArgs struct { ReadOnly string `json:"read-only,omitempty"` AllowOverwrite string `json:"allow-overwrite,omitempty"` IncrementalUpload string `json:"incremental-upload,omitempty"` + + // Passthrough flags β€” user-supplied flags not handled by structured fields + // (e.g. --allow-delete, --hello, --never=true). + // Worker's PopulateArgsSlice appends these last so they have highest precedence. + Args []string `json:"args,omitempty"` } const ( @@ -123,21 +127,6 @@ type envMounter interface { // NewMountpointS3Mounter creates a new MountpointS3Mounter from the unified // secret format. -// -// Source mapping and precedence (lowest β†’ highest): -// -// 1. secretMap identity keys β€” accessKey, secretKey, bucketName, objectPath, -// cosEndpoint, locationConstraint, uid, gid -// 2. StorageClass mountOptions (mountOptions []string arg) β€” parsed first -// 3. Secret mountOptions (secretMap["mountOptions"] multiline string) β€” parsed -// second and therefore wins over StorageClass on any overlapping flag -// -// Special case: if read-only is present in either source, it takes priority and -// clears allow-overwrite and incremental-upload regardless of where they came from. -// -// For passthrough flags (unrecognised by the structured parser), the same -// precedence applies: secret passthroughs are appended after SC passthroughs, -// so mount-s3 sees the secret value last (last-value-wins semantics). func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounterUtils utils.MounterUtils) Mounter { klog.Info("-newMountpointS3Mounter-") @@ -178,14 +167,9 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounter.AuthType = "hmac" // --- Merge and parse mountOptions --- - // Step 1: StorageClass mountOptions (already split by kubelet). scRemainder := parseMountpointS3Options(mounter, mountOptions) - - // Step 2: Secret mountOptions (multiline string, parsed after SC so it wins). secretOpts := splitSecretMountOptions(secretMap["mountOptions"]) secretRemainder := parseMountpointS3Options(mounter, secretOpts) - - // Step 3: SC passthroughs first, secret passthroughs last (last-value-wins). mounter.MountOptions = append(scRemainder, secretRemainder...) mounter.MounterUtils = mounterUtils @@ -197,14 +181,7 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, return mounter } -// splitSecretMountOptions splits the multiline mountOptions string from the -// secret into a []string suitable for parseMountpointS3Options. -// -// Rules: -// - Each non-empty line is one flag. -// - Lines beginning with '#' (after trimming) are comments β€” skipped. -// - Leading/trailing whitespace on each line is trimmed. -// - Blank lines are skipped. +// splitSecretMountOptions splits the multiline mountOptions string from the secret. func splitSecretMountOptions(raw string) []string { if raw == "" { return nil @@ -221,49 +198,19 @@ func splitSecretMountOptions(raw string) []string { } // parseMountpointS3Options parses mount-s3 options from the provided slice. -// -// Only flags that require code-level handling are matched by name β€” everything -// else is forwarded unchanged to mount-s3 via the returned remainder slice. -// -// The remainder always has a "--" prefix added when missing, so mount-s3 -// receives syntactically valid flags regardless of how the user wrote them. -// This handles both boolean flags ("allow-delete" β†’ "--allow-delete") and -// key-value pairs ("max-threads=32" β†’ "--max-threads=32") correctly because -// the original opt string is preserved and only the prefix is fixed. -// -// Rules: -// - Flags may be provided with or without a leading "--" prefix. -// - Boolean flags need no value. -// - Value flags must be in "flag=value" or "--flag=value" form. -// - "allow-other" is silently consumed β€” formulateMountOptions always adds it. -// - "umask" is consumed with a warning β€” mount-s3 has no --umask flag. -// - uid/gid override secretMap values (caller sets secretMap values first). func parseMountpointS3Options(mounter *MountpointS3Mounter, opts []string) []string { var remaining []string for _, opt := range opts { - // Normalise: strip optional leading "--" so switch cases are uniform. trimmed := strings.TrimPrefix(opt, "--") key, value, hasValue := strings.Cut(trimmed, "=") switch key { - - // ------------------------------------------------------------------ - // Always emitted β€” consume to avoid duplicates. - // ------------------------------------------------------------------ case "allow-other": // formulateMountOptions unconditionally adds --allow-other. - - // ------------------------------------------------------------------ - // Invalid for mount-s3 β€” warn and drop so mount-s3 doesn't fail. - // ------------------------------------------------------------------ case "umask": klog.Warningf("parseMountpointS3Options: 'umask' is not supported by mount-s3. " + "Use --dir-mode / --file-mode instead. Ignoring.") - - // ------------------------------------------------------------------ - // Identity β€” mountOptions values override secretMap values. - // ------------------------------------------------------------------ case "uid": if hasValue { mounter.UID = value @@ -272,20 +219,10 @@ func parseMountpointS3Options(mounter *MountpointS3Mounter, opts []string) []str if hasValue { mounter.GID = value } - - // ------------------------------------------------------------------ - // Log level β€” requires name translation in formulateMountOptions. - // mount-s3 has no --log-level flag; valid values map to separate flags: - // "debug" β†’ --debug, "debug-crt" β†’ --debug-crt, "no-log" β†’ --no-log - // ------------------------------------------------------------------ case "log-level": if hasValue { mounter.LogLevel = value } - - // ------------------------------------------------------------------ - // Directory flags β€” validated for existence before mount-s3 starts. - // ------------------------------------------------------------------ case "log-directory": if hasValue { mounter.LogDirectory = value @@ -294,26 +231,12 @@ func parseMountpointS3Options(mounter *MountpointS3Mounter, opts []string) []str if hasValue { mounter.CacheDir = value } - - // ------------------------------------------------------------------ - // Conflict trio β€” tracked for read-only priority resolution in Mount(). - // read-only clears allow-overwrite and incremental-upload if all three - // are set, so SC defaults don't block users from requesting read-only. - // ------------------------------------------------------------------ case "read-only": mounter.ReadOnly = true case "allow-overwrite": mounter.AllowOverwrite = true case "incremental-upload": mounter.IncrementalUpload = true - - // ------------------------------------------------------------------ - // Everything else β€” pass through to mount-s3 unchanged. - // Ensure "--" prefix so mount-s3 always receives a valid flag. - // Works for both boolean flags and key-value pairs: - // "allow-delete" β†’ "--allow-delete" - // "max-threads=32" β†’ "--max-threads=32" - // ------------------------------------------------------------------ default: if strings.HasPrefix(opt, "--") { remaining = append(remaining, opt) @@ -332,11 +255,6 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { klog.Infof("Mount args:\n\tsource: <%s>\n\ttarget: <%s>", source, target) // --- Read-only priority resolution --- - // If read-only is set, it takes priority over all write-related flags. - // AllowOverwrite and IncrementalUpload are cleared here, and allow-delete - // is filtered from MountOptions, so the mount succeeds cleanly in read-only mode. - // This means users can always request read-only via their secret even when - // the StorageClass has write flags as defaults. if s3.ReadOnly { if s3.AllowOverwrite { klog.Infof("MountpointS3Mounter: read-only set β€” clearing allow-overwrite") @@ -347,7 +265,6 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { s3.IncrementalUpload = false } - // Filter out allow-delete from passthrough options var filtered []string for _, opt := range s3.MountOptions { trimmed := strings.TrimPrefix(opt, "--") @@ -360,21 +277,9 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { s3.MountOptions = filtered } - // --- Directory validation --- - // mount-s3 fails with a confusing error if these directories don't exist. - // Validate here to surface a clear error message before spawning the process. - if s3.LogDirectory != "" { - if _, err := Stat(s3.LogDirectory); err != nil { - return fmt.Errorf("MountpointS3Mounter: log-directory %q does not exist or is not accessible: %w", - s3.LogDirectory, err) - } - } - if s3.CacheDir != "" { - if _, err := Stat(s3.CacheDir); err != nil { - return fmt.Errorf("MountpointS3Mounter: cache directory %q does not exist or is not accessible: %w", - s3.CacheDir, err) - } - } + // NOTE: No directory validation or creation here. + // LogDirectory and CacheDir are forwarded to the worker via workerNodeOp. + // The worker's Validate() calls ensureDir() which creates them if missing. // --- Config path --- var configPath string @@ -391,7 +296,6 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { return err } - // Build bucket path with optional object path prefix. bucketName := s3.BucketName if s3.ObjectPath != "" { trimmedPath := strings.TrimPrefix(s3.ObjectPath, "/") @@ -412,6 +316,8 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { payload := fmt.Sprintf(`{"path":"%s","bucket":"%s","mounter":"%s","args":%s}`, target, bucketName, constants.AMAZONS3MOUNTER, jsonData) + klog.Infof("MountpointS3Mounter: worker payload: %s", payload) + err = mounterRequest(payload, "http://unix/api/cos/mount") if err != nil { klog.Errorf("failed to mount on worker: %v", err) @@ -426,8 +332,7 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { return m.FuseMountWithEnv(target, constants.MountpointS3BinaryPath, args, envVars) } - klog.Warning("MounterUtils does not implement FuseMountWithEnv, falling back to FuseMount. " + - "Ensure AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE are set in the process environment.") + klog.Warning("MounterUtils does not implement FuseMountWithEnv, falling back to FuseMount.") return s3.MounterUtils.FuseMount(target, constants.MountpointS3BinaryPath, args) } @@ -461,15 +366,8 @@ func (s3 *MountpointS3Mounter) Unmount(target string) error { } // createS3MountConfig creates the AWS credentials and config files. -// mount-s3 locates these via AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE env vars. -// -// Directory structure: -// -// / -// credentials <- AWS credentials (access/secret key) -// config <- AWS config (region, endpoint_url) func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) error { - if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 -- 0755 permissions required for AWS config directory access + if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 -- config directory needs to be readable by mount-s3 process klog.Errorf("MountpointS3Mounter: Cannot create config dir %s: %v", configPathWithVolID, err) return err } @@ -505,7 +403,7 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er // writeConfigFile writes lines to a file with 0600 permissions. func writeConfigFile(filePath string, lines []string) error { - f, err := CreateFile(filePath) // #nosec G304 -- filePath is constructed from validated volume ID and config directory + f, err := CreateFile(filePath) // #nosec G304 -- filePath is constructed from validated volume ID and predefined config file names if err != nil { return fmt.Errorf("cannot create file %s: %w", filePath, err) } @@ -515,7 +413,7 @@ func writeConfigFile(filePath string, lines []string) error { } }() - if err := Chmod(filePath, 0600); err != nil { // #nosec G302 -- 0600 permissions required to protect AWS credentials file + if err := Chmod(filePath, 0600); err != nil { // #nosec G302 -- credentials file must be readable only by owner for security return fmt.Errorf("cannot chmod file %s: %w", filePath, err) } @@ -529,24 +427,6 @@ func writeConfigFile(filePath string, lines []string) error { } // formulateMountOptions builds the CLI args, env vars, and worker payload for mount-s3. -// -// Only structured fields are emitted here. All other user-supplied flags arrive -// pre-formatted in s3.MountOptions and are appended at the end. -// -// By the time this is called, Mount() has already: -// - Cleared AllowOverwrite / IncrementalUpload if ReadOnly is set -// - Validated LogDirectory and CacheDir existence -// -// Flag ordering: -// 1. Positional args : bucket, mountpoint -// 2. Always-on : --allow-other -// 3. SecretMap-sourced : --endpoint-url, --region -// 4. Identity : --uid, --gid -// 5. Logging : --debug/--debug-crt/--no-log, --log-directory -// 6. Cache : --cache -// 7. Write flags : --allow-overwrite, --incremental-upload -// 8. Read-only : --read-only -// 9. User passthrough : s3.MountOptions (appended last β€” highest precedence) func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathWithVolID string) (nodeServerOp []string, envVars []string, workerNodeOp s3MounterArgs) { nodeServerOp = []string{bucket, target, "--allow-other"} @@ -584,7 +464,6 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW } // --- Log level (name translation) --- - // mount-s3 has no --log-level flag; each level maps to its own flag. switch s3.LogLevel { case "debug": nodeServerOp = append(nodeServerOp, "--debug") @@ -602,21 +481,19 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW } } - // --- Log directory (existence validated in Mount()) --- + // --- Log directory --- if s3.LogDirectory != "" { nodeServerOp = append(nodeServerOp, "--log-directory="+s3.LogDirectory) workerNodeOp.LogDirectory = s3.LogDirectory } - // --- Cache directory (existence validated in Mount()) --- + // --- Cache directory --- if s3.CacheDir != "" { nodeServerOp = append(nodeServerOp, "--cache="+s3.CacheDir) workerNodeOp.CacheDir = s3.CacheDir } // --- Write flags --- - // These are only emitted if ReadOnly is false. - // Mount() clears these when ReadOnly is set, so no conflict is possible here. if s3.AllowOverwrite { nodeServerOp = append(nodeServerOp, "--allow-overwrite") workerNodeOp.AllowOverwrite = "true" @@ -627,16 +504,15 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW } // --- Read-only --- - // Safe to emit here β€” Mount() already cleared AllowOverwrite and - // IncrementalUpload above, so no conflicting flags will be present. if s3.ReadOnly { nodeServerOp = append(nodeServerOp, "--read-only") workerNodeOp.ReadOnly = "true" } // --- User passthrough (appended last β€” highest precedence) --- - // All entries already have "--" prefix (enforced by parseMountpointS3Options). + // Set on BOTH paths so the worker daemon receives passthrough flags too. nodeServerOp = append(nodeServerOp, s3.MountOptions...) + workerNodeOp.Args = s3.MountOptions klog.Infof("MountpointS3 nodeServerOp: %v", nodeServerOp) klog.Infof("MountpointS3 workerNodeOp: %+v", workerNodeOp) From ab62b9025ffd0edf2bddbeb1413c8e7688068ce8 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sat, 11 Apr 2026 15:07:38 +0530 Subject: [PATCH 099/128] publish 0.12.22 version --- cos-csi-mounter/server/s3mounter.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go index 77bf07ec..191bfe56 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/s3mounter.go @@ -33,6 +33,11 @@ type s3MounterArgs struct { ReadOnly string `json:"read-only,omitempty"` AllowOverwrite string `json:"allow-overwrite,omitempty"` IncrementalUpload string `json:"incremental-upload,omitempty"` + + // Passthrough flags β€” user-supplied flags not handled by structured fields + // (e.g. --allow-delete, --hello, --never=true). + // PopulateArgsSlice appends these last so they have highest precedence. + Args []string `json:"args,omitempty"` } // PopulateArgsSlice builds the CLI args slice for mount-s3. @@ -127,6 +132,11 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string result = append(result, "--cache="+args.CacheDir) } + // --- User passthrough flags (appended last β€” highest precedence) --- + // These are flags like --allow-delete, --hello, --never=true that aren't + // handled by the structured fields above. + result = append(result, args.Args...) + return result, nil } From 643025d57131a64fc0dc52a9da8d3b6c7f0c9d66 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Mon, 13 Apr 2026 00:29:52 +0530 Subject: [PATCH 100/128] publish 0.12.23 version --- cos-csi-mounter/server/s3mounter.go | 66 +++--------- pkg/mounter/mounter_s3mounter.go | 159 ++++++++++++++-------------- 2 files changed, 98 insertions(+), 127 deletions(-) diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go index 191bfe56..613e3a66 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/s3mounter.go @@ -34,25 +34,25 @@ type s3MounterArgs struct { AllowOverwrite string `json:"allow-overwrite,omitempty"` IncrementalUpload string `json:"incremental-upload,omitempty"` - // Passthrough flags β€” user-supplied flags not handled by structured fields - // (e.g. --allow-delete, --hello, --never=true). - // PopulateArgsSlice appends these last so they have highest precedence. + // Passthrough flags β€” user-supplied flags not handled by structured fields. + // Secret flags override SC flags when the same key appears in both. + // These are already deduplicated by the node server before being sent here. Args []string `json:"args,omitempty"` } // PopulateArgsSlice builds the CLI args slice for mount-s3. -// -// Key rules: -// - AwsCredentialsFile and AwsConfigFile are NOT CLI flags β€” use EnvVars() for those -// - Boolean flags (--allow-other, --read-only) are emitted without a value -// because mount-s3 does not accept --allow-other=true syntax -// - LogLevel maps to --debug / --debug-crt / --no-log (mount-s3 has no --log-level) func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string, error) { result := []string{bucket, targetPath} - // --- Read-only priority resolution --- - // If read-only is set, clear all write-related flags to ensure clean read-only mount. - // This prevents conflicts where both read-only and write flags are set. + // Block incremental-upload flag (causes data loss with IBM COS). + // This flag uses multiple PutObject calls that overwrite each other, + // resulting in ~75% data loss. + if args.IncrementalUpload == "true" { + logger.Warn("incremental-upload is not supported with IBM Cloud Object Storage, disabling") + args.IncrementalUpload = "" + } + + // Read-only priority resolution. if args.ReadOnly == "true" { if args.AllowOverwrite == "true" { logger.Warn("read-only is set, clearing allow-overwrite") @@ -64,48 +64,31 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string } } - // --allow-other: boolean flag, no value if args.AllowOther == "true" { result = append(result, "--allow-other") } - - // --read-only: boolean flag, no value if args.ReadOnly == "true" { result = append(result, "--read-only") } - - // --allow-overwrite: boolean flag, no value (cleared if read-only is set) if args.AllowOverwrite == "true" { result = append(result, "--allow-overwrite") } - - // --incremental-upload: boolean flag, no value (cleared if read-only is set) if args.IncrementalUpload == "true" { result = append(result, "--incremental-upload") } - - // --uid if args.UID != "" { result = append(result, "--uid="+args.UID) } - - // --gid if args.GID != "" { result = append(result, "--gid="+args.GID) } - - // --endpoint-url if args.EndpointURL != "" { result = append(result, "--endpoint-url="+args.EndpointURL) } - - // --region if args.Region != "" { result = append(result, "--region="+args.Region) } - // Log level: mount-s3 has no --log-level flag. - // Map to the supported flags only. switch args.LogLevel { case "debug": result = append(result, "--debug") @@ -122,27 +105,20 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string } } - // --log-directory if args.LogDirectory != "" { result = append(result, "--log-directory="+args.LogDirectory) } - - // --cache if args.CacheDir != "" { result = append(result, "--cache="+args.CacheDir) } - // --- User passthrough flags (appended last β€” highest precedence) --- - // These are flags like --allow-delete, --hello, --never=true that aren't - // handled by the structured fields above. + // Passthrough flags appended last β€” already deduplicated by node server. result = append(result, args.Args...) return result, nil } -// EnvVars returns the environment variables that must be set on the mount-s3 -// subprocess so it can locate the AWS credentials and config files. -// The caller must add these to cmd.Env before cmd.Start(). +// EnvVars returns the environment variables for the mount-s3 subprocess. func (args s3MounterArgs) EnvVars() []string { var envVars []string if args.AwsCredentialsFile != "" { @@ -160,7 +136,6 @@ func (args s3MounterArgs) Validate(targetPath string) error { return err } - // allow-other must be a boolean string if set if args.AllowOther != "" { if isBool := isBoolString(args.AllowOther); !isBool { logger.Error("cannot convert value of allow-other into boolean", zap.Any("allow-other", args.AllowOther)) @@ -168,34 +143,27 @@ func (args s3MounterArgs) Validate(targetPath string) error { } } - // AwsCredentialsFile and AwsConfigFile replace the old AwsConfigDir check if args.AwsCredentialsFile == "" { logger.Error("aws-credentials-file is required for mount-s3") return fmt.Errorf("aws-credentials-file is required for mount-s3") } - if args.AwsConfigFile == "" { logger.Error("aws-config-file is required for mount-s3") return fmt.Errorf("aws-config-file is required for mount-s3") } - // read-only must be a boolean string if set if args.ReadOnly != "" { if isBool := isBoolString(args.ReadOnly); !isBool { logger.Error("cannot convert value of read-only into boolean", zap.Any("read-only", args.ReadOnly)) return fmt.Errorf("cannot convert value of read-only into boolean: %v", args.ReadOnly) } } - - // allow-overwrite must be a boolean string if set if args.AllowOverwrite != "" { if isBool := isBoolString(args.AllowOverwrite); !isBool { logger.Error("cannot convert value of allow-overwrite into boolean", zap.Any("allow-overwrite", args.AllowOverwrite)) return fmt.Errorf("cannot convert value of allow-overwrite into boolean: %v", args.AllowOverwrite) } } - - // incremental-upload must be a boolean string if set if args.IncrementalUpload != "" { if isBool := isBoolString(args.IncrementalUpload); !isBool { logger.Error("cannot convert value of incremental-upload into boolean", zap.Any("incremental-upload", args.IncrementalUpload)) @@ -203,15 +171,13 @@ func (args s3MounterArgs) Validate(targetPath string) error { } } - // Ensure cache directory exists if specified + // ensureDir creates the directory if it does not exist. if args.CacheDir != "" { if err := ensureDir(args.CacheDir); err != nil { logger.Error("failed to create cache directory", zap.String("cache-dir", args.CacheDir), zap.Error(err)) return fmt.Errorf("failed to create cache directory '%s': %w", args.CacheDir, err) } } - - // Ensure log directory exists if specified if args.LogDirectory != "" { if err := ensureDir(args.LogDirectory); err != nil { logger.Error("failed to create log directory", zap.String("log-dir", args.LogDirectory), zap.Error(err)) @@ -220,4 +186,4 @@ func (args s3MounterArgs) Validate(targetPath string) error { } return nil -} +} \ No newline at end of file diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 07ee66fe..fd2cc4e8 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -17,21 +17,7 @@ import ( ) // MountpointS3Mounter implements Mounter for AWS Mountpoint S3. -// -// Only flags that require code-level handling are modelled as struct fields: -// - Conflict resolution : ReadOnly takes priority β€” clears AllowOverwrite / IncrementalUpload -// - Directory creation : LogDirectory / CacheDir are created by the worker's Validate() -// - Name translation : LogLevel ("debug" β†’ --debug, not --log-level=debug) -// - Special precedence : UID / GID (secretMap < mountOptions) -// - Always-emitted : allow-other (deduplicated here) -// - Invalid flag : umask (warn + drop) -// - SecretMap-sourced : EndPoint / LocConstraint (injected from secret fields) -// -// All other flags are the user's responsibility and pass through unchanged to -// mount-s3 via MountOptions. The parser ensures every passthrough entry has a -// "--" prefix so mount-s3 always receives syntactically valid flags. type MountpointS3Mounter struct { - // Identity β€” sourced from secretMap, not mountOptions. BucketName string ObjectPath string EndPoint string @@ -40,71 +26,47 @@ type MountpointS3Mounter struct { AccessKey string SecretKey string - // UID / GID β€” secretMap sets initial value; mountOptions can override. UID string GID string - // LogLevel requires name translation: - // "debug" β†’ --debug - // "debug-crt" β†’ --debug-crt - // "no-log" β†’ --no-log - // mount-s3 has no --log-level flag. - LogLevel string - - // LogDirectory is forwarded to the worker; worker's Validate() creates it via ensureDir. + LogLevel string LogDirectory string + CacheDir string - // CacheDir is forwarded to the worker; worker's Validate() creates it via ensureDir. - CacheDir string - - // Conflict resolution β€” ReadOnly takes priority over write flags. - // If ReadOnly is set, AllowOverwrite and IncrementalUpload are cleared - // in Mount() before formulateMountOptions is called. This allows SC to - // set allow-overwrite and incremental-upload as safe defaults while still - // letting users request read-only via their secret without errors. - ReadOnly bool // --read-only - AllowOverwrite bool // --allow-overwrite - IncrementalUpload bool // --incremental-upload + ReadOnly bool + AllowOverwrite bool + IncrementalUpload bool - // MountOptions holds all remaining flags that pass through unchanged. - // The parser ensures every entry has a "--" prefix so mount-s3 sees valid flags. + // MountOptions holds deduplicated passthrough flags. + // SC flags are overridden by secret flags when the same key appears in both. MountOptions []string MounterUtils utils.MounterUtils } // s3MounterArgs holds the args sent to the worker node daemon for mount-s3. -// Structured fields cover all named flags; Args carries any remaining -// passthrough flags (e.g. --allow-delete, --hello, --never=true). // This struct must stay in sync with s3MounterArgs in the worker (cos-csi-mounter). type s3MounterArgs struct { - // Always set. AllowOther string `json:"allow-other,omitempty"` AwsCredentialsFile string `json:"aws-credentials-file,omitempty"` AwsConfigFile string `json:"aws-config-file,omitempty"` - // SecretMap-sourced. EndpointURL string `json:"endpoint-url,omitempty"` Region string `json:"region,omitempty"` - // Identity. UID string `json:"uid,omitempty"` GID string `json:"gid,omitempty"` - // Logging (translated from LogLevel). LogLevel string `json:"log-level,omitempty"` LogDirectory string `json:"log-directory,omitempty"` - // Cache. CacheDir string `json:"cache,omitempty"` - // Conflict trio. ReadOnly string `json:"read-only,omitempty"` AllowOverwrite string `json:"allow-overwrite,omitempty"` IncrementalUpload string `json:"incremental-upload,omitempty"` - // Passthrough flags β€” user-supplied flags not handled by structured fields - // (e.g. --allow-delete, --hello, --never=true). - // Worker's PopulateArgsSlice appends these last so they have highest precedence. + // Passthrough flags β€” user-supplied flags not handled by structured fields. + // Secret flags override SC flags when the same key appears in both. Args []string `json:"args,omitempty"` } @@ -118,29 +80,31 @@ var ( removeS3ConfigFile = removeS3MountConfigFile ) -// envMounter is a local interface for type assertion β€” allows calling -// FuseMountWithEnv on the concrete MounterOptsUtils without changing the -// MounterUtils interface, so other mounters (rclone etc.) are unaffected. type envMounter interface { FuseMountWithEnv(path string, comm string, args []string, envVars []string) error } // NewMountpointS3Mounter creates a new MountpointS3Mounter from the unified // secret format. +// +// Precedence (lowest β†’ highest): +// 1. secretMap identity keys +// 2. StorageClass mountOptions β€” parsed first +// 3. Secret mountOptions β€” parsed second, wins over SC on any overlapping flag +// +// For passthrough flags, deduplication is applied: if the same flag key appears +// in both SC and secret, the SC entry is dropped and the secret entry is kept. func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounterUtils utils.MounterUtils) Mounter { klog.Info("-newMountpointS3Mounter-") mounter := &MountpointS3Mounter{} - // --- Credentials --- if val, ok := secretMap["accessKey"]; ok { mounter.AccessKey = val } if val, ok := secretMap["secretKey"]; ok { mounter.SecretKey = val } - - // --- Bucket / endpoint identity --- if val, ok := secretMap["bucketName"]; ok { mounter.BucketName = val } @@ -154,7 +118,6 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounter.LocConstraint = val } - // --- UID / GID (lowest precedence β€” mountOptions overrides below) --- if secretMap["gid"] != "" && secretMap["uid"] == "" { mounter.UID = secretMap["gid"] } else if val := secretMap["uid"]; val != "" { @@ -166,21 +129,76 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounter.AuthType = "hmac" - // --- Merge and parse mountOptions --- + // Step 1: parse SC options. scRemainder := parseMountpointS3Options(mounter, mountOptions) + + // Step 2: parse secret options (structured fields set by secret override SC). secretOpts := splitSecretMountOptions(secretMap["mountOptions"]) secretRemainder := parseMountpointS3Options(mounter, secretOpts) - mounter.MountOptions = append(scRemainder, secretRemainder...) + + // Step 3: deduplicate passthroughs β€” secret wins over SC on same key. + mounter.MountOptions = deduplicatePassthrough(scRemainder, secretRemainder) + + // Step 4: Block incremental-upload flag (causes data loss with IBM COS). + // This flag uses multiple PutObject calls that overwrite each other instead of + // using proper multipart upload, resulting in ~75% data loss. + if mounter.IncrementalUpload { + klog.Info("mount-s3: --incremental-upload is not supported ") + mounter.IncrementalUpload = false + } + mounter.MounterUtils = mounterUtils klog.Infof("newMountpointS3Mounter args:\n\tbucketName: [%s]\n\tobjectPath: [%s]\n\tendPoint: [%s]\n\tlocationConstraint: [%s]\n\tauthType: [%s]", mounter.BucketName, mounter.ObjectPath, mounter.EndPoint, mounter.LocConstraint, mounter.AuthType) - klog.Infof("newMountpointS3Mounter: SC mountOptions=%v secretMountOptions=%v passthrough=%v", + klog.Infof("newMountpointS3Mounter: SC mountOptions=%v secretMountOptions=%v deduplicated passthrough=%v", mountOptions, secretOpts, mounter.MountOptions) return mounter } +// deduplicatePassthrough merges SC and secret passthrough flags. +// If the same flag key appears in both, the SC entry is dropped and the +// secret entry is kept (secret wins). +// +// Examples: +// +// SC: [--max-threads=32, --maximum-throughput-gbps=10, --allow-delete] +// Secret: [--max-threads=128, --maximum-throughput-gbps=25] +// Result: [--allow-delete, --max-threads=128, --maximum-throughput-gbps=25] +func deduplicatePassthrough(scFlags, secretFlags []string) []string { + // Build a set of keys present in secret flags. + secretKeys := make(map[string]struct{}, len(secretFlags)) + for _, flag := range secretFlags { + key := flagKey(flag) + secretKeys[key] = struct{}{} + } + + // Keep SC flags whose key does NOT appear in secret flags. + var result []string + for _, flag := range scFlags { + key := flagKey(flag) + if _, overridden := secretKeys[key]; overridden { + klog.Infof("deduplicatePassthrough: SC flag %q overridden by secret", flag) + } else { + result = append(result, flag) + } + } + + // Append all secret flags last. + result = append(result, secretFlags...) + return result +} + +// flagKey extracts the key portion from a passthrough flag. +// "--max-threads=128" β†’ "max-threads" +// "--allow-delete" β†’ "allow-delete" +func flagKey(flag string) string { + trimmed := strings.TrimPrefix(flag, "--") + key, _, _ := strings.Cut(trimmed, "=") + return key +} + // splitSecretMountOptions splits the multiline mountOptions string from the secret. func splitSecretMountOptions(raw string) []string { if raw == "" { @@ -189,6 +207,7 @@ func splitSecretMountOptions(raw string) []string { var opts []string for _, line := range strings.Split(raw, "\n") { line = strings.TrimSpace(line) + line = strings.TrimRight(line, ",") // ← ADD THIS if line == "" || strings.HasPrefix(line, "#") { continue } @@ -207,7 +226,6 @@ func parseMountpointS3Options(mounter *MountpointS3Mounter, opts []string) []str switch key { case "allow-other": - // formulateMountOptions unconditionally adds --allow-other. case "umask": klog.Warningf("parseMountpointS3Options: 'umask' is not supported by mount-s3. " + "Use --dir-mode / --file-mode instead. Ignoring.") @@ -254,7 +272,7 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { klog.Info("-MountpointS3Mounter Mount-") klog.Infof("Mount args:\n\tsource: <%s>\n\ttarget: <%s>", source, target) - // --- Read-only priority resolution --- + // Read-only priority resolution. if s3.ReadOnly { if s3.AllowOverwrite { klog.Infof("MountpointS3Mounter: read-only set β€” clearing allow-overwrite") @@ -264,7 +282,6 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { klog.Infof("MountpointS3Mounter: read-only set β€” clearing incremental-upload") s3.IncrementalUpload = false } - var filtered []string for _, opt := range s3.MountOptions { trimmed := strings.TrimPrefix(opt, "--") @@ -281,7 +298,6 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { // LogDirectory and CacheDir are forwarded to the worker via workerNodeOp. // The worker's Validate() calls ensureDir() which creates them if missing. - // --- Config path --- var configPath string if mountWorker { configPath = constants.MounterConfigPathOnHost @@ -367,7 +383,7 @@ func (s3 *MountpointS3Mounter) Unmount(target string) error { // createS3MountConfig creates the AWS credentials and config files. func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) error { - if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 -- config directory needs to be readable by mount-s3 process + if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 klog.Errorf("MountpointS3Mounter: Cannot create config dir %s: %v", configPathWithVolID, err) return err } @@ -403,7 +419,7 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er // writeConfigFile writes lines to a file with 0600 permissions. func writeConfigFile(filePath string, lines []string) error { - f, err := CreateFile(filePath) // #nosec G304 -- filePath is constructed from validated volume ID and predefined config file names + f, err := CreateFile(filePath) // #nosec G304 if err != nil { return fmt.Errorf("cannot create file %s: %w", filePath, err) } @@ -413,7 +429,7 @@ func writeConfigFile(filePath string, lines []string) error { } }() - if err := Chmod(filePath, 0600); err != nil { // #nosec G302 -- credentials file must be readable only by owner for security + if err := Chmod(filePath, 0600); err != nil { // #nosec G302 return fmt.Errorf("cannot chmod file %s: %w", filePath, err) } @@ -443,7 +459,6 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW AwsConfigFile: cfgFilePath, } - // --- SecretMap-sourced --- if s3.EndPoint != "" { nodeServerOp = append(nodeServerOp, "--endpoint-url="+s3.EndPoint) workerNodeOp.EndpointURL = s3.EndPoint @@ -452,8 +467,6 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW nodeServerOp = append(nodeServerOp, "--region="+s3.LocConstraint) workerNodeOp.Region = s3.LocConstraint } - - // --- Identity --- if s3.UID != "" { nodeServerOp = append(nodeServerOp, "--uid="+s3.UID) workerNodeOp.UID = s3.UID @@ -463,7 +476,6 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW workerNodeOp.GID = s3.GID } - // --- Log level (name translation) --- switch s3.LogLevel { case "debug": nodeServerOp = append(nodeServerOp, "--debug") @@ -481,19 +493,14 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW } } - // --- Log directory --- if s3.LogDirectory != "" { nodeServerOp = append(nodeServerOp, "--log-directory="+s3.LogDirectory) workerNodeOp.LogDirectory = s3.LogDirectory } - - // --- Cache directory --- if s3.CacheDir != "" { nodeServerOp = append(nodeServerOp, "--cache="+s3.CacheDir) workerNodeOp.CacheDir = s3.CacheDir } - - // --- Write flags --- if s3.AllowOverwrite { nodeServerOp = append(nodeServerOp, "--allow-overwrite") workerNodeOp.AllowOverwrite = "true" @@ -502,15 +509,13 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW nodeServerOp = append(nodeServerOp, "--incremental-upload") workerNodeOp.IncrementalUpload = "true" } - - // --- Read-only --- if s3.ReadOnly { nodeServerOp = append(nodeServerOp, "--read-only") workerNodeOp.ReadOnly = "true" } - // --- User passthrough (appended last β€” highest precedence) --- - // Set on BOTH paths so the worker daemon receives passthrough flags too. + // Passthrough β€” deduplicated so secret wins over SC. + // Set on BOTH paths so the worker daemon receives them too. nodeServerOp = append(nodeServerOp, s3.MountOptions...) workerNodeOp.Args = s3.MountOptions From 33f5d2e41beb070c60a49f281ed65291455c94d7 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Mon, 13 Apr 2026 10:20:28 +0530 Subject: [PATCH 101/128] publish 0.12.24 version --- pkg/mounter/mounter_s3mounter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index fd2cc4e8..dfa49c0c 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -383,7 +383,7 @@ func (s3 *MountpointS3Mounter) Unmount(target string) error { // createS3MountConfig creates the AWS credentials and config files. func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) error { - if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 + if err := MakeDir(configPathWithVolID, 0755); err != nil { // #nosec G301 -- config directory needs to be readable by mount-s3 process klog.Errorf("MountpointS3Mounter: Cannot create config dir %s: %v", configPathWithVolID, err) return err } From 0a808c711fdada1c44be6c474c92ebaaf00969c8 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Mon, 13 Apr 2026 11:39:33 +0530 Subject: [PATCH 102/128] publish 0.12.25 version --- pkg/mounter/mounter_s3mounter.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index dfa49c0c..903fcd7a 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -419,7 +419,7 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er // writeConfigFile writes lines to a file with 0600 permissions. func writeConfigFile(filePath string, lines []string) error { - f, err := CreateFile(filePath) // #nosec G304 + f, err := CreateFile(filePath) // #nosec G304 -- file path is constructed from controlled configPathWithVolID and constant filename if err != nil { return fmt.Errorf("cannot create file %s: %w", filePath, err) } @@ -429,7 +429,7 @@ func writeConfigFile(filePath string, lines []string) error { } }() - if err := Chmod(filePath, 0600); err != nil { // #nosec G302 + if err := Chmod(filePath, 0600); err != nil { // #nosec G302 -- credentials file requires restrictive permissions (owner read/write only) return fmt.Errorf("cannot chmod file %s: %w", filePath, err) } From da1a792d14d4c55460fae7f64d75d537bb1f62c8 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Tue, 12 May 2026 16:44:16 +0530 Subject: [PATCH 103/128] Updated testcases --- pkg/mounter/mounter-s3mounter_test.go | 890 ++++++++++++++++++++++++++ 1 file changed, 890 insertions(+) create mode 100644 pkg/mounter/mounter-s3mounter_test.go diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go new file mode 100644 index 00000000..7e7efb5f --- /dev/null +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -0,0 +1,890 @@ +package mounter + +import ( + "errors" + "os" + "testing" + mounterUtils "github.com/IBM/ibm-object-csi-driver/pkg/mounter/utils" + "github.com/stretchr/testify/assert" +) + +var ( + s3MounterSecretMap = map[string]string{ + "cosEndpoint": "https://s3.us-east.cloud-object-storage.appdomain.cloud", + "locationConstraint": "us-east", + "bucketName": "test-bucket", + "objectPath": "test-path", + "accessKey": "test-access-key", + "secretKey": "test-secret-key", + "uid": "1000", + "gid": "1000", + "mountOptions": `log-level=debug +read-only +cache=/tmp/cache`, + } + + s3MounterMountOptions = []string{"--dir-mode=0755", "--file-mode=0644"} +) + +func TestNewMountpointS3Mounter_Success(t *testing.T) { + mounter := NewMountpointS3Mounter(s3MounterSecretMap, s3MounterMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + + s3Mounter, ok := mounter.(*MountpointS3Mounter) + assert.True(t, ok) + + assert.Equal(t, s3MounterSecretMap["bucketName"], s3Mounter.BucketName) + assert.Equal(t, s3MounterSecretMap["objectPath"], s3Mounter.ObjectPath) + assert.Equal(t, s3MounterSecretMap["cosEndpoint"], s3Mounter.EndPoint) + assert.Equal(t, s3MounterSecretMap["locationConstraint"], s3Mounter.LocConstraint) + assert.Equal(t, s3MounterSecretMap["accessKey"], s3Mounter.AccessKey) + assert.Equal(t, s3MounterSecretMap["secretKey"], s3Mounter.SecretKey) + assert.Equal(t, s3MounterSecretMap["uid"], s3Mounter.UID) + assert.Equal(t, s3MounterSecretMap["gid"], s3Mounter.GID) + assert.Equal(t, "debug", s3Mounter.LogLevel) + assert.True(t, s3Mounter.ReadOnly) + assert.Equal(t, "/tmp/cache", s3Mounter.CacheDir) + assert.Equal(t, "hmac", s3Mounter.AuthType) + // MountOptions should contain SC options (dir-mode, file-mode) + assert.Contains(t, s3Mounter.MountOptions, "--dir-mode=0755") + assert.Contains(t, s3Mounter.MountOptions, "--file-mode=0644") +} + +func TestNewMountpointS3Mounter_GidWithoutUid(t *testing.T) { + secretMap := map[string]string{ + "gid": "2000", + } + + mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + + s3Mounter, ok := mounter.(*MountpointS3Mounter) + assert.True(t, ok) + assert.Equal(t, "2000", s3Mounter.UID) + assert.Equal(t, "2000", s3Mounter.GID) +} + +func TestNewMountpointS3Mounter_UidOverridesGid(t *testing.T) { + secretMap := map[string]string{ + "uid": "1000", + "gid": "2000", + } + + mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + + s3Mounter, ok := mounter.(*MountpointS3Mounter) + assert.True(t, ok) + assert.Equal(t, "1000", s3Mounter.UID) + assert.Equal(t, "2000", s3Mounter.GID) +} + +func TestMountpointS3Mount_NodeServer_Positive(t *testing.T) { + mountWorker = false + + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return os.CreateTemp("", "test") + } + Chmod = func(path string, perm os.FileMode) error { + return nil + } + + // Create a mock that implements envMounter interface + mockUtils := &mockEnvMounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseMountFn: func(path, comm string, args []string) error { + return nil + }, + }), + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mockUtils, + BucketName: "test-bucket", + ObjectPath: "test-path", + EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", + LocConstraint: "us-east", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + UID: "1000", + GID: "1000", + LogLevel: "debug", + ReadOnly: true, + MountOptions: s3MounterMountOptions, + } + + err := s3Mounter.Mount(source, target) + assert.NoError(t, err) +} + +func TestMountpointS3Mount_NodeServer_WithEnvMounter(t *testing.T) { + mountWorker = false + + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return os.CreateTemp("", "test") + } + Chmod = func(path string, perm os.FileMode) error { + return nil + } + + // Create a mock that implements envMounter interface + mockUtils := &mockEnvMounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mockUtils, + BucketName: "test-bucket", + EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", + LocConstraint: "us-east", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + MountOptions: s3MounterMountOptions, + } + + err := s3Mounter.Mount(source, target) + assert.NoError(t, err) +} + +func TestMountpointS3Mount_WorkerNode_Positive(t *testing.T) { + mountWorker = true + + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return os.CreateTemp("", "test") + } + Chmod = func(path string, perm os.FileMode) error { + return nil + } + mounterRequest = func(payload, url string) error { + return nil + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), + BucketName: "test-bucket", + EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", + LocConstraint: "us-east", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := s3Mounter.Mount(source, target) + assert.NoError(t, err) +} + +func TestMountpointS3Mount_CreateDirFails_Negative(t *testing.T) { + mountWorker = false + + MakeDir = func(path string, perm os.FileMode) error { + return errors.New("failed to create directory") + } + + s3Mounter := &MountpointS3Mounter{ + BucketName: "test-bucket", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := s3Mounter.Mount(source, target) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to create directory") +} + +func TestMountpointS3Mount_CreateCredFileFails_Negative(t *testing.T) { + mountWorker = false + + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return nil, errors.New("failed to create file") + } + + s3Mounter := &MountpointS3Mounter{ + BucketName: "test-bucket", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := s3Mounter.Mount(source, target) + assert.Error(t, err) + assert.Contains(t, err.Error(), "cannot create file") +} + +func TestMountpointS3Mount_WorkerNode_Negative(t *testing.T) { + mountWorker = true + + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return os.CreateTemp("", "test") + } + Chmod = func(path string, perm os.FileMode) error { + return nil + } + mounterRequest = func(payload, url string) error { + return errors.New("failed to perform http request") + } + + s3Mounter := &MountpointS3Mounter{ + BucketName: "test-bucket", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := s3Mounter.Mount(source, target) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to perform http request") +} + +func TestMountpointS3Unmount_NodeServer(t *testing.T) { + mountWorker = false + + removeS3ConfigFile = func(configPath, target string) {} + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseUnmountFn: func(path string) error { + return nil + }, + }), + } + + err := s3Mounter.Unmount(target) + assert.NoError(t, err) +} + +func TestMountpointS3Unmount_WorkerNode(t *testing.T) { + mountWorker = true + + removeS3ConfigFile = func(configPath, target string) {} + + mounterRequest = func(payload, url string) error { + return nil + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseUnmountFn: func(path string) error { + return nil + }, + }), + } + + err := s3Mounter.Unmount(target) + assert.NoError(t, err) +} + +func TestMountpointS3Unmount_WorkerNode_Negative(t *testing.T) { + mountWorker = true + + mounterRequest = func(payload, url string) error { + return errors.New("failed to create http request") + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseUnmountFn: func(path string) error { + return nil + }, + }), + } + + err := s3Mounter.Unmount(target) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to create http request") +} + +func TestMountpointS3Unmount_NodeServer_Negative(t *testing.T) { + mountWorker = false + + removeS3ConfigFile = func(configPath, target string) {} + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseUnmountFn: func(path string) error { + return errors.New("failed to unmount") + }, + }), + } + + err := s3Mounter.Unmount(target) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to unmount") +} + +func TestRemoveS3MountConfigFile_PathNotExists(t *testing.T) { + Stat = func(path string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } + removeS3MountConfigFile("/test", target) +} + +func TestRemoveS3MountConfigFile_StatRetryThenSuccess(t *testing.T) { + attempt := 0 + Stat = func(_ string) (os.FileInfo, error) { + if attempt == 0 { + attempt++ + return nil, errors.New("stat error") + } + return nil, nil + } + RemoveAll = func(_ string) error { + return nil + } + + removeS3MountConfigFile("/test", target) +} + +func TestRemoveS3MountConfigFile_RemoveRetryThenSuccess(t *testing.T) { + Stat = func(_ string) (os.FileInfo, error) { + return nil, nil + } + attempt := 0 + RemoveAll = func(_ string) error { + if attempt == 0 { + attempt++ + return errors.New("remove error") + } + return nil + } + + removeS3MountConfigFile("/test", target) +} + +func TestRemoveS3MountConfigFile_Negative(t *testing.T) { + called := 0 + Stat = func(_ string) (os.FileInfo, error) { + return nil, nil + } + RemoveAll = func(_ string) error { + called++ + return errors.New("remove failed") + } + + removeS3MountConfigFile("/test", target) + assert.Equal(t, maxRetries, called) +} + +func TestFormulateMountOptions_AllOptions(t *testing.T) { + s3Mounter := &MountpointS3Mounter{ + BucketName: "test-bucket", + ObjectPath: "test-path", + EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", + LocConstraint: "us-east", + UID: "1000", + GID: "2000", + LogLevel: "debug", + LogDirectory: "/var/log/mount-s3", + CacheDir: "/tmp/cache", + ReadOnly: true, + MountOptions: []string{"--dir-mode=0755", "--max-threads=32"}, + } + + nodeServerOp, envVars, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket/test-path", target, "/config/path") + + // Check node server options + assert.Contains(t, nodeServerOp, "test-bucket/test-path") + assert.Contains(t, nodeServerOp, target) + assert.Contains(t, nodeServerOp, "--allow-other") + assert.Contains(t, nodeServerOp, "--endpoint-url=https://s3.us-east.cloud-object-storage.appdomain.cloud") + assert.Contains(t, nodeServerOp, "--region=us-east") + assert.Contains(t, nodeServerOp, "--uid=1000") + assert.Contains(t, nodeServerOp, "--gid=2000") + assert.Contains(t, nodeServerOp, "--debug") + assert.Contains(t, nodeServerOp, "--log-directory=/var/log/mount-s3") + assert.Contains(t, nodeServerOp, "--cache=/tmp/cache") + assert.Contains(t, nodeServerOp, "--read-only") + assert.Contains(t, nodeServerOp, "--dir-mode=0755") + assert.Contains(t, nodeServerOp, "--max-threads=32") + + // Check env vars + assert.Len(t, envVars, 2) + assert.Contains(t, envVars[0], "AWS_SHARED_CREDENTIALS_FILE=") + assert.Contains(t, envVars[1], "AWS_CONFIG_FILE=") + + // Check worker node options + assert.Equal(t, "true", workerNodeOp.AllowOther) + assert.Equal(t, "https://s3.us-east.cloud-object-storage.appdomain.cloud", workerNodeOp.EndpointURL) + assert.Equal(t, "us-east", workerNodeOp.Region) + assert.Equal(t, "1000", workerNodeOp.UID) + assert.Equal(t, "2000", workerNodeOp.GID) + assert.Equal(t, "debug", workerNodeOp.LogLevel) + assert.Equal(t, "/var/log/mount-s3", workerNodeOp.LogDirectory) + assert.Equal(t, "/tmp/cache", workerNodeOp.CacheDir) + assert.Equal(t, "true", workerNodeOp.ReadOnly) + assert.Contains(t, workerNodeOp.Args, "--dir-mode=0755") + assert.Contains(t, workerNodeOp.Args, "--max-threads=32") +} + +func TestFormulateMountOptions_LogLevelDebugCrt(t *testing.T) { + s3Mounter := &MountpointS3Mounter{ + LogLevel: "debug-crt", + } + + nodeServerOp, _, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") + + assert.Contains(t, nodeServerOp, "--debug-crt") + assert.Equal(t, "debug-crt", workerNodeOp.LogLevel) +} + +func TestFormulateMountOptions_LogLevelNoLog(t *testing.T) { + s3Mounter := &MountpointS3Mounter{ + LogLevel: "no-log", + } + + nodeServerOp, _, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") + + assert.Contains(t, nodeServerOp, "--no-log") + assert.Equal(t, "no-log", workerNodeOp.LogLevel) +} + +func TestFormulateMountOptions_InvalidLogLevel(t *testing.T) { + s3Mounter := &MountpointS3Mounter{ + LogLevel: "invalid-level", + } + + nodeServerOp, _, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") + + // Should not contain any log level flag + assert.NotContains(t, nodeServerOp, "--debug") + assert.NotContains(t, nodeServerOp, "--debug-crt") + assert.NotContains(t, nodeServerOp, "--no-log") + assert.Empty(t, workerNodeOp.LogLevel) +} + +func TestFormulateMountOptions_MinimalConfig(t *testing.T) { + s3Mounter := &MountpointS3Mounter{ + MountOptions: []string{}, + } + + nodeServerOp, envVars, workerNodeOp := s3Mounter.formulateMountOptions("test-bucket", target, "/config/path") + + // Check basic options are present + assert.Contains(t, nodeServerOp, "test-bucket") + assert.Contains(t, nodeServerOp, target) + assert.Contains(t, nodeServerOp, "--allow-other") + + // Check env vars + assert.Len(t, envVars, 2) + + // Check worker node options + assert.Equal(t, "true", workerNodeOp.AllowOther) +} + +func TestCreateS3MountConfig_Success(t *testing.T) { + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return os.CreateTemp("", "test") + } + Chmod = func(path string, perm os.FileMode) error { + return nil + } + + s3Mounter := &MountpointS3Mounter{ + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + LocConstraint: "us-east", + EndPoint: "https://s3.us-east.cloud-object-storage.appdomain.cloud", + } + + err := createS3MountConfig("/config/path", s3Mounter) + assert.NoError(t, err) +} + +func TestCreateS3MountConfig_MakeDirFails(t *testing.T) { + MakeDir = func(path string, perm os.FileMode) error { + return errors.New("failed to create directory") + } + + s3Mounter := &MountpointS3Mounter{ + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := createS3MountConfig("/config/path", s3Mounter) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to create directory") +} + +func TestCreateS3MountConfig_CreateFileFails(t *testing.T) { + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return nil, errors.New("failed to create file") + } + + s3Mounter := &MountpointS3Mounter{ + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := createS3MountConfig("/config/path", s3Mounter) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to create file") +} + +func TestCreateS3MountConfig_ChmodFails(t *testing.T) { + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return os.CreateTemp("", "test") + } + Chmod = func(path string, perm os.FileMode) error { + return errors.New("failed to chmod") + } + + s3Mounter := &MountpointS3Mounter{ + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + } + + err := createS3MountConfig("/config/path", s3Mounter) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to chmod") +} + +// mockEnvMounter implements the envMounter interface for testing +type mockEnvMounter struct { + mounterUtils.MounterUtils +} + +func (m *mockEnvMounter) FuseMountWithEnv(path string, comm string, args []string, envVars []string) error { + return nil +} + + +// Test deduplicatePassthrough function +func TestDeduplicatePassthrough(t *testing.T) { + tests := []struct { + name string + scFlags []string + secretFlags []string + expected []string + }{ + { + name: "No overlap - all flags kept", + scFlags: []string{"--allow-delete", "--dir-mode=0755"}, + secretFlags: []string{"--max-threads=128"}, + expected: []string{"--allow-delete", "--dir-mode=0755", "--max-threads=128"}, + }, + { + name: "Complete overlap - secret wins", + scFlags: []string{"--max-threads=32", "--maximum-throughput-gbps=10"}, + secretFlags: []string{"--max-threads=128", "--maximum-throughput-gbps=25"}, + expected: []string{"--max-threads=128", "--maximum-throughput-gbps=25"}, + }, + { + name: "Partial overlap - secret wins on duplicates", + scFlags: []string{"--max-threads=32", "--maximum-throughput-gbps=10", "--allow-delete"}, + secretFlags: []string{"--max-threads=128", "--maximum-throughput-gbps=25"}, + expected: []string{"--allow-delete", "--max-threads=128", "--maximum-throughput-gbps=25"}, + }, + { + name: "Empty SC flags", + scFlags: []string{}, + secretFlags: []string{"--max-threads=128"}, + expected: []string{"--max-threads=128"}, + }, + { + name: "Empty secret flags", + scFlags: []string{"--max-threads=32"}, + secretFlags: []string{}, + expected: []string{"--max-threads=32"}, + }, + { + name: "Both empty", + scFlags: []string{}, + secretFlags: []string{}, + expected: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := deduplicatePassthrough(tt.scFlags, tt.secretFlags) + assert.ElementsMatch(t, tt.expected, result) + }) + } +} + +// Test flagKey function +func TestFlagKey(t *testing.T) { + tests := []struct { + name string + flag string + expected string + }{ + { + name: "Flag with value", + flag: "--max-threads=128", + expected: "max-threads", + }, + { + name: "Flag without value", + flag: "--allow-delete", + expected: "allow-delete", + }, + { + name: "Flag without double dash", + flag: "max-threads=128", + expected: "max-threads", + }, + { + name: "Complex flag with multiple equals", + flag: "--endpoint-url=https://s3.example.com", + expected: "endpoint-url", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := flagKey(tt.flag) + assert.Equal(t, tt.expected, result) + }) + } +} + +// Test splitSecretMountOptions function +func TestSplitSecretMountOptions(t *testing.T) { + tests := []struct { + name string + raw string + expected []string + }{ + { + name: "Basic multiline options", + raw: `log-level=debug +read-only +cache=/tmp/cache`, + expected: []string{"log-level=debug", "read-only", "cache=/tmp/cache"}, + }, + { + name: "Options with trailing commas", + raw: `log-level=debug, +read-only, +cache=/tmp/cache,`, + expected: []string{"log-level=debug", "read-only", "cache=/tmp/cache"}, + }, + { + name: "Options with comments", + raw: `log-level=debug +# This is a comment +read-only +cache=/tmp/cache`, + expected: []string{"log-level=debug", "read-only", "cache=/tmp/cache"}, + }, + { + name: "Options with empty lines", + raw: `log-level=debug + +read-only + +cache=/tmp/cache`, + expected: []string{"log-level=debug", "read-only", "cache=/tmp/cache"}, + }, + { + name: "Options with whitespace", + raw: ` log-level=debug + read-only + cache=/tmp/cache `, + expected: []string{"log-level=debug", "read-only", "cache=/tmp/cache"}, + }, + { + name: "Empty string", + raw: "", + expected: nil, + }, + { + name: "Only comments and empty lines", + raw: `# Comment 1 + +# Comment 2`, + expected: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := splitSecretMountOptions(tt.raw) + assert.Equal(t, tt.expected, result) + }) + } +} + +// Test parseMountpointS3Options function +func TestParseMountpointS3Options(t *testing.T) { + tests := []struct { + name string + opts []string + expectedMounter *MountpointS3Mounter + expectedRemaining []string + }{ + { + name: "Parse structured options", + opts: []string{"--uid=1000", "--gid=2000", "--log-level=debug", "--cache=/tmp/cache", "--read-only"}, + expectedMounter: &MountpointS3Mounter{ + UID: "1000", + GID: "2000", + LogLevel: "debug", + CacheDir: "/tmp/cache", + ReadOnly: true, + }, + expectedRemaining: []string{}, + }, + { + name: "Parse with passthrough options", + opts: []string{"--uid=1000", "--max-threads=32", "--dir-mode=0755"}, + expectedMounter: &MountpointS3Mounter{ + UID: "1000", + }, + expectedRemaining: []string{"--max-threads=32", "--dir-mode=0755"}, + }, + { + name: "Parse allow-overwrite and incremental-upload", + opts: []string{"--allow-overwrite", "--incremental-upload"}, + expectedMounter: &MountpointS3Mounter{ + AllowOverwrite: true, + IncrementalUpload: true, + }, + expectedRemaining: []string{}, + }, + { + name: "Parse log-directory", + opts: []string{"--log-directory=/var/log/mount-s3"}, + expectedMounter: &MountpointS3Mounter{ + LogDirectory: "/var/log/mount-s3", + }, + expectedRemaining: []string{}, + }, + { + name: "Ignore allow-other", + opts: []string{"--allow-other", "--uid=1000"}, + expectedMounter: &MountpointS3Mounter{ + UID: "1000", + }, + expectedRemaining: []string{}, + }, + { + name: "Add double dash to options without it", + opts: []string{"max-threads=32", "dir-mode=0755"}, + expectedMounter: &MountpointS3Mounter{}, + expectedRemaining: []string{"--max-threads=32", "--dir-mode=0755"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mounter := &MountpointS3Mounter{} + remaining := parseMountpointS3Options(mounter, tt.opts) + + assert.Equal(t, tt.expectedMounter.UID, mounter.UID) + assert.Equal(t, tt.expectedMounter.GID, mounter.GID) + assert.Equal(t, tt.expectedMounter.LogLevel, mounter.LogLevel) + assert.Equal(t, tt.expectedMounter.LogDirectory, mounter.LogDirectory) + assert.Equal(t, tt.expectedMounter.CacheDir, mounter.CacheDir) + assert.Equal(t, tt.expectedMounter.ReadOnly, mounter.ReadOnly) + assert.Equal(t, tt.expectedMounter.AllowOverwrite, mounter.AllowOverwrite) + assert.Equal(t, tt.expectedMounter.IncrementalUpload, mounter.IncrementalUpload) + assert.ElementsMatch(t, tt.expectedRemaining, remaining) + }) + } +} + +// Test incremental-upload blocking +func TestNewMountpointS3Mounter_IncrementalUploadBlocked(t *testing.T) { + secretMap := map[string]string{ + "bucketName": "test-bucket", + "accessKey": "test-access-key", + "secretKey": "test-secret-key", + "mountOptions": `incremental-upload +allow-overwrite`, + } + + mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + + s3Mounter, ok := mounter.(*MountpointS3Mounter) + assert.True(t, ok) + + // incremental-upload should be blocked (set to false) + assert.False(t, s3Mounter.IncrementalUpload, "incremental-upload should be blocked due to data loss issues") + // allow-overwrite should still be set + assert.True(t, s3Mounter.AllowOverwrite) +} + +// Test SC and secret mount options precedence +func TestNewMountpointS3Mounter_MountOptionsPrecedence(t *testing.T) { + secretMap := map[string]string{ + "bucketName": "test-bucket", + "accessKey": "test-access-key", + "secretKey": "test-secret-key", + "mountOptions": `max-threads=128 +maximum-throughput-gbps=25`, + } + + scMountOptions := []string{"--max-threads=32", "--maximum-throughput-gbps=10", "--allow-delete"} + + mounter := NewMountpointS3Mounter(secretMap, scMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + + s3Mounter, ok := mounter.(*MountpointS3Mounter) + assert.True(t, ok) + + // Secret options should override SC options for same keys + assert.Contains(t, s3Mounter.MountOptions, "--max-threads=128") + assert.Contains(t, s3Mounter.MountOptions, "--maximum-throughput-gbps=25") + assert.NotContains(t, s3Mounter.MountOptions, "--max-threads=32") + assert.NotContains(t, s3Mounter.MountOptions, "--maximum-throughput-gbps=10") + + // SC-only options should be preserved + assert.Contains(t, s3Mounter.MountOptions, "--allow-delete") +} + +// Test read-only priority resolution in Mount +func TestMountpointS3Mount_ReadOnlyPriority(t *testing.T) { + mountWorker = false + + MakeDir = func(path string, perm os.FileMode) error { + return nil + } + CreateFile = func(path string) (*os.File, error) { + return os.CreateTemp("", "test") + } + Chmod = func(path string, perm os.FileMode) error { + return nil + } + + mockUtils := &mockEnvMounter{ + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), + } + + s3Mounter := &MountpointS3Mounter{ + MounterUtils: mockUtils, + BucketName: "test-bucket", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + ReadOnly: true, + AllowOverwrite: true, // Should be cleared + IncrementalUpload: true, // Should be cleared + MountOptions: []string{"--allow-delete", "--max-threads=32"}, // allow-delete should be removed + } + + err := s3Mounter.Mount(source, target) + assert.NoError(t, err) + + // Verify read-only priority cleared conflicting options + assert.False(t, s3Mounter.AllowOverwrite) + assert.False(t, s3Mounter.IncrementalUpload) + assert.NotContains(t, s3Mounter.MountOptions, "--allow-delete") + assert.Contains(t, s3Mounter.MountOptions, "--max-threads=32") +} From 2c4c1f4d27e26373f3d1ef4c95c13ccee5788b5f Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Tue, 12 May 2026 17:13:39 +0530 Subject: [PATCH 104/128] Updated test-cases --- pkg/mounter/mounter-s3mounter_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go index 7e7efb5f..01046e45 100644 --- a/pkg/mounter/mounter-s3mounter_test.go +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -711,7 +711,7 @@ cache=/tmp/cache`, raw: `# Comment 1 # Comment 2`, - expected: []string{}, + expected: nil, }, } From 250cf288f0296a978d4db186d4158b49138459db Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Tue, 12 May 2026 18:41:36 +0530 Subject: [PATCH 105/128] updated test case --- pkg/mounter/mounter_test.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/pkg/mounter/mounter_test.go b/pkg/mounter/mounter_test.go index e97b00ff..8221c52d 100644 --- a/pkg/mounter/mounter_test.go +++ b/pkg/mounter/mounter_test.go @@ -90,6 +90,35 @@ func TestNewMounter(t *testing.T) { }, expectedErr: nil, }, + { + name: "MountpointS3 Mounter", + attrib: map[string]string{"mounter": constants.AMAZONS3MOUNTER}, + secretMap: map[string]string{ + "cosEndpoint": "test-endpoint", + "locationConstraint": "test-loc-constraint", + "bucketName": "test-bucket-name", + "objectPath": "test-obj-path", + "accessKey": "test-access-key", + "secretKey": "test-secret-key", + "uid": "1000", + "gid": "2000", + }, + mountOptions: []string{"--dir-mode=0755", "--file-mode=0644"}, + expected: &MountpointS3Mounter{ + BucketName: "test-bucket-name", + ObjectPath: "test-obj-path", + EndPoint: "test-endpoint", + LocConstraint: "test-loc-constraint", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + AuthType: "hmac", + UID: "1000", + GID: "2000", + MountOptions: []string{"--dir-mode=0755", "--file-mode=0644"}, + MounterUtils: &mounterUtils.MounterOptsUtils{}, + }, + expectedErr: nil, + }, { name: "Default Mounter", attrib: map[string]string{}, @@ -145,6 +174,14 @@ func TestNewMounter(t *testing.T) { rclone.MountOptions = nil expected.MountOptions = nil } + if s3mounter, ok := result.(*MountpointS3Mounter); ok { + expected := test.expected.(*MountpointS3Mounter) + if !stringSlicesEqualIgnoreOrder(s3mounter.MountOptions, expected.MountOptions) { + t.Errorf("MountOptions mismatch.\nGot: %v\nWant: %v", s3mounter.MountOptions, expected.MountOptions) + } + s3mounter.MountOptions = nil + expected.MountOptions = nil + } assert.Equal(t, result, test.expected) From b4ef2f4f627aa625760fbe4e39ac4213cc4fd5f7 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Tue, 14 Jul 2026 23:41:16 +0530 Subject: [PATCH 106/128] publish 0.12.28 version --- cos-csi-mounter/server/s3mounter.go | 22 ++++++++- pkg/mounter/mounter_s3mounter.go | 69 ++++++++++++++++++++--------- 2 files changed, 68 insertions(+), 23 deletions(-) diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go index 613e3a66..bb8c040b 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/s3mounter.go @@ -31,9 +31,13 @@ type s3MounterArgs struct { // Write operations - conflict resolution applied when read-only is set. ReadOnly string `json:"read-only,omitempty"` + AllowDelete string `json:"allow-delete,omitempty"` AllowOverwrite string `json:"allow-overwrite,omitempty"` IncrementalUpload string `json:"incremental-upload,omitempty"` + // ForcePathStyle is always true for IBM COS compatibility. + ForcePathStyle string `json:"force-path-style,omitempty"` + // Passthrough flags β€” user-supplied flags not handled by structured fields. // Secret flags override SC flags when the same key appears in both. // These are already deduplicated by the node server before being sent here. @@ -52,8 +56,12 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string args.IncrementalUpload = "" } - // Read-only priority resolution. + // Read-only priority resolution: suppress write-enabling flags. if args.ReadOnly == "true" { + if args.AllowDelete == "true" { + logger.Warn("read-only is set, clearing allow-delete") + args.AllowDelete = "" + } if args.AllowOverwrite == "true" { logger.Warn("read-only is set, clearing allow-overwrite") args.AllowOverwrite = "" @@ -70,6 +78,9 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string if args.ReadOnly == "true" { result = append(result, "--read-only") } + if args.AllowDelete == "true" { + result = append(result, "--allow-delete") + } if args.AllowOverwrite == "true" { result = append(result, "--allow-overwrite") } @@ -111,6 +122,9 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string if args.CacheDir != "" { result = append(result, "--cache="+args.CacheDir) } + if args.ForcePathStyle == "true" { + result = append(result, "--force-path-style") + } // Passthrough flags appended last β€” already deduplicated by node server. result = append(result, args.Args...) @@ -158,6 +172,12 @@ func (args s3MounterArgs) Validate(targetPath string) error { return fmt.Errorf("cannot convert value of read-only into boolean: %v", args.ReadOnly) } } + if args.AllowDelete != "" { + if isBool := isBoolString(args.AllowDelete); !isBool { + logger.Error("cannot convert value of allow-delete into boolean", zap.Any("allow-delete", args.AllowDelete)) + return fmt.Errorf("cannot convert value of allow-delete into boolean: %v", args.AllowDelete) + } + } if args.AllowOverwrite != "" { if isBool := isBoolString(args.AllowOverwrite); !isBool { logger.Error("cannot convert value of allow-overwrite into boolean", zap.Any("allow-overwrite", args.AllowOverwrite)) diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 903fcd7a..6290c8a1 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -29,11 +29,11 @@ type MountpointS3Mounter struct { UID string GID string - LogLevel string - LogDirectory string - CacheDir string + LogLevel string + CacheDir string ReadOnly bool + AllowDelete bool AllowOverwrite bool IncrementalUpload bool @@ -43,6 +43,12 @@ type MountpointS3Mounter struct { MounterUtils utils.MounterUtils } +const ( + // s3MountLogDirectory is the hardcoded log directory for mount-s3, matching + // the same convention as rclone's hardcoded --log-file=/var/log/rclone.log. + s3MountLogDirectory = "/var/log/mount-s3" +) + // s3MounterArgs holds the args sent to the worker node daemon for mount-s3. // This struct must stay in sync with s3MounterArgs in the worker (cos-csi-mounter). type s3MounterArgs struct { @@ -62,9 +68,13 @@ type s3MounterArgs struct { CacheDir string `json:"cache,omitempty"` ReadOnly string `json:"read-only,omitempty"` + AllowDelete string `json:"allow-delete,omitempty"` AllowOverwrite string `json:"allow-overwrite,omitempty"` IncrementalUpload string `json:"incremental-upload,omitempty"` + // ForcePathStyle is always true for IBM COS compatibility. + ForcePathStyle string `json:"force-path-style,omitempty"` + // Passthrough flags β€” user-supplied flags not handled by structured fields. // Secret flags override SC flags when the same key appears in both. Args []string `json:"args,omitempty"` @@ -147,6 +157,11 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounter.IncrementalUpload = false } + // Step 5: allow-delete and allow-overwrite are hardcoded defaults. + // They are suppressed later in Mount() when read-only is set. + mounter.AllowDelete = true + mounter.AllowOverwrite = true + mounter.MounterUtils = mounterUtils klog.Infof("newMountpointS3Mounter args:\n\tbucketName: [%s]\n\tobjectPath: [%s]\n\tendPoint: [%s]\n\tlocationConstraint: [%s]\n\tauthType: [%s]", @@ -242,17 +257,23 @@ func parseMountpointS3Options(mounter *MountpointS3Mounter, opts []string) []str mounter.LogLevel = value } case "log-directory": - if hasValue { - mounter.LogDirectory = value - } + // Hardcoded β€” user-supplied value is ignored. + klog.Warningf("parseMountpointS3Options: 'log-directory' is hardcoded to %q and cannot be overridden. Ignoring.", s3MountLogDirectory) + case "force-path-style": + // Hardcoded β€” always enabled for IBM COS compatibility. + klog.Warningf("parseMountpointS3Options: 'force-path-style' is hardcoded and cannot be overridden. Ignoring.") case "cache": if hasValue { mounter.CacheDir = value } case "read-only": mounter.ReadOnly = true + case "allow-delete": + // Hardcoded β€” always enabled unless read-only. User override ignored. + klog.Warningf("parseMountpointS3Options: 'allow-delete' is hardcoded and cannot be overridden. Ignoring.") case "allow-overwrite": - mounter.AllowOverwrite = true + // Hardcoded β€” always enabled unless read-only. User override ignored. + klog.Warningf("parseMountpointS3Options: 'allow-overwrite' is hardcoded and cannot be overridden. Ignoring.") case "incremental-upload": mounter.IncrementalUpload = true default: @@ -272,8 +293,12 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { klog.Info("-MountpointS3Mounter Mount-") klog.Infof("Mount args:\n\tsource: <%s>\n\ttarget: <%s>", source, target) - // Read-only priority resolution. + // Read-only priority resolution: suppress write-enabling flags. if s3.ReadOnly { + if s3.AllowDelete { + klog.Infof("MountpointS3Mounter: read-only set β€” clearing allow-delete") + s3.AllowDelete = false + } if s3.AllowOverwrite { klog.Infof("MountpointS3Mounter: read-only set β€” clearing allow-overwrite") s3.AllowOverwrite = false @@ -282,16 +307,6 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { klog.Infof("MountpointS3Mounter: read-only set β€” clearing incremental-upload") s3.IncrementalUpload = false } - var filtered []string - for _, opt := range s3.MountOptions { - trimmed := strings.TrimPrefix(opt, "--") - if trimmed != "allow-delete" { - filtered = append(filtered, opt) - } else { - klog.Infof("MountpointS3Mounter: read-only set β€” removing allow-delete from passthrough options") - } - } - s3.MountOptions = filtered } // NOTE: No directory validation or creation here. @@ -493,14 +508,24 @@ func (s3 *MountpointS3Mounter) formulateMountOptions(bucket, target, configPathW } } - if s3.LogDirectory != "" { - nodeServerOp = append(nodeServerOp, "--log-directory="+s3.LogDirectory) - workerNodeOp.LogDirectory = s3.LogDirectory - } + // log-directory is hardcoded β€” always passed, never user-configurable. + nodeServerOp = append(nodeServerOp, "--log-directory="+s3MountLogDirectory) + workerNodeOp.LogDirectory = s3MountLogDirectory + if s3.CacheDir != "" { nodeServerOp = append(nodeServerOp, "--cache="+s3.CacheDir) workerNodeOp.CacheDir = s3.CacheDir } + + // force-path-style is hardcoded β€” required for IBM COS compatibility. + nodeServerOp = append(nodeServerOp, "--force-path-style") + workerNodeOp.ForcePathStyle = "true" + + // allow-delete and allow-overwrite are hardcoded defaults, suppressed when read-only. + if s3.AllowDelete { + nodeServerOp = append(nodeServerOp, "--allow-delete") + workerNodeOp.AllowDelete = "true" + } if s3.AllowOverwrite { nodeServerOp = append(nodeServerOp, "--allow-overwrite") workerNodeOp.AllowOverwrite = "true" From 2ede06a6629db14db7d3f4665474548d5e9d21eb Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Tue, 14 Jul 2026 23:47:26 +0530 Subject: [PATCH 107/128] publish 0.12.28 version --- pkg/mounter/mounter_s3mounter.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 6290c8a1..ee1acf4b 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -29,8 +29,9 @@ type MountpointS3Mounter struct { UID string GID string - LogLevel string - CacheDir string + LogLevel string + LogDirectory string + CacheDir string ReadOnly bool AllowDelete bool @@ -257,8 +258,10 @@ func parseMountpointS3Options(mounter *MountpointS3Mounter, opts []string) []str mounter.LogLevel = value } case "log-directory": - // Hardcoded β€” user-supplied value is ignored. + // Hardcoded β€” user-supplied value is ignored; the struct field is set + // to the fixed path so callers can read it back consistently. klog.Warningf("parseMountpointS3Options: 'log-directory' is hardcoded to %q and cannot be overridden. Ignoring.", s3MountLogDirectory) + mounter.LogDirectory = s3MountLogDirectory case "force-path-style": // Hardcoded β€” always enabled for IBM COS compatibility. klog.Warningf("parseMountpointS3Options: 'force-path-style' is hardcoded and cannot be overridden. Ignoring.") From fdd93739b2b5c3ce857b35f2da571bf330489550 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Wed, 15 Jul 2026 00:00:59 +0530 Subject: [PATCH 108/128] publish 0.12.28 version --- pkg/mounter/mounter-s3mounter_test.go | 38 ++++++++++++++++----------- pkg/mounter/mounter_test.go | 24 +++++++++-------- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go index 01046e45..f862583f 100644 --- a/pkg/mounter/mounter-s3mounter_test.go +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -755,16 +755,19 @@ func TestParseMountpointS3Options(t *testing.T) { name: "Parse allow-overwrite and incremental-upload", opts: []string{"--allow-overwrite", "--incremental-upload"}, expectedMounter: &MountpointS3Mounter{ - AllowOverwrite: true, + // allow-overwrite is now hardcoded β€” parsed value is ignored. + AllowOverwrite: false, IncrementalUpload: true, }, expectedRemaining: []string{}, }, { name: "Parse log-directory", - opts: []string{"--log-directory=/var/log/mount-s3"}, + // log-directory is now hardcoded β€” any user-supplied value is ignored and + // the struct is always set to the fixed path s3MountLogDirectory. + opts: []string{"--log-directory=/some/custom/path"}, expectedMounter: &MountpointS3Mounter{ - LogDirectory: "/var/log/mount-s3", + LogDirectory: s3MountLogDirectory, }, expectedRemaining: []string{}, }, @@ -832,22 +835,25 @@ func TestNewMountpointS3Mounter_MountOptionsPrecedence(t *testing.T) { "mountOptions": `max-threads=128 maximum-throughput-gbps=25`, } - + + // --allow-delete is now hardcoded β€” it must not appear in passthrough MountOptions. scMountOptions := []string{"--max-threads=32", "--maximum-throughput-gbps=10", "--allow-delete"} mounter := NewMountpointS3Mounter(secretMap, scMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) s3Mounter, ok := mounter.(*MountpointS3Mounter) assert.True(t, ok) - - // Secret options should override SC options for same keys + + // Secret options should override SC options for same keys. assert.Contains(t, s3Mounter.MountOptions, "--max-threads=128") assert.Contains(t, s3Mounter.MountOptions, "--maximum-throughput-gbps=25") assert.NotContains(t, s3Mounter.MountOptions, "--max-threads=32") assert.NotContains(t, s3Mounter.MountOptions, "--maximum-throughput-gbps=10") - - // SC-only options should be preserved - assert.Contains(t, s3Mounter.MountOptions, "--allow-delete") + + // allow-delete is now hardcoded β€” it must NOT appear in the passthrough slice. + assert.NotContains(t, s3Mounter.MountOptions, "--allow-delete") + // But it is always enabled via the struct field. + assert.True(t, s3Mounter.AllowDelete) } // Test read-only priority resolution in Mount @@ -874,17 +880,19 @@ func TestMountpointS3Mount_ReadOnlyPriority(t *testing.T) { AccessKey: "test-access-key", SecretKey: "test-secret-key", ReadOnly: true, - AllowOverwrite: true, // Should be cleared - IncrementalUpload: true, // Should be cleared - MountOptions: []string{"--allow-delete", "--max-threads=32"}, // allow-delete should be removed + AllowDelete: true, // Should be cleared by read-only + AllowOverwrite: true, // Should be cleared by read-only + IncrementalUpload: true, // Should be cleared by read-only + MountOptions: []string{"--max-threads=32"}, } err := s3Mounter.Mount(source, target) assert.NoError(t, err) - - // Verify read-only priority cleared conflicting options + + // Verify read-only priority cleared all write-enabling flags. + assert.False(t, s3Mounter.AllowDelete) assert.False(t, s3Mounter.AllowOverwrite) assert.False(t, s3Mounter.IncrementalUpload) - assert.NotContains(t, s3Mounter.MountOptions, "--allow-delete") + // Passthrough options unrelated to write-access are preserved. assert.Contains(t, s3Mounter.MountOptions, "--max-threads=32") } diff --git a/pkg/mounter/mounter_test.go b/pkg/mounter/mounter_test.go index 8221c52d..0bef73f1 100644 --- a/pkg/mounter/mounter_test.go +++ b/pkg/mounter/mounter_test.go @@ -105,17 +105,19 @@ func TestNewMounter(t *testing.T) { }, mountOptions: []string{"--dir-mode=0755", "--file-mode=0644"}, expected: &MountpointS3Mounter{ - BucketName: "test-bucket-name", - ObjectPath: "test-obj-path", - EndPoint: "test-endpoint", - LocConstraint: "test-loc-constraint", - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - AuthType: "hmac", - UID: "1000", - GID: "2000", - MountOptions: []string{"--dir-mode=0755", "--file-mode=0644"}, - MounterUtils: &mounterUtils.MounterOptsUtils{}, + BucketName: "test-bucket-name", + ObjectPath: "test-obj-path", + EndPoint: "test-endpoint", + LocConstraint: "test-loc-constraint", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + AuthType: "hmac", + UID: "1000", + GID: "2000", + AllowDelete: true, + AllowOverwrite: true, + MountOptions: []string{"--dir-mode=0755", "--file-mode=0644"}, + MounterUtils: &mounterUtils.MounterOptsUtils{}, }, expectedErr: nil, }, From 9bcf80525e880551a25766c7d2072c59b5ca06c6 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 20 Jul 2026 00:19:14 +0530 Subject: [PATCH 109/128] Added readonly and gid variable to Mounter function --- .secrets.baseline | 19 ++++++++++++++++++- pkg/mounter/mounter-s3mounter_test.go | 24 +++++++++++------------- pkg/mounter/mounter.go | 2 +- pkg/mounter/mounter_s3mounter.go | 15 ++++++++------- 4 files changed, 38 insertions(+), 22 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index d1b6550a..074cfbd7 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -80,6 +80,7 @@ "README.md": [ { "hashed_secret": "325778ab9d49a6df7bc13a83563bec2de2a84c95", + "is_secret": true, "is_verified": false, "line_number": 72, "type": "Secret Keyword", @@ -89,6 +90,7 @@ "examples/kubernetes/cos-s3-csi-rclone-pvc.yaml": [ { "hashed_secret": "ccae38d9a6159915fbbf9bf72dbf6760e0c63c35", + "is_secret": true, "is_verified": false, "line_number": 7, "type": "Secret Keyword", @@ -98,6 +100,7 @@ "examples/kubernetes/cos-s3-csi-rclone-secret.yaml": [ { "hashed_secret": "d08f88df745fa7950b104e4a707a31cfce7b5841", + "is_secret": true, "is_verified": false, "line_number": 9, "type": "Secret Keyword", @@ -107,6 +110,7 @@ "examples/kubernetes/cos-s3-csi-s3fs-pvc.yaml": [ { "hashed_secret": "f5691ce6d36f7c4434f9f927ddc09db95c0df745", + "is_secret": true, "is_verified": false, "line_number": 7, "type": "Secret Keyword", @@ -116,6 +120,7 @@ "examples/kubernetes/cos-s3-csi-s3fs-secret.yaml": [ { "hashed_secret": "d08f88df745fa7950b104e4a707a31cfce7b5841", + "is_secret": true, "is_verified": false, "line_number": 10, "type": "Secret Keyword", @@ -148,6 +153,7 @@ }, { "hashed_secret": "b732fb611fd46a38e8667f9972e0cde777fbe37f", + "is_secret": true, "is_verified": false, "line_number": 207, "type": "Secret Keyword", @@ -162,6 +168,7 @@ }, { "hashed_secret": "62d6314bd936033553442d72fe8ce3b93ba8802d", + "is_secret": true, "is_verified": false, "line_number": 386, "type": "Secret Keyword", @@ -185,6 +192,7 @@ }, { "hashed_secret": "d4c3d66fd0c38547a3c7a4c6bdc29c36911bc030", + "is_secret": true, "is_verified": false, "line_number": 620, "type": "Secret Keyword", @@ -201,8 +209,9 @@ "pkg/driver/controllerserver_test.go": [ { "hashed_secret": "7e6a3680012346b94b54731e13d8a9ffa3790645", + "is_secret": true, "is_verified": false, - "line_number": 248, + "line_number": 249, "type": "Secret Keyword", "verified_result": null } @@ -248,6 +257,7 @@ "pkg/mounter/mounter-rclone_test.go": [ { "hashed_secret": "2e7a7ee14caebf378fc32d6cf6f557f347c96773", + "is_secret": true, "is_verified": false, "line_number": 115, "type": "Secret Keyword", @@ -288,6 +298,7 @@ "pkg/mounter/mounter-s3fs_test.go": [ { "hashed_secret": "2e7a7ee14caebf378fc32d6cf6f557f347c96773", + "is_secret": true, "is_verified": false, "line_number": 22, "type": "Secret Keyword", @@ -297,6 +308,7 @@ "pkg/mounter/mounter_test.go": [ { "hashed_secret": "2e7a7ee14caebf378fc32d6cf6f557f347c96773", + "is_secret": true, "is_verified": false, "line_number": 45, "type": "Secret Keyword", @@ -331,6 +343,7 @@ "pkg/s3client/s3client_test.go": [ { "hashed_secret": "d4c3d66fd0c38547a3c7a4c6bdc29c36911bc030", + "is_secret": true, "is_verified": false, "line_number": 57, "type": "Secret Keyword", @@ -338,6 +351,7 @@ }, { "hashed_secret": "2fa0ceed0ed702c459dd69734dfa1bc54e0bdd70", + "is_secret": true, "is_verified": false, "line_number": 123, "type": "Secret Keyword", @@ -347,6 +361,7 @@ "pkg/utils/driver_utils.go": [ { "hashed_secret": "c7c6508b19455e3e8040e60e9833fbede92e5d8e", + "is_secret": true, "is_verified": false, "line_number": 376, "type": "Secret Keyword", @@ -356,6 +371,7 @@ "tests/e2e/s3fs_test.go": [ { "hashed_secret": "d5cd5ec53cf11ad72f24451e6c077d94d0481046", + "is_secret": true, "is_verified": false, "line_number": 147, "type": "Secret Keyword", @@ -365,6 +381,7 @@ "tests/e2e/testsuites/basespecs.go": [ { "hashed_secret": "7519d22977923ba306715e7048cede9b5ee60d0b", + "is_secret": true, "is_verified": false, "line_number": 117, "type": "Secret Keyword", diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go index f862583f..b7807dcd 100644 --- a/pkg/mounter/mounter-s3mounter_test.go +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -17,9 +17,7 @@ var ( "accessKey": "test-access-key", "secretKey": "test-secret-key", "uid": "1000", - "gid": "1000", "mountOptions": `log-level=debug -read-only cache=/tmp/cache`, } @@ -27,7 +25,7 @@ cache=/tmp/cache`, ) func TestNewMountpointS3Mounter_Success(t *testing.T) { - mounter := NewMountpointS3Mounter(s3MounterSecretMap, s3MounterMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + mounter := NewMountpointS3Mounter(s3MounterSecretMap, s3MounterMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), "1000", true) s3Mounter, ok := mounter.(*MountpointS3Mounter) assert.True(t, ok) @@ -39,7 +37,7 @@ func TestNewMountpointS3Mounter_Success(t *testing.T) { assert.Equal(t, s3MounterSecretMap["accessKey"], s3Mounter.AccessKey) assert.Equal(t, s3MounterSecretMap["secretKey"], s3Mounter.SecretKey) assert.Equal(t, s3MounterSecretMap["uid"], s3Mounter.UID) - assert.Equal(t, s3MounterSecretMap["gid"], s3Mounter.GID) + assert.Equal(t, "1000", s3Mounter.GID) assert.Equal(t, "debug", s3Mounter.LogLevel) assert.True(t, s3Mounter.ReadOnly) assert.Equal(t, "/tmp/cache", s3Mounter.CacheDir) @@ -50,14 +48,13 @@ func TestNewMountpointS3Mounter_Success(t *testing.T) { } func TestNewMountpointS3Mounter_GidWithoutUid(t *testing.T) { - secretMap := map[string]string{ - "gid": "2000", - } + secretMap := map[string]string{} - mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), "2000", false) s3Mounter, ok := mounter.(*MountpointS3Mounter) assert.True(t, ok) + // When gid is set and no uid in secretMap, UID is also set to gid value. assert.Equal(t, "2000", s3Mounter.UID) assert.Equal(t, "2000", s3Mounter.GID) } @@ -65,13 +62,13 @@ func TestNewMountpointS3Mounter_GidWithoutUid(t *testing.T) { func TestNewMountpointS3Mounter_UidOverridesGid(t *testing.T) { secretMap := map[string]string{ "uid": "1000", - "gid": "2000", } - mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), "2000", false) s3Mounter, ok := mounter.(*MountpointS3Mounter) assert.True(t, ok) + // uid from secretMap takes precedence over gid param for the UID field. assert.Equal(t, "1000", s3Mounter.UID) assert.Equal(t, "2000", s3Mounter.GID) } @@ -739,7 +736,8 @@ func TestParseMountpointS3Options(t *testing.T) { GID: "2000", LogLevel: "debug", CacheDir: "/tmp/cache", - ReadOnly: true, + // read-only is now set via MounterParams.ReadOnly, not parsed from flags. + ReadOnly: false, }, expectedRemaining: []string{}, }, @@ -815,7 +813,7 @@ func TestNewMountpointS3Mounter_IncrementalUploadBlocked(t *testing.T) { allow-overwrite`, } - mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), "", false) s3Mounter, ok := mounter.(*MountpointS3Mounter) assert.True(t, ok) @@ -839,7 +837,7 @@ maximum-throughput-gbps=25`, // --allow-delete is now hardcoded β€” it must not appear in passthrough MountOptions. scMountOptions := []string{"--max-threads=32", "--maximum-throughput-gbps=10", "--allow-delete"} - mounter := NewMountpointS3Mounter(secretMap, scMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + mounter := NewMountpointS3Mounter(secretMap, scMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), "", false) s3Mounter, ok := mounter.(*MountpointS3Mounter) assert.True(t, ok) diff --git a/pkg/mounter/mounter.go b/pkg/mounter/mounter.go index ce15d2b9..fe8d8a5f 100644 --- a/pkg/mounter/mounter.go +++ b/pkg/mounter/mounter.go @@ -105,7 +105,7 @@ func (s *CSIMounterFactory) NewMounter(params MounterParams) Mounter { ReadOnly: params.ReadOnly, }) case constants.S3MOUNTER: - return NewMountpointS3Mounter(secretMap, mountFlags, mounterUtils) + return NewMountpointS3Mounter(secretMap, mountFlags, mounterUtils, params.Gid, params.ReadOnly) default: // default to s3fs return NewS3fsMounter(S3fsMounterParams{ diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index ee1acf4b..0c209e47 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -105,7 +105,7 @@ type envMounter interface { // // For passthrough flags, deduplication is applied: if the same flag key appears // in both SC and secret, the SC entry is dropped and the secret entry is kept. -func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounterUtils utils.MounterUtils) Mounter { +func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounterUtils utils.MounterUtils, gid string, readOnly bool) Mounter { klog.Info("-newMountpointS3Mounter-") mounter := &MountpointS3Mounter{} @@ -129,14 +129,14 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounter.LocConstraint = val } - if secretMap["gid"] != "" && secretMap["uid"] == "" { - mounter.UID = secretMap["gid"] + if gid != "" && secretMap["uid"] == "" { + mounter.UID = gid } else if val := secretMap["uid"]; val != "" { mounter.UID = val } - if val := secretMap["gid"]; val != "" { - mounter.GID = val - } + mounter.GID = gid + + mounter.ReadOnly = readOnly mounter.AuthType = "hmac" @@ -270,7 +270,8 @@ func parseMountpointS3Options(mounter *MountpointS3Mounter, opts []string) []str mounter.CacheDir = value } case "read-only": - mounter.ReadOnly = true + // read-only is now set directly from MounterParams.ReadOnly β€” ignore if passed via flags. + klog.Warningf("parseMountpointS3Options: 'read-only' is set via MounterParams and cannot be overridden via mount options. Ignoring.") case "allow-delete": // Hardcoded β€” always enabled unless read-only. User override ignored. klog.Warningf("parseMountpointS3Options: 'allow-delete' is hardcoded and cannot be overridden. Ignoring.") From 4809fd69387edb349e7c6366bcdb50ad53adc4a9 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 20 Jul 2026 00:38:13 +0530 Subject: [PATCH 110/128] publish 0.12.29 version --- pkg/mounter/mounter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/mounter/mounter.go b/pkg/mounter/mounter.go index fe8d8a5f..2025cd00 100644 --- a/pkg/mounter/mounter.go +++ b/pkg/mounter/mounter.go @@ -104,7 +104,7 @@ func (s *CSIMounterFactory) NewMounter(params MounterParams) Mounter { Gid: params.Gid, ReadOnly: params.ReadOnly, }) - case constants.S3MOUNTER: + case constants.AMAZONS3MOUNTER: return NewMountpointS3Mounter(secretMap, mountFlags, mounterUtils, params.Gid, params.ReadOnly) default: // default to s3fs From 95da11096c1e2c255cedb61392848185877277e3 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Mon, 20 Jul 2026 00:51:08 +0530 Subject: [PATCH 111/128] publish 0.12.49 version --- pkg/mounter/mounter_s3mounter.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter_s3mounter.go index 0c209e47..b33ce091 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter_s3mounter.go @@ -134,7 +134,11 @@ func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, } else if val := secretMap["uid"]; val != "" { mounter.UID = val } - mounter.GID = gid + if val := secretMap["gid"]; val != "" { + mounter.GID = val + } else { + mounter.GID = gid + } mounter.ReadOnly = readOnly From 00a8bbf18dcee8473149902bd16c7b83c7ae1b2e Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Tue, 18 Aug 2026 19:22:18 +0530 Subject: [PATCH 112/128] publish 0.12.50 version --- cos-csi-mounter/server/s3mounter.go | 13 +++++-- cos-csi-mounter/server/utils.go | 25 +++++++++----- cos-csi-mounter/server/utils_test.go | 51 ++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 11 deletions(-) diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go index bb8c040b..ffbc6c4a 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/s3mounter.go @@ -6,6 +6,13 @@ import ( "go.uber.org/zap" ) +var ( + // Safe base directory for mount-s3 log files. + s3MounterSafeLogDir = "/var/log/mount-s3" + // Safe base directory for mount-s3 cache files. + s3MounterSafeCacheDir = "/tmp" +) + // s3MounterArgs holds the args sent from the node server for mount-s3. // This struct must match the s3MounterArgs in pkg/mounter/mounter_s3mounter.go type s3MounterArgs struct { @@ -191,15 +198,15 @@ func (args s3MounterArgs) Validate(targetPath string) error { } } - // ensureDir creates the directory if it does not exist. + // ensureDir creates the directory if it does not exist, confined to its safe base. if args.CacheDir != "" { - if err := ensureDir(args.CacheDir); err != nil { + if err := ensureDir(args.CacheDir, s3MounterSafeCacheDir); err != nil { logger.Error("failed to create cache directory", zap.String("cache-dir", args.CacheDir), zap.Error(err)) return fmt.Errorf("failed to create cache directory '%s': %w", args.CacheDir, err) } } if args.LogDirectory != "" { - if err := ensureDir(args.LogDirectory); err != nil { + if err := ensureDir(args.LogDirectory, s3MounterSafeLogDir); err != nil { logger.Error("failed to create log directory", zap.String("log-dir", args.LogDirectory), zap.Error(err)) return fmt.Errorf("failed to create log directory '%s': %w", args.LogDirectory, err) } diff --git a/cos-csi-mounter/server/utils.go b/cos-csi-mounter/server/utils.go index 48915dbc..a927998f 100644 --- a/cos-csi-mounter/server/utils.go +++ b/cos-csi-mounter/server/utils.go @@ -126,27 +126,36 @@ func fileExists(path string) (bool, error) { return !info.IsDir(), nil } -// ensureDir creates the cache directory if it doesn't exist. -// It creates the directory with 0755 permissions (rwxr-xr-x). -func ensureDir(path string) error { +// ensureDir creates the directory at path if it doesn't exist. +// It first resolves the absolute path and verifies it is within safeBase +// to prevent path-traversal attacks before creating the directory. +func ensureDir(path, safeBase string) error { + absPath, err := absPathResolver(path) + if err != nil { + return fmt.Errorf("failed to resolve absolute path: %v", err) + } + if !strings.HasPrefix(absPath, safeBase) { + return fmt.Errorf("path %v is outside the safe directory", absPath) + } + // Check if directory already exists - info, err := os.Stat(path) + info, err := os.Stat(absPath) if err == nil { // Path exists, verify it's a directory if !info.IsDir() { - return fmt.Errorf("cache path exists but is not a directory: %s", path) + return fmt.Errorf("path exists but is not a directory: %s", absPath) } return nil } // If error is not "not exist", return it if !os.IsNotExist(err) { - return fmt.Errorf("failed to stat cache directory: %w", err) + return fmt.Errorf("failed to stat directory: %w", err) } // Directory doesn't exist, create it with parent directories - if err := os.MkdirAll(path, 0750); err != nil { // #nosec G301 -- cache directory permissions set to 0750 for security - return fmt.Errorf("failed to create cache directory: %w", err) + if err := os.MkdirAll(absPath, 0750); err != nil { // #nosec G301 -- directory permissions set to 0750 for security + return fmt.Errorf("failed to create directory: %w", err) } return nil diff --git a/cos-csi-mounter/server/utils_test.go b/cos-csi-mounter/server/utils_test.go index f111b021..8fc5f86a 100644 --- a/cos-csi-mounter/server/utils_test.go +++ b/cos-csi-mounter/server/utils_test.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "errors" + "os" "path/filepath" "testing" @@ -148,3 +149,53 @@ func TestFileExists_AbsFails(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "failed to resolve absolute path") } + +func TestEnsureDir_OutsideSafeDirectory(t *testing.T) { + err := ensureDir("/etc/passwd", "/tmp") + assert.Error(t, err) + assert.Contains(t, err.Error(), "outside the safe directory") +} + +func TestEnsureDir_PathTraversal(t *testing.T) { + err := ensureDir("/tmp/../../etc", "/tmp") + assert.Error(t, err) + assert.Contains(t, err.Error(), "outside the safe directory") +} + +func TestEnsureDir_CreatesDirectory(t *testing.T) { + dir := filepath.Join(t.TempDir(), "subdir") + safeBase := filepath.Dir(dir) + err := ensureDir(dir, safeBase) + assert.NoError(t, err) + info, statErr := os.Stat(dir) + assert.NoError(t, statErr) + assert.True(t, info.IsDir()) +} + +func TestEnsureDir_ExistingDirectory(t *testing.T) { + dir := t.TempDir() + err := ensureDir(dir, filepath.Dir(dir)) + assert.NoError(t, err) +} + +func TestEnsureDir_PathIsFile(t *testing.T) { + dir := t.TempDir() + file := filepath.Join(dir, "file.txt") + assert.NoError(t, os.WriteFile(file, []byte("x"), 0600)) + err := ensureDir(file, dir) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not a directory") +} + +func TestEnsureDir_AbsFails(t *testing.T) { + originalAbs := absPathResolver + defer func() { absPathResolver = originalAbs }() + + absPathResolver = func(path string) (string, error) { + return "", errors.New("abs error") + } + + err := ensureDir("some-path", "/tmp") + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to resolve absolute path") +} From 55297fa3f7afba42ba9d2fc7e5834ced1f91960b Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sun, 23 Aug 2026 22:53:29 +0530 Subject: [PATCH 113/128] publish 0.12.51 version --- .github/workflows/release.yml | 99 +++++++------ cos-csi-mounter/Makefile | 2 +- cos-csi-mounter/server/s3mounter.go | 12 -- ...nter_s3mounter.go => mounter-s3mounter.go} | 34 +++-- pkg/mounter/mounter-s3mounter_test.go | 133 +++++++----------- pkg/mounter/mounter.go | 9 +- pkg/mounter/utils/fake_mounter_utils.go | 12 +- pkg/mounter/utils/mounter_utils.go | 4 +- 8 files changed, 133 insertions(+), 172 deletions(-) rename pkg/mounter/{mounter_s3mounter.go => mounter-s3mounter.go} (94%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 838f38fb..b1feac85 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,10 +3,10 @@ name: CI & Release on: push: branches: - - aws-s3mounter + - main pull_request: branches: - - aws-s3mounter + - main concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest outputs: should_release: ${{ steps.check_release.outputs.should_release }} - + steps: - name: Checkout Code uses: actions/checkout@v7 @@ -25,12 +25,9 @@ jobs: - name: Print Go Version run: go version - - name: Print Go Version - run: go version - - name: Get last commit message id: check_commit - run: | + run: | message=$(git log -1 --pretty=%B) message="${message//'%'/'%25'}" # Escape '%' message="${message//$'\n'/'%0A'}" # Escape newlines @@ -41,7 +38,7 @@ jobs: - name: Check Commit Message run: | echo "Commit Message: ${{ steps.check_commit.outputs.message }}" - + - name: Check if release should be triggered id: check_release run: | @@ -57,7 +54,7 @@ jobs: needs: common-setup runs-on: ubuntu-latest permissions: - contents: write # needed to push coverage to gh-pages + contents: write # needed to push coverage to gh-pages pull-requests: write # needed to comment on PRs steps: @@ -66,42 +63,42 @@ jobs: with: fetch-depth: 0 - - name: Install system dependencies (bc + docker) - run: | - sudo apt-get update -qq - sudo apt-get install -y bc docker-ce-cli - - - name: Prepare environment - run: | - export GO111MODULE=on - go mod tidy - make deps - - - name: Check code formatting - run: make fmt - - - name: Run unit tests - run: make test - - - name: Generate coverage - run: | - make coverage - ./scripts/calculateCoverage.sh - touch "Passing" || touch "Failed" - - - name: Build driver image - run: | - make driver - env: - RHSM_USER: ${{ secrets.RHSM_USER }} - RHSM_PASS: ${{ secrets.RHSM_PASS }} - - - name: Publish coverage - if: success() - env: - GHE_TOKEN: ${{ secrets.GHE_TOKEN }} - run: | - ./scripts/publishCoverage.sh + - name: Install system dependencies (bc + docker) + run: | + sudo apt-get update -qq + sudo apt-get install -y bc docker-ce-cli + + - name: Prepare environment + run: | + export GO111MODULE=on + go mod tidy + make deps + + - name: Check code formatting + run: make fmt + + - name: Run unit tests + run: make test + + - name: Generate coverage + run: | + make coverage + ./scripts/calculateCoverage.sh + touch "Passing" || touch "Failed" + + - name: Build driver image + run: | + make driver + env: + RHSM_USER: ${{ secrets.RHSM_USER }} + RHSM_PASS: ${{ secrets.RHSM_PASS }} + + - name: Publish coverage + if: success() + env: + GHE_TOKEN: ${{ secrets.GHE_TOKEN }} + run: | + ./scripts/publishCoverage.sh release: needs: common-setup @@ -112,11 +109,11 @@ jobs: strategy: matrix: package_dir: - - cos-csi-mounter + - cos-csi-mounter env: IS_LATEST_RELEASE: 'true' - APP_VERSION: 0.12.50 + APP_VERSION: 0.12.51 steps: - name: Checkout Code @@ -142,12 +139,12 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.50 - name: v0.12.50 + tag_name: v0.12.51 + name: v0.12.51 body: | ## πŸš€ What’s New - - Testing aws-s3Mounter image - - Publishing new test version + - Add support for AddMountParam to pass additional s3fs mount options + - AddMountParam allows passing s3fs mount options as a comma-separated string prerelease: ${{ env.IS_LATEST_RELEASE != 'true' }} - name: Perform CodeQL Analysis diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index 210cdc13..a584a22c 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.50 +APP_VERSION := 0.12.51 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go index ffbc6c4a..1434ccbf 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/s3mounter.go @@ -139,18 +139,6 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string return result, nil } -// EnvVars returns the environment variables for the mount-s3 subprocess. -func (args s3MounterArgs) EnvVars() []string { - var envVars []string - if args.AwsCredentialsFile != "" { - envVars = append(envVars, "AWS_SHARED_CREDENTIALS_FILE="+args.AwsCredentialsFile) - } - if args.AwsConfigFile != "" { - envVars = append(envVars, "AWS_CONFIG_FILE="+args.AwsConfigFile) - } - return envVars -} - // Validate checks that required fields are present and values are valid. func (args s3MounterArgs) Validate(targetPath string) error { if err := pathValidator(targetPath); err != nil { diff --git a/pkg/mounter/mounter_s3mounter.go b/pkg/mounter/mounter-s3mounter.go similarity index 94% rename from pkg/mounter/mounter_s3mounter.go rename to pkg/mounter/mounter-s3mounter.go index b33ce091..1dd8fce7 100644 --- a/pkg/mounter/mounter_s3mounter.go +++ b/pkg/mounter/mounter-s3mounter.go @@ -91,8 +91,12 @@ var ( removeS3ConfigFile = removeS3MountConfigFile ) -type envMounter interface { - FuseMountWithEnv(path string, comm string, args []string, envVars []string) error +type MountpointS3MounterParams struct { + SecretMap map[string]string + MountOptions []string + MounterUtils utils.MounterUtils + Gid string + ReadOnly bool } // NewMountpointS3Mounter creates a new MountpointS3Mounter from the unified @@ -105,7 +109,13 @@ type envMounter interface { // // For passthrough flags, deduplication is applied: if the same flag key appears // in both SC and secret, the SC entry is dropped and the secret entry is kept. -func NewMountpointS3Mounter(secretMap map[string]string, mountOptions []string, mounterUtils utils.MounterUtils, gid string, readOnly bool) Mounter { +func NewMountpointS3Mounter(params MountpointS3MounterParams) Mounter { + secretMap := params.SecretMap + mountOptions := params.MountOptions + mounterUtils := params.MounterUtils + gid := params.Gid + readOnly := params.ReadOnly + klog.Info("-newMountpointS3Mounter-") mounter := &MountpointS3Mounter{} @@ -246,6 +256,7 @@ func parseMountpointS3Options(mounter *MountpointS3Mounter, opts []string) []str switch key { case "allow-other": + klog.Infof("parseMountpointS3Options: 'allow-other' is always enabled and cannot be overridden.") case "umask": klog.Warningf("parseMountpointS3Options: 'umask' is not supported by mount-s3. " + "Use --dir-mode / --file-mode instead. Ignoring.") @@ -311,10 +322,6 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { klog.Infof("MountpointS3Mounter: read-only set β€” clearing allow-overwrite") s3.AllowOverwrite = false } - if s3.IncrementalUpload { - klog.Infof("MountpointS3Mounter: read-only set β€” clearing incremental-upload") - s3.IncrementalUpload = false - } } // NOTE: No directory validation or creation here. @@ -367,12 +374,7 @@ func (s3 *MountpointS3Mounter) Mount(source string, target string) error { klog.Info("NodeServer Mounting...") - if m, ok := s3.MounterUtils.(envMounter); ok { - return m.FuseMountWithEnv(target, constants.MountpointS3BinaryPath, args, envVars) - } - - klog.Warning("MounterUtils does not implement FuseMountWithEnv, falling back to FuseMount.") - return s3.MounterUtils.FuseMount(target, constants.MountpointS3BinaryPath, args) + return s3.MounterUtils.FuseMountWithEnv(target, constants.MountpointS3BinaryPath, args, envVars) } // Unmount unmounts the S3 bucket. @@ -442,7 +444,7 @@ func createS3MountConfig(configPathWithVolID string, s3 *MountpointS3Mounter) er // writeConfigFile writes lines to a file with 0600 permissions. func writeConfigFile(filePath string, lines []string) error { - f, err := CreateFile(filePath) // #nosec G304 -- file path is constructed from controlled configPathWithVolID and constant filename + f, err := OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) // #nosec G304 -- file path is constructed from controlled configPathWithVolID and constant filename if err != nil { return fmt.Errorf("cannot create file %s: %w", filePath, err) } @@ -452,10 +454,6 @@ func writeConfigFile(filePath string, lines []string) error { } }() - if err := Chmod(filePath, 0600); err != nil { // #nosec G302 -- credentials file requires restrictive permissions (owner read/write only) - return fmt.Errorf("cannot chmod file %s: %w", filePath, err) - } - w := bufio.NewWriter(f) for _, line := range lines { if _, err := w.WriteString(line + "\n"); err != nil { diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go index b7807dcd..ed440529 100644 --- a/pkg/mounter/mounter-s3mounter_test.go +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -25,7 +25,13 @@ cache=/tmp/cache`, ) func TestNewMountpointS3Mounter_Success(t *testing.T) { - mounter := NewMountpointS3Mounter(s3MounterSecretMap, s3MounterMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), "1000", true) + mounter := NewMountpointS3Mounter(MountpointS3MounterParams{ + SecretMap: s3MounterSecretMap, + MountOptions: s3MounterMountOptions, + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), + Gid: "1000", + ReadOnly: true, + }) s3Mounter, ok := mounter.(*MountpointS3Mounter) assert.True(t, ok) @@ -50,7 +56,11 @@ func TestNewMountpointS3Mounter_Success(t *testing.T) { func TestNewMountpointS3Mounter_GidWithoutUid(t *testing.T) { secretMap := map[string]string{} - mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), "2000", false) + mounter := NewMountpointS3Mounter(MountpointS3MounterParams{ + SecretMap: secretMap, + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), + Gid: "2000", + }) s3Mounter, ok := mounter.(*MountpointS3Mounter) assert.True(t, ok) @@ -64,7 +74,11 @@ func TestNewMountpointS3Mounter_UidOverridesGid(t *testing.T) { "uid": "1000", } - mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), "2000", false) + mounter := NewMountpointS3Mounter(MountpointS3MounterParams{ + SecretMap: secretMap, + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), + Gid: "2000", + }) s3Mounter, ok := mounter.(*MountpointS3Mounter) assert.True(t, ok) @@ -79,21 +93,15 @@ func TestMountpointS3Mount_NodeServer_Positive(t *testing.T) { MakeDir = func(path string, perm os.FileMode) error { return nil } - CreateFile = func(path string) (*os.File, error) { + OpenFile = func(path string, flag int, perm os.FileMode) (*os.File, error) { return os.CreateTemp("", "test") } - Chmod = func(path string, perm os.FileMode) error { - return nil - } - // Create a mock that implements envMounter interface - mockUtils := &mockEnvMounter{ - MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ - FuseMountFn: func(path, comm string, args []string) error { - return nil - }, - }), - } + mockUtils := mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseMountWithEnvFn: func(path, comm string, args, envVars []string) error { + return nil + }, + }) s3Mounter := &MountpointS3Mounter{ MounterUtils: mockUtils, @@ -120,17 +128,15 @@ func TestMountpointS3Mount_NodeServer_WithEnvMounter(t *testing.T) { MakeDir = func(path string, perm os.FileMode) error { return nil } - CreateFile = func(path string) (*os.File, error) { + OpenFile = func(path string, flag int, perm os.FileMode) (*os.File, error) { return os.CreateTemp("", "test") } - Chmod = func(path string, perm os.FileMode) error { - return nil - } - // Create a mock that implements envMounter interface - mockUtils := &mockEnvMounter{ - MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), - } + mockUtils := mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseMountWithEnvFn: func(path, comm string, args, envVars []string) error { + return nil + }, + }) s3Mounter := &MountpointS3Mounter{ MounterUtils: mockUtils, @@ -152,12 +158,9 @@ func TestMountpointS3Mount_WorkerNode_Positive(t *testing.T) { MakeDir = func(path string, perm os.FileMode) error { return nil } - CreateFile = func(path string) (*os.File, error) { + OpenFile = func(path string, flag int, perm os.FileMode) (*os.File, error) { return os.CreateTemp("", "test") } - Chmod = func(path string, perm os.FileMode) error { - return nil - } mounterRequest = func(payload, url string) error { return nil } @@ -199,7 +202,7 @@ func TestMountpointS3Mount_CreateCredFileFails_Negative(t *testing.T) { MakeDir = func(path string, perm os.FileMode) error { return nil } - CreateFile = func(path string) (*os.File, error) { + OpenFile = func(path string, flag int, perm os.FileMode) (*os.File, error) { return nil, errors.New("failed to create file") } @@ -220,12 +223,9 @@ func TestMountpointS3Mount_WorkerNode_Negative(t *testing.T) { MakeDir = func(path string, perm os.FileMode) error { return nil } - CreateFile = func(path string) (*os.File, error) { + OpenFile = func(path string, flag int, perm os.FileMode) (*os.File, error) { return os.CreateTemp("", "test") } - Chmod = func(path string, perm os.FileMode) error { - return nil - } mounterRequest = func(payload, url string) error { return errors.New("failed to perform http request") } @@ -480,12 +480,9 @@ func TestCreateS3MountConfig_Success(t *testing.T) { MakeDir = func(path string, perm os.FileMode) error { return nil } - CreateFile = func(path string) (*os.File, error) { + OpenFile = func(path string, flag int, perm os.FileMode) (*os.File, error) { return os.CreateTemp("", "test") } - Chmod = func(path string, perm os.FileMode) error { - return nil - } s3Mounter := &MountpointS3Mounter{ AccessKey: "test-access-key", @@ -517,7 +514,7 @@ func TestCreateS3MountConfig_CreateFileFails(t *testing.T) { MakeDir = func(path string, perm os.FileMode) error { return nil } - CreateFile = func(path string) (*os.File, error) { + OpenFile = func(path string, flag int, perm os.FileMode) (*os.File, error) { return nil, errors.New("failed to create file") } @@ -531,37 +528,6 @@ func TestCreateS3MountConfig_CreateFileFails(t *testing.T) { assert.Contains(t, err.Error(), "failed to create file") } -func TestCreateS3MountConfig_ChmodFails(t *testing.T) { - MakeDir = func(path string, perm os.FileMode) error { - return nil - } - CreateFile = func(path string) (*os.File, error) { - return os.CreateTemp("", "test") - } - Chmod = func(path string, perm os.FileMode) error { - return errors.New("failed to chmod") - } - - s3Mounter := &MountpointS3Mounter{ - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - } - - err := createS3MountConfig("/config/path", s3Mounter) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to chmod") -} - -// mockEnvMounter implements the envMounter interface for testing -type mockEnvMounter struct { - mounterUtils.MounterUtils -} - -func (m *mockEnvMounter) FuseMountWithEnv(path string, comm string, args []string, envVars []string) error { - return nil -} - - // Test deduplicatePassthrough function func TestDeduplicatePassthrough(t *testing.T) { tests := []struct { @@ -861,27 +827,25 @@ func TestMountpointS3Mount_ReadOnlyPriority(t *testing.T) { MakeDir = func(path string, perm os.FileMode) error { return nil } - CreateFile = func(path string) (*os.File, error) { + OpenFile = func(path string, flag int, perm os.FileMode) (*os.File, error) { return os.CreateTemp("", "test") } - Chmod = func(path string, perm os.FileMode) error { - return nil - } - mockUtils := &mockEnvMounter{ - MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), - } + mockUtils := mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseMountWithEnvFn: func(path, comm string, args, envVars []string) error { + return nil + }, + }) s3Mounter := &MountpointS3Mounter{ - MounterUtils: mockUtils, - BucketName: "test-bucket", - AccessKey: "test-access-key", - SecretKey: "test-secret-key", - ReadOnly: true, - AllowDelete: true, // Should be cleared by read-only - AllowOverwrite: true, // Should be cleared by read-only - IncrementalUpload: true, // Should be cleared by read-only - MountOptions: []string{"--max-threads=32"}, + MounterUtils: mockUtils, + BucketName: "test-bucket", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + ReadOnly: true, + AllowDelete: true, // Should be cleared by read-only + AllowOverwrite: true, // Should be cleared by read-only + MountOptions: []string{"--max-threads=32"}, } err := s3Mounter.Mount(source, target) @@ -890,7 +854,6 @@ func TestMountpointS3Mount_ReadOnlyPriority(t *testing.T) { // Verify read-only priority cleared all write-enabling flags. assert.False(t, s3Mounter.AllowDelete) assert.False(t, s3Mounter.AllowOverwrite) - assert.False(t, s3Mounter.IncrementalUpload) // Passthrough options unrelated to write-access are preserved. assert.Contains(t, s3Mounter.MountOptions, "--max-threads=32") } diff --git a/pkg/mounter/mounter.go b/pkg/mounter/mounter.go index 2025cd00..f6e44199 100644 --- a/pkg/mounter/mounter.go +++ b/pkg/mounter/mounter.go @@ -27,6 +27,7 @@ var ( MakeDir = os.MkdirAll CreateFile = os.Create Chmod = os.Chmod + OpenFile = os.OpenFile Stat = os.Stat RemoveAll = os.RemoveAll ) @@ -105,7 +106,13 @@ func (s *CSIMounterFactory) NewMounter(params MounterParams) Mounter { ReadOnly: params.ReadOnly, }) case constants.AMAZONS3MOUNTER: - return NewMountpointS3Mounter(secretMap, mountFlags, mounterUtils, params.Gid, params.ReadOnly) + return NewMountpointS3Mounter(MountpointS3MounterParams{ + SecretMap: secretMap, + MountOptions: mountFlags, + MounterUtils: mounterUtils, + Gid: params.Gid, + ReadOnly: params.ReadOnly, + }) default: // default to s3fs return NewS3fsMounter(S3fsMounterParams{ diff --git a/pkg/mounter/utils/fake_mounter_utils.go b/pkg/mounter/utils/fake_mounter_utils.go index 72a843cd..1a4c8edb 100644 --- a/pkg/mounter/utils/fake_mounter_utils.go +++ b/pkg/mounter/utils/fake_mounter_utils.go @@ -1,8 +1,9 @@ package utils type FakeMounterUtilsFuncStruct struct { - FuseMountFn func(path string, comm string, args []string) error - FuseUnmountFn func(path string) error + FuseMountFn func(path string, comm string, args []string) error + FuseUnmountFn func(path string) error + FuseMountWithEnvFn func(path string, comm string, args []string, envVars []string) error } type FakeMounterUtilsFuncStructImpl struct { @@ -28,3 +29,10 @@ func (m *FakeMounterUtilsFuncStructImpl) FuseUnmount(path string) error { } panic("requested method should not be nil") } + +func (m *FakeMounterUtilsFuncStructImpl) FuseMountWithEnv(path string, comm string, args []string, envVars []string) error { + if m.FuncStruct.FuseMountWithEnvFn != nil { + return m.FuncStruct.FuseMountWithEnvFn(path, comm, args, envVars) + } + panic("requested method should not be nil") +} diff --git a/pkg/mounter/utils/mounter_utils.go b/pkg/mounter/utils/mounter_utils.go index 777095c4..87848804 100644 --- a/pkg/mounter/utils/mounter_utils.go +++ b/pkg/mounter/utils/mounter_utils.go @@ -27,6 +27,7 @@ var ErrTimeoutWaitProcess = errors.New("timeout waiting for process to end") type MounterUtils interface { FuseUnmount(path string) error FuseMount(path string, comm string, args []string) error + FuseMountWithEnv(path string, comm string, args []string, envVars []string) error } type MounterOptsUtils struct { @@ -37,8 +38,7 @@ func (su *MounterOptsUtils) FuseMount(path string, comm string, args []string) e return su.fuseMountInternal(path, comm, args, nil) } -// FuseMountWithEnv β€” on the concrete struct only, NOT on the interface. -// envVars are per-subprocess env vars (e.g. AWS_SHARED_CREDENTIALS_FILE) +// FuseMountWithEnv β€” envVars are per-subprocess env vars (e.g. AWS_SHARED_CREDENTIALS_FILE) func (su *MounterOptsUtils) FuseMountWithEnv(path string, comm string, args []string, envVars []string) error { return su.fuseMountInternal(path, comm, args, envVars) } From 4baf978f55c7d730c8c70b289b841a79be25bf51 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sun, 23 Aug 2026 22:59:01 +0530 Subject: [PATCH 114/128] publish 0.12.51 version --- cos-csi-mounter/server/fake_server.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cos-csi-mounter/server/fake_server.go b/cos-csi-mounter/server/fake_server.go index fe11f584..3d36d74f 100644 --- a/cos-csi-mounter/server/fake_server.go +++ b/cos-csi-mounter/server/fake_server.go @@ -27,6 +27,11 @@ func (m *MockMounterUtils) FuseUnmount(path string) error { return argsCalled.Error(0) } +func (m *MockMounterUtils) FuseMountWithEnv(path string, comm string, args []string, envVars []string) error { + argsCalled := m.Called(path, comm, args, envVars) + return argsCalled.Error(0) +} + type fakeListener struct{} func (d *fakeListener) Accept() (net.Conn, error) { From ea4444c8dbe0808fc9cbbd113b681eb86bd21603 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sun, 23 Aug 2026 23:06:32 +0530 Subject: [PATCH 115/128] publish 0.12.51 version --- pkg/mounter/mounter-s3mounter_test.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-s3mounter_test.go index ed440529..ec41f433 100644 --- a/pkg/mounter/mounter-s3mounter_test.go +++ b/pkg/mounter/mounter-s3mounter_test.go @@ -779,7 +779,10 @@ func TestNewMountpointS3Mounter_IncrementalUploadBlocked(t *testing.T) { allow-overwrite`, } - mounter := NewMountpointS3Mounter(secretMap, nil, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), "", false) + mounter := NewMountpointS3Mounter(MountpointS3MounterParams{ + SecretMap: secretMap, + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), + }) s3Mounter, ok := mounter.(*MountpointS3Mounter) assert.True(t, ok) @@ -803,7 +806,11 @@ maximum-throughput-gbps=25`, // --allow-delete is now hardcoded β€” it must not appear in passthrough MountOptions. scMountOptions := []string{"--max-threads=32", "--maximum-throughput-gbps=10", "--allow-delete"} - mounter := NewMountpointS3Mounter(secretMap, scMountOptions, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), "", false) + mounter := NewMountpointS3Mounter(MountpointS3MounterParams{ + SecretMap: secretMap, + MountOptions: scMountOptions, + MounterUtils: mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{}), + }) s3Mounter, ok := mounter.(*MountpointS3Mounter) assert.True(t, ok) From 4c61a5fad66dbc68528101a08f541bd7791ebb73 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sun, 23 Aug 2026 23:14:28 +0530 Subject: [PATCH 116/128] publish 0.12.51 version --- tests/sanity/sanity_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/sanity/sanity_test.go b/tests/sanity/sanity_test.go index d20c5139..ec16aff0 100644 --- a/tests/sanity/sanity_test.go +++ b/tests/sanity/sanity_test.go @@ -231,6 +231,10 @@ func (m *FakeNewMounterOptsUtils) FuseMount(path string, comm string, args []str return nil } +func (m *FakeNewMounterOptsUtils) FuseMountWithEnv(path string, comm string, args []string, envVars []string) error { + return nil +} + // Fake DriverStatsUtils type FakeNewDriverStatsUtils struct { } From 7cbc9aaf715e2b06ad9b86dbd476bef72ca76516 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Tue, 25 Aug 2026 11:57:22 +0530 Subject: [PATCH 117/128] publish 0.12.51 version --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1feac85..8bed79d7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -112,7 +112,7 @@ jobs: - cos-csi-mounter env: - IS_LATEST_RELEASE: 'true' + IS_LATEST_RELEASE: 'false' APP_VERSION: 0.12.51 steps: @@ -145,7 +145,7 @@ jobs: ## πŸš€ What’s New - Add support for AddMountParam to pass additional s3fs mount options - AddMountParam allows passing s3fs mount options as a comma-separated string - prerelease: ${{ env.IS_LATEST_RELEASE != 'true' }} + prerelease: true - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 From 1e83576858d03a75f2afe2a0e2a3c2795f8bd48f Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Thu, 27 Aug 2026 18:05:38 +0530 Subject: [PATCH 118/128] publish 0.12.51 version --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8bed79d7..e3db0612 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,10 +3,10 @@ name: CI & Release on: push: branches: - - main + - aws-s3mounter pull_request: branches: - - main + - aws-s3mounter concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} From 06a6749abc233b018ba622bcc9e8f2cb4f82130c Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sun, 30 Aug 2026 14:27:28 +0530 Subject: [PATCH 119/128] chore(aws-s3mounter): bump version to 1.0.7 and update release branch --- .github/workflows/release.yml | 2 +- cos-csi-mounter/Makefile | 2 +- cos-csi-mounter/server/utils.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e3db0612..dc31c898 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: CI & Release on: push: branches: - - aws-s3mounter + - main pull_request: branches: - aws-s3mounter diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index a584a22c..4a1373d7 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -1,5 +1,5 @@ NAME := cos-csi-mounter -APP_VERSION := 0.12.51 +APP_VERSION := 1.0.7 BUILD_DIR := $(NAME)-$(APP_VERSION) BIN_DIR := bin diff --git a/cos-csi-mounter/server/utils.go b/cos-csi-mounter/server/utils.go index a927998f..d2cf5bb9 100644 --- a/cos-csi-mounter/server/utils.go +++ b/cos-csi-mounter/server/utils.go @@ -130,7 +130,7 @@ func fileExists(path string) (bool, error) { // It first resolves the absolute path and verifies it is within safeBase // to prevent path-traversal attacks before creating the directory. func ensureDir(path, safeBase string) error { - absPath, err := absPathResolver(path) + absPath, err := absPathResolver(path) // #nosec G304 -- path is validated against safeBase below to prevent traversal if err != nil { return fmt.Errorf("failed to resolve absolute path: %v", err) } @@ -139,7 +139,7 @@ func ensureDir(path, safeBase string) error { } // Check if directory already exists - info, err := os.Stat(absPath) + info, err := os.Stat(absPath) // #nosec G304 -- absPath is validated against safeBase above if err == nil { // Path exists, verify it's a directory if !info.IsDir() { From 0949ac94577e7de14fcd4e3d5d8ba6d68d1eed0b Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Sun, 30 Aug 2026 14:31:11 +0530 Subject: [PATCH 120/128] chore(aws-s3mounter): bump version to 1.0.7 and update release branch --- .github/workflows/release.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dc31c898..840edc25 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -112,8 +112,8 @@ jobs: - cos-csi-mounter env: - IS_LATEST_RELEASE: 'false' - APP_VERSION: 0.12.51 + IS_LATEST_RELEASE: 'true' + APP_VERSION: 1.0.7 steps: - name: Checkout Code @@ -139,13 +139,13 @@ jobs: /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v0.12.51 - name: v0.12.51 + tag_name: v1.0.7 + name: v1.0.7 body: | ## πŸš€ What’s New - Add support for AddMountParam to pass additional s3fs mount options - AddMountParam allows passing s3fs mount options as a comma-separated string - prerelease: true + prerelease: ${{ env.IS_LATEST_RELEASE != 'true' }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@v4 \ No newline at end of file From 68f33e74d80da30836e113dba4270bb5f87e3275 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Wed, 2 Sep 2026 22:17:13 +0530 Subject: [PATCH 121/128] publish 0.12.51 version --- cos-csi-mounter/server/s3mounter.go | 24 +- cos-csi-mounter/server/s3mounter_test.go | 345 +++++++++++++++++++++++ cos-csi-mounter/server/server.go | 13 +- pkg/mounter/mounter-s3mounter.go | 2 +- 4 files changed, 368 insertions(+), 16 deletions(-) create mode 100644 cos-csi-mounter/server/s3mounter_test.go diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/s3mounter.go index 1434ccbf..6070b6ce 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/s3mounter.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "strconv" "go.uber.org/zap" ) @@ -73,10 +74,6 @@ func (args s3MounterArgs) PopulateArgsSlice(bucket, targetPath string) ([]string logger.Warn("read-only is set, clearing allow-overwrite") args.AllowOverwrite = "" } - if args.IncrementalUpload == "true" { - logger.Warn("read-only is set, clearing incremental-upload") - args.IncrementalUpload = "" - } } if args.AllowOther == "true" { @@ -186,6 +183,25 @@ func (args s3MounterArgs) Validate(targetPath string) error { } } + if args.UID != "" { + if _, err := strconv.Atoi(args.UID); err != nil { + logger.Error("uid must be a valid integer", zap.String("uid", args.UID)) + return fmt.Errorf("uid must be a valid integer: %v", args.UID) + } + } + if args.GID != "" { + if _, err := strconv.Atoi(args.GID); err != nil { + logger.Error("gid must be a valid integer", zap.String("gid", args.GID)) + return fmt.Errorf("gid must be a valid integer: %v", args.GID) + } + } + if args.ForcePathStyle != "" { + if isBool := isBoolString(args.ForcePathStyle); !isBool { + logger.Error("cannot convert value of force-path-style into boolean", zap.Any("force-path-style", args.ForcePathStyle)) + return fmt.Errorf("cannot convert value of force-path-style into boolean: %v", args.ForcePathStyle) + } + } + // ensureDir creates the directory if it does not exist, confined to its safe base. if args.CacheDir != "" { if err := ensureDir(args.CacheDir, s3MounterSafeCacheDir); err != nil { diff --git a/cos-csi-mounter/server/s3mounter_test.go b/cos-csi-mounter/server/s3mounter_test.go new file mode 100644 index 00000000..c55587fe --- /dev/null +++ b/cos-csi-mounter/server/s3mounter_test.go @@ -0,0 +1,345 @@ +package main + +import ( + "encoding/json" + "testing" + + "github.com/IBM/ibm-object-csi-driver/pkg/constants" + "github.com/stretchr/testify/assert" +) + +var ( + testCredFile = "/var/lib/coscsi-config/test/credentials" + testCfgFile = "/var/lib/coscsi-config/test/config" + testEndpoint = "https://s3.us-south.cloud-object-storage.appdomain.cloud" + testRegion = "us-south-standard" +) + +// --- PopulateArgsSlice tests --- + +func TestS3MounterPopulateArgsSlice_BasicArgs(t *testing.T) { + args := s3MounterArgs{ + AllowOther: "true", + AwsCredentialsFile: testCredFile, + AwsConfigFile: testCfgFile, + EndpointURL: testEndpoint, + Region: testRegion, + AllowDelete: "true", + AllowOverwrite: "true", + ForcePathStyle: "true", + } + + result, err := args.PopulateArgsSlice(testBucket, testTargetPath) + assert.NoError(t, err) + assert.Equal(t, testBucket, result[0]) + assert.Equal(t, testTargetPath, result[1]) + assert.Contains(t, result, "--allow-other") + assert.Contains(t, result, "--allow-delete") + assert.Contains(t, result, "--allow-overwrite") + assert.Contains(t, result, "--endpoint-url="+testEndpoint) + assert.Contains(t, result, "--region="+testRegion) + assert.Contains(t, result, "--force-path-style") +} + +func TestS3MounterPopulateArgsSlice_ReadOnlyClearsWriteFlags(t *testing.T) { + args := s3MounterArgs{ + AllowOther: "true", + ReadOnly: "true", + AllowDelete: "true", + AllowOverwrite: "true", + } + + result, err := args.PopulateArgsSlice(testBucket, testTargetPath) + assert.NoError(t, err) + assert.Contains(t, result, "--read-only") + assert.NotContains(t, result, "--allow-delete") + assert.NotContains(t, result, "--allow-overwrite") +} + +func TestS3MounterPopulateArgsSlice_IncrementalUploadBlocked(t *testing.T) { + args := s3MounterArgs{ + IncrementalUpload: "true", + } + + result, err := args.PopulateArgsSlice(testBucket, testTargetPath) + assert.NoError(t, err) + assert.NotContains(t, result, "--incremental-upload") +} + +func TestS3MounterPopulateArgsSlice_UIDAndGID(t *testing.T) { + args := s3MounterArgs{ + UID: "1000", + GID: "2000", + } + + result, err := args.PopulateArgsSlice(testBucket, testTargetPath) + assert.NoError(t, err) + assert.Contains(t, result, "--uid=1000") + assert.Contains(t, result, "--gid=2000") +} + +func TestS3MounterPopulateArgsSlice_LogLevels(t *testing.T) { + for _, level := range []string{"debug", "debug-crt", "no-log"} { + args := s3MounterArgs{LogLevel: level} + result, err := args.PopulateArgsSlice(testBucket, testTargetPath) + assert.NoError(t, err) + assert.Contains(t, result, "--"+level) + } +} + +func TestS3MounterPopulateArgsSlice_UnsupportedLogLevel(t *testing.T) { + args := s3MounterArgs{LogLevel: "verbose"} + result, err := args.PopulateArgsSlice(testBucket, testTargetPath) + assert.NoError(t, err) + // unsupported log level is silently ignored + assert.NotContains(t, result, "--verbose") +} + +func TestS3MounterPopulateArgsSlice_PassthroughArgs(t *testing.T) { + args := s3MounterArgs{ + Args: []string{"--max-threads=32", "--read-part-size=16777216"}, + } + + result, err := args.PopulateArgsSlice(testBucket, testTargetPath) + assert.NoError(t, err) + assert.Contains(t, result, "--max-threads=32") + assert.Contains(t, result, "--read-part-size=16777216") +} + +func TestS3MounterPopulateArgsSlice_CacheAndLogDir(t *testing.T) { + args := s3MounterArgs{ + CacheDir: "/tmp/cache", + LogDirectory: "/var/log/mount-s3", + } + + result, err := args.PopulateArgsSlice(testBucket, testTargetPath) + assert.NoError(t, err) + assert.Contains(t, result, "--cache=/tmp/cache") + assert.Contains(t, result, "--log-directory=/var/log/mount-s3") +} + +// --- Validate tests --- + +func TestS3MounterValidate_Success(t *testing.T) { + args := s3MounterArgs{ + AllowOther: "true", + AwsCredentialsFile: testCredFile, + AwsConfigFile: testCfgFile, + } + + err := args.Validate(testTargetPath) + assert.NoError(t, err) +} + +func TestS3MounterValidate_InvalidPath(t *testing.T) { + args := s3MounterArgs{ + AwsCredentialsFile: testCredFile, + AwsConfigFile: testCfgFile, + } + + err := args.Validate("invalid-path") + assert.Error(t, err) + assert.Contains(t, err.Error(), "bad value for target path") +} + +func TestS3MounterValidate_MissingCredentialsFile(t *testing.T) { + args := s3MounterArgs{ + AwsConfigFile: testCfgFile, + } + + err := args.Validate(testTargetPath) + assert.Error(t, err) + assert.Contains(t, err.Error(), "aws-credentials-file is required") +} + +func TestS3MounterValidate_MissingConfigFile(t *testing.T) { + args := s3MounterArgs{ + AwsCredentialsFile: testCredFile, + } + + err := args.Validate(testTargetPath) + assert.Error(t, err) + assert.Contains(t, err.Error(), "aws-config-file is required") +} + +func TestS3MounterValidate_InvalidAllowOther(t *testing.T) { + args := s3MounterArgs{ + AllowOther: "yes", + AwsCredentialsFile: testCredFile, + AwsConfigFile: testCfgFile, + } + + err := args.Validate(testTargetPath) + assert.Error(t, err) + assert.Contains(t, err.Error(), "cannot convert value of allow-other into boolean") +} + +func TestS3MounterValidate_InvalidBoolFields(t *testing.T) { + base := s3MounterArgs{ + AwsCredentialsFile: testCredFile, + AwsConfigFile: testCfgFile, + } + + tests := []struct { + name string + apply func(*s3MounterArgs) + errMsg string + }{ + {"invalid read-only", func(a *s3MounterArgs) { a.ReadOnly = "yes" }, "cannot convert value of read-only"}, + {"invalid allow-delete", func(a *s3MounterArgs) { a.AllowDelete = "yes" }, "cannot convert value of allow-delete"}, + {"invalid allow-overwrite", func(a *s3MounterArgs) { a.AllowOverwrite = "yes" }, "cannot convert value of allow-overwrite"}, + {"invalid incremental-upload", func(a *s3MounterArgs) { a.IncrementalUpload = "yes" }, "cannot convert value of incremental-upload"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := base + tt.apply(&args) + err := args.Validate(testTargetPath) + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.errMsg) + }) + } +} + +func TestS3MounterValidate_InvalidUID(t *testing.T) { + args := s3MounterArgs{ + AwsCredentialsFile: testCredFile, + AwsConfigFile: testCfgFile, + UID: "notanint", + } + err := args.Validate(testTargetPath) + assert.Error(t, err) + assert.Contains(t, err.Error(), "uid must be a valid integer") +} + +func TestS3MounterValidate_InvalidGID(t *testing.T) { + args := s3MounterArgs{ + AwsCredentialsFile: testCredFile, + AwsConfigFile: testCfgFile, + GID: "notanint", + } + err := args.Validate(testTargetPath) + assert.Error(t, err) + assert.Contains(t, err.Error(), "gid must be a valid integer") +} + +func TestS3MounterValidate_InvalidForcePathStyle(t *testing.T) { + args := s3MounterArgs{ + AwsCredentialsFile: testCredFile, + AwsConfigFile: testCfgFile, + ForcePathStyle: "yes", + } + err := args.Validate(testTargetPath) + assert.Error(t, err) + assert.Contains(t, err.Error(), "cannot convert value of force-path-style into boolean") +} + +func TestS3MounterValidate_ValidUIDAndGID(t *testing.T) { + args := s3MounterArgs{ + AwsCredentialsFile: testCredFile, + AwsConfigFile: testCfgFile, + UID: "1000", + GID: "2000", + } + err := args.Validate(testTargetPath) + assert.NoError(t, err) +} + +// --- ParseMounterArgs with AMAZONS3MOUNTER tests (N2) --- + +func TestParseMounterArgs_AmazonS3_Valid(t *testing.T) { + argsStruct := s3MounterArgs{ + AllowOther: "true", + AwsCredentialsFile: testCredFile, + AwsConfigFile: testCfgFile, + EndpointURL: testEndpoint, + Region: testRegion, + AllowDelete: "true", + AllowOverwrite: "true", + ForcePathStyle: "true", + } + b, _ := json.Marshal(argsStruct) + + req := MountRequest{ + Path: testTargetPath, + Bucket: testBucket, + Mounter: constants.AMAZONS3MOUNTER, + Args: b, + } + + args, err := req.ParseMounterArgs() + assert.NoError(t, err) + assert.NotNil(t, args) + assert.Contains(t, args, testBucket) + assert.Contains(t, args, "--allow-other") + assert.Contains(t, args, "--endpoint-url="+testEndpoint) +} + +func TestParseMounterArgs_AmazonS3_InvalidJSON(t *testing.T) { + req := MountRequest{ + Path: testTargetPath, + Bucket: testBucket, + Mounter: constants.AMAZONS3MOUNTER, + Args: json.RawMessage(`{"invalid-json"}`), + } + + args, err := req.ParseMounterArgs() + assert.Error(t, err) + assert.Nil(t, args) + assert.Contains(t, err.Error(), "invalid mount-s3 args decode error") +} + +func TestParseMounterArgs_AmazonS3_UnknownField(t *testing.T) { + req := MountRequest{ + Path: testTargetPath, + Bucket: testBucket, + Mounter: constants.AMAZONS3MOUNTER, + Args: json.RawMessage(`{"unknown-field": "value"}`), + } + + args, err := req.ParseMounterArgs() + assert.Error(t, err) + assert.Nil(t, args) + assert.Contains(t, err.Error(), "invalid mount-s3 args decode error") +} + +func TestParseMounterArgs_AmazonS3_ValidationFails(t *testing.T) { + argsStruct := s3MounterArgs{ + // Missing aws-credentials-file and aws-config-file + AllowOther: "true", + } + b, _ := json.Marshal(argsStruct) + + req := MountRequest{ + Path: testTargetPath, + Bucket: testBucket, + Mounter: constants.AMAZONS3MOUNTER, + Args: b, + } + + args, err := req.ParseMounterArgs() + assert.Error(t, err) + assert.Nil(t, args) + assert.Contains(t, err.Error(), "s3Mounter args validation failed") +} + +func TestParseMounterArgs_AmazonS3_InvalidTargetPath(t *testing.T) { + argsStruct := s3MounterArgs{ + AwsCredentialsFile: testCredFile, + AwsConfigFile: testCfgFile, + } + b, _ := json.Marshal(argsStruct) + + req := MountRequest{ + Path: "invalid-path", + Bucket: testBucket, + Mounter: constants.AMAZONS3MOUNTER, + Args: b, + } + + args, err := req.ParseMounterArgs() + assert.Error(t, err) + assert.Nil(t, args) + assert.Contains(t, err.Error(), "s3Mounter args validation failed") +} diff --git a/cos-csi-mounter/server/server.go b/cos-csi-mounter/server/server.go index ff39cb94..6deb56fe 100644 --- a/cos-csi-mounter/server/server.go +++ b/cos-csi-mounter/server/server.go @@ -32,13 +32,6 @@ var ( GitCommit = "none" ) -// envMounter is a local interface for type assertion β€” allows calling -// FuseMountWithEnv on the concrete MounterOptsUtils without changing the -// MounterUtils interface, so other mounters (rclone, s3fs) are unaffected. -type envMounter interface { - FuseMountWithEnv(path string, comm string, args []string, envVars []string) error -} - func init() { _ = flag.Set("logtostderr", "true") // #nosec G104: Attempt to set flags for logging to stderr only on best-effort basis.Error cannot be usefully handled. logger = setUpLogger() @@ -200,10 +193,8 @@ func handleCosMount(mounter mounterUtils.MounterUtils, parser MounterArgsParser) } // For mount-s3: pass AWS credential paths as env vars on the subprocess. - // FuseMountWithEnv is on the concrete MounterOptsUtils struct only (not the interface), - // so we type-assert β€” other mounters (rclone, s3fs) fall through to plain FuseMount. - if em, ok := mounter.(envMounter); ok && request.Mounter == constants.AMAZONS3MOUNTER { - err = em.FuseMountWithEnv(request.Path, request.Mounter, args, buildS3EnvVars(request.Args)) + if request.Mounter == constants.AMAZONS3MOUNTER { + err = mounter.FuseMountWithEnv(request.Path, request.Mounter, args, buildS3EnvVars(request.Args)) } else { err = mounter.FuseMount(request.Path, request.Mounter, args) } diff --git a/pkg/mounter/mounter-s3mounter.go b/pkg/mounter/mounter-s3mounter.go index 1dd8fce7..c3c692ce 100644 --- a/pkg/mounter/mounter-s3mounter.go +++ b/pkg/mounter/mounter-s3mounter.go @@ -237,7 +237,7 @@ func splitSecretMountOptions(raw string) []string { var opts []string for _, line := range strings.Split(raw, "\n") { line = strings.TrimSpace(line) - line = strings.TrimRight(line, ",") // ← ADD THIS + line = strings.TrimRight(line, ",") if line == "" || strings.HasPrefix(line, "#") { continue } From f679400ddcc096f44afd0947c1e5b01e3778d345 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Wed, 2 Sep 2026 22:23:08 +0530 Subject: [PATCH 122/128] publish 0.12.52 version --- cos-csi-mounter/server/s3mounter_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cos-csi-mounter/server/s3mounter_test.go b/cos-csi-mounter/server/s3mounter_test.go index c55587fe..4bd7086e 100644 --- a/cos-csi-mounter/server/s3mounter_test.go +++ b/cos-csi-mounter/server/s3mounter_test.go @@ -181,8 +181,8 @@ func TestS3MounterValidate_InvalidBoolFields(t *testing.T) { } tests := []struct { - name string - apply func(*s3MounterArgs) + name string + apply func(*s3MounterArgs) errMsg string }{ {"invalid read-only", func(a *s3MounterArgs) { a.ReadOnly = "yes" }, "cannot convert value of read-only"}, @@ -306,7 +306,6 @@ func TestParseMounterArgs_AmazonS3_UnknownField(t *testing.T) { func TestParseMounterArgs_AmazonS3_ValidationFails(t *testing.T) { argsStruct := s3MounterArgs{ - // Missing aws-credentials-file and aws-config-file AllowOther: "true", } b, _ := json.Marshal(argsStruct) From 8a27952395d0c6ff736c56571fd4ce8926cceac4 Mon Sep 17 00:00:00 2001 From: Dhruv Bavadiya Date: Wed, 2 Sep 2026 22:24:04 +0530 Subject: [PATCH 123/128] publish 0.12.52 version --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 840edc25..08c91eea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: CI & Release on: push: branches: - - main + - aws-s3mounter pull_request: branches: - aws-s3mounter From d38b651c3cf13784b11e1a656cdd5ea5392bc60f Mon Sep 17 00:00:00 2001 From: Ashima-Ashima1 Date: Thu, 5 Nov 2026 14:52:06 +0530 Subject: [PATCH 124/128] rename files --- .secrets.baseline | 32 +++++++++---------- .../server/{s3mounter.go => mount-s3.go} | 2 +- .../{s3mounter_test.go => mount-s3_test.go} | 0 ...unter-s3mounter.go => mounter-mount-s3.go} | 0 ...unter_test.go => mounter-mount-s3_test.go} | 0 pkg/mounter/utils/mounter_utils_stub.go | 2 +- 6 files changed, 18 insertions(+), 18 deletions(-) rename cos-csi-mounter/server/{s3mounter.go => mount-s3.go} (99%) rename cos-csi-mounter/server/{s3mounter_test.go => mount-s3_test.go} (100%) rename pkg/mounter/{mounter-s3mounter.go => mounter-mount-s3.go} (100%) rename pkg/mounter/{mounter-s3mounter_test.go => mounter-mount-s3_test.go} (100%) diff --git a/.secrets.baseline b/.secrets.baseline index a5a411bb..523411e2 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -237,6 +237,22 @@ "verified_result": null } ], + "pkg/mounter/mounter-mount-s3.go": [ + { + "hashed_secret": "a244263a4d970da788fefed5992903a119375616", + "is_verified": false, + "line_number": 197, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "2ace62c1befa19e3ea37dd52be9f6d508c5163e6", + "is_verified": false, + "line_number": 420, + "type": "Secret Keyword", + "verified_result": null + } + ], "pkg/mounter/mounter-rclone.go": [ { "hashed_secret": "39f69c278f46165447f30d10acf54277aaa3d5fc", @@ -333,22 +349,6 @@ "verified_result": null } ], - "pkg/mounter/mounter-s3mounter.go": [ - { - "hashed_secret": "a244263a4d970da788fefed5992903a119375616", - "is_verified": false, - "line_number": 197, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "2ace62c1befa19e3ea37dd52be9f6d508c5163e6", - "is_verified": false, - "line_number": 420, - "type": "Secret Keyword", - "verified_result": null - } - ], "pkg/mounter/mounter_test.go": [ { "hashed_secret": "2e7a7ee14caebf378fc32d6cf6f557f347c96773", diff --git a/cos-csi-mounter/server/s3mounter.go b/cos-csi-mounter/server/mount-s3.go similarity index 99% rename from cos-csi-mounter/server/s3mounter.go rename to cos-csi-mounter/server/mount-s3.go index 6070b6ce..e4f2f09d 100644 --- a/cos-csi-mounter/server/s3mounter.go +++ b/cos-csi-mounter/server/mount-s3.go @@ -15,7 +15,7 @@ var ( ) // s3MounterArgs holds the args sent from the node server for mount-s3. -// This struct must match the s3MounterArgs in pkg/mounter/mounter_s3mounter.go +// This struct must match the s3MounterArgs in pkg/mounter/mounter-mount-s3.go type s3MounterArgs struct { // Always set. AllowOther string `json:"allow-other,omitempty"` diff --git a/cos-csi-mounter/server/s3mounter_test.go b/cos-csi-mounter/server/mount-s3_test.go similarity index 100% rename from cos-csi-mounter/server/s3mounter_test.go rename to cos-csi-mounter/server/mount-s3_test.go diff --git a/pkg/mounter/mounter-s3mounter.go b/pkg/mounter/mounter-mount-s3.go similarity index 100% rename from pkg/mounter/mounter-s3mounter.go rename to pkg/mounter/mounter-mount-s3.go diff --git a/pkg/mounter/mounter-s3mounter_test.go b/pkg/mounter/mounter-mount-s3_test.go similarity index 100% rename from pkg/mounter/mounter-s3mounter_test.go rename to pkg/mounter/mounter-mount-s3_test.go diff --git a/pkg/mounter/utils/mounter_utils_stub.go b/pkg/mounter/utils/mounter_utils_stub.go index 6e78291a..01a6d340 100644 --- a/pkg/mounter/utils/mounter_utils_stub.go +++ b/pkg/mounter/utils/mounter_utils_stub.go @@ -15,4 +15,4 @@ type MounterUtils interface { type MounterOptsUtils struct{} func (su *MounterOptsUtils) FuseMount(_ string, _ string, _ []string) error { return nil } -func (su *MounterOptsUtils) FuseUnmount(_ string) error { return nil } +func (su *MounterOptsUtils) FuseUnmount(_ string) error { return nil } From 7ece170c5f6424294cd64ed768a60dac76357b35 Mon Sep 17 00:00:00 2001 From: Ashima-Ashima1 Date: Thu, 5 Nov 2026 14:59:25 +0530 Subject: [PATCH 125/128] rebase branch --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 44b9bed6..9c2bbbcc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,10 +3,10 @@ name: CI & Release on: push: branches: - - aws-s3mounter + - main pull_request: branches: - - aws-s3mounter + - main concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} From 6a054707d541a38e002b3cc4e6abbb4d667e8992 Mon Sep 17 00:00:00 2001 From: Ashima-Ashima1 Date: Thu, 5 Nov 2026 15:29:07 +0530 Subject: [PATCH 126/128] rebase branch --- cos-csi-mounter/server/server.go | 3 + cos-csi-mounter/server/utils_test.go | 503 +++++++++++++++++++++++++++ 2 files changed, 506 insertions(+) diff --git a/cos-csi-mounter/server/server.go b/cos-csi-mounter/server/server.go index e703d1d3..ebdd33fe 100644 --- a/cos-csi-mounter/server/server.go +++ b/cos-csi-mounter/server/server.go @@ -100,6 +100,7 @@ func handleSignals() { <-signals socketPath := filepath.Join(constants.SocketDir, constants.SocketFile) if err := os.Remove(socketPath); err != nil { + // Handle it properly: log it, retry, return, etc. logger.Warn("Failed to remove socket on exit", zap.String("path", socketPath), zap.Error(err)) } os.Exit(0) @@ -124,6 +125,7 @@ func startService(setupSocketFunc func() (net.Listener, error), router http.Hand logger.Error("Failed to create socket", zap.Error(err)) return err } + // Close the listener at the end defer func() { if err := listener.Close(); err != nil { fmt.Fprintf(os.Stderr, "failed to close listener: %v\n", err) @@ -134,6 +136,7 @@ func startService(setupSocketFunc func() (net.Listener, error), router http.Hand logger.Info("Starting cos-csi-mounter service...") + // Serve HTTP requests over Unix socket server := &http.Server{ Handler: router, ReadHeaderTimeout: 3 * time.Second, diff --git a/cos-csi-mounter/server/utils_test.go b/cos-csi-mounter/server/utils_test.go index d795a380..0d45ec2e 100644 --- a/cos-csi-mounter/server/utils_test.go +++ b/cos-csi-mounter/server/utils_test.go @@ -3,10 +3,17 @@ package main import ( "encoding/json" "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" "path/filepath" + "strings" "testing" + "testing/iotest" "github.com/IBM/ibm-object-csi-driver/pkg/constants" + "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) @@ -182,3 +189,499 @@ func TestFileExists_AbsFails(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "failed to resolve absolute path") } + +// ── decodeNoDuplicateKeys tests (Remediation 2 β€” report 4078540) ───────────── + +func TestDecodeNoDuplicateKeys_ValidMountRequest(t *testing.T) { + // Positive: well-formed body with no duplicate keys β†’ decodes correctly. + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + body := `{"path":"/var/lib/kubelet/pods/p1/volumes/x","bucket":"my-bucket","mounter":"s3fs","args":{}}` + c.Request = httptest.NewRequest("POST", "/", strings.NewReader(body)) + + var req MountRequest + ok := decodeNoDuplicateKeys(c, &req) + + assert.True(t, ok) + assert.Equal(t, "/var/lib/kubelet/pods/p1/volumes/x", req.Path) + assert.Equal(t, "my-bucket", req.Bucket) + assert.Equal(t, "s3fs", req.Mounter) +} + +func TestDecodeNoDuplicateKeys_DuplicatePathKey_Rejected(t *testing.T) { + // Negative: the exact attack from the report β€” duplicate "path" key. + // Must return HTTP 400 and not decode the victim path into the struct. + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + body := `{"path":"/var/lib/kubelet/pods/legit/volumes/x",` + + `"path":"/var/lib/kubelet/pods/victim/volumes/pvc-victim/mount",` + + `"bucket":"b","mounter":"s3fs","args":{}}` + c.Request = httptest.NewRequest("POST", "/", strings.NewReader(body)) + + var req MountRequest + ok := decodeNoDuplicateKeys(c, &req) + + assert.False(t, ok) + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), `duplicate key`) +} + +func TestDecodeNoDuplicateKeys_DuplicateBucketKey_Rejected(t *testing.T) { + // Negative: duplicate key other than "path" is also rejected. + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + body := `{"path":"/var/lib/kubelet/pods/p/volumes/x","bucket":"b1","bucket":"b2","mounter":"s3fs","args":{}}` + c.Request = httptest.NewRequest("POST", "/", strings.NewReader(body)) + + var req MountRequest + ok := decodeNoDuplicateKeys(c, &req) + + assert.False(t, ok) + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), `duplicate key`) +} + +func TestDecodeNoDuplicateKeys_UnknownField_Rejected(t *testing.T) { + // Negative: unknown top-level field is rejected (DisallowUnknownFields on envelope). + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + body := `{"path":"/var/lib/kubelet/pods/p/volumes/x","bucket":"b","mounter":"s3fs","args":{},"evil":"injection"}` + c.Request = httptest.NewRequest("POST", "/", strings.NewReader(body)) + + var req MountRequest + ok := decodeNoDuplicateKeys(c, &req) + + assert.False(t, ok) + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "invalid request") +} + +func TestDecodeNoDuplicateKeys_InvalidJSON_Rejected(t *testing.T) { + // Negative: completely broken JSON. + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + c.Request = httptest.NewRequest("POST", "/", strings.NewReader(`not-json`)) + + var req MountRequest + ok := decodeNoDuplicateKeys(c, &req) + + assert.False(t, ok) + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestDecodeNoDuplicateKeys_ValidUnmountRequest(t *testing.T) { + // Positive: unmount body with single "path" key β†’ decodes correctly. + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + body := `{"path":"/var/lib/kubelet/pods/p1/volumes/x"}` + c.Request = httptest.NewRequest("POST", "/", strings.NewReader(body)) + + var req struct { + Path string `json:"path"` + } + ok := decodeNoDuplicateKeys(c, &req) + + assert.True(t, ok) + assert.Equal(t, "/var/lib/kubelet/pods/p1/volumes/x", req.Path) +} + +func TestDecodeNoDuplicateKeys_DuplicatePathInUnmount_Rejected(t *testing.T) { + // Negative: duplicate "path" in unmount body is also rejected. + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + body := `{"path":"/var/lib/kubelet/pods/p1/volumes/x","path":"/mnt/evil"}` + c.Request = httptest.NewRequest("POST", "/", strings.NewReader(body)) + + var req struct { + Path string `json:"path"` + } + ok := decodeNoDuplicateKeys(c, &req) + + assert.False(t, ok) + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), `duplicate key`) +} + +// ── exact-key, trailing-data and skipJSONValue tests ───────────────────────── + +// newDecodeCtx builds a gin test context whose request body is the given reader. +func newDecodeCtx(body io.Reader) (*gin.Context, *httptest.ResponseRecorder) { + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest("POST", "/", body) + return c, w +} + +func TestDecodeNoDuplicateKeys_NonExactKey_Rejected(t *testing.T) { + // Negative: encoding/json matches keys case-insensitively and with Unicode + // folds. Any key that is not an exact match for a json tag must be rejected, + // whether or not the exact key is also present. + bodies := []string{ + `{"path":"/var/lib/kubelet/pods/legit/volumes/x","PATH":"/var/lib/kubelet/pods/victim/volumes/pvc/mount"}`, + `{"PATH":"/var/lib/kubelet/pods/p/volumes/x"}`, + `{"path":"/var/lib/kubelet/pods/p/volumes/x","bucket":"a","buc\u212aet":"b"}`, // Kelvin sign + `{"path":"/var/lib/kubelet/pods/p/volumes/x","args":{},"arg\u017f":{}}`, // long s + } + for _, body := range bodies { + c, w := newDecodeCtx(strings.NewReader(body)) + + var req MountRequest + ok := decodeNoDuplicateKeys(c, &req) + + assert.False(t, ok, body) + assert.Equal(t, http.StatusBadRequest, w.Code, body) + assert.Contains(t, w.Body.String(), "invalid request", body) + } +} + +func TestDecodeNoDuplicateKeys_NonExactKeyInUnmount_Rejected(t *testing.T) { + // Negative: the exact-key rule also applies to the anonymous unmount struct. + c, w := newDecodeCtx(strings.NewReader(`{"path":"/var/lib/kubelet/pods/p/volumes/x","Path":"/mnt/evil"}`)) + + var req struct { + Path string `json:"path"` + } + ok := decodeNoDuplicateKeys(c, &req) + + assert.False(t, ok) + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestDecodeNoDuplicateKeys_TrailingData_Rejected(t *testing.T) { + // Negative: any data after the JSON object must be rejected. + bodies := []string{ + `{"path":"/var/lib/kubelet/pods/p/volumes/x"}{"path":"/mnt/evil"}`, + `{"path":"/var/lib/kubelet/pods/p/volumes/x"} trailing`, + `{"path":"/var/lib/kubelet/pods/p/volumes/x"} []`, + } + for _, body := range bodies { + c, w := newDecodeCtx(strings.NewReader(body)) + + var req struct { + Path string `json:"path"` + } + ok := decodeNoDuplicateKeys(c, &req) + + assert.False(t, ok, body) + assert.Equal(t, http.StatusBadRequest, w.Code, body) + } +} + +func TestDecodeNoDuplicateKeys_TrailingWhitespace_Accepted(t *testing.T) { + // Positive: trailing whitespace/newlines are not data and must still pass. + c, w := newDecodeCtx(strings.NewReader("{\"path\":\"/var/lib/kubelet/pods/p/volumes/x\"}\n \n")) + + var req struct { + Path string `json:"path"` + } + ok := decodeNoDuplicateKeys(c, &req) + + assert.True(t, ok) + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "/var/lib/kubelet/pods/p/volumes/x", req.Path) +} + +func TestDecodeNoDuplicateKeys_NestedValuesSkipped(t *testing.T) { + // Positive: nested objects/arrays are skipped correctly, and keys repeated + // inside nested values are not treated as top-level duplicates. + body := `{"path":"/var/lib/kubelet/pods/p/volumes/x","bucket":"b","mounter":"s3fs",` + + `"args":{"a":[1,{"path":"n"},[2,3]],"path":"m"}}` + c, _ := newDecodeCtx(strings.NewReader(body)) + + var req MountRequest + ok := decodeNoDuplicateKeys(c, &req) + + assert.True(t, ok) + assert.Equal(t, "/var/lib/kubelet/pods/p/volumes/x", req.Path) + assert.Equal(t, "b", req.Bucket) + assert.JSONEq(t, `{"a":[1,{"path":"n"},[2,3]],"path":"m"}`, string(req.Args)) +} + +func TestDecodeNoDuplicateKeys_NotAnObject_Rejected(t *testing.T) { + // Negative: top-level value must be a JSON object. + for _, body := range []string{``, `null`, `[]`, `"str"`, `123`} { + c, w := newDecodeCtx(strings.NewReader(body)) + + var req MountRequest + ok := decodeNoDuplicateKeys(c, &req) + + assert.False(t, ok, body) + assert.Equal(t, http.StatusBadRequest, w.Code, body) + assert.Contains(t, w.Body.String(), "expected JSON object", body) + } +} + +func TestDecodeNoDuplicateKeys_TruncatedValue_Rejected(t *testing.T) { + // Negative: bodies that end inside a value or before the closing brace. + bodies := []string{ + `{"path"`, + `{"path":`, + `{"path":{"a":`, + `{"path":"x","args":[1,[2`, + `{"path":"x"`, + } + for _, body := range bodies { + c, w := newDecodeCtx(strings.NewReader(body)) + + var req MountRequest + ok := decodeNoDuplicateKeys(c, &req) + + assert.False(t, ok, body) + assert.Equal(t, http.StatusBadRequest, w.Code, body) + } +} + +func TestDecodeNoDuplicateKeys_ReadError_Rejected(t *testing.T) { + // Negative: failure while reading the request body. + c, w := newDecodeCtx(iotest.ErrReader(errors.New("boom"))) + + var req MountRequest + ok := decodeNoDuplicateKeys(c, &req) + + assert.False(t, ok) + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "failed to read request body") + // Internal error detail must not leak to the client. + assert.NotContains(t, w.Body.String(), "boom") +} + +func TestJSONFieldNames(t *testing.T) { + type sample struct { + Plain string `json:"plain"` + WithOpt string `json:"with_opt,omitempty"` + Skipped string `json:"-"` + NoName string `json:",omitempty"` + Untagged string + } + + // Pointer to struct. + assert.Equal(t, + map[string]struct{}{"plain": {}, "with_opt": {}}, + jsonFieldNames(&sample{})) + + // Pointer to pointer to struct. + p := &sample{} + assert.Equal(t, + map[string]struct{}{"plain": {}, "with_opt": {}}, + jsonFieldNames(&p)) + + // The real request types. + assert.Equal(t, + map[string]struct{}{"path": {}, "bucket": {}, "mounter": {}, "args": {}}, + jsonFieldNames(&MountRequest{})) +} + +func TestSkipJSONValue(t *testing.T) { + // Input is `{"a":,"b":1}`. After consuming '{' and "a", skipJSONValue + // must consume exactly , leaving "b" as the next token. + values := []string{ + `"str"`, + `123`, + `true`, + `null`, + `{}`, + `[]`, + `{"x":1,"x":2}`, + `[1,2,3]`, + `{"x":[1,{"y":[2,{"z":{}}]}],"w":"v"}`, + `[[[[]]]]`, + `[{"a":[]},{"b":{}}]`, + } + for _, v := range values { + dec := json.NewDecoder(strings.NewReader(`{"a":` + v + `,"b":1}`)) + _, err := dec.Token() // '{' + assert.NoError(t, err, v) + _, err = dec.Token() // "a" + assert.NoError(t, err, v) + + assert.NoError(t, skipJSONValue(dec), v) + + tok, err := dec.Token() + assert.NoError(t, err, v) + assert.Equal(t, "b", tok, v) + } +} + +func TestSkipJSONValue_Errors(t *testing.T) { + // Empty input and values truncated at various nesting depths must error. + inputs := []string{ + ``, + `{`, + `[`, + `[1,2`, + `{"a":1`, + `[[1,2]`, + `{"a":[1,{"b":2}`, + `]`, + } + for _, in := range inputs { + dec := json.NewDecoder(strings.NewReader(in)) + assert.Error(t, skipJSONValue(dec), in) + } +} + +func TestHandleCosMount_NonExactKey_Rejected(t *testing.T) { + // End-to-end: handler returns 400 for a case-variant key and never mounts. + gin.SetMode(gin.TestMode) + mockMounter := new(MockMounterUtils) + router := gin.Default() + router.POST("/mount", handleCosMount(mockMounter, &MockMounterArgsParser{})) + + body := `{"path":"/var/lib/kubelet/pods/legit/volumes/x",` + + `"PATH":"/var/lib/kubelet/pods/victim/volumes/pvc-victim/mount",` + + `"bucket":"b","mounter":"s3fs","args":{}}` + w := httptest.NewRecorder() + req := httptest.NewRequest("POST", "/mount", strings.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + mockMounter.AssertNotCalled(t, "FuseMount") +} + +func TestHandleCosUnmount_TrailingData_Rejected(t *testing.T) { + // End-to-end: handler returns 400 for a second appended JSON object. + gin.SetMode(gin.TestMode) + mockMounter := new(MockMounterUtils) + router := gin.Default() + router.POST("/unmount", handleCosUnmount(mockMounter)) + + body := `{"path":"/var/lib/kubelet/pods/p1/volumes/x"}{"path":"/mnt/evil"}` + w := httptest.NewRecorder() + req := httptest.NewRequest("POST", "/unmount", strings.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + mockMounter.AssertNotCalled(t, "FuseUnmount") +} + +func TestHandleCosMount_DuplicatePathKey_Rejected(t *testing.T) { + // End-to-end: the handler itself must return 400 when "path" appears twice. + gin.SetMode(gin.TestMode) + mockMounter := new(MockMounterUtils) + router := gin.Default() + router.POST("/mount", handleCosMount(mockMounter, &MockMounterArgsParser{})) + + body := `{"path":"/var/lib/kubelet/pods/legit/volumes/x",` + + `"path":"/var/lib/kubelet/pods/victim/volumes/pvc-victim/mount",` + + `"bucket":"b","mounter":"s3fs","args":{}}` + w := httptest.NewRecorder() + req := httptest.NewRequest("POST", "/mount", strings.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "duplicate key") + // FuseMount must never have been called. + mockMounter.AssertNotCalled(t, "FuseMount") +} + +func TestHandleCosUnmount_DuplicatePathKey_Rejected(t *testing.T) { + // End-to-end: unmount handler must also reject duplicate "path". + gin.SetMode(gin.TestMode) + mockMounter := new(MockMounterUtils) + router := gin.Default() + router.POST("/unmount", handleCosUnmount(mockMounter)) + + body := `{"path":"/var/lib/kubelet/pods/p1/volumes/x","path":"/mnt/evil"}` + w := httptest.NewRecorder() + req := httptest.NewRequest("POST", "/unmount", strings.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "duplicate key") + mockMounter.AssertNotCalled(t, "FuseUnmount") +} + +// ── pathValidator tests (Remediation 3 β€” report 4078540) ───────────────────── + +func TestPathValidator_LegitPodPath_Passes(t *testing.T) { + // Positive: a normal pod volume path under /var/lib/kubelet/pods. + err := pathValidator("/var/lib/kubelet/pods/abc-123/volumes/kubernetes.io~csi/pvc/mount") + assert.NoError(t, err) +} + +func TestPathValidator_AltSafeDirPath_Passes(t *testing.T) { + // Positive: path under the second safe directory /var/data/kubelet/pods. + err := pathValidator("/var/data/kubelet/pods/abc-123/volumes/kubernetes.io~csi/pvc/mount") + assert.NoError(t, err) +} + +func TestPathValidator_ExactSafeDir_Rejected(t *testing.T) { + // Negative: the safe directory itself does not end with /mount. + err := pathValidator("/var/lib/kubelet/pods") + assert.Error(t, err) + assert.Contains(t, err.Error(), "bad value for target path") +} + +func TestPathValidator_MissingMountSuffix_Rejected(t *testing.T) { + // Negative: valid prefix under safe dir but does not end with /mount. + err := pathValidator("/var/lib/kubelet/pods/abc-123/volumes/kubernetes.io~csi/pvc") + assert.Error(t, err) + assert.Contains(t, err.Error(), "bad value for target path") +} + +func TestPathValidator_PodsEvilSuffix_Rejected(t *testing.T) { + // Negative: the exact bypass from the report β€” /var/lib/kubelet/pods-evil + // shares the string prefix "/var/lib/kubelet/pods" but is a different directory. + // Before the fix this passed; after the fix it must be rejected. + err := pathValidator("/var/lib/kubelet/pods-evil/volumes/x/mount") + assert.Error(t, err) + assert.Contains(t, err.Error(), "bad value for target path") +} + +func TestPathValidator_PodsEvilSuffixAltDir_Rejected(t *testing.T) { + // Same bypass attempt against the /var/data/kubelet/pods safe dir. + err := pathValidator("/var/data/kubelet/pods-evil/volumes/x/mount") + assert.Error(t, err) + assert.Contains(t, err.Error(), "bad value for target path") +} + +func TestPathValidator_EtcPath_Rejected(t *testing.T) { + // Negative: completely out-of-bounds path. + err := pathValidator("/etc/passwd") + assert.Error(t, err) + assert.Contains(t, err.Error(), "bad value for target path") +} + +func TestPathValidator_RootPath_Rejected(t *testing.T) { + // Negative: filesystem root. + err := pathValidator("/") + assert.Error(t, err) + assert.Contains(t, err.Error(), "bad value for target path") +} + +func TestPathValidator_AbsResolverFails_ReturnsError(t *testing.T) { + // Negative: absPathResolver returns an error β†’ pathValidator propagates it. + orig := absPathResolver + defer func() { absPathResolver = orig }() + + absPathResolver = func(string) (string, error) { + return "", fmt.Errorf("mock resolve failure") + } + + err := pathValidator("/var/lib/kubelet/pods/x") + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to resolve absolute mount path") +} From f168f69a3cce6d8cc6eadb02824459aaae0b9712 Mon Sep 17 00:00:00 2001 From: Ashima-Ashima1 Date: Thu, 5 Nov 2026 19:12:24 +0530 Subject: [PATCH 127/128] minor fixes --- .github/workflows/release.yml | 2 +- pkg/mounter/utils/mounter_utils_stub.go | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9c2bbbcc..e62b3dff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -152,4 +152,4 @@ jobs: prerelease: ${{ env.IS_LATEST_RELEASE != 'true' }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 \ No newline at end of file + uses: github/codeql-action/analyze@v4 diff --git a/pkg/mounter/utils/mounter_utils_stub.go b/pkg/mounter/utils/mounter_utils_stub.go index 01a6d340..ede666f8 100644 --- a/pkg/mounter/utils/mounter_utils_stub.go +++ b/pkg/mounter/utils/mounter_utils_stub.go @@ -8,6 +8,7 @@ package utils type MounterUtils interface { FuseUnmount(path string) error FuseMount(path string, comm string, args []string) error + FuseMountWithEnv(path string, comm string, args []string, envVars []string) error } // MounterOptsUtils is an empty struct whose methods are defined in mounter_utils.go (Linux only). @@ -16,3 +17,6 @@ type MounterOptsUtils struct{} func (su *MounterOptsUtils) FuseMount(_ string, _ string, _ []string) error { return nil } func (su *MounterOptsUtils) FuseUnmount(_ string) error { return nil } +func (su *MounterOptsUtils) FuseMountWithEnv(_ string, _ string, _ []string, _ []string) error { + return nil +} From b7031b5644cf813c654eed62931ff64eb26e6481 Mon Sep 17 00:00:00 2001 From: Ashima-Ashima1 Date: Thu, 5 Nov 2026 19:32:34 +0530 Subject: [PATCH 128/128] minor fixes --- cos-csi-mounter/server/mount-s3.go | 2 +- cos-csi-mounter/server/utils_test.go | 51 +++++++++++++++++ pkg/constants/constants.go | 28 +++++----- pkg/mounter/mounter-mount-s3_test.go | 14 ++--- pkg/mounter/mounter-s3fs_test.go | 82 ++++++++++++++-------------- pkg/mounter/mounter.go | 12 ++-- pkg/utils/set.go | 1 - 7 files changed, 120 insertions(+), 70 deletions(-) diff --git a/cos-csi-mounter/server/mount-s3.go b/cos-csi-mounter/server/mount-s3.go index e4f2f09d..849009b3 100644 --- a/cos-csi-mounter/server/mount-s3.go +++ b/cos-csi-mounter/server/mount-s3.go @@ -217,4 +217,4 @@ func (args s3MounterArgs) Validate(targetPath string) error { } return nil -} \ No newline at end of file +} diff --git a/cos-csi-mounter/server/utils_test.go b/cos-csi-mounter/server/utils_test.go index 0d45ec2e..2b209306 100644 --- a/cos-csi-mounter/server/utils_test.go +++ b/cos-csi-mounter/server/utils_test.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "net/http/httptest" + "os" "path/filepath" "strings" "testing" @@ -685,3 +686,53 @@ func TestPathValidator_AbsResolverFails_ReturnsError(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "failed to resolve absolute mount path") } + +func TestEnsureDir_OutsideSafeDirectory(t *testing.T) { + err := ensureDir("/etc/passwd", "/tmp") + assert.Error(t, err) + assert.Contains(t, err.Error(), "outside the safe directory") +} + +func TestEnsureDir_PathTraversal(t *testing.T) { + err := ensureDir("/tmp/../../etc", "/tmp") + assert.Error(t, err) + assert.Contains(t, err.Error(), "outside the safe directory") +} + +func TestEnsureDir_CreatesDirectory(t *testing.T) { + dir := filepath.Join(t.TempDir(), "subdir") + safeBase := filepath.Dir(dir) + err := ensureDir(dir, safeBase) + assert.NoError(t, err) + info, statErr := os.Stat(dir) + assert.NoError(t, statErr) + assert.True(t, info.IsDir()) +} + +func TestEnsureDir_ExistingDirectory(t *testing.T) { + dir := t.TempDir() + err := ensureDir(dir, filepath.Dir(dir)) + assert.NoError(t, err) +} + +func TestEnsureDir_PathIsFile(t *testing.T) { + dir := t.TempDir() + file := filepath.Join(dir, "file.txt") + assert.NoError(t, os.WriteFile(file, []byte("x"), 0600)) + err := ensureDir(file, dir) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not a directory") +} + +func TestEnsureDir_AbsFails(t *testing.T) { + originalAbs := absPathResolver + defer func() { absPathResolver = originalAbs }() + + absPathResolver = func(path string) (string, error) { + return "", errors.New("abs error") + } + + err := ensureDir("some-path", "/tmp") + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to resolve absolute path") +} diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 02dcf9a1..fee30f2a 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -14,9 +14,9 @@ const ( KPEncryptionAlgorithm = "AES256" // https://github.com/IBM/ibm-cos-sdk-go/blob/master/service/s3/api.go#L9130-L9136 - S3FS = "s3fs" - RClone = "rclone" - AMAZONS3MOUNTER = "mount-s3" + S3FS = "s3fs" + RClone = "rclone" + AMAZONS3MOUNTER = "mount-s3" DefaultNamespace = "default" @@ -31,16 +31,16 @@ const ( NodeRegionLabel = "topology.kubernetes.io/region" // Timeout specifies a time limit for requests made by HTTP Client - Timeout = 3 * time.Minute - COSCSIMounterSocketPath = "/var/lib/coscsi-sock/coscsi.sock" - COSCSIMounterSocketPathEnv = "COS_CSI_MOUNTER_SOCKET" - MounterConfigPathOnHost = "/var/lib/coscsi-config" - MounterConfigPathOnPodS3fs = "/var/lib/ibmc-s3fs" - MountpointS3BinaryPath = "/usr/bin/mount-s3" - - // Config paths for mountpoint-s3 (same pattern as rclone) - MounterConfigPathOnPodS3Mount = "/var/lib/coscsi-config/s3mount" - MounterConfigPathOnPodRclone = "/root/.config/rclone" + Timeout = 3 * time.Minute + COSCSIMounterSocketPath = "/var/lib/coscsi-sock/coscsi.sock" + COSCSIMounterSocketPathEnv = "COS_CSI_MOUNTER_SOCKET" + MounterConfigPathOnHost = "/var/lib/coscsi-config" + MounterConfigPathOnPodS3fs = "/var/lib/ibmc-s3fs" + MountpointS3BinaryPath = "/usr/bin/mount-s3" + + // Config paths for mountpoint-s3 (same pattern as rclone) + MounterConfigPathOnPodS3Mount = "/var/lib/coscsi-config/s3mount" + MounterConfigPathOnPodRclone = "/root/.config/rclone" // Interval to wait till next loop Interval = 500 * time.Millisecond @@ -62,7 +62,7 @@ const ( KubeNodeName = "KUBE_NODE_NAME" MaxVolumesPerNodeEnv = "MAX_VOLUMES_PER_NODE" - IAMEndpointEnv = "IAM_ENDPOINT" + IAMEndpointEnv = "IAM_ENDPOINT" COSResourceConfigEndpointEnv = "COS_RESOURCE_CONFIG_ENDPOINT" CipherSuitesKey = "cipher_suites" diff --git a/pkg/mounter/mounter-mount-s3_test.go b/pkg/mounter/mounter-mount-s3_test.go index ec41f433..4ca9bd96 100644 --- a/pkg/mounter/mounter-mount-s3_test.go +++ b/pkg/mounter/mounter-mount-s3_test.go @@ -2,10 +2,10 @@ package mounter import ( "errors" - "os" - "testing" mounterUtils "github.com/IBM/ibm-object-csi-driver/pkg/mounter/utils" "github.com/stretchr/testify/assert" + "os" + "testing" ) var ( @@ -744,9 +744,9 @@ func TestParseMountpointS3Options(t *testing.T) { expectedRemaining: []string{}, }, { - name: "Add double dash to options without it", - opts: []string{"max-threads=32", "dir-mode=0755"}, - expectedMounter: &MountpointS3Mounter{}, + name: "Add double dash to options without it", + opts: []string{"max-threads=32", "dir-mode=0755"}, + expectedMounter: &MountpointS3Mounter{}, expectedRemaining: []string{"--max-threads=32", "--dir-mode=0755"}, }, } @@ -755,7 +755,7 @@ func TestParseMountpointS3Options(t *testing.T) { t.Run(tt.name, func(t *testing.T) { mounter := &MountpointS3Mounter{} remaining := parseMountpointS3Options(mounter, tt.opts) - + assert.Equal(t, tt.expectedMounter.UID, mounter.UID) assert.Equal(t, tt.expectedMounter.GID, mounter.GID) assert.Equal(t, tt.expectedMounter.LogLevel, mounter.LogLevel) @@ -786,7 +786,7 @@ allow-overwrite`, s3Mounter, ok := mounter.(*MountpointS3Mounter) assert.True(t, ok) - + // incremental-upload should be blocked (set to false) assert.False(t, s3Mounter.IncrementalUpload, "incremental-upload should be blocked due to data loss issues") // allow-overwrite should still be set diff --git a/pkg/mounter/mounter-s3fs_test.go b/pkg/mounter/mounter-s3fs_test.go index 96247fbe..11ad88b5 100644 --- a/pkg/mounter/mounter-s3fs_test.go +++ b/pkg/mounter/mounter-s3fs_test.go @@ -14,9 +14,9 @@ import ( var ( secretMap = map[string]string{ - "cosEndpoint": "test-endpoint", - "locationConstraint": "test-loc-constraint", - "bucketName": "test-bucket-name", + "cosEndpoint": "test-endpoint", + "locationConstraint": "test-loc-constraint", + "bucketName": "test-bucket-name", "objectPath": "test-obj-path", constants.AccessKey: "test-access-key", constants.SecretKey: "test-secret-key", @@ -48,18 +48,18 @@ func TestNewS3fsMounter_Success(t *testing.T) { func TestNewS3fsMounter_Success_Hmac(t *testing.T) { secretMap := map[string]string{ - "cosEndpoint": "test-endpoint", - "locationConstraint": "test-loc-constraint", - "bucketName": "test-bucket-name", + "cosEndpoint": "test-endpoint", + "locationConstraint": "test-loc-constraint", + "bucketName": "test-bucket-name", "objectPath": "test-obj-path", constants.AccessKey: "test-access-key", constants.SecretKey: "test-secret-key", constants.KpRootKeyCRN: "test-kp-root-key-crn", "mountOptions": "\nkey1\nkey2=value2\nkey=val1=val2", - "tmpdir": "test-tmpdir", - "use_cache": "true", - "gid": "test-gid", - "iamEndpoint": "test-iamEndpoint", + "tmpdir": "test-tmpdir", + "use_cache": "true", + "gid": "test-gid", + "iamEndpoint": "test-iamEndpoint", } mountOptions := []string{"opt1=val1", "opt2=val2", " ", "opt3"} @@ -301,11 +301,11 @@ func TestRemoveS3FSCredFile_Negative(t *testing.T) { func TestAddMountParam_Integration(t *testing.T) { tests := []struct { - name string - defaultOpts []string - secretOpts string - wantUnknown []string - wantEmpty bool + name string + defaultOpts []string + secretOpts string + wantUnknown []string + wantEmpty bool }{ {"OnlyKnown", []string{"allow_other"}, "kernel_cache", nil, true}, {"OnlyUnknown", nil, "enable_content_md5\ncomplement_stat", []string{"enable_content_md5", "complement_stat"}, false}, @@ -390,7 +390,7 @@ func TestUpdateS3FSMountOptions_SpecialSecretFields(t *testing.T) { t.Run(tt.name, func(t *testing.T) { opts, _ := updateS3FSMountOptions(tt.defaultOpts, tt.secret, GetKnownS3FSOptions(), nil, "", false) assert.NotNil(t, opts) - + optsStr := strings.Join(opts, " ") if tt.wantUID { assert.Contains(t, optsStr, "uid=") @@ -407,9 +407,9 @@ func TestUpdateS3FSMountOptions_DefaultParams(t *testing.T) { "cipher_suites": "AESGCM", "empty_param": "", } - + opts, _ := updateS3FSMountOptions(nil, map[string]string{}, GetKnownS3FSOptions(), defaultParams, "", false) - + optsStr := strings.Join(opts, " ") assert.Contains(t, optsStr, "cipher_suites=AESGCM") assert.NotContains(t, optsStr, "empty_param") @@ -448,11 +448,11 @@ func TestUpdateS3FSMountOptions_ReadOnly(t *testing.T) { func TestUpdateS3FSMountOptions_GidParam(t *testing.T) { tests := []struct { - name string - secret map[string]string - gid string - wantGID string - wantUID string + name string + secret map[string]string + gid string + wantGID string + wantUID string }{ { name: "gid param overrides secretMap gid", @@ -511,44 +511,44 @@ func TestUpdateS3FSMountOptionsWithUnknownOptions(t *testing.T) { expectAddMountParamContains string }{ { - name: "Known and unknown options", - defaultMountOp: []string{"allow_other", "enable_content_md5=true"}, - secretMap: map[string]string{"mountOptions": "cipher_suites=default"}, + name: "Known and unknown options", + defaultMountOp: []string{"allow_other", "enable_content_md5=true"}, + secretMap: map[string]string{"mountOptions": "cipher_suites=default"}, expectAddMountParamPresent: true, expectAddMountParamContains: "enable_content_md5=true", }, { - name: "Only known options", - defaultMountOp: []string{"allow_other"}, - secretMap: map[string]string{}, + name: "Only known options", + defaultMountOp: []string{"allow_other"}, + secretMap: map[string]string{}, expectAddMountParamPresent: false, expectAddMountParamContains: "", }, { - name: "Unknown option without value", - defaultMountOp: []string{"custom_flag"}, - secretMap: map[string]string{}, + name: "Unknown option without value", + defaultMountOp: []string{"custom_flag"}, + secretMap: map[string]string{}, expectAddMountParamPresent: true, expectAddMountParamContains: "custom_flag", }, { - name: "Multiple unknown options from secretMap", - defaultMountOp: []string{}, - secretMap: map[string]string{"mountOptions": "unknown1=val1\nunknown2=val2"}, + name: "Multiple unknown options from secretMap", + defaultMountOp: []string{}, + secretMap: map[string]string{"mountOptions": "unknown1=val1\nunknown2=val2"}, expectAddMountParamPresent: true, expectAddMountParamContains: "unknown1=val1", }, { - name: "Empty line in mountOptions", - defaultMountOp: []string{}, - secretMap: map[string]string{"mountOptions": "allow_other\n\nunknown_opt=test"}, + name: "Empty line in mountOptions", + defaultMountOp: []string{}, + secretMap: map[string]string{"mountOptions": "allow_other\n\nunknown_opt=test"}, expectAddMountParamPresent: true, expectAddMountParamContains: "unknown_opt=test", }, { - name: "Invalid option in defaultMountOp", - defaultMountOp: []string{"", "allow_other"}, - secretMap: map[string]string{}, + name: "Invalid option in defaultMountOp", + defaultMountOp: []string{"", "allow_other"}, + secretMap: map[string]string{}, expectAddMountParamPresent: false, expectAddMountParamContains: "", }, diff --git a/pkg/mounter/mounter.go b/pkg/mounter/mounter.go index f6e44199..e0b8cc7e 100644 --- a/pkg/mounter/mounter.go +++ b/pkg/mounter/mounter.go @@ -40,13 +40,13 @@ type Mounter interface { type CSIMounterFactory struct{} type MounterParams struct { - Attrib map[string]string - SecretMap map[string]string - MountFlags []string + Attrib map[string]string + SecretMap map[string]string + MountFlags []string KnownS3FSOptions *pkgutils.Set - DefaultMOMap map[string]string - Gid string - ReadOnly bool + DefaultMOMap map[string]string + Gid string + ReadOnly bool } type NewMounterFactory interface { diff --git a/pkg/utils/set.go b/pkg/utils/set.go index 980c6490..3656e330 100644 --- a/pkg/utils/set.go +++ b/pkg/utils/set.go @@ -61,4 +61,3 @@ func (s *Set) Contains(key string) bool { func (s *Set) Size() int { return len(s.m) } -