Skip to content

Optimize workspace root mount validation#746

Open
mashenjun wants to merge 1 commit into
mainfrom
optimize/csi-root-stat-validation
Open

Optimize workspace root mount validation#746
mashenjun wants to merge 1 commit into
mainfrom
optimize/csi-root-stat-validation

Conversation

@mashenjun

@mashenjun mashenjun commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • validate workspace-root mounts with Stat/HEAD instead of listing every root entry
  • fall back to one List request only when HEAD returns 404 or 405 for legacy server compatibility
  • cover the fast path, bounded fallback, authentication/server failures, non-directory roots, and fallback failures

Why

CSI workspace-root mounts only need to verify that the remote root is a directory. Listing and decoding every root entry makes startup work grow with root size, while the server already provides a constant-size root HEAD response.

Testing

  • go test -race ./pkg/fuse -run ^TestValidateWorkspaceRootRequestBehavior$ -count=1

Compatibility

  • no CLI flag or API contract changes
  • supported servers use HEAD only
  • legacy servers without root HEAD support retain a single List fallback

Summary by CodeRabbit

  • Bug Fixes
    • Improved validation when mounting a workspace configured at the remote root.
    • Mounting now distinguishes between missing or unsupported root checks and genuine authentication or server errors.
    • Added clearer error reporting when the remote root is unavailable or is not a directory.
    • Improved fallback connectivity checks for servers that do not support direct root inspection.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Mount-time validation for a remote root of / now checks directory status with Stat, conditionally falls back to List, and reports specific errors. Tests verify HTTP request sequencing, status handling, directory validation, and fallback failures.

Changes

Workspace root validation

Layer / File(s) Summary
Workspace root validation logic
pkg/fuse/mount.go
validateWorkspaceRoot validates that / is a directory, permits 404 and 405 responses to fall back to List("/"), and returns specific validation errors.
Mount integration and request behavior
pkg/fuse/mount.go, pkg/fuse/mount_remote_root_test.go
Mount uses the validator for /; tests verify HEAD/GET sequencing, authentication and server failures, directory checks, and fallback error propagation.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • mem9-ai/drive9#361: Both changes update mount-time validation for the / remote-root case with directory checks and conditional listing fallback.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: optimizing workspace root mount validation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch optimize/csi-root-stat-validation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/fuse/mount_remote_root_test.go`:
- Around line 104-109: Replace t.Fatalf with t.Errorf in the
validateWorkspaceRoot error assertions at pkg/fuse/mount_remote_root_test.go
lines 104-109, and in the per-request-element assertions at lines 116-120, so
each test case reports all mismatches instead of stopping at the first failure.

In `@pkg/fuse/mount.go`:
- Around line 207-225: Update validateWorkspaceRoot to accept context.Context,
use c.StatCtx and c.ListCtx with the context as the first argument, and invert
the error check while preserving existing validation behavior. Update the call
sites in pkg/fuse/mount.go:285 and pkg/fuse/mount_remote_root_test.go:103 to
pass context.Background() as the first argument.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f2c4a8a8-19e7-42a9-9d6b-42bfe38d8241

📥 Commits

Reviewing files that changed from the base of the PR and between 4691b21 and 96145c3.

📒 Files selected for processing (2)
  • pkg/fuse/mount.go
  • pkg/fuse/mount_remote_root_test.go

Comment on lines +104 to +109
if tt.wantErr == "" && err != nil {
t.Fatalf("validateWorkspaceRoot() error = %v, want nil", err)
}
if tt.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tt.wantErr)) {
t.Fatalf("validateWorkspaceRoot() error = %v, want containing %q", err, tt.wantErr)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use t.Errorf for assertion failures.

As per coding guidelines, use t.Errorf instead of t.Fatalf for assertion failures so that the test can report multiple issues rather than halting at the first mismatch.

  • pkg/fuse/mount_remote_root_test.go#L104-L109: Replace t.Fatalf with t.Errorf when asserting the returned error.
  • pkg/fuse/mount_remote_root_test.go#L116-L120: Replace t.Fatalf with t.Errorf when checking each request element in the loop.
🛠️ Proposed fixes

For lines 104-109:

-			if tt.wantErr == "" && err != nil {
-				t.Fatalf("validateWorkspaceRoot() error = %v, want nil", err)
-			}
-			if tt.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tt.wantErr)) {
-				t.Fatalf("validateWorkspaceRoot() error = %v, want containing %q", err, tt.wantErr)
-			}
+			if tt.wantErr == "" && err != nil {
+				t.Errorf("validateWorkspaceRoot() error = %v, want nil", err)
+			}
+			if tt.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tt.wantErr)) {
+				t.Errorf("validateWorkspaceRoot() error = %v, want containing %q", err, tt.wantErr)
+			}

