diff --git a/examples/09-dns-and-route/README.md b/examples/09-dns-and-route/README.md index a3ce662..3ba1d84 100644 --- a/examples/09-dns-and-route/README.md +++ b/examples/09-dns-and-route/README.md @@ -34,6 +34,8 @@ This adds an additional nginx service to the compose file which contains an HTTP By default, this listens on http://localhost:8080. +By default, this uses a `Prefix` route matching type so `/` can match `/any/request/path` but you can add a `score-compose.score.dev/route-provisioner-path-type: Exact` annotation to a Route to restrict this behavior. + ## Limitations Technically, the `dns` resource should produce random or appropriate hostnames for each dns resource. However, this implementation always generates localhost since we don't by default create real DNS names or modify the local /etc/hosts file. diff --git a/internal/command/default.provisioners.yaml b/internal/command/default.provisioners.yaml index 7c48816..05806d2 100644 --- a/internal/command/default.provisioners.yaml +++ b/internal/command/default.provisioners.yaml @@ -326,7 +326,8 @@ {{ $target := (printf "%s:%d" $targetHost $targetPort) }} {{ $hBefore := dig .Init.sk "hosts" (dict) .Shared }} {{ $rBefore := dig .Params.host (dict) $hBefore }} - {{ $inner := dict "path" .Params.path "target" $target "port" $targetPort }} + {{ $pathType := dig "score-compose.score.dev/route-provisioner-path-type" "Prefix" (dig "annotations" (dict) (.Metadata | default (dict))) }} + {{ $inner := dict "path" .Params.path "target" $target "port" $targetPort "path_type" $pathType }} {{ $rAfter := (merge $rBefore (dict .Uid $inner)) }} {{ $hAfter := (merge $hBefore (dict .Params.host $rAfter)) }} hosts: {{ $hAfter | toRawJson }} @@ -363,14 +364,16 @@ } {{ range $k, $v := $r }} + # the basic path variant, "/" or "/one/two" location = {{ index $v "path" }} { set $backend {{ index $v "target" }}; rewrite ^{{ index $v "path" }}(.*)$ /$1 break; proxy_pass http://$backend; } - {{ if not (eq (index $v "path") "/") }} - location {{ index $v "path" }}/ { + # The prefix match variants are included by default but can be excluded via 'score-compose.score.dev/route-provisioner-path-type' annotation + {{ if eq (index $v "path_type") "Prefix" }} + location {{ index $v "path" | trimSuffix "/" }}/ { set $backend {{ index $v "target" }}; rewrite ^{{ index $v "path" }}/(.*)$ /$1 break; proxy_pass http://$backend; diff --git a/internal/command/generate_test.go b/internal/command/generate_test.go index 31c625d..1121ed3 100644 --- a/internal/command/generate_test.go +++ b/internal/command/generate_test.go @@ -806,6 +806,9 @@ resources: port: foo r3: type: route + metadata: + annotations: + score-compose.score.dev/route-provisioner-path-type: Exact params: host: localhost2 path: /third @@ -822,23 +825,34 @@ resources: assert.Len(t, sd.State.Workloads, 1) assert.Len(t, sd.State.Resources, 3) x := sd.State.SharedState["default-provisioners-routing-instance"].(map[string]interface{}) - assert.Contains(t, x["instanceServiceName"].(string), "routing-") + instanceServiceName := x["instanceServiceName"].(string) + assert.Contains(t, instanceServiceName, "routing-") delete(x, "instanceServiceName") assert.Equal(t, map[string]interface{}{ "default-provisioners-routing-instance": map[string]interface{}{ "hosts": map[string]interface{}{ "localhost1": map[string]interface{}{ - "route.default#example.r1": map[string]interface{}{"path": "/first", "port": 8080, "target": "example-example:8080"}, - "route.default#example.r2": map[string]interface{}{"path": "/second", "port": 8080, "target": "example-example:8080"}, + "route.default#example.r1": map[string]interface{}{"path": "/first", "port": 8080, "target": "example-example:8080", "path_type": "Prefix"}, + "route.default#example.r2": map[string]interface{}{"path": "/second", "port": 8080, "target": "example-example:8080", "path_type": "Prefix"}, }, "localhost2": map[string]interface{}{ - "route.default#example.r3": map[string]interface{}{"path": "/third", "port": 8080, "target": "example-example:8080"}, + "route.default#example.r3": map[string]interface{}{"path": "/third", "port": 8080, "target": "example-example:8080", "path_type": "Exact"}, }, }, "instancePort": 8080, }, }, sd.State.SharedState) + // validate that the wildcard routes don't exist for /third + raw, err := os.ReadFile(filepath.Join(td, ".score-compose", "mounts", instanceServiceName, "nginx.conf")) + assert.NoError(t, err) + assert.Contains(t, string(raw), `location = /first`) + assert.Contains(t, string(raw), `location /first/`) + assert.Contains(t, string(raw), `location = /second`) + assert.Contains(t, string(raw), `location /second/`) + assert.Contains(t, string(raw), `location = /third`) + assert.NotContains(t, string(raw), `location /third/`) + t.Run("validate compose spec", func(t *testing.T) { if os.Getenv("NO_DOCKER") != "" { t.Skip("NO_DOCKER is set")