Skip to content

Clarify storage descriptor and normalization contracts#383

Merged
AptS-1547 merged 3 commits into
masterfrom
feat/issue-372
Jul 5, 2026
Merged

Clarify storage descriptor and normalization contracts#383
AptS-1547 merged 3 commits into
masterfrom
feat/issue-372

Conversation

@AptS-1738

@AptS-1738 AptS-1738 commented Jul 5, 2026

Copy link
Copy Markdown
Member

Summary

  • add shared storage field semantics and normalization helpers
  • reuse the helpers from storage policy and managed ingress normalization paths
  • document descriptor-driven storage UI contracts in English and link the existing Chinese contract
  • align remote target form validation with backend descriptor field metadata

Verification

  • cargo test --lib storage::field_contract
  • cargo test --lib remote_storage_target_service::tests::normalize
  • cargo test --lib storage::connectors
  • cargo test --lib max_file_size_at_service_boundary
  • cargo check
  • bun run test -- RemoteNodeRemoteStorageTargetSection remoteStorageTargetDialogShared
  • bunx biome check src/components/admin/admin-remote-nodes-page/RemoteNodeRemoteStorageTargetSection.tsx src/components/admin/admin-remote-nodes-page/RemoteNodeRemoteStorageTargetForm.tsx

Closes #372

Summary by CodeRabbit

  • 新功能
    • 新增“存储字段契约”文档,补充驱动字段作用域、归一化规则及密钥保留/保守降级策略,并更新开发入口。
    • 远程存储目标驱动字段增加对相对本地路径的校验信息(用于更准确的输入约束)。
  • Bug 修复
    • 编辑远程存储目标时,secret 仅在“驱动未变且字段标记允许”时保留;placeholder 与校验提示同步调整。
    • 表单校验改为基于驱动字段定义计算必填与路径校验,更一致的错误提示。
    • 存储策略 max_file_size 创建与更新均拒绝负值,并覆盖边界校验。

Add comprehensive documentation and shared normalization utilities for storage descriptor fields across storage policies and remote storage targets.

**Documentation:**
- Add storage descriptor and field normalization contract document (EN/ZH-CN)
- Document descriptor separation rationale and normalization boundaries
- Add descriptor rules, normalization rules, implementation boundaries, and test guidelines
- Update developer docs index to reference new contract document

**Shared field semantics:**
- Add `src/storage/field_contract.rs` with shared field kind, semantics, and normalization helpers
- Add `StorageDescriptorFieldSemantics` to unify required/secret/boolean field contracts
- Add `normalize_required_storage_field` for trimming and blank validation
- Add `preserve_secret_when_omitted` for secret edit-flow preservation
- Add `normalize_object_storage_prefix` for S3/object-storage prefix normalization
- Add `normalize_storage_policy_max_file_size` with negative value rejection
- Add `normalize_relative_local_target_path` with escape-segment detection
- Add comprehensive unit tests for all normalization helpers

**Backend integration:**
- Integrate field contract into storage connector and remote target driver descriptors
- Apply `max_file_size` normalization in storage policy create/update services
- Replace inline `normalize_non_blank` with shared `normalize_required_storage_field`
- Apply `normalize_object_storage_prefix` in S3 remote target driver
- Apply `preserve_secret_when_omitted` in remote target update flows
- Add service-level tests for negative `max_file_size` rejection

**Frontend integration:**
- Update remote storage target form to use descriptor-based secret preservation logic
- Replace hardcoded `driver_type === "s3"` checks with `secretKeyField?.secret === true`
- Update validation to check `basePathField?.required` and `basePathField?.help_key` semantics
- Simplify credential preservation check to generic same-driver-type + secret-field pattern
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9d327014-e81f-4cb4-b39f-45e78e97914a

📥 Commits

Reviewing files that changed from the base of the PR and between 179b0d3 and 854671a.

📒 Files selected for processing (4)
  • frontend-panel/src/components/admin/admin-remote-nodes-page/RemoteNodeRemoteStorageTargetSection.test.tsx
  • frontend-panel/src/components/admin/admin-remote-nodes-page/RemoteNodeRemoteStorageTargetSection.tsx
  • src/services/remote_storage_target_service/driver.rs
  • src/services/remote_storage_target_service/tests.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/services/remote_storage_target_service/tests.rs

