Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"permissions": {
"allow": [
"WebFetch(domain:github.com)",
"Bash(grep:*)",
"Bash(make:*)"
]
},
"enabledMcpjsonServers": [
"playwright",
"taskmaster-ai",
"deepwiki"
]
}
24 changes: 24 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": [
"@playwright/mcp@latest",
"--headless"
]
},
"taskmaster-ai": {
"command": "npx",
"args": [
"-y",
"--package=task-master-ai",
"task-master-ai"
],
"env": {}
},
"deepwiki": {
"type": "sse",
"url": "https://mcp.deepwiki.com/sse"
}
}
}
128 changes: 128 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

This is a Container Storage Interface (CSI) driver for MooseFS, a distributed file system. The driver enables Kubernetes to provision and manage persistent volumes backed by MooseFS storage.

## Build Commands

**Compile the binary:**
```bash
make compile
```

**Build development Docker image:**
```bash
make build-dev
```

**Build production Docker image:**
```bash
make build-prod
```

**Cross-platform build with buildx:**
```bash
make dev-buildx # For development
make prod-buildx # For production
```

**Clean build artifacts:**
```bash
make clean
```

## Driver Architecture

The CSI driver operates in two distinct modes, each running as separate processes:

### Controller Service (driver/controller.go)
- Handles volume lifecycle operations: create, delete, expand
- Manages volume publishing/unpublishing to nodes
- Runs as a single instance in the cluster
- Maintains a MooseFS mount at `/mnt/controller` for volume operations
- Supports dynamic provisioning with quotas and static provisioning

### Node Service (driver/node.go)
- Handles volume mounting/unmounting on individual nodes
- Manages multiple MooseFS client connections (pool-based)
- Each node runs one instance with configurable mount point count
- Mount points: `/mnt/${nodeId}[_${mountId}]`

### Core Components

**mfsHandler (driver/mfs_handler.go):**
- Abstraction layer for MooseFS operations
- Handles mfsmount/mfsumount commands
- Manages quotas via mfsgetquota/mfssetquota
- Creates volume directories and mount volumes

**Service Interface (driver/service.go):**
- gRPC server setup and request routing
- Common driver initialization and logging
- CSI endpoint management (Unix sockets)

## Key Configuration

The driver is configured via `deploy/csi-moosefs-config.yaml`:
- `master_host`: MooseFS master server address
- `master_port`: MooseFS master port (default: 9421)
- `k8s_root_dir`: Root directory on MooseFS for all volumes
- `mount_count`: Number of pre-created MooseFS clients per node

## Volume Management

**Dynamic Provisioning:**
- Volumes created in `${k8s_root_dir}/${driver_working_dir}/volumes/`
- Quotas enforced via MooseFS quota system
- Support for volume expansion (increase only)

**Static Provisioning:**
- Mount any MooseFS directory using `mfsSubDir` parameter
- No quota management for static volumes

**Mount Volumes:**
- Special volumes for mounting existing MooseFS directories
- Created in `${k8s_root_dir}/${driver_working_dir}/mount_volumes/`

## Dependencies

- Go 1.14+
- github.com/container-storage-interface/spec v1.5.0 (updated for fsGroup support)
- google.golang.org/grpc v1.36.0
- github.com/sirupsen/logrus v1.8.0
- gopkg.in/natefinch/lumberjack.v2 v2.0.0 (log rotation)

## Deployment Structure

The driver uses two main manifests:
- `deploy/csi-moosefs-config.yaml`: Configuration values
- `deploy/csi-moosefs.yaml`: Pod specs for controller and node services

Version compatibility is maintained between Kubernetes versions and driver releases (see README).

## FSGroup Support

The driver implements full Kubernetes CSI fsGroup support to ensure proper UID/GID handling:

**Features:**
- `VOLUME_MOUNT_GROUP` node capability advertised
- `fsGroupPolicy: File` configured in CSIDriver spec
- Automatic application of fsGroup permissions during NodePublishVolume
- Volume directories get group ownership set to fsGroup with 0775 permissions

