فهرست منبع

feat: support YAML merge in strict configuration mode (#4809)

fatedier 4 هفته پیش
والد
کامیت
3128350dd6
4فایلهای تغییر یافته به همراه154 افزوده شده و 5 حذف شده
  1. 2 2
      Release.md
  2. 32 2
      pkg/config/load.go
  3. 119 0
      pkg/config/load_test.go
  4. 1 1
      pkg/util/version/version.go

+ 2 - 2
Release.md

@@ -1,3 +1,3 @@
-### Bug Fixes
+## Features
 
-*   **VirtualNet:** Resolved various issues related to connection handling, TUN device management, and stability in the virtual network feature.
+* Support for YAML merge functionality (anchors and references with dot-prefixed fields) in strict configuration mode without requiring `--strict-config=false` parameter.

+ 32 - 2
pkg/config/load.go

@@ -111,6 +111,33 @@ func LoadConfigureFromFile(path string, c any, strict bool) error {
 	return LoadConfigure(content, c, strict)
 }
 
+// parseYAMLWithDotFieldsHandling parses YAML with dot-prefixed fields handling
+// This function handles both cases efficiently: with or without dot fields
+func parseYAMLWithDotFieldsHandling(content []byte, target any) error {
+	var temp any
+	if err := yaml.Unmarshal(content, &temp); err != nil {
+		return err
+	}
+
+	// Remove dot fields if it's a map
+	if tempMap, ok := temp.(map[string]any); ok {
+		for key := range tempMap {
+			if strings.HasPrefix(key, ".") {
+				delete(tempMap, key)
+			}
+		}
+	}
+
+	// Convert to JSON and decode with strict validation
+	jsonBytes, err := json.Marshal(temp)
+	if err != nil {
+		return err
+	}
+	decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
+	decoder.DisallowUnknownFields()
+	return decoder.Decode(target)
+}
+
 // LoadConfigure loads configuration from bytes and unmarshal into c.
 // Now it supports json, yaml and toml format.
 func LoadConfigure(b []byte, c any, strict bool) error {
@@ -134,10 +161,13 @@ func LoadConfigure(b []byte, c any, strict bool) error {
 		}
 		return decoder.Decode(c)
 	}
-	// It wasn't JSON. Unmarshal as YAML.
+
+	// Handle YAML content
 	if strict {
-		return yaml.UnmarshalStrict(b, c)
+		// In strict mode, always use our custom handler to support YAML merge
+		return parseYAMLWithDotFieldsHandling(b, c)
 	}
+	// Non-strict mode, parse normally
 	return yaml.Unmarshal(b, c)
 }
 

+ 119 - 0
pkg/config/load_test.go

@@ -187,3 +187,122 @@ unixPath = "/tmp/uds.sock"
 	err = LoadConfigure([]byte(pluginStr), &clientCfg, true)
 	require.Error(err)
 }
+
+// TestYAMLMergeInStrictMode tests that YAML merge functionality works
+// even in strict mode by properly handling dot-prefixed fields
+func TestYAMLMergeInStrictMode(t *testing.T) {
+	require := require.New(t)
+
+	yamlContent := `
+serverAddr: "127.0.0.1"
+serverPort: 7000
+
+.common: &common
+  type: stcp
+  secretKey: "test-secret"
+  localIP: 127.0.0.1
+  transport:
+    useEncryption: true
+    useCompression: true
+
+proxies:
+- name: ssh
+  localPort: 22
+  <<: *common
+- name: web
+  localPort: 80
+  <<: *common
+`
+
+	clientCfg := v1.ClientConfig{}
+	// This should work in strict mode
+	err := LoadConfigure([]byte(yamlContent), &clientCfg, true)
+	require.NoError(err)
+
+	// Verify the merge worked correctly
+	require.Equal("127.0.0.1", clientCfg.ServerAddr)
+	require.Equal(7000, clientCfg.ServerPort)
+	require.Len(clientCfg.Proxies, 2)
+
+	// Check first proxy
+	sshProxy := clientCfg.Proxies[0].ProxyConfigurer
+	require.Equal("ssh", sshProxy.GetBaseConfig().Name)
+	require.Equal("stcp", sshProxy.GetBaseConfig().Type)
+
+	// Check second proxy
+	webProxy := clientCfg.Proxies[1].ProxyConfigurer
+	require.Equal("web", webProxy.GetBaseConfig().Name)
+	require.Equal("stcp", webProxy.GetBaseConfig().Type)
+}
+
+// TestOptimizedYAMLProcessing tests the optimization logic for YAML processing
+func TestOptimizedYAMLProcessing(t *testing.T) {
+	require := require.New(t)
+
+	yamlWithDotFields := []byte(`
+serverAddr: "127.0.0.1"
+.common: &common
+  type: stcp
+proxies:
+- name: test
+  <<: *common
+`)
+
+	yamlWithoutDotFields := []byte(`
+serverAddr: "127.0.0.1"
+proxies:
+- name: test
+  type: tcp
+  localPort: 22
+`)
+
+	// Test that YAML without dot fields works in strict mode
+	clientCfg := v1.ClientConfig{}
+	err := LoadConfigure(yamlWithoutDotFields, &clientCfg, true)
+	require.NoError(err)
+	require.Equal("127.0.0.1", clientCfg.ServerAddr)
+	require.Len(clientCfg.Proxies, 1)
+	require.Equal("test", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Name)
+
+	// Test that YAML with dot fields still works in strict mode
+	err = LoadConfigure(yamlWithDotFields, &clientCfg, true)
+	require.NoError(err)
+	require.Equal("127.0.0.1", clientCfg.ServerAddr)
+	require.Len(clientCfg.Proxies, 1)
+	require.Equal("test", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Name)
+	require.Equal("stcp", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Type)
+}
+
+// TestYAMLEdgeCases tests edge cases for YAML parsing, including non-map types
+func TestYAMLEdgeCases(t *testing.T) {
+	require := require.New(t)
+
+	// Test array at root (should fail for frp config)
+	arrayYAML := []byte(`
+- item1
+- item2
+`)
+	clientCfg := v1.ClientConfig{}
+	err := LoadConfigure(arrayYAML, &clientCfg, true)
+	require.Error(err) // Should fail because ClientConfig expects an object
+
+	// Test scalar at root (should fail for frp config)
+	scalarYAML := []byte(`"just a string"`)
+	err = LoadConfigure(scalarYAML, &clientCfg, true)
+	require.Error(err) // Should fail because ClientConfig expects an object
+
+	// Test empty object (should work)
+	emptyYAML := []byte(`{}`)
+	err = LoadConfigure(emptyYAML, &clientCfg, true)
+	require.NoError(err)
+
+	// Test nested structure without dots (should work)
+	nestedYAML := []byte(`
+serverAddr: "127.0.0.1"
+serverPort: 7000
+`)
+	err = LoadConfigure(nestedYAML, &clientCfg, true)
+	require.NoError(err)
+	require.Equal("127.0.0.1", clientCfg.ServerAddr)
+	require.Equal(7000, clientCfg.ServerPort)
+}

+ 1 - 1
pkg/util/version/version.go

@@ -14,7 +14,7 @@
 
 package version
 
-var version = "0.62.1"
+var version = "0.63.0"
 
 func Full() string {
 	return version