Documentation
¶
Overview ¶
Package proxy serves the Temporal WorkflowService on a local unix socket, forwarding every request to an upstream Temporal frontend over gRPC. The socket path is derived from the upstream host:port, so local workers connect without TLS while the upstream hop stays secured.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var Module = fx.Options(fx.Invoke(func(p ProxyParams) error { if p.Config.Encryption.Enabled && p.Vault == nil { return fmt.Errorf("encryption is enabled but no vault was provided") } // Built once (not per upstream) so its collectors register with Prometheus // exactly once; a per-upstream build would panic on duplicate registration. var encReporter *Reporter if p.Vault != nil { encReporter = NewReporter(p.Factory.ForSubsystem("encryption")) } conns := make([]*connect.Conn, 0, len(p.Config.Upstreams)) for i := range p.Config.Upstreams { up := &p.Config.Upstreams[i] if err := up.Validate(); err != nil { return fmt.Errorf("invalid upstream configuration: %w", err) } // Request-independent dial options: namespace translation and outbound // credentials. Per-request credentials are added by the resolver. var dialOpts []grpc.DialOption rules := &up.Namespaces.Rules if rules.Configured() { dialOpts = append(dialOpts, translationDialOptions(p.Translator, rules.Remote, rules.Local)...) } cp, err := outbound.CredentialProviderFor(up.Credentials) if err != nil { return fmt.Errorf("invalid credentials for upstream %q: %w", up.Name, err) } if cp != nil { dialOpts = append(dialOpts, outbound.DialOptions(cp)...) } if p.Vault != nil { enc, err := EncryptionInterceptor(p.Config.Encryption.Enabled, p.Vault, encReporter) if err != nil { return fmt.Errorf("failed to build encryption interceptor for upstream %q: %w", up.Name, err) } dialOpts = append(dialOpts, grpc.WithChainUnaryInterceptor(enc)) } res, err := upstreamResolver(up, dialOpts) if err != nil { return err } conn, err := connect.NewConn(p.Pool.ConnOrCreate, res) if err != nil { return err } conns = append(conns, conn) var opts []Option if p.Logger != nil { opts = append(opts, WithLogger(p.Logger)) } svr, err := New(up.Listen.HostPort, conn, opts...) if err != nil { return fmt.Errorf("failed to create proxy for upstream %q: %w", up.Name, err) } p.Lifecycle.Append(fx.Hook{ OnStart: func(context.Context) error { lis, err := svr.Listen(p.Context) if err != nil { return fmt.Errorf("failed to start proxy for upstream %q: %w", up.Name, err) } go func() { defer func() { _ = lis.Close() }() if err := svr.Start(p.Context, lis); err != nil { _ = p.Shutdowner.Shutdown(fx.ExitCode(1)) } }() return nil }, OnStop: svr.Stop, }) } p.Lifecycle.Append(fx.StartHook(func(ctx context.Context) error { if err := connect.WaitReady(ctx, conns...); err != nil { return fmt.Errorf("upstream connection not ready: %w", err) } return nil })) return nil }))
Module is the fx module that constructs the proxy Server from ProxyParams and binds its lifecycle to the application.
Functions ¶
func EncryptionInterceptor ¶
EncryptionInterceptor returns a unary client interceptor that opens inbound response payloads using v and, when enabled is true, seals outbound request payloads as well. Sealing is gated so encryption can be turned off for new traffic while still opening data sealed earlier: inbound decryption always runs. Each payload is sealed under the DEK for the request's namespace, read from the outgoing gRPC metadata via meta.NamespaceFrom, so the upstream never sees plaintext while local workers still exchange cleartext. On the way back only payloads this interceptor sealed (identified by the encryptionEncoding marker) are opened; anything else passes through untouched. Search attributes are skipped so they stay queryable upstream. r records the duration and result of every seal/open through VaultOp. It returns an error only if the underlying visitor interceptor cannot be constructed.
Types ¶
type DynamicResolver ¶
type DynamicResolver struct {
// contains filtered or unexported fields
}
DynamicResolver is a connect.Resolver that renders an upstream's dial target (and optional TLS server name) per request from the local namespace and request metadata. It always reports IsStatic as false, so a connect.Conn built from it resolves lazily on every call. A non-templated hostPort renders to itself, so a DynamicResolver also serves upstreams with a fixed address. Construct one with NewDynamicResolver.
func NewDynamicResolver ¶
func NewDynamicResolver(up *config.Upstream, opts ...ResolverOption) (*DynamicResolver, error)
NewDynamicResolver builds a DynamicResolver for up. It compiles the hostPort and TLS server-name templates (failing if either is malformed) and applies opts. By default the remote namespace equals the local one and no dial options are added; use WithRemoteNamespacer and WithOptionsFactory to change that.
func (*DynamicResolver) IsStatic ¶
func (r *DynamicResolver) IsStatic() bool
IsStatic reports that a DynamicResolver always resolves per request.
func (*DynamicResolver) Resolve ¶
func (r *DynamicResolver) Resolve(ctx context.Context) (string, string, []grpc.DialOption, error)
Resolve renders the dial target and server name from ctx and returns the pool cache key, the dial target, and the dial options. The cache key combines the target and rendered server name so that two requests to the same address with different server names get distinct pooled connections. It fails with codes.Internal (naming the upstream and template) when a template fails to render, the rendered address is empty or malformed, or the options factory errors; nothing is dialed in those cases.
type Option ¶
type Option func(*Options)
Option configures a Server via New.
func WithLogger ¶
WithLogger sets the logger used by the proxy.
type Options ¶
type Options struct {
// contains filtered or unexported fields
}
Options configures a Server at construction time.
type ProxyParams ¶
type ProxyParams struct {
fx.In
Lifecycle fx.Lifecycle
Shutdowner fx.Shutdowner
// Required values
Context context.Context
Config *config.Config
Translator *protoutil.Translator
Pool *connect.Pool
Vault *crypto.Vault
Factory *metrics.Factory
// Optional values
Logger logger.Logger `optional:"true"`
}
ProxyParams collects the fx-provided dependencies needed to construct and run the proxy Server. Context, Config, Translator, and Pool are required; Logger is optional and falls back to the default used by New when not supplied. protoutil.Module provides the Translator and connect.Module provides the Pool in the assembled application.
type Reporter ¶
type Reporter struct {
// contains filtered or unexported fields
}
Reporter records envelope-operation telemetry to Prometheus: each seal (encrypt) and open (decrypt) the encryption interceptor performs, timed end to end, including any KEK wrap or unwrap and any DEK cache lookup along the way. The AES-step duration alone is owned by internal/kms. The namespace label is unbounded, so handles are resolved per call via WithLabelValues rather than pre-computed. A Reporter is safe for concurrent use.
func NewReporter ¶
NewReporter builds the Prometheus-backed vault-operation Reporter. f must already be scoped to the "encryption" subsystem by the caller.
type ResolverOption ¶
type ResolverOption func(*DynamicResolver)
ResolverOption configures a DynamicResolver at construction.
func WithOptionsFactory ¶
func WithOptionsFactory(f func(RouteData) ([]grpc.DialOption, error)) ResolverOption
WithOptionsFactory sets the function that produces the dial options for a resolved request. It receives the rendered host and server name via RouteData.
func WithRemoteNamespacer ¶
func WithRemoteNamespacer(f func(string) string) ResolverOption
WithRemoteNamespacer sets the function that maps the local namespace to the remote one, making RemoteNamespace available to the templates.
type RouteData ¶
type RouteData struct {
template.UpstreamContext
ResolvedServerName string
}
RouteData is passed to the options factory once a request has been resolved. It carries the template context used for rendering plus the resolved TLS server name, so the factory can build dial options (e.g. credentials whose SNI depends on the rendered server name).
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server proxies the Temporal WorkflowService. It re-serves an upstream frontend on a local unix socket, letting local workers connect without TLS while the upstream hop stays secured. The upstream connection(s) it forwards to are owned by the shared connect.Pool, not by this Server.
func New ¶
New constructs a Server that forwards WorkflowService traffic to the upstream reachable through cc. The local listener is a unix socket whose path is derived from hostPort. cc is typically a resolvingConn; the connection(s) it uses are owned by the shared pool, not by this Server.
func (*Server) Listen ¶
Listen removes any socket left behind by a prior run and binds the proxy's local unix socket, returning the listener. Binding is separate from Start so callers can bind synchronously during startup (the socket is then listening, and the OS backlogs connections) before serving in the background, ensuring no request is routed to an unbound socket.