Documentation
¶
Index ¶
- Variables
- func DefaultTenantIDPattern() *regexp.Regexp
- func FanOutRetentionCleanup(ctx context.Context, log logger.Logger, tenants []string, ...) error
- func ForEachTenant(ctx context.Context, tenants []string, ...) error
- func GetTenant(ctx context.Context) (string, bool)
- func MayReadPath(resolver TenantResolver) bool
- func Resolve(ctx context.Context, resolver TenantResolver, req *http.Request) (tenantID string, fromPath bool, err error)
- func SetTenant(ctx context.Context, tenantID string) context.Context
- func TenantID(ctx context.Context) (string, error)
- func ValidateTenantID(id string) error
- type CompositeResolver
- type HeaderResolver
- type PathResolver
- type RetentionDelete
- type SubdomainResolver
- type TenantChecker
- type TenantResolver
- type ValidatingResolver
Constants ¶
This section is empty.
Variables ¶
var ErrInvalidTenantID = errors.New("multitenant: tenant id does not match the tenant-ID grammar")
ErrInvalidTenantID reports a tenant identifier that does not match DefaultTenantIDPattern.
var ErrNoTenant = errors.New("multitenant: no tenant in context")
ErrNoTenant reports a context that carries no tenant. It is the error form of GetTenant's ok result, so every lane answers "no tenant" the same way.
var ErrTenantResolutionFailed = errors.New("tenant resolution failed")
ErrTenantResolutionFailed is returned when a resolver cannot determine the tenant identifier.
var ErrUnknownTenant error = &unknownTenantError{}
ErrUnknownTenant is returned for a well-formed tenant ID that names no tenant the deployment serves. The HTTP layer answers it as a client fault (400). It wraps ErrTenantResolutionFailed, so a tenant-owned family's Tenant step that returns it is a client fault.
var ErrUntrustedForwardedHost = errors.New("multitenant: X-Forwarded-Host from an untrusted peer")
ErrUntrustedForwardedHost is returned by a SubdomainResolver with TrustProxies set when a peer it does not trust sends X-Forwarded-Host. CompositeResolver stops on it instead of trying the next sub-resolver.
Functions ¶
func DefaultTenantIDPattern ¶ added in v0.61.0
DefaultTenantIDPattern returns the framework's tenant identifier grammar.
func FanOutRetentionCleanup ¶ added in v0.42.0
func FanOutRetentionCleanup( ctx context.Context, log logger.Logger, tenants []string, getDB func(context.Context) (dbtypes.Interface, error), retention time.Duration, name string, del RetentionDelete, ) error
FanOutRetentionCleanup runs a retention delete once per tenant, resolving each tenant's database via getDB with that tenant injected into the context (SetTenant). Per-tenant errors AND panics are collected with errors.Join so one unhealthy tenant cannot block the others. Single-tenant callers pass tenants=[""] (SetTenant("") is a no-op). name labels the job in log and error messages (e.g. "outbox", "inbox").
This is shared by the outbox and inbox cleanup jobs, whose Execute bodies are otherwise identical — only the store delete and the label differ.
func ForEachTenant ¶ added in v0.72.0
func ForEachTenant(ctx context.Context, tenants []string, fn func(ctx context.Context, tenantID string) error) error
ForEachTenant runs fn once per tenant, in the given order, each inside that tenant's own lease scope (ADR-032): the tenant's context carries the tenant (SetTenant; "" runs fn once with no tenant) and a fresh scope that shadows any enclosing request, message or job scope, drained when fn returns or panics. A tenant-sweeping job then holds about one tenant's leased handles at a time instead of every tenant's until the job returns.
ctx is checked before each tenant: once it is done no further tenant starts and ctx.Err() joins the result. Errors from fn are joined unwrapped (fn receives the tenant ID and owns its text) and the sweep continues. A panic in fn is not recovered; it propagates after that tenant's scope has drained. A nil or empty list returns nil.
Every borrow inside fn must use fn's ctx or a context derived from it: a borrow on the outer context lands in the enclosing scope and is not bounded. A handle, database.Session or transaction obtained inside fn must not be used after fn returns. Releasing a lease does not close a cached handle, so the bound matters only when a sweep covers more tenants than the manager's max size. The caller supplies the list: Config.PerTenantJobKeys() for static tenants, or its own list for dynamic sources.
func MayReadPath ¶ added in v0.73.0
func MayReadPath(resolver TenantResolver) bool
MayReadPath reports whether Resolve can answer fromPath true for resolver: it is false only when every member able to produce an id is a *HeaderResolver or a *SubdomainResolver. It walks the resolver tree, so call it once at setup and keep the answer. A cycle (a *CompositeResolver or *ValidatingResolver reachable from itself) fails closed: the walk answers true rather than recurse without end.
func Resolve ¶ added in v0.73.0
func Resolve(ctx context.Context, resolver TenantResolver, req *http.Request) (tenantID string, fromPath bool, err error)
Resolve resolves the tenant as resolver.ResolveTenant does, with the same error and, when err is nil, the same id; on error the id is always empty and fromPath false. It reports whether the id may have come from the request path: fromPath is true when a *PathResolver produced it, or a resolver whose provenance is unknown. Provenance is judged on the concrete framework types alone, so any other type, including one embedding or wrapping a framework resolver, counts as unknown.
func TenantID ¶ added in v0.61.0
TenantID is GetTenant in error form: the tenant identifier, or ErrNoTenant.
func ValidateTenantID ¶ added in v0.61.0
ValidateTenantID checks an identifier against DefaultTenantIDPattern.
SECURITY: the identifier is caller-written (an HTTP header, a message stamp), so the error carries its byte length only — never the value itself.
Types ¶
type CompositeResolver ¶
type CompositeResolver struct {
Resolvers []TenantResolver
TenantRegex *regexp.Regexp // Optional validation pattern
}
CompositeResolver tries multiple resolvers until one succeeds. ErrUntrustedForwardedHost is terminal: it is returned at once and no later resolver is tried.
func (*CompositeResolver) ResolveTenant ¶
ResolveTenant implements TenantResolver.
type HeaderResolver ¶
type HeaderResolver struct {
HeaderName string
}
HeaderResolver extracts the tenant identifier from a configured request header.
func (*HeaderResolver) ResolveTenant ¶
ResolveTenant implements TenantResolver.
type PathResolver ¶ added in v0.34.0
PathResolver extracts the tenant identifier from a path segment.
Segment is 1-indexed (segment 1 is the first non-empty path part after the leading slash). When Prefix is non-empty, the resolver only attempts extraction for requests whose path equals Prefix exactly or starts with "Prefix/" — non-matching paths return ErrTenantResolutionFailed so the composite chain (if any) can fall through to other resolvers.
The resolver reads the path the framework's echo router matches: URL.RawPath when net/url keeps one, URL.Path otherwise. The prefix is compared and the segment returned as sent, still percent-escaped and untrimmed, exactly as the router binds a path parameter at that position (/a%2Fb/acme/x, segment 2, is "acme"; /%61cme/x, segment 1, is "%61cme", which the tenant-ID grammar refuses). A router that unescapes path values, such as net/http's ServeMux, can bind a different value for the same segment.
func (*PathResolver) ResolveTenant ¶ added in v0.34.0
ResolveTenant implements TenantResolver.
type RetentionDelete ¶ added in v0.42.0
type RetentionDelete func(ctx context.Context, db dbtypes.Interface, cutoff time.Time) (int64, error)
RetentionDelete removes rows older than cutoff from the resolved tenant database. It is the per-vendor delete a retention-cleanup job runs (e.g. the outbox's DeletePublished or the inbox's DeleteProcessed) and matches those store methods' signatures directly.
type SubdomainResolver ¶
type SubdomainResolver struct {
RootDomain string
TrustProxies bool
// TrustedProxies adds peer ranges trusted to send X-Forwarded-Host, on top of
// loopback, link-local, private and unix-socket peers.
TrustedProxies []*net.IPNet
}
SubdomainResolver extracts the tenant identifier from the request host.
func (*SubdomainResolver) ResolveTenant ¶
ResolveTenant implements TenantResolver.
type TenantChecker ¶ added in v0.73.0
TenantChecker reports whether a tenant id names a tenant the deployment holds; *config.TenantStore implements it. The check is in memory, a stated Context-First exception: no context, no I/O, no blocking. A source backed by a remote catalog refreshes its own set out of band and answers from it.
The contract every implementation must keep:
- tenantID may be an unadmitted request value: a caller of HasTenant may pass it before admitting it. An implementation must never log it, echo it, put it in an error, send it outbound, pass it to a deps.* call, or use it as a cache, store or metric key. It may only compare it against the set it holds.
- HasTenant answers false for "" and for any key that is not a tenant id, including a config.NamedDatabasePrefix key.
- HasTenant is safe for concurrent use.
- The complete tenant set is held before app.New: no first call may answer false for a tenant only because a load is still in flight.
- On a failed refresh the implementation keeps the last good set; it never shrinks to empty because a refresh failed.
- A source that refreshes logs the size of each refresh's result, a count and never the ids, so operators can tell when a pod's set is stale.
- HasTenant never panics.
- The answer is existence, not authorization: true says the deployment holds the tenant, not that the caller may act for it.
type TenantResolver ¶
type TenantResolver interface {
ResolveTenant(ctx context.Context, req *http.Request) (string, error)
}
TenantResolver resolves the tenant identifier from an incoming request.
type ValidatingResolver ¶
type ValidatingResolver struct {
Resolver TenantResolver
TenantRegex *regexp.Regexp
}
ValidatingResolver wraps any resolver with tenant ID validation.
func (*ValidatingResolver) ResolveTenant ¶
ResolveTenant implements TenantResolver with validation.