| Safe Haskell | None |
|---|---|
| Language | GHC2021 |
Ecluse.Composition
Description
The composition-root wiring: turn a validated Config and the
process-global credential providers into the served MountBindings, failing fast
and aggregated on any boot problem.
This is the listener-free heart of the composition root (Ecluse calls it): it
holds no sockets, no network, and no real clock of its own -- the clock and the
ecosystem-to-adapter resolver are injected -- so the boot-time validation is
unit-tested without opening a listener. Its one effect is preparing each mount's rule
set (prepare), which allocates per-rule engine state once at boot
(a breaker for a resilient rule; the built-in rules need none today), so binding
assembly is IO; everything else stays a pure function of the validated config.
The composition root's other concerns live in the sibling modules: the boot-error vocabulary and rendering in Ecluse.Composition.BootError, the credential providers and mirror-target credential selection in Ecluse.Composition.Credential, the mirror-queue backend selection in Ecluse.Composition.MirrorQueue, and the config-derived runtime sizings in Ecluse.Composition.Sizing.
Fail-fast at boot
Three boot failures are aggregated into one report so a single run shows every
problem: a rule policy that does not resolve (PolicyBootError, surfaced by
loadConfig), a configured mount whose ecosystem has no adapter
wired (MissingAdapter), and a mount with no initialised mirror-write provider
(UnresolvedCredential). A bad configuration is thus a loud, immediate startup
failure, never a quietly mis-enforced or half-wired state (see
docs/architecture/configuration.md → Validation).
Synopsis
- planMounts :: (Ecosystem -> PackumentDeps -> Maybe PublishDeps -> Maybe MountBinding) -> IO UTCTime -> (Ecosystem -> RuleDeps) -> CredentialProviders -> Limits -> Maybe PublishBudget -> Config -> IO (Either [BootError] [MountBinding])
- composeBindings :: (Ecosystem -> PackumentDeps -> Maybe PublishDeps -> Maybe MountBinding) -> IO UTCTime -> (Ecosystem -> RuleDeps) -> CredentialProviders -> Limits -> Maybe PublishBudget -> Config -> IO (Either [BootError] [MountBinding])
- validateComposition :: Config -> [BootError]
- data PublishBudget = PublishBudget {
- pbBodyBudget :: ByteAdmission
- pbMaxRequestBytes :: Int
- data PublishTarget = PublishTarget {
- ptEcosystem :: Ecosystem
- ptMirrorUrl :: Text
- ptCredentials :: CredentialProvider
- planPublishTargets :: CredentialProviders -> Config -> Either [BootError] [PublishTarget]
Boot-time wiring
planMounts :: (Ecosystem -> PackumentDeps -> Maybe PublishDeps -> Maybe MountBinding) -> IO UTCTime -> (Ecosystem -> RuleDeps) -> CredentialProviders -> Limits -> Maybe PublishBudget -> Config -> IO (Either [BootError] [MountBinding]) Source #
Validate the environment layer and optional document into the served mount
bindings, or the aggregated boot errors. The composition root's single entry: it
runs loadConfig (whose policy errors become PolicyBootErrors) and then
composeBindings, so policy, missing-adapter, and unresolved-credential failures
all surface from one call.
The ecosystem-to-adapter resolver, the wall-clock source, and the rules' boot-bound
capabilities are injected (the composition root supplies mountBindingFor,
getCurrentTime, and each ecosystem's RuleDeps), so this validation
opens no socket. The capabilities are per ecosystem because a mount's rules must
borrow their ecosystem's advisory database, never a neighbour's.
It is IO only because composeBindings prepares each mount's rules (allocating
per-rule engine state once at boot).
composeBindings :: (Ecosystem -> PackumentDeps -> Maybe PublishDeps -> Maybe MountBinding) -> IO UTCTime -> (Ecosystem -> RuleDeps) -> CredentialProviders -> Limits -> Maybe PublishBudget -> Config -> IO (Either [BootError] [MountBinding]) Source #
Turn a validated Config into the served MountBindings, or the aggregated
boot errors. For each mount, in ecosystem order: its credential reference must
resolve to an initialised provider, and its ecosystem must resolve to an adapter
(through the injected resolver, which the mount's PackumentDeps are built from).
Errors aggregate across every mount. The Limits arrive resolved (the byte cap
from the memory plan, Ecluse.Composition.MemoryPlan, married to the pinned
structural counts) and are carried onto every mount's deps, so the data plane
reads each metadata body bounded (security.md invariant 4).
validateComposition :: Config -> [BootError] Source #
The pure structural validation a boot enforces beyond loadConfig,
shared with ecluse check-config so the checker can never pass a configuration the
proxy refuses: a served mount whose ecosystem has no registered adapter
(MissingAdapter), and the publish policy of every configured publication target
(PublishAllowMissing, PublishStaticCredentialNeedsEdge). Pure and
side-effect-free: no provider is initialised and no credential minted -- the
mirrored-mount credential expectations are already structural on Config itself
(each mirrored mount carries the credential loadConfig derived from
its target). Only the provider-initialisation check (UnresolvedCredential) stays
with composeBindings, which consumes this same function for everything else.
Publish-side wiring
data PublishBudget Source #
The publish-side byte discipline the composition root builds from the memory plan's publish tenant and hands to every publishing mount: the process-wide aggregate byte-admission and the per-request cap (the chunked-body weight). Present exactly when a publication target is configured -- the tenant and the target derive from the same predicate, so a publishing mount without a budget is unrepresentable at the root.
Constructors
| PublishBudget | |
Fields
| |
data PublishTarget Source #
One ecosystem's resolved publish target: the mirror-target endpoint the mirror worker writes approved artifacts to, paired with the credential provider that mints its bearer token.
This is the publish side of the per-ecosystem composition (the serve side is the
mount's PackumentDeps). The worker's single consumer builds a registry-protocol
client from these -- the endpoint as its base URL, the provider's token as its
bearer -- so the publish client is resolved here at the composition root rather than
re-derived per request.
Constructors
| PublishTarget | |
Fields
| |
planPublishTargets :: CredentialProviders -> Config -> Either [BootError] [PublishTarget] Source #
Resolve each configured mount to its publish target, or the aggregated boot
errors. The publish side of planMounts: it validates the same config and resolves
each mount's mirror-target endpoint and write credential, so the worker's publish
client can be built at the composition root.
An unresolved credential reference is the same fail-loud boot error composeBindings
reports for the serve side, so the two surfaces never disagree on what is wired.