diff --git a/Dockerfile b/Dockerfile index 5e02dda8..d1600c80 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,39 @@ ARG RHSM_USER=blank ENV RHSM_PASS "${RHSM_PASS}" ENV RHSM_USER "${RHSM_USER}" +ADD register-sys.sh /usr/bin/ +RUN microdnf update --setopt=tsflags=nodocs && \ + microdnf install -y --nodocs hostname subscription-manager +RUN hostname; chmod 755 /usr/bin/register-sys.sh && /usr/bin/register-sys.sh +# Install build tools +RUN microdnf update --setopt=tsflags=nodocs && \ + microdnf install -y --nodocs \ + fuse \ + fuse-devel \ + cmake3 \ + clang \ + git \ + pkgconf && \ + microdnf clean all + +# Install rust +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && \ + source "$HOME/.cargo/env" + +# Build mountpoint-s3 +RUN git clone --recurse-submodules https://github.com/awslabs/mountpoint-s3.git && \ + source "$HOME/.cargo/env" && \ + cd mountpoint-s3 && \ + cargo build --release + +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6-941 as s3fs-builder + +ARG RHSM_PASS="alexander.minbaev@ibm.com" +ARG RHSM_USER="bonbon14011401!!" + +ENV RHSM_PASS "${RHSM_PASS}" +ENV RHSM_USER "${RHSM_USER}" + ADD register-sys.sh /usr/bin/ RUN microdnf update --setopt=tsflags=nodocs && \ microdnf install -y --nodocs hostname subscription-manager @@ -35,9 +68,9 @@ ENV GO_VERSION=1.24.1 RUN echo $ARCH $GO_VERSION RUN wget -q https://dl.google.com/go/go$GO_VERSION.linux-$ARCH.tar.gz && \ - tar -xf go$GO_VERSION.linux-$ARCH.tar.gz && \ - rm go$GO_VERSION.linux-$ARCH.tar.gz && \ - mv go /usr/local +tar -xf go$GO_VERSION.linux-$ARCH.tar.gz && \ +rm go$GO_VERSION.linux-$ARCH.tar.gz && \ +mv go /usr/local ENV GOROOT /usr/local/go ENV GOPATH /go @@ -61,6 +94,7 @@ LABEL description="IBM CSI Object Storage Plugin" LABEL build-date=${build_date} LABEL git-commit-id=${git_commit_id} RUN yum update -y && yum install fuse fuse-libs fuse3 fuse3-libs -y +COPY --from=mountpoint-builder /mountpoint-s3/target/release/mount-s3 /usr/bin/mount-s3 COPY --from=s3fs-builder /usr/local/bin/s3fs /usr/bin/s3fs COPY --from=rclone-builder /usr/local/bin/rclone /usr/bin/rclone COPY ibm-object-csi-driver ibm-object-csi-driver diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index ed1a85e1..680abe5d 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -8,6 +8,7 @@ const ( S3FS = "s3fs" RClone = "rclone" + MNTS3 = "mountpoint" DefaultNamespace = "default" IAMEP = "https://private.iam.cloud.ibm.com/identity/token" diff --git a/pkg/mounter/fake_mounter.go b/pkg/mounter/fake_mounter.go index 673f74cd..f368a5ea 100644 --- a/pkg/mounter/fake_mounter.go +++ b/pkg/mounter/fake_mounter.go @@ -22,6 +22,8 @@ func (f *FakeMounterFactory) NewMounter(attrib map[string]string, secretMap map[ return fakenewS3fsMounter(f.IsFailedMount) case constants.RClone: return fakenewRcloneMounter(f.IsFailedMount) + case constants.MNTS3: + return fakenewMountpointMounter(f.IsFailedMount) default: return fakenewS3fsMounter(f.IsFailedMount) } diff --git a/pkg/mounter/fake_mounter_mountpoint.go b/pkg/mounter/fake_mounter_mountpoint.go new file mode 100644 index 00000000..5c3149f8 --- /dev/null +++ b/pkg/mounter/fake_mounter_mountpoint.go @@ -0,0 +1,34 @@ +package mounter + +import "errors" + +type fakemountpointMounter struct { + bucketName string + objPath string + endPoint string + accessKey string + secretKey string + isFailedMount bool +} + +func fakenewMountpointMounter(isFailedMount bool) Mounter { + return &fakemountpointMounter{ + bucketName: bucketName, + objPath: objPath, + endPoint: endPoint, + accessKey: keys, + secretKey: keys, + isFailedMount: isFailedMount, + } +} + +func (mnpt *fakemountpointMounter) Mount(source string, target string) error { + if mnpt.isFailedMount { + return errors.New("failed to mount mountpoint") + } + return nil +} + +func (mnpt *fakemountpointMounter) Unmount(target string) error { + return nil +} diff --git a/pkg/mounter/mounter-mountpoint.go b/pkg/mounter/mounter-mountpoint.go new file mode 100644 index 00000000..b3e76829 --- /dev/null +++ b/pkg/mounter/mounter-mountpoint.go @@ -0,0 +1,127 @@ +/******************************************************************************* +* IBM Confidential +* OCO Source Materials +* IBM Cloud Kubernetes Service, 5737-D43 +* (C) Copyright IBM Corp. 2023 All Rights Reserved. +* The source code for this program is not published or otherwise divested of +* its trade secrets, irrespective of what has been deposited with +* the U.S. Copyright Office. +******************************************************************************/ + +// Package mounter +package mounter + +import ( + "crypto/sha256" + "fmt" + "os" + "path" + + // "github.com/IBM/ibm-object-csi-driver/pkg/constants" + "github.com/IBM/ibm-object-csi-driver/pkg/mounter/utils" + "k8s.io/klog/v2" +) + +// Mounter interface defined in mounter.go +// MountpointMounter Implements Mounter +type MountpointMounter struct { + BucketName string //From Secret in SC + ObjPath string //From Secret in SC + EndPoint string //From Secret in SC + AccessKey string + SecretKey string + MountOptions []string + MounterUtils utils.MounterUtils +} + +func NewMountpointMounter(secretMap map[string]string, mountOptions []string, mounterUtils utils.MounterUtils) Mounter { + klog.Info("-newMountpointMounter-") + + var ( + val string + check bool + mounter *MountpointMounter + ) + + mounter = &MountpointMounter{} + + if val, check = secretMap["cosEndpoint"]; check { + mounter.EndPoint = val + } + if val, check = secretMap["bucketName"]; check { + mounter.BucketName = val + } + if val, check = secretMap["objPath"]; check { + mounter.ObjPath = val + } + if val, check = secretMap["accessKey"]; check { + mounter.AccessKey = val + } + if val, check = secretMap["secretKey"]; check { + mounter.SecretKey = val + } + + klog.Infof("newMntS3Mounter args:\n\tbucketName: [%s]\n\tobjPath: [%s]\n\tendPoint: [%s]", + mounter.BucketName, mounter.ObjPath, mounter.EndPoint) + + mounter.MountOptions = mountOptions + mounter.MounterUtils = mounterUtils + + return mounter +} + +const ( + mntS3Cmd = "mount-s3" + metaRootMntS3 = "/var/lib/ibmc-mntS3" +) + +func (mntS3 *MountpointMounter) Stage(stagePath string) error { + return nil +} +func (mntS3 *MountpointMounter) Unstage(stagePath string) error { + return nil +} +func (mntS3 *MountpointMounter) Mount(source string, target string) error { + klog.Info("-MountpointMounter Mount-") + klog.Infof("Mount args:\n\tsource: <%s>\n\ttarget: <%s>", source, target) + var pathExist bool + var err error + metaPath := path.Join(metaRootMntS3, fmt.Sprintf("%x", sha256.Sum256([]byte(target)))) + + if pathExist, err = checkPath(metaPath); err != nil { + klog.Errorf("MountpointMounter Mount: Cannot stat directory %s: %v", metaPath, err) + return err + } + + if !pathExist { + if err = mkdirAll(metaPath, 0755); // #nosec G301: used for mntS3 + err != nil { + klog.Errorf("MountpointMounter Mount: Cannot create directory %s: %v", metaPath, err) + return err + } + } + + os.Setenv("AWS_ACCESS_KEY_ID", mntS3.AccessKey) + os.Setenv("AWS_SECRET_ACCESS_KEY", mntS3.SecretKey) + + args := []string{ + fmt.Sprintf("--endpoint-url=%v", mntS3.EndPoint), + mntS3.BucketName, + target, + } + + if mntS3.ObjPath != "" { + args = append(args, fmt.Sprintf("--prefix %s", mntS3.ObjPath)) + } + return mntS3.MounterUtils.FuseMount(target, mntS3Cmd, args) +} +func (mntS3 *MountpointMounter) Unmount(target string) error { + klog.Info("-MountpointMounter Unmount-") + metaPath := path.Join(metaRootMntS3, fmt.Sprintf("%x", sha256.Sum256([]byte(target)))) + err := os.RemoveAll(metaPath) + if err != nil { + return err + } + + return mntS3.MounterUtils.FuseUnmount(target) +} diff --git a/pkg/mounter/mounter-mountpoint_test.go b/pkg/mounter/mounter-mountpoint_test.go new file mode 100644 index 00000000..5e489fcb --- /dev/null +++ b/pkg/mounter/mounter-mountpoint_test.go @@ -0,0 +1,284 @@ +// Package mounter +package mounter + +import ( + "errors" + "os" + "testing" + + mounterUtils "github.com/IBM/ibm-object-csi-driver/pkg/mounter/utils" + "github.com/stretchr/testify/assert" +) + +// Mock the secretMap and mountOptions +var secretMapMountpoint = map[string]string{ + "cosEndpoint": "test-endpoint", + "bucketName": "test-bucket-name", + "objPath": "test-obj-path", + "accessKey": "test-access-key", + "secretKey": "test-secret-key", + "apiKey": "test-api-key", +} + +var mountOptionsMountpoint = []string{"opt1=val1", "opt2=val2"} + +func TestNewMountpointMounter_Success(t *testing.T) { + mounter := NewMountpointMounter(secretMapMountpoint, mountOptionsMountpoint, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + + mntpMounter, ok := mounter.(*MountpointMounter) + if !ok { + t.Errorf("NewMountpointMounter() failed to return an instance of MountpointMounter") + } + + assert.Equal(t, mntpMounter.BucketName, secretMapMountpoint["bucketName"]) + assert.Equal(t, mntpMounter.ObjPath, secretMapMountpoint["objPath"]) + assert.Equal(t, mntpMounter.EndPoint, secretMapMountpoint["cosEndpoint"]) +} + +func TestNewMountpointMounter_Success_Hmac(t *testing.T) { + // Mock the secretMap and mountOptions + secretMap := map[string]string{ + "cosEndpoint": "test-endpoint", + "bucketName": "test-bucket-name", + "objPath": "test-obj-path", + "accessKey": "test-access-key", + "secretKey": "test-secret-key", + } + + mounter := NewMountpointMounter(secretMap, mountOptionsMountpoint, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + + mntpMounter, ok := mounter.(*MountpointMounter) + if !ok { + t.Errorf("NewMountpointMounter() failed to return an instance of MountpointMounter") + } + + assert.Equal(t, mntpMounter.BucketName, secretMap["bucketName"]) + assert.Equal(t, mntpMounter.ObjPath, secretMap["objPath"]) + assert.Equal(t, mntpMounter.EndPoint, secretMap["cosEndpoint"]) +} + +func TestNewMountpointMounter_MountOptsInSecret_Invalid(t *testing.T) { + secretMap := map[string]string{ + "cosEndpoint": "test-endpoint", + "bucketName": "test-bucket-name", + "objPath": "test-obj-path", + "accessKey": "test-access-key", + "secretKey": "test-secret-key", + "apiKey": "test-api-key", + "mountOptions": "upload_concurrency", + } + mounter := NewMountpointMounter(secretMap, mountOptionsMountpoint, mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{})) + + mntpMounter, ok := mounter.(*MountpointMounter) + if !ok { + t.Errorf("NewMountpointMounter() failed to return an instance of MountpointMounter") + } + + assert.Equal(t, mntpMounter.BucketName, secretMap["bucketName"]) + assert.Equal(t, mntpMounter.ObjPath, secretMap["objPath"]) + assert.Equal(t, mntpMounter.EndPoint, secretMap["cosEndpoint"]) +} + +func Test_MountpointMount_Positive(t *testing.T) { + mounter := NewMountpointMounter(secretMapMountpoint, mountOptionsMountpoint, + mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseMountFn: func(path string, comm string, args []string) error { + return nil + }, + })) + FakeMkdirAll := func(path string, perm os.FileMode) error { + return nil + } + + // Replace mkdirAllFunc with the Fake function + mkdirAllFunc = FakeMkdirAll + defer func() { mkdirAllFunc = os.MkdirAll }() + mntpMounter, ok := mounter.(*MountpointMounter) + if !ok { + t.Fatal("NewMountpointMounter() did not return a MountpointMounter") + } + + target := "/tmp/test-mount" + + err := mntpMounter.Mount("source", target) + assert.NoError(t, err) +} + +func Test_MountpointMount_Positive_Empty_ObjPath(t *testing.T) { + secretMap := map[string]string{ + "cosEndpoint": "test-endpoint", + "bucketName": "test-bucket-name", + "accessKey": "test-access-key", + "secretKey": "test-secret-key", + "apiKey": "test-api-key", + } + mounter := NewMountpointMounter(secretMap, mountOptionsMountpoint, + mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseMountFn: func(path string, comm string, args []string) error { + return nil + }, + })) + + FakeMkdirAll := func(path string, perm os.FileMode) error { + return nil + } + + // Replace mkdirAllFunc with the Fake function + mkdirAllFunc = FakeMkdirAll + defer func() { mkdirAllFunc = os.MkdirAll }() + + mntpMounter, ok := mounter.(*MountpointMounter) + if !ok { + t.Fatal("NewMountpointMounter() did not return a MountpointMounter") + } + + target := "/tmp/test-mount" + + err := mntpMounter.Mount("source", target) + assert.NoError(t, err) +} + +func Test_MountpointMount_Error_Creating_Mount_Point(t *testing.T) { + mounter := NewMountpointMounter(secretMapMountpoint, mountOptionsMountpoint, + mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseMountFn: func(path string, comm string, args []string) error { + return nil + }, + })) + + mntpMounter, ok := mounter.(*MountpointMounter) + if !ok { + t.Fatal("NewMountpointMounter() did not return a MountpointMounter") + } + + mockMkdirAll := func(path string, perm os.FileMode) error { + return errors.New("error creating mount path") + } + + // Replace mkdirAllFunc with the mock function + mkdirAllFunc = mockMkdirAll + defer func() { mkdirAllFunc = os.MkdirAll }() + + target := "/tmp/test-mount" + + err := mntpMounter.Mount("source", target) + assert.Error(t, err, "Cannot create directory") +} + +func Test_MountpointMount_ErrorMount(t *testing.T) { + mounter := NewMountpointMounter(secretMapMountpoint, mountOptionsMountpoint, + mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseMountFn: func(path string, comm string, args []string) error { + return errors.New("error mounting volume") + }, + })) + + FakeMkdirAll := func(path string, perm os.FileMode) error { + return nil + } + + // Replace mkdirAllFunc with the Fake function + mkdirAllFunc = FakeMkdirAll + defer func() { mkdirAllFunc = os.MkdirAll }() + + mntpMounter, ok := mounter.(*MountpointMounter) + if !ok { + t.Fatal("NewMountpointMounter() did not return a MountpointMounter") + } + + target := "/tmp/test-mount" + + err := mntpMounter.Mount("source", target) + assert.Error(t, err, "error mounting volume") +} + +func Test_MountpointUnmount_Positive(t *testing.T) { + secretMap := map[string]string{ + "cosEndpoint": "test-endpoint", + "bucketName": "test-bucket-name", + "objPath": "test-obj-path", + "accessKey": "test-access-key", + "secretKey": "test-secret-key", + "apiKey": "test-api-key", + } + mounter := NewMountpointMounter(secretMap, []string{"mountOption1", "mountOption2"}, + mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseUnmountFn: func(path string) error { + return nil + }, + })) + + mntpMounter, ok := mounter.(*MountpointMounter) + if !ok { + t.Fatal("NewMountpointMounter() did not return a MountpointMounter") + } + + target := "/tmp/test-unmount" + + // Creating a directory to simulate a mounted path + err := os.MkdirAll(target, os.ModePerm) + if err != nil { + t.Fatalf("Test_MountpointUnmount_Positive() failed to create directory: %v", err) + } + + err = mntpMounter.Unmount(target) + assert.NoError(t, err) + if err != nil { + t.Errorf("Test_MountpointUnmount_Positive() failed to unmount: %v", err) + } + + err = os.RemoveAll(target) + if err != nil { + t.Errorf("Failed to remove directory: %v", err) + } +} + +func Test_MountpointUnmount_Error(t *testing.T) { + secretMap := map[string]string{ + "cosEndpoint": "test-endpoint", + "bucketName": "test-bucket-name", + "objPath": "test-obj-path", + "accessKey": "test-access-key", + "secretKey": "test-secret-key", + "apiKey": "test-api-key", + } + mounter := NewMountpointMounter(secretMap, []string{"mountOption1", "mountOption2"}, + mounterUtils.NewFakeMounterUtilsImpl(mounterUtils.FakeMounterUtilsFuncStruct{ + FuseUnmountFn: func(path string) error { + return errors.New("error unmounting volume") + }, + })) + + mntpMounter := mounter.(*MountpointMounter) + + target := "/tmp/test-unmount" + + // Creating a directory to simulate a mounted path + err := os.MkdirAll(target, os.ModePerm) + if err != nil { + t.Fatalf("TestMountpointUnmount_Error() failed to create directory: %v", err) + } + + err = mntpMounter.Unmount(target) + assert.Error(t, err, "error unmounting volume") + + err = os.RemoveAll(target) + if err != nil { + t.Errorf("Failed to remove directory: %v", err) + } +} + +func TestUpdateMountpointMountOptions(t *testing.T) { + defaultMountOp := []string{"option1=value1", "option2=value2"} + secretMap := map[string]string{ + "mountOptions": "additional_option=value3", + } + + updatedOptions := updateMountOptions(defaultMountOp, secretMap) + + assert.ElementsMatch(t, updatedOptions, []string{ + "option1=value1", + "option2=value2", + "additional_option=value3", + }) +} diff --git a/pkg/mounter/mounter.go b/pkg/mounter/mounter.go index 16c84221..6a7f9f41 100644 --- a/pkg/mounter/mounter.go +++ b/pkg/mounter/mounter.go @@ -47,6 +47,8 @@ func (s *CSIMounterFactory) NewMounter(attrib map[string]string, secretMap map[s return NewS3fsMounter(secretMap, mountFlags, mounterUtils) case constants.RClone: return NewRcloneMounter(secretMap, mountFlags, mounterUtils) + case constants.MNTS3: + return NewMountpointMounter(secretMap, mountFlags, mounterUtils) default: // default to s3fs return NewS3fsMounter(secretMap, mountFlags, mounterUtils) diff --git a/pkg/mounter/mounter_test.go b/pkg/mounter/mounter_test.go index 7ca6b28c..e864163f 100644 --- a/pkg/mounter/mounter_test.go +++ b/pkg/mounter/mounter_test.go @@ -75,6 +75,28 @@ func TestNewMounter(t *testing.T) { }, expectedErr: nil, }, + { + name: "Mountpoint Mounter", + attrib: map[string]string{"mounter": constants.MNTS3}, + secretMap: map[string]string{ + "cosEndpoint": "test-endpoint", + "bucketName": "test-bucket-name", + "objPath": "test-obj-path", + "accessKey": "test-access-key", + "secretKey": "test-secret-key", + }, + mountOptions: []string{"opt1=val1", "opt2=val2"}, + expected: &MountpointMounter{ + BucketName: "test-bucket-name", + ObjPath: "test-obj-path", + EndPoint: "test-endpoint", + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + MountOptions: []string{"opt1=val1", "opt2=val2"}, + MounterUtils: &(mounterUtils.MounterOptsUtils{}), + }, + expectedErr: nil, + }, { name: "Default Mounter", attrib: map[string]string{},