📝 Walkthrough

Walkthrough

新增共享存储字段契约与归一化 helper,后端 descriptor、远程存储目标、storage policy 和前端表单都改为读取这些契约与验证元数据,并补充了对应文档与测试。

Changes

字段契约与归一化统一

Layer / File(s) Summary
字段契约核心类型与纯函数
src/storage/field_contract.rs, src/storage/mod.rs
新增字段语义类型与必填、secret、前缀、最大文件大小、相对路径的归一化/校验函数,并声明为 crate 内部模块且补充单元测试。
Connector descriptor 接入字段语义
src/storage/connector_descriptor.rs
字段类型枚举建立双向映射,字段构造改为由共享语义对象派生 kind/required/secret。
远程存储目标服务字段归一化重构
src/services/remote_storage_target_service/driver.rs, src/services/remote_storage_target_service/normalization.rs, src/services/remote_storage_target_service/paths.rs, src/services/remote_storage_target_service/tests.rs
远程目标的字段描述、秘钥保留、本地路径校验与 S3 字段归一化改用共享 helper,并移除旧的本地实现。
Storage policy max_file_size 边界校验
src/services/policy_service/policies.rs
create/update 流程对 max_file_size 统一做非负校验,并增加负值场景测试。
前端表单校验与 secret 保留逻辑适配
frontend-panel/src/components/admin/admin-remote-nodes-page/RemoteNodeRemoteStorageTargetForm.tsx, frontend-panel/src/components/admin/admin-remote-nodes-page/RemoteNodeRemoteStorageTargetSection.tsx, frontend-panel/src/components/admin/admin-remote-nodes-page/RemoteNodeRemoteStorageTargetSection.test.tsx, frontend-panel/src/services/api.generated.ts
前端改为依据驱动字段描述和 validation 元数据判断必填、本地路径与 secret 保留行为。
开发文档补充
developer-docs/en/README.md, developer-docs/en/storage-descriptor-normalization-contract.md, developer-docs/zh-CN/storage-descriptor-normalization-contract.md
新增契约文档并补充入口与模块归属说明。

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant 管理端表单
  participant 后端驱动描述符
  participant field_contract.rs
  participant remote_storage_target_service

  管理端表单->>后端驱动描述符: 读取字段描述符与 validation
  后端驱动描述符->>field_contract.rs: 派生字段语义/归一化规则
  remote_storage_target_service->>field_contract.rs: 规范化名称、secret、路径、前缀
  field_contract.rs-->>remote_storage_target_service: 返回归一化结果或错误
  remote_storage_target_service-->>管理端表单: 提供描述符与校验元数据
Loading

Possibly related PRs

Suggested labels: Enhancement, Priority: Medium, Rust, TypeScript, Documentation

Poem

契约落地,字段归一,
secret 该藏就藏好,别装神秘。
后端前端各认同一张图,
这回总算没把路径写成迷宫。

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed 标题与变更一致,准确概括了存储描述符与归一化契约的澄清。
Description check ✅ Passed 描述基本完整,包含摘要、验证和关闭项,但未严格使用模板中的 Test plan/Notes for reviewers 结构。
Linked Issues check ✅ Passed 实现覆盖了契约文档、共享归一化辅助函数、前后端描述符联动和测试,符合 #372 的核心目标。
Out of Scope Changes check ✅ Passed 未见明显与 #372 无关的附加改动,变更集中在契约、归一化和前端对齐上。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 feat/issue-372

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.

@codecov

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.27273% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...odes-page/RemoteNodeRemoteStorageTargetSection.tsx 73.68% 3 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@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.

🧹 Nitpick comments (2)
frontend-panel/src/components/admin/admin-remote-nodes-page/RemoteNodeRemoteStorageTargetSection.tsx (1)

161-195: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

别拿 help_key 当语义开关frontend-panel/src/components/admin/admin-remote-nodes-page/RemoteNodeRemoteStorageTargetSection.tsx:161-172
help_key 只是提示文案 key,不该拿来决定本地路径校验。这里改成后端 descriptor 里的显式语义位,不然 key 一改,校验逻辑就跟着飘。

