ecluse
Safe HaskellNone
LanguageGHC2021

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 pure, IO-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, mirroring how Env assembly is kept pure of IO.

Global providers, per-mount reference

A CredentialProvider is the service's own cloud identity, built once from the environment layer (initCredentialProviders) and held process-global; a mount does not carry a provider, it only names which backend it draws on (its mtCredential). The boot-time check is the resolution of that reference: every distinct credential backend named across all mounts must resolve to an initialized provider, or the app halts at boot (see docs/architecture/cloud-backends.md → "Credential Provider"). Only the static backend has a leaf (from MIRROR_TARGET_TOKEN); a mount naming codeartifact or adc resolves to no provider and is an honest boot failure.

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 naming a credential backend with no initialized 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.mdValidation).

Synopsis

Global credential providers

data CredentialProviders Source #

The process-global credential providers, keyed by the backend they implement. Built once at the composition root from the environment layer; a mount references one by name and never holds its own.

The keyset (see initializedBackends) is the boot-check's pure surface — a mount that names a backend absent from it has an unresolved credential reference.

initCredentialProviders :: CredentialReporters -> EnvConfig -> IO (Either [BootError] CredentialProviders) Source #

Build the global credential providers from the environment layer, or the aggregated boot errors that block them. The mirror-target write provider is selected by cfgMirrorTargetCredentialProvider (see planMirrorCredential):

  • static — built from MIRROR_TARGET_TOKEN (cfgMirrorTargetToken) when set; absent, no static provider is initialized, so a mount naming static fails the boot-time credential-reference check.
  • codeartifact — the CodeArtifact inputs are resolved (resolveCodeArtifactConfig); a required input that resolves by neither an explicit key nor the mirror-target host is a fail-loud boot error. On success the generic refresh/cache wrapper around the CodeArtifact mint leaf (newCodeArtifactProvider) is built, which mints once eagerly — so a misconfigured identity fails here at boot. AWS credentials are the ambient container/task role (the standard chain), never an Écluse key. A mint that throws at boot (a transient AWS error, or a permanent one like a bad domain/region or missing permission) is caught and rendered as a CodeArtifactMintFailed boot error rather than escaping as a raw exception, so it joins the aggregated failure block.
  • gcp-artifact-registry — recognised but not built in this binary, so selecting it is a fail-loud boot error rather than a silent fall-through.

The static provider is also built whenever MIRROR_TARGET_TOKEN is present, independent of the selector, so a static token never goes unused.

The CredentialReporters are handed to the refreshing CodeArtifact provider so its mint breaker and refresh outcomes record to telemetry; the static provider never refreshes, so they do not concern it. The composition root supplies the deferred reporters that go live once the telemetry substrate exists.

initializedBackends :: CredentialProviders -> Set CredentialBackend Source #

The set of credential backends that resolved to an initialized provider — the pure surface the boot-time credential-reference check reasons over.

lookupProvider :: CredentialBackend -> CredentialProviders -> Maybe CredentialProvider Source #

Look up the initialized provider for a backend, Nothing when none is initialized (the unresolved-reference case the boot check rejects).

Mirror-target credential provider selection

planMirrorCredential :: EnvConfig -> Either [BootError] (Maybe CodeArtifactConfig) Source #

Decide what mirror-target write provider the environment layer selects, as the pure half of initCredentialProviders: Nothing when the static provider is selected (its leaf is the MIRROR_TARGET_TOKEN already handled there), Just a resolved CodeArtifactConfig when codeartifact is selected, or the aggregated boot errors that block the selection.

The gcp-artifact-registry arm is recognised but not built in this binary, so it is a fail-loud MirrorCredentialProviderUnavailable boot error — never a silent fall-through to a different provider, mirroring how planMirrorQueue treats the GCP queue arm.

resolveCodeArtifactConfig :: EnvConfig -> Either [BootError] CodeArtifactConfig Source #

Resolve the CodeArtifact inputs for the mirror-target token, or the aggregated boot errors naming each input that could not be resolved.

