Skip to content

Commit ef35fba

Browse files
committed
feat: add comprehensive mount pod configuration system with DaemonSet support
- Implement three mount modes: per-pvc, shared-pod, and daemonset - Add ConfigMap-based configuration for per-StorageClass mount settings - Support DaemonSet deployment with flexible nodeAffinity configuration - Add automatic fallback from DaemonSet to shared pod when node scheduling fails - Rename daemonset_config to mount_config for clarity - Add node compatibility checking before DaemonSet scheduling - Include comprehensive tests for mount selection and fallback behavior - Update documentation with configuration examples and best practices This allows users to optimize mount pod deployment based on their specific needs without modifying existing StorageClasses, with automatic fallback ensuring workloads never fail due to mount pod scheduling issues.
1 parent bcdffd7 commit ef35fba

File tree

9 files changed

+311
-49
lines changed

9 files changed

+311
-49
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
apiVersion: v1
22
kind: ConfigMap
33
metadata:
4-
name: juicefs-daemonset-config
4+
name: juicefs-mount-config
55
namespace: kube-system
66
data:
7-
# Default configuration for all StorageClasses (if no specific config exists)
7+
# Default configuration for all StorageClasses
88
default: |
99
nodeAffinity:
1010
requiredDuringSchedulingIgnoredDuringExecution:
1111
nodeSelectorTerms:
1212
- matchExpressions:
1313
- key: node-role.kubernetes.io/control-plane
1414
operator: DoesNotExist
15-
16-
# Specific configuration for a StorageClass named "juicefs-sc"
15+
16+
# StorageClass "juicefs-sc" configuration
1717
juicefs-sc: |
1818
nodeAffinity:
1919
requiredDuringSchedulingIgnoredDuringExecution:
@@ -23,16 +23,3 @@ data:
2323
operator: In
2424
values:
2525
- "true"
26-
27-
# Another StorageClass with different affinity
28-
juicefs-sc-performance: |
29-
nodeAffinity:
30-
preferredDuringSchedulingIgnoredDuringExecution:
31-
- weight: 100
32-
preference:
33-
matchExpressions:
34-
- key: node.kubernetes.io/instance-type
35-
operator: In
36-
values:
37-
- m5.xlarge
38-
- m5.2xlarge

docs/en/guide/daemonset-mount.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Create a ConfigMap to define node affinity for your StorageClasses without modif
3838
apiVersion: v1
3939
kind: ConfigMap
4040
metadata:
41-
name: juicefs-daemonset-config
41+
name: juicefs-mount-config
4242
namespace: kube-system
4343
data:
4444
# Default configuration for all StorageClasses

