Files
93cc503639 Support Internal Nexus Callback Routing (#8308)
## What changed?
This PR introduces two key features to enable out-of-the-box Nexus
functionality with zero configuration:

1. **Always allow `temporal://system` callback URLs**: The
`component.callbacks.allowedAddresses` configuration now automatically
allows `temporal://system` URLs regardless of configured address rules.
This special scheme+host combination bypasses normal URL validation and
is always permitted.

2. **New UseSystemCallbackURL toggle**: Added
`component.nexusoperations.useSystemCallbackURL` dynamic config that
controls callback URL generation for worker targets:
- When `false` (default): Uses the existing callback URL template
approach
   - When `true`: Worker targets automatically get `temporal://system`
- External targets continue to use template-generated URLs regardless of
this setting
- NOTE: if this toggle is `true` HTTPCallerProvider must be able to
route internal requests based on theSource and Token headers. It should
also override the path to `/nexus/callback`. See the HTTPCallerProvider
code changes here.

3. **Callback URL Validation** has been refactored. The
`AddressMatchRules` struct now includes a `Validate` method that handles
the special `temporal://system` case and applies configured rules for
other URLs.
4. **Callback Source Header**: will no longer be populated for external
Targets.

## Why?
Once `component.nexusoperations.useSystemCallbackURL` is enabled by
default in a future release, these changes will allow Self-Hosted
Temporal Servers to support Nexus with zero dynamic configuration
See this issue: https://github.com/temporalio/temporal/issues/8298

## How did you test it?
- [x] built
- [x] run locally and tested manually
- [x] covered by existing tests
- [x] added new unit test(s)
- [x] added new functional test(s)

---------

Co-authored-by: alex stanfield <[email protected]>
Co-authored-by: Roey Berman <[email protected]>
2025-09-29 21:15:16 +00:00

72 lines
2.4 KiB
Go

package callbacks
import (
"net/http"
"go.temporal.io/server/common"
"go.temporal.io/server/common/cluster"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/log/tag"
"go.temporal.io/server/common/nexus"
)
// Header key used to identify callbacks that originate from and target the same cluster.
// Note: this is the nexusoperations.NexusCallbackSourceHeader stripped of Nexus-Callback-
const callbackSourceHeader = "source"
func routeRequest(
r *http.Request,
clusterMetadata cluster.Metadata,
httpClientCache *cluster.FrontendHTTPClientCache,
defaultClient *http.Client,
localClient *common.FrontendHTTPClient,
logger log.Logger,
) (*http.Response, error) {
// This source header is populated in nexusoperations/executors (via the ClientProvider) for worker targets
// if this header is not populated then we assume it's and external target.
if r.Header == nil || r.Header.Get(callbackSourceHeader) == "" {
return defaultClient.Do(r)
}
// If we got here, we assume that the endpoint in the original call was a worker target, and we should route
// internally, either to a local frontend, or one of the other connected clusters' frontends.
var frontendClient *common.FrontendHTTPClient
callbackSource := r.Header.Get(callbackSourceHeader)
for clusterName, clusterInfo := range clusterMetadata.GetAllClusterInfo() {
if callbackSource == clusterInfo.ClusterID {
if clusterMetadata.GetCurrentClusterName() == clusterName {
frontendClient = localClient
} else {
fec, err := httpClientCache.Get(clusterName)
if err != nil {
logger.Warn(
"HTTPCallerProvider unable to get FrontendHTTPClient for callback target cluster. Using local HTTP Client.",
tag.SourceCluster(clusterMetadata.GetCurrentClusterName()),
tag.TargetCluster(clusterName),
tag.Error(err),
)
frontendClient = localClient
} else {
frontendClient = fec
}
}
break
}
}
if frontendClient == nil {
// This can happen when a cluster is disconnected.
logger.Warn(
"HTTPCallerProvider unable to find the target cluster. Using local HTTP Client.",
tag.SourceCluster(clusterMetadata.GetCurrentClusterName()),
)
frontendClient = localClient
}
if r.URL.String() == nexus.SystemCallbackURL {
r.URL.Path = nexus.PathCompletionCallbackNoIdentifier
}
r.URL.Scheme = frontendClient.Scheme
r.URL.Host = frontendClient.Address
r.Host = frontendClient.Address
return frontendClient.Do(r)
}