Pulse/internal/unifiedresources/platform_admission.go
rcourtman eb279d089b Report platform admission on the canonical resource aggregations
The app shell decides which primary platform pages exist by classifying
every resource in the legacy full-state payload, which is why it has to
download that payload before it can render navigation. This publishes the
same answer as a `platformAdmission` facet on the canonical resource
aggregations, so admission has one definition instead of two that can
drift.

Counts cannot answer it, which is the whole reason this is a facet rather
than a client-side tally over `bySource`. A TrueNAS or Proxmox host
reports through the agent source and carries the "agent" platform scope,
so a count-based derivation admits the standalone page for an estate that
has no Pulse agent in it at all. Ownership is per-resource evidence, so it
is evaluated per resource here.

Verified against the live client classifier over real estates rather than
by inspection: the facet and the classifier agree on all six pages across
ten estates each for a 652-resource and a 216-resource estate, including
the cases that break a count-based derivation (Proxmox-only, TrueNAS-only,
vSphere-only and provider-owned-agents-only estates all correctly withhold
the standalone page, while a genuine Pulse agent admits it).

The tenant-fallback contract pin gains the new field. Its invariant, that
an empty tenant returns no resources rather than seeding from the raw
snapshot, is unchanged and still pinned; an empty estate admits nothing.

No caller reads the facet yet. Moving the shell onto it is a separate
change, gated by the same parity comparison.

Contracts: unified-resources and api-contracts gain the facet and its
derivation rule; agent-lifecycle and storage-recovery gain the ownership
clause, since an agent-typed host owned by a provider must not admit the
standalone page.
2026-08-18 07:35:34 +01:00

117 lines
3.9 KiB
Go

package unifiedresources
// Platform admission answers one question for the app shell: which primary
// platform pages exist for this estate. The frontend used to derive it by
// classifying every resource in the legacy full-state payload, which is why the
// shell had to download that payload before it could render its navigation.
// Deriving it here keeps a single definition of admission and lets the shell
// read it from the canonical resource aggregations instead.
//
// Counts cannot answer it. A TrueNAS or Proxmox host reports through the agent
// source and carries the "agent" platform scope, but it is owned by its
// provider and must not admit the standalone page on its own; only a genuine
// Pulse agent does. That distinction is per-resource evidence, so it is
// evaluated per resource here rather than inferred from a source tally.
// PlatformAdmission reports which primary platform pages an estate admits.
type PlatformAdmission struct {
Proxmox bool `json:"proxmox"`
Docker bool `json:"docker"`
Kubernetes bool `json:"kubernetes"`
TrueNAS bool `json:"truenas"`
VMware bool `json:"vmware"`
Standalone bool `json:"standalone"`
}
// agentProviderOwnerPlatformScopes are the platform scopes that mean an
// agent-typed resource belongs to a provider's platform page rather than the
// standalone one.
var agentProviderOwnerPlatformScopes = map[string]struct{}{
"proxmox-pve": {},
"proxmox-pbs": {},
"proxmox-pmg": {},
"kubernetes": {},
"truenas": {},
"vmware-vsphere": {},
}
// platformScopesForResource returns the canonical platform scopes for a
// resource, deriving them when the resource has not been refreshed yet so that
// admission never depends on refresh ordering.
func platformScopesForResource(resource Resource) []string {
if len(resource.PlatformScopes) > 0 {
return resource.PlatformScopes
}
clone := resource
RefreshPlatformScopes(&clone)
return clone.PlatformScopes
}
func hasProviderOwnerPlatformEvidence(resource Resource, scopes []string) bool {
for _, scope := range scopes {
if _, ok := agentProviderOwnerPlatformScopes[scope]; ok {
return true
}
}
for _, source := range resource.Sources {
if _, ok := agentProviderOwnerPlatformScopes[platformScopeForSource(source)]; ok {
return true
}
}
for source := range resource.SourceStatus {
if _, ok := agentProviderOwnerPlatformScopes[platformScopeForSource(source)]; ok {
return true
}
}
return false
}
func hasPulseAgentSourceEvidence(resource Resource) bool {
if hasDataSource(resource.Sources, SourceAgent) {
return true
}
_, ok := resource.SourceStatus[SourceAgent]
return ok
}
// IsPulseAgentPlatformResource reports whether a resource is a Pulse-managed
// host in its own right, rather than a host surfaced through the provider that
// owns it. Only these admit the standalone platform page.
func IsPulseAgentPlatformResource(resource Resource) bool {
if CanonicalResourceType(resource.Type) != ResourceTypeAgent {
return false
}
if hasProviderOwnerPlatformEvidence(resource, platformScopesForResource(resource)) {
return false
}
return hasPulseAgentSourceEvidence(resource)
}
// BuildPlatformAdmission reports which primary platform pages the given
// resources admit.
func BuildPlatformAdmission(resources []Resource) PlatformAdmission {
admission := PlatformAdmission{}
for _, resource := range resources {
for _, scope := range platformScopesForResource(resource) {
switch scope {
case "proxmox-pve", "proxmox-pbs", "proxmox-pmg":
admission.Proxmox = true
case "docker":
admission.Docker = true
case "kubernetes":
admission.Kubernetes = true
case "truenas":
admission.TrueNAS = true
case "vmware-vsphere":
admission.VMware = true
case "availability":
// Availability endpoints are operated from the standalone page.
admission.Standalone = true
}
}
if !admission.Standalone && IsPulseAgentPlatformResource(resource) {
admission.Standalone = true
}
}
return admission
}