For lines 116-120:

-			for i := range tt.wantRequests {
-				if requests[i] != tt.wantRequests[i] {
-					t.Fatalf("requests[%d] = %q, want %q", i, requests[i], tt.wantRequests[i])
-				}
-			}
+			for i := range tt.wantRequests {
+				if requests[i] != tt.wantRequests[i] {
+					t.Errorf("requests[%d] = %q, want %q", i, requests[i], tt.wantRequests[i])
+				}
+			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if tt.wantErr == "" && err != nil {
t.Fatalf("validateWorkspaceRoot() error = %v, want nil", err)
}
if tt.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tt.wantErr)) {
t.Fatalf("validateWorkspaceRoot() error = %v, want containing %q", err, tt.wantErr)
}
if tt.wantErr == "" && err != nil {
t.Errorf("validateWorkspaceRoot() error = %v, want nil", err)
}
if tt.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tt.wantErr)) {
t.Errorf("validateWorkspaceRoot() error = %v, want containing %q", err, tt.wantErr)
}
Suggested change
if tt.wantErr == "" && err != nil {
t.Fatalf("validateWorkspaceRoot() error = %v, want nil", err)
}
if tt.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tt.wantErr)) {
t.Fatalf("validateWorkspaceRoot() error = %v, want containing %q", err, tt.wantErr)
}
for i := range tt.wantRequests {
if requests[i] != tt.wantRequests[i] {
t.Errorf("requests[%d] = %q, want %q", i, requests[i], tt.wantRequests[i])
}
}
📍 Affects 1 file
  • pkg/fuse/mount_remote_root_test.go#L104-L109 (this comment)
  • pkg/fuse/mount_remote_root_test.go#L116-L120
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/fuse/mount_remote_root_test.go` around lines 104 - 109, Replace t.Fatalf
with t.Errorf in the validateWorkspaceRoot error assertions at
pkg/fuse/mount_remote_root_test.go lines 104-109, and in the per-request-element
assertions at lines 116-120, so each test case reports all mismatches instead of
stopping at the first failure.

Source: Coding guidelines

Comment thread pkg/fuse/mount.go
Comment on lines +207 to +225
func validateWorkspaceRoot(c *client.Client) error {
stat, err := c.Stat("/")
if err == nil {
if !stat.IsDir {
return fmt.Errorf("remote root %q is not a directory", "/")
}
return nil
}

var statusErr *client.StatusError
if !errors.As(err, &statusErr) ||
(statusErr.StatusCode != http.StatusNotFound && statusErr.StatusCode != http.StatusMethodNotAllowed) {
return fmt.Errorf("cannot reach drive9 server: %w", err)
}
if _, listErr := c.List("/"); listErr != nil {
return fmt.Errorf("cannot reach drive9 server: %w", listErr)
}
return nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Pass context.Context to I/O calls and use idiomatic error checking.

As per coding guidelines, pass context.Context as the first parameter to all I/O calls. validateWorkspaceRoot performs HTTP requests, so it must accept and propagate a context (using c.StatCtx and c.ListCtx). This also presents an opportunity to refactor the unidiomatic if err == nil block.

  • pkg/fuse/mount.go#L207-L225: Update signature to func validateWorkspaceRoot(ctx context.Context, c *client.Client) error, switch to Ctx client methods, and invert the error check for better line of sight.
  • pkg/fuse/mount.go#L285-L285: Pass context.Background() as the first argument.
  • pkg/fuse/mount_remote_root_test.go#L103-L103: Pass context.Background() as the first argument.
🛠️ Proposed refactor for `validateWorkspaceRoot`
-func validateWorkspaceRoot(c *client.Client) error {
-	stat, err := c.Stat("/")
-	if err == nil {
-		if !stat.IsDir {
-			return fmt.Errorf("remote root %q is not a directory", "/")
-		}
-		return nil
-	}
-
-	var statusErr *client.StatusError
-	if !errors.As(err, &statusErr) ||
-		(statusErr.StatusCode != http.StatusNotFound && statusErr.StatusCode != http.StatusMethodNotAllowed) {
-		return fmt.Errorf("cannot reach drive9 server: %w", err)
-	}
-	if _, listErr := c.List("/"); listErr != nil {
-		return fmt.Errorf("cannot reach drive9 server: %w", listErr)
-	}
-	return nil
+func validateWorkspaceRoot(ctx context.Context, c *client.Client) error {
+	stat, err := c.StatCtx(ctx, "/")
+	if err != nil {
+		var statusErr *client.StatusError
+		if !errors.As(err, &statusErr) ||
+			(statusErr.StatusCode != http.StatusNotFound && statusErr.StatusCode != http.StatusMethodNotAllowed) {
+			return fmt.Errorf("cannot reach drive9 server: %w", err)
+		}
+		if _, listErr := c.ListCtx(ctx, "/"); listErr != nil {
+			return fmt.Errorf("cannot reach drive9 server: %w", listErr)
+		}
+		return nil
+	}
+
+	if !stat.IsDir {
+		return fmt.Errorf("remote root %q is not a directory", "/")
+	}
+	return nil
 }
📍 Affects 2 files
  • pkg/fuse/mount.go#L207-L225 (this comment)
  • pkg/fuse/mount.go#L285-L285
  • pkg/fuse/mount_remote_root_test.go#L103-L103
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/fuse/mount.go` around lines 207 - 225, Update validateWorkspaceRoot to
accept context.Context, use c.StatCtx and c.ListCtx with the context as the
first argument, and invert the error check while preserving existing validation
behavior. Update the call sites in pkg/fuse/mount.go:285 and
pkg/fuse/mount_remote_root_test.go:103 to pass context.Background() as the first
argument.