Each required input is resolved __(a) from its explicit MIRROR_TARGET_CODEARTIFACT_* key, else (b) by parsing the mirror-target URL host__ of the form {domain}-{owner}.d.codeartifact.{region}.amazonaws.com (the documented host fallback). The region resolves explicit key → host → AWS_REGION: the endpoint host encodes the domain's authoritative region, so it outranks the process-wide AWS_REGION (a cross-region deploy mints against the domain's region, not the caller's). The mirror-target URL is the resolved one — an unset MIRROR_TARGET_URL has already folded onto the private upstream — so a private-upstream CodeArtifact endpoint is parsed too. The optional token-duration carries through (cfgMirrorCodeArtifactTokenDuration).

The {owner} is a 12-digit AWS account id: a resolved owner (from either source) that is not 12 digits is a fail-loud CodeArtifactConfigInvalid error, and a host whose tail after the last hyphen is not an account id is not a CodeArtifact endpoint at all (so it falls through to the named-key check). If a required input resolves by neither source, that is a fail-loud CodeArtifactConfigMissing boot error naming the exact key the operator must set, aggregated so one run reports every problem.

Boot-time wiring

data BootError Source #

A reason the composition root refuses to start. Every case is a fail-loud boot failure; they are aggregated so a single run reports every problem an operator must fix.

Constructors

PolicyBootError PolicyError

A rule policy did not resolve (surfaced by loadConfig).

MissingAdapter Ecosystem

A configured mount's ecosystem has no adapter wired, so it cannot be served (a loud miss, never a silent drop). Carries the ecosystem.

UnresolvedCredential Ecosystem CredentialBackend

A mount names a credential backend with no initialized provider. Carries the ecosystem of the mount and the unresolved backend.

QueueProviderUnavailable QueueBackend

The configured mirror-queue backend has no implementation compiled into this binary, so no queue can be built for it. Carries the unavailable backend. An honest refusal — never a silent fall-through to a different backend.

QueueRegionMissing

The SQS mirror-queue backend was selected but no AWS region was supplied (AWS_REGION), so the queue cannot be scoped to a region.

QueueUrlMissing QueueBackend

A cloud mirror-queue backend (e.g. sqs) was selected but no MIRROR_QUEUE_URL was supplied, so there is no queue to send jobs to. The in-memory backend does not raise this — it has no external queue.

QueueEndpointMalformed Text

The configured SQS endpoint override (AWS_ENDPOINT_URL_SQS / AWS_ENDPOINT_URL) is not a parseable endpoint URL. Carries the offending value.

MirrorCredentialProviderUnavailable CredentialBackend

The selected mirror-target credential provider has no implementation compiled into this binary. Carries the unavailable provider. An honest refusal, never a silent fall-through.

CodeArtifactConfigMissing Text

A required CodeArtifact input for the mirror-target token could not be resolved from either its explicit key or the mirror-target host. Carries the name of the key the operator must set.

CodeArtifactConfigInvalid Text Text

A CodeArtifact input resolved but is malformed (e.g. a domain owner that is not a 12-digit AWS account id). Carries the key and a reason.

CodeArtifactMintFailed Text

The eager boot-time CodeArtifact mint threw — a transient AWS error (worth a retry) or a permanent one (a bad domain/region or missing permission, to be fixed). Carries the rendered exception so the cause is legible and aggregated.

Instances

Instances details
Show BootError Source # 
Instance details

Defined in Ecluse.Composition

Eq BootError Source # 
Instance details

Defined in Ecluse.Composition

renderBootError :: BootError -> Text Source #

Render a BootError as a human-facing line for the aggregated failure block.

planMounts :: (Ecosystem -> Maybe PackumentDeps -> Maybe MountBinding) -> IO UTCTime -> CredentialProviders -> EnvConfig -> Maybe ConfigDoc -> 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 and the wall-clock source are injected (the composition root supplies mountBindingFor and getCurrentTime), so this validation is pure of IO and unit-testable without a socket.

composeBindings :: (Ecosystem -> Maybe PackumentDeps -> Maybe MountBinding) -> IO UTCTime -> CredentialProviders -> Config -> 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 initialized provider, and its ecosystem must resolve to an adapter (through the injected resolver, fed real PackumentDeps so the packument route is served rather than the 501 stub). Errors aggregate across every mount.

