-
Notifications
You must be signed in to change notification settings - Fork 0
[template]: Add hostPort/serverName templating #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
pseudomuto
wants to merge
1
commit into
main
Choose a base branch
from
host_templating
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| // Package template renders the templated fields used by the proxy against | ||
| // per-request values. | ||
| // | ||
| // Templates are ordinary text/template source. They are bound to the context | ||
| // they render against: | ||
| // | ||
| // - [ParseRouting] yields a template over a [RoutingContext] (local namespace | ||
| // and metadata), used while routing rules choose an upstream. The remote | ||
| // namespace is not available here because translation is defined per | ||
| // upstream. | ||
| // - [ParseUpstream] yields a template over an [UpstreamContext] (local and | ||
| // remote namespace, and metadata), used to render the chosen upstream's | ||
| // hostPort and serverName. | ||
| // | ||
| // A reference to a field absent from the bound context (such as RemoteNamespace | ||
| // in a routing template) fails at parse time. A reference to an absent metadata | ||
| // key renders as the empty string. | ||
| package template | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| package template | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "strings" | ||
| texttemplate "text/template" | ||
| ) | ||
|
|
||
| var probeMeta = map[string]string{ | ||
| "dc": "probe", | ||
| "x-cluster": "probe", | ||
| } | ||
|
|
||
| type ( | ||
| // RoutingContext is available when evaluating routing rules, before an | ||
| // upstream has been selected. It deliberately omits the remote namespace: | ||
| // namespace translation is defined per upstream, so the remote name is not | ||
| // known until routing has chosen one. | ||
| RoutingContext struct { | ||
| LocalNamespace string | ||
| Metadata map[string]string | ||
| } | ||
|
|
||
| // UpstreamContext is available when rendering an upstream's hostPort and | ||
| // serverName, after routing has selected the upstream and the remote | ||
| // namespace is known. | ||
| UpstreamContext struct { | ||
| LocalNamespace string | ||
| RemoteNamespace string | ||
| Metadata map[string]string | ||
| } | ||
|
|
||
| // Template is a compiled template bound to the context type T it renders | ||
| // against. Construct one with [ParseRouting] or [ParseUpstream]. | ||
| Template[T any] struct { | ||
| tmpl *texttemplate.Template | ||
| raw string | ||
| } | ||
| ) | ||
|
|
||
| // ParseRouting compiles s into a [Template] rendered against a [RoutingContext]. | ||
| // A reference to a field absent from that context (such as RemoteNamespace) | ||
| // fails here rather than at request time. | ||
| func ParseRouting(s string) (*Template[RoutingContext], error) { | ||
| return parse(s, RoutingContext{LocalNamespace: "probe", Metadata: probeMeta}) | ||
| } | ||
|
|
||
| // ParseUpstream compiles s into a [Template] rendered against an | ||
| // [UpstreamContext]. | ||
| func ParseUpstream(s string) (*Template[UpstreamContext], error) { | ||
| return parse(s, UpstreamContext{LocalNamespace: "probe", RemoteNamespace: "probe", Metadata: probeMeta}) | ||
| } | ||
|
|
||
| // Must returns t or panics if err is non-nil. It wraps a Parse call for | ||
| // package-level variables and tests where an invalid template is a programming | ||
| // error. | ||
| func Must[T any](t *Template[T], err error) *Template[T] { | ||
| if err != nil { | ||
| panic(err) | ||
| } | ||
|
|
||
| return t | ||
| } | ||
|
|
||
| // Render evaluates the template against ctx. A reference to an absent metadata | ||
| // key renders as the empty string. | ||
| func (t *Template[T]) Render(ctx T) (string, error) { | ||
| var sb strings.Builder | ||
| if err := t.tmpl.Execute(&sb, ctx); err != nil { | ||
| return "", fmt.Errorf("template: render %q: %w", t.raw, err) | ||
| } | ||
|
|
||
| return sb.String(), nil | ||
| } | ||
|
|
||
| // String returns the raw template source. | ||
| func (t *Template[T]) String() string { | ||
| return t.raw | ||
| } | ||
|
|
||
| // parse compiles s and validates it by rendering against probe, surfacing | ||
| // references to unknown fields at parse time. Absent metadata keys render empty | ||
| // under missingkey=zero and so do not trip the check. | ||
|
pseudomuto marked this conversation as resolved.
|
||
| func parse[T any](s string, probe T) (*Template[T], error) { | ||
| tmpl, err := texttemplate.New("template").Option("missingkey=zero").Parse(s) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("template: parse %q: %w", s, err) | ||
| } | ||
|
|
||
| t := &Template[T]{tmpl: tmpl, raw: s} | ||
| if _, err := t.Render(probe); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return t, nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| package template_test | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/temporalio/temporal-proxy/internal/template" | ||
| ) | ||
|
|
||
| func TestParseUpstream_Render(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| tmpl string | ||
| ctx template.UpstreamContext | ||
| want string | ||
| }{ | ||
| { | ||
| name: "static string round-trips", | ||
| tmpl: "localhost:7233", | ||
| want: "localhost:7233", | ||
| }, | ||
| { | ||
| name: "remote namespace in hostPort", | ||
| tmpl: "{{ .RemoteNamespace }}.acme-cloud.tmprl.cloud:7233", | ||
| ctx: template.UpstreamContext{RemoteNamespace: "payments"}, | ||
| want: "payments.acme-cloud.tmprl.cloud:7233", | ||
| }, | ||
| { | ||
| name: "remote namespace in serverName", | ||
| tmpl: "{{ .RemoteNamespace }}.aws.tmprl.cloud", | ||
| ctx: template.UpstreamContext{RemoteNamespace: "payments"}, | ||
| want: "payments.aws.tmprl.cloud", | ||
| }, | ||
| { | ||
| name: "local namespace", | ||
| tmpl: "{{ .LocalNamespace }}", | ||
| ctx: template.UpstreamContext{LocalNamespace: "orders"}, | ||
| want: "orders", | ||
| }, | ||
| { | ||
| name: "metadata dot form", | ||
| tmpl: "{{ .Metadata.dc }}.internal", | ||
| ctx: template.UpstreamContext{Metadata: map[string]string{"dc": "west"}}, | ||
| want: "west.internal", | ||
| }, | ||
| { | ||
| name: "metadata index form", | ||
| tmpl: `{{ index .Metadata "x-cluster" }}`, | ||
| ctx: template.UpstreamContext{Metadata: map[string]string{"x-cluster": "c1"}}, | ||
| want: "c1", | ||
| }, | ||
| { | ||
| name: "absent metadata key renders empty", | ||
| tmpl: "{{ .Metadata.dc }}.internal", | ||
| ctx: template.UpstreamContext{Metadata: map[string]string{}}, | ||
| want: ".internal", | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| tmpl, err := template.ParseUpstream(tt.tmpl) | ||
| require.NoError(t, err) | ||
| require.Equal(t, tt.tmpl, tmpl.String()) | ||
|
|
||
| got, err := tmpl.Render(tt.ctx) | ||
| require.NoError(t, err) | ||
| require.Equal(t, tt.want, got) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestParseRouting_Render(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| tmpl string | ||
| ctx template.RoutingContext | ||
| want string | ||
| }{ | ||
| { | ||
| name: "local namespace", | ||
| tmpl: "{{ .LocalNamespace }}", | ||
| ctx: template.RoutingContext{LocalNamespace: "orders"}, | ||
| want: "orders", | ||
| }, | ||
| { | ||
| name: "metadata index form", | ||
| tmpl: `{{ index .Metadata "x-cluster" }}`, | ||
| ctx: template.RoutingContext{Metadata: map[string]string{"x-cluster": "c1"}}, | ||
| want: "c1", | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| tmpl, err := template.ParseRouting(tt.tmpl) | ||
| require.NoError(t, err) | ||
|
|
||
| got, err := tmpl.Render(tt.ctx) | ||
| require.NoError(t, err) | ||
| require.Equal(t, tt.want, got) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // A routing template cannot reference RemoteNamespace (it is not known until an | ||
| // upstream is selected); the same template is valid for an upstream. | ||
| func TestRemoteNamespace_scopedToUpstream(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| const tmpl = "{{ .RemoteNamespace }}.acme-cloud.tmprl.cloud:7233" | ||
|
|
||
| _, err := template.ParseRouting(tmpl) | ||
| require.Error(t, err) | ||
| require.Contains(t, err.Error(), "RemoteNamespace") | ||
|
|
||
| _, err = template.ParseUpstream(tmpl) | ||
| require.NoError(t, err) | ||
| } | ||
|
|
||
| func TestParse_errors(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| tmpl string | ||
| }{ | ||
| {name: "malformed syntax", tmpl: "{{ .LocalNamespace"}, | ||
| {name: "unknown field", tmpl: "{{ .Foo }}.acme.com"}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| _, rErr := template.ParseRouting(tt.tmpl) | ||
| require.Error(t, rErr) | ||
|
|
||
| _, uErr := template.ParseUpstream(tt.tmpl) | ||
| require.Error(t, uErr) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestMust(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| require.NotPanics(t, func() { template.Must(template.ParseUpstream("{{ .RemoteNamespace }}.acme.com")) }) | ||
| require.NotPanics(t, func() { template.Must(template.ParseRouting("{{ .LocalNamespace }}")) }) | ||
| require.Panics(t, func() { template.Must(template.ParseRouting("{{ .RemoteNamespace }}")) }) | ||
| require.Panics(t, func() { template.Must(template.ParseUpstream("{{ .Foo }}")) }) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.