Skip to content

[dataplane]: Translate the methods Cloud does not serve on a frontend - #154

Open
Vaughan-Temporal wants to merge 1 commit into
vaughan/translation-listnamespacesfrom
vaughan/translation-integration
Open

[dataplane]: Translate the methods Cloud does not serve on a frontend#154
Vaughan-Temporal wants to merge 1 commit into
vaughan/translation-listnamespacesfrom
vaughan/translation-integration

Conversation

@Vaughan-Temporal

@Vaughan-Temporal Vaughan-Temporal commented Sep 4, 2026

Copy link
Copy Markdown

Stacked on #153. This is the first PR in the stack that changes behaviour.

Nothing installed the translation registry, so the mapping from #153 was compiled in but never reached. This installs it
on a Cloud upstream — which needs no configuration to say so. Building on #147: an upstream is already known to be
Temporal Cloud, by declaration or by its address, and translating the methods Cloud does not serve on a frontend follows
from exactly that.

Design

The control plane is derived from the upstreamcloud.APIHostPort, the upstream's own credentials (one Temporal
Cloud API key authorises both frontend and control plane), and a default outbound TLS configuration. TLS is not
inherited: the control plane is a different host, so the upstream's server name or client certificate would not apply.

Where CloudService lives is a fixed fact about Cloud, not a policy, so it travels with the translation rather than
being restated as routing an operator could get wrong in either direction.

An on-prem upstream is never translated — it is not IsCloud(), so the interceptor is never installed on it. The
guard is at installation rather than per-call deliberately: a runtime check on the dial target would break private-link
Cloud upstreams, whose per-VPC hostnames carry no Cloud domain and are the reason cloud: true exists.

Config

Everything above needs no configuration. One optional block overrides it:

apiTranslations:
  enabled: false          # default true
  cloudApi:               # optional
    hostPort: saas-api.tmprl.cloud:443
    tls: {}
    credentials:
      static:
        apiKey: ${TEMPORAL_API_KEY}
  • enabled: false forwards the method untranslated, for a Cloud upstream where the untranslated failure is preferred to
    a translated answer. Enabled is a *bool because a plain one cannot distinguish absent from false, and the default
    has to be on — translation makes a method work that cannot work otherwise, so an operator opts out of a fix.
  • cloudApi: reaches a different Cloud environment, or supplies credentials or TLS the upstream does not carry.

Two subtleties worth reviewing

Interceptor ordering is load-bearing. Translation is installed last so it is innermost. Via leaves the chain when
a translation fires, so anything below it would not run for a translated call. Innermost, the namespace translator above
still sees the converted reply — a configured suffix rewrites payments.a1b2c to payments with no special casing.
Move it up and that silently stops working while every other test still passes, so
TestEndToEndNamespaceRulesApplyToATranslatedReply guards it. Mutation-checked.

Pool keying. staticResolver keys the pool by dial target, safe "because upstream hostPorts are unique (enforced by
config)" — see its comment. Control-plane connections break that: every Cloud upstream resolves to the same address, so
two upstreams with different API keys would share whichever connection was created first, and with it the first's
credentials. keyedResolver keys on a derived name instead.

A non-Cloud cloudApi: address warns rather than fails. A test double or private environment legitimately carries no
Cloud domain and that is indistinguishable from a typo — matching how #147 handles a namespace it cannot validate on a
templated upstream.

The connection is not opened eagerly: translation is incidental to normal traffic, so an unreachable control plane must
not stop the proxy serving everything else.

mTLS upstreams must configure a key

The Cloud Ops API accepts an API key only — unlike a namespace frontend it does not
accept mTLS. So a Cloud upstream authenticating with a client certificate has no credential to inherit, and must name an
API key under apiTranslations.cloudApi.credentials or its translated methods are refused. That is the case the block
exists for; documented on the type.

Scope

+768 / −0, 7 files. dataplane.go is the whole integration in 96 lines. Six e2e tests drive the full stack (gateway →
router → forwarder → interceptor) against a fake CloudService.


Stack

PR
1 #152 translation library — mechanism only
2 #153 ListNamespaces → CloudService.GetNamespaces mapping
3 #154 integration — first behaviour change
4 #155 listnamespace example

Merge bottom-up. Each PR targets the one above it, so its diff shows only its own change.

🤖 Generated with Claude Code

@Vaughan-Temporal
Vaughan-Temporal requested review from a team and pseudomuto as code owners September 4, 2026 16:39
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@codecov-commenter

codecov-commenter commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 16 lines in your changes missing coverage. Please review.

📢 Thoughts on this report? Let us know!

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Vaughan-Temporal
Vaughan-Temporal force-pushed the vaughan/translation-integration branch from afc8c0b to ab16050 Compare September 4, 2026 17:24
Nothing installed the translation registry, so the ListNamespaces translation was
compiled in but never reached. Install it on a Cloud upstream, which needs no new
configuration to say so: an upstream is already known to be Temporal Cloud, by
declaration or by its address, and translating the methods Cloud does not serve
on a frontend follows from exactly that.

The control plane is derived from the upstream: cloud.APIHostPort, the upstream's
own credentials (one Temporal Cloud API key authorizes both the frontend and the
control plane), and a default outbound TLS configuration. TLS is not inherited,
since the control plane is a different host and the upstream's server name or
client certificate would not apply to it. Where CloudService lives is a fixed
fact about Cloud rather than a policy, so it travels with the translation instead
of being restated as routing an operator could get wrong in either direction.

Deriving per upstream rather than sharing one connection matters for pooling. A
static resolver keys the pool by dial target, which is unique among upstreams but
not among control planes: every Cloud upstream resolves to the same address, so a
shared key would hand the second upstream whichever connection the first created,
and with it the first's credentials. The pool key is the derived name instead.

The connection is not opened eagerly. Translation is incidental to an upstream's
normal traffic, so a control plane that is unreachable must not stop the proxy
serving everything else.

An optional cloudApi block overrides the endpoint for another Cloud environment,
or supplies credentials or TLS the upstream does not carry. An address that is
not a Cloud endpoint is reported at startup rather than rejected, since a test
double or a private environment legitimately carries no Cloud domain and that
cannot be told apart from a typo.

Translation is installed last so it is the innermost interceptor. Via leaves the
chain when a translation fires, so anything below it would not run for a
translated call; innermost, the namespace translator and payload codecs above
still see the method and message types the caller asked for, and a configured
namespace suffix rewrites the converted reply with no special casing. That
ordering is load-bearing and otherwise invisible, so it has a test of its own.
@Vaughan-Temporal
Vaughan-Temporal force-pushed the vaughan/translation-integration branch from ab16050 to 787a9e4 Compare September 4, 2026 17:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants