Skip to content

Commit c4bd485

Browse files
committed
enhance: upstream parser for variables #1402
1 parent bc0844a commit c4bd485

File tree

4 files changed

+251
-2
lines changed

4 files changed

+251
-2
lines changed

app/src/views/site/site_edit/components/SiteEditor/store.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@ export const useSiteEditorStore = defineStore('siteEditor', () => {
6868
await buildConfig()
6969
}
7070

71+
if (data.value.sync_node_ids === null) {
72+
data.value.sync_node_ids = []
73+
}
74+
75+
// @ts-expect-error allow comparing with empty string for legacy data
76+
if (data.value.namespace_id === '') {
77+
data.value.namespace_id = 0
78+
}
79+
7180
const response = await site.updateItem(encodeURIComponent(name.value), {
7281
content: configText.value,
7382
overwrite: true,

app/src/views/stream/store.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ export const useStreamEditorStore = defineStore('streamEditor', () => {
5656
await buildConfig()
5757
}
5858

59+
if (data.value.sync_node_ids === null) {
60+
data.value.sync_node_ids = []
61+
}
62+
63+
// @ts-expect-error allow comparing with empty string for legacy data
64+
if (data.value.namespace_id === '') {
65+
data.value.namespace_id = 0
66+
}
67+
5968
const response = await stream.updateItem(encodeURIComponent(name.value), {
6069
content: configText.value,
6170
overwrite: true,

internal/upstream/upstream_parser.go

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func ParseProxyTargetsAndUpstreamsFromRawContent(content string) *ParseResult {
4242
upstreams := make(map[string][]ProxyTarget)
4343

4444
// First, collect all upstream names and their contexts
45+
// Also collect literal variable assignments from `set $var value;`
4546
upstreamNames := make(map[string]bool)
4647
upstreamContexts := make(map[string]*TheUpstreamContext)
4748
upstreamRegex := regexp.MustCompile(`(?s)upstream\s+([^\s]+)\s*\{([^}]+)\}`)
@@ -92,13 +93,24 @@ func ParseProxyTargetsAndUpstreamsFromRawContent(content string) *ParseResult {
9293
}
9394
}
9495

96+
// Collect simple literal variables defined via `set $var value;`
97+
// Only variables with literal values (no nginx variables inside) are recorded.
98+
variableValues := extractLiteralSetVariables(content)
99+
95100
// Parse proxy_pass directives, but skip upstream references
96101
proxyPassRegex := regexp.MustCompile(`(?m)^\s*proxy_pass\s+([^;]+);`)
97102
proxyMatches := proxyPassRegex.FindAllStringSubmatch(content, -1)
98103

99104
for _, match := range proxyMatches {
100105
if len(match) >= 2 {
101-
proxyPassURL := strings.TrimSpace(match[1])
106+
rawValue := strings.TrimSpace(match[1])
107+
108+
// If the value is a single variable like `$target`, try to resolve it from `set $target ...;`
109+
if resolved, ok := resolveSingleVariable(rawValue, variableValues); ok {
110+
rawValue = resolved
111+
}
112+
113+
proxyPassURL := rawValue
102114
// Skip if this proxy_pass references an upstream
103115
if !isUpstreamReference(proxyPassURL, upstreamNames) {
104116
target := parseProxyPassURL(proxyPassURL, "proxy_pass")
@@ -115,7 +127,14 @@ func ParseProxyTargetsAndUpstreamsFromRawContent(content string) *ParseResult {
115127

116128
for _, match := range grpcMatches {
117129
if len(match) >= 2 {
118-
grpcPassURL := strings.TrimSpace(match[1])
130+
rawValue := strings.TrimSpace(match[1])
131+
132+
// If the value is a single variable like `$target`, try to resolve it from `set $target ...;`
133+
if resolved, ok := resolveSingleVariable(rawValue, variableValues); ok {
134+
rawValue = resolved
135+
}
136+
137+
grpcPassURL := rawValue
119138
// Skip if this grpc_pass references an upstream
120139
if !isUpstreamReference(grpcPassURL, upstreamNames) {
121140
target := parseProxyPassURL(grpcPassURL, "grpc_pass")
@@ -391,3 +410,63 @@ func isUpstreamReference(passURL string, upstreamNames map[string]bool) bool {
391410

392411
return false
393412
}
413+
414+
// extractLiteralSetVariables parses `set $var value;` directives from the entire content and
415+
// returns a map of variable name to its literal value. Values containing nginx variables are ignored.
416+
func extractLiteralSetVariables(content string) map[string]string {
417+
result := make(map[string]string)
418+
419+
// Capture variable name and raw value (without trailing semicolon)
420+
setRegex := regexp.MustCompile(`(?m)^\s*set\s+\$([A-Za-z0-9_]+)\s+([^;]+);`)
421+
matches := setRegex.FindAllStringSubmatch(content, -1)
422+
for _, m := range matches {
423+
if len(m) < 3 {
424+
continue
425+
}
426+
name := m[1]
427+
value := strings.TrimSpace(m[2])
428+
429+
// Remove surrounding quotes if any
430+
if len(value) >= 2 {
431+
if (strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`)) ||
432+
(strings.HasPrefix(value, `'`) && strings.HasSuffix(value, `'`)) {
433+
value = strings.Trim(value, `"'`)
434+
}
435+
}
436+
437+
// Ignore values containing nginx variables unless it is a single variable reference
438+
if strings.Contains(value, "$") {
439+
// Support simple indirection: set $a $b;
440+
if resolved, ok := resolveSingleVariable(value, result); ok {
441+
result[name] = resolved
442+
}
443+
continue
444+
}
445+
446+
// Record literal value
447+
result[name] = value
448+
}
449+
return result
450+
}
451+
452+
// resolveSingleVariable resolves an expression that is exactly a single variable like `$target`
453+
// using the provided map. Returns (resolvedValue, true) if resolvable; otherwise ("", false).
454+
func resolveSingleVariable(expr string, variables map[string]string) (string, bool) {
455+
expr = strings.TrimSpace(expr)
456+
// Match exactly `$varName` with optional surrounding spaces
457+
varOnlyRegex := regexp.MustCompile(`^\$([A-Za-z0-9_]+)$`)
458+
sub := varOnlyRegex.FindStringSubmatch(expr)
459+
if len(sub) < 2 {
460+
return "", false
461+
}
462+
name := sub[1]
463+
val, ok := variables[name]
464+
if !ok {
465+
return "", false
466+
}
467+
// Guard against cyclic or unresolved values that still contain variables
468+
if strings.Contains(val, "$") {
469+
return "", false
470+
}
471+
return val, true
472+
}

internal/upstream/upstream_parser_test.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,3 +753,155 @@ func TestGrpcPassPortDefaults(t *testing.T) {
753753
})
754754
}
755755
}
756+
757+
// New tests covering `set $var ...;` with proxy_pass/grpc_pass
758+
func TestSetVariableProxyPass_HTTP(t *testing.T) {
759+
config := `
760+
server {
761+
listen 80;
762+
set $target http://example.com;
763+
location / {
764+
proxy_pass $target;
765+
}
766+
}`
767+
768+
targets := ParseProxyTargetsFromRawContent(config)
769+
770+
expected := ProxyTarget{Host: "example.com", Port: "80", Type: "proxy_pass"}
771+
if len(targets) != 1 {
772+
t.Fatalf("Expected 1 target, got %d", len(targets))
773+
}
774+
got := targets[0]
775+
if got.Host != expected.Host || got.Port != expected.Port || got.Type != expected.Type {
776+
t.Errorf("Unexpected target: got=%+v expected=%+v", got, expected)
777+
}
778+
}
779+
780+
func TestSetVariableProxyPass_HTTPS(t *testing.T) {
781+
config := `
782+
server {
783+
listen 80;
784+
set $target https://example.com;
785+
location / {
786+
proxy_pass $target;
787+
}
788+
}`
789+
790+
targets := ParseProxyTargetsFromRawContent(config)
791+
792+
expected := ProxyTarget{Host: "example.com", Port: "443", Type: "proxy_pass"}
793+
if len(targets) != 1 {
794+
t.Fatalf("Expected 1 target, got %d", len(targets))
795+
}
796+
got := targets[0]
797+
if got.Host != expected.Host || got.Port != expected.Port || got.Type != expected.Type {
798+
t.Errorf("Unexpected target: got=%+v expected=%+v", got, expected)
799+
}
800+
}
801+
802+
func TestSetVariableProxyPass_QuotedValue(t *testing.T) {
803+
config := `
804+
server {
805+
listen 80;
806+
set $target "http://example.com:9090";
807+
location / {
808+
proxy_pass $target;
809+
}
810+
}`
811+
812+
targets := ParseProxyTargetsFromRawContent(config)
813+
814+
expected := ProxyTarget{Host: "example.com", Port: "9090", Type: "proxy_pass"}
815+
if len(targets) != 1 {
816+
t.Fatalf("Expected 1 target, got %d", len(targets))
817+
}
818+
got := targets[0]
819+
if got.Host != expected.Host || got.Port != expected.Port || got.Type != expected.Type {
820+
t.Errorf("Unexpected target: got=%+v expected=%+v", got, expected)
821+
}
822+
}
823+
824+
func TestSetVariableProxyPass_UnresolvableIgnored(t *testing.T) {
825+
config := `
826+
server {
827+
listen 80;
828+
set $target http://example.com$request_uri;
829+
location / {
830+
proxy_pass $target;
831+
}
832+
}`
833+
834+
targets := ParseProxyTargetsFromRawContent(config)
835+
836+
// Because the variable value contains nginx variables, it should be ignored
837+
if len(targets) != 0 {
838+
t.Errorf("Expected 0 targets, got %d", len(targets))
839+
for i, target := range targets {
840+
t.Logf("Target %d: %+v", i, target)
841+
}
842+
}
843+
}
844+
845+
func TestSetVariableProxyPass_UpstreamReferenceIgnored(t *testing.T) {
846+
config := `
847+
upstream api-1 {
848+
server 127.0.0.1:9000;
849+
keepalive 16;
850+
}
851+
server {
852+
listen 80;
853+
set $target http://api-1/;
854+
location / {
855+
proxy_pass $target;
856+
}
857+
}`
858+
859+
targets := ParseProxyTargetsFromRawContent(config)
860+
861+
// Expect only upstream servers, and proxy_pass via $target should be ignored
862+
expectedTargets := []ProxyTarget{
863+
{Host: "127.0.0.1", Port: "9000", Type: "upstream"},
864+
}
865+
866+
if len(targets) != len(expectedTargets) {
867+
t.Errorf("Expected %d targets, got %d", len(expectedTargets), len(targets))
868+
for i, target := range targets {
869+
t.Logf("Target %d: %+v", i, target)
870+
}
871+
return
872+
}
873+
874+
targetMap := make(map[string]ProxyTarget)
875+
for _, target := range targets {
876+
key := formatSocketAddress(target.Host, target.Port) + ":" + target.Type
877+
targetMap[key] = target
878+
}
879+
for _, expected := range expectedTargets {
880+
key := formatSocketAddress(expected.Host, expected.Port) + ":" + expected.Type
881+
if _, found := targetMap[key]; !found {
882+
t.Errorf("Expected target not found: %+v", expected)
883+
}
884+
}
885+
}
886+
887+
func TestSetVariableGrpcPass(t *testing.T) {
888+
config := `
889+
server {
890+
listen 80 http2;
891+
set $g grpc://127.0.0.1:9090;
892+
location /svc/ {
893+
grpc_pass $g;
894+
}
895+
}`
896+
897+
targets := ParseProxyTargetsFromRawContent(config)
898+
899+
expected := ProxyTarget{Host: "127.0.0.1", Port: "9090", Type: "grpc_pass"}
900+
if len(targets) != 1 {
901+
t.Fatalf("Expected 1 target, got %d", len(targets))
902+
}
903+
got := targets[0]
904+
if got.Host != expected.Host || got.Port != expected.Port || got.Type != expected.Type {
905+
t.Errorf("Unexpected target: got=%+v expected=%+v", got, expected)
906+
}
907+
}

0 commit comments

Comments
 (0)