mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 14:06:36 +08:00
fix(storage): allow staging local config when S3 is unreachable
When switching storage drivers in admin settings, save now validates connectivity against the selected target backend instead of retesting the still-active driver. The active driver remains unchanged until migration completes, so unreachable MinIO no longer blocks saving local storage settings.
This commit is contained in:
@@ -149,13 +149,8 @@ export function StorageConfigTab() {
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: async (value: StorageConfig) => {
|
||||
const activeDriver = query.data?.config.driver || value.driver
|
||||
const valueToSave = {
|
||||
...value,
|
||||
driver: activeDriver,
|
||||
}
|
||||
await services.adminSystemConfig.updateSystemConfig(storageConfigKey, {
|
||||
value: JSON.stringify(valueToSave),
|
||||
value: JSON.stringify(value),
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -307,19 +307,12 @@ func validateAndMergeStorageConfig(ctx context.Context, value string, currentCon
|
||||
|
||||
// 合并被掩码屏蔽的敏感信息,获取完整的真实配置
|
||||
targetCfg := storage.MergeMaskedSecrets(newCfg, currentCfg)
|
||||
|
||||
// 校验配置参数是否合法
|
||||
if err := storage.ValidateConfig(targetCfg); err != nil {
|
||||
return "", fmt.Errorf("验证存储配置参数失败: %w", err)
|
||||
if err := validateMergedStorageConfig(ctx, currentCfg, newCfg, targetCfg); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 进行连通性测试验证,如果测试失败则拒绝保存
|
||||
testBackend, err := storage.NewBackend(ctx, targetCfg, targetCfg.Driver)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("初始化测试存储实例失败: %w", err)
|
||||
}
|
||||
if err := testBackend.Test(ctx); err != nil {
|
||||
return "", fmt.Errorf("存储连通性测试失败: %w", err)
|
||||
if newCfg.Driver != "" && newCfg.Driver != currentCfg.Driver {
|
||||
// 切换存储类型时仅暂存目标后端参数,活动 driver 由迁移任务正式切换。
|
||||
targetCfg.Driver = currentCfg.Driver
|
||||
}
|
||||
|
||||
// 序列化为最终保存的真实明文配置,防止保存屏蔽的 ****** 字符
|
||||
@@ -330,3 +323,35 @@ func validateAndMergeStorageConfig(ctx context.Context, value string, currentCon
|
||||
|
||||
return string(unmaskedVal), nil
|
||||
}
|
||||
|
||||
func validateMergedStorageConfig(ctx context.Context, currentCfg, newCfg, targetCfg storage.Config) error {
|
||||
if newCfg.Driver != "" && newCfg.Driver != currentCfg.Driver {
|
||||
if err := validateDriverConfig(targetCfg, newCfg.Driver); err != nil {
|
||||
return fmt.Errorf("验证目标存储配置参数失败: %w", err)
|
||||
}
|
||||
pendingCfg := targetCfg
|
||||
pendingCfg.Driver = newCfg.Driver
|
||||
return testStorageBackend(ctx, pendingCfg, newCfg.Driver)
|
||||
}
|
||||
|
||||
if err := storage.ValidateConfig(targetCfg); err != nil {
|
||||
return fmt.Errorf("验证存储配置参数失败: %w", err)
|
||||
}
|
||||
return testStorageBackend(ctx, targetCfg, targetCfg.Driver)
|
||||
}
|
||||
|
||||
func validateDriverConfig(cfg storage.Config, driver storage.Driver) error {
|
||||
cfg.Driver = driver
|
||||
return storage.ValidateConfig(cfg)
|
||||
}
|
||||
|
||||
func testStorageBackend(ctx context.Context, cfg storage.Config, driver storage.Driver) error {
|
||||
testBackend, err := storage.NewBackend(ctx, cfg, driver)
|
||||
if err != nil {
|
||||
return fmt.Errorf("初始化测试存储实例失败: %w", err)
|
||||
}
|
||||
if err := testBackend.Test(ctx); err != nil {
|
||||
return fmt.Errorf("存储连通性测试失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -440,4 +440,57 @@ func TestUpdateStorageConfigValidation(t *testing.T) {
|
||||
t.Errorf("expected 400 Bad Request, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("stage local config while active s3 is unreachable", func(t *testing.T) {
|
||||
activeCfg := storage.DefaultConfig()
|
||||
activeCfg.Driver = storage.DriverS3
|
||||
activeCfg.S3.Endpoint = "http://127.0.0.1:9999"
|
||||
activeCfg.S3.Region = "us-east-1"
|
||||
activeCfg.S3.Bucket = "wavelet"
|
||||
activeCfg.S3.AccessKeyID = "test"
|
||||
activeCfg.S3.SecretAccessKey = "test"
|
||||
activeBytes, _ := json.Marshal(activeCfg)
|
||||
seedCfg := model.SystemConfig{
|
||||
Key: "storage_config",
|
||||
Value: string(activeBytes),
|
||||
Type: "system",
|
||||
}
|
||||
if err := dbConn.Where("key = ?", "storage_config").
|
||||
Assign(map[string]any{"value": seedCfg.Value, "type": seedCfg.Type}).
|
||||
FirstOrCreate(&seedCfg).Error; err != nil {
|
||||
t.Fatalf("seed active storage config failed: %v", err)
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
stagedCfg := activeCfg
|
||||
stagedCfg.Driver = storage.DriverLocal
|
||||
stagedCfg.Local.Root = tempDir
|
||||
|
||||
cfgBytes, _ := json.Marshal(stagedCfg)
|
||||
payload := UpdateSystemConfigRequest{Value: string(cfgBytes)}
|
||||
body, _ := json.Marshal(payload)
|
||||
req, _ := http.NewRequest("PUT", "/api/v1/admin/system-configs/storage_config", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 OK, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var dbCfg model.SystemConfig
|
||||
if err := dbConn.Where("key = ?", "storage_config").First(&dbCfg).Error; err != nil {
|
||||
t.Fatalf("load saved storage config failed: %v", err)
|
||||
}
|
||||
var savedCfg storage.Config
|
||||
if err := json.Unmarshal([]byte(dbCfg.Value), &savedCfg); err != nil {
|
||||
t.Fatalf("parse saved storage config failed: %v", err)
|
||||
}
|
||||
if savedCfg.Driver != storage.DriverS3 {
|
||||
t.Fatalf("active driver = %q, want %q until migration completes", savedCfg.Driver, storage.DriverS3)
|
||||
}
|
||||
if savedCfg.Local.Root != tempDir {
|
||||
t.Fatalf("staged local root = %q, want %q", savedCfg.Local.Root, tempDir)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user