From 4f2078e00621b6a134ee9d181d341b245c2938f9 Mon Sep 17 00:00:00 2001 From: Kartik Ramchandani Date: Tue, 10 Feb 2026 15:33:13 +0530 Subject: [PATCH] Code for amazon s3 mounter --- 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 6dcd751e..92c617e0 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 533e9eae..98ba123b 100644 --- a/pkg/mounter/mounter.go +++ b/pkg/mounter/mounter.go @@ -74,6 +74,8 @@ func (s *CSIMounterFactory) NewMounter(attrib map[string]string, secretMap map[s return NewS3fsMounter(secretMap, mountFlags, mounterUtils, defaultMOMap) case constants.RClone: return NewRcloneMounter(secretMap, mountFlags, mounterUtils) + case constants.S3MOUNTER: + return NewMountpointS3Mounter(secretMap, mountFlags, mounterUtils) default: // default to s3fs return NewS3fsMounter(secretMap, mountFlags, mounterUtils, defaultMOMap) 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", + ) +}