docs/en/guide/mount-pod-configuration.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ data:
5757
- "true"
5858
```
5959
60+
## Automatic Fallback Behavior
61+
62+
When using DaemonSet mode, the CSI Driver automatically falls back to shared pod mode if:
63+
64+
1. The DaemonSet cannot schedule a pod on the node due to nodeAffinity restrictions
65+
2. The node has taints that prevent the DaemonSet pod from being scheduled
66+
3. The DaemonSet pod fails to become ready within the timeout period
67+
68+
This ensures that workloads can still mount volumes even if the DaemonSet configuration prevents pods from running on certain nodes.
69+
6070
## Mount Modes Explained
6171
6272
### Per-PVC Mode

pkg/config/mount_config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import (
3030

3131
const (
3232
MountConfigMapName = "juicefs-mount-config"
33-
// Use the same DefaultConfigKey from daemonset_config.go
33+
DefaultConfigKey = "default"
3434
)
3535

3636
// MountMode defines how mount pods are deployed

pkg/config/daemonset_config.go renamed to pkg/config/mount_config_helper.go

Lines changed: 20 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,65 +28,57 @@ import (
2828
"github.com/juicedata/juicefs-csi-driver/pkg/k8sclient"
2929
)
3030

31-
const (
32-
DaemonSetConfigMapName = "juicefs-daemonset-config"
33-
DefaultConfigKey = "default"
34-
)
35-
36-
// DaemonSetConfig represents the configuration for a DaemonSet deployment
37-
type DaemonSetConfig struct {
38-
Enabled *bool `yaml:"enabled,omitempty"` // Explicitly enable/disable DaemonSet for this StorageClass
39-
NodeAffinity *corev1.NodeAffinity `yaml:"nodeAffinity,omitempty"`
40-
}
31+
// Helper functions for DaemonSet node affinity configuration
32+
// The main MountConfig struct is defined in mount_config.go
4133

42-
// GetDaemonSetConfig retrieves the DaemonSet configuration for a given StorageClass
34+
// GetDaemonSetNodeAffinity retrieves the DaemonSet node affinity configuration for a given StorageClass
4335
// It first checks for a StorageClass-specific configuration, then falls back to default
44-
func GetDaemonSetConfig(ctx context.Context, client *k8sclient.K8sClient, storageClassName string) (*corev1.NodeAffinity, error) {
45-
log := klog.NewKlogr().WithName("daemonset-config")
36+
func GetDaemonSetNodeAffinity(ctx context.Context, client *k8sclient.K8sClient, storageClassName string) (*corev1.NodeAffinity, error) {
37+
log := klog.NewKlogr().WithName("mount-config")
4638

4739
// Try to get the ConfigMap
48-
configMap, err := client.GetConfigMap(ctx, DaemonSetConfigMapName, Namespace)
40+
configMap, err := client.GetConfigMap(ctx, MountConfigMapName, Namespace)
4941
if err != nil {
5042
if k8serrors.IsNotFound(err) {
51-
log.V(1).Info("DaemonSet ConfigMap not found, using no node affinity",
52-
"configMap", DaemonSetConfigMapName, "namespace", Namespace)
43+
log.V(1).Info("Mount ConfigMap not found, using no node affinity",
44+
"configMap", MountConfigMapName, "namespace", Namespace)
5345
return nil, nil // No ConfigMap means no node affinity restrictions
5446
}
55-
return nil, fmt.Errorf("failed to get DaemonSet ConfigMap: %v", err)
47+
return nil, fmt.Errorf("failed to get Mount ConfigMap: %v", err)
5648
}
5749

5850
// Try to get StorageClass-specific configuration
5951
if configData, exists := configMap.Data[storageClassName]; exists {
60-
log.V(1).Info("Found StorageClass-specific DaemonSet configuration",
52+
log.V(1).Info("Found StorageClass-specific mount configuration",
6153
"storageClass", storageClassName)
62-
return parseDaemonSetConfig(configData)
54+
return parseDaemonSetNodeAffinity(configData)
6355
}
6456

6557
// Fall back to default configuration
6658
if configData, exists := configMap.Data[DefaultConfigKey]; exists {
67-
log.V(1).Info("Using default DaemonSet configuration for StorageClass",
59+
log.V(1).Info("Using default mount configuration for StorageClass",
6860
"storageClass", storageClassName)
69-
return parseDaemonSetConfig(configData)
61+
return parseDaemonSetNodeAffinity(configData)
7062
}
7163

72-
log.V(1).Info("No DaemonSet configuration found for StorageClass",
64+
log.V(1).Info("No mount configuration found for StorageClass",
7365
"storageClass", storageClassName)
7466
return nil, nil
7567
}
7668

77-
// parseDaemonSetConfig parses the configuration string into a NodeAffinity
78-
func parseDaemonSetConfig(configData string) (*corev1.NodeAffinity, error) {
79-
config := &DaemonSetConfig{}
69+
// parseDaemonSetNodeAffinity parses the configuration string to extract NodeAffinity
70+
func parseDaemonSetNodeAffinity(configData string) (*corev1.NodeAffinity, error) {
71+
config := &MountConfig{}
8072
if err := yaml.Unmarshal([]byte(configData), config); err != nil {
81-
return nil, fmt.Errorf("failed to parse DaemonSet configuration: %v", err)
73+
return nil, fmt.Errorf("failed to parse mount configuration: %v", err)
8274
}
8375
return config.NodeAffinity, nil
8476
}
8577

8678
// LoadDaemonSetNodeAffinity loads node affinity for a StorageClass from ConfigMap
8779
// This is called when creating or updating a DaemonSet for mount pods
8880
func LoadDaemonSetNodeAffinity(ctx context.Context, client *k8sclient.K8sClient, jfsSetting *JfsSetting) error {
89-
log := klog.NewKlogr().WithName("daemonset-config")
81+
log := klog.NewKlogr().WithName("mount-config")
9082

9183
// Skip if not using DaemonSet deployment
9284
if !StorageClassShareMount || !StorageClassDaemonSet {
@@ -109,7 +101,7 @@ func LoadDaemonSetNodeAffinity(ctx context.Context, client *k8sclient.K8sClient,
109101
storageClassName = jfsSetting.UniqueId
110102
}
111103

112-
nodeAffinity, err := GetDaemonSetConfig(ctx, client, storageClassName)
104+
nodeAffinity, err := GetDaemonSetNodeAffinity(ctx, client, storageClassName)
113105
if err != nil {
114106
log.Error(err, "Failed to get DaemonSet configuration",
115107
"storageClass", storageClassName)

pkg/juicefs/mount/daemonset_mount.go

Lines changed: 184 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"strings"
2323
"time"
2424

25+
corev1 "k8s.io/api/core/v1"
2526
k8serrors "k8s.io/apimachinery/pkg/api/errors"
2627
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2728
"k8s.io/apimachinery/pkg/util/uuid"
@@ -37,6 +38,23 @@ import (
3738
"github.com/juicedata/juicefs-csi-driver/pkg/util/resource"
3839
)
3940

41+
// DaemonSetSchedulingError indicates that a DaemonSet cannot schedule on a specific node
42+
type DaemonSetSchedulingError struct {
43+
DaemonSetName string
44+
NodeName string
45+
Message string
46+
}
47+
48+
func (e *DaemonSetSchedulingError) Error() string {
49+
return e.Message
50+
}
51+
52+
// IsDaemonSetSchedulingError checks if the error is a DaemonSet scheduling error
53+
func IsDaemonSetSchedulingError(err error) bool {
54+
_, ok := err.(*DaemonSetSchedulingError)
55+
return ok
56+
}
57+
4058
type DaemonSetMount struct {
4159
log klog.Logger
4260
k8sMount.SafeFormatAndMount
@@ -294,6 +312,25 @@ func (d *DaemonSetMount) createOrUpdateDaemonSet(ctx context.Context, dsName str
294312
func (d *DaemonSetMount) waitUntilDaemonSetReady(ctx context.Context, dsName string, jfsSetting *jfsConfig.JfsSetting) error {
295313
log := util.GenLog(ctx, d.log, "waitUntilDaemonSetReady")
296314

315+
// First, check if the DaemonSet can schedule a pod on this node
316+
canSchedule, err := d.canScheduleOnNode(ctx, dsName)
317+
if err != nil {
318+
log.Error(err, "Failed to check if DaemonSet can schedule on node")
319+
// Continue anyway, might be a transient error
320+
}
321+
322+
if !canSchedule {
323+
// DaemonSet cannot schedule on this node due to nodeAffinity
324+
// Return a specific error that can be handled by the caller
325+
log.Info("DaemonSet cannot schedule on this node due to nodeAffinity, need fallback",
326+
"dsName", dsName, "nodeName", jfsConfig.NodeName)
327+
return &DaemonSetSchedulingError{
328+
DaemonSetName: dsName,
329+
NodeName: jfsConfig.NodeName,
330+
Message: "DaemonSet cannot schedule on this node due to nodeAffinity restrictions",
331+
}
332+
}
333+
297334
// Wait for DaemonSet to have pods ready on current node
298335
timeout := 5 * time.Minute
299336
waitCtx, cancel := context.WithTimeout(ctx, timeout)
@@ -302,7 +339,12 @@ func (d *DaemonSetMount) waitUntilDaemonSetReady(ctx context.Context, dsName str
302339
for {
303340
select {
304341
case <-waitCtx.Done():
305-
return fmt.Errorf("timeout waiting for DaemonSet %s to be ready on node %s", dsName, jfsConfig.NodeName)
342+
// Timeout - could be because pod cannot be scheduled on this node
343+
return &DaemonSetSchedulingError{
344+
DaemonSetName: dsName,
345+
NodeName: jfsConfig.NodeName,
346+
Message: fmt.Sprintf("timeout waiting for DaemonSet pod to be ready on node %s", jfsConfig.NodeName),
347+
}
306348
default:
307349
ds, err := d.K8sClient.GetDaemonSet(waitCtx, dsName, jfsConfig.Namespace)
308350
if err != nil {
@@ -416,4 +458,145 @@ func (d *DaemonSetMount) getDaemonSetNameFromTarget(ctx context.Context, target
416458
}
417459

418460
return ""
461+
}
462+
463+
// canScheduleOnNode checks if a DaemonSet can schedule a pod on the current node
464+
func (d *DaemonSetMount) canScheduleOnNode(ctx context.Context, dsName string) (bool, error) {
465+
log := util.GenLog(ctx, d.log, "canScheduleOnNode")
466+
467+
// Get the DaemonSet
468+
ds, err := d.K8sClient.GetDaemonSet(ctx, dsName, jfsConfig.Namespace)
469+
if err != nil {
470+
return false, err
471+
}
472+
473+
// Get the current node
474+
node, err := d.K8sClient.GetNode(ctx, jfsConfig.NodeName)
475+
if err != nil {
476+
log.Error(err, "Failed to get node", "nodeName", jfsConfig.NodeName)
477+
return false, err
478+
}
479+
480+
// Check if the node matches the DaemonSet's nodeAffinity
481+
if ds.Spec.Template.Spec.Affinity != nil && ds.Spec.Template.Spec.Affinity.NodeAffinity != nil {
482+
nodeAffinity := ds.Spec.Template.Spec.Affinity.NodeAffinity
483+
484+
// Check required node affinity
485+
if nodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution != nil {
486+
matches := false
487+
for _, term := range nodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms {
488+
if nodeMatchesSelectorTerm(node, &term) {
489+
matches = true
490+
break
491+
}
492+
}
493+
if !matches {
494+
log.Info("Node does not match DaemonSet's required node affinity",
495+
"nodeName", jfsConfig.NodeName, "dsName", dsName)
496+
return false, nil
497+
}
498+
}
499+
}
500+
501+
// Check if the node has any taints that would prevent scheduling
502+
// (This is a simplified check - a full implementation would need to check tolerations)
503+
if len(node.Spec.Taints) > 0 && len(ds.Spec.Template.Spec.Tolerations) == 0 {
504+
for _, taint := range node.Spec.Taints {
505+
if taint.Effect == corev1.TaintEffectNoSchedule || taint.Effect == corev1.TaintEffectNoExecute {
506+
log.Info("Node has taints that prevent scheduling",
507+
"nodeName", jfsConfig.NodeName, "taint", taint)
508+
return false, nil
509+
}
510+
}
511+
}
512+
513+
return true, nil
514+
}
515+
516+
// nodeMatchesSelectorTerm checks if a node matches a node selector term
517+
func nodeMatchesSelectorTerm(node *corev1.Node, term *corev1.NodeSelectorTerm) bool {
518+
// Check match expressions
519+
for _, expr := range term.MatchExpressions {
520+
if !nodeMatchesExpression(node, &expr) {
521+
return false
522+
}
523+
}
524+
525+
// Check match fields
526+
for _, field := range term.MatchFields {
527+
if !nodeMatchesFieldSelector(node, &field) {
528+
return false
529+
}
530+
}
531+
532+
return true
533+
}
534+
535+
// nodeMatchesExpression checks if a node matches a label selector requirement
536+
func nodeMatchesExpression(node *corev1.Node, expr *corev1.NodeSelectorRequirement) bool {
537+
value, exists := node.Labels[expr.Key]
538+
539+
switch expr.Operator {
540+
case corev1.NodeSelectorOpIn:
541+
if !exists {
542+
return false
543+
}
544+
for _, v := range expr.Values {
545+
if value == v {
546+
return true
547+
}
548+
}
549+
return false
550+
case corev1.NodeSelectorOpNotIn:
551+
if !exists {
552+
return true
553+
}
554+
for _, v := range expr.Values {
555+
if value == v {
556+
return false
557+
}
558+
}
559+
return true
560+
case corev1.NodeSelectorOpExists:
561+
return exists
562+
case corev1.NodeSelectorOpDoesNotExist:
563+
return !exists
564+
case corev1.NodeSelectorOpGt, corev1.NodeSelectorOpLt:
565+
// These operators are typically used for numeric comparisons
566+
// For simplicity, we're not implementing them here
567+
return true
568+
default:
569+
return false
570+
}
571+
}
572+
573+
// nodeMatchesFieldSelector checks if a node matches a field selector
574+
func nodeMatchesFieldSelector(node *corev1.Node, field *corev1.NodeSelectorRequirement) bool {
575+
var value string
576+
switch field.Key {
577+
case "metadata.name":
578+
value = node.Name
579+
// Add more field selectors as needed
580+
default:
581+
return false
582+
}
583+
584+
switch field.Operator {
585+
case corev1.NodeSelectorOpIn:
586+
for _, v := range field.Values {
587+
if value == v {
588+
return true
589+
}
590+
}
591+
return false
592+
case corev1.NodeSelectorOpNotIn:
593+
for _, v := range field.Values {
594+
if value == v {
595+
return false
596+
}
597+
}
598+
return true
599+
default:
600+
return false
601+
}
419602
}

0 commit comments

Comments
 (0)