providers

package
v0.2.39 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 2, 2026 License: Apache-2.0 Imports: 55 Imported by: 0

Documentation

Overview

Package providers backs the hub's pluggable-provider extension surface: an in-memory routing table, reverse proxies for /ui/providers/{name}/* and /services/providers/{name}/*, and the controller that keeps the table in sync with ProviderCatalogEntry resources.

Index

Constants

View Source
const (
	// ScopeGlobal is a platform provider under root:railgrid:providers, available
	// to every Org.
	ScopeGlobal = "global"
	// ScopeOrg is a provider the caller's own Org registered and runs itself,
	// under root:railgrid:tenants:<org>:providers.
	ScopeOrg = "org"
)

providerDTO is the shape returned by GET /api/providers. Stable, portal- owned wire format — not the CatalogEntry CRD shape. Provider scope values on the wire. Kept lowercase and stable — the portal branches on them to render the "Self-managed" section of the catalog.

View Source
const (
	// AnnotationActiveTokenSecret names, on the provider ServiceAccount, the
	// token Secret the hub currently mints kubeconfigs from. Its absence means
	// ProviderTokenSecretName, which is what every workspace provisioned
	// before rotation existed carries.
	//
	// The pointer lives on the ServiceAccount rather than being derived from
	// Secret names so that "which credential is current" has exactly one
	// answer, and so a half-finished rotation (new Secret created, pointer not
	// yet moved) keeps handing out the old, still-valid credential rather than
	// an unpopulated one.
	AnnotationActiveTokenSecret = "providers.railgrid.ai/active-token-secret"

	// AnnotationTokenSecretExpiry is stamped on a rotated-out token Secret with
	// the RFC3339 time after which it may be deleted. Until then both tokens
	// authenticate as the same ServiceAccount, which is what lets a provider be
	// rolled onto the new credential without a gap.
	AnnotationTokenSecretExpiry = "providers.railgrid.ai/delete-after"
)
View Source
const (
	// PathProviderUIGrant is the gorilla/mux pattern of the grant endpoint. It
	// must be registered BEFORE the heartbeat PathPrefix route on the same
	// /api/providers prefix, or every POST there is claimed by the heartbeat
	// handler first.
	PathProviderUIGrant = PathListProviders + "/{name}/ui-grant"

	// UIGrantQueryParam is the query parameter the UI proxy reads the grant
	// from. It sits in the query rather than the path so the bundle URL keeps
	// the /ui/providers/{name}/ shape the provider's own portalkit derives its
	// service base from, and so the portal's provider-fetch allow list is
	// unchanged.
	UIGrantQueryParam = "grant"
)
View Source
const ConditionAPIGroupsUnknown = "APIGroupsUnknown"

ConditionAPIGroupsUnknown is set on a CatalogEntry whose provider declares an APIExport that the hub cannot (yet) read the served API groups from.

It is not folded into Ready. A provider with an unreadable export is still routable, still discoverable, and its UI and backend still work; what it cannot do is own an API group, so every scoped-identity rule naming one of its groups is refused with unknown_group until the export is readable. That is a narrow, silent failure without a condition to point at, which is exactly why there is one.

View Source
const DefaultCredentialGracePeriod = 24 * time.Hour

DefaultCredentialGracePeriod is how long a rotated-out provider token stays valid. It has to outlast a leisurely rollout of the provider's chart — the operator has to take the new kubeconfig, put it in a Secret, and restart the workload — so a day rather than an hour, and short enough that a leaked credential is not indefinitely live.

View Source
const EdgesProviderName = "edges"

EdgesProviderName is the platform provider that owns the tunnel. An org-owned provider's data plane is carried by the platform's edges provider, never by an org-owned copy of it: the tunnel is platform infrastructure, and letting an org supply the transport for its own traffic would put the org on both ends of the trust boundary.

View Source
const HeartbeatTTL = 90 * time.Second

HeartbeatTTL is how long a provider's last heartbeat is considered fresh. After this duration, the sweeper flips the registry record's HeartbeatStale flag, which the Ready computation observes.

View Source
const KubeconfigSecretKey = "kubeconfig"

KubeconfigSecretKey is the required data key inside that Secret.

View Source
const KubeconfigSecretName = "railgrid-provider-kubeconfig"

KubeconfigSecretName is the Secret the provider charts read their kcp credential from. The charts hardcode the data key `kubeconfig` and default the secret name to this, so the generated instructions must match exactly — a mismatch produces a pod that starts and then fails to reach kcp.

View Source
const PathListProviders = "/api/providers"

PathListProviders is the portal-facing list endpoint. It returns the names, display labels, and routing metadata for every provider the hub knows about. The portal builds its catalog page and dynamic side-nav from this response. Auth is enforced by the standard railgrid token middleware mounted upstream of this handler.

View Source
const PathProviderHeartbeat = "/api/providers"

PathProviderHeartbeat is the prefix for the heartbeat endpoint. The handler extracts the provider name from the path: POST /api/providers/{name}/heartbeat

View Source
const ProviderClusterRoleName = "railgrid:provider"

ProviderClusterRoleName is the generated, narrower role bound to the provider ServiceAccount in its own workspace when the hub runs with --provider-workspace-cluster-admin=false.

View Source
const ProviderKubeconfigSecretKey = "kubeconfig"

ProviderKubeconfigSecretKey is the data key the provider kubeconfig is stored under in the Secret the Provider controller writes into system:providers.

View Source
const ProviderSAName = "provider"

ProviderSAName is the standard ServiceAccount name created in every provider's sub-workspace. The provider pod is expected to mount a kubeconfig minted from this SA's token.

View Source
const ProviderSANamespace = "default"

ProviderSANamespace is the namespace ProviderSAName lives in. It must stay in lockstep with EnsureProviderSA, which is what actually creates the SA.

View Source
const ProviderSAUsername = "system:serviceaccount:" + ProviderSANamespace + ":" + ProviderSAName

ProviderSAUsername is the username kcp reports for the provider's own service account: the one MintProviderKubeconfigAtPath issues the provider kubeconfig for. Every provider's heartbeat must authenticate as exactly it.

View Source
const ProviderTokenSecretName = ProviderSAName + ProviderTokenSecretSuffix

ProviderTokenSecretName is the Secret every provider workspace starts with. Rotation mints differently-named Secrets beside it and records which one is current on the ServiceAccount, so this stays the name of the *first* credential rather than always the live one — read the live one with activeTokenSecretName.

View Source
const ProviderTokenSecretSuffix = "-token"

ProviderTokenSecretSuffix is appended to the SA name to form the kubernetes.io/service-account-token Secret that holds the provider's long-lived bearer. kcp's token controller populates it; the token does not expire (valid until the Secret or its ServiceAccount is deleted), so the provider pod — and any downstream consumer such as the kro cluster — never needs a rotation loop.

View Source
const SweepInterval = 30 * time.Second

SweepInterval is how often the sweeper goroutine walks the registry to evict stale heartbeats. Should comfortably divide HeartbeatTTL.

View Source
const (

	// UIIntegrityResync bounds how long a pin is reused without re-reading the
	// bundle, and is the FALLBACK path only: an upstream that returns an ETag
	// on /main.js is revalidated with a conditional GET on every reconcile, so
	// a bundle rebuilt at an unchanged version is re-pinned within one
	// reconcile instead of within this interval. Embedded assets and upstreams
	// that send no ETag keep the interval. A version change (chart upgrade or a
	// heartbeat reporting a new version) forces an immediate re-hash either way.
	UIIntegrityResync = 10 * time.Minute
)

Variables

View Source
var (
	// ErrHeartbeatNoBearer means the request carried no bearer token (401).
	ErrHeartbeatNoBearer = errors.New("heartbeat has no bearer token")
	// ErrHeartbeatTokenRejected means the workspace did not authenticate the
	// token at all (401).
	ErrHeartbeatTokenRejected = errors.New("heartbeat bearer token was not authenticated")
	// ErrHeartbeatWrongIdentity means the token authenticated as something
	// other than the provider's own service account (403).
	ErrHeartbeatWrongIdentity = errors.New("heartbeat bearer token is not the provider's service account")
	// ErrHeartbeatAuthUnavailable means the hub has no way to verify the
	// token right now: no kcp, or the provider's workspace is not known yet
	// (503).
	ErrHeartbeatAuthUnavailable = errors.New("heartbeat authentication unavailable")
)
View Source
var Categories = []Category{
	{Name: "Edges", Icon: "Server", Order: 10},
	{Name: "AI", Icon: "Sparkles", Order: 20},
	{Name: "Demo", Icon: "Package", Order: 90},
}

Categories is the canonical list of built-in nav categories. Order matters for the default sort; alphabetical-by-display-name kicks in only as a tiebreaker between equal Order values.

View Source
var DefaultDelegationExclude = []string{EdgesProviderName}

DefaultDelegationExclude is the built-in exclusion list for DelegationPlatform: platform providers whose backend cannot act on a workspace-scoped ServiceAccount token. See docs/provider-scoping.md "Delegated tokens" for the per-provider verdicts behind this list.

edges: its SSH data plane resolves the caller's identity with a TokenReview and, for an edge with spec.sshUserMapping=identity, uses the resulting username as the Linux user to log in as (providers/edges/internal/tunnel/edges_proxy_builder.go fetchSSHCredentials). A delegated token resolves to system:serviceaccount:default:railgrid-du-<hash>, which is not the human's account and is not a login name on any host, so the session would be refused or land on the wrong user. Everything else in edges — the tunnel, the k8s subresource, the Service proxy — works with a delegated token and already receives one on the org-owned provider path (serveOverEdge). Lifting this needs the SSH path to take its identity from X-Railgrid-User (which the hub still sends) rather than from the bearer.