**Implementation Details:**
- fsGroup extracted from `VolumeCapability.MountVolume.VolumeMountGroup`
- Applied via `applyFSGroupPermissions()` in driver/mfs_handler.go:345
- Permissions changed using `os.Chown()` and `os.Chmod()` syscalls
- Non-fatal: permission errors logged but don't fail the mount operation

**Usage:**
Set fsGroup in Pod SecurityContext - the driver will automatically apply it:
```yaml
spec:
securityContext:
fsGroup: 1000
```
Binary file added cmd/moosefs-csi-plugin/moosefs-csi
Binary file not shown.
1 change: 1 addition & 0 deletions deploy/csi-moosefs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ metadata:
spec:
attachRequired: true
podInfoOnMount: false
fsGroupPolicy: File

---
kind: StorageClass
Expand Down
52 changes: 50 additions & 2 deletions driver/mfs_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"path"
"strconv"
"strings"
"syscall"

"gopkg.in/natefinch/lumberjack.v2"
)
Expand Down Expand Up @@ -269,17 +270,30 @@ func (mnt *mfsHandler) MountMfs() error {
}

func (mnt *mfsHandler) BindMount(mfsSource string, target string, options ...string) error {
return mnt.BindMountWithFSGroup(mfsSource, target, nil, options...)
}

