mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 02:28:59 +00:00
Add monitored-system admission extension hook
This commit is contained in:
parent
3596acfeb2
commit
bd8b2efd1b
10 changed files with 233 additions and 7 deletions
|
|
@ -1175,6 +1175,13 @@ lifecycle-local counter, drift on source priority, or treat missing monitored-
|
|||
system usage as zero; when an active cap is present and usage cannot be
|
||||
resolved, the route must fail closed with retryable unavailable guidance
|
||||
instead of silently admitting a net-new monitored system.
|
||||
That same lifecycle boundary also assumes any private monitored-system
|
||||
admission policy hook stays backend-owned. Even when `pkg/server/server.go`
|
||||
wires `ResolveMonitoredSystemAdmissionPolicy` into shared `internal/api/`
|
||||
admission helpers, setup and install flows must continue to source candidate
|
||||
projection, replacement identity, and usage-availability truth from the
|
||||
canonical monitored-system preview/resolver path instead of adding a second
|
||||
lifecycle-local policy branch or exemption rule.
|
||||
When lifecycle-adjacent setup or support surfaces need to explain why a
|
||||
candidate would count or dedupe, they must consume the shared monitored-system
|
||||
ledger preview contract rather than rebuilding a second preview model from
|
||||
|
|
|
|||
|
|
@ -1470,6 +1470,17 @@ must expose `reason` as `limit_reached`, `preexisting_usage`, or
|
|||
`legacy_migration_capture_pending` so browser surfaces can distinguish a full
|
||||
plan boundary from a frozen above-plan carry-forward and from migrated legacy
|
||||
continuity that is still being verified.
|
||||
That same admission family also owns the private monitored-system policy hook
|
||||
boundary. `internal/api/enterprise_extension_monitored_system_admission.go`
|
||||
may register one private `ResolveMonitoredSystemAdmissionPolicy` hook through
|
||||
`pkg/extensions/monitored_system_admission.go`, but that hook must consume the
|
||||
canonical counted-system input already resolved by shared API admission
|
||||
helpers. Private builds may not use that extension point to invent
|
||||
provider-local counters, replacement semantics, or usage-availability fallbacks
|
||||
that diverge from the shared monitored-system resolver. `pkg/server/server.go`
|
||||
may wire the hook during startup, but public runtime still owns counted-system
|
||||
projection unless and until a later governed enforcement slice actually routes
|
||||
live admission through that private decision boundary.
|
||||
That same contract now also owns migrated legacy continuity. When a supported
|
||||
v5 license auto-exchanges or is activated manually in v6, `/api/license/status`
|
||||
and `/api/license/entitlements` must surface `max_monitored_systems` from the
|
||||
|
|
|
|||
|
|
@ -79,6 +79,13 @@ querying, and the operator-facing storage health presentation layer.
|
|||
current/projected grouped systems plus enforced limit verdicts for setup and
|
||||
support clarity, but storage and recovery must not reinterpret those routes
|
||||
as recovery-local onboarding or restore APIs.
|
||||
That same adjacent API boundary also owns any private monitored-system
|
||||
policy hook wired through `pkg/server/server.go`. Storage- or
|
||||
recovery-adjacent writes may observe the shared verdict returned by
|
||||
`ResolveMonitoredSystemAdmissionPolicy`, but they must not inject
|
||||
provider-specific policy, infer a different over-cap rule, or bypass the
|
||||
canonical monitored-system preview/resolver contract when the private hook
|
||||
is absent.
|
||||
Any adjacent list surfaces that reuse `internal/api/resources.go` must also
|
||||
preserve the canonical unified-resource `name -> type -> id` order so
|
||||
duplicate-name storage and recovery resources do not reshuffle between cold
|
||||
|
|
|
|||
|
|
@ -202,13 +202,14 @@ func TestPaidDomainBoundaryAudit(t *testing.T) {
|
|||
// Use an exact allowlist to prevent new paid implementations from
|
||||
// hiding behind the enterprise_extension_ prefix.
|
||||
knownBinderFiles := map[string]bool{
|
||||
"enterprise_extension_ai_alert_analysis.go": true,
|
||||
"enterprise_extension_ai_autofix.go": true,
|
||||
"enterprise_extension_ai_investigation.go": true,
|
||||
"enterprise_extension_audit_admin.go": true,
|
||||
"enterprise_extension_rbac_admin.go": true,
|
||||
"enterprise_extension_reporting_admin.go": true,
|
||||
"enterprise_extension_sso_admin.go": true,
|
||||
"enterprise_extension_ai_alert_analysis.go": true,
|
||||
"enterprise_extension_ai_autofix.go": true,
|
||||
"enterprise_extension_ai_investigation.go": true,
|
||||
"enterprise_extension_audit_admin.go": true,
|
||||
"enterprise_extension_monitored_system_admission.go": true,
|
||||
"enterprise_extension_rbac_admin.go": true,
|
||||
"enterprise_extension_reporting_admin.go": true,
|
||||
"enterprise_extension_sso_admin.go": true,
|
||||
}
|
||||
|
||||
// These reporting files are intentionally public shared surfaces rather than
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import (
|
|||
"github.com/rcourtman/pulse-go-rewrite/internal/vmware"
|
||||
authpkg "github.com/rcourtman/pulse-go-rewrite/pkg/auth"
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/cloudauth"
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/extensions"
|
||||
pkglicensing "github.com/rcourtman/pulse-go-rewrite/pkg/licensing"
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/metrics"
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/reporting"
|
||||
|
|
@ -5735,6 +5736,57 @@ func TestContract_MonitoredSystemUsageUnavailableIncludesReason(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestContract_ResolveMonitoredSystemAdmissionPolicyHookUsesCanonicalInput(t *testing.T) {
|
||||
SetResolveMonitoredSystemAdmissionPolicy(nil)
|
||||
t.Cleanup(func() { SetResolveMonitoredSystemAdmissionPolicy(nil) })
|
||||
|
||||
if hook := getResolveMonitoredSystemAdmissionPolicy(); hook != nil {
|
||||
t.Fatalf("expected no monitored-system admission hook by default, got %v", hook)
|
||||
}
|
||||
|
||||
SetResolveMonitoredSystemAdmissionPolicy(func(_ context.Context, input extensions.MonitoredSystemAdmissionInput) extensions.MonitoredSystemAdmissionDecision {
|
||||
return extensions.MonitoredSystemAdmissionDecision{
|
||||
Current: input.Current,
|
||||
Additional: input.Additional,
|
||||
Limit: input.Limit,
|
||||
UsageAvailable: input.UsageAvailable,
|
||||
UsageUnavailableReason: input.UsageUnavailableReason,
|
||||
Exceeded: input.CandidateCountsTowardCap &&
|
||||
input.UsageAvailable &&
|
||||
input.Additional > 0 &&
|
||||
input.Limit > 0 &&
|
||||
input.Current+input.Additional > input.Limit,
|
||||
}
|
||||
})
|
||||
|
||||
hook := getResolveMonitoredSystemAdmissionPolicy()
|
||||
if hook == nil {
|
||||
t.Fatal("expected monitored-system admission hook to round-trip through the shared API boundary")
|
||||
}
|
||||
|
||||
decision := hook(context.Background(), extensions.MonitoredSystemAdmissionInput{
|
||||
Current: 5,
|
||||
Additional: 1,
|
||||
Limit: 5,
|
||||
UsageAvailable: true,
|
||||
CandidateCountsTowardCap: true,
|
||||
})
|
||||
if !decision.Exceeded {
|
||||
t.Fatalf("expected canonical counted-system input to preserve the exceeded verdict, got %+v", decision)
|
||||
}
|
||||
|
||||
decision = hook(context.Background(), extensions.MonitoredSystemAdmissionInput{
|
||||
Current: 5,
|
||||
Additional: 1,
|
||||
Limit: 5,
|
||||
UsageAvailable: true,
|
||||
CandidateCountsTowardCap: false,
|
||||
})
|
||||
if decision.Exceeded {
|
||||
t.Fatalf("expected non-counted candidate to stay outside the exceeded verdict, got %+v", decision)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContract_PlatformConnectionWritesFailClosedWhenUsageUnavailable(t *testing.T) {
|
||||
t.Run("truenas add", func(t *testing.T) {
|
||||
setTrueNASFeatureForTest(t, true)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/extensions"
|
||||
)
|
||||
|
||||
var (
|
||||
monitoredSystemAdmissionPolicyMu sync.RWMutex
|
||||
resolveMonitoredSystemPolicyFunc extensions.ResolveMonitoredSystemAdmissionPolicyFunc
|
||||
)
|
||||
|
||||
// SetResolveMonitoredSystemAdmissionPolicy registers a private commercial
|
||||
// policy hook for monitored-system admission decisions.
|
||||
func SetResolveMonitoredSystemAdmissionPolicy(fn extensions.ResolveMonitoredSystemAdmissionPolicyFunc) {
|
||||
monitoredSystemAdmissionPolicyMu.Lock()
|
||||
defer monitoredSystemAdmissionPolicyMu.Unlock()
|
||||
resolveMonitoredSystemPolicyFunc = fn
|
||||
}
|
||||
|
||||
func getResolveMonitoredSystemAdmissionPolicy() extensions.ResolveMonitoredSystemAdmissionPolicyFunc {
|
||||
monitoredSystemAdmissionPolicyMu.RLock()
|
||||
defer monitoredSystemAdmissionPolicyMu.RUnlock()
|
||||
return resolveMonitoredSystemPolicyFunc
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/extensions"
|
||||
)
|
||||
|
||||
func TestGetResolveMonitoredSystemAdmissionPolicy_DefaultNil(t *testing.T) {
|
||||
SetResolveMonitoredSystemAdmissionPolicy(nil)
|
||||
t.Cleanup(func() { SetResolveMonitoredSystemAdmissionPolicy(nil) })
|
||||
|
||||
if getResolveMonitoredSystemAdmissionPolicy() != nil {
|
||||
t.Fatal("expected nil when no monitored-system admission policy hook is registered")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetResolveMonitoredSystemAdmissionPolicy_RoundTripsHook(t *testing.T) {
|
||||
SetResolveMonitoredSystemAdmissionPolicy(nil)
|
||||
t.Cleanup(func() { SetResolveMonitoredSystemAdmissionPolicy(nil) })
|
||||
|
||||
SetResolveMonitoredSystemAdmissionPolicy(func(_ context.Context, input extensions.MonitoredSystemAdmissionInput) extensions.MonitoredSystemAdmissionDecision {
|
||||
return extensions.MonitoredSystemAdmissionDecision{
|
||||
Current: input.Current,
|
||||
Additional: input.Additional,
|
||||
Limit: input.Limit,
|
||||
UsageAvailable: input.UsageAvailable,
|
||||
UsageUnavailableReason: input.UsageUnavailableReason,
|
||||
Exceeded: input.UsageAvailable && input.Additional > 0 && input.Limit > 0 && input.Current+input.Additional > input.Limit,
|
||||
}
|
||||
})
|
||||
|
||||
hook := getResolveMonitoredSystemAdmissionPolicy()
|
||||
if hook == nil {
|
||||
t.Fatal("expected monitored-system admission policy hook to be returned")
|
||||
}
|
||||
|
||||
decision := hook(context.Background(), extensions.MonitoredSystemAdmissionInput{
|
||||
Current: 5,
|
||||
Additional: 1,
|
||||
Limit: 5,
|
||||
UsageAvailable: true,
|
||||
UsageUnavailableReason: "",
|
||||
})
|
||||
if !decision.Exceeded {
|
||||
t.Fatalf("expected hook result to preserve exceeded state, got %+v", decision)
|
||||
}
|
||||
if decision.Current != 5 || decision.Additional != 1 || decision.Limit != 5 {
|
||||
t.Fatalf("expected hook result to preserve admission values, got %+v", decision)
|
||||
}
|
||||
}
|
||||
33
pkg/extensions/monitored_system_admission.go
Normal file
33
pkg/extensions/monitored_system_admission.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package extensions
|
||||
|
||||
import "context"
|
||||
|
||||
// MonitoredSystemAdmissionInput captures the public counted-system admission
|
||||
// state computed by the OSS runtime before any private commercial policy hook
|
||||
// is consulted.
|
||||
type MonitoredSystemAdmissionInput struct {
|
||||
Current int
|
||||
Additional int
|
||||
Limit int
|
||||
UsageAvailable bool
|
||||
UsageUnavailableReason string
|
||||
CandidateCountsTowardCap bool
|
||||
}
|
||||
|
||||
// MonitoredSystemAdmissionDecision captures the commercial admission outcome
|
||||
// that a private build may return. This intentionally stays scoped to backend
|
||||
// admission semantics rather than customer-facing billing/posture messaging.
|
||||
type MonitoredSystemAdmissionDecision struct {
|
||||
Current int
|
||||
Additional int
|
||||
Limit int
|
||||
UsageAvailable bool
|
||||
UsageUnavailableReason string
|
||||
Exceeded bool
|
||||
}
|
||||
|
||||
// ResolveMonitoredSystemAdmissionPolicyFunc allows private builds to own the
|
||||
// commercial monitored-system admission decision without importing internal API
|
||||
// packages. The public runtime remains the source of truth for counted-system
|
||||
// projection and may call this hook in a later migration slice.
|
||||
type ResolveMonitoredSystemAdmissionPolicyFunc func(context.Context, MonitoredSystemAdmissionInput) MonitoredSystemAdmissionDecision
|
||||
|
|
@ -96,6 +96,11 @@ type BusinessHooks struct {
|
|||
// CreateAlertAnalyzer creates the premium alert-triggered analyzer.
|
||||
// Returns nil in OSS. Enterprise provides a concrete implementation.
|
||||
CreateAlertAnalyzer func(deps aicontracts.AlertAnalyzerDeps) aicontracts.AlertAnalyzer
|
||||
|
||||
// ResolveMonitoredSystemAdmissionPolicy allows private builds to own the
|
||||
// commercial monitored-system admission decision without importing internal
|
||||
// API packages. Public runtime still owns counted-system projection.
|
||||
ResolveMonitoredSystemAdmissionPolicy extensions.ResolveMonitoredSystemAdmissionPolicyFunc
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
@ -299,6 +304,7 @@ func Run(ctx context.Context, version string) error {
|
|||
createInvestigationStore := globalHooks.CreateInvestigationStore
|
||||
createInvestigationOrchestrator := globalHooks.CreateInvestigationOrchestrator
|
||||
createAlertAnalyzer := globalHooks.CreateAlertAnalyzer
|
||||
resolveMonitoredSystemAdmissionPolicy := globalHooks.ResolveMonitoredSystemAdmissionPolicy
|
||||
globalHooksMu.Unlock()
|
||||
|
||||
api.SetAIInvestigationEnabled(aiInvestigationEnabled)
|
||||
|
|
@ -306,6 +312,7 @@ func Run(ctx context.Context, version string) error {
|
|||
api.SetCreateInvestigationStore(createInvestigationStore)
|
||||
api.SetCreateInvestigationOrchestrator(createInvestigationOrchestrator)
|
||||
api.SetCreateAlertAnalyzer(createAlertAnalyzer)
|
||||
api.SetResolveMonitoredSystemAdmissionPolicy(resolveMonitoredSystemAdmissionPolicy)
|
||||
api.SetRBACAdminEndpointsBinder(bindRBACAdminEndpoints)
|
||||
api.SetAuditAdminEndpointsBinder(bindAuditAdminEndpoints)
|
||||
api.SetSSOAdminEndpointsBinder(bindSSOAdminEndpoints)
|
||||
|
|
|
|||
|
|
@ -9,17 +9,30 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/extensions"
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/metrics"
|
||||
)
|
||||
|
||||
func TestBusinessHooks(t *testing.T) {
|
||||
called := false
|
||||
resolveCalled := false
|
||||
hook := func(store *metrics.Store) {
|
||||
called = true
|
||||
}
|
||||
|
||||
SetBusinessHooks(BusinessHooks{
|
||||
OnMetricsStoreReady: hook,
|
||||
ResolveMonitoredSystemAdmissionPolicy: func(_ context.Context, input extensions.MonitoredSystemAdmissionInput) extensions.MonitoredSystemAdmissionDecision {
|
||||
resolveCalled = true
|
||||
return extensions.MonitoredSystemAdmissionDecision{
|
||||
Current: input.Current,
|
||||
Additional: input.Additional,
|
||||
Limit: input.Limit,
|
||||
UsageAvailable: input.UsageAvailable,
|
||||
UsageUnavailableReason: input.UsageUnavailableReason,
|
||||
Exceeded: input.UsageAvailable && input.Additional > 0 && input.Limit > 0 && input.Current+input.Additional > input.Limit,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
globalHooksMu.Lock()
|
||||
|
|
@ -34,6 +47,23 @@ func TestBusinessHooks(t *testing.T) {
|
|||
if !called {
|
||||
t.Error("expected hook to be called")
|
||||
}
|
||||
|
||||
if globalHooks.ResolveMonitoredSystemAdmissionPolicy == nil {
|
||||
t.Error("expected ResolveMonitoredSystemAdmissionPolicy to be set")
|
||||
}
|
||||
|
||||
decision := globalHooks.ResolveMonitoredSystemAdmissionPolicy(context.Background(), extensions.MonitoredSystemAdmissionInput{
|
||||
Current: 5,
|
||||
Additional: 1,
|
||||
Limit: 5,
|
||||
UsageAvailable: true,
|
||||
})
|
||||
if !resolveCalled {
|
||||
t.Error("expected monitored-system admission policy hook to be called")
|
||||
}
|
||||
if !decision.Exceeded {
|
||||
t.Fatalf("expected exceeded decision from hook, got %+v", decision)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPerformAutoImport_Success(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue