Преглед изворни кода

enhance: upstream parser for variables #1402

0xJacky пре 3 месеци
родитељ
комит
c4bd485c4f

+ 9 - 0
app/src/views/site/site_edit/components/SiteEditor/store.ts

@@ -68,6 +68,15 @@ export const useSiteEditorStore = defineStore('siteEditor', () => {
         await buildConfig()
       }
 
+      if (data.value.sync_node_ids === null) {
+        data.value.sync_node_ids = []
+      }
+
+      // @ts-expect-error allow comparing with empty string for legacy data
+      if (data.value.namespace_id === '') {
+        data.value.namespace_id = 0
+      }
+
       const response = await site.updateItem(encodeURIComponent(name.value), {
         content: configText.value,
         overwrite: true,

+ 9 - 0
app/src/views/stream/store.ts

@@ -56,6 +56,15 @@ export const useStreamEditorStore = defineStore('streamEditor', () => {
         await buildConfig()
       }
 
+      if (data.value.sync_node_ids === null) {
+        data.value.sync_node_ids = []
+      }
+
+      // @ts-expect-error allow comparing with empty string for legacy data
+      if (data.value.namespace_id === '') {
+        data.value.namespace_id = 0
+      }
+
       const response = await stream.updateItem(encodeURIComponent(name.value), {
         content: configText.value,
         overwrite: true,

+ 81 - 2
internal/upstream/upstream_parser.go

@@ -42,6 +42,7 @@ func ParseProxyTargetsAndUpstreamsFromRawContent(content string) *ParseResult {
 	upstreams := make(map[string][]ProxyTarget)
 
 	// First, collect all upstream names and their contexts
+	// Also collect literal variable assignments from `set $var value;`
 	upstreamNames := make(map[string]bool)
 	upstreamContexts := make(map[string]*TheUpstreamContext)
 	upstreamRegex := regexp.MustCompile(`(?s)upstream\s+([^\s]+)\s*\{([^}]+)\}`)
@@ -92,13 +93,24 @@ func ParseProxyTargetsAndUpstreamsFromRawContent(content string) *ParseResult {
 		}
 	}
 
+	// Collect simple literal variables defined via `set $var value;`
+	// Only variables with literal values (no nginx variables inside) are recorded.
+	variableValues := extractLiteralSetVariables(content)
+
 	// Parse proxy_pass directives, but skip upstream references
 	proxyPassRegex := regexp.MustCompile(`(?m)^\s*proxy_pass\s+([^;]+);`)
 	proxyMatches := proxyPassRegex.FindAllStringSubmatch(content, -1)
 
 	for _, match := range proxyMatches {
 		if len(match) >= 2 {
-			proxyPassURL := strings.TrimSpace(match[1])
+			rawValue := strings.TrimSpace(match[1])
+
+			// If the value is a single variable like `$target`, try to resolve it from `set $target ...;`
+			if resolved, ok := resolveSingleVariable(rawValue, variableValues); ok {
+				rawValue = resolved
+			}
+
+			proxyPassURL := rawValue
 			// Skip if this proxy_pass references an upstream
 			if !isUpstreamReference(proxyPassURL, upstreamNames) {
 				target := parseProxyPassURL(proxyPassURL, "proxy_pass")
@@ -115,7 +127,14 @@ func ParseProxyTargetsAndUpstreamsFromRawContent(content string) *ParseResult {
 
 	for _, match := range grpcMatches {
 		if len(match) >= 2 {
-			grpcPassURL := strings.TrimSpace(match[1])
+			rawValue := strings.TrimSpace(match[1])
+
+			// If the value is a single variable like `$target`, try to resolve it from `set $target ...;`
+			if resolved, ok := resolveSingleVariable(rawValue, variableValues); ok {
+				rawValue = resolved
+			}
+
+			grpcPassURL := rawValue
 			// Skip if this grpc_pass references an upstream
 			if !isUpstreamReference(grpcPassURL, upstreamNames) {
 				target := parseProxyPassURL(grpcPassURL, "grpc_pass")
@@ -391,3 +410,63 @@ func isUpstreamReference(passURL string, upstreamNames map[string]bool) bool {
 
 	return false
 }
+
+// extractLiteralSetVariables parses `set $var value;` directives from the entire content and
+// returns a map of variable name to its literal value. Values containing nginx variables are ignored.
+func extractLiteralSetVariables(content string) map[string]string {
+	result := make(map[string]string)
+
+	// Capture variable name and raw value (without trailing semicolon)
+	setRegex := regexp.MustCompile(`(?m)^\s*set\s+\$([A-Za-z0-9_]+)\s+([^;]+);`)
+	matches := setRegex.FindAllStringSubmatch(content, -1)
+	for _, m := range matches {
+		if len(m) < 3 {
+			continue
+		}
+		name := m[1]
+		value := strings.TrimSpace(m[2])
+
+		// Remove surrounding quotes if any
+		if len(value) >= 2 {
+			if (strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`)) ||
+				(strings.HasPrefix(value, `'`) && strings.HasSuffix(value, `'`)) {
+				value = strings.Trim(value, `"'`)
+			}
+		}
+
+		// Ignore values containing nginx variables unless it is a single variable reference
+		if strings.Contains(value, "$") {
+			// Support simple indirection: set $a $b;
+			if resolved, ok := resolveSingleVariable(value, result); ok {
+				result[name] = resolved
+			}
+			continue
+		}
+
+		// Record literal value
+		result[name] = value
+	}
+	return result
+}
+
+// resolveSingleVariable resolves an expression that is exactly a single variable like `$target`
+// using the provided map. Returns (resolvedValue, true) if resolvable; otherwise ("", false).
+func resolveSingleVariable(expr string, variables map[string]string) (string, bool) {
+	expr = strings.TrimSpace(expr)
+	// Match exactly `$varName` with optional surrounding spaces
+	varOnlyRegex := regexp.MustCompile(`^\$([A-Za-z0-9_]+)$`)
+	sub := varOnlyRegex.FindStringSubmatch(expr)
+	if len(sub) < 2 {
+		return "", false
+	}
+	name := sub[1]
+	val, ok := variables[name]
+	if !ok {
+		return "", false
+	}
+	// Guard against cyclic or unresolved values that still contain variables
+	if strings.Contains(val, "$") {
+		return "", false
+	}
+	return val, true
+}

+ 152 - 0
internal/upstream/upstream_parser_test.go

@@ -753,3 +753,155 @@ func TestGrpcPassPortDefaults(t *testing.T) {
 		})
 	}
 }
+
+// New tests covering `set $var ...;` with proxy_pass/grpc_pass
+func TestSetVariableProxyPass_HTTP(t *testing.T) {
+	config := `
+server {
+    listen 80;
+    set $target http://example.com;
+    location / {
+        proxy_pass $target;
+    }
+}`
+
+	targets := ParseProxyTargetsFromRawContent(config)
+
+	expected := ProxyTarget{Host: "example.com", Port: "80", Type: "proxy_pass"}
+	if len(targets) != 1 {
+		t.Fatalf("Expected 1 target, got %d", len(targets))
+	}
+	got := targets[0]
+	if got.Host != expected.Host || got.Port != expected.Port || got.Type != expected.Type {
+		t.Errorf("Unexpected target: got=%+v expected=%+v", got, expected)
+	}
+}
+
+func TestSetVariableProxyPass_HTTPS(t *testing.T) {
+	config := `
+server {
+    listen 80;
+    set $target https://example.com;
+    location / {
+        proxy_pass $target;
+    }
+}`
+
+	targets := ParseProxyTargetsFromRawContent(config)
+
+	expected := ProxyTarget{Host: "example.com", Port: "443", Type: "proxy_pass"}
+	if len(targets) != 1 {
+		t.Fatalf("Expected 1 target, got %d", len(targets))
+	}
+	got := targets[0]
+	if got.Host != expected.Host || got.Port != expected.Port || got.Type != expected.Type {
+		t.Errorf("Unexpected target: got=%+v expected=%+v", got, expected)
+	}
+}
+
+func TestSetVariableProxyPass_QuotedValue(t *testing.T) {
+	config := `
+server {
+    listen 80;
+    set $target "http://example.com:9090";
+    location / {
+        proxy_pass $target;
+    }
+}`
+
+	targets := ParseProxyTargetsFromRawContent(config)
+
+	expected := ProxyTarget{Host: "example.com", Port: "9090", Type: "proxy_pass"}
+	if len(targets) != 1 {
+		t.Fatalf("Expected 1 target, got %d", len(targets))
+	}
+	got := targets[0]
+	if got.Host != expected.Host || got.Port != expected.Port || got.Type != expected.Type {
+		t.Errorf("Unexpected target: got=%+v expected=%+v", got, expected)
+	}
+}
+
+func TestSetVariableProxyPass_UnresolvableIgnored(t *testing.T) {
+	config := `
+server {
+    listen 80;
+    set $target http://example.com$request_uri;
+    location / {
+        proxy_pass $target;
+    }
+}`
+
+	targets := ParseProxyTargetsFromRawContent(config)
+
+	// Because the variable value contains nginx variables, it should be ignored
+	if len(targets) != 0 {
+		t.Errorf("Expected 0 targets, got %d", len(targets))
+		for i, target := range targets {
+			t.Logf("Target %d: %+v", i, target)
+		}
+	}
+}
+
+func TestSetVariableProxyPass_UpstreamReferenceIgnored(t *testing.T) {
+	config := `
+upstream api-1 {
+    server 127.0.0.1:9000;
+    keepalive 16;
+}
+server {
+    listen 80;
+    set $target http://api-1/;
+    location / {
+        proxy_pass $target;
+    }
+}`
+
+	targets := ParseProxyTargetsFromRawContent(config)
+
+	// Expect only upstream servers, and proxy_pass via $target should be ignored
+	expectedTargets := []ProxyTarget{
+		{Host: "127.0.0.1", Port: "9000", Type: "upstream"},
+	}
+
+	if len(targets) != len(expectedTargets) {
+		t.Errorf("Expected %d targets, got %d", len(expectedTargets), len(targets))
+		for i, target := range targets {
+			t.Logf("Target %d: %+v", i, target)
+		}
+		return
+	}
+
+	targetMap := make(map[string]ProxyTarget)
+	for _, target := range targets {
+		key := formatSocketAddress(target.Host, target.Port) + ":" + target.Type
+		targetMap[key] = target
+	}
+	for _, expected := range expectedTargets {
+		key := formatSocketAddress(expected.Host, expected.Port) + ":" + expected.Type
+		if _, found := targetMap[key]; !found {
+			t.Errorf("Expected target not found: %+v", expected)
+		}
+	}
+}
+
+func TestSetVariableGrpcPass(t *testing.T) {
+	config := `
+server {
+    listen 80 http2;
+    set $g grpc://127.0.0.1:9090;
+    location /svc/ {
+        grpc_pass $g;
+    }
+}`
+
+	targets := ParseProxyTargetsFromRawContent(config)
+
+	expected := ProxyTarget{Host: "127.0.0.1", Port: "9090", Type: "grpc_pass"}
+	if len(targets) != 1 {
+		t.Fatalf("Expected 1 target, got %d", len(targets))
+	}
+	got := targets[0]
+	if got.Host != expected.Host || got.Port != expected.Port || got.Type != expected.Type {
+		t.Errorf("Unexpected target: got=%+v expected=%+v", got, expected)
+	}
+}