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
- type AcceptedClaim
- type AcceptedComposition
- type AcceptedHubAccess
- type ClaimRef
- type ClaimState
- type CompositionState
- type ConstOrgQuota
- type CreateOrgRequest
- type CreateWorkspaceRequest
- type EdgeInstallTargetView
- type EdgeTargetRef
- type EnableProviderRequest
- type EnableProviderResponse
- type EnabledProviderDetail
- type Handler
- type HubAccessState
- type IdentityAttestor
- type IdentityHandler
- type IdentityService
- type KubeconfigConfig
- type ListAppAccessGrantsResponse
- type ListEdgeInstallTargetsResponse
- type ListEnabledProvidersResponse
- type ListResponse
- type Manager
- func (m *Manager) OrgQuota() OrgQuotaPolicy
- func (m *Manager) SoftDeleteGracePeriod() time.Duration
- func (m *Manager) WithHubAccessGrants(store *hubaccess.Store, platformDefault bool) *Manager
- func (m *Manager) WithKubeconfig(cfg KubeconfigConfig) *Manager
- func (m *Manager) WithOrgProviders(ops OrgProviderOps, creds ProviderCredentialMinter) *Manager
- func (m *Manager) WithOrgQuota(p OrgQuotaPolicy) *Manager
- func (m *Manager) WithProviderRegistry(p ProviderLookup) *Manager
- func (m *Manager) WithSoftDeleteGracePeriod(d time.Duration) *Manager
- type MembershipAddRequest
- type MembershipPatchRequest
- type MembershipView
- type OrgProviderOps
- type OrgProviderView
- type OrgQuotaPolicy
- type OrgView
- type PatchOrgRequest
- type PatchWorkspaceRequest
- type ProviderCredentialMinter
- type ProviderLookup
- type ReacceptClaimFailure
- type ReacceptClaimsResponse
- type ReacceptedClaim
- type RegisterOrgProviderRequest
- type RegisterOrgProviderResponse
- type RotateOrgProviderCredentialResponse
- type SelfView
- type UserSuggestion
- type ValidationError
- type WorkspaceOps
- type WorkspaceView
Constants ¶
const ( // PathIdentities is the collection: POST to create-or-refresh, GET to // list. PathIdentities = "/api/identities" // PathIdentity is one record by name: DELETE to revoke. PathIdentity = "/api/identities/{name}" )
The scoped-identity REST surface. It is NOT mounted under the tenant-scoped subrouter: its callers are providers holding their own service-account token, not humans holding an org membership, so it authenticates itself the way the heartbeat does — bearer → TokenReview in the provider's workspace → subject must be that workspace's provider service account. A caller is therefore always exactly one provider, and that provider is the only one whose identities it can create, list or delete.
const ( // UserSearchMinQuery is the shortest query answered, in characters. UserSearchMinQuery = 5 // UserSearchMaxResults caps the suggestions per query. UserSearchMaxResults = 5 )
User search lets any signed-in person discover other accounts' member IDs, so it is deliberately narrow: a query must be a real prefix, only a handful of suggestions come back, and each caller gets a small token bucket. With the portal's debounce and client-side narrowing, typing one address costs one or two requests; walking the user directory costs minutes per few names.
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 AcceptedComposition ¶ added in v0.2.7
type AcceptedComposition struct {
// Provider is the DEPENDENCY whose kind is composed (the owner of Group),
// not the provider being enabled.
Provider string `json:"provider"`
Group string `json:"group"`
Resource string `json:"resource"`
}
AcceptedComposition identifies one accepted composition by the dependency it hangs off and the kind it composes. Verbs are never sent: they come from the provider's declaration, read fresh every time the policy mints, so a caller cannot widen one by asking.
type AcceptedHubAccess ¶
AcceptedHubAccess identifies one accepted hub capability by its declared (capability, scope). Limits come from the provider's declaration.
type ClaimState ¶ added in v0.2.21
type ClaimState struct {
// Pending are claims the export declares that the binding does not list.
// Transient for a provider's own requirements (the reconciler accepts them
// moments after the export changes); lasting for a composition nobody has
// consented to, which re-enabling offers for acceptance.
Pending []ClaimRef `json:"pending,omitempty"`
// Rejected are claims the binding lists as Rejected. The hub never
// overturns these; re-enabling offers them again.
Rejected []ClaimRef `json:"rejected,omitempty"`
// Mismatch is kcp's explanation when an accepted claim differs from the
// export in verbs or selector — typically a selector the provider
// narrowed after this workspace enabled it. kcp makes an accepted claim's
// selector immutable, so only disabling and re-enabling applies the
// narrower scope.
Mismatch string `json:"mismatch,omitempty"`
}
ClaimState is one provider binding's permission-claim standing.
type CompositionState ¶ added in v0.2.7
type CompositionState struct {
// Granted are the compositions in force (declared and accepted).
Granted []AcceptedComposition `json:"granted,omitempty"`
// Pending are declared compositions nobody has accepted: new in the
// provider's catalog entry, or declined. Re-enabling offers them again.
Pending []AcceptedComposition `json:"pending,omitempty"`
// Implicit is true when at least one granted composition is in force only
// through the platform default (nobody entitled to decide it has yet).
Implicit bool `json:"implicit,omitempty"`
}
CompositionState is one provider's composition standing in a workspace.
type ConstOrgQuota ¶ added in v0.2.32
type ConstOrgQuota int32
ConstOrgQuota is an OrgQuotaPolicy with one cap for every user.
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.hub.access)
// 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"`
// AcceptedCompositions lists the compositions — the entries of
// CatalogEntry.spec.requires that NAME a provider — the user accepted: kinds
// of another provider this provider's reconcilers will create and manage in
// this workspace. Each must be declared, on the provider it names.
// Accepting is a workspace decision, so a workspace or org admin makes
// it. Omitted compositions are declined, and the decision is recorded
// either way — the same rules hub access follows, in the same Grant.
AcceptedCompositions []AcceptedComposition `json:"acceptedCompositions,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"`
// Compositions is what was granted of the provider's declared
// compositions, when it declares any.
Compositions []AcceptedComposition `json:"compositions,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"`
// 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"`
// Compositions reports the same for the kinds of other providers this
// provider manages here. A pending composition is why a provider that
// looks enabled cannot create what it is for: its reconciler's identity
// is refused the rule (composition_not_granted), and re-enabling offers
// the consent again. Absent when the provider declares none.
Compositions *CompositionState `json:"compositions,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"`
// Claims reports how the binding's accepted permission claims stand
// against what the provider's export declares today. The hub keeps a
// provider's own requirements accepted automatically, so what shows up
// here is what still needs a person: a composition awaiting consent, a
// claim the tenant rejected, or a scope kcp will not change in place.
// Absent when everything matches.
Claims *ClaimState `json:"claims,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 (*Handler) RegisterAdmin ¶ added in v0.2.7
RegisterAdmin attaches the platform-admin routes this package owns. r is the /api/admin subrouter, already gated by pkg/hub/admin's Middleware, so reaching a handler here already proves the caller is a platform admin — there is no per-tenant context and no role check to make.
Effective routes:
POST /api/admin/providers/{name}/claims/reaccept re-accept the provider's
claims on every existing
tenant APIBinding
It lives here rather than in pkg/hub/admin because the work is the fleet-wide twin of the per-workspace Enable flow next door in providers_enable.go: same registry lookup, same kcp.ProviderClaim list, same Bootstrapper.
func (*Handler) RegisterTenantScoped ¶
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 ¶
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 GET /api/users/me the caller's own identity GET /api/users/search?q= suggest users by email/name/member ID prefix (rate-limited) 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 IdentityAttestor ¶ added in v0.2.7
type IdentityAttestor interface {
Attest(ctx context.Context, r *http.Request, provider string) (string, error)
}
IdentityAttestor authenticates a provider-asserted identity request and returns the authenticated subject.
type IdentityHandler ¶ added in v0.2.7
type IdentityHandler struct {
// contains filtered or unexported fields
}
IdentityHandler serves /api/identities.
func NewIdentityHandler ¶ added in v0.2.7
func NewIdentityHandler(service IdentityService, attestor IdentityAttestor, log logr.Logger) *IdentityHandler
NewIdentityHandler constructs the handler. Missing dependencies fail closed at request time with 503 rather than minting anything unverified.
func (*IdentityHandler) Register ¶ added in v0.2.7
func (h *IdentityHandler) Register(router *mux.Router)
Register mounts the routes on router.
type IdentityService ¶ added in v0.2.7
type IdentityService interface {
Ensure(ctx context.Context, req identity.Request, mode tenancyv1alpha1.ScopedIdentityAttestationMode, subject string) (*identity.Token, error)
ReleaseByName(ctx context.Context, provider, name string) error
List(ctx context.Context, provider, clusterID string, owner *identity.Owner) ([]tenancyv1alpha1.ScopedIdentity, error)
}
IdentityService is the seam the handler drives.
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) OrgQuota ¶ added in v0.2.32
func (m *Manager) OrgQuota() OrgQuotaPolicy
OrgQuota returns the installed policy, or the hub default.
func (*Manager) SoftDeleteGracePeriod ¶ added in v0.2.31
SoftDeleteGracePeriod returns the configured recovery window, falling back to softdelete.DefaultGracePeriod.
func (*Manager) WithHubAccessGrants ¶
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) WithOrgQuota ¶ added in v0.2.32
func (m *Manager) WithOrgQuota(p OrgQuotaPolicy) *Manager
WithOrgQuota installs the organizations-per-user policy createOrg checks.
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.
func (*Manager) WithSoftDeleteGracePeriod ¶ added in v0.2.31
WithSoftDeleteGracePeriod sets the recovery window the undelete handlers enforce. Wire it to the same value the softdelete reconciler runs with. Non-positive keeps softdelete.DefaultGracePeriod.
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 OrgQuotaPolicy ¶ added in v0.2.32
OrgQuotaPolicy answers how many Organizations a User may create. The metering-backed implementation reads the `organizations` limit from the user's Entitlement; the fallback is a constant.
type OrgView ¶
type OrgView struct {
// InitialWorkspacePending distinguishes initial provisioning from an empty org.
InitialWorkspacePending bool `json:"initialWorkspacePending,omitempty"`
UUID string `json:"uuid"`
DisplayName string `json:"displayName"`
Personal bool `json:"personal"`
WorkspaceCreation string `json:"workspaceCreation"`
CatalogEntryCreation string `json:"catalogEntryCreation"`
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"`
}
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 ReacceptClaimFailure ¶ added in v0.2.7
type ReacceptClaimFailure struct {
Org string `json:"org"`
Workspace string `json:"workspace"`
Binding string `json:"binding"`
Error string `json:"error"`
}
ReacceptClaimFailure names the binding that could not be migrated and why. The (org, workspace, binding) triple is enough to find the object by hand.
type ReacceptClaimsResponse ¶ added in v0.2.7
type ReacceptClaimsResponse struct {
Provider string `json:"provider"`
// Claims echoes what was applied, so the operator can see in the response
// whether the hub is serving the CatalogEntry version they think it is
// before reading the counts below.
Claims []ReacceptedClaim `json:"claims"`
Updated int `json:"updated"`
Unchanged int `json:"unchanged"`
Failed []ReacceptClaimFailure `json:"failed"`
}
ReacceptClaimsResponse is the body of POST /api/admin/providers/{name}/claims/reaccept.
Updated / Unchanged / Failed are counted per APIBinding, and a binding that fails does not abort the run: one tenant workspace that is unreachable, or whose binding is mid-deletion, must not hide the rest of the fleet from the operator driving the rollout. Re-running the endpoint is the retry, and a second run over an already-migrated fleet reports everything Unchanged.
type ReacceptedClaim ¶ added in v0.2.7
type ReacceptedClaim struct {
Group string `json:"group,omitempty"`
Resource string `json:"resource"`
Verbs []string `json:"verbs"`
}
ReacceptedClaim is one claim the migration applied, as the CatalogEntry declares it.
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 SelfView ¶ added in v0.2.1
type SelfView struct {
// User is the User CR name.
User string `json:"user"`
// Email is empty for static-token users, which have no mailbox.
Email string `json:"email,omitempty"`
DisplayName string `json:"displayName,omitempty"`
// RBACIdentity is the kcp username the caller authenticates as. It is
// also what another admin types to add the caller to an Organization
// or Workspace when there is no email to share.
RBACIdentity string `json:"rbacIdentity"`
}
SelfView is the caller's own identity, as GET /api/users/me returns it.
type UserSuggestion ¶ added in v0.2.1
type UserSuggestion struct {
// User is the User CR name; the membership endpoints accept it.
User string `json:"user"`
// MemberID is what to put in the add-member box: the email, or the
// RBAC identity for accounts without one (static tokens).
MemberID string `json:"memberId"`
Email string `json:"email,omitempty"`
DisplayName string `json:"displayName,omitempty"`
}
UserSuggestion is one GET /api/users/search result: enough to recognise the person and add them, nothing more.
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, expectedUID 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)
// 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
// ListProviderAPIBindingsForExport walks the whole tenant fleet and
// returns every APIBinding of one APIExport, wherever it lives, and
// ReacceptProviderAPIBindingClaims rewrites one such binding's claim set
// to what the provider declares today. Together they are the claims
// migration AGENTS.md §5.1 requires: `init` updates the provider-side
// APIExport, and nothing else ever revisits the per-tenant bindings that
// decide what the provider may actually touch. See
// admin_provider_claims.go.
ListProviderAPIBindingsForExport(ctx context.Context, exportPath, exportName string) ([]kcp.ProviderBindingRef, error)
ReacceptProviderAPIBindingClaims(ctx context.Context, ref kcp.ProviderBindingRef, exportPath, exportName string, claims []kcp.ProviderClaim) (bool, 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.