🤖 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
`@frontend-panel/src/components/admin/admin-remote-nodes-page/RemoteNodeRemoteStorageTargetSection.tsx`
around lines 161 - 195, The local path validation in
RemoteNodeRemoteStorageTargetSection is incorrectly using basePathField.help_key
as the semantic switch for whether to enforce relative-path rules. Update the
validation logic around requiresLocalRelativePath/localPathError to rely on an
explicit descriptor flag from the backend schema instead of the help_key string,
and keep the rest of the form-required checks unchanged.

Source: Coding guidelines

src/services/remote_storage_target_service/driver.rs (1)

51-63: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

From 里塞 panic!,这不是"转换",是埋雷。

RemoteStorageTargetDriverFieldKind 压根没有 Select 变体,From<StorageDescriptorFieldKind> 却硬着头皮实现全量转换,遇到 Select 直接炸。现在两个调用点(remote_storage_target_text_field/remote_storage_target_boolean_field)确实不会喂 Select 进来,所以暂时安全——但 From 语义上就该是保证成功的转换,写成"大概率成功,偶尔崩"的样子,谁用谁倒霉。字段契约本来就是给 storage policy 和 managed ingress 共享的,以后有人复制粘贴出个下拉框字段,这颗雷就爆了。

改用 TryFrom,让"managed ingress 不支持 Select"这件事在类型层面显式暴露出来,而不是靠运行时炸给你看。

♻️ 建议改用 TryFrom
-impl From<StorageDescriptorFieldKind> for RemoteStorageTargetDriverFieldKind {
-    fn from(value: StorageDescriptorFieldKind) -> Self {
-        match value {
-            StorageDescriptorFieldKind::Text => Self::Text,
-            StorageDescriptorFieldKind::Secret => Self::Secret,
-            StorageDescriptorFieldKind::Boolean => Self::Boolean,
-            StorageDescriptorFieldKind::Number => Self::Number,
-            StorageDescriptorFieldKind::Select => {
-                panic!("managed ingress target descriptors do not support select fields")
-            }
-        }
-    }
-}
+impl TryFrom<StorageDescriptorFieldKind> for RemoteStorageTargetDriverFieldKind {
+    type Error = &'static str;
+
+    fn try_from(value: StorageDescriptorFieldKind) -> Result<Self, Self::Error> {
+        match value {
+            StorageDescriptorFieldKind::Text => Ok(Self::Text),
+            StorageDescriptorFieldKind::Secret => Ok(Self::Secret),
+            StorageDescriptorFieldKind::Boolean => Ok(Self::Boolean),
+            StorageDescriptorFieldKind::Number => Ok(Self::Number),
+            StorageDescriptorFieldKind::Select => {
+                Err("managed ingress target descriptors do not support select fields")
+            }
+        }
+    }
+}

调用处 semantics.kind.into() 改成 semantics.kind.try_into().expect("..."),两个调用点都是内部硬编码不会触发 Select,.expect 的成本不变,但类型契约更诚实。

🤖 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 `@src/services/remote_storage_target_service/driver.rs` around lines 51 - 63,
The impl of From<StorageDescriptorFieldKind> for
RemoteStorageTargetDriverFieldKind is not a total conversion because the Select
branch panics, so replace it with TryFrom to make the unsupported case explicit.
Update the conversion in driver.rs and adjust the call sites in
remote_storage_target_text_field and remote_storage_target_boolean_field to use
try_into() with a clear expect message, since those paths are internal and
should never hit Select. This keeps the type contract honest and avoids runtime
panic hidden inside From.
🤖 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.

Nitpick comments:
In
`@frontend-panel/src/components/admin/admin-remote-nodes-page/RemoteNodeRemoteStorageTargetSection.tsx`:
- Around line 161-195: The local path validation in
RemoteNodeRemoteStorageTargetSection is incorrectly using basePathField.help_key
as the semantic switch for whether to enforce relative-path rules. Update the
validation logic around requiresLocalRelativePath/localPathError to rely on an
explicit descriptor flag from the backend schema instead of the help_key string,
and keep the rest of the form-required checks unchanged.