Functions

func APIExportGroups added in v0.2.7

func APIExportGroups(export *unstructured.Unstructured) []string

APIExportGroups projects spec.resources[].group off an APIExport into a deduped, sorted list. Exported so the catalog reconciler's tests can build the same projection from a fake export without a live kcp.

func BuiltinNames

func BuiltinNames() []string

BuiltinNames returns the names of every registered builtin, sorted alphabetically. Used as the default for the --providers flag so the help text is stable + readable.

func CompileProviderActionSchema

func CompileProviderActionSchema(raw json.RawMessage, name string) (*jsonschema.Schema, error)

CompileProviderActionSchema compiles one catalog-declared JSON Schema with format assertions enabled. It is exported for the action transport's bounded fallback cache used by tests and embedders that construct a normalized Provider directly instead of going through the catalog reconciler.

func EncodeKubeconfig

func EncodeKubeconfig(kc []byte) string

EncodeKubeconfig is a tiny helper for status reporting — surface the minted kubeconfig as a base64-encoded blob so cluster operators can fish it out of the CatalogEntry status without needing to read a Secret (relevant when --provider-secret-write is disabled).

func NewHeartbeatHandler

func NewHeartbeatHandler(reg *Registry, record HeartbeatRecorder, authenticate HeartbeatAuthenticator, mode HeartbeatAuthMode, log logr.Logger) http.Handler

NewHeartbeatHandler returns an http.Handler serving POST /api/providers/{name}/heartbeat.

The handler is mounted on the root router with no auth middleware in front of it, so it authenticates the beat itself: authenticate must accept the request as coming from provider {name}'s own service account (the one the hub minted its kubeconfig for) before the beat counts. Without that check anyone able to reach the hub could keep a dead provider Ready and forge its reportedVersion. In HeartbeatAuthWarn mode a failed check is logged and the beat is still recorded; in HeartbeatAuthEnforce it is rejected with 401 (no or unrecognised token), 403 (some other identity) or 503 (could not verify). A nil authenticate means the hub cannot verify anything (no kcp) and behaves like a check that always fails with ErrHeartbeatAuthUnavailable.

A rejection says only which of those three classes it was: the reason goes to the log, never to the caller. In enforce mode a beat for a name that is not registered is refused with exactly the 401 a bad credential gets, so the endpoint cannot be used to enumerate provider names.

record may be nil (no kcp configured), in which case liveness stays local to this process and the hub must not be scaled beyond one replica.

func ParseURL

func ParseURL(raw string) (*url.URL, error)

ParseURL is a small helper for the controller that converts a spec string into a *url.URL with the requirements the proxies rely on (non-empty host, absolute).

func RegisterBuiltin

func RegisterBuiltin(s BuiltinSpec)

RegisterBuiltin adds a first-party provider to the registry. Called from providers/<name>/manifest.go init() functions. Panics on duplicate names (which would be a programmer error — providers/<name> uniqueness matches the package path).

func RunSweeper

func RunSweeper(ctx context.Context, reg *Registry, log logr.Logger)

RunSweeper periodically marks providers stale when their last heartbeat is older than HeartbeatTTL. It derives staleness purely from the timestamps already in the registry, so every hub replica runs its own copy and they all reach the same verdict — this is deliberately NOT leader-gated. Returns when ctx is done.

func SetupProviderWithManager

func SetupProviderWithManager(mgr mcmanager.Manager, kcpConfig *rest.Config, opts CatalogReconcilerOptions) error

SetupProviderWithManager wires the Provider provisioning reconciler into the admin multicluster manager (bound to admin.railgrid.ai, whose Provider objects live in root:railgrid:system:providers). kcpConfig is the admin rest.Config used to create sub-workspaces and write the kubeconfig Secret with admin credentials.

Types

type BuiltinChild

type BuiltinChild struct {
	DisplayName  string
	BuiltinRoute string
}

BuiltinChild is a single sub-navigation item. Renders indented under the parent in the portal side nav.

type BuiltinSpec

type BuiltinSpec struct {
	// Name is the CatalogEntry resource name. Must be a valid k8s name
	// (lowercase, hyphens, no slashes).
	Name string

	// DisplayName is what the portal shows in nav + catalog cards.
	DisplayName string

	// Description is a short blurb on the catalog card.
	Description string

	// Category groups this entry in the portal nav. Matched against
	// Categories[].Name; unknown categories render with a fallback icon.
	Category string

	// IconURL is a portal-relative path to an icon. Empty means fall back
	// to the registry-category icon or a generic Puzzle glyph.
	IconURL string

	// BuiltinRoute is the Vue Router route name the portal renders when
	// this provider's main entry is clicked.
	BuiltinRoute string

	// Children adds sub-navigation under this provider in the side nav.
	// Used by providers that span multiple portal pages.
	Children []BuiltinChild

	// Requires lists other builtin names this entry depends on. The hub
	// refuses to start if the enabled set contains this entry but not all
	// of its requirements — e.g. mcp.Requires = [kubernetes-edges,
	// server-edges].
	Requires []string

	// LocalUIAssets, when non-nil, is the pre-built micro-frontend bundle
	// (Vite dist/) the hub serves under /ui/providers/{Name}/* instead of
	// reverse-proxying to spec.serving.ui.url. Built-in providers `go:embed` their
	// own portal/dist into this FS so the hub binary ships their UI inline;
	// third-party providers leave this nil and run their own HTTP server.
	//
	// When set, the FS root must contain at minimum main.js (the custom
	// element bundle the portal's ProviderFrame loads) and index.html (the
	// fallback page for direct browser visits). icon.svg is recommended.
	LocalUIAssets fs.FS
}

BuiltinSpec describes a first-party provider the hub bootstraps into root:railgrid:providers on startup. Each provider lives in its own package under providers/<name>/ and calls RegisterBuiltin in its init() — the hub picks them up via blank import in cmd/railgrid-hub/main.go.

The split is deliberately declarative: this struct is the contract a provider package publishes. The kcp apply, RBAC plumbing, and reconcile loop all read from here.

func AllBuiltins

func AllBuiltins() []BuiltinSpec

AllBuiltins returns a snapshot of every registered builtin in registration order (which == cmd/railgrid-hub blank-import order). Stable across calls within a process lifetime since init() runs once.

func BuiltinByName

func BuiltinByName(name string) (BuiltinSpec, bool)

BuiltinByName fetches a single registered spec. Returns ok=false when the name isn't registered.

func ResolveEnabledBuiltins

func ResolveEnabledBuiltins(enabled []string) ([]BuiltinSpec, error)

ResolveEnabledBuiltins picks the subset of builtins named in `enabled` and validates that every Requires dep is also enabled. Returns a single error listing every problem (missing name OR missing dep) so callers fix the flag in one edit.

Empty `enabled` selects all registered builtins — matches the flag's default behavior.

type CatalogReconciler

type CatalogReconciler struct {
	// contains filtered or unexported fields
}

CatalogReconciler keeps the in-process Registry in sync with the cluster's CatalogEntry resources AND provisions the kcp-side artefacts each provider needs (sub-workspace + APIResourceSchemas + APIExport).

Scope as of Phase 1B:

  • On create/update: parse spec.serving.ui.url and spec.serving.backend.url, set the registry entry, and apply the inline APIResourceSchemas + APIExport in the per-provider sub-workspace.
  • On delete: drop the registry entry. (Cascade GC of the sub-workspace and its APIExport is deferred — Phase 5 hardening.)

Deferred:

  • Heartbeat-driven readiness (Phase 1C).
  • Provider ServiceAccount + kubeconfig Secret mint (only required for providers that ship a controller — Phase 1D).
  • RBAC grant + MaximalPermissionPolicy enabling tenant Enable (Phase 3).

func SetupCatalogWithManager

func SetupCatalogWithManager(mgr mcmanager.Manager, reg *Registry, kcpConfig *rest.Config, opts CatalogReconcilerOptions) (*CatalogReconciler, error)

SetupCatalogWithManager wires the reconciler into a multicluster manager and returns it, so the caller can hand it the hooks that only exist elsewhere in the hub — notably the UI proxy's observed-bundle pin (ProviderProxy.SetMainJSIntegrityObserver).

kcpConfig is the admin rest.Config used only to RESOLVE each provider's workspace cluster ID (read-only) for the Enable flow. Pass nil to run the controller in registry-only mode (no kcp reads). The hub no longer provisions providers — that moved to admin onboarding + provider Helm init.

func (*CatalogReconciler) ObserveMainJSIntegrity added in v0.2.7

func (r *CatalogReconciler) ObserveMainJSIntegrity(name, integrity string)

ObserveMainJSIntegrity records the SRI pin of the /main.js body the UI proxy actually streamed to a browser (see ProviderProxy.SetMainJSIntegrityObserver).

It is the second half of the answer to a bundle that changes at an unchanged version: the reconcile loop revalidates every reconcile, but a browser can still ask for the bundle in the window between the rebuild and the next reconcile, and what it receives is authoritative — that body is what the integrity attribute has to match. So the pin is corrected from the bytes served, for the provider's CURRENT version, and the next reconcile writes it through to the CatalogEntry status like any other pin.

The cached record deliberately loses its ETag here: that validator described the body the hub hashed, not the one it just served, and keeping it would let the next conditional GET confirm the superseded pin with a 304.