Source: Coding guidelines

@qiffang qiffang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GREEN at 96145c3

Clean, well-scoped optimization. Reviewed against mandatory checklist:

Correctness:

  • validateWorkspaceRoot tries Stat("/") (HEAD) first — O(1) check vs previous List("/") which decoded every root entry
  • On success, validates IsDir — correct guard against non-directory roots (new coverage)
  • Fallback to List("/") only on 404/405 — matches legacy server compat requirement
  • Auth failures (401), server errors (500) propagate immediately without fallback — correct, avoids masking real errors

Edge cases verified:

Case Behavior
Stat succeeds, root is dir Pass, 1 request
Stat succeeds, root is NOT dir Error with clear message, 1 request
Stat 404 (legacy server) Falls back to List, 2 requests max
Stat 405 (method not allowed) Falls back to List, 2 requests max
Stat 401 (auth failure) Error immediately, no fallback
Stat 500 (server error) Error immediately, no fallback
List fallback also fails Returns List error

Test coverage:

  • 7 table-driven subtests cover all paths above
  • Request recording verifies exact request count and order (no extra requests)
  • Uses httptest.Server with proper sync.Mutex for request recording

No issues found. No CLI flag or API contract changes. Backward compatible — legacy servers get one additional HEAD before the existing List.

@qiffang qiffang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adversary-2 review — PR #746

Verdict: GREEN

The change is correct, minimal, and well-tested. Stat/HEAD replaces List for workspace-root validation; fallback to List is bounded to 404/405 only. 7 test cases cover the decision tree exhaustively. No API contract change.

Minor observations (not blocking):

1. readError on HEAD responses returns empty-body message
readError calls io.ReadAll(resp.Body) on HEAD responses which have no body. This falls through to StatusError{Message: "HTTP 404: "} (note trailing : ). The function still works correctly — errors.As succeeds and StatusCode is populated — but the Message field has a cosmetic trailing : artifact. Not a regression since this existed before and Stat was already used for non-root paths.

2. Test uses sync.Mutex for request recording — correct
The httptest.Server handler runs in a goroutine, so the mutex is necessary. Verified the lock is held during both writes and the final assertion read.

3. headIsDir: false default is intentional
In the "root must be a directory" test case, headIsDir defaults to false (Go zero value), which correctly triggers the non-directory error. The test name makes this clear.

No edge case gaps. No hidden assumptions. Tests verify request sequences, not just outcomes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants