restapi

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 15, 2026 License: Apache-2.0 Imports: 27 Imported by: 0

Documentation

Overview

Package restapi implements roadmap step 10: the hub-mediated REST surface for Org / Workspace / Membership / User CRUD per docs/organizations.md decision O-10 ("Org workspaces are hub-mediated only"), plus the undelete actions wired to PR #212's soft-delete reconciler, the self-leave / role PATCH from O-12, and the ?cascade=true shortcut from O-9.

Endpoints mount at /api/* and run behind two middlewares from pkg/hub/tenant:

  • tenant.UserOnlyMiddleware on /api/users + /api/orgs (the org-list / org-create surface, plus any User self-service endpoint that doesn't claim an active Org). The handler only needs the caller's User identity.
  • tenant.Middleware on /api/orgs/{org}* and /api/orgs/{org}/workspaces/{ws}*. The header-bound TenantContext carries (User, OrgUUID, WorkspaceUUID, Role); the handler additionally enforces path/header consistency and any role requirement.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AcceptedClaim

type AcceptedClaim struct {
	Group    string `json:"group,omitempty"`
	Resource string `json:"resource"`
}

AcceptedClaim identifies one permission claim the user accepted in the confirmation dialog by its declared (group, resource) tuple. Verbs come from the provider's CatalogEntry — the user only chooses whether to grant them, not which verbs.

type AcceptedHubAccess

type AcceptedHubAccess struct {
	Capability string `json:"capability"`
	Scope      string `json:"scope"`
}

AcceptedHubAccess identifies one accepted hub capability by its declared (capability, scope). Limits come from the provider's declaration.

type CreateOrgRequest

type CreateOrgRequest struct {
	DisplayName          string `json:"displayName"`
	WorkspaceCreation    string `json:"workspaceCreation,omitempty"`    // "members" | "admin"; default "members"
	CatalogEntryCreation string `json:"catalogEntryCreation,omitempty"` // "members" | "admin"; default "admin"
}

CreateOrgRequest is the POST /api/orgs body.

type CreateWorkspaceRequest

type CreateWorkspaceRequest struct {
	DisplayName string `json:"displayName"`
}

CreateWorkspaceRequest is the POST body for creating a Workspace.

type EdgeInstallTargetView

type EdgeInstallTargetView struct {
	Workspace            string `json:"workspace"`
	WorkspaceDisplayName string `json:"workspaceDisplayName,omitempty"`
	Name                 string `json:"name"`
	// Eligible is the single field the UI should gate on. It is Connected
	// today, named separately so tightening the rule later (agent version
	// floor, say) does not require every caller to learn the new condition.
	Eligible     bool   `json:"eligible"`
	Connected    bool   `json:"connected"`
	Phase        string `json:"phase,omitempty"`
	AgentVersion string `json:"agentVersion,omitempty"`
}

EdgeInstallTargetView is one candidate cluster for a self-hosted provider.

type EdgeTargetRef

type EdgeTargetRef struct {
	// Workspace is the team workspace UUID the edge lives in.
	Workspace string `json:"workspace"`
	// Name is the KubernetesCluster's name.
	Name string `json:"name"`
}

EdgeTargetRef addresses one KubernetesCluster edge within the Org.

type EnableProviderRequest

type EnableProviderRequest struct {
	AcceptedClaims []AcceptedClaim `json:"acceptedClaims"`
	// AcceptedHubAccess lists the hub capabilities (CatalogEntry.spec.hubAccess)
	// the user accepted. Each must be declared by the provider. Accepting an
	// org-scoped capability requires an org admin; a workspace-scoped one, an
	// admin of this workspace or of the org. Omitted capabilities are not
	// granted, and the decision is recorded either way.
	AcceptedHubAccess []AcceptedHubAccess `json:"acceptedHubAccess,omitempty"`
}

EnableProviderRequest is the body of POST .../providers/{name}/enable. Mirrors the dialog state — for each declared permission claim, whether the user accepted it. Claims the user didn't tick are sent through to kcp as state=Rejected, which prevents the binding from going Bound and surfaces the mismatch to the user.

type EnableProviderResponse

type EnableProviderResponse struct {
	BindingName string `json:"bindingName"`
	// HubAccess is what was granted, when the provider declares any.
	HubAccess []AcceptedHubAccess `json:"hubAccess,omitempty"`
}

EnableProviderResponse is the success body. Mirrors what the portal would have learned from a direct kcp POST so the existing UI code can use it unchanged.

type EnabledProviderDetail

type EnabledProviderDetail struct {
	BindingName string `json:"bindingName"`
	// ExportPath is the workspace path of the bound APIExport.
	ExportPath string `json:"exportPath"`
	// SelfHosted is true when the binding targets the Org's own provider
	// instance rather than the platform's.
	SelfHosted bool `json:"selfHosted"`
	// StaleClaims lists claims this provider still pins to a different copy of
	// a dependency than this workspace binds — the state a provider lands in
	// when that dependency is swapped underneath it. kcp reports such a binding
	// as healthy while serving none of the claimed resources, so this is the
	// only place the condition is visible before a downstream 404.
	StaleClaims []StaleClaim `json:"staleClaims,omitempty"`
	// Terminating is true when the provider has been disabled but kcp is still
	// cascade-deleting the bound APIs' resources. The binding (and the
	// provider's API surface) remains live until that finishes, so the portal
	// must render this as "disabling", not as enabled or disabled.
	Terminating bool `json:"terminating,omitempty"`
	// HubAccess reports the provider's hub capabilities in this workspace:
	// which are in force and which it declares but nobody accepted yet.
	// Absent when the provider declares none.
	HubAccess *HubAccessState `json:"hubAccess,omitempty"`
	// DeletionBlocked is kcp's explanation of what is holding a terminating
	// binding open — e.g. "Some content in the workspace has finalizers
	// remaining: <finalizer> in 3 resource instances". A binding in this state
	// never finishes disabling on its own; whoever owns the named finalizer has
	// to act, and this message is the only place the user learns that.
	DeletionBlocked string `json:"deletionBlocked,omitempty"`
}

EnabledProviderDetail says not just THAT a provider is enabled here but WHICH instance of it is. An Org that self-hosts a provider the platform also ships has two exports competing for one name, and a workspace bound before the switch still points at the platform one. Reporting only the binding name would render that as plain "Enabled" and tell the user they are running their own instance when they are not.

type Handler

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

Handler is the HTTP surface. One handler instance registers all /api/* endpoints across the two middlewares.

func NewHandler

func NewHandler(mgr *Manager) *Handler

NewHandler constructs a Handler.

func (*Handler) RegisterTenantScoped

func (h *Handler) RegisterTenantScoped(r *mux.Router)

RegisterTenantScoped attaches the routes that require an active Org (and optionally Workspace) context. r is the subrouter wrapped in tenant.Middleware.

Effective routes:

GET    /api/orgs/{org}                                                  get one Org
PATCH  /api/orgs/{org}                                                  patch Org metadata
DELETE /api/orgs/{org}                                                  soft-delete Org
POST   /api/orgs/{org}/undelete                                         undelete Org

GET    /api/orgs/{org}/memberships                                      list org-scope members
POST   /api/orgs/{org}/memberships                                      add an org-scope member
DELETE /api/orgs/{org}/memberships/me                                   self-leave (O-12)
PATCH  /api/orgs/{org}/memberships/{user}                               role patch (O-12)
DELETE /api/orgs/{org}/memberships/{user}                               remove an org-scope member

GET    /api/orgs/{org}/workspaces                                       list workspaces in this Org
POST   /api/orgs/{org}/workspaces                                       create a workspace
GET    /api/orgs/{org}/workspaces/{ws}                                  get one Workspace
PATCH  /api/orgs/{org}/workspaces/{ws}                                  patch Workspace metadata
DELETE /api/orgs/{org}/workspaces/{ws}                                  soft-delete Workspace
POST   /api/orgs/{org}/workspaces/{ws}/undelete                         undelete Workspace

GET    /api/orgs/{org}/workspaces/{ws}/memberships                      list workspace-scope members
POST   /api/orgs/{org}/workspaces/{ws}/memberships                      add workspace-scope member
DELETE /api/orgs/{org}/workspaces/{ws}/memberships/me                   self-leave Workspace
PATCH  /api/orgs/{org}/workspaces/{ws}/memberships/{user}               role patch
DELETE /api/orgs/{org}/workspaces/{ws}/memberships/{user}               remove a member

GET    /api/orgs/{org}/workspaces/{ws}/kubeconfig                       download a workspace-scoped kubeconfig

func (*Handler) RegisterUserOnly

func (h *Handler) RegisterUserOnly(r *mux.Router)

RegisterUserOnly attaches the routes that only need the caller's User identity (no active Org / Workspace yet). r is the subrouter wrapped in tenant.UserOnlyMiddleware.

Effective routes:

GET    /api/orgs                       list orgs the caller is in
POST   /api/orgs                       create a new Org
DELETE /api/users/me                   soft-delete self (O-8)
POST   /api/users/me/undelete          undelete self (O-8)

type HubAccessState

type HubAccessState struct {
	// Granted are the capabilities in force (declared and accepted).
	Granted []AcceptedHubAccess `json:"granted,omitempty"`
	// Pending are declared capabilities nobody has accepted: new in the
	// provider's catalog entry, or declined. Re-enabling offers them again.
	Pending []AcceptedHubAccess `json:"pending,omitempty"`
	// Implicit is true when at least one granted capability is in force only
	// through the platform default (nobody entitled to decide it has yet).
	Implicit bool `json:"implicit,omitempty"`
}

HubAccessState is one provider's hub-access standing in a workspace.

type KubeconfigConfig

type KubeconfigConfig struct {
	HubExternalURL string
	DevMode        bool
	OIDCIssuerURL  string
	OIDCClientID   string
}

KubeconfigConfig configures the workspace-scoped kubeconfig download endpoint. When OIDCIssuerURL is set, the generator emits an exec credential plugin entry that runs `railgrid get-token` against the issuer — the same shape the OIDC login flow produces. When unset, the handler embeds the caller's bearer token directly (static-token mode).

HubExternalURL is required either way; the kubeconfig server URL is HubExternalURL + /clusters/<clusterName>. DevMode toggles insecure-skip-tls-verify so the file works against self-signed dev hubs without extra flags.

type ListAppAccessGrantsResponse

type ListAppAccessGrantsResponse struct {
	Items []kcp.AppAccessGrant `json:"items"`
}

ListAppAccessGrantsResponse is the body of GET .../app-access.

type ListEdgeInstallTargetsResponse

type ListEdgeInstallTargetsResponse struct {
	Items []EdgeInstallTargetView `json:"items"`
	// Eligible is true when at least one item is.
	Eligible bool `json:"eligible"`
	// Reason is human-readable and empty when Eligible.
	Reason string `json:"reason,omitempty"`
}

ListEdgeInstallTargetsResponse is the body of GET /api/orgs/{org}/providers/install-targets.

Reason exists so the portal can explain a disabled self-host button instead of just greying it out: "no edge at all" and "an edge whose agent never connected" need different next steps from the user.

type ListEnabledProvidersResponse

type ListEnabledProvidersResponse struct {
	BindingNamesByProvider map[string]string                `json:"bindingNamesByProvider"`
	BindingsByProvider     map[string]EnabledProviderDetail `json:"bindingsByProvider"`
}

ListEnabledProvidersResponse is the body of GET .../providers/enabled. Items are keyed by provider name (the binding's metadata.name matches the provider name by existing convention) so the portal can do "is provider X enabled" lookups in O(1) without indexing client-side.

BindingNamesByProvider is retained as-is for existing consumers; BindingsByProvider adds the export the binding actually points at.

type ListResponse

type ListResponse[T any] struct {
	Items []T `json:"items"`
}

ListResponse wraps a list payload so we can add pagination metadata later without breaking clients.

type Manager

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

Manager holds the dependencies every handler needs: the railgrid typed client (for Org / User / UMI CR access in root:railgrid:users) and the WorkspaceOps (kcp Bootstrapper in production; fake in tests).

func NewManager

func NewManager(client *railgridclient.Client, bootstrapper WorkspaceOps) *Manager

NewManager builds a Manager from the userClient (typed railgrid client against root:railgrid:users) and the WorkspaceOps. Production callers pass a kcp.Bootstrapper; tests pass a fake.

func (*Manager) WithHubAccessGrants

func (m *Manager) WithHubAccessGrants(store *hubaccess.Store, platformDefault bool) *Manager

WithHubAccessGrants installs the grant store the Enable flow records hub access in, and whether undecided platform providers get their declared capabilities (the gate's platform default).

func (*Manager) WithKubeconfig

func (m *Manager) WithKubeconfig(cfg KubeconfigConfig) *Manager

WithKubeconfig sets the kubeconfig-download configuration. Optional — when unset (the zero value) the download endpoint returns 503 so the route registration stays independent of OIDC wiring.

func (*Manager) WithOrgProviders

func (m *Manager) WithOrgProviders(ops OrgProviderOps, creds ProviderCredentialMinter) *Manager

WithOrgProviders installs the org-owned provider dependencies: the workspace lifecycle (the kcp Bootstrapper) and the credential minter (the providers Provisioner). Both are required together — without them the /api/orgs/{org}/providers endpoints return 501, keeping route registration independent of provider wiring for tests and minimal hubs.

func (*Manager) WithProviderRegistry

func (m *Manager) WithProviderRegistry(p ProviderLookup) *Manager

WithProviderRegistry installs the lookup used by the enableProvider handler. Wired from the hub's in-memory providers.Registry. When unset, POST .../providers/{name}/enable returns 501 — keeping the route registration independent of provider wiring for tests / minimal hubs.

type MembershipAddRequest

type MembershipAddRequest struct {
	User string `json:"user"`
	Role string `json:"role"` // admin | member
	// Invite pre-provisions a pending User when the identifier is an email
	// with no matching account. The pending User carries the email-derived
	// RBAC identity immediately (so memberships and app grants written now
	// apply on arrival) and is adopted by the first OIDC sign-in with that
	// email. Without this flag an unknown identifier stays a clean 404 so
	// typos cannot mint ghost users.
	Invite bool `json:"invite,omitempty"`
}

MembershipAddRequest is the POST body for adding a Membership.

type MembershipPatchRequest

type MembershipPatchRequest struct {
	Role string `json:"role"`
}

MembershipPatchRequest is the PATCH body for role changes (O-12).

type MembershipView

type MembershipView struct {
	User string `json:"user"`
	// RBACIdentity is the member's kcp username (User.Spec.RBACIdentity,
	// "railgrid:<email>") — the subject string every tenant-workspace RBAC
	// binding uses. Consumers writing RBAC (e.g. App Studio's app-access
	// grants) must bind this, never the User CR name.
	RBACIdentity string `json:"rbacIdentity,omitempty"`
	// Email / UserDisplayName label the member as a person — `user` is the
	// CR name (e.g. "static-user-47b9dce0e91570a1", "user-x2f9…"), which is
	// meaningless in a member list. Best-effort: pending invites may have
	// only an email; some accounts may have neither.
	Email                string `json:"email,omitempty"`
	UserDisplayName      string `json:"userDisplayName,omitempty"`
	Role                 string `json:"role"`
	OrgUUID              string `json:"orgUUID"`
	WorkspaceUUID        string `json:"workspaceUUID,omitempty"`
	OrgDisplayName       string `json:"orgDisplayName,omitempty"`
	WorkspaceDisplayName string `json:"workspaceDisplayName,omitempty"`
}

MembershipView is the REST projection of a single org-or-workspace scope membership.

type OrgProviderOps

type OrgProviderOps interface {
	EnsureOrgProviderWorkspace(ctx context.Context, orgUUID, name string) (string, error)
	ListOrgProviderWorkspaces(ctx context.Context, orgUUID string) ([]kcp.OrgProviderWorkspace, error)
	GetOrgProviderWorkspace(ctx context.Context, orgUUID, name string) (*kcp.OrgProviderWorkspace, error)
	DeleteOrgProviderWorkspace(ctx context.Context, orgUUID, name string) error
	ListEdgeInstallTargets(ctx context.Context, orgUUID string) ([]kcp.EdgeInstallTarget, error)
	GetEdgeInstallTarget(ctx context.Context, orgUUID, wsUUID, name string) (*kcp.EdgeInstallTarget, error)

	// RecordProviderEdgeBinding stamps the chosen edge onto the provider
	// Workspace at registration. The backend proxy reads it back to route this
	// provider's data plane over the tunnel, so it must live somewhere only the
	// hub writes — not on the tenant-authored CatalogEntry.
	RecordProviderEdgeBinding(ctx context.Context, orgUUID, providerName, wsUUID, edgeName string) error
}

OrgProviderOps is the slice of *kcp.Bootstrapper this surface needs to build and tear down an Org's provider workspaces. Separate from WorkspaceOps so tests can fake it without implementing the whole tenancy surface.

type OrgProviderView

type OrgProviderView struct {
	Name string `json:"name"`
	// WorkspacePath is where the Org installs its provider chart.
	WorkspacePath string `json:"workspacePath"`
	// ClusterName is the logical cluster ID of that workspace.
	ClusterName string `json:"clusterName,omitempty"`
	// Phase is the kcp workspace phase (e.g. "Ready").
	Phase string `json:"phase,omitempty"`
	// Registered is true once the provider's chart has run its `init` and
	// self-registered a CatalogEntry the hub can see. Until then the workspace
	// exists but the provider is not installable by any Workspace.
	Registered bool `json:"registered"`
	// DisplayName / Description / Version / APIExportName mirror the
	// CatalogEntry once Registered; empty before that.
	DisplayName   string `json:"displayName,omitempty"`
	Description   string `json:"description,omitempty"`
	Version       string `json:"version,omitempty"`
	APIExportName string `json:"apiExportName,omitempty"`
	Ready         bool   `json:"ready"`

	// InstalledVersion is the version the Org's copy is running, and
	// AvailableVersion is what the platform's copy of the same provider runs
	// now — the pair the upgrade verdict compared. Chart versions from the
	// selfHosting recipe when both sides carry one; otherwise the entries'
	// spec.version, because not every provider stamps a chart version — the
	// infrastructure operator registers from its embedded manifest, which has
	// no chart to read one from. AvailableVersion is empty when the provider
	// has no installable platform counterpart (an Org-authored provider).
	InstalledVersion string `json:"installedVersion,omitempty"`
	AvailableVersion string `json:"availableVersion,omitempty"`
	// UpgradeAvailable is true when the installed and available versions are
	// both known and differ. The hub owns the comparison so the portal cannot
	// drift from however "needs an upgrade" is defined later.
	UpgradeAvailable bool `json:"upgradeAvailable,omitempty"`
}

OrgProviderView is the REST projection of one org-owned provider. It merges two sources that can legitimately disagree: the kcp workspace (which the hub creates at registration) and the catalog registry (which only populates once the Org has actually installed the provider's chart). The gap between them is the "registered, not yet connected" state the UI needs to show.

type OrgView

type OrgView struct {
	UUID                 string     `json:"uuid"`
	DisplayName          string     `json:"displayName"`
	Personal             bool       `json:"personal"`
	WorkspaceCreation    string     `json:"workspaceCreation"`
	CatalogEntryCreation string     `json:"catalogEntryCreation"`
	WorkspaceQuota       int32      `json:"workspaceQuota,omitempty"`
	CreatedAt            time.Time  `json:"createdAt"`
	DeletionRequestedAt  *time.Time `json:"deletionRequestedAt,omitempty"`
	// Role is the CALLER's org-scope role ("admin" | "member") — what the
	// tenant middleware will resolve for org-scope requests. The portal
	// uses it to hide admin-only controls (member management, rename,
	// delete) instead of rendering buttons that can only 403.
	Role string `json:"role,omitempty"`
}

OrgView is the REST projection of an Organization.

type PatchOrgRequest

type PatchOrgRequest struct {
	DisplayName          string `json:"displayName,omitempty"`
	WorkspaceCreation    string `json:"workspaceCreation,omitempty"`
	CatalogEntryCreation string `json:"catalogEntryCreation,omitempty"`
	WorkspaceQuota       *int32 `json:"workspaceQuota,omitempty"`
}

PatchOrgRequest is the PATCH /api/orgs/{org} body. Empty fields are ignored.

type PatchWorkspaceRequest

type PatchWorkspaceRequest struct {
	DisplayName string `json:"displayName,omitempty"`
}

PatchWorkspaceRequest is the PATCH body. Currently only displayName is editable.

type ProviderCredentialMinter

type ProviderCredentialMinter interface {
	EnsureProviderSAAtPath(ctx context.Context, workspacePath string) error
	MintProviderKubeconfigAtPath(ctx context.Context, workspacePath, hubExternalURL string) ([]byte, error)
	// RotateProviderCredentialAtPath issues a NEW token for the same
	// ServiceAccount and retires the previous one after a grace period.
	RotateProviderCredentialAtPath(ctx context.Context, workspacePath, providerName, hubExternalURL string) (*providers.RotatedCredential, error)
}

ProviderCredentialMinter mints the workspace-scoped credential an Org installs its provider chart with. Implemented by *pkg/hub/providers.Provisioner.

type ProviderLookup

type ProviderLookup interface {
	Get(name string) (providers.Provider, bool)
	GetForOrg(orgUUID, name string) (providers.Provider, bool)
	ListForOrg(orgUUID string) []providers.Provider
}

ProviderLookup is the slice of pkg/hub/providers.Registry the enableProvider handler needs. Pulled out as an interface so tests can swap in a fake without depending on the in-memory registry package.

Tenant-scoped handlers must use GetForOrg, never Get: Get resolves only platform-global providers, so an Org's own provider would be invisible to Enable. Get remains for the surfaces with no tenant context.

type RegisterOrgProviderRequest

type RegisterOrgProviderRequest struct {
	// Name identifies the provider within the Org. It becomes the workspace
	// name, the CatalogEntry name the provider's chart must use, and the
	// binding name in every Workspace that enables it.
	//
	// When self-hosting a platform provider, use that provider's name: the
	// chart registers its CatalogEntry under its own name, so anything else
	// would leave the workspace and the registered provider mismatched.
	Name string `json:"name"`

	// SourceProvider names the platform provider being self-hosted, when this
	// registration is "run your own copy of X" rather than "register something
	// I wrote". It selects whose published deployment recipe the returned
	// instructions are rendered from. Defaults to Name.
	SourceProvider string `json:"sourceProvider,omitempty"`

	// Edge names the cluster the provider will be installed into. Required:
	// the hub reaches an org-owned provider's backend over that edge's tunnel
	// and has no other route into a tenant cluster
	// (docs/byo-provider-edge-transport.md E-2).
	Edge *EdgeTargetRef `json:"edge,omitempty"`
}

RegisterOrgProviderRequest is the body of POST /api/orgs/{org}/providers.

type RegisterOrgProviderResponse

type RegisterOrgProviderResponse struct {
	Provider OrgProviderView `json:"provider"`
	// Kubeconfig is the workspace-scoped credential, PEM-free YAML.
	Kubeconfig string `json:"kubeconfig"`
	// Instructions are the rendered install steps, present when the provider
	// being self-hosted publishes a deployment recipe. Nil for a provider the
	// Org wrote itself, which the Org already knows how to deploy.
	Instructions *providers.InstallInstructions `json:"instructions,omitempty"`
}

RegisterOrgProviderResponse carries the install credential back exactly once per call. The kubeconfig embeds a long-lived ServiceAccount token, so it is returned in the response body and never persisted anywhere the portal can re-read casually — callers that lose it re-fetch from the kubeconfig endpoint, which re-mints from the same Secret and therefore yields the same token.

type RotateOrgProviderCredentialResponse

type RotateOrgProviderCredentialResponse struct {
	Provider OrgProviderView `json:"provider"`
	// Kubeconfig is the NEW credential. Shown exactly once, like registration.
	Kubeconfig string `json:"kubeconfig"`
	// PreviousValidUntil is when the credential this call replaced stops
	// working (RFC3339); empty when there was nothing to retire. It is the
	// deadline for reinstalling the chart with the new kubeconfig: until then
	// both tokens belong to the same ServiceAccount and the provider keeps
	// working on either.
	PreviousValidUntil string `json:"previousValidUntil,omitempty"`
	RotatedAt          string `json:"rotatedAt"`
	// Instructions are re-rendered alongside the credential — whoever rotates
	// is about to run the install commands again.
	Instructions *providers.InstallInstructions `json:"instructions,omitempty"`
}

RotateOrgProviderCredentialResponse is the body of POST /api/orgs/{org}/providers/{name}/credentials/rotate.

type StaleClaim

type StaleClaim struct {
	// Group and Resource name the claimed resource, e.g.
	// "infrastructure.railgrid.ai" / "instances".
	Group    string `json:"group"`
	Resource string `json:"resource"`
	// BoundExportPath is the copy this workspace actually uses, and so the one
	// the provider would have to be repointed at.
	BoundExportPath string `json:"boundExportPath"`
	// ClaimedIdentity and BoundIdentity are the two hashes, truncated: enough
	// to tell them apart in a UI, and the full values are of no use to a reader
	// who cannot act on them anyway.
	ClaimedIdentity string `json:"claimedIdentity"`
	BoundIdentity   string `json:"boundIdentity"`
	// Repointable is true when the provider is self-hosted by this Org, in
	// which case re-enabling it repairs the pin. For a platform provider the
	// export is shared by every Org and re-enabling changes nothing, so the
	// portal must not offer that as the fix.
	Repointable bool `json:"repointable"`
}

StaleClaim describes one mispointed claim in terms the portal can render without knowing what an identityHash is.

type ValidationError

type ValidationError struct{ Msg string }

ValidationError is the sentinel for handler-side input validation failures. writeError translates it into 400. Use newValidationError to construct.

func (*ValidationError) Error

func (e *ValidationError) Error() string

type WorkspaceOps

type WorkspaceOps interface {
	// Org workspace lifecycle
	EnsureOrgWorkspace(ctx context.Context, orgUUID string) error

	// Org Membership CRUD (CRs in the Org workspace)
	EnsureOrgMembership(ctx context.Context, orgUUID, userName, role string) error
	ListOrgMemberships(ctx context.Context, orgUUID string) ([]string, error)
	GetOrgMembershipRole(ctx context.Context, orgUUID, userName string) (string, error)
	PatchOrgMembershipRole(ctx context.Context, orgUUID, userName, role string) error
	DeleteOrgMembership(ctx context.Context, orgUUID, userName string) error

	// Child Workspace lifecycle + projections
	EnsureChildWorkspace(ctx context.Context, orgUUID, wsUUID string) error
	EnsureChildWorkspaceRailgridBinding(ctx context.Context, orgUUID, wsUUID string) error
	EnsureChildWorkspaceDefaultMCPServer(ctx context.Context, orgUUID, wsUUID string) error
	// EnsureChildWorkspaceAdmin grants cluster-admin in the child team
	// Workspace to the given rbacIdentity. Idempotent. Used at workspace
	// create time and when adding a member, since the kcp-side CRB is
	// otherwise only seeded by the org bootstrap controller for the
	// user's default workspace — every other workspace would 403 from
	// the kcp proxy without this call.
	EnsureChildWorkspaceAdmin(ctx context.Context, orgUUID, wsUUID, rbacIdentity string) error
	// RevokeChildWorkspaceAdmin removes the rbacIdentity's cluster-admin
	// binding from the child team Workspace. Grants are per user, so this
	// never touches another member's access. Idempotent on NotFound.
	RevokeChildWorkspaceAdmin(ctx context.Context, orgUUID, wsUUID, rbacIdentity string) error
	// ListOrgMembershipRoles returns user name → role for every org-scope
	// Membership CR. Used to bind the Org's admins into a new child
	// workspace (O-15).
	ListOrgMembershipRoles(ctx context.Context, orgUUID string) (map[string]string, error)
	// ListChildTeamWorkspaces lists the Org's team workspaces, excluding
	// infrastructure children such as the `providers` container for org-owned
	// providers. Tenant-facing views must use this rather than the unfiltered
	// list: the container has no Memberships, so it would render as a workspace
	// row that 403s the moment a user selects it.
	ListChildTeamWorkspaces(ctx context.Context, orgUUID string) ([]string, error)
	GetWorkspaceDisplayName(ctx context.Context, orgUUID, wsUUID string) (string, error)
	SetWorkspaceDisplayName(ctx context.Context, orgUUID, wsUUID, displayName string) error
	GetWorkspaceDeletionRequestedAt(ctx context.Context, orgUUID, wsUUID string) (*time.Time, bool, error)
	SetWorkspaceDeletionAnnotation(ctx context.Context, orgUUID, wsUUID string, at time.Time) error
	ClearWorkspaceDeletionAnnotation(ctx context.Context, orgUUID, wsUUID string) error

	// GetChildWorkspaceClusterName returns the kcp logical-cluster name
	// (the /clusters/<name> hash) for a child workspace. Used to build
	// per-workspace kubeconfigs for the download endpoint.
	GetChildWorkspaceClusterName(ctx context.Context, orgUUID, wsUUID string) (string, error)

	// MCPServer CRUD in the tenant workspace. The in-core reconciler provisions
	// each server's identity; GetMCPServerToken follows status.tokenSecretRef
	// (empty string = not provisioned yet).
	ListMCPServers(ctx context.Context, clusterName string) ([]kcp.MCPServerInfo, error)
	CreateMCPServer(ctx context.Context, clusterName, name, displayName, instructions string, readOnly bool) error
	UpdateMCPServer(ctx context.Context, clusterName, name, displayName, instructions string, readOnly bool) error
	DeleteMCPServer(ctx context.Context, clusterName, name string) error
	GetMCPServerToken(ctx context.Context, clusterName, name string) (string, error)

	// EnsureProviderAPIBinding creates an APIBinding in the target
	// child workspace, owned by the hub's kcp-admin identity. Used by
	// the POST .../providers/{name}/enable handler — see
	// pkg/hub/kcp/bootstrap.go for the rationale (proxy's
	// defaultCluster pre-check would 403 any user attempt against a
	// non-default workspace, even with valid RBAC).
	EnsureProviderAPIBinding(ctx context.Context, orgUUID, wsUUID, bindingName, exportPath, exportName string, claims []kcp.ProviderClaim) error

	// ListProviderAPIBindings returns the set of provider APIBindings
	// (those referencing root:railgrid:providers:*) in the target
	// workspace, keyed by provider name. Used by the
	// GET .../providers/enabled handler so the portal can refresh the
	// "enabled providers" sidebar set on every workspace switch
	// without 403'ing through the kcp user-proxy.
	ListProviderAPIBindings(ctx context.Context, orgUUID, wsUUID string) (map[string]kcp.ProviderBinding, error)

	// StaleClaimIdentities reports, per provider bound in the workspace, any
	// permission claim still pinned to a different copy of a dependency than
	// the workspace binds — the state every dependent lands in when a provider
	// is swapped for a self-hosted one. kcp reports those bindings as healthy
	// while serving none of the claimed resources, so the portal warning fed
	// by this is the only sign before a downstream 404.
	StaleClaimIdentities(ctx context.Context, orgUUID, wsUUID string) (map[string][]kcp.ClaimIdentityMismatch, error)

	// DeleteProviderAPIBinding removes a provider APIBinding from the
	// target workspace. Used by the POST .../providers/{name}/disable
	// handler. NotFound is a no-op.
	DeleteProviderAPIBinding(ctx context.Context, orgUUID, wsUUID, bindingName string) error

	// EnsureProviderEdgeProxyGrant / RemoveProviderEdgeProxyGrant manage
	// the ClusterRole/ClusterRoleBinding pair that lets a provider's SA
	// (under its cluster-qualified identity) use the "proxy" verb on
	// edges in the tenant workspace. Applied on Enable when the provider
	// declares spec.edgeProxyAccess; removed on Disable.
	EnsureProviderEdgeProxyGrant(ctx context.Context, orgUUID, wsUUID, providerName, subject string) error
	RemoveProviderEdgeProxyGrant(ctx context.Context, orgUUID, wsUUID, providerName string) error

	// ListAppAccessGrants / RemoveAppAccessGrant surface the published-app
	// invitations (labeled ClusterRoleBindings, plain workspace RBAC) so
	// tenant settings can show and revoke them. See app_access.go.
	ListAppAccessGrants(ctx context.Context, orgUUID, wsUUID string) ([]kcp.AppAccessGrant, error)
	RemoveAppAccessGrant(ctx context.Context, orgUUID, wsUUID, bindingName string) error
}

WorkspaceOps is the slice of *kcp.Bootstrapper the REST handlers need to materialise / inspect / mutate workspaces, memberships, and soft-delete state. Pulled out as an interface so unit tests can use a fake without standing up embedded kcp.

Implemented by *pkg/hub/kcp.Bootstrapper.

type WorkspaceView

type WorkspaceView struct {
	UUID                string     `json:"uuid"`
	OrgUUID             string     `json:"orgUUID"`
	DisplayName         string     `json:"displayName,omitempty"`
	ClusterName         string     `json:"clusterName,omitempty"`
	DeletionRequestedAt *time.Time `json:"deletionRequestedAt,omitempty"`
	// Role is the CALLER's workspace-scope role ("admin" | "member"), or
	// empty when they hold no workspace-scope row here (possible for org
	// admins, who can list every workspace but manage only those they are
	// workspace-admin in — the middleware matches exact (org, ws) rows).
	// The portal gates workspace-admin controls on it.
	Role string `json:"role,omitempty"`
}

WorkspaceView is the REST projection of a child Workspace.

ClusterName is the kcp logical-cluster short hash backing this workspace (Workspace.spec.cluster). The portal uses it to address the kcp proxy at `/clusters/{clusterName}` for the active workspace; without it, a workspace switch in the UI cannot retarget per-workspace queries (MCP, edges, …) to the new cluster. May be empty when the workspace has not yet reached Ready and kcp has not assigned a cluster hash — callers should treat it as "not switchable yet" rather than an error.

Jump to

Keyboard shortcuts

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