diff --git a/build/csi-agent/Dockerfile b/build/csi-agent/Dockerfile index 1a9888915..23867227c 100644 --- a/build/csi-agent/Dockerfile +++ b/build/csi-agent/Dockerfile @@ -15,8 +15,11 @@ RUN --mount=type=bind,target=. \ FROM registry-cn-hangzhou.ack.aliyuncs.com/dev/alinux:3-update ARG TARGETPLATFORM -ARG OSSFS_VERSION=v1.91.7.ack.1 -ARG OSSFS2_VERSION=2.0.1beta +# OSSFS and OSSFS2 client versions +# Note: These are the versions of the OSS client RPMs included in the image. +# When upgrading the image version, the client versions may remain unchanged. +ARG OSSFS_VERSION=v1.91.7.ack.2 +ARG OSSFS2_VERSION=v2.0.2beta.ack.1 ARG ALINAS_UTILS_VERSION=1.6-1.20241101165952.ce0ef4 ARG EFC_VERSION=1.6-20241028201622.a31063 RUN set -ex; \ @@ -30,7 +33,7 @@ RUN set -ex; \ if [ "${ARCH}" = "x86_64" ]; then \ yum install -y https://aliyun-alinas-eac.oss-cn-beijing.aliyuncs.com/aliyun-alinas-utils-${ALINAS_UTILS_VERSION}.al7.noarch.rpm; \ yum install -y https://aliyun-alinas-eac.oss-cn-beijing.aliyuncs.com/alinas-efc-${EFC_VERSION}.release.${ARCH}.rpm; \ - yum install -y https://gosspublic.alicdn.com/ossfs/ossfs2_${OSSFS2_VERSION}_linux_${ARCH}.rpm; \ + yum install -y https://ack-csiplugin.oss-cn-hangzhou.aliyuncs.com/ossfs2/ossfs2_${OSSFS2_VERSION}_centos8.0_${ARCH}.rpm; \ fi; \ yum clean all diff --git a/build/multi/Dockerfile.multi b/build/multi/Dockerfile.multi index 232f50895..d7a0d87da 100644 --- a/build/multi/Dockerfile.multi +++ b/build/multi/Dockerfile.multi @@ -17,7 +17,7 @@ RUN --mount=type=bind,target=. \ FROM registry-cn-hangzhou.ack.aliyuncs.com/dev/ack-base/distroless/base-debian12:latest@sha256:cef75d12148305c54ef5769e6511a5ac3c820f39bf5c8a4fbfd5b76b4b8da843 as distroless-base LABEL maintainers="Alibaba Cloud Authors" description="Alibaba Cloud CSI Plugin" -LABEL defaultOssfsImageTag="v1.91.7.ack.1-570be5f-aliyun" defaultOssfs2ImageTag="v2.0.2.ack.1-a76655f-aliyun" +LABEL defaultOssfsImageTag="v1.91.7.ack.2-f04b152-aliyun" defaultOssfs2ImageTag="v2.0.2.ack.2-6ef4e9c-aliyun" FROM distroless-base as csi-base COPY --link --from=build /out/plugin.csi.alibabacloud.com /usr/bin/plugin.csi.alibabacloud.com diff --git a/build/multi/Dockerfile.multi.asi b/build/multi/Dockerfile.multi.asi index 6a5733407..934849a91 100644 --- a/build/multi/Dockerfile.multi.asi +++ b/build/multi/Dockerfile.multi.asi @@ -25,7 +25,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ FROM registry.eu-west-1.aliyuncs.com/acs/alinux:3-update as base LABEL maintainers="Alibaba Cloud Authors" description="Alibaba Cloud CSI Plugin" -LABEL defaultOssfsImageTag="v1.91.7.ack.1-570be5f-aliyun" defaultOssfs2ImageTag="v2.0.2.ack.1-a76655f-aliyun" +LABEL defaultOssfsImageTag="v1.91.7.ack.2-f04b152-aliyun" defaultOssfs2ImageTag="v2.0.2.ack.2-6ef4e9c-aliyun" RUN yum install -y ca-certificates file tzdata nfs-utils xfsprogs e4fsprogs pciutils iputils strace util-linux nc telnet tar cpio lsof && \ yum clean all diff --git a/deploy/chart/values.yaml b/deploy/chart/values.yaml index ef38da506..ddbe8daba 100644 --- a/deploy/chart/values.yaml +++ b/deploy/chart/values.yaml @@ -137,7 +137,7 @@ images: tag: "v2.14.0-aliyun" ossfs: repo: acs/csi-ossfs - tag: "v1.91.7.ack.1-570be5f-aliyun" + tag: "v1.91.7.ack.2-f04b152-aliyun" ossfs2: repo: acs/csi-ossfs2 - tag: "v2.0.2.ack.1-a76655f-aliyun" + tag: "v2.0.2.ack.2-6ef4e9c-aliyun" diff --git a/pkg/mounter/cmd_mounter.go b/pkg/mounter/cmd_mounter.go index 483c23175..6b46d339e 100644 --- a/pkg/mounter/cmd_mounter.go +++ b/pkg/mounter/cmd_mounter.go @@ -5,9 +5,10 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "time" - "github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pkg/mounter/utils" + "k8s.io/klog/v2" "k8s.io/mount-utils" ) @@ -27,11 +28,19 @@ func NewOssCmdMounter(execPath, volumeId string, inner mount.Interface) Mounter } } +func (m *OssCmdMounter) Name() string { + return "cmd-mounter" +} + +func (m *OssCmdMounter) RotateToken(target, fstype string, secrets map[string]string) error { + return ErrNotImplemented(m.Name(), fstype, "rotateToken") +} + func (m *OssCmdMounter) MountWithSecrets(source, target, fstype string, options []string, secrets map[string]string) error { ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(timeout)) defer cancel() - passwd, err := utils.SaveOssSecretsToFile(secrets) + passwd, err := saveOssSecretsToFile(secrets) if err != nil { return err } @@ -47,3 +56,21 @@ func (m *OssCmdMounter) MountWithSecrets(source, target, fstype string, options } return nil } + +func saveOssSecretsToFile(secrets map[string]string) (filePath string, err error) { + passwd := secrets["passwd-ossfs"] + if passwd == "" { + return + } + + tmpDir, err := os.MkdirTemp("", "ossfs-") + if err != nil { + return "", err + } + filePath = filepath.Join(tmpDir, "passwd") + if err = os.WriteFile(filePath, []byte(passwd), 0o600); err != nil { + return "", err + } + klog.V(4).InfoS("created ossfs passwd file", "path", filePath) + return +} diff --git a/pkg/mounter/mounter.go b/pkg/mounter/mounter.go index 0897313ff..f070a7407 100644 --- a/pkg/mounter/mounter.go +++ b/pkg/mounter/mounter.go @@ -1,10 +1,26 @@ package mounter import ( + "fmt" + "strings" + mountutils "k8s.io/mount-utils" ) type Mounter interface { mountutils.Interface + Name() string MountWithSecrets(source, target, fstype string, options []string, secrets map[string]string) error + RotateToken(target, fstype string, secrets map[string]string) error +} + +func ErrNotImplemented(driver, mounterType, method string) error { + return fmt.Errorf("%s(%s): %s not implemented", mounterType, driver, method) +} + +func IsNotImplementedErr(err error) bool { + if err == nil { + return false + } + return strings.Contains(err.Error(), "not implemented") } diff --git a/pkg/mounter/oss/oss_fuse_manager.go b/pkg/mounter/oss/oss_fuse_manager.go index a4fdd1204..1568b5cee 100644 --- a/pkg/mounter/oss/oss_fuse_manager.go +++ b/pkg/mounter/oss/oss_fuse_manager.go @@ -48,6 +48,17 @@ const ( AuthTypePublic = "public" ) +type AccessKey struct { + AkID string `json:"akId"` + AkSecret string `json:"akSecret"` +} +type TokenSecret struct { + AccessKeyId string `json:"AccessKeyId"` + AccessKeySecret string `json:"AccessKeySecret"` + Expiration string `json:"Expiration"` + SecurityToken string `json:"SecurityToken"` +} + // Options contains options for target oss type Options struct { DirectAssigned bool @@ -60,9 +71,10 @@ type Options struct { // authorization options // accesskey - AkID string `json:"akId"` - AkSecret string `json:"akSecret"` - SecretRef string `json:"secretRef"` + AccessKey `json:",inline"` + TokenSecret `json:",inline"` + SecretRef string `json:"secretRef"` + // RRSA RoleName string `json:"roleName"` // also for STS RoleArn string `json:"roleArn"` diff --git a/pkg/mounter/oss/ossfs.go b/pkg/mounter/oss/ossfs.go index 9755d1aee..1eb628772 100644 --- a/pkg/mounter/oss/ossfs.go +++ b/pkg/mounter/oss/ossfs.go @@ -20,7 +20,7 @@ import ( ) var defaultOssfsImageTag = "v1.88.4-80d165c-aliyun" -var defaultOssfsUpdatedImageTag = "v1.91.7.ack.1-570be5f-aliyun" +var defaultOssfsUpdatedImageTag = "v1.91.7.ack.2-f04b152-aliyun" var defaultOssfsDbglevel = utils.DebugLevelWarn const ( @@ -87,7 +87,15 @@ func (f *fuseOssfs) PrecheckAuthConfig(o *Options, onNode bool) error { if features.FunctionalMutableFeatureGate.Enabled(features.RundCSIProtocol3) { return nil } - if o.SecretRef != "" { + // Token authentication: + // For runc scenarios, set the SecretRef parameter. + runc := o.SecretRef != "" + // For rund or eci scenarios, configure Token in nodePublishSecretRef or nodeStageSecretRef. + rund := o.AccessKeyId != "" && o.AccessKeySecret != "" && o.Expiration != "" && o.SecurityToken != "" + if runc && rund { + return fmt.Errorf("Token and secretRef cannot be set at the same time") + } + if rund || runc { if o.AkID != "" || o.AkSecret != "" { return fmt.Errorf("AK and secretRef cannot be set at the same time") } @@ -119,13 +127,26 @@ func (f *fuseOssfs) MakeAuthConfig(o *Options, m metadata.MetadataProvider) (*ut case AuthTypeSTS: authCfg.RoleName = o.RoleName default: - if o.SecretRef != "" { - authCfg.SecretRef = o.SecretRef - } else { + // fixed AKSK + if o.AkID != "" && o.AkSecret != "" { authCfg.Secrets = map[string]string{ utils.GetPasswdFileName(f.Name()): fmt.Sprintf("%s:%s:%s", o.Bucket, o.AkID, o.AkSecret), } + return authCfg, nil + } + // secretRef for RunC + if o.SecretRef != "" { + authCfg.SecretRef = o.SecretRef + return authCfg, nil + } + // token secret for RunD + authCfg.Secrets = map[string]string{ + KeyAccessKeyId: o.AccessKeyId, + KeyAccessKeySecret: o.AccessKeySecret, + KeySecurityToken: o.SecurityToken, + KeyExpiration: o.Expiration, } + } return authCfg, nil } @@ -289,11 +310,17 @@ func (f *fuseOssfs) getAuthOptions(o *Options, region string) (mountOptions []st mountOptions = append(mountOptions, "ram_role="+o.RoleName) } default: + // fixed AKSK + if o.AkID != "" && o.AkSecret != "" { + // for aksk in secret, it will make passwd_file option in mount-proxy server as it's under a tempdir + return + } + // secretRef for runC or token secret for runD if o.SecretRef != "" { mountOptions = append(mountOptions, fmt.Sprintf("passwd_file=%s", filepath.Join(utils.GetConfigDir(o.FuseType), utils.GetPasswdFileName(o.FuseType)))) - mountOptions = append(mountOptions, "use_session_token") } - // publishSecretRef will make option in mount-proxy server + // for token in secret, it will make passwd_file option in mount-proxy server as it's under a tempdir + mountOptions = append(mountOptions, "use_session_token") } return } diff --git a/pkg/mounter/oss/ossfs2.go b/pkg/mounter/oss/ossfs2.go index 4ff2d4988..f58cbaa59 100644 --- a/pkg/mounter/oss/ossfs2.go +++ b/pkg/mounter/oss/ossfs2.go @@ -17,7 +17,7 @@ import ( "k8s.io/utils/ptr" ) -var defaultOssfs2ImageTag = "v2.0.2.ack.1-a76655f-aliyun" +var defaultOssfs2ImageTag = "v2.0.2.ack.2-6ef4e9c-aliyun" var defaultOssfs2Dbglevel = utils.DebugLevelInfo type fuseOssfs2 struct { @@ -64,7 +64,16 @@ func (f *fuseOssfs2) PrecheckAuthConfig(o *Options, onNode bool) error { if features.FunctionalMutableFeatureGate.Enabled(features.RundCSIProtocol3) { return nil } - if o.SecretRef != "" { + // Token authentication: + // For runc scenarios, set the SecretRef parameter. + runc := o.SecretRef != "" + // For rund or eci scenarios, configure Token in nodePublishSecretRef or nodeStageSecretRef. + // Expiration is not required for ossfs2.0 + rund := o.AccessKeyId != "" && o.AccessKeySecret != "" && o.SecurityToken != "" + if runc && rund { + return fmt.Errorf("Token and secretRef cannot be set at the same time") + } + if rund || runc { if o.AkID != "" || o.AkSecret != "" { return fmt.Errorf("AK and secretRef cannot be set at the same time") } @@ -95,13 +104,25 @@ func (f *fuseOssfs2) MakeAuthConfig(o *Options, m metadata.MetadataProvider) (au case AuthTypeSTS: authCfg.RoleName = o.RoleName case "": + // fixed AKSK + if o.AkID != "" && o.AkSecret != "" { + authCfg.Secrets = map[string]string{ + utils.GetPasswdFileName(f.Name()): fmt.Sprintf("--oss_access_key_id=%s\n--oss_access_key_secret=%s", o.AkID, o.AkSecret), + } + return + } + // secretRef for RunC if o.SecretRef != "" { authCfg.SecretRef = o.SecretRef return } + // token secret for RunD authCfg.Secrets = map[string]string{ - utils.GetPasswdFileName(f.Name()): fmt.Sprintf("--oss_access_key_id=%s\n--oss_access_key_secret=%s", o.AkID, o.AkSecret), + KeyAccessKeyId: o.AccessKeyId, + KeyAccessKeySecret: o.AccessKeySecret, + KeySecurityToken: o.SecurityToken, } + default: return nil, fmt.Errorf("%s do not support authType: %s", f.Name(), o.AuthType) } @@ -162,6 +183,11 @@ func (f *fuseOssfs2) getAuthOptions(o *Options, region string) (mountOptions []s mountOptions = append(mountOptions, "ram_role="+o.RoleName) } case "": + // fixed AKSK + if o.AkID != "" && o.AkSecret != "" { + // for aksk in secret, it will make passwd_file option in mount-proxy server as it's under a tempdir + return + } if o.SecretRef != "" { mountOptions = append(mountOptions, fmt.Sprintf("oss_sts_multi_conf_ak_file=%s", filepath.Join(utils.GetConfigDir(o.FuseType), utils.GetPasswdFileName(o.FuseType), KeyAccessKeyId)), @@ -169,7 +195,7 @@ func (f *fuseOssfs2) getAuthOptions(o *Options, region string) (mountOptions []s fmt.Sprintf("oss_sts_multi_conf_token_file=%s", filepath.Join(utils.GetConfigDir(o.FuseType), utils.GetPasswdFileName(o.FuseType), KeySecurityToken)), ) } - // publishSecretRef will make option in mount-proxy server + // for token in secret, it will make passwd_file option in mount-proxy server as it's under a tempdir default: return nil } diff --git a/pkg/mounter/oss/ossfs2_test.go b/pkg/mounter/oss/ossfs2_test.go index 62f324c62..067dce450 100644 --- a/pkg/mounter/oss/ossfs2_test.go +++ b/pkg/mounter/oss/ossfs2_test.go @@ -69,16 +69,19 @@ func TestPrecheckAuthConfig_ossfs2(t *testing.T) { { "empty aksecret", &Options{ - AkID: "test-ak", - AkSecret: "", + AccessKey: AccessKey{ + AkID: "test-ak", + }, }, true, }, { "success - aksk", &Options{ - AkID: "test-ak", - AkSecret: "test-ak-secret", + AccessKey: AccessKey{ + AkID: "test-ak", + AkSecret: "test-ak-secret", + }, }, false, }, @@ -89,6 +92,37 @@ func TestPrecheckAuthConfig_ossfs2(t *testing.T) { }, false, }, + { + name: "token republish", + opts: &Options{ + URL: "1.1.1.1", + Bucket: "aliyun", + Path: "/path", + TokenSecret: TokenSecret{ + AccessKeyId: "akid", + AccessKeySecret: "aksecret", + SecurityToken: "securitytoken", + }, + FuseType: OssFs2Type, + }, + wantErr: false, + }, + { + name: "conflicts token", + opts: &Options{ + URL: "1.1.1.1", + Bucket: "aliyun", + Path: "/path", + TokenSecret: TokenSecret{ + AccessKeyId: "akid", + AccessKeySecret: "aksecret", + SecurityToken: "securitytoken", + }, + SecretRef: "non-empty", + FuseType: OssFs2Type, + }, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -124,8 +158,10 @@ func TestMakeAuthConfig_ossfs2(t *testing.T) { { "aksk", &Options{ - AkID: "test-ak", - AkSecret: "test-ak-secret", + AccessKey: AccessKey{ + AkID: "test-ak", + AkSecret: "test-ak-secret", + }, }, &utils.AuthConfig{ Secrets: map[string]string{ @@ -180,6 +216,29 @@ func TestMakeAuthConfig_ossfs2(t *testing.T) { }, false, }, + { + name: "OtherAuthType_TokenSecrets", + options: &Options{ + AuthType: "", + Bucket: "bucket", + TokenSecret: TokenSecret{ + AccessKeyId: "ak-id", + AccessKeySecret: "ak-secret", + Expiration: "expiration", + SecurityToken: "security-token", + }, + FuseType: OssFs2Type, + }, + wantCfg: &utils.AuthConfig{ + AuthType: "", + Secrets: map[string]string{ + KeyAccessKeyId: "ak-id", + KeyAccessKeySecret: "ak-secret", + KeySecurityToken: "security-token", + }, + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -201,8 +260,10 @@ func TestMakeMountOptions_ossfs2(t *testing.T) { { name: "ro", opts: &Options{ - AkID: "test-ak", - AkSecret: "test-ak-secret", + AccessKey: AccessKey{ + AkID: "test-ak", + AkSecret: "test-ak-secret", + }, Bucket: "test-bucket", Path: "/", URL: "oss://test-bucket/", @@ -218,8 +279,10 @@ func TestMakeMountOptions_ossfs2(t *testing.T) { { name: "sigv4", opts: &Options{ - AkID: "test-ak", - AkSecret: "test-ak-secret", + AccessKey: AccessKey{ + AkID: "test-ak", + AkSecret: "test-ak-secret", + }, Bucket: "test-bucket", Path: "/", URL: "oss://test-bucket/", @@ -236,8 +299,10 @@ func TestMakeMountOptions_ossfs2(t *testing.T) { { name: "sigv4 with empty region", opts: &Options{ - AkID: "test-ak", - AkSecret: "test-ak-secret", + AccessKey: AccessKey{ + AkID: "test-ak", + AkSecret: "test-ak-secret", + }, Bucket: "test-bucket", Path: "/", URL: "oss://test-bucket/", @@ -374,6 +439,18 @@ func TestGetAuthOpttions_ossfs2(t *testing.T) { "rrsa_endpoint=https://sts-vpc.cn-hangzhou.aliyuncs.com", }, }, + { + name: "token", + opts: &Options{ + FuseType: "ossfs2", + TokenSecret: TokenSecret{ + AccessKeyId: "test-id", + AccessKeySecret: "test-secret", + SecurityToken: "test-token", + }, + }, + wantOptions: nil, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/mounter/oss/ossfs_test.go b/pkg/mounter/oss/ossfs_test.go index 17fb7dffe..959085ad7 100644 --- a/pkg/mounter/oss/ossfs_test.go +++ b/pkg/mounter/oss/ossfs_test.go @@ -81,11 +81,13 @@ func TestPrecheckAuthConfig_ossfs(t *testing.T) { { name: "success with accessKey", opts: &Options{ - URL: "1.1.1.1", - Bucket: "aliyun", - Path: "/path", - AkID: "11111", - AkSecret: "22222", + URL: "1.1.1.1", + Bucket: "aliyun", + Path: "/path", + AccessKey: AccessKey{ + AkID: "akid", + AkSecret: "aksecret", + }, FuseType: OssFsType, }, wantErr: false, @@ -108,8 +110,10 @@ func TestPrecheckAuthConfig_ossfs(t *testing.T) { Bucket: "aliyun", Path: "/path", SecretRef: "secret", - AkID: "11111", - FuseType: OssFsType, + AccessKey: AccessKey{ + AkID: "akid", + }, + FuseType: OssFsType, }, wantErr: true, }, @@ -127,11 +131,13 @@ func TestPrecheckAuthConfig_ossfs(t *testing.T) { { name: "use assumeRole with non-RRSA authType", opts: &Options{ - URL: "1.1.1.1", - Bucket: "aliyun", - Path: "/path", - SecretRef: "secret", - AkID: "11111", + URL: "1.1.1.1", + Bucket: "aliyun", + Path: "/path", + SecretRef: "secret", + AccessKey: AccessKey{ + AkID: "akid", + }, AssumeRoleArn: "test-assume-role-arn", FuseType: OssFsType, }, @@ -194,6 +200,39 @@ func TestPrecheckAuthConfig_ossfs(t *testing.T) { }, wantErr: false, }, + { + name: "token republish", + opts: &Options{ + URL: "1.1.1.1", + Bucket: "aliyun", + Path: "/path", + TokenSecret: TokenSecret{ + AccessKeyId: "akid", + AccessKeySecret: "aksecret", + Expiration: "expiration", + SecurityToken: "securitytoken", + }, + FuseType: OssFsType, + }, + wantErr: false, + }, + { + name: "conflicts token", + opts: &Options{ + URL: "1.1.1.1", + Bucket: "aliyun", + Path: "/path", + TokenSecret: TokenSecret{ + AccessKeyId: "akid", + AccessKeySecret: "aksecret", + Expiration: "expiration", + SecurityToken: "securitytoken", + }, + SecretRef: "non-empty", + FuseType: OssFsType, + }, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -284,8 +323,10 @@ func TestMakeAuthConfig_ossfs(t *testing.T) { options: &Options{ AuthType: "", Bucket: "bucket", - AkID: "ak-id", - AkSecret: "ak-secret", + AccessKey: AccessKey{ + AkID: "ak-id", + AkSecret: "ak-secret", + }, FuseType: OssFsType, }, expectedConfig: &utils.AuthConfig{ @@ -296,6 +337,30 @@ func TestMakeAuthConfig_ossfs(t *testing.T) { }, expectedError: nil, }, + { + name: "OtherAuthType_TokenSecrets", + options: &Options{ + AuthType: "", + Bucket: "bucket", + TokenSecret: TokenSecret{ + AccessKeyId: "ak-id", + AccessKeySecret: "ak-secret", + Expiration: "expiration", + SecurityToken: "security-token", + }, + FuseType: OssFsType, + }, + expectedConfig: &utils.AuthConfig{ + AuthType: "", + Secrets: map[string]string{ + KeyAccessKeyId: "ak-id", + KeyAccessKeySecret: "ak-secret", + KeySecurityToken: "security-token", + KeyExpiration: "expiration", + }, + }, + expectedError: nil, + }, } for _, tt := range tests { @@ -324,6 +389,10 @@ func TestMakeMountOptions_ossfs(t *testing.T) { name: "Basic Options", opts: &Options{ URL: "oss://bucket", + AccessKey: AccessKey{ + AkID: "ak-id", + AkSecret: "ak-secret", + }, }, expected: []string{ "url=oss://bucket", @@ -335,6 +404,10 @@ func TestMakeMountOptions_ossfs(t *testing.T) { opts: &Options{ URL: "oss://bucket", ReadOnly: true, + AccessKey: AccessKey{ + AkID: "ak-id", + AkSecret: "ak-secret", + }, }, expected: []string{ "url=oss://bucket", @@ -347,6 +420,10 @@ func TestMakeMountOptions_ossfs(t *testing.T) { opts: &Options{ URL: "oss://bucket", Encrypted: EncryptedTypeAes256, + AccessKey: AccessKey{ + AkID: "ak-id", + AkSecret: "ak-secret", + }, }, expected: []string{ "url=oss://bucket", @@ -360,6 +437,10 @@ func TestMakeMountOptions_ossfs(t *testing.T) { URL: "oss://bucket", Encrypted: EncryptedTypeKms, KmsKeyId: "1234", + AccessKey: AccessKey{ + AkID: "ak-id", + AkSecret: "ak-secret", + }, }, expected: []string{ "url=oss://bucket", @@ -389,6 +470,10 @@ func TestMakeMountOptions_ossfs(t *testing.T) { opts: &Options{ URL: "oss://bucket", SigVersion: SigV4, + AccessKey: AccessKey{ + AkID: "ak-id", + AkSecret: "ak-secret", + }, }, region: "us-east-1", expected: []string{ @@ -434,6 +519,21 @@ func TestMakeMountOptions_ossfs(t *testing.T) { }, { name: "DefaultAuthType", + opts: &Options{ + URL: "oss://bucket", + FuseType: "ossfs", + AccessKey: AccessKey{ + AkID: "ak-id", + AkSecret: "ak-secret", + }, + }, + expected: []string{ + "url=oss://bucket", + "use_metrics", + }, + }, + { + name: "SecretRef-AuthType", opts: &Options{ URL: "oss://bucket", SecretRef: "secret", @@ -446,6 +546,21 @@ func TestMakeMountOptions_ossfs(t *testing.T) { "use_metrics", }, }, + { + name: "TokenSecret-AuthType", + opts: &Options{ + URL: "oss://bucket", + FuseType: "ossfs", + TokenSecret: TokenSecret{ + AccessKeyId: "akid", + }, + }, + expected: []string{ + "url=oss://bucket", + "use_session_token", + "use_metrics", + }, + }, } for _, tt := range tests { @@ -532,6 +647,25 @@ func TestGetAuthOpttions_ossfs(t *testing.T) { name: "aksk", opts: &Options{ FuseType: "ossfs", + AccessKey: AccessKey{ + AkID: "test-akid", + AkSecret: "test-aksecret", + }, + }, + }, + { + name: "token", + opts: &Options{ + FuseType: "ossfs", + TokenSecret: TokenSecret{ + AccessKeyId: "test-akid", + AccessKeySecret: "test-aksecret", + Expiration: "2023-01-01T00:00:00Z", + SecurityToken: "test-token", + }, + }, + wantOptions: []string{ + "use_session_token", }, }, } diff --git a/pkg/mounter/proxy/client/client.go b/pkg/mounter/proxy/client/client.go index 9fe126774..25fa1c773 100644 --- a/pkg/mounter/proxy/client/client.go +++ b/pkg/mounter/proxy/client/client.go @@ -21,6 +21,7 @@ const ( type Client interface { Mount(req *proxy.MountRequest) (*proxy.Response, error) + RotateToken(req *proxy.RotateTokenRequest) (*proxy.Response, error) } type client struct { @@ -94,3 +95,12 @@ func (c *client) Mount(req *proxy.MountRequest) (*proxy.Response, error) { Body: req, }) } + +func (c *client) RotateToken(req *proxy.RotateTokenRequest) (*proxy.Response, error) { + return c.doRequest(&proxy.Request{ + Header: proxy.Header{ + Method: proxy.RotateToken, + }, + Body: req, + }) +} diff --git a/pkg/mounter/proxy/protocol.go b/pkg/mounter/proxy/protocol.go index 911e4ad53..cee6645e6 100644 --- a/pkg/mounter/proxy/protocol.go +++ b/pkg/mounter/proxy/protocol.go @@ -12,7 +12,8 @@ const ( type Method string const ( - Mount Method = "mount" + Mount Method = "mount" + RotateToken Method = "rotateToken" ) type Header struct { @@ -43,3 +44,9 @@ type MountRequest struct { MountFlags []string `json:"mountFlags,omitempty"` Secrets map[string]string `json:"secrets,omitempty"` } + +type RotateTokenRequest struct { + Target string `json:"target,omitempty"` + Fstype string `json:"fstype,omitempty"` + Secrets map[string]string `json:"secrets,omitempty"` +} diff --git a/pkg/mounter/proxy/server/alinas/driver.go b/pkg/mounter/proxy/server/alinas/driver.go index 255b43e2a..e779a9bde 100644 --- a/pkg/mounter/proxy/server/alinas/driver.go +++ b/pkg/mounter/proxy/server/alinas/driver.go @@ -11,9 +11,10 @@ import ( "syscall" "time" + "github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pkg/mounter" "github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pkg/mounter/proxy" "github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pkg/mounter/proxy/server" - mounter "github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pkg/mounter/utils" + mounterutils "github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pkg/mounter/utils" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/klog/v2" "k8s.io/mount-utils" @@ -52,6 +53,10 @@ func (h *Driver) Mount(ctx context.Context, req *proxy.MountRequest) error { return h.mounter.Mount(req.Source, req.Target, req.Fstype, options) } +func (h *Driver) RotateToken(ctx context.Context, req *proxy.RotateTokenRequest) error { + return mounter.ErrNotImplemented("proxy-mounter", h.Name(), "rotateToken") +} + func (h *Driver) Init() { go runCommandForever("aliyun-alinas-mount-watchdog") go runCommandForever("aliyun-cpfs-mount-watchdog") @@ -79,7 +84,7 @@ func addAutoFallbackNFSMountOptions(mountOptions []string) []string { isEFC := false isVSC := false for _, options := range mountOptions { - for _, option := range mounter.SplitMountOptions(options) { + for _, option := range mounterutils.SplitMountOptions(options) { if option == "" { continue } diff --git a/pkg/mounter/proxy/server/driver.go b/pkg/mounter/proxy/server/driver.go index 3c04a80da..cba37d3bb 100644 --- a/pkg/mounter/proxy/server/driver.go +++ b/pkg/mounter/proxy/server/driver.go @@ -13,6 +13,7 @@ type Driver interface { Init() Terminate() Mount(ctx context.Context, req *proxy.MountRequest) error + RotateToken(ctx context.Context, req *proxy.RotateTokenRequest) error } var ( @@ -31,3 +32,11 @@ func handleMountRequest(ctx context.Context, req *proxy.MountRequest) error { } return h.Mount(ctx, req) } + +func handleRotateTokenRequest(ctx context.Context, req *proxy.RotateTokenRequest) error { + h := fstypeToDriver[req.Fstype] + if h == nil { + return fmt.Errorf("fstype %q not supported", req.Fstype) + } + return h.RotateToken(ctx, req) +} diff --git a/pkg/mounter/proxy/server/handler.go b/pkg/mounter/proxy/server/handler.go index 9df77b723..47d91145d 100644 --- a/pkg/mounter/proxy/server/handler.go +++ b/pkg/mounter/proxy/server/handler.go @@ -105,6 +105,20 @@ func handle(ctx context.Context, req *rawRequest) proxy.Response { Error: err.Error(), } } + case proxy.RotateToken: + var rotateTokenReq proxy.RotateTokenRequest + err := json.Unmarshal(req.Body, &rotateTokenReq) + if err != nil { + return proxy.Response{ + Error: err.Error(), + } + } + err = handleRotateTokenRequest(ctx, &rotateTokenReq) + if err != nil { + return proxy.Response{ + Error: err.Error(), + } + } default: return proxy.Response{ Error: "invalid method", diff --git a/pkg/mounter/proxy/server/ossfs/driver.go b/pkg/mounter/proxy/server/ossfs/driver.go index c8e5cacde..e1b3759eb 100644 --- a/pkg/mounter/proxy/server/ossfs/driver.go +++ b/pkg/mounter/proxy/server/ossfs/driver.go @@ -6,10 +6,12 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "sync" "syscall" "time" + "github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pkg/mounter/oss" "github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pkg/mounter/proxy" "github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pkg/mounter/proxy/server" "github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pkg/mounter/utils" @@ -47,11 +49,17 @@ func (h *Driver) Mount(ctx context.Context, req *proxy.MountRequest) error { options := req.Options // prepare passwd file - passwdFile, err := utils.SaveOssSecretsToFile(req.Secrets) + passwdFile, tokenDir, credOpts, err := prepareCredentialFiles(req.Target, req.Secrets) if err != nil { - return err + return fmt.Errorf("prepare credential files failed: %w", err) + } + options = append(options, credOpts...) + if passwdFile != "" { + klog.V(4).InfoS("created ossfs passwd file", "path", passwdFile) + } + if tokenDir != "" { + klog.V(4).InfoS("created ossfs token directory", "dir", tokenDir) } - options = append(options, "passwd_file="+passwdFile) args := mount.MakeMountArgs(req.Source, req.Target, "", options) args = append(args, req.MountFlags...) @@ -84,9 +92,9 @@ func (h *Driver) Mount(ctx context.Context, req *proxy.MountRequest) error { klog.InfoS("ossfs exited", "mountpoint", target, "pid", pid) } ossfsExited <- err - if err := os.Remove(passwdFile); err != nil { - klog.ErrorS(err, "Remove passwd file", "mountpoint", target, "path", passwdFile) - } + // Note: No need to clean up credential files since after rotation support, + // files are stored in fixed paths and won't generate multiple copies that + // could lead to files leakage. close(ossfsExited) }() @@ -134,6 +142,28 @@ func (h *Driver) Mount(ctx context.Context, req *proxy.MountRequest) error { return err } +func (h *Driver) RotateToken(ctx context.Context, req *proxy.RotateTokenRequest) error { + // no need to rotate if there is no token in request + if req.Secrets == nil { + return nil + } + if token := req.Secrets[oss.KeySecurityToken]; token == "" { + return nil + } + + // prepare passwd file + hashDir := utils.GetPasswdHashDir(req.Target) + tokenDir := filepath.Join(hashDir, utils.GetPasswdFileName("ossfs")) + rotated, err := rotateTokenFiles(tokenDir, req.Secrets) + if err != nil { + return fmt.Errorf("rotate token files failed: %w", err) + } + if rotated { + klog.V(4).InfoS("rotate ossfs token files") + } + return nil +} + func (h *Driver) Init() {} func (h *Driver) Terminate() { diff --git a/pkg/mounter/proxy/server/ossfs/utils.go b/pkg/mounter/proxy/server/ossfs/utils.go new file mode 100644 index 000000000..636f896c8 --- /dev/null +++ b/pkg/mounter/proxy/server/ossfs/utils.go @@ -0,0 +1,77 @@ +package ossfs + +import ( + "fmt" + "os" + "path/filepath" + + "k8s.io/klog/v2" + + "github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pkg/mounter/oss" + "github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pkg/mounter/utils" +) + +// rotateTokenFiles rotates (or initializes) token files +// This function assumes that token rotation is required when executed +func rotateTokenFiles(dir string, secrets map[string]string) (rotated bool, err error) { + var fileUpdate bool + tokenKey := []string{oss.KeyAccessKeyId, oss.KeyAccessKeySecret, oss.KeySecurityToken, oss.KeyExpiration} + for _, key := range tokenKey { + val := secrets[key] + if val == "" { + err = fmt.Errorf("invalid authorization. %s is empty", key) + klog.Error(err) + return + } + fileUpdate, err = utils.WriteFileWithLock(filepath.Join(dir, key), []byte(val), 0o600) + if err != nil { + klog.Errorf("WriteFileWithLock %s failed %v", key, err) + return + } + rotated = fileUpdate || rotated + } + return +} + +// prepareCredentialFiles returns: +// 1. file: path of ossfs credential file for fixed AKSK +// 2. dir: dorectory of ossfs credential files for token +// 3. options: extra options +// 4. error +func prepareCredentialFiles(target string, secrets map[string]string) (file, dir string, options []string, err error) { + // fixed AKSK + hashDir := utils.GetPasswdHashDir(target) + + if passwd := secrets[utils.GetPasswdFileName("ossfs")]; passwd != "" { + err = os.MkdirAll(hashDir, 0o644) + if err != nil { + klog.Errorf("mkdirall hashdir failed %v", err) + return + } + _, err = utils.WriteFileWithLock(filepath.Join(hashDir, utils.GetPasswdFileName("ossfs")), []byte(passwd), 0o600) + if err != nil { + return + } + file = filepath.Join(hashDir, utils.GetPasswdFileName("ossfs")) + options = append(options, "passwd_file="+file) + return + } + + // token + if token := secrets[oss.KeySecurityToken]; token != "" { + tokenDir := filepath.Join(hashDir, utils.GetPasswdFileName("ossfs")) + err = os.MkdirAll(tokenDir, 0o644) + if err != nil { + klog.Errorf("mkdirall tokenDir failed %v", err) + return + } + _, err = rotateTokenFiles(tokenDir, secrets) + if err != nil { + return + } + dir = tokenDir + options = append(options, "passwd_file="+dir) + return + } + return +} diff --git a/pkg/mounter/proxy/server/ossfs/utils_test.go b/pkg/mounter/proxy/server/ossfs/utils_test.go new file mode 100644 index 000000000..d5a239976 --- /dev/null +++ b/pkg/mounter/proxy/server/ossfs/utils_test.go @@ -0,0 +1,125 @@ +package ossfs + +import ( + "os" + "path/filepath" + "testing" + + "github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pkg/mounter/oss" + "github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pkg/mounter/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/klog/v2" +) + +var ( + OssfsPasswdFile = utils.GetPasswdFileName("ossfs") +) + +func TestPrepareCredentialFiles(t *testing.T) { + tests := []struct { + name string + secrets map[string]string + wantFile bool + wantDir bool + wantOpts bool + wantErr bool + }{ + { + name: "EmptySecrets", + secrets: map[string]string{}, + }, + { + name: "FixedAKSKExists", + secrets: map[string]string{OssfsPasswdFile: "testPasswd"}, + wantFile: true, + wantDir: false, + wantOpts: true, + wantErr: false, + }, + { + name: "TokenSecretsExists", + secrets: map[string]string{ + oss.KeyAccessKeyId: "testAKID", + oss.KeyAccessKeySecret: "testAKSecret", + oss.KeyExpiration: "testExpiration", + oss.KeySecurityToken: "testSecurityToken", + }, + wantFile: false, + wantDir: true, + wantOpts: true, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hash := utils.ComputeMountPathHash("/mnt/target1") + file, dir, options, err := prepareCredentialFiles("/mnt/target1", tt.secrets) + assert.Equal(t, tt.wantErr, err != nil) + assert.Equal(t, tt.wantFile, file != "") + assert.Equal(t, tt.wantDir, dir != "") + assert.Equal(t, tt.wantOpts, len(options) != 0) + err = os.RemoveAll("/tmp/" + hash) + if err != nil { + klog.ErrorS(err, "Remove token directory", "dir", "/tmp/"+hash) + } + }) + } + +} + +func TestRotateTokenFiles(t *testing.T) { + mountPath := "/mnt/target2" + hash := utils.ComputeMountPathHash(mountPath) + hashDir := filepath.Join("/tmp", hash) + err := os.MkdirAll(hashDir, 0o644) + require.NoError(t, err) + + // case 1: initialize fiexd AKSK + secrets := map[string]string{OssfsPasswdFile: "testPasswd"} + rotated, err := rotateTokenFiles(hashDir, secrets) + assert.Error(t, err) + assert.Equal(t, false, rotated) + + // case 2: initialize token + secrets = map[string]string{ + oss.KeyAccessKeyId: "testAKID", + oss.KeyAccessKeySecret: "testAKSecret", + oss.KeyExpiration: "testExpiration", + oss.KeySecurityToken: "testSecurityToken", + } + rotated, err = rotateTokenFiles(hashDir, secrets) + assert.NoError(t, err) + assert.Equal(t, true, rotated) + ak, _ := os.ReadFile(filepath.Join(hashDir, oss.KeyAccessKeyId)) + assert.Equal(t, "testAKID", string(ak)) + sk, _ := os.ReadFile(filepath.Join(hashDir, oss.KeyAccessKeySecret)) + assert.Equal(t, "testAKSecret", string(sk)) + exp, _ := os.ReadFile(filepath.Join(hashDir, oss.KeyExpiration)) + assert.Equal(t, "testExpiration", string(exp)) + st, _ := os.ReadFile(filepath.Join(hashDir, oss.KeySecurityToken)) + assert.Equal(t, "testSecurityToken", string(st)) + + // case 3: rotate token + secrets = map[string]string{ + oss.KeyAccessKeyId: "newAKID", + oss.KeyAccessKeySecret: "newAKSecret", + oss.KeyExpiration: "newExpiration", + oss.KeySecurityToken: "newSecurityToken", + } + rotated, err = rotateTokenFiles(hashDir, secrets) + assert.NoError(t, err) + assert.Equal(t, true, rotated) + ak, _ = os.ReadFile(filepath.Join(hashDir, oss.KeyAccessKeyId)) + assert.Equal(t, "newAKID", string(ak)) + sk, _ = os.ReadFile(filepath.Join(hashDir, oss.KeyAccessKeySecret)) + assert.Equal(t, "newAKSecret", string(sk)) + exp, _ = os.ReadFile(filepath.Join(hashDir, oss.KeyExpiration)) + assert.Equal(t, "newExpiration", string(exp)) + st, _ = os.ReadFile(filepath.Join(hashDir, oss.KeySecurityToken)) + assert.Equal(t, "newSecurityToken", string(st)) + err = os.RemoveAll(hashDir) + if err != nil { + t.Errorf("removeall hashdir failed %v", err) + } +} diff --git a/pkg/mounter/proxy/server/ossfs2/driver.go b/pkg/mounter/proxy/server/ossfs2/driver.go index b70c3e647..d8252e706 100644 --- a/pkg/mounter/proxy/server/ossfs2/driver.go +++ b/pkg/mounter/proxy/server/ossfs2/driver.go @@ -11,6 +11,7 @@ import ( "syscall" "time" + "github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pkg/mounter/oss" "github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pkg/mounter/proxy" "github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pkg/mounter/proxy/server" "github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pkg/mounter/utils" @@ -48,18 +49,16 @@ func (h *Driver) Mount(ctx context.Context, req *proxy.MountRequest) error { options := req.Options // prepare passwd file - var passwdFile string - if passwd := req.Secrets[utils.GetPasswdFileName("ossfs2")]; passwd != "" { - tmpDir, err := os.MkdirTemp("", "ossfs2-") - if err != nil { - return err - } - passwdFile = filepath.Join(tmpDir, "passwd") - err = os.WriteFile(passwdFile, []byte(passwd), 0o600) - if err != nil { - return err - } - klog.V(4).InfoS("created ossfs2 configuration file", "path", passwdFile) + passwdFile, tokenDir, credOpts, err := prepareCredentialFiles(req.Target, req.Secrets) + if err != nil { + return fmt.Errorf("prepare credential files failed: %w", err) + } + options = append(options, credOpts...) + if passwdFile != "" { + klog.V(4).InfoS("created ossfs2 passwd file", "path", passwdFile) + } + if tokenDir != "" { + klog.V(4).InfoS("created ossfs2 token directory", "dir", tokenDir) } args := []string{"mount", req.Target} @@ -77,7 +76,7 @@ func (h *Driver) Mount(ctx context.Context, req *proxy.MountRequest) error { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - err := cmd.Start() + err = cmd.Start() if err != nil { return fmt.Errorf("start ossfs2 failed: %w", err) } @@ -100,9 +99,9 @@ func (h *Driver) Mount(ctx context.Context, req *proxy.MountRequest) error { klog.InfoS("ossfs2 exited", "mountpoint", target, "pid", pid) } ossfsExited <- err - if err := os.Remove(passwdFile); err != nil { - klog.ErrorS(err, "Remove configuration file", "mountpoint", target, "path", passwdFile) - } + // Note: No need to clean up credential files since after rotation support, + // files are stored in fixed paths and won't generate multiple copies that + // could lead to files leakage. close(ossfsExited) }() @@ -150,6 +149,28 @@ func (h *Driver) Mount(ctx context.Context, req *proxy.MountRequest) error { return err } +func (h *Driver) RotateToken(ctx context.Context, req *proxy.RotateTokenRequest) error { + // no need to rotate if there is no token in request + if req.Secrets == nil { + return nil + } + if token := req.Secrets[oss.KeySecurityToken]; token == "" { + return nil + } + + // prepare passwd file + hashDir := utils.GetPasswdHashDir(req.Target) + tokenDir := filepath.Join(hashDir, utils.GetPasswdFileName("ossfs2")) + rotated, err := rotateTokenFiles(tokenDir, req.Secrets) + if err != nil { + return fmt.Errorf("rotate token files failed: %w", err) + } + if rotated { + klog.V(4).InfoS("rotate ossfs2 token files") + } + return nil +} + func (h *Driver) Init() {} func (h *Driver) Terminate() { diff --git a/pkg/mounter/proxy/server/ossfs2/utils.go b/pkg/mounter/proxy/server/ossfs2/utils.go new file mode 100644 index 000000000..aee8dd4d1 --- /dev/null +++ b/pkg/mounter/proxy/server/ossfs2/utils.go @@ -0,0 +1,80 @@ +package ossfs2 + +import ( + "fmt" + "os" + "path/filepath" + + "k8s.io/klog/v2" + + "github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pkg/mounter/oss" + "github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pkg/mounter/utils" +) + +// rotateTokenFiles rotates (or initializes) token files +// This function assumes that token rotation is required when executed +func rotateTokenFiles(dir string, secrets map[string]string) (rotated bool, err error) { + var fileUpdate bool + tokenKey := []string{oss.KeyAccessKeyId, oss.KeyAccessKeySecret, oss.KeySecurityToken} + for _, key := range tokenKey { + val := secrets[key] + if val == "" { + err = fmt.Errorf("invalid authorization. %s is empty", key) + klog.Error(err) + return + } + fileUpdate, err = utils.WriteFileWithLock(filepath.Join(dir, key), []byte(val), 0o600) + if err != nil { + klog.Errorf("WriteFileWithLock %s failed %v", key, err) + return + } + rotated = rotated || fileUpdate + } + return +} + +// prepareCredentialFiles returns: +// 1. file: path of ossfs2 credential file for fixed AKSK +// 2. dir: dorectory of ossfs2 credential files for token +// 3. options: extra options +// 4. error +func prepareCredentialFiles(target string, secrets map[string]string) (file, dir string, options []string, err error) { + // fixed AKSK + hashDir := utils.GetPasswdHashDir(target) + + if passwd := secrets[utils.GetPasswdFileName("ossfs2")]; passwd != "" { + err = os.MkdirAll(hashDir, 0o644) + if err != nil { + klog.Errorf("mkdirall hashdir failed %v", err) + return + } + _, err = utils.WriteFileWithLock(filepath.Join(hashDir, utils.GetPasswdFileName("ossfs2")), []byte(passwd), 0o600) + if err != nil { + return + } + file = filepath.Join(hashDir, utils.GetPasswdFileName("ossfs2")) + return + } + + // token + if token := secrets[oss.KeySecurityToken]; token != "" { + tokenDir := filepath.Join(hashDir, utils.GetPasswdFileName("ossfs2")) + err = os.MkdirAll(tokenDir, 0o644) + if err != nil { + klog.Errorf("mkdirall tokenDir failed %v", err) + return + } + _, err = rotateTokenFiles(tokenDir, secrets) + if err != nil { + return + } + dir = tokenDir + options = append(options, + fmt.Sprintf("oss_sts_multi_conf_ak_file=%s", filepath.Join(dir, oss.KeyAccessKeyId)), + fmt.Sprintf("oss_sts_multi_conf_sk_file=%s", filepath.Join(dir, oss.KeyAccessKeySecret)), + fmt.Sprintf("oss_sts_multi_conf_token_file=%s", filepath.Join(dir, oss.KeySecurityToken)), + ) + return + } + return +} diff --git a/pkg/mounter/proxy/server/ossfs2/utils_test.go b/pkg/mounter/proxy/server/ossfs2/utils_test.go new file mode 100644 index 000000000..0cd18e74f --- /dev/null +++ b/pkg/mounter/proxy/server/ossfs2/utils_test.go @@ -0,0 +1,120 @@ +package ossfs2 + +import ( + "os" + "path/filepath" + "testing" + + "github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pkg/mounter/oss" + "github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pkg/mounter/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/klog/v2" +) + +var ( + OssfsPasswdFile = utils.GetPasswdFileName("ossfs2") +) + +func TestPrepareCredentialFiles(t *testing.T) { + tests := []struct { + name string + secrets map[string]string + wantFile bool + wantDir bool + wantOpts bool + wantErr bool + }{ + { + name: "EmptySecrets", + secrets: map[string]string{}, + }, + { + name: "FixedAKSKExists", + secrets: map[string]string{OssfsPasswdFile: "testPasswd"}, + wantFile: true, + wantDir: false, + wantOpts: false, + wantErr: false, + }, + { + name: "TokenSecretsExists", + secrets: map[string]string{ + oss.KeyAccessKeyId: "testAKID", + oss.KeyAccessKeySecret: "testAKSecret", + oss.KeySecurityToken: "testSecurityToken", + }, + wantFile: false, + wantDir: true, + wantOpts: true, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hash := utils.ComputeMountPathHash("/mnt/target2") + file, dir, options, err := prepareCredentialFiles("/mnt/target2", tt.secrets) + assert.Equal(t, tt.wantErr, err != nil) + assert.Equal(t, tt.wantFile, file != "") + assert.Equal(t, tt.wantDir, dir != "") + assert.Equal(t, tt.wantOpts, len(options) != 0) + err = os.RemoveAll("/tmp/" + hash) + if err != nil { + klog.ErrorS(err, "Remove token directory", "dir", "/tmp/"+hash) + } + }) + } + +} + +func TestRotateTokenFiles(t *testing.T) { + mountPath := "/mnt/target2" + hash := utils.ComputeMountPathHash(mountPath) + hashDir := filepath.Join("/tmp", hash) + err := os.MkdirAll(hashDir, 0o644) + require.NoError(t, err) + + // case 1: initialize fiexd AKSK + secrets := map[string]string{OssfsPasswdFile: "testPasswd"} + rotated, err := rotateTokenFiles(hashDir, secrets) + assert.Error(t, err) + assert.False(t, rotated) + + // case 2: initialize token + secrets = map[string]string{ + oss.KeyAccessKeyId: "testAKID", + oss.KeyAccessKeySecret: "testAKSecret", + oss.KeyExpiration: "testExpiration", + oss.KeySecurityToken: "testSecurityToken", + } + rotated, err = rotateTokenFiles(hashDir, secrets) + assert.NoError(t, err) + assert.True(t, rotated) + ak, _ := os.ReadFile(filepath.Join(hashDir, oss.KeyAccessKeyId)) + assert.Equal(t, "testAKID", string(ak)) + sk, _ := os.ReadFile(filepath.Join(hashDir, oss.KeyAccessKeySecret)) + assert.Equal(t, "testAKSecret", string(sk)) + st, _ := os.ReadFile(filepath.Join(hashDir, oss.KeySecurityToken)) + assert.Equal(t, "testSecurityToken", string(st)) + + // case 3: rotate token + secrets = map[string]string{ + oss.KeyAccessKeyId: "newAKID", + oss.KeyAccessKeySecret: "newAKSecret", + oss.KeyExpiration: "newExpiration", + oss.KeySecurityToken: "newSecurityToken", + } + rotated, err = rotateTokenFiles(hashDir, secrets) + assert.NoError(t, err) + assert.True(t, rotated) + ak, _ = os.ReadFile(filepath.Join(hashDir, oss.KeyAccessKeyId)) + assert.Equal(t, "newAKID", string(ak)) + sk, _ = os.ReadFile(filepath.Join(hashDir, oss.KeyAccessKeySecret)) + assert.Equal(t, "newAKSecret", string(sk)) + st, _ = os.ReadFile(filepath.Join(hashDir, oss.KeySecurityToken)) + assert.Equal(t, "newSecurityToken", string(st)) + err = os.RemoveAll(hashDir) + if err != nil { + t.Errorf("removeall hashdir failed %v", err) + } +} diff --git a/pkg/mounter/proxy_mounter.go b/pkg/mounter/proxy_mounter.go index ed52f7a1c..97734680a 100644 --- a/pkg/mounter/proxy_mounter.go +++ b/pkg/mounter/proxy_mounter.go @@ -21,6 +21,10 @@ func NewProxyMounter(socketPath string, inner mountutils.Interface) Mounter { } } +func (m *ProxyMounter) Name() string { + return "proxy-mounter" +} + func (m *ProxyMounter) MountWithSecrets(source, target, fstype string, options []string, secrets map[string]string) error { dclient := client.NewClient(m.socketPath) resp, err := dclient.Mount(&proxy.MountRequest{ @@ -50,3 +54,20 @@ func (m *ProxyMounter) MountWithSecrets(source, target, fstype string, options [ func (m *ProxyMounter) Mount(source string, target string, fstype string, options []string) error { return m.MountWithSecrets(source, target, fstype, options, nil) } + +func (m *ProxyMounter) RotateToken(target, fstype string, secrets map[string]string) error { + dclient := client.NewClient(m.socketPath) + resp, err := dclient.RotateToken(&proxy.RotateTokenRequest{ + Target: target, + Fstype: fstype, + Secrets: secrets, + }) + if err != nil { + return fmt.Errorf("call mounter daemon: %w", err) + } + err = resp.ToError() + if err != nil { + return fmt.Errorf("failed to rotate token: %w", err) + } + return nil +} diff --git a/pkg/mounter/utils/filelock.go b/pkg/mounter/utils/filelock.go new file mode 100644 index 000000000..176724f33 --- /dev/null +++ b/pkg/mounter/utils/filelock.go @@ -0,0 +1,51 @@ +package utils + +import ( + "os" + "sync" +) + +var ( + // Store the read-write lock corresponding to each file path + fileLocks = sync.Map{} +) + +func getFileLock(path string) *sync.RWMutex { + lock, _ := fileLocks.LoadOrStore(path, &sync.RWMutex{}) + return lock.(*sync.RWMutex) +} + +// WriteFileWithLock safely writes data to file with locking +func WriteFileWithLock(path string, data []byte, perm os.FileMode) (done bool, err error) { + lock := getFileLock(path) + + // First try to acquire read lock to check if content is consistent + lock.RLock() + if existingData, err := os.ReadFile(path); err == nil { + // If file exists and content is the same, return directly to avoid redundant write + if string(existingData) == string(data) { + lock.RUnlock() + return false, nil + } + } + lock.RUnlock() + + // Content is different or file does not exist, need to write new content + // Acquire write lock + lock.Lock() + defer lock.Unlock() + + // Check content again (double-checked locking pattern) + if existingData, err := os.ReadFile(path); err == nil { + if string(existingData) == string(data) { + return false, nil + } + } + + // Perform write operation + err = os.WriteFile(path, data, perm) + if err == nil { + done = true + } + return +} diff --git a/pkg/mounter/utils/fuse_pod_manager.go b/pkg/mounter/utils/fuse_pod_manager.go index f063b81f7..a5afd1809 100644 --- a/pkg/mounter/utils/fuse_pod_manager.go +++ b/pkg/mounter/utils/fuse_pod_manager.go @@ -44,7 +44,7 @@ type AuthConfig struct { RrsaConfig *RrsaConfig // for csi-secret-store SecretProviderClassName string - // for AK/SK + // for AK/SK with or without token Secrets map[string]string // for Token from Secret SecretRef string @@ -195,7 +195,7 @@ func (fpm *FusePodManager) labelsAndListOptionsFor(c *FusePodContext, target str labels[FuseTypeLabelKey] = c.FuseType } if target != "" { - labels[FuseMountPathHashLabelKey] = computeMountPathHash(target) + labels[FuseMountPathHashLabelKey] = ComputeMountPathHash(target) } listOptions := metav1.ListOptions{ FieldSelector: fields.OneTermEqualSelector("spec.nodeName", c.NodeName).String(), diff --git a/pkg/mounter/utils/helper.go b/pkg/mounter/utils/helper.go index 041eb8e98..371487ef0 100644 --- a/pkg/mounter/utils/helper.go +++ b/pkg/mounter/utils/helper.go @@ -27,7 +27,7 @@ const ( OssFs2Type = "ossfs2" ) -func computeMountPathHash(target string) string { +func ComputeMountPathHash(target string) string { hasher := fnv.New32a() hasher.Write([]byte(target)) return rand.SafeEncodeString(fmt.Sprint(hasher.Sum32())) @@ -190,7 +190,7 @@ func CleanupCredentialSecret(ctx context.Context, clientset kubernetes.Interface const MaxRoleSessionNameLimit = 64 func GetRoleSessionName(volumeId, target, fuseType string) string { - name := fmt.Sprintf("%s.%s.%s", fuseType, volumeId, computeMountPathHash(target)) + name := fmt.Sprintf("%s.%s.%s", fuseType, volumeId, ComputeMountPathHash(target)) if len(name) > MaxRoleSessionNameLimit { name = name[:MaxRoleSessionNameLimit] } @@ -224,3 +224,7 @@ func AppendRRSAAuthOptions(m metadata.MetadataProvider, options []string, volume } return options, nil } + +func GetPasswdHashDir(target string) string { + return filepath.Join("/tmp", ComputeMountPathHash(target)) +} diff --git a/pkg/mounter/utils/helper_test.go b/pkg/mounter/utils/helper_test.go index 4b92e24e1..75ef20082 100644 --- a/pkg/mounter/utils/helper_test.go +++ b/pkg/mounter/utils/helper_test.go @@ -13,7 +13,7 @@ func Test_GetRoleSessionName(t *testing.T) { target string wantName string }{ - {"vol1", "/mnt/target1", "ossfs.vol1." + computeMountPathHash("/mnt/target1")}, + {"vol1", "/mnt/target1", "ossfs.vol1." + ComputeMountPathHash("/mnt/target1")}, {"hereisalonglongpvnamethatisalreadylongerthan64ibeleive", "/mnt/target2", "ossfs.hereisalonglongpvnamethatisalreadylongerthan64ibeleive.c85"}, } diff --git a/pkg/mounter/utils/utils.go b/pkg/mounter/utils/utils.go index 5b0331ad6..59b780615 100644 --- a/pkg/mounter/utils/utils.go +++ b/pkg/mounter/utils/utils.go @@ -3,12 +3,9 @@ package utils import ( "errors" "fmt" - "os" - "path/filepath" "time" "golang.org/x/sys/unix" - "k8s.io/klog/v2" ) func WaitFdReadable(fd int, timeout time.Duration) error { @@ -29,21 +26,3 @@ func WaitFdReadable(fd int, timeout time.Duration) error { } return nil } - -func SaveOssSecretsToFile(secrets map[string]string) (filePath string, err error) { - passwd := secrets["passwd-ossfs"] - if passwd == "" { - return - } - - tmpDir, err := os.MkdirTemp("", "ossfs-") - if err != nil { - return "", err - } - filePath = filepath.Join(tmpDir, "passwd") - if err = os.WriteFile(filePath, []byte(passwd), 0o600); err != nil { - return "", err - } - klog.V(4).InfoS("created ossfs passwd file", "path", filePath) - return -} diff --git a/pkg/oss/nodeserver.go b/pkg/oss/nodeserver.go index 7c0c0396d..77cfdfeda 100644 --- a/pkg/oss/nodeserver.go +++ b/pkg/oss/nodeserver.go @@ -49,10 +49,6 @@ type nodeServer struct { } const ( - // AkID is Ak ID - AkID = "akId" - // AkSecret is Ak Secret - AkSecret = "akSecret" // OssFsType is the oss filesystem type OssFsType = "ossfs" // OssFs2Type is the ossfs2 filesystem type @@ -64,6 +60,20 @@ const ( ossfsExecPath = "/usr/local/bin/ossfs" ) +// fixed accesskeys +const ( + AkID = "akId" + AkSecret = "akSecret" +) + +// token accesskeys +const ( + KeyAccessKeyId = "AccessKeyId" + KeyAccessKeySecret = "AccessKeySecret" + KeyExpiration = "Expiration" + KeySecurityToken = "SecurityToken" +) + // for cases where fuseType does not affect like UnPublishVolume, // use unifiedFsType instead var unifiedFsType = OssFsType @@ -99,15 +109,7 @@ func (ns *nodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublis if err := validateNodePublishVolumeRequest(req); err != nil { return nil, err } - // check if already mounted - notMnt, err := isNotMountPoint(ns.rawMounter, targetPath) - if err != nil { - return nil, err - } - if !notMnt { - klog.Infof("NodePublishVolume: %s already mounted", targetPath) - return &csi.NodePublishVolumeResponse{}, nil - } + attachPath := mounterutils.GetAttachPath(req.VolumeId) // parse options // ensure fuseType is not empty @@ -168,6 +170,32 @@ func (ns *nodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublis ossfsMounter = mounter.NewProxyMounter(socketPath, ns.rawMounter) } + // check if already mounted for re-publish + notMnt, err := isNotMountPoint(ns.rawMounter, targetPath) + if err != nil { + return nil, err + } + if !notMnt { + klog.Infof("NodePublishVolume: %s already mounted", targetPath) + if !features.FunctionalMutableFeatureGate.Enabled(features.RundCSIProtocol3) && needRotateToken(opts, authCfg.Secrets) { + // mountPath is the target path for mounter + mountPath := attachPath + if ns.skipAttach { + mountPath = targetPath + } + err := ossfsMounter.RotateToken(mountPath, opts.FuseType, authCfg.Secrets) + if err != nil { + // if is mounter not supported, return unimplentederror to avoid retry + if mounter.IsNotImplementedErr(err) { + return nil, status.Error(codes.Unimplemented, err.Error()) + } + return nil, status.Error(codes.Internal, err.Error()) + } + klog.Infof("NodePublishVolume: %s already rotated token", targetPath) + } + return &csi.NodePublishVolumeResponse{}, nil + } + // When work as csi-agent, directly mount on the target path. if ns.skipAttach { if opts.FuseType == OssFsType { @@ -183,7 +211,6 @@ func (ns *nodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublis // When work as csi nodeserver, mount on the attach path under /run/fuse.ossfs and then perform the bind mount. // check whether the attach path is mounted - attachPath := mounterutils.GetAttachPath(req.VolumeId) notMnt, err = isNotMountPoint(ns.rawMounter, attachPath) if err != nil { return nil, err diff --git a/pkg/oss/utils.go b/pkg/oss/utils.go index 7db4db04a..8d6d3d8f7 100644 --- a/pkg/oss/utils.go +++ b/pkg/oss/utils.go @@ -70,9 +70,17 @@ func parseOptions(volOptions, secrets map[string]string, volCaps []*csi.VolumeCa opts := &oss.Options{ UseSharedPath: true, Path: "/", - AkID: strings.TrimSpace(secrets[AkID]), - AkSecret: strings.TrimSpace(secrets[AkSecret]), - MetricsTop: defaultMetricsTop, + AccessKey: oss.AccessKey{ + AkID: strings.TrimSpace(secrets[AkID]), + AkSecret: strings.TrimSpace(secrets[AkSecret]), + }, + TokenSecret: oss.TokenSecret{ + AccessKeyId: strings.TrimSpace(secrets[KeyAccessKeyId]), + AccessKeySecret: strings.TrimSpace(secrets[KeyAccessKeySecret]), + Expiration: strings.TrimSpace(secrets[KeyExpiration]), + SecurityToken: strings.TrimSpace(secrets[KeySecurityToken]), + }, + MetricsTop: defaultMetricsTop, } var volumeAsSubpath bool @@ -173,7 +181,7 @@ func parseOptions(volOptions, secrets map[string]string, volCaps []*csi.VolumeCa } url := opts.URL - region := metadata.MustGet(m, metadata.RegionID) + region, _ := m.Get(metadata.RegionID) if region != "" && utils.GetNetworkType() == "vpc" { url, _ = setNetworkType(url, region) } @@ -195,7 +203,7 @@ func parseOptions(volOptions, secrets map[string]string, volCaps []*csi.VolumeCa switch opts.AuthType { case "": // try to get ak/sk from env - if opts.SecretRef == "" && (opts.AkID == "" || opts.AkSecret == "") { + if opts.SecretRef == "" && opts.SecurityToken == "" && (opts.AkID == "" || opts.AkSecret == "") { ac := utils.GetEnvAK() opts.AkID = ac.AccessKeyID opts.AkSecret = ac.AccessKeySecret @@ -429,3 +437,15 @@ func makeMountOptions(opt *oss.Options, fpm *oss.OSSFusePodManager, m metadata.M mountOptions = append(mountOptions, ops...) return } + +func needRotateToken(opt *oss.Options, secrets map[string]string) (needRotate bool) { + if len(secrets) == 0 { + return false + } + // TODO: Remove this check if when ossfs support rotate fixed AKSK. + ak := secrets[mounter.GetPasswdFileName(opt.FuseType)] + if ak == "" { + needRotate = true + } + return +} diff --git a/pkg/oss/utils_test.go b/pkg/oss/utils_test.go index 9afe482b6..baef9e6b8 100644 --- a/pkg/oss/utils_test.go +++ b/pkg/oss/utils_test.go @@ -60,10 +60,12 @@ func Test_parseOptions(t *testing.T) { }, } expectedOptions = &oss.Options{ - Bucket: "test-bucket", - URL: "https://oss-cn-hangzhou.aliyuncs.com", - AkID: "test-akid", - AkSecret: "test-aksecret", + Bucket: "test-bucket", + URL: "https://oss-cn-hangzhou.aliyuncs.com", + AccessKey: oss.AccessKey{ + AkID: "test-akid", + AkSecret: "test-aksecret", + }, FuseType: "ossfs", Path: "/volume-id", UseSharedPath: true, @@ -94,11 +96,13 @@ func Test_parseOptions(t *testing.T) { }, } expectedOptions = &oss.Options{ - Bucket: "test-bucket", - URL: "http://oss-cn-beijing-internal.aliyuncs.com", - OtherOpts: "-o max_stat_cache_size=0 -o allow_other", - AkID: "test-akid", - AkSecret: "test-aksecret", + Bucket: "test-bucket", + URL: "http://oss-cn-beijing-internal.aliyuncs.com", + OtherOpts: "-o max_stat_cache_size=0 -o allow_other", + AccessKey: oss.AccessKey{ + AkID: "test-akid", + AkSecret: "test-aksecret", + }, UseSharedPath: true, FuseType: "ossfs", MetricsTop: defaultMetricsTop, @@ -148,6 +152,54 @@ func Test_parseOptions(t *testing.T) { testNPReq.Readonly, "", true, m) assert.Equal(t, expectedOptions, gotOptions) + // test token republish + testNPReq = csi.NodePublishVolumeRequest{ + VolumeContext: map[string]string{ + "bucket": "test-bucket", + "url": "oss-cn-beijing.aliyuncs.com", + "otheropts": "-o max_stat_cache_size=0 -o allow_other", + "UseSharedPath": "false", + }, + Secrets: map[string]string{ + "AccessKeyId": "test-akid", + "AccessKeySecret": "test-aksecret", + "SecurityToken": "test-token", + "Expiration": "2023-01-01T00:00:00Z", + }, + VolumeCapability: &csi.VolumeCapability{ + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER, + }, + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "ossfs", + MountFlags: []string{"-o max_stat_cache_size=0 -o allow_other"}, + }, + }, + }, + Readonly: true, + } + expectedOptions = &oss.Options{ + Bucket: "test-bucket", + URL: "http://oss-cn-beijing-internal.aliyuncs.com", + OtherOpts: "-o max_stat_cache_size=0 -o allow_other", + FuseType: "ossfs", + MetricsTop: defaultMetricsTop, + Path: "/", + ReadOnly: true, + UseSharedPath: false, + TokenSecret: oss.TokenSecret{ + AccessKeyId: "test-akid", + AccessKeySecret: "test-aksecret", + SecurityToken: "test-token", + Expiration: "2023-01-01T00:00:00Z", + }, + } + gotOptions = parseOptions(testNPReq.GetVolumeContext(), + testNPReq.GetSecrets(), []*csi.VolumeCapability{testNPReq.GetVolumeCapability()}, + testNPReq.Readonly, "", true, m) + assert.Equal(t, expectedOptions, gotOptions) + // test authtype options := map[string]string{ "url": "oss-cn-beijing.aliyuncs.com", @@ -156,8 +208,10 @@ func Test_parseOptions(t *testing.T) { t.Setenv("ACCESS_KEY_SECRET", "test-aksecret") gotOptions = parseOptions(options, nil, nil, true, "", true, m) expectedOptions = &oss.Options{ - AkID: "test-akid", - AkSecret: "test-aksecret", + AccessKey: oss.AccessKey{ + AkID: "test-akid", + AkSecret: "test-aksecret", + }, FuseType: "ossfs", Path: "/", UseSharedPath: true, @@ -533,11 +587,13 @@ func Test_checkOssOptions(t *testing.T) { { name: "empty fuse type", opts: &oss.Options{ - URL: "1.1.1.1", - Bucket: "aliyun", - Path: "/path", - AkID: "11111", - AkSecret: "22222", + URL: "1.1.1.1", + Bucket: "aliyun", + Path: "/path", + AccessKey: oss.AccessKey{ + AkID: "11111", + AkSecret: "22222", + }, }, errType: ParamError, }, @@ -554,11 +610,13 @@ func Test_checkOssOptions(t *testing.T) { { name: "invalid path", opts: &oss.Options{ - URL: "1.1.1.1", - Bucket: "aliyun", - Path: "abc/", - AkID: "11111", - AkSecret: "22222", + URL: "1.1.1.1", + Bucket: "aliyun", + Path: "abc/", + AccessKey: oss.AccessKey{ + AkID: "11111", + AkSecret: "22222", + }, FuseType: OssFsType, }, errType: PathError, @@ -566,10 +624,12 @@ func Test_checkOssOptions(t *testing.T) { { name: "empty URL", opts: &oss.Options{ - Bucket: "aliyun", - Path: "/path", - AkID: "11111", - AkSecret: "22222", + Bucket: "aliyun", + Path: "/path", + AccessKey: oss.AccessKey{ + AkID: "11111", + AkSecret: "22222", + }, FuseType: OssFsType, }, errType: ParamError, @@ -577,11 +637,13 @@ func Test_checkOssOptions(t *testing.T) { { name: "invalid encrypted type", opts: &oss.Options{ - URL: "1.1.1.1", - Bucket: "aliyun", - Path: "/path", - AkID: "11111", - AkSecret: "22222", + URL: "1.1.1.1", + Bucket: "aliyun", + Path: "/path", + AccessKey: oss.AccessKey{ + AkID: "11111", + AkSecret: "22222", + }, Encrypted: "invalid", FuseType: OssFsType, }, @@ -590,11 +652,13 @@ func Test_checkOssOptions(t *testing.T) { { name: "valid kms sse", opts: &oss.Options{ - URL: "1.1.1.1", - Bucket: "aliyun", - Path: "/path", - AkID: "11111", - AkSecret: "22222", + URL: "1.1.1.1", + Bucket: "aliyun", + Path: "/path", + AccessKey: oss.AccessKey{ + AkID: "11111", + AkSecret: "22222", + }, Encrypted: oss.EncryptedTypeKms, FuseType: OssFsType, }, @@ -603,11 +667,13 @@ func Test_checkOssOptions(t *testing.T) { { name: "invalid url", opts: &oss.Options{ - URL: "aliyun.oss-cn-hangzhou.aliyuncs.com", - Bucket: "aliyun", - Path: "/path", - AkID: "11111", - AkSecret: "22222", + URL: "aliyun.oss-cn-hangzhou.aliyuncs.com", + Bucket: "aliyun", + Path: "/path", + AccessKey: oss.AccessKey{ + AkID: "11111", + AkSecret: "22222", + }, FuseType: OssFsType, }, errType: UrlError, @@ -637,11 +703,13 @@ func TestMakeAuthConfig(t *testing.T) { ossfs := oss.NewFuseOssfs(nil, fakeMeta) ossfsFpm := oss.NewOSSFusePodManager(ossfs, nil) opt := &oss.Options{ - URL: "1.1.1.1", - Bucket: "aliyun", - Path: "/path", - AkID: "11111", - AkSecret: "22222", + URL: "1.1.1.1", + Bucket: "aliyun", + Path: "/path", + AccessKey: oss.AccessKey{ + AkID: "11111", + AkSecret: "22222", + }, FuseType: OssFsType, } want := &mounterutils.AuthConfig{ @@ -656,11 +724,13 @@ func TestMakeAuthConfig(t *testing.T) { ossfs2 := oss.NewFuseOssfs2(nil, fakeMeta) ossfs2Fpm := oss.NewOSSFusePodManager(ossfs2, nil) opt2 := &oss.Options{ - URL: "1.1.1.1", - Bucket: "aliyun", - Path: "/path", - AkID: "11111", - AkSecret: "22222", + URL: "1.1.1.1", + Bucket: "aliyun", + Path: "/path", + AccessKey: oss.AccessKey{ + AkID: "11111", + AkSecret: "22222", + }, FuseType: OssFs2Type, } want2 := &mounterutils.AuthConfig{ @@ -679,11 +749,13 @@ func TestMakeMountOptions(t *testing.T) { ossfs := oss.NewFuseOssfs(nil, fakeMeta) ossfsFpm := oss.NewOSSFusePodManager(ossfs, nil) opt := &oss.Options{ - URL: "1.1.1.1", - Bucket: "aliyun", - Path: "/path", - AkID: "11111", - AkSecret: "22222", + URL: "1.1.1.1", + Bucket: "aliyun", + Path: "/path", + AccessKey: oss.AccessKey{ + AkID: "11111", + AkSecret: "22222", + }, FuseType: OssFsType, OtherOpts: "-o allow_other -o max_stat_cache_size=0", SigVersion: "v4", @@ -713,11 +785,13 @@ func TestMakeMountOptions(t *testing.T) { ossfs2 := oss.NewFuseOssfs2(nil, fakeMeta) ossfs2Fpm := oss.NewOSSFusePodManager(ossfs2, nil) opt2 := &oss.Options{ - URL: "1.1.1.1", - Bucket: "aliyun", - Path: "/path", - AkID: "11111", - AkSecret: "22222", + URL: "1.1.1.1", + Bucket: "aliyun", + Path: "/path", + AccessKey: oss.AccessKey{ + AkID: "11111", + AkSecret: "22222", + }, FuseType: OssFs2Type, OtherOpts: "-o attr_timeout=60", SigVersion: "v4", @@ -733,3 +807,57 @@ func TestMakeMountOptions(t *testing.T) { assert.NoError(t, err) assert.Equal(t, want2, got2) } + +func TestNeedRotateToken(t *testing.T) { + tests := []struct { + name string + opt *oss.Options + secrets map[string]string + wantSecrets map[string]string + want bool + }{ + { + name: "FuseType not OssFsType", + opt: &oss.Options{FuseType: "OtherType"}, + secrets: map[string]string{mounterutils.GetPasswdFileName("OtherType"): "value1", "key2": "value2"}, + wantSecrets: map[string]string{mounterutils.GetPasswdFileName("OtherType"): "value1", "key2": "value2"}, + want: false, + }, + { + name: "FuseType not OssFsType but not set fixed AKSK", + opt: &oss.Options{FuseType: "OtherType"}, + secrets: map[string]string{"key1": "value1"}, + wantSecrets: map[string]string{"key1": "value1"}, + want: true, + }, + { + name: "secrets is nil", + opt: &oss.Options{FuseType: OssFsType}, + secrets: nil, + wantSecrets: nil, + want: false, + }, + { + name: "secrets includes more info", + opt: &oss.Options{FuseType: OssFsType}, + secrets: map[string]string{mounterutils.GetPasswdFileName(OssFsType): "value1", "key2": "value2"}, + wantSecrets: map[string]string{mounterutils.GetPasswdFileName(OssFsType): "value1", "key2": "value2"}, + want: false, + }, + { + name: "secrets only has passwd file info", + opt: &oss.Options{FuseType: OssFsType}, + secrets: map[string]string{mounterutils.GetPasswdFileName(OssFsType): "value1"}, + wantSecrets: map[string]string{mounterutils.GetPasswdFileName(OssFsType): "value1"}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + need := needRotateToken(tt.opt, tt.secrets) + assert.Equal(t, tt.want, need) + assert.Equal(t, tt.wantSecrets, tt.secrets) + }) + } +}