Only platform providers with a proxied UI are corrected. Org-owned bundles travel the edge tunnel and are pinned per grant (ui_grant.go), and embedded assets are read from this binary and cannot disagree with it.

func (*CatalogReconciler) Reconcile

Reconcile parses one CatalogEntry and updates the registry + status.

type CatalogReconcilerOptions

type CatalogReconcilerOptions struct {
	// HubExternalURL / HubInternalURL are kept for symmetry with the
	// admin onboarding service; the catalog controller no longer provisions or
	// mints kubeconfigs, so they are currently unused by the reconciler.
	HubExternalURL string
	HubInternalURL string

	// EdgeRoutes resolves an org-owned provider's edge transport and reconciles
	// the hub-owned Service in front of it. Nil leaves org-owned providers on
	// their declared backend URL, which is the pre-edge-transport behaviour.
	EdgeRoutes EdgeRouteResolver

	// Provisioner configures the Provisioner these controllers build — notably
	// WithWorkspaceClusterAdmin, which decides the role a provider's
	// ServiceAccount is bound to in its own workspace. Nil means the
	// Provisioner defaults, so a caller that forgets it gets the wide,
	// backwards-compatible binding rather than silently narrowing one.
	Provisioner []ProvisionerOption
}

CatalogReconcilerOptions threads optional extras into the reconciler without bloating its constructor signature. All fields optional.

type Category

type Category struct {
	// Name is the case-sensitive value that must match
	// CatalogEntry.spec.category for a provider to be grouped here.
	Name string
	// Icon is the lucide-vue-next component name (e.g. "Server",
	// "Sparkles"). Used by the portal as a string lookup; the actual Vue
	// component is resolved client-side.
	Icon string
	// Order is the sort key (ascending). Lower numbers appear higher in
	// the nav. Use multiples of 10 to leave room for future entries.
	Order int
}

Category describes how the portal renders a `spec.category` value in the nav and on the providers catalog page. Each entry binds a name (matched case-sensitively against CatalogEntry.spec.category) to a display label, a lucide-vue-next icon component name, and a sort hint.

Categories not in this list still render — a CatalogEntry can put itself in an ad-hoc category like "Observability" — but they fall back to the default Puzzle icon and sort after the named ones.

To add a new built-in category: append an entry here, pick a Lucide icon name from https://lucide.dev/icons/ (the portal maps the string to an imported component in lib/categoryIcons.ts). No portal-side change is required unless the icon isn't already imported.

func CategoryByName

func CategoryByName(name string) (Category, bool)

CategoryByName returns the Category record for name, or zero value (and ok=false) when unknown. Ad-hoc categories used by third-party CatalogEntries surface here with no record.

type ClusterResolver

type ClusterResolver interface {
	CatalogEntryCluster(name string) (string, bool)
}

ClusterResolver reports the logical cluster a provider's CatalogEntry lives in. The catalog reconciler records it from the cluster it observed the entry in, so the recorder patches the object where it actually is.

type DelegatedCaller

type DelegatedCaller struct {
	User          string
	OrgUUID       string
	WorkspaceUUID string
}

DelegatedCaller is the verified human a delegated token is minted for: the hub User name plus the tenant workspace the token will be scoped to. Callers must have authenticated the user and verified their membership in (OrgUUID, WorkspaceUUID) before building one — issuance mints on the hub's own authority and re-checks neither.

type DelegatedTokenIssuer

type DelegatedTokenIssuer interface {
	IssueDelegatedUserToken(ctx context.Context, orgUUID, wsUUID string, user serviceaccounts.Identity, provider serviceaccounts.DelegatedProvider) (string, time.Time, error)
}

DelegatedTokenIssuer mints the credential the hub sends to an org-owned provider in place of the caller's own hub bearer: a ServiceAccount token in the caller's tenant workspace, annotated with the human user it stands in for. Implemented by *serviceaccounts.Manager.

type DelegatedTokenIssuerFunc

type DelegatedTokenIssuerFunc func(ctx context.Context, orgUUID, wsUUID string, user serviceaccounts.Identity, provider serviceaccounts.DelegatedProvider) (string, time.Time, error)

DelegatedTokenIssuerFunc adapts a plain function to DelegatedTokenIssuer.

func (DelegatedTokenIssuerFunc) IssueDelegatedUserToken

func (f DelegatedTokenIssuerFunc) IssueDelegatedUserToken(ctx context.Context, orgUUID, wsUUID string, user serviceaccounts.Identity, provider serviceaccounts.DelegatedProvider) (string, time.Time, error)

IssueDelegatedUserToken satisfies DelegatedTokenIssuer.

type DelegationMode

type DelegationMode string

DelegationMode selects which providers receive a delegated ServiceAccount token in place of the caller's own hub bearer on the backend proxy.

Org-owned providers are never a question of mode: they always receive the delegated token (see serveOverEdge). The mode decides what a PLATFORM provider — one in root:railgrid:providers, dialled directly by the hub — gets.

const (
	// DelegationOff forwards the caller's hub bearer to platform providers
	// as-is. This is the historical behaviour and the default for this
	// release.
	DelegationOff DelegationMode = "off"
	// DelegationPlatform swaps the bearer for a delegated token on every
	// platform provider except those listed in DelegationPolicy.Exclude.
	DelegationPlatform DelegationMode = "platform"
	// DelegationAll swaps the bearer on every platform provider and ignores
	// the exclusion list. For deployments that have verified — or do not
	// run — the providers the default exclusion covers.
	DelegationAll DelegationMode = "all"
)

func ParseDelegationMode

func ParseDelegationMode(s string) (DelegationMode, error)

ParseDelegationMode accepts the flag spelling of a DelegationMode. An empty string is DelegationOff so an unset option behaves like the default.

type DelegationPolicy

type DelegationPolicy struct {
	Mode DelegationMode
	// Exclude names platform providers that keep receiving the caller's
	// bearer under DelegationPlatform. Ignored under DelegationAll. Matching
	// is exact on the provider name.
	Exclude []string
}

DelegationPolicy is the backend proxy's answer to "does this platform provider get the caller's bearer or a delegated token?".

func (DelegationPolicy) DelegatesPlatform

func (pol DelegationPolicy) DelegatesPlatform(name string) bool

DelegatesPlatform reports whether the platform provider name should have the caller's bearer replaced with a delegated token.

type EdgeRoute

type EdgeRoute struct {
	// WorkspaceUUID is the team workspace holding the edge and the Service.
	WorkspaceUUID string
	// Cluster is that workspace's kcp logical-cluster ID, which is what the
	// edges verb path addresses. Resolved by the catalog reconciler; a route
	// with an empty Cluster is not usable and is treated as absent.
	Cluster string
	// EdgeName is the KubernetesCluster edge whose agent carries the tunnel.
	EdgeName string
	// ServiceName is the hub-owned edges.railgrid.ai/Service in front of the
	// provider inside the tenant's cluster.
	ServiceName string
}

EdgeRoute says an org-owned provider's backend is reached over its edge tunnel instead of by dialling a URL.

Mirrors kcp.EdgeRoute rather than importing it, matching how ProviderClaim is mirrored in the other direction: pkg/hub/kcp deliberately does not depend on this package, and this package should not gain a dependency on the bootstrap surface just to name three strings.

func (*EdgeRoute) EdgeProxyPath

func (e *EdgeRoute) EdgeProxyPath(rest string) string

EdgeProxyPath returns the kcp path that carries one request to this provider's backend, with rest appended: the edges provider's services/{name}/proxy custom subresource in the route's workspace,

/clusters/{cluster}/apis/edges.railgrid.ai/v1alpha1/services/{service}/proxy{rest}

The hub dials kcp for this hop exactly as any other caller of a verb would: kcp authorizes it and reverse-proxies it to the edges provider, which carries it down the tunnel. There is no hub→provider grammar left to take instead.

func (*EdgeRoute) Usable

func (e *EdgeRoute) Usable() bool

Usable reports whether the route can actually be addressed. A route recorded at registration but whose workspace cluster has not been resolved yet is not usable, and a provider with one must 503 rather than fall back to dialling BackendURL — that address is inside the tenant's cluster, so falling back would mean the hub dialling something it cannot reach and, worse, would make routing depend on a field the tenant controls.

type EdgeRouteResolver

type EdgeRouteResolver interface {
	// ResolveProviderEdgeRoute returns the route for an org-owned provider, or
	// nil when it has none. backendURL is the address the provider published
	// about itself, from which the in-cluster Service target is derived and
	// validated; implementations reconcile the hub-owned Service as a side
	// effect so the tunnel has something to land on.
	ResolveProviderEdgeRoute(ctx context.Context, orgUUID, providerName, backendURL string) (*EdgeRoute, error)
}

EdgeRouteResolver reads back the edge binding the hub recorded at registration. Implemented by *kcp.Bootstrapper; declared as an interface so the catalog reconciler depends on the capability rather than the bootstrapper.

type HeartbeatAuthMode

type HeartbeatAuthMode string

HeartbeatAuthMode selects what the heartbeat handler does when a beat fails authentication.

const (
	// HeartbeatAuthWarn records the beat anyway and logs the failure with the
	// provider name and reason. It is the default for this release so that
	// providers deployed from charts that predate the authenticated heartbeat
	// keep working while operators roll them forward; the next release flips
	// the default to HeartbeatAuthEnforce.
	HeartbeatAuthWarn HeartbeatAuthMode = "warn"
	// HeartbeatAuthEnforce rejects unauthenticated beats. A provider that
	// cannot prove it holds its own service-account token goes stale after
	// HeartbeatTTL, which is the correct outcome.
	HeartbeatAuthEnforce HeartbeatAuthMode = "enforce"
)