In `@src/services/remote_storage_target_service/driver.rs`:
- Around line 51-63: The impl of From<StorageDescriptorFieldKind> for
RemoteStorageTargetDriverFieldKind is not a total conversion because the Select
branch panics, so replace it with TryFrom to make the unsupported case explicit.
Update the conversion in driver.rs and adjust the call sites in
remote_storage_target_text_field and remote_storage_target_boolean_field to use
try_into() with a clear expect message, since those paths are internal and
should never hit Select. This keeps the type contract honest and avoids runtime
panic hidden inside From.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ead29429-5be1-4d86-b6c9-ab7dd6fb896f

📥 Commits

Reviewing files that changed from the base of the PR and between e305e1b and 0c75125.

📒 Files selected for processing (12)
  • developer-docs/en/README.md
  • developer-docs/en/storage-descriptor-normalization-contract.md
  • developer-docs/zh-CN/storage-descriptor-normalization-contract.md
  • frontend-panel/src/components/admin/admin-remote-nodes-page/RemoteNodeRemoteStorageTargetForm.tsx
  • frontend-panel/src/components/admin/admin-remote-nodes-page/RemoteNodeRemoteStorageTargetSection.tsx
  • src/services/policy_service/policies.rs
  • src/services/remote_storage_target_service/driver.rs
  • src/services/remote_storage_target_service/normalization.rs
  • src/services/remote_storage_target_service/paths.rs
  • src/storage/connector_descriptor.rs
  • src/storage/field_contract.rs
  • src/storage/mod.rs

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend-panel/src/components/admin/admin-remote-nodes-page/RemoteNodeRemoteStorageTargetSection.tsx (1)

183-197: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

access_key 不能跟 secret_key 一起跳过编辑态必填校验。 这里的 access_key 不是 secret 字段,编辑时清空后应该立刻报必填错;现在只要 driver 不变就一律放行,会把空值拖到后端才被拒掉。

按 field.secret 区分
 	const accessKeyError =
 		accessKeyField?.required &&
-		!preservesExistingCredentialValues &&
+		!(accessKeyField?.secret && preservesExistingCredentialValues) &&
 		!form.access_key.trim()
 			? t("remote_node_ingress_profile_access_key_required")
 			: null;
🤖 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
`@frontend-panel/src/components/admin/admin-remote-nodes-page/RemoteNodeRemoteStorageTargetSection.tsx`
around lines 183 - 197, The edit-mode required check in
RemoteNodeRemoteStorageTargetSection should not skip validation for access_key
just because the driver_type is unchanged; update the
preservesExistingCredentialValues logic or split the conditions so only secret
fields can preserve existing values in edit mode. Keep secret_key behavior tied
to field.secret, but make access_key revalidate immediately when cleared, using
the existing form/accessKeyField/secretKeyField checks in this component.
🧹 Nitpick comments (1)
src/services/remote_storage_target_service/driver.rs (1)

51-65: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

TryFrom + .expect() 组合有点绕,容易埋雷。

Select 分支被拒绝,然后 text_field_with_validation/boolean_field 里立刻 .expect() 兜底——这两处目前确实不可能命中 Selecttext/secret/boolean 语义函数只产出对应 kind),但把"不可能发生"的分支从编译期保证退化成运行时 panic,后续如果有人改了 field_contract 里的语义函数,这里就直接崩,而不是编译不过。

♻️ 可选写法:避免 expect
-impl TryFrom<StorageDescriptorFieldKind> for RemoteStorageTargetDriverFieldKind {
-    type Error = &'static str;
-
-    fn try_from(value: StorageDescriptorFieldKind) -> std::result::Result<Self, Self::Error> {
-        match value {
-            StorageDescriptorFieldKind::Text => Ok(Self::Text),
-            StorageDescriptorFieldKind::Secret => Ok(Self::Secret),
-            StorageDescriptorFieldKind::Boolean => Ok(Self::Boolean),
-            StorageDescriptorFieldKind::Number => Ok(Self::Number),
-            StorageDescriptorFieldKind::Select => {
-                Err("managed ingress target descriptors do not support select fields")
-            }
-        }
-    }
-}
+fn non_select_field_kind(kind: StorageDescriptorFieldKind) -> RemoteStorageTargetDriverFieldKind {
+    match kind {
+        StorageDescriptorFieldKind::Text => RemoteStorageTargetDriverFieldKind::Text,
+        StorageDescriptorFieldKind::Secret => RemoteStorageTargetDriverFieldKind::Secret,
+        StorageDescriptorFieldKind::Boolean => RemoteStorageTargetDriverFieldKind::Boolean,
+        StorageDescriptorFieldKind::Number => RemoteStorageTargetDriverFieldKind::Number,
+        StorageDescriptorFieldKind::Select => unreachable!(
+            "managed ingress target descriptors only construct text/secret/boolean fields"
+        ),
+    }
+}

