diff --git a/api/v1alpha1/cacheruntimeclass_types.go b/api/v1alpha1/cacheruntimeclass_types.go index 8e8ab3ffb76..c2e2b225cf8 100644 --- a/api/v1alpha1/cacheruntimeclass_types.go +++ b/api/v1alpha1/cacheruntimeclass_types.go @@ -81,10 +81,6 @@ type ExecutionCommonEntry struct { TimeoutSeconds int32 `json:"timeout,omitempty"` } -// EncryptOptionComponentDependency defines the configuration for encrypt option dependency -type EncryptOptionComponentDependency struct { -} - // ExtraResourcesComponentDependency defines the extra resources configuration for component dependencies type ExtraResourcesComponentDependency struct { // ConfigMaps is a list of ConfigMaps in the same namespace to mount into the component @@ -94,15 +90,23 @@ type ExtraResourcesComponentDependency struct { // RuntimeComponentDependencies defines the dependencies required by a CacheRuntime component type RuntimeComponentDependencies struct { - // EncryptOption is the configuration for encrypt option secret mount + // SecretMount controls whether dataset encrypt-option secrets are mounted into this component pod. + // Defaults to true for Master/Worker, false for Client unless explicitly enabled. // +optional - EncryptOption *EncryptOptionComponentDependency `json:"encryptOption,omitempty"` + SecretMount *SecretMountComponentDependency `json:"secretMount,omitempty"` // ExtraResources specifies the usage of extra resources such as ConfigMaps // +optional ExtraResources *ExtraResourcesComponentDependency `json:"extraResources,omitempty"` } +// SecretMountComponentDependency defines the secret mount configuration for component dependencies +type SecretMountComponentDependency struct { + // Enabled indicates whether dataset encrypt-option secrets should be mounted into this component pod. + // +optional + Enabled bool `json:"enabled,omitempty"` +} + // HeadlessRuntimeComponentService defines the configuration for headless service type HeadlessRuntimeComponentService struct { } diff --git a/api/v1alpha1/openapi_generated.go b/api/v1alpha1/openapi_generated.go index 7c4fd6dc490..f7138b9488d 100644 --- a/api/v1alpha1/openapi_generated.go +++ b/api/v1alpha1/openapi_generated.go @@ -79,7 +79,6 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/fluid-cloudnative/fluid/api/v1alpha1.EFCRuntimeList": schema_fluid_cloudnative_fluid_api_v1alpha1_EFCRuntimeList(ref), "github.com/fluid-cloudnative/fluid/api/v1alpha1.EFCRuntimeSpec": schema_fluid_cloudnative_fluid_api_v1alpha1_EFCRuntimeSpec(ref), "github.com/fluid-cloudnative/fluid/api/v1alpha1.EncryptOption": schema_fluid_cloudnative_fluid_api_v1alpha1_EncryptOption(ref), - "github.com/fluid-cloudnative/fluid/api/v1alpha1.EncryptOptionComponentDependency": schema_fluid_cloudnative_fluid_api_v1alpha1_EncryptOptionComponentDependency(ref), "github.com/fluid-cloudnative/fluid/api/v1alpha1.EncryptOptionSource": schema_fluid_cloudnative_fluid_api_v1alpha1_EncryptOptionSource(ref), "github.com/fluid-cloudnative/fluid/api/v1alpha1.ExecutionCommonEntry": schema_fluid_cloudnative_fluid_api_v1alpha1_ExecutionCommonEntry(ref), "github.com/fluid-cloudnative/fluid/api/v1alpha1.ExecutionEntries": schema_fluid_cloudnative_fluid_api_v1alpha1_ExecutionEntries(ref), @@ -138,6 +137,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/fluid-cloudnative/fluid/api/v1alpha1.RuntimeTopology": schema_fluid_cloudnative_fluid_api_v1alpha1_RuntimeTopology(ref), "github.com/fluid-cloudnative/fluid/api/v1alpha1.ScriptProcessor": schema_fluid_cloudnative_fluid_api_v1alpha1_ScriptProcessor(ref), "github.com/fluid-cloudnative/fluid/api/v1alpha1.SecretKeySelector": schema_fluid_cloudnative_fluid_api_v1alpha1_SecretKeySelector(ref), + "github.com/fluid-cloudnative/fluid/api/v1alpha1.SecretMountComponentDependency": schema_fluid_cloudnative_fluid_api_v1alpha1_SecretMountComponentDependency(ref), "github.com/fluid-cloudnative/fluid/api/v1alpha1.TargetDataset": schema_fluid_cloudnative_fluid_api_v1alpha1_TargetDataset(ref), "github.com/fluid-cloudnative/fluid/api/v1alpha1.TargetDatasetWithMountPath": schema_fluid_cloudnative_fluid_api_v1alpha1_TargetDatasetWithMountPath(ref), "github.com/fluid-cloudnative/fluid/api/v1alpha1.TargetPath": schema_fluid_cloudnative_fluid_api_v1alpha1_TargetPath(ref), @@ -3790,17 +3790,6 @@ func schema_fluid_cloudnative_fluid_api_v1alpha1_EncryptOption(ref common.Refere } } -func schema_fluid_cloudnative_fluid_api_v1alpha1_EncryptOptionComponentDependency(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "EncryptOptionComponentDependency defines the configuration for encrypt option dependency", - Type: []string{"object"}, - }, - }, - } -} - func schema_fluid_cloudnative_fluid_api_v1alpha1_EncryptOptionSource(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -6823,10 +6812,10 @@ func schema_fluid_cloudnative_fluid_api_v1alpha1_RuntimeComponentDependencies(re Description: "RuntimeComponentDependencies defines the dependencies required by a CacheRuntime component", Type: []string{"object"}, Properties: map[string]spec.Schema{ - "encryptOption": { + "secretMount": { SchemaProps: spec.SchemaProps{ - Description: "EncryptOption is the configuration for encrypt option secret mount", - Ref: ref("github.com/fluid-cloudnative/fluid/api/v1alpha1.EncryptOptionComponentDependency"), + Description: "SecretMount controls whether dataset encrypt-option secrets are mounted into this component pod. Defaults to true for Master/Worker, false for Client unless explicitly enabled.", + Ref: ref("github.com/fluid-cloudnative/fluid/api/v1alpha1.SecretMountComponentDependency"), }, }, "extraResources": { @@ -6839,7 +6828,7 @@ func schema_fluid_cloudnative_fluid_api_v1alpha1_RuntimeComponentDependencies(re }, }, Dependencies: []string{ - "github.com/fluid-cloudnative/fluid/api/v1alpha1.EncryptOptionComponentDependency", "github.com/fluid-cloudnative/fluid/api/v1alpha1.ExtraResourcesComponentDependency"}, + "github.com/fluid-cloudnative/fluid/api/v1alpha1.ExtraResourcesComponentDependency", "github.com/fluid-cloudnative/fluid/api/v1alpha1.SecretMountComponentDependency"}, } } @@ -7607,6 +7596,26 @@ func schema_fluid_cloudnative_fluid_api_v1alpha1_SecretKeySelector(ref common.Re } } +func schema_fluid_cloudnative_fluid_api_v1alpha1_SecretMountComponentDependency(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "SecretMountComponentDependency defines the secret mount configuration for component dependencies", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "enabled": { + SchemaProps: spec.SchemaProps{ + Description: "Enabled indicates whether dataset encrypt-option secrets should be mounted into this component pod.", + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + }, + }, + } +} + func schema_fluid_cloudnative_fluid_api_v1alpha1_TargetDataset(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index bd206fcc086..e6f7214ad7b 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -346,6 +346,13 @@ func (in *CacheRuntimeClass) DeepCopyInto(out *CacheRuntimeClass) { (*in).DeepCopyInto(*out) } in.ExtraResources.DeepCopyInto(&out.ExtraResources) + if in.DataOperationSpecs != nil { + in, out := &in.DataOperationSpecs, &out.DataOperationSpecs + *out = make([]DataOperationSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CacheRuntimeClass. @@ -1031,6 +1038,31 @@ func (in *DataMigrateSpec) DeepCopy() *DataMigrateSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DataOperationSpec) DeepCopyInto(out *DataOperationSpec) { + *out = *in + if in.Command != nil { + in, out := &in.Command, &out.Command + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Args != nil { + in, out := &in.Args, &out.Args + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataOperationSpec. +func (in *DataOperationSpec) DeepCopy() *DataOperationSpec { + if in == nil { + return nil + } + out := new(DataOperationSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DataProcess) DeepCopyInto(out *DataProcess) { *out = *in @@ -1541,21 +1573,6 @@ func (in *EncryptOption) DeepCopy() *EncryptOption { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *EncryptOptionComponentDependency) DeepCopyInto(out *EncryptOptionComponentDependency) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EncryptOptionComponentDependency. -func (in *EncryptOptionComponentDependency) DeepCopy() *EncryptOptionComponentDependency { - if in == nil { - return nil - } - out := new(EncryptOptionComponentDependency) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EncryptOptionSource) DeepCopyInto(out *EncryptOptionSource) { *out = *in @@ -2888,9 +2905,9 @@ func (in *RuntimeComponentDefinition) DeepCopy() *RuntimeComponentDefinition { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RuntimeComponentDependencies) DeepCopyInto(out *RuntimeComponentDependencies) { *out = *in - if in.EncryptOption != nil { - in, out := &in.EncryptOption, &out.EncryptOption - *out = new(EncryptOptionComponentDependency) + if in.SecretMount != nil { + in, out := &in.SecretMount, &out.SecretMount + *out = new(SecretMountComponentDependency) **out = **in } if in.ExtraResources != nil { @@ -3203,6 +3220,21 @@ func (in *SecretKeySelector) DeepCopy() *SecretKeySelector { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretMountComponentDependency) DeepCopyInto(out *SecretMountComponentDependency) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretMountComponentDependency. +func (in *SecretMountComponentDependency) DeepCopy() *SecretMountComponentDependency { + if in == nil { + return nil + } + out := new(SecretMountComponentDependency) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TargetDataset) DeepCopyInto(out *TargetDataset) { *out = *in diff --git a/charts/fluid/fluid/crds/data.fluid.io_cacheruntimeclasses.yaml b/charts/fluid/fluid/crds/data.fluid.io_cacheruntimeclasses.yaml index 177eb366e32..4af3480c5e0 100644 --- a/charts/fluid/fluid/crds/data.fluid.io_cacheruntimeclasses.yaml +++ b/charts/fluid/fluid/crds/data.fluid.io_cacheruntimeclasses.yaml @@ -72,8 +72,6 @@ spec: properties: dependencies: properties: - encryptOption: - type: object extraResources: properties: configMaps: @@ -86,6 +84,11 @@ spec: type: object type: array type: object + secretMount: + properties: + enabled: + type: boolean + type: object type: object executionEntries: properties: @@ -3489,8 +3492,6 @@ spec: properties: dependencies: properties: - encryptOption: - type: object extraResources: properties: configMaps: @@ -3503,6 +3504,11 @@ spec: type: object type: array type: object + secretMount: + properties: + enabled: + type: boolean + type: object type: object executionEntries: properties: @@ -6906,8 +6912,6 @@ spec: properties: dependencies: properties: - encryptOption: - type: object extraResources: properties: configMaps: @@ -6920,6 +6924,11 @@ spec: type: object type: array type: object + secretMount: + properties: + enabled: + type: boolean + type: object type: object executionEntries: properties: diff --git a/config/crd/bases/data.fluid.io_cacheruntimeclasses.yaml b/config/crd/bases/data.fluid.io_cacheruntimeclasses.yaml index 177eb366e32..4af3480c5e0 100644 --- a/config/crd/bases/data.fluid.io_cacheruntimeclasses.yaml +++ b/config/crd/bases/data.fluid.io_cacheruntimeclasses.yaml @@ -72,8 +72,6 @@ spec: properties: dependencies: properties: - encryptOption: - type: object extraResources: properties: configMaps: @@ -86,6 +84,11 @@ spec: type: object type: array type: object + secretMount: + properties: + enabled: + type: boolean + type: object type: object executionEntries: properties: @@ -3489,8 +3492,6 @@ spec: properties: dependencies: properties: - encryptOption: - type: object extraResources: properties: configMaps: @@ -3503,6 +3504,11 @@ spec: type: object type: array type: object + secretMount: + properties: + enabled: + type: boolean + type: object type: object executionEntries: properties: @@ -6906,8 +6912,6 @@ spec: properties: dependencies: properties: - encryptOption: - type: object extraResources: properties: configMaps: @@ -6920,6 +6924,11 @@ spec: type: object type: array type: object + secretMount: + properties: + enabled: + type: boolean + type: object type: object executionEntries: properties: diff --git a/docs/en/dev/generic_cache_runtime_integration.md b/docs/en/dev/generic_cache_runtime_integration.md index 81cf7b0dcd2..16765b0b6e1 100644 --- a/docs/en/dev/generic_cache_runtime_integration.md +++ b/docs/en/dev/generic_cache_runtime_integration.md @@ -81,8 +81,7 @@ The component in Topology mainly contains the following content: | Options | Default options, will be overridden by user settings | | | Template | PodTemplateSpec native field | | | Service | Currently only supports Headless | | -| Dependencies | EncryptOption | Whether this component needs Fluid to mount the access keys defined in Dataset for accessing data sources [Not supported in current version], using the keys defined in Dataset for access. | -| | ExtraResources | Whether this component needs to mount additional ConfigMaps (the dependent ConfigMap information is defined in the ExtraResources field of CacheRuntimeClass). | +| Dependencies | ExtraResources | Whether this component needs to mount additional ConfigMaps (the dependent ConfigMap information is defined in the ExtraResources field of CacheRuntimeClass). | | ExecutionEntries| MountUFS | For Master-Worker architecture, when Master is Ready, the underlying file system mount operation needs to be executed. | | ExecutionEntries| ReportSummary | How the cache system defines operations to obtain cache information metrics [Not supported in current version]. | @@ -260,7 +259,7 @@ spec: In cacheruntime, all control plane processes are handled by Fluid. However, as a data caching engine, when providing services, the entire cache system requires **topology**, **data source**, **authentication**, and **cache information**. Fluid will provide this information to components through configuration files based on different Component roles. The component's internal process is responsible for parsing this configuration to perform environment variable configuration, data engine configuration file generation, and other operations. After preparation is complete, the data engine process can be started. For specific parsing details, please refer to the table below: * Taking the above resources as an example, the Config examples mounted by Master/Worker/Client and maintained by Fluid are as follows: - +the `mounts`, `accessModes`, and `targetPath` fields in the JSON are all derived from the Dataset's Spec definition. ```json { @@ -274,6 +273,10 @@ In cacheruntime, all control plane processes are handled by Fluid. However, as a "region_name": "us-east-1", "secret": "minioadmin" }, + "encryptOptions": { + "access-key": "/etc/fluid/secrets/minio-secret/access-key", + "secret-key": "/etc/fluid/secrets/minio-secret/secret-key" + }, "name": "minio", "path": "/minio" } diff --git a/docs/zh/dev/generic_cache_runtime_integration.md b/docs/zh/dev/generic_cache_runtime_integration.md index efa756b0874..5db17c4d6d9 100644 --- a/docs/zh/dev/generic_cache_runtime_integration.md +++ b/docs/zh/dev/generic_cache_runtime_integration.md @@ -81,8 +81,7 @@ Topology中comopent主要包含以下内容 | Options | 默认options,会被用户设置覆盖 | | | Template | PodTemplateSpec 原生字段 | | | Service | 目前仅支持Headless | | -| Dependencies | EncryptOption | 该组件是否需要Fluid为其挂载Dataset中定义的用于访问数据源的访问密钥 【当前版本暂未支持】,使用 Dataset 的定义的密钥进行访问。 | -| | ExtraResources | 该组件是否需要挂载额外的 ConfigMap (其依赖的ConfigMap 信息定义于 CacheRuntimeClass 的 ExtraResources 字段)。 | +| Dependencies | ExtraResources | 该组件是否需要挂载额外的 ConfigMap (其依赖的ConfigMap 信息定义于 CacheRuntimeClass 的 ExtraResources 字段)。 | | ExecutionEntries| MountUFS | 对于Master-Worker架构,当Master Ready时,需要执行底层文件系统的挂载操作。 | | ExecutionEntries| ReportSummary | 缓存系统定义如何获取缓存信息指标的操作 【当前版本暂未支持】。 | @@ -259,8 +258,7 @@ spec: 在cacheruntime中,控制面的所有流程全都有Fluid来负责,但作为数据缓存引擎,提供服务时,需要整个缓存系统中的**拓扑**、**数据源、认证、缓存信息,**Fluid会根据不同的Component角色来通过配置文件的方式提供至组件内部,由组件内部进程负责解析该配置,来进行环境变量配置、数据引擎配置文件生成等操作,准备就绪后,可拉起数据引擎进程,解析过程中具体可参考下表: -* 以上述资源为例,Master/Worker/Client挂载的由Fluid维护的Config示例如下: - +* 以上述资源为例,Master/Worker/Client挂载的由Fluid维护的Config示例如下: 其中,json中的"mounts", "accessModes", "targetPath" 字段信息均是来自于 DataSet 的 Spec 定义。 ```json { @@ -274,6 +272,10 @@ spec: "region_name": "us-east-1", "secret": "minioadmin" }, + "encryptOptions": { + "access-key": "/etc/fluid/secrets/minio-secret/access-key", + "secret-key": "/etc/fluid/secrets/minio-secret/secret-key" + }, "name": "minio", "path": "/minio" } diff --git a/pkg/common/cacheruntime.go b/pkg/common/cacheruntime.go index 8d992a8b168..cfb7cb74bfd 100644 --- a/pkg/common/cacheruntime.go +++ b/pkg/common/cacheruntime.go @@ -77,12 +77,14 @@ type CacheRuntimeConfig struct { // MountConfig defines the mount config about dataset Mounts type MountConfig struct { MountPoint string `json:"mountPoint"` - // TODO: separate encrypt options with mount files for security - Options map[string]string `json:"options,omitempty"` - Name string `json:"name,omitempty"` - Path string `json:"path,omitempty"` - ReadOnly bool `json:"readOnly,omitempty"` - Shared bool `json:"shared,omitempty"` + // Non-encrypted mount options, key is the option name, value is the option value. + Options map[string]string `json:"options,omitempty"` + // Encrypted mount options, key is the option name, value is the secret mount path in container. + EncryptOptions map[string]string `json:"encryptOptions,omitempty"` + Name string `json:"name,omitempty"` + Path string `json:"path,omitempty"` + ReadOnly bool `json:"readOnly,omitempty"` + Shared bool `json:"shared,omitempty"` } type CacheRuntimeComponentConfig struct { diff --git a/pkg/ddc/cache/engine/cache_engine_suite_test.go b/pkg/ddc/cache/engine/cache_engine_suite_test.go new file mode 100644 index 00000000000..2d96d314b6d --- /dev/null +++ b/pkg/ddc/cache/engine/cache_engine_suite_test.go @@ -0,0 +1,13 @@ +package engine + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestCacheEngine(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Cache Engine Suite", Label("cache_engine")) +} diff --git a/pkg/ddc/cache/engine/cm.go b/pkg/ddc/cache/engine/cm.go index 980996defb7..1f03abb6a69 100644 --- a/pkg/ddc/cache/engine/cm.go +++ b/pkg/ddc/cache/engine/cm.go @@ -18,6 +18,7 @@ package engine import ( "encoding/json" + datav1alpha1 "github.com/fluid-cloudnative/fluid/api/v1alpha1" "github.com/fluid-cloudnative/fluid/pkg/common" "github.com/fluid-cloudnative/fluid/pkg/utils" @@ -117,12 +118,12 @@ func (e *CacheEngine) generateRuntimeConfigData(runtime *datav1alpha1.CacheRunti Shared: m.Shared, Path: m.Path, } - // TODO: 默认的加密项的处理?(挂载的形式到 Master 等 Pod 中?)安全性该如何考虑 - options, err := e.generateDatasetMountOptions(&m, dataset.Spec.SharedEncryptOptions, dataset.Spec.SharedOptions) + options, encryptOptions, err := e.generateDatasetMountOptions(&m, dataset.Spec.SharedEncryptOptions, dataset.Spec.SharedOptions) if err != nil { return nil, err } mountCg.Options = options + mountCg.EncryptOptions = encryptOptions mounts = append(mounts, mountCg) } diff --git a/pkg/ddc/cache/engine/dataset.go b/pkg/ddc/cache/engine/dataset.go index df2fc43483f..da707cdad7a 100644 --- a/pkg/ddc/cache/engine/dataset.go +++ b/pkg/ddc/cache/engine/dataset.go @@ -19,15 +19,14 @@ package engine import ( "context" "fmt" + "reflect" + datav1alpha1 "github.com/fluid-cloudnative/fluid/api/v1alpha1" "github.com/fluid-cloudnative/fluid/pkg/common" "github.com/fluid-cloudnative/fluid/pkg/utils" - "github.com/fluid-cloudnative/fluid/pkg/utils/kubeclient" - securityutil "github.com/fluid-cloudnative/fluid/pkg/utils/security" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" - "reflect" ) func (e *CacheEngine) BindToDataset() (err error) { @@ -102,54 +101,47 @@ func (e *CacheEngine) UpdateDatasetStatus(phase datav1alpha1.DatasetPhase) (err } func (e *CacheEngine) generateDatasetMountOptions(m *datav1alpha1.Mount, sharedEncryptOptions []datav1alpha1.EncryptOption, - sharedOptions map[string]string) (map[string]string, error) { + sharedOptions map[string]string) (mOptions map[string]string, encryptOptions map[string]string, err error) { - // initialize mount options - mOptions := map[string]string{} + // Initialize return maps + mOptions = make(map[string]string) + encryptOptions = make(map[string]string) + + // initialize mount options, mount options will overwrite shared options. for k, v := range sharedOptions { mOptions[k] = v } - for key, value := range m.Options { mOptions[key] = value } - // if encryptOptions have the same key with options, it will overwrite the corresponding value - var err error - mOptions, err = e.genEncryptOptions(sharedEncryptOptions, mOptions, m.Name) + // collect encrypt options, mount options will overwrite shared options. + err = e.collectEncryptOptions(sharedEncryptOptions, encryptOptions) if err != nil { - return mOptions, err + return } - - // gen public encryptOptions - mOptions, err = e.genEncryptOptions(m.EncryptOptions, mOptions, m.Name) + err = e.collectEncryptOptions(m.EncryptOptions, encryptOptions) if err != nil { - return mOptions, err + return } - return mOptions, nil + return } -func (e *CacheEngine) genEncryptOptions(EncryptOptions []datav1alpha1.EncryptOption, mOptions map[string]string, name string) (map[string]string, error) { - for _, item := range EncryptOptions { - if _, ok := mOptions[item.Name]; ok { - err := fmt.Errorf("the option %s is set more than one times, please double check the dataset's option and encryptOptions", item.Name) - return mOptions, err - } +func (e *CacheEngine) collectEncryptOptions(encryptOpts []datav1alpha1.EncryptOption, existingEncryptOpts map[string]string) error { - securityutil.UpdateSensitiveKey(item.Name) + for _, item := range encryptOpts { sRef := item.ValueFrom.SecretKeyRef - secret, err := kubeclient.GetSecret(e.Client, sRef.Name, e.namespace) - if err != nil { - e.Log.Error(err, "get secret by mount encrypt options failed", "name", item.Name) - return mOptions, err + if sRef.Name == "" || sRef.Key == "" { + return fmt.Errorf("encryptOption %s has empty secretKeyRef name or key", item.Name) } - e.Log.Info("get value from secret", "mount name", name, "secret key", sRef.Key) + // Construct the secret mount path in the container + // The secret will be mounted at /etc/fluid/secrets// + secretPath := getSecretFilePath(sRef.Name, sRef.Key) - v := secret.Data[sRef.Key] - mOptions[item.Name] = string(v) + // Store in map: key is option name, value is secret path + existingEncryptOpts[item.Name] = secretPath } - - return mOptions, nil + return nil } diff --git a/pkg/ddc/cache/engine/dataset_test.go b/pkg/ddc/cache/engine/dataset_test.go new file mode 100644 index 00000000000..5200cb8227e --- /dev/null +++ b/pkg/ddc/cache/engine/dataset_test.go @@ -0,0 +1,402 @@ +/* +Copyright 2026 The Fluid Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package engine + +import ( + datav1alpha1 "github.com/fluid-cloudnative/fluid/api/v1alpha1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("generateDatasetMountOptions Tests", Label("pkg.ddc.cache.engine.dataset_test.go"), func() { + var engine *CacheEngine + + BeforeEach(func() { + engine = &CacheEngine{} + }) + + Describe("generateDatasetMountOptions", func() { + Context("when mount has no options and no encrypt options", func() { + It("should return empty maps", func() { + mount := &datav1alpha1.Mount{ + Name: "test-mount", + MountPoint: "s3://test-bucket", + } + var sharedEncryptOptions []datav1alpha1.EncryptOption + sharedOptions := map[string]string{} + + mOptions, encryptOptions, err := engine.generateDatasetMountOptions(mount, sharedEncryptOptions, sharedOptions) + + Expect(err).NotTo(HaveOccurred()) + Expect(mOptions).To(BeEmpty()) + Expect(encryptOptions).To(BeEmpty()) + }) + }) + + Context("when mount has only options", func() { + It("should return mount options correctly", func() { + mount := &datav1alpha1.Mount{ + Name: "test-mount", + MountPoint: "s3://test-bucket", + Options: map[string]string{ + "option1": "value1", + "option2": "value2", + }, + } + var sharedEncryptOptions []datav1alpha1.EncryptOption + sharedOptions := map[string]string{} + + mOptions, encryptOptions, err := engine.generateDatasetMountOptions(mount, sharedEncryptOptions, sharedOptions) + + Expect(err).NotTo(HaveOccurred()) + Expect(mOptions).To(HaveLen(2)) + Expect(mOptions["option1"]).To(Equal("value1")) + Expect(mOptions["option2"]).To(Equal("value2")) + Expect(encryptOptions).To(BeEmpty()) + }) + }) + + Context("when shared options exist", func() { + It("should merge shared options with mount options", func() { + mount := &datav1alpha1.Mount{ + Name: "test-mount", + MountPoint: "s3://test-bucket", + Options: map[string]string{ + "mount-option": "mount-value", + }, + } + var sharedEncryptOptions []datav1alpha1.EncryptOption + sharedOptions := map[string]string{ + "shared-option": "shared-value", + } + + mOptions, encryptOptions, err := engine.generateDatasetMountOptions(mount, sharedEncryptOptions, sharedOptions) + + Expect(err).NotTo(HaveOccurred()) + Expect(mOptions).To(HaveLen(2)) + Expect(mOptions["shared-option"]).To(Equal("shared-value")) + Expect(mOptions["mount-option"]).To(Equal("mount-value")) + Expect(encryptOptions).To(BeEmpty()) + }) + + It("mount options should override shared options with same key", func() { + mount := &datav1alpha1.Mount{ + Name: "test-mount", + MountPoint: "s3://test-bucket", + Options: map[string]string{ + "common-option": "mount-value", + }, + } + var sharedEncryptOptions []datav1alpha1.EncryptOption + sharedOptions := map[string]string{ + "common-option": "shared-value", + } + + mOptions, encryptOptions, err := engine.generateDatasetMountOptions(mount, sharedEncryptOptions, sharedOptions) + + Expect(err).NotTo(HaveOccurred()) + Expect(mOptions).To(HaveLen(1)) + // Mount options should override shared options + Expect(mOptions["common-option"]).To(Equal("mount-value")) + Expect(encryptOptions).To(BeEmpty()) + }) + }) + + Context("when shared encrypt options exist", func() { + It("should collect shared encrypt options correctly", func() { + mount := &datav1alpha1.Mount{ + Name: "test-mount", + MountPoint: "s3://test-bucket", + } + sharedEncryptOptions := []datav1alpha1.EncryptOption{ + { + Name: "aws-access-key-id", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: "aws-secret", + Key: "access-key", + }, + }, + }, + } + sharedOptions := map[string]string{} + + mOptions, encryptOptions, err := engine.generateDatasetMountOptions(mount, sharedEncryptOptions, sharedOptions) + + Expect(err).NotTo(HaveOccurred()) + Expect(mOptions).To(BeEmpty()) + Expect(encryptOptions).To(HaveLen(1)) + Expect(encryptOptions["aws-access-key-id"]).To(Equal("/etc/fluid/secrets/aws-secret/access-key")) + }) + }) + + Context("when mount has encrypt options", func() { + It("should collect mount encrypt options correctly", func() { + mount := &datav1alpha1.Mount{ + Name: "test-mount", + MountPoint: "s3://test-bucket", + EncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "aws-secret-access-key", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: "aws-secret", + Key: "secret-key", + }, + }, + }, + }, + } + var sharedEncryptOptions []datav1alpha1.EncryptOption + sharedOptions := map[string]string{} + + mOptions, encryptOptions, err := engine.generateDatasetMountOptions(mount, sharedEncryptOptions, sharedOptions) + + Expect(err).NotTo(HaveOccurred()) + Expect(mOptions).To(BeEmpty()) + Expect(encryptOptions).To(HaveLen(1)) + Expect(encryptOptions["aws-secret-access-key"]).To(Equal("/etc/fluid/secrets/aws-secret/secret-key")) + }) + }) + + Context("when both shared and mount encrypt options exist", func() { + It("should collect all encrypt options", func() { + mount := &datav1alpha1.Mount{ + Name: "test-mount", + MountPoint: "s3://test-bucket", + EncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "mount-encrypt-option", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: "mount-secret", + Key: "key1", + }, + }, + }, + }, + } + sharedEncryptOptions := []datav1alpha1.EncryptOption{ + { + Name: "shared-encrypt-option", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: "shared-secret", + Key: "key2", + }, + }, + }, + } + sharedOptions := map[string]string{} + + mOptions, encryptOptions, err := engine.generateDatasetMountOptions(mount, sharedEncryptOptions, sharedOptions) + + Expect(err).NotTo(HaveOccurred()) + Expect(mOptions).To(BeEmpty()) + Expect(encryptOptions).To(HaveLen(2)) + Expect(encryptOptions["shared-encrypt-option"]).To(Equal("/etc/fluid/secrets/shared-secret/key2")) + Expect(encryptOptions["mount-encrypt-option"]).To(Equal("/etc/fluid/secrets/mount-secret/key1")) + }) + + It("mount encrypt options should override shared encrypt options with same name", func() { + mount := &datav1alpha1.Mount{ + Name: "test-mount", + MountPoint: "s3://test-bucket", + EncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "common-encrypt-option", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: "mount-secret", + Key: "mount-key", + }, + }, + }, + }, + } + sharedEncryptOptions := []datav1alpha1.EncryptOption{ + { + Name: "common-encrypt-option", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: "shared-secret", + Key: "shared-key", + }, + }, + }, + } + sharedOptions := map[string]string{} + + mOptions, encryptOptions, err := engine.generateDatasetMountOptions(mount, sharedEncryptOptions, sharedOptions) + + Expect(err).NotTo(HaveOccurred()) + Expect(mOptions).To(BeEmpty()) + Expect(encryptOptions).To(HaveLen(1)) + // Mount encrypt options should override shared encrypt options + Expect(encryptOptions["common-encrypt-option"]).To(Equal("/etc/fluid/secrets/mount-secret/mount-key")) + }) + }) + + Context("when encrypt option has empty secret key ref", func() { + It("should return error when secret name is empty", func() { + mount := &datav1alpha1.Mount{ + Name: "test-mount", + MountPoint: "s3://test-bucket", + EncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "test-option", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: "", + Key: "some-key", + }, + }, + }, + }, + } + var sharedEncryptOptions []datav1alpha1.EncryptOption + sharedOptions := map[string]string{} + + mOptions, encryptOptions, err := engine.generateDatasetMountOptions(mount, sharedEncryptOptions, sharedOptions) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("empty secretKeyRef name or key")) + Expect(mOptions).To(BeEmpty()) + Expect(encryptOptions).To(BeEmpty()) + }) + + It("should return error when secret key is empty", func() { + mount := &datav1alpha1.Mount{ + Name: "test-mount", + MountPoint: "s3://test-bucket", + EncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "test-option", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: "some-secret", + Key: "", + }, + }, + }, + }, + } + var sharedEncryptOptions []datav1alpha1.EncryptOption + sharedOptions := map[string]string{} + + mOptions, encryptOptions, err := engine.generateDatasetMountOptions(mount, sharedEncryptOptions, sharedOptions) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("empty secretKeyRef name or key")) + Expect(mOptions).To(BeEmpty()) + Expect(encryptOptions).To(BeEmpty()) + }) + }) + + Context("when complex scenario with all options", func() { + It("should handle all types of options together", func() { + mount := &datav1alpha1.Mount{ + Name: "test-mount", + MountPoint: "s3://test-bucket", + Options: map[string]string{ + "mount-opt1": "mount-val1", + "mount-opt2": "mount-val2", + }, + EncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "mount-encrypt", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: "mount-secret", + Key: "mount-key", + }, + }, + }, + }, + } + sharedEncryptOptions := []datav1alpha1.EncryptOption{ + { + Name: "shared-encrypt", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: "shared-secret", + Key: "shared-key", + }, + }, + }, + } + sharedOptions := map[string]string{ + "shared-opt1": "shared-val1", + "mount-opt1": "shared-val-override", + } + + mOptions, encryptOptions, err := engine.generateDatasetMountOptions(mount, sharedEncryptOptions, sharedOptions) + + Expect(err).NotTo(HaveOccurred()) + // Check mount options: 3 total (2 from mount + 1 from shared, but mount-opt1 overridden by mount) + Expect(mOptions).To(HaveLen(3)) + Expect(mOptions["shared-opt1"]).To(Equal("shared-val1")) + Expect(mOptions["mount-opt1"]).To(Equal("mount-val1")) // Mount overrides shared + Expect(mOptions["mount-opt2"]).To(Equal("mount-val2")) + + // Check encrypt options: 2 total + Expect(encryptOptions).To(HaveLen(2)) + Expect(encryptOptions["shared-encrypt"]).To(Equal("/etc/fluid/secrets/shared-secret/shared-key")) + Expect(encryptOptions["mount-encrypt"]).To(Equal("/etc/fluid/secrets/mount-secret/mount-key")) + }) + }) + + Context("when multiple encrypt options reference same secret", func() { + It("should generate correct paths for each option", func() { + mount := &datav1alpha1.Mount{ + Name: "test-mount", + MountPoint: "s3://test-bucket", + EncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "option1", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: "same-secret", + Key: "key1", + }, + }, + }, + { + Name: "option2", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: "same-secret", + Key: "key2", + }, + }, + }, + }, + } + var sharedEncryptOptions []datav1alpha1.EncryptOption + sharedOptions := map[string]string{} + + mOptions, encryptOptions, err := engine.generateDatasetMountOptions(mount, sharedEncryptOptions, sharedOptions) + + Expect(err).NotTo(HaveOccurred()) + Expect(mOptions).To(BeEmpty()) + Expect(encryptOptions).To(HaveLen(2)) + Expect(encryptOptions["option1"]).To(Equal("/etc/fluid/secrets/same-secret/key1")) + Expect(encryptOptions["option2"]).To(Equal("/etc/fluid/secrets/same-secret/key2")) + }) + }) + }) +}) diff --git a/pkg/ddc/cache/engine/setup.go b/pkg/ddc/cache/engine/setup.go index eb45ee32420..3c7ff1ff45e 100644 --- a/pkg/ddc/cache/engine/setup.go +++ b/pkg/ddc/cache/engine/setup.go @@ -76,15 +76,6 @@ func (e *CacheEngine) Setup(ctx cruntime.ReconcileRequestContext) (ready bool, e } } - // dataset mount - if runtimeValue.Master.Enabled { - // currently only support mount ufs for master - err = e.PrepareUFS(runtimeClass.Topology.Master.ExecutionEntries, runtimeValue) - if err != nil { - return false, err - } - } - ready, err = e.CheckAndUpdateRuntimeStatus(runtimeValue) if err != nil { _ = utils.LoggingErrorExceptConflict(e.Log, err, "Failed to check if the runtime is ready", types.NamespacedName{Namespace: e.namespace, Name: e.name}) @@ -94,6 +85,16 @@ func (e *CacheEngine) Setup(ctx cruntime.ReconcileRequestContext) (ready bool, e return } + // dataset mount after runtime ready to ensure master pod is ready for executing commands. + if runtimeValue.Master.Enabled && runtimeClass.Topology != nil && + runtimeClass.Topology.Master != nil && runtimeClass.Topology.Master.ExecutionEntries != nil { + // currently only support mount ufs for master in master-worker architecture + err = e.PrepareUFS(runtimeClass.Topology.Master.ExecutionEntries.MountUFS, runtimeValue) + if err != nil { + return false, err + } + } + if err = e.BindToDataset(); err != nil { _ = utils.LoggingErrorExceptConflict(e.Log, err, "Failed to bind the dataset", types.NamespacedName{Namespace: e.namespace, Name: e.name}) return false, err diff --git a/pkg/ddc/cache/engine/status.go b/pkg/ddc/cache/engine/status.go index d745a20cb01..5381dfd66bf 100644 --- a/pkg/ddc/cache/engine/status.go +++ b/pkg/ddc/cache/engine/status.go @@ -19,14 +19,15 @@ package engine import ( "context" "fmt" + "reflect" + "time" + fluidapi "github.com/fluid-cloudnative/fluid/api/v1alpha1" "github.com/fluid-cloudnative/fluid/pkg/common" "github.com/fluid-cloudnative/fluid/pkg/ddc/cache/component" "github.com/fluid-cloudnative/fluid/pkg/utils" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" - "reflect" - "time" ) func (e *CacheEngine) setMasterComponentStatus(componentValue *common.CacheRuntimeComponentValue, status *fluidapi.CacheRuntimeStatus) (ready bool, err error) { diff --git a/pkg/ddc/cache/engine/transform.go b/pkg/ddc/cache/engine/transform.go index 709955dce09..67cf72f6ade 100644 --- a/pkg/ddc/cache/engine/transform.go +++ b/pkg/ddc/cache/engine/transform.go @@ -19,12 +19,13 @@ package engine import ( "errors" "fmt" + "time" + datav1alpha1 "github.com/fluid-cloudnative/fluid/api/v1alpha1" "github.com/fluid-cloudnative/fluid/pkg/common" "github.com/fluid-cloudnative/fluid/pkg/utils" "github.com/fluid-cloudnative/fluid/pkg/utils/transformer" corev1 "k8s.io/api/core/v1" - "time" ) // CacheRuntimeComponentCommonConfig common config for transform @@ -52,7 +53,8 @@ type RuntimeConfigVolumeConfig struct { func (e *CacheEngine) transform(dataset *datav1alpha1.Dataset, runtime *datav1alpha1.CacheRuntime, runtimeClass *datav1alpha1.CacheRuntimeClass) (*common.CacheRuntimeValue, error) { - if runtimeClass.Topology.Master == nil && runtimeClass.Topology.Worker == nil && runtimeClass.Topology.Client == nil { + if runtimeClass.Topology == nil || + (runtimeClass.Topology.Master == nil && runtimeClass.Topology.Worker == nil && runtimeClass.Topology.Client == nil) { return nil, fmt.Errorf("at least one component should be defined in runtimeClass") } defer utils.TimeTrack(time.Now(), "CacheRuntime.transform", "name", runtime.Name) @@ -71,15 +73,15 @@ func (e *CacheEngine) transform(dataset *datav1alpha1.Dataset, runtime *datav1al } // transform the master/worker/client - err = e.transformMaster(runtime, runtimeClass, runtimeCommonConfig, runtimeValue) + err = e.transformMaster(dataset, runtime, runtimeClass, runtimeCommonConfig, runtimeValue) if err != nil { return nil, err } - err = e.transformWorker(runtime, runtimeClass, runtimeCommonConfig, runtimeValue) + err = e.transformWorker(dataset, runtime, runtimeClass, runtimeCommonConfig, runtimeValue) if err != nil { return nil, err } - err = e.transformClient(runtime, runtimeClass, runtimeCommonConfig, runtimeValue) + err = e.transformClient(dataset, runtime, runtimeClass, runtimeCommonConfig, runtimeValue) if err != nil { return nil, err } diff --git a/pkg/ddc/cache/engine/transform_client.go b/pkg/ddc/cache/engine/transform_client.go index 35305148535..9c5dea48a58 100644 --- a/pkg/ddc/cache/engine/transform_client.go +++ b/pkg/ddc/cache/engine/transform_client.go @@ -22,11 +22,11 @@ import ( corev1 "k8s.io/api/core/v1" ) -func (e *CacheEngine) transformClient(runtime *datav1alpha1.CacheRuntime, runtimeClass *datav1alpha1.CacheRuntimeClass, +func (e *CacheEngine) transformClient(dataset *datav1alpha1.Dataset, runtime *datav1alpha1.CacheRuntime, runtimeClass *datav1alpha1.CacheRuntimeClass, config *CacheRuntimeComponentCommonConfig, value *common.CacheRuntimeValue) error { - if runtimeClass.Topology.Client == nil || runtime.Spec.Client.Disabled { - value.Client.Enabled = false + if runtimeClass.Topology == nil || runtimeClass.Topology.Client == nil || runtime.Spec.Client.Disabled { + value.Client = &common.CacheRuntimeComponentValue{Enabled: false} return nil } @@ -52,6 +52,11 @@ func (e *CacheEngine) transformClient(runtime *datav1alpha1.CacheRuntime, runtim return err } + // transform encrypt options to client volumes (default disabled for Client) + if shouldMountSecrets(component.Dependencies.SecretMount, false) { + e.transformEncryptOptionsToComponentVolumes(dataset, value.Client) + } + podTemplateSpec := &value.Client.PodTemplateSpec // TODO: transform runtime.Spec.Client, runtimeClass.Topology.Client, dataset.Spec into PodTemplateSpec diff --git a/pkg/ddc/cache/engine/transform_master.go b/pkg/ddc/cache/engine/transform_master.go index 04112d4f7de..34e961eb85e 100644 --- a/pkg/ddc/cache/engine/transform_master.go +++ b/pkg/ddc/cache/engine/transform_master.go @@ -21,11 +21,11 @@ import ( "github.com/fluid-cloudnative/fluid/pkg/common" ) -func (e *CacheEngine) transformMaster(runtime *datav1alpha1.CacheRuntime, runtimeClass *datav1alpha1.CacheRuntimeClass, +func (e *CacheEngine) transformMaster(dataset *datav1alpha1.Dataset, runtime *datav1alpha1.CacheRuntime, runtimeClass *datav1alpha1.CacheRuntimeClass, config *CacheRuntimeComponentCommonConfig, value *common.CacheRuntimeValue) error { // TODO: these two field both indicate Master enabled or not, should be combined into one field. - if runtimeClass.Topology.Master == nil || runtime.Spec.Master.Disabled { - value.Master.Enabled = false + if runtimeClass.Topology == nil || runtimeClass.Topology.Master == nil || runtime.Spec.Master.Disabled { + value.Master = &common.CacheRuntimeComponentValue{Enabled: false} return nil } @@ -51,6 +51,11 @@ func (e *CacheEngine) transformMaster(runtime *datav1alpha1.CacheRuntime, runtim return err } + // transform encrypt options to master volumes (default enabled for Master) + if shouldMountSecrets(component.Dependencies.SecretMount, true) { + e.transformEncryptOptionsToComponentVolumes(dataset, value.Master) + } + // TODO: transform runtime.Spec.Master, runtimeClass.Topology.Master, dataset.Spec into PodTemplateSpec return nil diff --git a/pkg/ddc/cache/engine/transform_volumes.go b/pkg/ddc/cache/engine/transform_volumes.go new file mode 100644 index 00000000000..25987a17294 --- /dev/null +++ b/pkg/ddc/cache/engine/transform_volumes.go @@ -0,0 +1,83 @@ +/* +Copyright 2026 The Fluid Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package engine + +import ( + datav1alpha1 "github.com/fluid-cloudnative/fluid/api/v1alpha1" + "github.com/fluid-cloudnative/fluid/pkg/common" + "github.com/fluid-cloudnative/fluid/pkg/utils" + corev1 "k8s.io/api/core/v1" +) + +// transformEncryptOptionsToComponentVolumes transforms encrypt options from dataset spec to component pod volumes +// This function can be reused for both Master and Worker components +func (e *CacheEngine) transformEncryptOptionsToComponentVolumes(dataset *datav1alpha1.Dataset, component *common.CacheRuntimeComponentValue) { + if component == nil || !component.Enabled || len(component.PodTemplateSpec.Spec.Containers) == 0 { + return + } + + // Helper to add secret volume and mount to the component + addSecret := func(secretName string) { + if secretName == "" { + return + } + volName := getSecretVolumeName(secretName) + volumeToAdd := corev1.Volume{ + Name: volName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: secretName, + }, + }, + } + component.PodTemplateSpec.Spec.Volumes = utils.AppendOrOverrideVolume( + component.PodTemplateSpec.Spec.Volumes, volumeToAdd) + + volumeMountToAdd := corev1.VolumeMount{ + Name: volName, + ReadOnly: true, + MountPath: getSecretMountPath(secretName), + } + component.PodTemplateSpec.Spec.Containers[0].VolumeMounts = utils.AppendOrOverrideVolumeMounts( + component.PodTemplateSpec.Spec.Containers[0].VolumeMounts, volumeMountToAdd) + } + + // 1. Process shared encrypt options once + for _, encryptOpt := range dataset.Spec.SharedEncryptOptions { + addSecret(encryptOpt.ValueFrom.SecretKeyRef.Name) + } + + // 2. Process mount-specific encrypt options, override shared options + for _, m := range dataset.Spec.Mounts { + if common.IsFluidNativeScheme(m.MountPoint) { + continue + } + for _, encryptOpt := range m.EncryptOptions { + addSecret(encryptOpt.ValueFrom.SecretKeyRef.Name) + } + } +} + +// shouldMountSecrets determines whether secrets should be mounted based on configuration and default behavior +// config: the SecretMount configuration from CacheRuntimeClass (can be nil) +// defaultEnabled: the default behavior when config is nil or not explicitly set +func shouldMountSecrets(config *datav1alpha1.SecretMountComponentDependency, defaultEnabled bool) bool { + if config == nil { + return defaultEnabled + } + return config.Enabled +} diff --git a/pkg/ddc/cache/engine/transform_volumes_test.go b/pkg/ddc/cache/engine/transform_volumes_test.go new file mode 100644 index 00000000000..1dd05308f19 --- /dev/null +++ b/pkg/ddc/cache/engine/transform_volumes_test.go @@ -0,0 +1,684 @@ +/* +Copyright 2026 The Fluid Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package engine + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + datav1alpha1 "github.com/fluid-cloudnative/fluid/api/v1alpha1" + "github.com/fluid-cloudnative/fluid/pkg/common" + corev1 "k8s.io/api/core/v1" +) + +// Constants for test values +const ( + testSecretName1 = "test-secret-1" + testSecretName2 = "test-secret-2" + testSecretKey = "access-key" + testMountName = "test-mount" + testMountPoint = "s3://test-bucket" + nativeMountPoint = "local:///mnt/test" +) + +var _ = Describe("CacheEngine Transform Volumes Tests", Label("pkg.ddc.cache.engine.transform_volumes_test.go"), func() { + var ( + engine *CacheEngine + value *common.CacheRuntimeValue + ) + + BeforeEach(func() { + engine = &CacheEngine{} + value = &common.CacheRuntimeValue{ + Master: &common.CacheRuntimeComponentValue{ + Enabled: true, + PodTemplateSpec: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "master", + }, + }, + }, + }, + }, + } + }) + + Describe("transformEncryptOptionsToMasterVolumes", func() { + Context("when dataset has shared encrypt options", func() { + It("should correctly transform shared encrypt options to master volumes", func() { + dataset := &datav1alpha1.Dataset{ + Spec: datav1alpha1.DatasetSpec{ + Mounts: []datav1alpha1.Mount{ + { + MountPoint: testMountPoint, + Name: testMountName, + }, + }, + SharedEncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "aws-access-key-id", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: testSecretName1, + Key: testSecretKey, + }, + }, + }, + }, + }, + } + + engine.transformEncryptOptionsToComponentVolumes(dataset, value.Master) + + Expect(value.Master.PodTemplateSpec.Spec.Volumes).To(HaveLen(1)) + Expect(value.Master.PodTemplateSpec.Spec.Volumes[0].Name).To(Equal(secretVolumeNamePrefix + testSecretName1)) + Expect(value.Master.PodTemplateSpec.Spec.Volumes[0].Secret.SecretName).To(Equal(testSecretName1)) + + Expect(value.Master.PodTemplateSpec.Spec.Containers[0].VolumeMounts).To(HaveLen(1)) + Expect(value.Master.PodTemplateSpec.Spec.Containers[0].VolumeMounts[0].Name).To(Equal(secretVolumeNamePrefix + testSecretName1)) + Expect(value.Master.PodTemplateSpec.Spec.Containers[0].VolumeMounts[0].MountPath).To(Equal("/etc/fluid/secrets/" + testSecretName1)) + Expect(value.Master.PodTemplateSpec.Spec.Containers[0].VolumeMounts[0].ReadOnly).To(BeTrue()) + }) + }) + + Context("when dataset has mount-specific encrypt options", func() { + It("should correctly transform mount encrypt options to master volumes", func() { + dataset := &datav1alpha1.Dataset{ + Spec: datav1alpha1.DatasetSpec{ + Mounts: []datav1alpha1.Mount{ + { + MountPoint: testMountPoint, + Name: testMountName, + EncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "aws-secret-access-key", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: testSecretName2, + Key: testSecretKey, + }, + }, + }, + }, + }, + }, + }, + } + + engine.transformEncryptOptionsToComponentVolumes(dataset, value.Master) + + Expect(value.Master.PodTemplateSpec.Spec.Volumes).To(HaveLen(1)) + Expect(value.Master.PodTemplateSpec.Spec.Volumes[0].Name).To(Equal(secretVolumeNamePrefix + testSecretName2)) + Expect(value.Master.PodTemplateSpec.Spec.Volumes[0].Secret.SecretName).To(Equal(testSecretName2)) + + Expect(value.Master.PodTemplateSpec.Spec.Containers[0].VolumeMounts).To(HaveLen(1)) + Expect(value.Master.PodTemplateSpec.Spec.Containers[0].VolumeMounts[0].Name).To(Equal(secretVolumeNamePrefix + testSecretName2)) + Expect(value.Master.PodTemplateSpec.Spec.Containers[0].VolumeMounts[0].MountPath).To(Equal("/etc/fluid/secrets/" + testSecretName2)) + Expect(value.Master.PodTemplateSpec.Spec.Containers[0].VolumeMounts[0].ReadOnly).To(BeTrue()) + }) + }) + + Context("when dataset has both shared and mount-specific encrypt options", func() { + It("should correctly transform all encrypt options to master volumes", func() { + dataset := &datav1alpha1.Dataset{ + Spec: datav1alpha1.DatasetSpec{ + Mounts: []datav1alpha1.Mount{ + { + MountPoint: testMountPoint, + Name: testMountName, + EncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "aws-secret-access-key", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: testSecretName2, + Key: testSecretKey, + }, + }, + }, + }, + }, + }, + SharedEncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "aws-access-key-id", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: testSecretName1, + Key: testSecretKey, + }, + }, + }, + }, + }, + } + + engine.transformEncryptOptionsToComponentVolumes(dataset, value.Master) + + Expect(value.Master.PodTemplateSpec.Spec.Volumes).To(HaveLen(2)) + volumeNames := []string{ + value.Master.PodTemplateSpec.Spec.Volumes[0].Name, + value.Master.PodTemplateSpec.Spec.Volumes[1].Name, + } + Expect(volumeNames).To(ContainElements( + secretVolumeNamePrefix+testSecretName1, + secretVolumeNamePrefix+testSecretName2, + )) + + Expect(value.Master.PodTemplateSpec.Spec.Containers[0].VolumeMounts).To(HaveLen(2)) + mountNames := []string{ + value.Master.PodTemplateSpec.Spec.Containers[0].VolumeMounts[0].Name, + value.Master.PodTemplateSpec.Spec.Containers[0].VolumeMounts[1].Name, + } + Expect(mountNames).To(ContainElements( + secretVolumeNamePrefix+testSecretName1, + secretVolumeNamePrefix+testSecretName2, + )) + }) + }) + + Context("when dataset has native fluid scheme mount", func() { + It("should skip native fluid scheme mounts", func() { + dataset := &datav1alpha1.Dataset{ + Spec: datav1alpha1.DatasetSpec{ + Mounts: []datav1alpha1.Mount{ + { + MountPoint: nativeMountPoint, + Name: testMountName, + EncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "some-option", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: testSecretName1, + Key: testSecretKey, + }, + }, + }, + }, + }, + }, + }, + } + + engine.transformEncryptOptionsToComponentVolumes(dataset, value.Master) + + Expect(value.Master.PodTemplateSpec.Spec.Volumes).To(BeEmpty()) + Expect(value.Master.PodTemplateSpec.Spec.Containers[0].VolumeMounts).To(BeEmpty()) + }) + }) + + Context("when master is disabled", func() { + It("should not add any volumes", func() { + value.Master.Enabled = false + dataset := &datav1alpha1.Dataset{ + Spec: datav1alpha1.DatasetSpec{ + Mounts: []datav1alpha1.Mount{ + { + MountPoint: testMountPoint, + Name: testMountName, + EncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "some-option", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: testSecretName1, + Key: testSecretKey, + }, + }, + }, + }, + }, + }, + }, + } + + engine.transformEncryptOptionsToComponentVolumes(dataset, value.Master) + + Expect(value.Master.PodTemplateSpec.Spec.Volumes).To(BeEmpty()) + Expect(value.Master.PodTemplateSpec.Spec.Containers[0].VolumeMounts).To(BeEmpty()) + }) + }) + + Context("when master is nil", func() { + It("should not panic", func() { + value.Master = nil + dataset := &datav1alpha1.Dataset{ + Spec: datav1alpha1.DatasetSpec{ + Mounts: []datav1alpha1.Mount{ + { + MountPoint: testMountPoint, + Name: testMountName, + }, + }, + }, + } + + // Should not panic + engine.transformEncryptOptionsToComponentVolumes(dataset, value.Master) + }) + }) + + Context("when same secret is used multiple times", func() { + It("should override existing volume and volume mount", func() { + dataset := &datav1alpha1.Dataset{ + Spec: datav1alpha1.DatasetSpec{ + Mounts: []datav1alpha1.Mount{ + { + MountPoint: testMountPoint, + Name: testMountName, + EncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "option1", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: testSecretName1, + Key: testSecretKey, + }, + }, + }, + }, + }, + { + MountPoint: "s3://another-bucket", + Name: "another-mount", + EncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "option2", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: testSecretName1, + Key: testSecretKey, + }, + }, + }, + }, + }, + }, + }, + } + + engine.transformEncryptOptionsToComponentVolumes(dataset, value.Master) + + // Should only have one volume for the same secret + Expect(value.Master.PodTemplateSpec.Spec.Volumes).To(HaveLen(1)) + Expect(value.Master.PodTemplateSpec.Spec.Volumes[0].Name).To(Equal(secretVolumeNamePrefix + testSecretName1)) + + // Should only have one volume mount for the same secret + Expect(value.Master.PodTemplateSpec.Spec.Containers[0].VolumeMounts).To(HaveLen(1)) + Expect(value.Master.PodTemplateSpec.Spec.Containers[0].VolumeMounts[0].Name).To(Equal(secretVolumeNamePrefix + testSecretName1)) + }) + }) + + Context("when encrypt option has empty secret name", func() { + It("should skip encrypt options with empty secret name in shared encrypt options", func() { + dataset := &datav1alpha1.Dataset{ + Spec: datav1alpha1.DatasetSpec{ + Mounts: []datav1alpha1.Mount{ + { + MountPoint: testMountPoint, + Name: testMountName, + }, + }, + SharedEncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "aws-access-key-id", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: "", // Empty secret name + Key: testSecretKey, + }, + }, + }, + }, + }, + } + + engine.transformEncryptOptionsToComponentVolumes(dataset, value.Master) + + // Should not add any volumes for empty secret name + Expect(value.Master.PodTemplateSpec.Spec.Volumes).To(BeEmpty()) + Expect(value.Master.PodTemplateSpec.Spec.Containers[0].VolumeMounts).To(BeEmpty()) + }) + + It("should skip encrypt options with empty secret name in mount encrypt options", func() { + dataset := &datav1alpha1.Dataset{ + Spec: datav1alpha1.DatasetSpec{ + Mounts: []datav1alpha1.Mount{ + { + MountPoint: testMountPoint, + Name: testMountName, + EncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "aws-secret-access-key", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: "", // Empty secret name + Key: testSecretKey, + }, + }, + }, + }, + }, + }, + }, + } + + engine.transformEncryptOptionsToComponentVolumes(dataset, value.Master) + + // Should not add any volumes for empty secret name + Expect(value.Master.PodTemplateSpec.Spec.Volumes).To(BeEmpty()) + Expect(value.Master.PodTemplateSpec.Spec.Containers[0].VolumeMounts).To(BeEmpty()) + }) + + It("should skip empty secret names but process valid ones", func() { + dataset := &datav1alpha1.Dataset{ + Spec: datav1alpha1.DatasetSpec{ + Mounts: []datav1alpha1.Mount{ + { + MountPoint: testMountPoint, + Name: testMountName, + EncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "invalid-option", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: "", // Empty secret name - should be skipped + Key: testSecretKey, + }, + }, + }, + { + Name: "valid-option", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: testSecretName1, // Valid secret name + Key: testSecretKey, + }, + }, + }, + }, + }, + }, + SharedEncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "another-invalid-option", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: "", // Empty secret name - should be skipped + Key: testSecretKey, + }, + }, + }, + }, + }, + } + + engine.transformEncryptOptionsToComponentVolumes(dataset, value.Master) + + // Should only add volume for the valid secret name + Expect(value.Master.PodTemplateSpec.Spec.Volumes).To(HaveLen(1)) + Expect(value.Master.PodTemplateSpec.Spec.Volumes[0].Name).To(Equal(secretVolumeNamePrefix + testSecretName1)) + Expect(value.Master.PodTemplateSpec.Spec.Volumes[0].Secret.SecretName).To(Equal(testSecretName1)) + + Expect(value.Master.PodTemplateSpec.Spec.Containers[0].VolumeMounts).To(HaveLen(1)) + Expect(value.Master.PodTemplateSpec.Spec.Containers[0].VolumeMounts[0].Name).To(Equal(secretVolumeNamePrefix + testSecretName1)) + Expect(value.Master.PodTemplateSpec.Spec.Containers[0].VolumeMounts[0].MountPath).To(Equal("/etc/fluid/secrets/" + testSecretName1)) + }) + }) + }) + + Describe("transformEncryptOptionsToComponentVolumes for Worker", func() { + BeforeEach(func() { + value.Worker = &common.CacheRuntimeComponentValue{ + Enabled: true, + PodTemplateSpec: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "worker", + }, + }, + }, + }, + } + }) + + Context("when dataset has shared encrypt options for worker", func() { + It("should correctly transform shared encrypt options to worker volumes", func() { + dataset := &datav1alpha1.Dataset{ + Spec: datav1alpha1.DatasetSpec{ + Mounts: []datav1alpha1.Mount{ + { + MountPoint: testMountPoint, + Name: testMountName, + }, + }, + SharedEncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "aws-access-key-id", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: testSecretName1, + Key: testSecretKey, + }, + }, + }, + }, + }, + } + + engine.transformEncryptOptionsToComponentVolumes(dataset, value.Worker) + + Expect(value.Worker.PodTemplateSpec.Spec.Volumes).To(HaveLen(1)) + Expect(value.Worker.PodTemplateSpec.Spec.Volumes[0].Name).To(Equal(secretVolumeNamePrefix + testSecretName1)) + Expect(value.Worker.PodTemplateSpec.Spec.Volumes[0].Secret.SecretName).To(Equal(testSecretName1)) + + Expect(value.Worker.PodTemplateSpec.Spec.Containers[0].VolumeMounts).To(HaveLen(1)) + Expect(value.Worker.PodTemplateSpec.Spec.Containers[0].VolumeMounts[0].Name).To(Equal(secretVolumeNamePrefix + testSecretName1)) + Expect(value.Worker.PodTemplateSpec.Spec.Containers[0].VolumeMounts[0].MountPath).To(Equal("/etc/fluid/secrets/" + testSecretName1)) + Expect(value.Worker.PodTemplateSpec.Spec.Containers[0].VolumeMounts[0].ReadOnly).To(BeTrue()) + }) + }) + + Context("when worker is disabled", func() { + It("should not add any volumes", func() { + value.Worker.Enabled = false + dataset := &datav1alpha1.Dataset{ + Spec: datav1alpha1.DatasetSpec{ + Mounts: []datav1alpha1.Mount{ + { + MountPoint: testMountPoint, + Name: testMountName, + EncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "some-option", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: testSecretName1, + Key: testSecretKey, + }, + }, + }, + }, + }, + }, + }, + } + + engine.transformEncryptOptionsToComponentVolumes(dataset, value.Worker) + + Expect(value.Worker.PodTemplateSpec.Spec.Volumes).To(BeEmpty()) + Expect(value.Worker.PodTemplateSpec.Spec.Containers[0].VolumeMounts).To(BeEmpty()) + }) + }) + + Context("when encrypt option has empty secret name for worker", func() { + It("should skip encrypt options with empty secret name", func() { + dataset := &datav1alpha1.Dataset{ + Spec: datav1alpha1.DatasetSpec{ + Mounts: []datav1alpha1.Mount{ + { + MountPoint: testMountPoint, + Name: testMountName, + EncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "aws-secret-access-key", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: "", // Empty secret name + Key: testSecretKey, + }, + }, + }, + }, + }, + }, + SharedEncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "aws-access-key-id", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: "", // Empty secret name + Key: testSecretKey, + }, + }, + }, + }, + }, + } + + engine.transformEncryptOptionsToComponentVolumes(dataset, value.Worker) + + // Should not add any volumes for empty secret name + Expect(value.Worker.PodTemplateSpec.Spec.Volumes).To(BeEmpty()) + Expect(value.Worker.PodTemplateSpec.Spec.Containers[0].VolumeMounts).To(BeEmpty()) + }) + }) + }) + + Describe("shouldMountSecrets helper function", func() { + Context("when SecretMount config is nil", func() { + It("should return defaultEnabled value", func() { + // Test with defaultEnabled = true (for Master/Worker) + Expect(shouldMountSecrets(nil, true)).To(BeTrue()) + + // Test with defaultEnabled = false (for Client) + Expect(shouldMountSecrets(nil, false)).To(BeFalse()) + }) + }) + + Context("when SecretMount config is provided", func() { + It("should return the configured Enabled value", func() { + // Test with Enabled = true + config := &datav1alpha1.SecretMountComponentDependency{ + Enabled: true, + } + Expect(shouldMountSecrets(config, false)).To(BeTrue()) + + // Test with Enabled = false + config.Enabled = false + Expect(shouldMountSecrets(config, true)).To(BeFalse()) + }) + }) + }) + + Describe("Client component secret mount behavior", func() { + var ( + clientValue *common.CacheRuntimeComponentValue + dataset *datav1alpha1.Dataset + ) + + BeforeEach(func() { + clientValue = &common.CacheRuntimeComponentValue{ + Enabled: true, + PodTemplateSpec: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "client", + }, + }, + }, + }, + } + dataset = &datav1alpha1.Dataset{ + Spec: datav1alpha1.DatasetSpec{ + SharedEncryptOptions: []datav1alpha1.EncryptOption{ + { + Name: "test-secret", + ValueFrom: datav1alpha1.EncryptOptionSource{ + SecretKeyRef: datav1alpha1.SecretKeySelector{ + Name: testSecretName1, + Key: testSecretKey, + }, + }, + }, + }, + }, + } + }) + + Context("when Client has no SecretMount configuration (default disabled)", func() { + It("should not mount secrets to client pod", func() { + // Simulate Client with nil SecretMount (default behavior) + if shouldMountSecrets(nil, false) { + engine.transformEncryptOptionsToComponentVolumes(dataset, clientValue) + } + + // Should not add any volumes for Client by default + Expect(clientValue.PodTemplateSpec.Spec.Volumes).To(BeEmpty()) + Expect(clientValue.PodTemplateSpec.Spec.Containers[0].VolumeMounts).To(BeEmpty()) + }) + }) + + Context("when Client has SecretMount explicitly enabled", func() { + It("should mount secrets to client pod", func() { + // Simulate Client with SecretMount enabled + secretMountConfig := &datav1alpha1.SecretMountComponentDependency{ + Enabled: true, + } + if shouldMountSecrets(secretMountConfig, false) { + engine.transformEncryptOptionsToComponentVolumes(dataset, clientValue) + } + + // Should add volumes for Client when explicitly enabled + Expect(clientValue.PodTemplateSpec.Spec.Volumes).To(HaveLen(1)) + Expect(clientValue.PodTemplateSpec.Spec.Volumes[0].Name).To(Equal(secretVolumeNamePrefix + testSecretName1)) + Expect(clientValue.PodTemplateSpec.Spec.Containers[0].VolumeMounts).To(HaveLen(1)) + }) + }) + + Context("when Client has SecretMount explicitly disabled", func() { + It("should not mount secrets to client pod", func() { + // Simulate Client with SecretMount explicitly disabled + secretMountConfig := &datav1alpha1.SecretMountComponentDependency{ + Enabled: false, + } + if shouldMountSecrets(secretMountConfig, false) { + engine.transformEncryptOptionsToComponentVolumes(dataset, clientValue) + } + + // Should not add any volumes for Client + Expect(clientValue.PodTemplateSpec.Spec.Volumes).To(BeEmpty()) + Expect(clientValue.PodTemplateSpec.Spec.Containers[0].VolumeMounts).To(BeEmpty()) + }) + }) + }) +}) diff --git a/pkg/ddc/cache/engine/transform_worker.go b/pkg/ddc/cache/engine/transform_worker.go index 6671c7949fc..418f4eea527 100644 --- a/pkg/ddc/cache/engine/transform_worker.go +++ b/pkg/ddc/cache/engine/transform_worker.go @@ -21,11 +21,11 @@ import ( "github.com/fluid-cloudnative/fluid/pkg/common" ) -func (e *CacheEngine) transformWorker(runtime *datav1alpha1.CacheRuntime, runtimeClass *datav1alpha1.CacheRuntimeClass, +func (e *CacheEngine) transformWorker(dataset *datav1alpha1.Dataset, runtime *datav1alpha1.CacheRuntime, runtimeClass *datav1alpha1.CacheRuntimeClass, config *CacheRuntimeComponentCommonConfig, value *common.CacheRuntimeValue) error { - if runtimeClass.Topology.Worker == nil || runtime.Spec.Worker.Disabled { - value.Worker.Enabled = false + if runtimeClass.Topology == nil || runtimeClass.Topology.Worker == nil || runtime.Spec.Worker.Disabled { + value.Worker = &common.CacheRuntimeComponentValue{Enabled: false} return nil } @@ -51,6 +51,11 @@ func (e *CacheEngine) transformWorker(runtime *datav1alpha1.CacheRuntime, runtim return err } + // transform encrypt options to worker volumes (default enabled for Worker) + if shouldMountSecrets(component.Dependencies.SecretMount, true) { + e.transformEncryptOptionsToComponentVolumes(dataset, value.Worker) + } + // TODO: transform runtime.Spec.Worker, runtimeClass.Topology.Worker, dataset.Spec into PodTemplateSpec return nil diff --git a/pkg/ddc/cache/engine/ufs.go b/pkg/ddc/cache/engine/ufs.go index cac88ec42ec..32730ba36a8 100644 --- a/pkg/ddc/cache/engine/ufs.go +++ b/pkg/ddc/cache/engine/ufs.go @@ -17,14 +17,14 @@ package engine import ( + "time" + datav1alpha1 "github.com/fluid-cloudnative/fluid/api/v1alpha1" "github.com/fluid-cloudnative/fluid/pkg/common" - "time" ) -func (e *CacheEngine) PrepareUFS(entries *datav1alpha1.ExecutionEntries, value *common.CacheRuntimeValue) error { +func (e *CacheEngine) PrepareUFS(mountUfs *datav1alpha1.ExecutionCommonEntry, value *common.CacheRuntimeValue) error { // execute mount command in master pod - mountUfs := entries.MountUFS if mountUfs == nil { return nil } diff --git a/pkg/ddc/cache/engine/util.go b/pkg/ddc/cache/engine/util.go index ff95d183cad..f199ca45051 100644 --- a/pkg/ddc/cache/engine/util.go +++ b/pkg/ddc/cache/engine/util.go @@ -17,9 +17,22 @@ package engine import ( + "crypto/sha256" + "encoding/hex" "fmt" + "github.com/fluid-cloudnative/fluid/pkg/common" "github.com/fluid-cloudnative/fluid/pkg/utils" + "k8s.io/apimachinery/pkg/util/validation" +) + +// Precomputed max lengths for the 63-char DNS limit +const ( + secretVolumeNamePrefix = "cache-mnt-secret-" + secretMaxTotalLength = validation.DNS1035LabelMaxLength + prefixSecretVolumeLength = len(secretVolumeNamePrefix) + hashSuffixLength = 8 + truncatedSecretMaxLength = secretMaxTotalLength - prefixSecretVolumeLength - hashSuffixLength ) // GetComponentName gets the component name using runtime name and component type. @@ -73,3 +86,38 @@ func (e *CacheEngine) getRuntimeConfigFileName() string { func (e *CacheEngine) getRuntimeClassExtraConfigMapVolumeName(name string) string { return fmt.Sprintf("fluid-extra-%s-%s", e.name, name) } + +// getSecretVolumeName generates the volume name for a secret mount +func getSecretVolumeName(name string) string { + fullName := fmt.Sprintf("%s%s", secretVolumeNamePrefix, name) + // check volume name length + if len(fullName) <= validation.DNS1035LabelMaxLength { + return fullName + } + + // Case 2: Long name - truncate + hash (fallback) + // Step 1: Truncate secret to 36 chars + truncatedName := name + if len(truncatedName) > truncatedSecretMaxLength { + truncatedName = truncatedName[:truncatedSecretMaxLength] + } + + // Step 2: Generate 8-char SHA-256 hash of the ORIGINAL secret name (prevents collisions) + hash := sha256.Sum256([]byte(name)) + shortHash := hex.EncodeToString(hash[:])[:hashSuffixLength] + + // Step 3: Combine to exact 63 chars + volumeName := secretVolumeNamePrefix + truncatedName + shortHash + + return volumeName +} + +// getSecretMountPath generates the base mount path for a secret in the container +func getSecretMountPath(secretName string) string { + return fmt.Sprintf("/etc/fluid/secrets/%s", secretName) +} + +// getSecretFilePath generates the full file path for a secret key in the container +func getSecretFilePath(secretName, secretKey string) string { + return fmt.Sprintf("%s/%s", getSecretMountPath(secretName), secretKey) +} diff --git a/pkg/ddc/cache/engine/util_test.go b/pkg/ddc/cache/engine/util_test.go new file mode 100644 index 00000000000..440f0ce69f2 --- /dev/null +++ b/pkg/ddc/cache/engine/util_test.go @@ -0,0 +1,196 @@ +/* +Copyright 2026 The Fluid Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package engine + +import ( + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/util/validation" +) + +var _ = Describe("getSecretVolumeName Tests", Label("pkg.ddc.cache.engine.util_test.go"), func() { + Describe("getSecretVolumeName", func() { + Context("when secret name is short", func() { + It("should return prefix + secretName without truncation", func() { + secretName := "test-secret" + result := getSecretVolumeName(secretName) + expected := secretVolumeNamePrefix + secretName + + Expect(result).To(Equal(expected)) + Expect(len(result)).To(BeNumerically("<=", validation.DNS1035LabelMaxLength)) + }) + + It("should handle empty secret name", func() { + secretName := "" + result := getSecretVolumeName(secretName) + expected := secretVolumeNamePrefix + + Expect(result).To(Equal(expected)) + Expect(len(result)).To(BeNumerically("<=", validation.DNS1035LabelMaxLength)) + }) + + It("should handle single character secret name", func() { + secretName := "a" + result := getSecretVolumeName(secretName) + expected := secretVolumeNamePrefix + "a" + + Expect(result).To(Equal(expected)) + Expect(len(result)).To(BeNumerically("<=", validation.DNS1035LabelMaxLength)) + }) + }) + + Context("when secret name is at the boundary", func() { + It("should not truncate when total length equals DNS1035LabelMaxLength", func() { + // Calculate max secret name length that fits exactly + maxSecretLen := validation.DNS1035LabelMaxLength - prefixSecretVolumeLength + secretName := strings.Repeat("a", maxSecretLen) + result := getSecretVolumeName(secretName) + expected := secretVolumeNamePrefix + secretName + + Expect(result).To(Equal(expected)) + Expect(len(result)).To(Equal(validation.DNS1035LabelMaxLength)) + }) + + It("should truncate when total length exceeds DNS1035LabelMaxLength by 1", func() { + // Calculate secret name length that exceeds by 1 + maxSecretLen := validation.DNS1035LabelMaxLength - prefixSecretVolumeLength + 1 + secretName := strings.Repeat("a", maxSecretLen) + result := getSecretVolumeName(secretName) + + Expect(len(result)).To(BeNumerically("<=", validation.DNS1035LabelMaxLength)) + Expect(result).To(HavePrefix(secretVolumeNamePrefix)) + // Should have hash suffix + Expect(len(result)).To(Equal(validation.DNS1035LabelMaxLength)) + }) + }) + + Context("when secret name is very long", func() { + It("should truncate and add hash suffix", func() { + secretName := strings.Repeat("very-long-secret-name-", 10) // 240 chars + result := getSecretVolumeName(secretName) + + Expect(len(result)).To(BeNumerically("<=", validation.DNS1035LabelMaxLength)) + Expect(result).To(HavePrefix(secretVolumeNamePrefix)) + // Should end with 8-char hash + Expect(len(result)).To(Equal(validation.DNS1035LabelMaxLength)) + + // Verify hash part is present (last 8 chars) + hashPart := result[len(result)-hashSuffixLength:] + Expect(hashPart).To(MatchRegexp("^[0-9a-f]{8}$")) + }) + + It("should generate different hashes for different secret names with same prefix", func() { + secretName1 := strings.Repeat("a", 100) + "suffix1" + secretName2 := strings.Repeat("a", 100) + "suffix2" + + result1 := getSecretVolumeName(secretName1) + result2 := getSecretVolumeName(secretName2) + + Expect(result1).NotTo(Equal(result2)) + // Both should be valid DNS names + Expect(len(result1)).To(BeNumerically("<=", validation.DNS1035LabelMaxLength)) + Expect(len(result2)).To(BeNumerically("<=", validation.DNS1035LabelMaxLength)) + }) + + It("should handle extremely long secret names", func() { + secretName := strings.Repeat("x", 1000) + result := getSecretVolumeName(secretName) + + Expect(len(result)).To(BeNumerically("<=", validation.DNS1035LabelMaxLength)) + Expect(result).To(HavePrefix(secretVolumeNamePrefix)) + Expect(len(result)).To(Equal(validation.DNS1035LabelMaxLength)) + }) + }) + + Context("when secret name contains special characters", func() { + It("should handle secret names with hyphens", func() { + secretName := "my-test-secret-name" + result := getSecretVolumeName(secretName) + expected := secretVolumeNamePrefix + secretName + + Expect(result).To(Equal(expected)) + }) + + It("should handle secret names with numbers", func() { + secretName := "secret123" + result := getSecretVolumeName(secretName) + expected := secretVolumeNamePrefix + secretName + + Expect(result).To(Equal(expected)) + }) + + It("should handle long secret names with special characters", func() { + secretName := strings.Repeat("test-secret-123-", 10) + result := getSecretVolumeName(secretName) + + Expect(len(result)).To(BeNumerically("<=", validation.DNS1035LabelMaxLength)) + Expect(result).To(HavePrefix(secretVolumeNamePrefix)) + }) + }) + + Context("DNS label validation", func() { + It("should always generate valid DNS-1035 label names", func() { + testCases := []string{ + "short", + "medium-length-secret-name", + strings.Repeat("a", 50), + strings.Repeat("b", 100), + strings.Repeat("c", 200), + "test-with-hyphens-and-numbers-123", + } + + for _, secretName := range testCases { + result := getSecretVolumeName(secretName) + + // Check length constraint + Expect(len(result)).To(BeNumerically("<=", validation.DNS1035LabelMaxLength), + "Result length %d exceeds max %d for secret: %s", + len(result), validation.DNS1035LabelMaxLength, secretName) + + // Check starts with letter + Expect(result[0]).To(Satisfy(func(c byte) bool { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') + }), "Result should start with a letter for secret: %s", secretName) + + // Check only contains lowercase letters, numbers, and hyphens + Expect(result).To(MatchRegexp("^[a-z0-9-]+$"), + "Result contains invalid characters for secret: %s", secretName) + } + }) + }) + + Context("collision prevention", func() { + It("should generate unique volume names for different secrets that truncate to same prefix", func() { + // Two different secrets that would truncate to the same prefix + secretName1 := strings.Repeat("a", truncatedSecretMaxLength) + "different1" + secretName2 := strings.Repeat("a", truncatedSecretMaxLength) + "different2" + + result1 := getSecretVolumeName(secretName1) + result2 := getSecretVolumeName(secretName2) + + // Should be different due to hash + Expect(result1).NotTo(Equal(result2)) + + // Both should be valid length + Expect(len(result1)).To(Equal(validation.DNS1035LabelMaxLength)) + Expect(len(result2)).To(Equal(validation.DNS1035LabelMaxLength)) + }) + }) + }) +}) diff --git a/test/gha-e2e/curvine/cacheruntimeclass.yaml b/test/gha-e2e/curvine/cacheruntimeclass.yaml index 7bb01ddd049..e192ca94c34 100644 --- a/test/gha-e2e/curvine/cacheruntimeclass.yaml +++ b/test/gha-e2e/curvine/cacheruntimeclass.yaml @@ -170,6 +170,9 @@ topology: apiVersion: apps/v1 kind: DaemonSet dependencies: + # Uncomment to enable secret mount for client (e.g., for JuiceFS FUSE pods) + # secretMount: + # enabled: true extraResources: # 使用 extraResources 时,需要定义其挂载路径 configMaps: diff --git a/test/gha-e2e/curvine/dataset.yaml b/test/gha-e2e/curvine/dataset.yaml index e6f7ebe0969..f589741305e 100644 --- a/test/gha-e2e/curvine/dataset.yaml +++ b/test/gha-e2e/curvine/dataset.yaml @@ -1,11 +1,11 @@ -# apiVersion: v1 -# kind: Secret -# metadata: -# name: curvine-secret -# stringData: -# access-key: minioadmin -# secret-key: minioadmin -# --- +apiVersion: v1 +kind: Secret +metadata: + name: curvine-secret +stringData: + access-key: minioadmin + secret-key: minioadmin +--- apiVersion: data.fluid.io/v1alpha1 kind: Dataset metadata: @@ -26,5 +26,14 @@ spec: endpoint_url: "http://minio:9000" region_name: "us-east-1" path_style: "true" - access: "minioadmin" - secret: "minioadmin" + encryptOptions: + - name: access + valueFrom: + secretKeyRef: + name: curvine-secret + key: access-key + - name: secret + valueFrom: + secretKeyRef: + name: curvine-secret + key: secret-key diff --git a/test/gha-e2e/curvine/mount.yaml b/test/gha-e2e/curvine/mount.yaml index 093978af2c3..4cc1d7a903a 100644 --- a/test/gha-e2e/curvine/mount.yaml +++ b/test/gha-e2e/curvine/mount.yaml @@ -33,11 +33,30 @@ data: fi [ -z "$mountPoint" ] && { echo "mountPoint is not set or empty"; exit 1; } - [ -z "$mountPoint" ] && { echo "path is not set or empty"; exit 1; } + [ -z "$path" ] && { echo "path is not set or empty"; exit 1; } # ==================== 字段提取加p 仅输出匹配结果 ==================== - access=$(echo "$item" | sed -nE 's/.*"access":"([^"]+)".*/\1/p') - secret=$(echo "$item" | sed -nE 's/.*"secret":"([^"]+)".*/\1/p') + # Extract encryptOptions paths from the JSON + encryptOptions_raw=$(echo "$item" | sed -nE 's/.*"encryptOptions":\{([^}]+)\}.*/\1/p') + + # Extract access and secret file paths from encryptOptions + access_path="" + secret_path="" + if [ -n "$encryptOptions_raw" ]; then + access_path=$(echo "$encryptOptions_raw" | sed -nE 's/.*"access":"([^"]+)".*/\1/p') + secret_path=$(echo "$encryptOptions_raw" | sed -nE 's/.*"secret":"([^"]+)".*/\1/p') + fi + + # Read actual values from secret files if paths are provided + access="" + secret="" + if [ -n "$access_path" ] && [ -f "$access_path" ]; then + access=$(cat "$access_path") + fi + if [ -n "$secret_path" ] && [ -f "$secret_path" ]; then + secret=$(cat "$secret_path") + fi + endpoint=$(echo "$item" | sed -nE 's/.*"endpoint_url":"([^"]+)".*/\1/p') region=$(echo "$item" | sed -nE 's/.*"region_name":"([^"]+)".*/\1/p') path_style=$(echo "$item" | sed -nE 's/.*"path_style":"([^"]+)".*/\1/p')