Mirror-queue backend selection

data MirrorQueuePlan Source #

Which mirror-queue backend the composition root will build, resolved from config: the durable AWS sqs backend (with its SqsConfig), or the bounded best-effort in-memory backend (with its MemoryQueueConfig). The pure decision planMirrorQueue yields; the composition root pattern-matches it to make the one constructor call, and mirrorQueuePlanWarning tells it whether a boot warning is due.

Constructors

SqsBackend SqsConfig

The durable AWS SQS backend, built by Ecluse.Queue.Sqs.newSqsQueue.

MemoryBackend MemoryQueueConfig

The bounded in-memory backend, built by newBoundedInMemoryQueue. Non-durable and best-effort — boot warns.

planMirrorQueue :: EnvConfig -> Either [BootError] MirrorQueuePlan Source #

Select the mirror-queue backend from the environment layer, yielding the MirrorQueuePlan the composition root builds the queue from, or the aggregated boot errors that block it.

This is the pure half of the queue's backend choice — the single place that knows which backends this binary can build. The AWS sqs backend resolves to a SqsBackend carrying its SqsConfig (the queue URL and region, with the provider knobs at their defaults); the composition root passes that to Ecluse.Queue.Sqs.newSqsQueue. The memory backend resolves to a MemoryBackend carrying its depth cap, built in-process with no cloud queue (MIRROR_QUEUE_URL and AWS_REGION are not consulted for it) — an explicit operator choice for a simple, single-node, or air-gapped deployment, never an automatic fallback (which would soften the fail-loud-on-misconfig posture); the composition root emits the memoryQueueBootWarning on selection. The GCP pubsub arm is recognised but not built, so it is a fail-loud QueueProviderUnavailable boot error rather than a silent fall-through. MIRROR_QUEUE_URL is optional at the env layer; it is required here for sqs (the jobs need a queue), so a missing one is a fail-loud QueueUrlMissing boot error, and a missing AWS_REGION under sqs is a QueueRegionMissing boot error — the sqs arm aggregates the region, queue-URL, and endpoint failures, and the whole result is a list so it aggregates with the rest of the boot-time validation.

When an endpoint override is configured (AWS_ENDPOINT_URL_SQS, else AWS_ENDPOINT_URL — the AWS-SDK-standard variables), it is parsed into the backend's SqsEndpoint so the released image can target a local emulator (ministack) or a VPC endpoint without a test-only code path; a malformed override URL is a fail-loud QueueEndpointMalformed boot error. With no override, the SQS backend uses AWS's default endpoint and credential resolution.

mirrorQueuePlanWarning :: MirrorQueuePlan -> Maybe Text Source #

The loud boot warning a MirrorQueuePlan warrants before its queue is built, or Nothing for a durable backend that needs none. The composition root logs the Just at WarningS on selection, so an operator who chose the in-memory backend is told plainly that the mirror is non-durable — never a silent surprise.

memoryQueueBootWarning :: Text Source #

The boot warning emitted when the in-memory mirror-queue backend is selected: it states plainly that the mirror is in-memory, non-durable, and best-effort, and that a lost job is re-mirrored on the next demand (so there is no data loss, only deferred mirroring), so the choice is never mistaken for a durable cloud backend.

memoryQueueDropWarning :: Int -> Text Source #

The cap-overflow drop warning for the in-memory backend, carrying the running total of dropped jobs (this report is rate-limited at the queue, so it does not fire per dropped job). A note on a one-line follow-up: a drop metric (ecluse.mirror.*, S26 PR2) hooks in alongside this log once that catalogue lands.

Publish-side wiring

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 -> EnvConfig -> Maybe ConfigDoc -> 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.

Config-derived runtime settings

cacheConfigFor :: EnvConfig -> CacheConfig Source #

The metadata-cache tunables drawn from the validated environment layer — its TTL and entry bound — so a deployment's cache settings flow from config rather than the built-in defaults (see Ecluse.Server.Cache).