不影响功能,纯粹图个心安,爱改不改。

Also applies to: 106-144, 146-166

🤖 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 `@src/services/remote_storage_target_service/driver.rs` around lines 51 - 65,
The TryFrom + expect flow in RemoteStorageTargetDriverFieldKind is too fragile
because Select is rejected only at runtime and then unwrapped in field builders.
Update the conversion and the callers in RemoteStorageTargetDriverFieldKind,
text_field_with_validation, and boolean_field so they do not rely on .expect for
impossible kinds; instead, make the unsupported case handled explicitly at the
type/Result boundary so future changes to field_contract semantics fail safely
without a panic.
🤖 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.

Outside diff comments:
In
`@frontend-panel/src/components/admin/admin-remote-nodes-page/RemoteNodeRemoteStorageTargetSection.tsx`:
- Around line 183-197: The edit-mode required check in
RemoteNodeRemoteStorageTargetSection should not skip validation for access_key
just because the driver_type is unchanged; update the
preservesExistingCredentialValues logic or split the conditions so only secret
fields can preserve existing values in edit mode. Keep secret_key behavior tied
to field.secret, but make access_key revalidate immediately when cleared, using
the existing form/accessKeyField/secretKeyField checks in this component.

---

Nitpick comments:
In `@src/services/remote_storage_target_service/driver.rs`:
- Around line 51-65: The TryFrom + expect flow in
RemoteStorageTargetDriverFieldKind is too fragile because Select is rejected
only at runtime and then unwrapped in field builders. Update the conversion and
the callers in RemoteStorageTargetDriverFieldKind, text_field_with_validation,
and boolean_field so they do not rely on .expect for impossible kinds; instead,
make the unsupported case handled explicitly at the type/Result boundary so
future changes to field_contract semantics fail safely without a panic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 44b5f1b8-524f-430c-b15b-644a900c1ae8

📥 Commits

Reviewing files that changed from the base of the PR and between 0c75125 and 179b0d3.

📒 Files selected for processing (4)
  • frontend-panel/src/components/admin/admin-remote-nodes-page/RemoteNodeRemoteStorageTargetSection.tsx
  • frontend-panel/src/services/api.generated.ts
  • src/services/remote_storage_target_service/driver.rs
  • src/services/remote_storage_target_service/tests.rs
✅ Files skipped from review due to trivial changes (1)
  • frontend-panel/src/services/api.generated.ts

…rving secret

Changed S3 remote storage target edit flow to require access_key input on every edit while continuing to preserve the existing secret_key value when unchanged. Updated validation logic to distinguish between public credentials (access_key) and secret credentials (secret_key), with only the latter being preserved during edits.

Backend changes:
- Renamed `preservesExistingCredentialValues` to `preservesExistingSecretValue` with stricter logic
- Added `remote_storage_target_field_kind` helper to centralize field kind conversion with proper error handling
- Changed field builder functions to return `Result<RemoteStorageTargetDriverFieldDescriptor>` for consistent error propagation
- Updated `descriptor()` methods across connector traits to return `Result<RemoteStorageTargetDriverDescriptor>`

Frontend changes:
- Updated test to verify access_key is now required on edit and included in update payload
- Modified validation to always require access_key input regardless of edit mode
- Retained secret_key preservation behavior (only validated when field is non-secret or value changed)

Test changes:
- Updated test name and assertions to reflect new access_key requirement
- Added validation checks for disabled save button and required field message
- Verified rotated access_key appears in update payload
@AptS-1547

Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@AptS-1547 AptS-1547 merged commit 1df68e1 into master Jul 5, 2026
5 of 9 checks passed
@AptS-1547 AptS-1547 deleted the feat/issue-372 branch July 5, 2026 16:48
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.

0.3: Clarify storage descriptor and normalization contracts

2 participants