func ParseHeartbeatAuthMode

func ParseHeartbeatAuthMode(s string) (HeartbeatAuthMode, error)

ParseHeartbeatAuthMode validates a --provider-heartbeat-auth flag value.

type HeartbeatAuthenticator

type HeartbeatAuthenticator func(ctx context.Context, r *http.Request, providerName string) error

HeartbeatAuthenticator decides whether r is allowed to heartbeat on behalf of providerName. A nil error accepts the beat. Errors wrap one of the ErrHeartbeat* sentinels so the handler can pick the status code; anything else is treated as "could not verify" (503), which the provider retries.

func NewTokenReviewHeartbeatAuthenticator

func NewTokenReviewHeartbeatAuthenticator(kcpConfig *rest.Config, clusters ClusterResolver) (HeartbeatAuthenticator, error)

NewTokenReviewHeartbeatAuthenticator returns a HeartbeatAuthenticator that accepts a beat for provider N only when its bearer token authenticates, via TokenReview in N's own workspace, as that workspace's provider service account. Successful verifications are cached for heartbeatAuthCacheTTL and rejections for heartbeatAuthNegativeCacheTTL, at most heartbeatAuthNegativeCacheMax of them at once.

type HeartbeatRecorder

type HeartbeatRecorder func(ctx context.Context, name, version string, at time.Time) error

HeartbeatRecorder persists a heartbeat to the provider's CatalogEntry status.

A heartbeat reaches exactly one hub replica — whichever the load balancer picked — but every replica routes provider traffic and therefore needs the liveness signal. Writing it to CatalogEntry.status is what fans it back out: each replica's catalog watch delivers the update and refreshes its own registry. Without this, non-receiving replicas would mark a perfectly healthy provider stale after HeartbeatTTL and start refusing to proxy to it.

func NewCatalogHeartbeatRecorder

func NewCatalogHeartbeatRecorder(kcpConfig *rest.Config, clusters ClusterResolver) (HeartbeatRecorder, error)

NewCatalogHeartbeatRecorder returns a HeartbeatRecorder that stamps CatalogEntry.status in the provider's own workspace (root:railgrid:providers/<name>), where each provider's `init` self-registers its entry.

Addressing the entry by cluster rather than by a fixed path matters: an earlier version wrote to root:railgrid:system:providers, which serves the API but holds no entries, so every beat 404'd and provider liveness never crossed replicas.

A merge patch (rather than a read-modify-write) keeps concurrent beats from different providers — and a beat racing the catalog reconciler's own status write — from conflicting.

type InstallInstructions

type InstallInstructions struct {
	Provider      string `json:"provider"`
	WorkspacePath string `json:"workspacePath"`
	Namespace     string `json:"namespace"`
	ReleaseName   string `json:"releaseName"`
	ChartRef      string `json:"chartRef"`
	ChartVersion  string `json:"chartVersion,omitempty"`
	// KubeconfigFilename is what the instructions assume the credential was
	// saved as, relative to the working directory.
	KubeconfigFilename string        `json:"kubeconfigFilename"`
	Steps              []InstallStep `json:"steps"`
	// Upgrade is the single command that moves an already-installed copy to
	// ChartVersion while keeping the values the release was installed with.
	// Rendered separately from Steps because an upgrade needs none of them:
	// the namespace, Secret, and values all survive from the original install.
	Upgrade *InstallStep    `json:"upgrade,omitempty"`
	Values  []ResolvedValue `json:"values,omitempty"`
	DocsURL string          `json:"docsURL,omitempty"`
	// ValuesDoc is the chart's values reference in Markdown, carried inline by
	// the provider so the portal can render it without leaving the install
	// panel and without reaching the internet.
	ValuesDoc string `json:"valuesDoc,omitempty"`
	// Warnings surface anything the operator must resolve by hand before the
	// commands will work.
	Warnings []string `json:"warnings,omitempty"`
}

InstallInstructions is everything an org needs to stand up its own copy.

func RenderInstallInstructions

func RenderInstallInstructions(sh *SelfHosting, opts InstallOptions) InstallInstructions

RenderInstallInstructions builds the install steps for one provider.

It never fails: a provider whose metadata is incomplete still produces instructions, with placeholders and warnings for the parts the hub could not fill in. Returning an error instead would leave the user with a freshly created workspace, a live credential, and nothing telling them what to do next — strictly worse than imperfect instructions that say what is missing.

type InstallOptions

type InstallOptions struct {
	// ProviderName is the name the org registered its copy under.
	ProviderName string
	// WorkspacePath is the org provider workspace the credential targets.
	WorkspacePath string
	// HubURL is the address the org's cluster reaches this hub at. It must be
	// externally reachable: the provider runs outside the platform, so an
	// in-cluster service DNS name would be unroutable for it.
	HubURL string
}

InstallOptions carries the per-organization facts the provider's own metadata cannot know.

type InstallStep

type InstallStep struct {
	Title       string `json:"title"`
	Description string `json:"description,omitempty"`
	Command     string `json:"command"`
}

InstallStep is one shell command, with a human explanation of what it does. Kept as discrete steps rather than one blob so the portal can render them individually with per-step copy buttons, and so a user who already has, say, the namespace can skip a step knowingly.

type ListHandler

type ListHandler struct {
	// contains filtered or unexported fields
}

ListHandler serves GET /api/providers. It is registered on the router early — before the auth stack that resolves tenant context exists — so the middleware that scopes the response to an Org is installed afterwards via SetMiddleware. This mirrors how the UI and backend proxies take their tenant resolvers late.

Until a middleware is installed the handler still serves correctly; it just sees no tenant context and returns the platform-global catalog.

func NewListHandler

func NewListHandler(reg *Registry) *ListHandler

NewListHandler returns an http.Handler serving GET /api/providers.

The handler reads from the in-memory Registry — no kcp round-trip per request, which is appropriate for what is effectively a UI catalog poll. All display metadata is published into the Registry by the catalog controller, so this handler has no other dependencies.

Scope follows the caller's tenant context, which tenant.OptionalMiddleware populates only after verifying membership. With an Org, the response is that Org's own providers plus the platform-global ones; without, it is the platform-global ones alone. An Org's providers are therefore never visible to another Org, and an unauthenticated-for-that-org caller degrades to the global catalog rather than an error.

func (*ListHandler) ServeHTTP

