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
- Variables
- func APIExportGroups(export *unstructured.Unstructured) []string
- func AuthenticatedShardHandler(next http.Handler) http.Handler
- func BindingReadiness(item *unstructured.Unstructured) (bool, string)
- func CompileProviderActionSchema(raw json.RawMessage, name string) (*jsonschema.Schema, error)
- func CopyOrgProviderBackendProtocolHeaders(dst, src http.Header, routePath string)
- func CopyOrgProviderResponseHeaders(dst, src http.Header)
- func CopyOrgProviderUIAssetResponseHeaders(dst, src http.Header)
- func DeleteOrgProviderClaimPolicy(ctx context.Context, config *rest.Config, cluster string) error
- func EmbeddedRelayTransport(next http.RoundTripper, handler http.Handler) http.RoundTripper
- func NewHeartbeatHandler(reg *Registry, record HeartbeatRecorder, authenticate HeartbeatAuthenticator, ...) http.Handler
- func ParseURL(raw string) (*url.URL, error)
- func ProviderDataPlaneURL(base, catalogCluster, provider string) (string, error)
- func ReleaseOrgProviderGroups(ctx context.Context, config *rest.Config, org, cluster, uid string) error
- func RunSweeper(ctx context.Context, reg *Registry, log logr.Logger)
- func SetupProviderWithManager(mgr mcmanager.Manager, kcpConfig *rest.Config, opts CatalogReconcilerOptions) error
- func ShardRelayTLSConfig(caFile string, allowedNames []string) (*tls.Config, error)
- func TokenReviewMatchesCluster(status authnv1.TokenReviewStatus, cluster string) bool
- func ValidateOrgProviderPrivateResource(resource schema.GroupResource) error
- type AgentIngressResolver
- type AgentIngressTarget
- type BoundCopyResolver
- type CatalogReconciler
- type CatalogReconcilerOptions
- type Category
- type ClusterResolver
- type DelegatedCaller
- type DelegatedTokenIssuer
- type DelegatedTokenIssuerFunc
- type DelegationMode
- type DelegationPolicy
- type EdgeRoute
- type EdgeRouteResolver
- type HeartbeatAuthenticator
- type HeartbeatRecorder
- type InstallInstructions
- type InstallOptions
- type InstallStep
- type ListHandler
- type MainJSIntegrityObserver
- type NavChild
- type OnboardedWorkspace
- type OrgBootstrapIdentity
- type OrgProviderBootstrapAuthenticator
- type OrgProviderBootstrapHandler
- type OrgProviderRoute
- type Provider
- type ProviderAction
- type ProviderActionLimits
- type ProviderActionResource
- type ProviderAssistantSkill
- type ProviderAssistantSkillResource
- type ProviderDTO
- type ProviderProxy
- func (p *ProviderProxy) OrgProviderRoute(ctx context.Context, prov Provider, caller DelegatedCaller) (OrgProviderRoute, error)
- func (p *ProviderProxy) ServeHTTP(w http.ResponseWriter, r *http.Request)
- func (p *ProviderProxy) SetAgentIngressResolver(resolve AgentIngressResolver)
- func (p *ProviderProxy) SetBoundCopyResolver(resolve BoundCopyResolver)
- func (p *ProviderProxy) SetClusterResolver(f func(ctx context.Context, tenantPath string) (string, error))
- func (p *ProviderProxy) SetDelegatedTokenIssuer(i DelegatedTokenIssuer)
- func (p *ProviderProxy) SetDelegationPolicy(pol DelegationPolicy)
- func (p *ProviderProxy) SetFallback(h http.Handler)
- func (p *ProviderProxy) SetKCPFrontDoor(target *url.URL, transport http.RoundTripper)
- func (p *ProviderProxy) SetMainJSIntegrityObserver(o MainJSIntegrityObserver)
- func (p *ProviderProxy) SetTenantResolver(r TenantResolver)
- func (p *ProviderProxy) SetUIGrantAuthorizer(authorize func(context.Context, string, string, string) bool)
- func (p *ProviderProxy) SetUIGrantKeys(keys serviceaccounts.ProofKeySource)
- type ProviderReconciler
- type ProviderRelay
- type Provisioner
- func (p *Provisioner) DeleteKubeconfigSecret(ctx context.Context, namespace, name string) error
- func (p *Provisioner) DeleteProviderWorkspace(ctx context.Context, name string) error
- func (p *Provisioner) EnsureOrgProviderPrivateResourceRBAC(ctx context.Context, workspacePath string, resources []schema.GroupResource) error
- func (p *Provisioner) EnsureProviderSA(ctx context.Context, providerName string) error
- func (p *Provisioner) EnsureProviderSAAtPath(ctx context.Context, workspacePath string) error
- func (p *Provisioner) EnsureProviderWorkspace(ctx context.Context, name string) (string, error)
- func (p *Provisioner) ListProviderWorkspaces(ctx context.Context) ([]OnboardedWorkspace, error)
- func (p *Provisioner) MintProviderKubeconfig(ctx context.Context, providerName, hubExternalURL string) ([]byte, error)
- func (p *Provisioner) MintProviderKubeconfigAtPath(ctx context.Context, workspacePath, hubExternalURL string) ([]byte, error)
- func (p *Provisioner) ResolveAPIExportGroups(ctx context.Context, workspacePath, exportName string) ([]string, error)
- func (p *Provisioner) ResolveClusterPath(ctx context.Context, clusterID string) (string, error)
- func (p *Provisioner) ResolveWorkspaceCluster(ctx context.Context, name string) (string, error)
- func (p *Provisioner) RotateProviderCredential(ctx context.Context, providerName, hubExternalURL string) (*RotatedCredential, error)
- func (p *Provisioner) RotateProviderCredentialAtPath(ctx context.Context, workspacePath, providerName, hubExternalURL string) (*RotatedCredential, error)
- func (p *Provisioner) SweepExpiredProviderTokens(ctx context.Context, cluster string) (deleted int, nextExpiry time.Time, err error)
- func (p *Provisioner) WriteKubeconfigSecret(ctx context.Context, namespace, name, key string, kc []byte, ...) error
- type ProvisionerOption
- type Registry
- func (r *Registry) BoundCopy(org, name, path, export, cluster string) (Provider, bool)
- func (r *Registry) CatalogEntryCluster(name string) (string, bool)
- func (r *Registry) Delete(name string) bool
- func (r *Registry) DeleteByCluster(cluster, name string) bool
- func (r *Registry) DeleteScoped(orgUUID, name string) bool
- func (r *Registry) Get(name string) (Provider, bool)
- func (r *Registry) GetForOrg(orgUUID, name string) (Provider, bool)
- func (r *Registry) Heartbeat(name, reportedVersion string, now time.Time) bool
- func (r *Registry) List() []Provider
- func (r *Registry) ListForOrg(orgUUID string) []Provider
- func (r *Registry) OnRequirementsChange(fn RequirementsListener) (unregister func())
- func (r *Registry) ProviderWorkspaceCluster(name string) (string, bool)
- func (r *Registry) RequirementsListeners() int
- func (r *Registry) SetWorkspaceCluster(orgUUID, name, cluster string) bool
- func (r *Registry) SweepStale(now time.Time, ttl time.Duration) int
- func (r *Registry) Upsert(p Provider)
- type RelayAuthorization
- type RequirementsListener
- type ResolvedValue
- type RotatedCredential
- type SelfHosting
- type SelfHostingValue
- type TenantResolver
- type TenantResolverFunc
- type UIGrantHandler
Constants ¶
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.
const ( // OrgProviderBootstrapPolicyAnnotation is authoritative only on the // provider Workspace in its hub-owned parent. The provider runtime cannot // write annotations on that Workspace. OrgProviderBootstrapPolicyAnnotation = "providers.railgrid.ai/bootstrap-policy" OrgProviderBootstrapPolicy = "restricted-v1" OrgProviderClusterRoleName = "railgrid:org-provider" OrgProviderEdgeRouteWorkspaceAnnotation = "edges.railgrid.ai/route-workspace" OrgProviderEdgeRouteEdgeAnnotation = "edges.railgrid.ai/route-edge" // OrgProviderPrivateResourceLimit bounds both private CRD declarations and // the runtime permissions the hub may derive from them. OrgProviderPrivateResourceLimit = 16 )
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" )
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" )
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.
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.
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.
const EmbeddedProviderRelayURL = "http://railgrid-provider-relay.internal"
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.
const KubeconfigSecretKey = "kubeconfig"
KubeconfigSecretKey is the required data key inside that Secret.
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.
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.
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
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.
const ProviderKubeconfigSecretKey = "kubeconfig"
ProviderKubeconfigSecretKey is the data key the provider kubeconfig is stored under in the Secret the Provider controller writes into system:providers.
const ProviderRelayPrefix = "/provider-dataplane/"
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.
const ProviderSANamespace = "default"
ProviderSANamespace is the namespace ProviderSAName lives in. It must stay in lockstep with EnsureProviderSA, which is what actually creates the SA.
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.
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.
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.
const SweepInterval = 30 * time.Second
SweepInterval is how often the sweeper goroutine walks the registry to evict stale heartbeats. Should comfortably divide HeartbeatTTL.
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 ¶
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") // token right now: no kcp, or the provider's workspace is not known yet // (503). ErrHeartbeatAuthUnavailable = errors.New("heartbeat authentication unavailable") )
var ( )
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.
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 platform token resolves to system:serviceaccount:default:railgrid-pdu-<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.
ErrBoundCopyUnavailable means the workspace has no usable registered copy. It deliberately carries no tenant, registration, or upstream error details.
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 AuthenticatedShardHandler ¶ added in v0.2.47
AuthenticatedShardHandler is safe only behind ShardRelayTLSConfig; it checks verified chains again so mounting this handler on ordinary HTTP fails closed.
func BindingReadiness ¶ added in v0.2.43
func BindingReadiness(item *unstructured.Unstructured) (bool, string)
BindingReadiness is shared by routing and the enabled-provider projection. A Bound phase alone does not establish that the export and claims are usable.
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 CopyOrgProviderBackendProtocolHeaders ¶ added in v0.2.47
CopyOrgProviderBackendProtocolHeaders preserves OAuth redirects and MCP authentication metadata on their ordinary backend routes. The custom- subresource relay uses stricter rules for both headers.
func CopyOrgProviderResponseHeaders ¶ added in v0.2.47
CopyOrgProviderResponseHeaders copies the common response headers the hub preserves for organization-owned providers. src must be a private upstream header map; dst may already contain headers installed by trusted outer hub middleware, which are left in place unless one of the explicitly copied or forced headers below replaces it. OAuth and MCP headers are added separately by the ordinary backend proxy; the custom-subresource relay applies stricter rules to them.
In particular, upstream cookies, browser policy, CORS, reporting, transport, and trailer headers are never copied. The forced policy also makes direct navigation to an untrusted HTML or SVG response inert and non-cacheable.
func CopyOrgProviderUIAssetResponseHeaders ¶ added in v0.2.47
CopyOrgProviderUIAssetResponseHeaders applies the common provider response policy to an org-owned UI asset. Asset responses keep content metadata for SRI, caching, and range requests, and always suppress external referrers.
func DeleteOrgProviderClaimPolicy ¶ added in v0.2.47
DeleteOrgProviderClaimPolicy removes the Hub-owned admission declaration for an exporter after its tenant bindings and export have been drained.
func EmbeddedRelayTransport ¶ added in v0.2.47
func EmbeddedRelayTransport(next http.RoundTripper, handler http.Handler) http.RoundTripper
EmbeddedRelayTransport intercepts only the non-network embedded relay origin. net.Pipe retains net/http streaming and upgrade support without listening on any socket. All other shard destinations retain their original transport.
func NewHeartbeatHandler ¶
func NewHeartbeatHandler(reg *Registry, record HeartbeatRecorder, authenticate HeartbeatAuthenticator, 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. A failed check 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. 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 ¶
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 ProviderDataPlaneURL ¶ added in v0.2.47
ProviderDataPlaneURL is the only endpoint URL the hub publishes for an org provider. kcp appends the original /clusters/{consumer}/apis/... request.
func ReleaseOrgProviderGroups ¶ added in v0.2.47
func ReleaseOrgProviderGroups(ctx context.Context, config *rest.Config, org, cluster, uid string) error
ReleaseOrgProviderGroups is UID-fenced and called only after export drain. A delayed teardown cannot release a replacement registration's reservations.
func RunSweeper ¶
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.
func ShardRelayTLSConfig ¶ added in v0.2.47
ShardRelayTLSConfig authenticates only explicitly named shard certificates. It is used on a dedicated listener, never on the browser-facing listener.
func TokenReviewMatchesCluster ¶ added in v0.2.43
func TokenReviewMatchesCluster(status authnv1.TokenReviewStatus, cluster string) bool
TokenReviewMatchesCluster verifies the issuer reported by native kcp after authenticating the credential. Its JWT validators look up the account in the token's issuing cluster, not necessarily the TokenReview endpoint's cluster. Identical ServiceAccount usernames in different clusters are different principals. Never infer this issuer from the request URL or unverified JWT.
func ValidateOrgProviderPrivateResource ¶ added in v0.2.47
func ValidateOrgProviderPrivateResource(resource schema.GroupResource) error
ValidateOrgProviderPrivateResource rejects names that could mutate bootstrap, authentication, routing, or platform objects. CRD conversion webhooks are rejected separately by the bootstrap payload validator.
Types ¶
type AgentIngressResolver ¶ added in v0.2.43
type AgentIngressResolver func(context.Context, string, string, string, string) (AgentIngressTarget, error)
AgentIngressResolver verifies an edge's bootstrap/scoped credential before returning its target. No human bearer is forwarded by this path.
type AgentIngressTarget ¶ added in v0.2.43
type AgentIngressTarget struct {
OrgUUID string
ExportPath string
ExportName string
ExportCluster string
}
AgentIngressTarget is the authenticated consumer organization and native export identity selected by its actual Edges binding.
type BoundCopyResolver ¶ added in v0.2.43
type BoundCopyResolver func(ctx context.Context, tenantPath, cluster, name string) (Provider, error)
BoundCopyResolver selects a backend using the authenticated caller's verified workspace and its live APIBinding. It must never select from request headers or silently substitute the catalog's same-named organization copy.
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 ¶
func (r *CatalogReconciler) Reconcile(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error)
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.
type ClusterResolver ¶
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 ¶
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 ¶
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 ¶
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 HeartbeatAuthenticator ¶
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 providerWorkspaceClusterResolver) (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 ¶
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 platform or organization workspace, where each provider's `init` self-registers its entry. The HTTP handler carries the TokenReview target through the request context to preserve org scope.
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 {
Values map[string]string
// Insecure is set only by a development hub for its self-signed certificate.
Insecure bool
// 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.
func (*ListHandler) SetUIGrantHandler ¶ added in v0.2.47
func (h *ListHandler) SetUIGrantHandler(grants *UIGrantHandler)
SetUIGrantHandler installs the existing scoped asset-grant issuer used for org-owned provider icons. It is wired alongside the catalog middleware once the hub auth stack exists. Without it, org icons fall back to the portal's generic provider glyph.
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 ¶
type NavChild struct {
}
NavChild mirrors CatalogEntry.spec.serving.ui.children — a single sub-nav entry the portal renders indented under its parent provider.
type OnboardedWorkspace ¶
OnboardedWorkspace is a provider sub-workspace under root:railgrid:providers created by onboarding (independent of whether a CatalogEntry has registered the provider yet).
type OrgBootstrapIdentity ¶ added in v0.2.47
type OrgBootstrapIdentity struct {
RegistrationUID string
OrgUUID string
Cluster string
WorkspacePath string
EdgeWorkspace string
EdgeName string
}
OrgBootstrapIdentity comes from live kcp state verified by TokenReview. The organization, provider workspace, and selected Edge route never come from headers or the submitted declaration.
type OrgProviderBootstrapAuthenticator ¶ added in v0.2.47
type OrgProviderBootstrapAuthenticator func(context.Context, *http.Request, string) (OrgBootstrapIdentity, error)
func NewOrgProviderBootstrapAuthenticator ¶ added in v0.2.47
func NewOrgProviderBootstrapAuthenticator(config *rest.Config) (OrgProviderBootstrapAuthenticator, error)
NewOrgProviderBootstrapAuthenticator authenticates the restricted runtime identity before a CatalogEntry exists. A JWT cluster is only a bounded routing hint: TokenReview, the LogicalCluster, its hub-owned Workspace, the selected Edge route, and the live ServiceAccount must all agree. No result is cached, so registration removal and credential replacement take effect immediately.
type OrgProviderBootstrapHandler ¶ added in v0.2.47
type OrgProviderBootstrapHandler struct {
// contains filtered or unexported fields
}
OrgProviderBootstrapHandler accepts a provider declaration only from the restricted ServiceAccount in the Hub-registered organization workspace. The Hub validates and applies every object that can affect routing or RBAC.
func NewOrgProviderBootstrapHandler ¶ added in v0.2.47
func NewOrgProviderBootstrapHandler(config *rest.Config, reg *Registry, relayBase string) (*OrgProviderBootstrapHandler, error)
NewOrgProviderBootstrapHandler creates the authenticated bootstrap endpoint. An empty relay base is allowed at construction so a platform that does not configure provider relay can still serve ordinary platform providers; an org bootstrap will receive a clear 503 when it needs the relay.
func (*OrgProviderBootstrapHandler) ServeHTTP ¶ added in v0.2.47
func (h *OrgProviderBootstrapHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
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.
//
// Heartbeat authentication reviews the provider ServiceAccount in this
// workspace and checks its native verified issuer. It can differ
// from CatalogEntryCluster for legacy platform entries; catalog placement
// does not establish where the export or credential lives. The admin API
// also exposes it for operators.
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
// EndpointsValid is true when spec.serving.ui.url/spec.serving.backend.url parsed cleanly
// and at least one endpoint was declared.
// 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.
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 ¶
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 ¶
ProviderAssistantSkillResource is one package-relative supporting resource.
type ProviderDTO ¶ added in v0.2.43
type ProviderDTO = providerDTO
ProviderDTO is the stable portal-facing provider presentation. It excludes backend addresses and credentials and is shared by catalog and bound-copy views.
func PresentProvider ¶ added in v0.2.43
func PresentProvider(p Provider) ProviderDTO
PresentProvider projects one exact registered copy without resolving its name through catalog shadowing. Callers select and authorize the copy first.
func PresentProviderForTenant ¶ added in v0.2.47
func PresentProviderForTenant(ctx context.Context, p Provider, caller tenant.TenantContext, grants *UIGrantHandler) (ProviderDTO, bool)
PresentProviderForTenant projects one exact provider copy and, when its icon is an org-owned asset served by that provider, replaces the raw UI path with a short-lived scoped asset grant for the verified caller. A platform or external icon keeps its existing URL. The bool reports whether a grant URL was issued so tenant-specific responses can disable shared caching.
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) SetAgentIngressResolver ¶ added in v0.2.43
func (p *ProviderProxy) SetAgentIngressResolver(resolve AgentIngressResolver)
func (*ProviderProxy) SetBoundCopyResolver ¶ added in v0.2.43
func (p *ProviderProxy) SetBoundCopyResolver(resolve BoundCopyResolver)
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) SetUIGrantAuthorizer ¶ added in v0.2.43
func (p *ProviderProxy) SetUIGrantAuthorizer(authorize func(context.Context, string, string, string) bool)
SetUIGrantAuthorizer rechecks current workspace membership on every asset redemption; a browser asset grant does not extend a revoked membership.
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 ¶
func (r *ProviderReconciler) Reconcile(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error)
Reconcile provisions (or tears down) one Provider.
type ProviderRelay ¶ added in v0.2.47
type ProviderRelay struct {
Registry *Registry
Proxy *ProviderProxy
Keys serviceaccounts.ProofKeySource
Authorize RelayAuthorization
Discovery func(context.Context, Provider, string, string) (*metav1.APIResourceList, error)
}
ProviderRelay accepts requests only from the dedicated authenticated shard listener or the embedded shard's in-process transport.
func (*ProviderRelay) PublicKey ¶ added in v0.2.47
func (h *ProviderRelay) PublicKey(w http.ResponseWriter, r *http.Request)
func (*ProviderRelay) ServeHTTP ¶ added in v0.2.47
func (h *ProviderRelay) ServeHTTP(w http.ResponseWriter, r *http.Request)
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) EnsureOrgProviderPrivateResourceRBAC ¶ added in v0.2.47
func (p *Provisioner) EnsureOrgProviderPrivateResourceRBAC(ctx context.Context, workspacePath string, resources []schema.GroupResource) error
EnsureOrgProviderPrivateResourceRBAC replaces the runtime grant for the exact private CRDs admitted by bootstrap. It grants objects and status, never CRD or schema writes. Omitting a former resource revokes that grant.
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 ¶
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. An organization provider gets the restricted runtime role and submits its declaration through the authenticated Hub bootstrap endpoint; it cannot mutate APIExports, schemas, endpoint slices, CRDs, or RBAC directly. Platform provider permissions remain controlled by the existing operator setting. Cross-workspace reach still comes from the APIExport's accepted permission claims.
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 ¶
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 ¶
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 WithProviderKubeconfigTLS ¶ added in v0.2.47
func WithProviderKubeconfigTLS(caFile string, insecure bool) ProvisionerOption
WithProviderKubeconfigTLS configures the provider-facing Hub trust, never the KCP admin client's trust. Insecure is reserved for the Hub's explicit dev mode.
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 (*Registry) BoundCopy ¶ added in v0.2.43
BoundCopy matches the immutable coordinates of an actual APIBinding against a current registration visible to this organization. A same-named replacement with a new provider workspace is a different copy.
func (*Registry) CatalogEntryCluster ¶
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 ¶
Delete removes the platform-global record for name. Returns true if anything was removed.
func (*Registry) DeleteByCluster ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
This compatibility entry point is platform-scoped. The HTTP handler selects organization records by issuer and requires authenticated heartbeats.
func (*Registry) List ¶
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 ¶
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) ProviderWorkspaceCluster ¶ added in v0.2.43
ProviderWorkspaceCluster returns the platform provider's actual APIExport and ServiceAccount workspace. CatalogEntryCluster remains the recorder's address and may be a different legacy catalog workspace.
func (*Registry) RequirementsListeners ¶ added in v0.2.21
RequirementsListeners reports how many listeners are registered. For tests that pin the unregister contract.
func (*Registry) SetWorkspaceCluster ¶
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 ¶
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 ¶
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 RelayAuthorization ¶ added in v0.2.47
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"`
// LiteralString keeps operator-supplied references out of Helm's value grammar.
LiteralString bool `json:"-"`
}
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) InstallInputs ¶ added in v0.2.43
func (s *SelfHosting) InstallInputs() []SelfHostingValue
InstallInputs returns only settings the operator must supply. Fixed recipe values and hub-owned addressing/credential settings are never browser inputs.
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.
func (*SelfHosting) ValidateInstallInputs ¶ added in v0.2.43
func (s *SelfHosting) ValidateInstallInputs(values map[string]string) error
type SelfHostingValue ¶
SelfHostingValue is one Helm value the installer must set. A value whose Name ends in SecretRef.name declares a Secret prerequisite in the installation namespace. Declare the sibling SecretRef.key too when the chart consumes a particular data key; the installer checks that key without reading credentials into the recipe or assuming undeclared chart defaults.
type TenantResolver ¶
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 ¶
TenantResolverFunc adapts a plain function to the TenantResolver interface. Lets callers compose the lookup inline without declaring a type.
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.
Source Files
¶
- agent_ingress.go
- api.go
- bound_copy.go
- categories.go
- controller.go
- edgeroute.go
- heartbeat.go
- heartbeat_auth.go
- heartbeat_recorder.go
- org_bootstrap.go
- org_bootstrap_admission.go
- org_bootstrap_auth.go
- org_claim_policy.go
- org_group_ownership.go
- org_provider_route.go
- org_provision.go
- provider_controller.go
- provider_relay.go
- provider_relay_response.go
- provider_relay_transport.go
- provision.go
- proxy.go
- proxy_delegation.go
- proxy_edge.go
- proxy_ui_integrity.go
- registry.go
- response_headers.go
- selfhosting.go
- token_review_cluster.go
- ui_grant.go
- ui_integrity.go