func (mnt *mfsHandler) BindMountWithFSGroup(mfsSource string, target string, fsGroup *int64, options ...string) error {
mounter := Mounter{}
source := mnt.HostPathTo(mfsSource)
log.Infof("BindMount - source: %s, target: %s, options: %v", source, target, options)
log.Infof("BindMountWithFSGroup - source: %s, target: %s, fsGroup: %v, options: %v", source, target, fsGroup, options)

if isMounted, err := mounter.IsMounted(target); err != nil {
return err
} else if !isMounted {
if err := mounter.Mount(source, target, fsType, append(options, "bind")...); err != nil {
return err
}

// Apply fsGroup permissions if specified
if fsGroup != nil {
if err := mnt.applyFSGroupPermissions(source, *fsGroup); err != nil {
log.Errorf("BindMountWithFSGroup - Failed to apply fsGroup permissions: %v", err)
// Don't fail the mount for permission errors, log and continue
}
}
} else {
log.Infof("BindMount - target %s is already mounted", target)
log.Infof("BindMountWithFSGroup - target %s is already mounted", target)
}
return nil
}
Expand Down Expand Up @@ -323,3 +337,37 @@ func (mnt *mfsHandler) HostPluginDataPath() string {
func (mnt *mfsHandler) HostPathTo(to string) string {
return path.Join(mnt.hostMountPath, to)
}

// applyFSGroupPermissions applies the specified fsGroup ownership to the volume directory
// This function sets the group ownership of the volume root directory to the fsGroup
// and ensures it's group-writable (0775 permissions)
func (mnt *mfsHandler) applyFSGroupPermissions(volumePath string, fsGroup int64) error {
log.Infof("applyFSGroupPermissions - path: %s, fsGroup: %d", volumePath, fsGroup)

// Get current file info
fileInfo, err := os.Stat(volumePath)
if err != nil {
return fmt.Errorf("failed to stat volume path %s: %v", volumePath, err)
}

// Get current uid (should remain unchanged)
stat, ok := fileInfo.Sys().(*syscall.Stat_t)
if !ok {
return fmt.Errorf("failed to get syscall.Stat_t for %s", volumePath)
}
currentUID := int(stat.Uid)

// Change ownership to current uid and specified fsGroup
if err := os.Chown(volumePath, currentUID, int(fsGroup)); err != nil {
return fmt.Errorf("failed to chown %s to %d:%d: %v", volumePath, currentUID, fsGroup, err)
}

// Set permissions to 0775 (owner+group writable, others readable)
// This ensures the fsGroup can write to the volume
if err := os.Chmod(volumePath, 0775); err != nil {
return fmt.Errorf("failed to chmod %s to 0775: %v", volumePath, err)
}

log.Infof("applyFSGroupPermissions - successfully applied fsGroup %d to %s", fsGroup, volumePath)
return nil
}
16 changes: 15 additions & 1 deletion driver/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package driver
import (
"context"
"math/rand"
"strconv"

"github.com/container-storage-interface/spec/lib/go/csi"
"google.golang.org/grpc/codes"
Expand All @@ -40,6 +41,7 @@ var nodeCapabilities = []csi.NodeServiceCapability_RPC_Type{
//csi.NodeServiceCapability_RPC_GET_VOLUME_STATS,
// csi.NodeServiceCapability_RPC_VOLUME_CONDITION,
// csi.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME,
csi.NodeServiceCapability_RPC_VOLUME_MOUNT_GROUP,
}

func NewNodeService(mfsmaster string, mfsmaster_port int, rootPath, pluginDataPath, nodeId, mfsMountOptions string, mountPointsCount int) (*NodeService, error) {
Expand Down Expand Up @@ -87,10 +89,22 @@ func (ns *NodeService) NodePublishVolume(ctx context.Context, req *csi.NodePubli
if req.GetReadonly() {
options = append(options, "ro")
}

// Extract fsGroup from VolumeCapability if provided
var fsGroup *int64
if mountVol := req.VolumeCapability.GetMount(); mountVol != nil && mountVol.VolumeMountGroup != "" {
if gid, err := strconv.ParseInt(mountVol.VolumeMountGroup, 10, 64); err == nil {
fsGroup = &gid
log.Infof("NodePublishVolume - Parsed fsGroup: %d", gid)
} else {
log.Errorf("NodePublishVolume - Failed to parse fsGroup '%s': %v", mountVol.VolumeMountGroup, err)
}
}

if handler, err := ns.pickHandler(req.GetVolumeContext(), req.GetPublishContext()); err != nil {
return nil, err
} else {
if err := handler.BindMount(source, target, options...); err != nil {
if err := handler.BindMountWithFSGroup(source, target, fsGroup, options...); err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
}
Expand Down
64 changes: 64 additions & 0 deletions examples/fsgroup-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# FSGroup Support Test

This example demonstrates the MooseFS CSI driver's fsGroup support, which automatically applies proper UID/GID permissions to volumes.

## What This Tests

- Pod running as non-root user (UID 1000)
- fsGroup set to 1000 in Pod SecurityContext
- CSI driver automatically applies group ownership to volume
- Container can write to the volume without permission errors

## Usage

1. Apply the PVC:
```bash
kubectl apply -f pvc.yaml
```

2. Apply the Pod:
```bash
kubectl apply -f pod.yaml
```

3. Verify the Pod can write to the volume:
```bash
kubectl exec test-fsgroup-pod -- touch /data/test-file
kubectl exec test-fsgroup-pod -- ls -la /data/
```

## Expected Results

- The volume directory should have group ownership set to 1000
- The container running as UID 1000 should be able to create files
- Files created should be owned by UID 1000 and GID 1000

## Verification Commands

Check permissions on the volume:
```bash
kubectl exec test-fsgroup-pod -- ls -ld /data
```

Test write access:
```bash
kubectl exec test-fsgroup-pod -- echo "Hello World" > /data/test.txt
kubectl exec test-fsgroup-pod -- cat /data/test.txt
```

## Cleanup

```bash
kubectl delete -f pod.yaml
kubectl delete -f pvc.yaml
```

## How It Works

1. **CSI Driver Configuration**: `fsGroupPolicy: File` in CSIDriver spec enables fsGroup support
2. **Node Capability**: `VOLUME_MOUNT_GROUP` capability tells Kubernetes the driver handles fsGroup
3. **Permission Application**: During `NodePublishVolume`, the driver:
- Extracts fsGroup from `VolumeCapability.MountVolume.VolumeMountGroup`
- Calls `applyFSGroupPermissions()` to set group ownership
- Changes directory permissions to 0775 (group writable)
4. **Result**: Pod processes can write to the volume even when running as non-root
22 changes: 22 additions & 0 deletions examples/fsgroup-test/pod.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
apiVersion: v1
kind: Pod
metadata:
name: test-fsgroup-pod
spec:
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: test-container
image: busybox:latest
command: ["sleep", "3600"]
volumeMounts:
- name: test-volume
mountPath: /data
securityContext:
allowPrivilegeEscalation: false
volumes:
- name: test-volume
persistentVolumeClaim:
claimName: test-fsgroup-pvc
Loading