func (h *ListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler.

func (*ListHandler) SetMiddleware

func (h *ListHandler) SetMiddleware(mw func(http.Handler) http.Handler)

SetMiddleware installs the middleware wrapped around every list request, typically tenant.OptionalMiddleware. Safe to call while the server is serving.

type MainJSIntegrityObserver added in v0.2.7

type MainJSIntegrityObserver interface {
	ObserveMainJSIntegrity(name, integrity string)
}

MainJSIntegrityObserver is told the SRI metadata of the /main.js body the UI proxy actually served for a provider. (*CatalogReconciler) implements it; see ObserveMainJSIntegrity for what it does with the value.

type NavChild struct {
	DisplayName  string
	BuiltinRoute string
}

NavChild mirrors CatalogEntry.spec.serving.ui.children — a single sub-nav entry the portal renders indented under its parent provider.

type OnboardedWorkspace

type OnboardedWorkspace struct {
	Name    string
	Cluster string
	Phase   string
}

OnboardedWorkspace is a provider sub-workspace under root:railgrid:providers created by onboarding (independent of whether a CatalogEntry has registered the provider yet).

type OrgProviderRoute

type OrgProviderRoute struct {
	// BaseURL addresses the provider's backend root on kcp's front door,
	// through the edges provider's services/{name}/proxy custom subresource.
	// Append the provider-relative path (e.g. "/mcp").
	BaseURL string
	// Transport sends requests addressed under BaseURL authenticated to kcp
	// as the hub, carrying the caller's delegated token as the upstream
	// Authorization the edges service proxy presents to the provider —
	// whatever Authorization the request carried. It refuses any request
	// not addressed under BaseURL, so the delegated token cannot be pointed
	// anywhere else.
	Transport http.RoundTripper
}

OrgProviderRoute is how one hub-originated request reaches one org-owned provider's backend on behalf of one caller.

type Provider

type Provider struct {
	Name string
	// OrgUUID is empty for platform-global providers (those under
	// root:railgrid:providers) and set to the owning Org's UUID for org-owned
	// "bring your own" providers, which live at
	// root:railgrid:tenants:<org>:providers:<name>. The catalog reconciler derives
	// it from the workspace path the CatalogEntry was observed in — never from
	// a field on the entry itself, which the provider's own init writes and so
	// cannot be trusted to attribute ownership.
	//
	// It scopes visibility: an org-owned provider is listed and enableable only
	// within its own Org, and can never shadow a platform provider's routes.
	OrgUUID     string
	DisplayName string // human-readable label, surfaced to the portal
	// Description is the CatalogEntry's short blurb. The portal renders it on
	// catalog cards and in the first-run welcome flow, where it is the only
	// thing telling a new user what the provider actually does.
	Description string
	IconURL     string // optional, defaults to /ui/providers/{name}/icon.svg
	Category    string // optional grouping in the portal nav; empty = top-level
	// Requires mirrors CatalogEntry.spec.requires verbatim: everything this
	// provider needs that it does not own, keyed by API group. It is the single
	// source for both the permission claims a tenant accepts at Enable
	// (providersv1alpha1.RequiredCoordinates) and the dependency edges the hub
	// checks before enabling (providersv1alpha1.Dependencies) — the two used to
	// be separate declarations that had to be merged.
	Requires []providersv1alpha1.ProviderRequirement
	UIURL    *url.URL // proxy target for /ui/providers/{name}/*; nil → 404
	// MainJSIntegrity is the SRI pin ("sha384-<base64>") of the bundle the
	// portal loads from /ui/providers/{name}/main.js, computed by the catalog
	// reconciler from the same source the UI proxy serves. Empty when the hub
	// could not (yet) hash the bundle; the portal then loads it unpinned.
	MainJSIntegrity string
	BackendURL      *url.URL // proxy target for /services/providers/{name}/*; nil → 404
	// BackendHealthRequired is set for every provider that declares a backend.
	// Platform providers are probed directly; org-owned providers are probed
	// only through the hub-owned Edge route. BackendHealthy is the latest
	// bounded healthPath probe result.
	BackendHealthRequired bool
	BackendHealthy        bool
	Actions               []ProviderAction
	// Export mirrors CatalogEntry.spec.export: the APIExport name and the
	// resources it serves, with the verbs and actions on each. It is kept as the
	// API type rather than remirrored, because everything that walks a
	// provider's callable surface already has helpers on it —
	// Export.Coordinates(), Export.Resource(), providersv1alpha1
	// .ProviderDeclaredVerbs — and a hub-side copy would have to be kept in step
	// with them by hand. Nil for a provider that exports no API of its own.
	//
	// Declaring a verb or an action serves nothing — the provider still
	// enforces every call with its own SSAR — but it is what makes the
	// {resource}/{verb} coordinate machine-readable, which is what lets the
	// hub's scoped-identity service mint a capability for it.
	Export *providersv1alpha1.ProviderExport
	// AssistantSkills contains only validated, provider-supplied inline App
	// Studio packages. It intentionally carries no provider URL, credential, or
	// runtime handle; the authenticated catalog API is the sole distribution
	// boundary.
	AssistantSkills []ProviderAssistantSkill
	BuiltinRoute    string     // when set, portal renders this Vue route instead of loading /main.js
	Children        []NavChild // sub-nav entries surfaced indented under this provider
	Version         string     // CatalogEntry.spec.version (chart-declared)
	APIExportPath   string     // kcp workspace path hosting the APIExport (e.g. root:railgrid:providers:cost)
	APIExportName   string     // APIExport name (e.g. cost.providers.railgrid.ai)
	// APIGroups are the API groups this provider SERVES, read by the catalog
	// reconciler from spec.resources[].group on the provider's own APIExport
	// (apis.kcp.io/v1alpha2, named APIExportName, in APIExportPath) — deduped
	// and sorted.
	//
	// It is deliberately a separate field from APIExportName rather than
	// derived from it. The export is named after the provider
	// (`edges.providers.railgrid.ai`); the kinds it serves are in a different
	// group (`edges.railgrid.ai`), sometimes several groups, and the mapping
	// between the two is a naming convention nobody enforces. The APIExport is
	// the only thing that actually knows, so it is what is read.
	//
	// Everything that asks "which provider owns this API group" answers from
	// here: the scoped-identity policy (pkg/hub/identity, clauses A, B, C and
	// E) and composition admission below. Empty means the hub has not managed
	// to read the export yet — a fail-closed state, not a permissive one: the
	// policy refuses such a group with unknown_group, and the reconciler
	// reports it as the APIGroupsUnknown condition on the CatalogEntry.
	APIGroups []string
	// SelfHosting carries the provider's own deployment recipe, from which the
	// hub renders per-organization install instructions. Nil when the provider
	// is platform-operated only.
	SelfHosting *SelfHosting
	// HubAccess mirrors CatalogEntry.spec.hub.access: the hub REST
	// capabilities the provider requests. Declaring grants nothing; they are
	// enforced only as accepted by a tenant (pkg/hub/hubaccess).
	HubAccess []providersv1alpha1.ProviderHubAccess
	// WorkspaceCluster is the logical cluster ID of the provider's
	// sub-workspace (Workspace.spec.cluster of root:railgrid:providers:{name})
	// — the same cluster name kcp embeds in the provider SA's token claims.
	// Set via SetWorkspaceCluster after provisioning; empty until then.
	//
	// Its one reader today is the admin providers API (pkg/hub/admin), which
	// shows an operator which workspace a registered provider actually lives
	// in. It used to anchor the qualified RBAC subject of the Enable-time
	// edges-proxy grant; that grant was deleted (no provider authenticates as
	// its own SA against a tenant workspace any more).
	WorkspaceCluster string

	// CatalogEntryCluster is the logical cluster the provider's CatalogEntry
	// was observed in, recorded by the catalog reconciler from the multicluster
	// request. The heartbeat recorder patches the entry there; a fixed
	// workspace path is wrong, because each provider's `init` registers its
	// entry in the provider's own workspace. Empty until the catalog watch has
	// seen the entry.
	CatalogEntryCluster string

	// EdgeRoute, when non-nil, says this provider's backend is reached through
	// an edge tunnel rather than by dialling BackendURL.
	//
	// Only ever set for an org-owned provider, and only from the binding the
	// HUB recorded at registration — never from the CatalogEntry, which the
	// tenant's own chart authors (docs/byo-provider-edge-transport.md E-1).
	// When set, BackendURL is ignored for routing: it names an address inside
	// the tenant's cluster, meaningful there and undialable from here.
	EdgeRoute *EdgeRoute

	// ShadowsPlatform is true on an org-owned record returned by ListForOrg
	// when a platform provider of the same name exists: for that Org, this
	// copy is what /services/providers/{name} reaches, and the platform one
	// is hidden. Derived at listing time, never stored, so the portal can say
	// "this overrides a platform provider" without a second lookup.
	ShadowsPlatform bool

	// LocalUIAssets, when non-nil, is an embedded fs.FS that the UI proxy
	// serves under /ui/providers/{Name}/* instead of forwarding to UIURL.
	// Populated for first-party providers whose Vite-built portal/dist is
	// baked into the hub binary via //go:embed. Third-party providers leave
	// it nil and rely on UIURL.
	LocalUIAssets fs.FS

	// EndpointsValid is true when spec.serving.ui.url/spec.serving.backend.url parsed cleanly
	// and at least one endpoint was declared (or LocalUIAssets is set).
	// The catalog controller sets this; the sweeper does not touch it.
	EndpointsValid bool

	// LastHeartbeat is the provider's most recent beat. It is set both by the
	// POST /api/providers/{name}/heartbeat handler on the replica that served
	// it and by the catalog reconciler from CatalogEntry.status.lastHeartbeat,
	// which is how the signal reaches replicas that did not serve the beat.
	// Zero until the first heartbeat (or for providers that don't heartbeat).
	LastHeartbeat time.Time
	// ReportedVersion is the version string the provider pod sent in its
	// most recent heartbeat — may diverge from Version during a chart
	// upgrade.
	ReportedVersion string
	// HeartbeatRequired distinguishes "heartbeats-not-configured" from
	// "stale heartbeat". It flips to true on the first heartbeat received,
	// and stays true. From then on, missing heartbeats mark the provider
	// not Ready.
	HeartbeatRequired bool
	// HeartbeatStale is maintained by the sweeper. When HeartbeatRequired
	// is true and now - LastHeartbeat exceeds HeartbeatTTL, the sweeper
	// flips this to true.
	HeartbeatStale bool
}

Provider is the in-memory record the proxies consult to route a request. Fields are nil-able to reflect that UI and backend are independently optional in the source ProviderCatalogEntry.

func (Provider) Coordinates added in v0.2.7

func (p Provider) Coordinates() []providersv1alpha1.ProviderCoordinate

Coordinates is every {resource}/{verb} this provider publishes, verbs then actions, each carrying its parent resource's apiVersion and kind. It is the flat form everything that walks a provider's callable surface wants, and it is derived rather than stored so it cannot drift from Export.

func (Provider) Readiness

func (p Provider) Readiness() (ready bool, reason, message string)

Readiness returns the aggregate provider verdict and a stable, sanitized explanation suitable for the portal API. Backend response bodies, URLs, and transport errors never cross this boundary.

func (Provider) Ready

func (p Provider) Ready() bool

Ready returns true when the provider's declared endpoints and runtime health signals are all ready.

type ProviderAction

type ProviderAction struct {
	Name            string
	Version         string
	DisplayName     string
	Description     string
	Resource        ProviderActionResource
	InputSchema     json.RawMessage
	OutputSchema    json.RawMessage
	InputValidator  *jsonschema.Schema
	OutputValidator *jsonschema.Schema
	SchemaDigest    string
	ExecutionMode   string
	ReadOnly        bool
	Risk            providersv1alpha1.ProviderActionRisk
	Idempotency     string
	Limits          ProviderActionLimits
	Consent         providersv1alpha1.ProviderActionConsent
	Deprecation     *providersv1alpha1.ProviderActionDeprecation
}

ProviderAction is the hub's normalized view of one CatalogEntry action. The catalog API owns the wire shape; the registry keeps the complete discovery and policy metadata needed by both the forward-only action transport and the portal's consent UI. It deliberately does not retain provider transport URLs on each action.

func ParseProviderActions

func ParseProviderActions(export *providersv1alpha1.ProviderExport) ([]ProviderAction, error)

ParseProviderActions normalizes every action the export declares and compiles both schemas with the full JSON Schema implementation. Callers must handle the error: admitting an action without compiled validators would make the router fail open on malformed catalog metadata.

An action is declared ON the resource it is served on, so the resource it may receive comes from its parent entry rather than from a bound-resource block of its own — the two can no longer disagree.

func (ProviderAction) ID

func (a ProviderAction) ID() string

ID is the action's catalogued identity, "<name>/<version>" — the string grants, consent records and the assistant catalog key on. It mirrors providersv1alpha1.ProviderAction.ID so the hub never stores a second spelling of it.

type ProviderActionLimits

type ProviderActionLimits struct {
	TimeoutSeconds int64
	MaxInputBytes  int64
	MaxOutputBytes int64
	MaxResultItems int64
}

ProviderActionLimits carries catalog-enforced byte, timeout, and result bounds into the transport without coupling it to API object pointers.

type ProviderActionResource

type ProviderActionResource struct {
	APIVersion string
	Kind       string
	Resource   string
}

ProviderActionResource identifies the provider-owned resource an action is allowed to receive. The request's resourceRef must match all three fields. It is the action's parent ProviderExportResource: the coordinate kcp routes on is (that resource, this action's name).

type ProviderAssistantSkill

type ProviderAssistantSkill struct {
	PackageName string
	Version     string
	Digest      string
	Skill       string
	Resources   []ProviderAssistantSkillResource
}

ProviderAssistantSkill is the registry's normalized view of one inline App Studio skill package. Resource paths remain package-relative and content is copied into the catalog response only after CatalogEntry validation.

type ProviderAssistantSkillResource

type ProviderAssistantSkillResource struct {
	Path    string
	Content string
}

ProviderAssistantSkillResource is one package-relative supporting resource.

type ProviderProxy

type ProviderProxy struct {
	// contains filtered or unexported fields
}

ProviderProxy is the shared implementation backing both proxies. Exported so the server can call SetFallback on the UI proxy after the portal SPA handler is built (the two are constructed at different points in Server.Run).

func NewBackendProxy

func NewBackendProxy(reg *Registry, log logr.Logger) *ProviderProxy

NewBackendProxy returns an http.Handler serving /services/providers/{name}/* by reverse proxying to the provider's spec.serving.backend.url, for the route classes that are not kcp API traffic: MCP, browser OAuth, signed webhooks, the agent tunnel and health. Data-plane verbs and actions are not routes here at all — they are kcp custom subresources on the provider's APIExport, reached through the kcp front door at /clusters/{id}/apis/…. The proxy strips the requestheader identity a kcp shard would stamp (stripShardIdentityHeaders), so the provider's shard-facing route cannot be reached through the hub wearing an identity of the caller's choosing.

An org-owned provider always has the user's Authorization header replaced with a short-lived delegated token (see serveOverEdge and SetDelegatedTokenIssuer); a platform provider gets the same treatment when the DelegationPolicy selects it (SetDelegationPolicy) and the caller's bearer as-is otherwise. If a TenantResolver is installed via SetTenantResolver (and a cluster resolver via SetClusterResolver), the proxy resolves the caller's identity and injects X-Railgrid-User plus the caller's default workspace's kcp logical-cluster ID as both X-Railgrid-Tenant and X-Railgrid-Cluster, so the provider can scope work without re-parsing the bearer token. Incoming X-Railgrid-User / X-Railgrid-Tenant / X-Railgrid-Cluster headers are ALWAYS stripped before the request is forwarded — a third-party caller can't forge identity by setting those headers directly.

func NewUIProxy

func NewUIProxy(reg *Registry, log logr.Logger) *ProviderProxy

NewUIProxy returns an http.Handler serving /ui/providers/{name}/* by reverse proxying to the provider's spec.serving.ui.url. The handler is mounted in the hub router WITHOUT http.StripPrefix; this proxy strips the /ui/providers/{name} segment itself so it can inject X-Railgrid-Base-Path before forwarding.

Routing nuance: the portal SPA also lives at /ui/, with Vue Router serving /providers/{name} (and arbitrary sub-paths) as in-app routes that mount ProviderFrame. The proxy therefore only handles requests whose last path segment looks like a file (contains a "."): main.js, icon.svg, etc. Bare names like /ui/providers/quickstart or /ui/providers/quickstart/some-page are SPA routes — the proxy calls SetFallback() to dispatch them back to the portal SPA so a hard refresh preserves portal chrome.

Unknown name → 404. Provider not Ready → 503. Provider without a UI → 404.

func (*ProviderProxy) OrgProviderRoute

func (p *ProviderProxy) OrgProviderRoute(ctx context.Context, prov Provider, caller DelegatedCaller) (OrgProviderRoute, error)

OrgProviderRoute resolves the route for a hub-originated request to the org-owned provider prov on behalf of caller: the same edge hop serveOverEdge uses and the same delegated token delegatedAuthorization mints, with the same refusals (no user, no org, another org's provider, no team workspace, no issuer, mint failure). Every failure is an error. There is no fallback to BackendURL and none to a caller-supplied credential — the caller's bearer is not an input here at all.

The caller must already be authenticated and its membership in (OrgUUID, WorkspaceUUID) verified; see DelegatedCaller.

func (*ProviderProxy) ServeHTTP

func (p *ProviderProxy) ServeHTTP(w http.ResponseWriter, r *http.Request)

func (*ProviderProxy) SetClusterResolver

func (p *ProviderProxy) SetClusterResolver(f func(ctx context.Context, tenantPath string) (string, error))

SetClusterResolver installs the resolver mapping a tenant workspace path to its kcp logical-cluster ID, injected as both X-Railgrid-Tenant and X-Railgrid-Cluster on backend-proxied requests. Wire alongside SetTenantResolver; without it neither tenant header is sent (the workspace path is never used in their place) and any inbound value is still stripped.

func (*ProviderProxy) SetDelegatedTokenIssuer

func (p *ProviderProxy) SetDelegatedTokenIssuer(i DelegatedTokenIssuer)

SetDelegatedTokenIssuer installs the issuer used on the org-owned provider path. Wire alongside SetTenantResolver. Without one, every authenticated request to an org-owned provider is refused rather than forwarded with the caller's hub token.

func (*ProviderProxy) SetDelegationPolicy

func (p *ProviderProxy) SetDelegationPolicy(pol DelegationPolicy)

SetDelegationPolicy installs the platform-provider delegation policy. Wire alongside SetDelegatedTokenIssuer; without a call the proxy stays at DelegationOff. Only the backend proxy consults it — the UI proxy carries no credential to substitute.

func (*ProviderProxy) SetFallback

func (p *ProviderProxy) SetFallback(h http.Handler)

SetFallback installs the portal SPA handler invoked for non-asset paths under /ui/providers/{name}. See NewUIProxy for the rationale.

func (*ProviderProxy) SetKCPFrontDoor added in v0.2.7

func (p *ProviderProxy) SetKCPFrontDoor(target *url.URL, transport http.RoundTripper)

SetKCPFrontDoor tells the backend proxy where kcp is and how to authenticate to it, for the edge hop to org-owned providers. Without it that hop fails closed with a 503.

func (*ProviderProxy) SetMainJSIntegrityObserver added in v0.2.7

func (p *ProviderProxy) SetMainJSIntegrityObserver(o MainJSIntegrityObserver)

SetMainJSIntegrityObserver wires the UI proxy to the component that owns the pin. Wire after both exist (see pkg/hub/server.go, where the proxy is built early in Run and the catalog reconciler later); nil leaves the pin entirely to the reconcile loop. Calling it on the backend proxy has no effect — only the UI proxy serves /main.js.

func (*ProviderProxy) SetTenantResolver

func (p *ProviderProxy) SetTenantResolver(r TenantResolver)

SetTenantResolver installs the resolver that names the caller and their default workspace on backend-proxied requests (X-Railgrid-User and, with SetClusterResolver, the tenant headers). The UI proxy has no use for it.

func (*ProviderProxy) SetUIGrantKeys

func (p *ProviderProxy) SetUIGrantKeys(keys serviceaccounts.ProofKeySource)

SetUIGrantKeys installs the secret the UI proxy opens grants with. The same source mints them (UIGrantHandler reads it through the proxy), so every replica agrees. Without one the proxy refuses every grant-bearing request rather than serving an org bundle unscoped.

type ProviderReconciler

type ProviderReconciler struct {
	// contains filtered or unexported fields
}

ProviderReconciler provisions the kcp-side scaffolding declared by a Provider CR: the per-provider sub-workspace, the "provider" ServiceAccount, and a minted kubeconfig written into a Secret in root:railgrid:system:providers. It is the level-driven replacement for the former imperative admin "onboard" call — it chains the same Provisioner steps and then persists the kubeconfig instead of returning it over HTTP.

Scope is deliberately narrow: workspace + ServiceAccount + Secret, nothing else. The provider's APIExport / schemas / endpoint slice / bind grant remain the provider's own `init` responsibility.

func (*ProviderReconciler) Reconcile

Reconcile provisions (or tears down) one Provider.

type Provisioner

type Provisioner struct {
	// contains filtered or unexported fields
}

Provisioner owns the kcp-side side-effects of provisioning a provider: creating the per-provider sub-workspace, the "provider" ServiceAccount, and the minted kubeconfig Secret. It is driven by the Provider CR reconciler (provider_controller.go); the provider's own APIExport/schemas come from its `init`.

func NewProvisioner

func NewProvisioner(kcpConfig *rest.Config, opts ...ProvisionerOption) *Provisioner

NewProvisioner returns a Provisioner that performs provider-workspace side-effects (workspace, ServiceAccount, minted kubeconfig) against kcp using the given admin config. Used by the admin onboarding API (pkg/hub/admin); the catalog controller no longer provisions.

The default is the historical behaviour — cluster-admin in the provider's own workspace. Pass WithWorkspaceClusterAdmin(false) to bind the narrower generated role instead.

func (*Provisioner) DeleteKubeconfigSecret

func (p *Provisioner) DeleteKubeconfigSecret(ctx context.Context, namespace, name string) error

DeleteKubeconfigSecret removes the kubeconfig Secret from root:railgrid:system:providers. Idempotent (NotFound tolerated).

func (*Provisioner) DeleteProviderWorkspace

func (p *Provisioner) DeleteProviderWorkspace(ctx context.Context, name string) error

DeleteProviderWorkspace deletes the provider sub-workspace root:railgrid:providers/{name}. kcp cascades the ServiceAccount, its token Secret, and any APIExport / APIResourceSchemas the provider created there. Idempotent (NotFound tolerated).

func (*Provisioner) EnsureProviderSA

func (p *Provisioner) EnsureProviderSA(ctx context.Context, providerName string) error

EnsureProviderSA creates the "provider" ServiceAccount in the platform provider's sub-workspace (root:railgrid:providers/{name}) and grants it cluster-admin within that workspace. Idempotent.

func (*Provisioner) EnsureProviderSAAtPath

func (p *Provisioner) EnsureProviderSAAtPath(ctx context.Context, workspacePath string) error

EnsureProviderSAAtPath is EnsureProviderSA against an arbitrary provider workspace path. Org-owned providers live at root:railgrid:tenants/{org}/providers/{name} rather than under the platform parent, but are otherwise identical — same `provider` WorkspaceType, so the same missing `default` namespace and the same workspace-scoped cluster-admin grant apply.

func (*Provisioner) EnsureProviderWorkspace

func (p *Provisioner) EnsureProviderWorkspace(ctx context.Context, name string) (string, error)

EnsureProviderWorkspace creates root:railgrid:providers/{name} if it does not exist and waits for it to reach phase Ready. Idempotent. Returns the workspace's logical cluster ID (Workspace.spec.cluster) — the cluster name kcp embeds in the provider SA's token claims, recorded in the registry and reported by the admin providers API.

func (*Provisioner) ListProviderWorkspaces

func (p *Provisioner) ListProviderWorkspaces(ctx context.Context) ([]OnboardedWorkspace, error)

ListProviderWorkspaces returns the provider sub-workspaces under root:railgrid:providers. Used by the admin UI so onboarded providers appear even before their Helm chart (and CatalogEntry) is installed.

func (*Provisioner) MintProviderKubeconfig

func (p *Provisioner) MintProviderKubeconfig(ctx context.Context, providerName, hubExternalURL string) ([]byte, error)

MintProviderKubeconfig ensures a long-lived (legacy) token for the provider SA and returns a base64-encoded exec-credential-less kubeconfig the provider pod can mount. The token is read from a kubernetes.io/service-account-token Secret populated by kcp's token controller, so it does not expire and needs no rotation. The server URL is hubExternalURL + /clusters/{logical-cluster-id} so the provider's typed Kubernetes clients land in its own workspace by default. The ID (not the workspace path) is used so the kubeconfig works once requests reach a kcp shard, which only resolves /clusters/<id>.

func (*Provisioner) MintProviderKubeconfigAtPath

func (p *Provisioner) MintProviderKubeconfigAtPath(ctx context.Context, workspacePath, hubExternalURL string) ([]byte, error)

MintProviderKubeconfigAtPath is MintProviderKubeconfig against an arbitrary provider workspace path, for org-owned providers under root:railgrid:tenants/{org}/providers/{name}.

The resulting kubeconfig is what an Org admin installs their provider's Helm chart with. It is scoped to that one workspace: the SA is cluster-admin there and nowhere else, so it can create the provider's APIExport, schemas, endpoint slice, and CatalogEntry, but cannot read the Org workspace above it or any team workspace beside it. Cross-workspace reach only ever comes from the APIExport's permission claims, which each consuming workspace accepts individually at Enable time.

func (*Provisioner) ResolveAPIExportGroups added in v0.2.7

func (p *Provisioner) ResolveAPIExportGroups(ctx context.Context, workspacePath, exportName string) ([]string, error)

ResolveAPIExportGroups returns the API groups an APIExport serves, read from spec.resources[].group on the live object in the provider's workspace, deduped and sorted.

This is the ONE authoritative answer to "which API groups does this provider serve". The CatalogEntry names the export but not its groups, and the two differ for most providers (`edges.providers.railgrid.ai` exports `edges.railgrid.ai`), so nothing downstream may infer one from the other.

It reads the LIVE object rather than the generated apiexport.yaml on purpose: spec.resources has several writers. The provider's `init` applies what apigen produced, and some providers add entries at runtime — the infrastructure provider ships an export with no resources at all and mints its APIResourceSchemas and its Templates CachedResource from the operator (see provider-sdk/install.ApplyAPIExport's merge). Only the object in the workspace has all of them.

An export that exists but serves nothing yields an empty list and no error: that is a real state (init has applied the export but not yet its schemas), and it is fail-closed downstream.

func (*Provisioner) ResolveClusterPath

func (p *Provisioner) ResolveClusterPath(ctx context.Context, clusterID string) (string, error)

ResolveClusterPath returns the canonical kcp workspace path of a logical cluster (e.g. root:railgrid:tenants:<org>:providers:<name>), read from the kcp.io/path annotation kcp stamps on the cluster's LogicalCluster object when the workspace is created.

It deliberately uses the hub's kcp-admin config addressed at /clusters/<id> rather than any APIExport virtual-workspace client: a VW only serves the resources its APIExport declares, and providers.railgrid.ai claims none, so core.kcp.io/LogicalCluster is simply not present there.

An empty path with a nil error means the read succeeded but the cluster carries no path annotation. Callers may treat that as "not a workspace this hub created through the Workspace API", because every workspace created that way is annotated.

func (*Provisioner) ResolveWorkspaceCluster

func (p *Provisioner) ResolveWorkspaceCluster(ctx context.Context, name string) (string, error)

ResolveWorkspaceCluster returns the logical cluster ID of the provider's sub-workspace (root:railgrid:providers/{name}), read-only. Returns "" (no error) when the workspace does not exist yet — i.e. the provider has not been onboarded. The catalog reconciler feeds this into the registry so the admin providers API can report a provider's workspace without the hub provisioning anything.

func (*Provisioner) RotateProviderCredential

func (p *Provisioner) RotateProviderCredential(ctx context.Context, providerName, hubExternalURL string) (*RotatedCredential, error)

RotateProviderCredential rotates the platform provider at root:railgrid:providers/{name}.

func (*Provisioner) RotateProviderCredentialAtPath

func (p *Provisioner) RotateProviderCredentialAtPath(ctx context.Context, workspacePath, providerName, hubExternalURL string) (*RotatedCredential, error)

RotateProviderCredentialAtPath issues a SECOND long-lived token for the provider's ServiceAccount, makes it the one the hub hands out, and schedules the previous one for deletion after the grace period.

Both tokens belong to the same ServiceAccount, so during the grace period the provider authenticates identically with either: the identity kcp reports is system:serviceaccount:default:provider in both cases, which is what every hub-side check keys on. That is what makes rotation a rolling change rather than an outage — the operator installs the new kubeconfig whenever it suits them, and the old credential stops working on its own.

providerName is only used to record status.credentialsRotatedAt on the provider's CatalogEntry; pass "" to skip that.

func (*Provisioner) SweepExpiredProviderTokens

func (p *Provisioner) SweepExpiredProviderTokens(ctx context.Context, cluster string) (deleted int, nextExpiry time.Time, err error)

SweepExpiredProviderTokens deletes the provider token Secrets in a provider workspace whose grace period has passed, and reports when the next one lapses (zero when none is pending).

It is what actually ends the old credential's life: rotation only writes the expiry. Running it from the catalog reconciler means it runs wherever a provider is known, including org-owned workspaces the hub never lists.

cluster may be a workspace path or a logical cluster ID.

func (*Provisioner) WriteKubeconfigSecret

func (p *Provisioner) WriteKubeconfigSecret(ctx context.Context, namespace, name, key string, kc []byte, providerName string) error

WriteKubeconfigSecret create-or-updates a Secret in root:railgrid:system:providers (where the Provider CR lives, NOT the provider sub-workspace) holding the provider's minted kubeconfig under key. The Secret lives next to the Provider CR so a provider pod (or dev tooling) can read its credentials from one well-known place. Idempotent. Ensures the target namespace exists first.

type ProvisionerOption

type ProvisionerOption func(*Provisioner)

ProvisionerOption configures a Provisioner.

func WithCredentialGracePeriod

func WithCredentialGracePeriod(d time.Duration) ProvisionerOption

WithCredentialGracePeriod overrides how long a rotated-out provider token Secret stays usable before it is swept. Zero or negative restores the default.

func WithWorkspaceClusterAdmin

func WithWorkspaceClusterAdmin(clusterAdmin bool) ProvisionerOption

WithWorkspaceClusterAdmin selects the role the provider's ServiceAccount is bound to inside its own provider workspace: true keeps the historical cluster-admin binding, false binds the generated, narrower railgrid:provider ClusterRole (see providerClusterRoleRules).

It is a Provisioner-level option rather than a per-call argument because every caller that creates a provider SA — admin onboarding, the Provider reconciler, org-owned registration — must agree: a hub that narrowed the role in one path and not another would leave the wider binding in place for whichever path ran last.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry is the hub's source of truth for provider routing. It is updated by the catalog controller on Watch events and read by the proxies on each request. Reads vastly outnumber writes; an RWMutex is sufficient.

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty Registry.

func (*Registry) CatalogEntryCluster

func (r *Registry) CatalogEntryCluster(name string) (string, bool)

CatalogEntryCluster returns the logical cluster the platform provider's CatalogEntry lives in, and whether it is known yet. It satisfies the heartbeat recorder's ClusterResolver, and is global-scoped for the same reason Get is: it is reached from the bare-name heartbeat endpoint.

func (*Registry) Delete

func (r *Registry) Delete(name string) bool

Delete removes the platform-global record for name. Returns true if anything was removed.

func (*Registry) DeleteByCluster

func (r *Registry) DeleteByCluster(cluster, name string) bool

DeleteByCluster removes the record whose CatalogEntry was observed in the given logical cluster. This is what the catalog reconciler needs on a delete event: the object is already gone, so its workspace path — and therefore its scope — can no longer be resolved, but the cluster the event arrived from is still known and uniquely identifies the record. Returns true if anything was removed.

func (*Registry) DeleteScoped

func (r *Registry) DeleteScoped(orgUUID, name string) bool

DeleteScoped removes the record for one provider in one scope. orgUUID is empty for platform-global providers. Returns true if anything was removed.

func (*Registry) Get

func (r *Registry) Get(name string) (Provider, bool)

Get returns a copy of the platform-global Provider record (or false if unknown). A copy is returned so callers can safely inspect fields without holding the lock.

Get deliberately never returns an org-owned provider. It backs the request path that has no tenant context — the UI/backend proxies and the heartbeat endpoint, which address providers by bare name. If org-owned records were reachable here, an Org could register a provider named after a platform one and capture its route. Callers that do hold tenant context use GetForOrg.

func (*Registry) GetForOrg

func (r *Registry) GetForOrg(orgUUID, name string) (Provider, bool)

GetForOrg resolves a provider name in the scope of one Org: the Org's own provider wins, falling back to the platform-global one. Used by the tenant-scoped Enable/Disable path, where the caller's Org is known.

func (*Registry) Heartbeat

func (r *Registry) Heartbeat(name, reportedVersion string, now time.Time) bool

Heartbeat records a heartbeat for a known platform provider. Returns false if the name is not in the registry (caller should reject with 404). reportedVersion is optional; pass empty to leave the field untouched.

Global-scoped: the heartbeat endpoint addresses providers by bare name with no tenant context, so an org-owned provider cannot beat here — and cannot keep a platform provider of the same name looking alive. Org-owned providers consequently never set HeartbeatRequired, which leaves their readiness resting on endpoint validity alone.

func (*Registry) List

func (r *Registry) List() []Provider

List returns a snapshot of every registered provider across all scopes. Callers serving a tenant must use ListForOrg instead; this is for the sweeper and admin surfaces that legitimately span orgs.

func (*Registry) ListForOrg

func (r *Registry) ListForOrg(orgUUID string) []Provider

ListForOrg returns the providers visible to one Org: every platform-global provider plus that Org's own. An org-owned provider shadows a platform one of the same name, matching GetForOrg's resolution order, so the portal never shows two cards with the same name.

func (*Registry) OnRequirementsChange added in v0.2.21

func (r *Registry) OnRequirementsChange(fn RequirementsListener) (unregister func())

OnRequirementsChange registers fn to be called from Upsert whenever a provider's spec.requires differs from what the registry held before (or the provider is new), and returns the function that unregisters it. The requirements are what the hub keeps accepted on every tenant APIBinding to the provider's export, so this is the signal that those bindings need revisiting — the binding-claims reconciler subscribes here rather than growing a second watch on CatalogEntries. Subscribers that live for one leadership term unregister when the term ends, so the registry (which lives for the process) does not accumulate dead listeners.

func (*Registry) RequirementsListeners added in v0.2.21

func (r *Registry) RequirementsListeners() int

RequirementsListeners reports how many listeners are registered. For tests that pin the unregister contract.

func (*Registry) SetWorkspaceCluster

func (r *Registry) SetWorkspaceCluster(orgUUID, name, cluster string) bool

SetWorkspaceCluster records the logical cluster ID of the provider's sub-workspace. Called by the catalog controller once provisioning has resolved it (the workspace must be Ready before spec.cluster is set). Returns false if the provider is not in the registry.

func (*Registry) SweepStale

func (r *Registry) SweepStale(now time.Time, ttl time.Duration) int

SweepStale walks the registry and marks providers whose last heartbeat is older than ttl as stale. Providers that have never heartbeated (HeartbeatRequired=false) are left alone — they're treated as "doesn't heartbeat" not "missed a beat". Returns the number of records flipped.

func (*Registry) Upsert

func (r *Registry) Upsert(p Provider)

Upsert replaces the spec-derived fields of the registry record for p.Name (or inserts a new record).

Liveness is merged, not overwritten. p.LastHeartbeat carries what the CatalogEntry status says (the cross-replica signal); the record may already hold a newer beat this replica served directly, or one whose status write was throttled. Taking the later of the two keeps the merge monotone, so neither source can move a provider backwards in time and flip it stale.

type RequirementsListener added in v0.2.21

type RequirementsListener func(p Provider)

RequirementsListener receives a copy of the provider whose requirements changed. It must not block: it is called from the catalog reconciler.

type ResolvedValue

type ResolvedValue struct {
	Name        string `json:"name"`
	Value       string `json:"value"`
	Description string `json:"description,omitempty"`
	// Unresolved marks a value the installer must supply themselves — the
	// command contains a placeholder that will not work as-is.
	Unresolved bool `json:"unresolved,omitempty"`
}

ResolvedValue is one Helm value in the generated command, carried alongside the command so the portal can explain each one and flag the ones the hub could not fill in.

type RotatedCredential

type RotatedCredential struct {
	// Kubeconfig is the new credential, in the same shape registration
	// returns.
	Kubeconfig []byte
	// SecretName is the token Secret the new credential came from.
	SecretName string
	// PreviousSecretName is the Secret that was current before, empty when
	// there was none to retire.
	PreviousSecretName string
	// PreviousValidUntil is when the previous credential stops working. Zero
	// when nothing was retired. Until then BOTH tokens authenticate as the
	// same ServiceAccount, so anything that checks the provider's identity —
	// the heartbeat's TokenReview included — keeps accepting the old one while
	// the provider is rolled onto the new one.
	PreviousValidUntil time.Time
	// RotatedAt is the rotation's wall-clock time, recorded on the
	// CatalogEntry status.
	RotatedAt time.Time
}

RotatedCredential describes the outcome of a rotation.

type SelfHosting

type SelfHosting struct {
	Supported    bool
	ChartRepo    string
	ChartName    string
	ChartVersion string
	Namespace    string
	ReleaseName  string
	DocsURL      string
	// ValuesDoc is the chart's values reference in Markdown, carried inline so
	// it works without internet access and matches the deployed chart version.
	ValuesDoc      string
	RequiredValues []SelfHostingValue
}

SelfHosting mirrors CatalogEntry.spec.serving.selfHosting: how an organization runs its own copy of this provider. Nil when the provider does not offer it.

func (*SelfHosting) Installable

func (s *SelfHosting) Installable() bool

Installable reports whether there is enough here to generate instructions. Supported alone is not enough — without chart coordinates the hub would emit a helm command with a blank chart reference.

type SelfHostingValue

type SelfHostingValue struct {
	Name        string
	Description string
	Value       string
}

SelfHostingValue is one Helm value the installer must set.

type TenantResolver

type TenantResolver interface {
	Resolve(r *http.Request) (user, tenantPath string, err error)
}

TenantResolver resolves the caller's identity (User CR name) and tenant workspace path (e.g. root:railgrid:orgs:{orgUUID}) from an HTTP request's bearer token. Implementations typically wrap proxy.KCPProxy.IdentifyUser plus a User → Organization → WorkspacePath lookup; see pkg/hub/server.go for the canonical wiring. Returns an error when the caller can't be resolved — the proxy treats that as "no headers to inject" (best effort), it does NOT 401, so anonymous reads of provider /healthz keep working.

type TenantResolverFunc

type TenantResolverFunc func(r *http.Request) (string, string, error)

TenantResolverFunc adapts a plain function to the TenantResolver interface. Lets callers compose the lookup inline without declaring a type.

func (TenantResolverFunc) Resolve

func (f TenantResolverFunc) Resolve(r *http.Request) (string, string, error)

Resolve satisfies TenantResolver.

type UIGrantHandler

type UIGrantHandler struct {
	// contains filtered or unexported fields
}

UIGrantHandler serves POST /api/providers/{name}/ui-grant. It is built early (so it can be registered ahead of the heartbeat prefix route) and configured once the auth stack exists, like the proxies.

func NewUIGrantHandler

func NewUIGrantHandler(reg *Registry, uiProxy *ProviderProxy, log logr.Logger) *UIGrantHandler

NewUIGrantHandler builds the grant endpoint over reg, minting with the keys and issuer installed on uiProxy (SetUIGrantKeys, SetDelegatedTokenIssuer).

func (*UIGrantHandler) ServeHTTP

func (h *UIGrantHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

func (*UIGrantHandler) SetTenantResolver

func (h *UIGrantHandler) SetTenantResolver(r TenantResolver)

SetTenantResolver installs the resolver that names the caller. It is the same resolver the backend proxy uses, so the membership checks behind X-Railgrid-Org / X-Railgrid-Workspace are the same ones. Until one is installed every request is refused.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL