module Ecluse.Composition (
ResolveAdapter,
WiringPorts (..),
BootWiring (..),
resolveBootWiring,
planMounts,
firstPartyName,
PublishBudget (..),
PublishTarget (..),
planPublishTargets,
) where
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Data.Text qualified as T
import Data.Time (UTCTime)
import Validation (eitherToValidation, validationToEither)
import Ecluse.Composition.BootError (BootError (..))
import Ecluse.Composition.Credential (
BuildCredentials,
CredentialProviders,
initCredentialProviders,
initializedEcosystems,
lookupProvider,
noCredentialProviders,
)
import Ecluse.Composition.Endpoints (publicationTargetUrl)
import Ecluse.Composition.MirrorRole (MirrorMintPlan (MintMirrorWrite, SkipMirrorWrite))
import Ecluse.Composition.Validate (
ValidatedPlan (vpMounts, vpPublications, vpSettings),
VettedMount (vmAdapter, vmConfig, vmEcosystem, vmMount),
VettedPublication (vpubFirstParty, vpubStaticToken, vpubTarget),
)
import Ecluse.Config (
AppConfig (..),
EgressSettings (..),
FirstParty (..),
IntegritySettings (..),
MirrorTarget (mtUrl),
Mount (..),
MountConfig (..),
MountIntegrity (..),
MountRegistries (..),
ServerSettings (..),
StoreTag,
Url,
regMirrorTarget,
regPrivateUpstream,
unUrl,
)
import Ecluse.Core.Credential (CredentialProvider)
import Ecluse.Core.Credential.Refresh (CredentialReporters)
import Ecluse.Core.Ecosystem (Ecosystem, prefixFor)
import Ecluse.Core.Package (PackageName)
import Ecluse.Core.Registry.Adapter (
RegistryAdapter,
adapterArtifact,
adapterMetadata,
adapterProjectName,
adapterPublish,
)
import Ecluse.Core.Registry.Adapter.Capability (AdapterArtifact (artifactHosts))
import Ecluse.Core.Registry.Npm.Publish (npmPublishAllowed)
import Ecluse.Core.Registry.PyPI.FirstParty (pypiFirstPartyName)
import Ecluse.Core.Rules (RuleDeps, prepare, rdCurrentAdvisoryEtag, rdSourceReporter)
import Ecluse.Core.Rules.Outage (SourceReporter (noteAdmission))
import Ecluse.Core.Security (Limits, maxPublishRequestBytes)
import Ecluse.Core.Security.Egress (RegistryUrl, mkRegistryUrl)
import Ecluse.Core.Server.Admission.Bytes (ByteAdmission)
import Ecluse.Core.Server.Context (MountBinding, PackumentDeps (..), PublishDeps (..))
import Ecluse.Core.Server.Response (HelpMessage, mkHelpMessage)
import Ecluse.Core.Server.Upstream (MirrorServePlan (MirrorOnAdmit, NoMirrorWrite), mountUpstreams)
import Ecluse.Core.Text (stripTrailingSlash)
type ResolveAdapter = Ecosystem -> PackumentDeps -> Maybe PublishDeps -> Maybe MountBinding
data WiringPorts = WiringPorts
{ WiringPorts -> Ecosystem -> StoreTag -> CredentialReporters
wpReporters :: Ecosystem -> StoreTag -> CredentialReporters
, WiringPorts -> BuildCredentials
wpBuildCredentials :: BuildCredentials
, WiringPorts -> ResolveAdapter
wpResolveAdapter :: ResolveAdapter
, WiringPorts -> IO UTCTime
wpClock :: IO UTCTime
, WiringPorts -> Ecosystem -> RuleDeps
wpRuleDeps :: Ecosystem -> RuleDeps
}
data BootWiring = BootWiring
{ BootWiring -> [MountBinding]
bwBindings :: [MountBinding]
, BootWiring -> [PublishTarget]
bwPublishTargets :: [PublishTarget]
}
resolveBootWiring :: WiringPorts -> MirrorMintPlan -> Limits -> Maybe PublishBudget -> ValidatedPlan -> IO (Either [BootError] BootWiring)
resolveBootWiring :: WiringPorts
-> MirrorMintPlan
-> Limits
-> Maybe PublishBudget
-> ValidatedPlan
-> IO (Either [BootError] BootWiring)
resolveBootWiring WiringPorts
ports MirrorMintPlan
mintPlan Limits
limits Maybe PublishBudget
publishBudget ValidatedPlan
plan = do
providersE <- IO (Either [BootError] CredentialProviders)
mintedProviders
case providersE of
Left [BootError]
errs -> Either [BootError] BootWiring -> IO (Either [BootError] BootWiring)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ([BootError] -> Either [BootError] BootWiring
forall a b. a -> Either a b
Left [BootError]
errs)
Right CredentialProviders
providers -> do
bindingsE <- ResolveAdapter
-> IO UTCTime
-> (Ecosystem -> RuleDeps)
-> MirrorMintPlan
-> CredentialProviders
-> Limits
-> Maybe PublishBudget
-> ValidatedPlan
-> IO (Either [BootError] [MountBinding])
planMounts (WiringPorts -> ResolveAdapter
wpResolveAdapter WiringPorts
ports) (WiringPorts -> IO UTCTime
wpClock WiringPorts
ports) (WiringPorts -> Ecosystem -> RuleDeps
wpRuleDeps WiringPorts
ports) MirrorMintPlan
mintPlan CredentialProviders
providers Limits
limits Maybe PublishBudget
publishBudget ValidatedPlan
plan
pure . validationToEither $
BootWiring
<$> eitherToValidation bindingsE
<*> eitherToValidation (planPublishTargets mintPlan providers plan)
where
mintedProviders :: IO (Either [BootError] CredentialProviders)
mintedProviders :: IO (Either [BootError] CredentialProviders)
mintedProviders = case MirrorMintPlan
mintPlan of
MirrorMintPlan
SkipMirrorWrite -> Either [BootError] CredentialProviders
-> IO (Either [BootError] CredentialProviders)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (CredentialProviders -> Either [BootError] CredentialProviders
forall a b. b -> Either a b
Right CredentialProviders
noCredentialProviders)
MirrorMintPlan
MintMirrorWrite -> BuildCredentials
-> (Ecosystem -> StoreTag -> CredentialReporters)
-> [Mount]
-> IO (Either [BootError] CredentialProviders)
initCredentialProviders (WiringPorts -> BuildCredentials
wpBuildCredentials WiringPorts
ports) (WiringPorts -> Ecosystem -> StoreTag -> CredentialReporters
wpReporters WiringPorts
ports) ((VettedMount -> Mount) -> [VettedMount] -> [Mount]
forall a b. (a -> b) -> [a] -> [b]
map VettedMount -> Mount
vmMount (ValidatedPlan -> [VettedMount]
vpMounts ValidatedPlan
plan))
data PublishBudget = PublishBudget
{ PublishBudget -> ByteAdmission
pbBodyBudget :: ByteAdmission
, PublishBudget -> Int
pbMaxRequestBytes :: Int
}
planMounts ::
ResolveAdapter ->
IO UTCTime ->
(Ecosystem -> RuleDeps) ->
MirrorMintPlan ->
CredentialProviders ->
Limits ->
Maybe PublishBudget ->
ValidatedPlan ->
IO (Either [BootError] [MountBinding])
planMounts :: ResolveAdapter
-> IO UTCTime
-> (Ecosystem -> RuleDeps)
-> MirrorMintPlan
-> CredentialProviders
-> Limits
-> Maybe PublishBudget
-> ValidatedPlan
-> IO (Either [BootError] [MountBinding])
planMounts ResolveAdapter
resolveAdapter IO UTCTime
clock Ecosystem -> RuleDeps
ruleDepsFor MirrorMintPlan
mintPlan CredentialProviders
providers Limits
limits Maybe PublishBudget
publishBudget ValidatedPlan
plan = do
bindingResults <- (VettedMount -> IO (Either [BootError] MountBinding))
-> [VettedMount] -> IO [Either [BootError] MountBinding]
forall (t :: * -> *) (f :: * -> *) a b.
(Traversable t, Applicative f) =>
(a -> f b) -> t a -> f (t b)
forall (f :: * -> *) a b.
Applicative f =>
(a -> f b) -> [a] -> f [b]
traverse VettedMount -> IO (Either [BootError] MountBinding)
bindingFor (ValidatedPlan -> [VettedMount]
vpMounts ValidatedPlan
plan)
pure $ case partitionEithers bindingResults of
([], [MountBinding]
bindings) -> [MountBinding] -> Either [BootError] [MountBinding]
forall a b. b -> Either a b
Right [MountBinding]
bindings
([[BootError]]
errs, [MountBinding]
_) -> [BootError] -> Either [BootError] [MountBinding]
forall a b. a -> Either a b
Left ([[BootError]] -> [BootError]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [[BootError]]
errs)
where
app :: AppConfig
app :: AppConfig
app = ValidatedPlan -> AppConfig
vpSettings ValidatedPlan
plan
ctx :: WiringContext
ctx :: WiringContext
ctx =
WiringContext
{ wcApp :: AppConfig
wcApp = AppConfig
app
, wcLimits :: Limits
wcLimits = Limits
limits
, wcClock :: IO UTCTime
wcClock = IO UTCTime
clock
, wcRuleDeps :: Ecosystem -> RuleDeps
wcRuleDeps = Ecosystem -> RuleDeps
ruleDepsFor
, wcPublishBudget :: Maybe PublishBudget
wcPublishBudget = Maybe PublishBudget
publishBudget
,
wcHelp :: Maybe HelpMessage
wcHelp = Text -> HelpMessage
mkHelpMessage (Text -> HelpMessage) -> Maybe Text -> Maybe HelpMessage
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ServerSettings -> Maybe Text
srvHelpMessage (AppConfig -> ServerSettings
cfgServer AppConfig
app)
}
bindingFor :: VettedMount -> IO (Either [BootError] MountBinding)
bindingFor :: VettedMount -> IO (Either [BootError] MountBinding)
bindingFor VettedMount
vetted = do
deps <- WiringContext
-> RegistryAdapter -> Mount -> MountConfig -> IO PackumentDeps
packumentDepsFor WiringContext
ctx (VettedMount -> RegistryAdapter
vmAdapter VettedMount
vetted) (VettedMount -> Mount
vmMount VettedMount
vetted) (VettedMount -> MountConfig
vmConfig VettedMount
vetted)
pure $ case (credentialError mintPlan providers (vmMount vetted), resolveAdapter eco deps (mountPublishDeps ctx plan vetted)) of
(Maybe BootError
Nothing, Just MountBinding
binding) -> MountBinding -> Either [BootError] MountBinding
forall a b. b -> Either a b
Right MountBinding
binding
(Maybe BootError
mCredErr, Maybe MountBinding
mBinding) ->
[BootError] -> Either [BootError] MountBinding
forall a b. a -> Either a b
Left (Maybe BootError -> [BootError]
forall a. Maybe a -> [a]
maybeToList Maybe BootError
mCredErr [BootError] -> [BootError] -> [BootError]
forall a. Semigroup a => a -> a -> a
<> [Ecosystem -> BootError
MissingAdapter Ecosystem
eco | Maybe MountBinding -> Bool
forall a. Maybe a -> Bool
isNothing Maybe MountBinding
mBinding])
where
eco :: Ecosystem
eco = VettedMount -> Ecosystem
vmEcosystem VettedMount
vetted
data WiringContext = WiringContext
{ WiringContext -> AppConfig
wcApp :: AppConfig
, WiringContext -> Limits
wcLimits :: Limits
, WiringContext -> IO UTCTime
wcClock :: IO UTCTime
, WiringContext -> Ecosystem -> RuleDeps
wcRuleDeps :: Ecosystem -> RuleDeps
, WiringContext -> Maybe PublishBudget
wcPublishBudget :: Maybe PublishBudget
, WiringContext -> Maybe HelpMessage
wcHelp :: Maybe HelpMessage
}
mountPublishDeps :: WiringContext -> ValidatedPlan -> VettedMount -> Maybe PublishDeps
mountPublishDeps :: WiringContext -> ValidatedPlan -> VettedMount -> Maybe PublishDeps
mountPublishDeps WiringContext
ctx ValidatedPlan
plan VettedMount
vetted =
Ecosystem
-> Map Ecosystem VettedPublication -> Maybe VettedPublication
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup (VettedMount -> Ecosystem
vmEcosystem VettedMount
vetted) (ValidatedPlan -> Map Ecosystem VettedPublication
vpPublications ValidatedPlan
plan)
Maybe VettedPublication
-> (VettedPublication -> Maybe PublishDeps) -> Maybe PublishDeps
forall a b. Maybe a -> (a -> Maybe b) -> Maybe b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= RegistryAdapter
-> AppConfig
-> Limits
-> Maybe PublishBudget
-> Maybe HelpMessage
-> VettedPublication
-> Maybe PublishDeps
publishDepsFor (VettedMount -> RegistryAdapter
vmAdapter VettedMount
vetted) (WiringContext -> AppConfig
wcApp WiringContext
ctx) (WiringContext -> Limits
wcLimits WiringContext
ctx) (WiringContext -> Maybe PublishBudget
wcPublishBudget WiringContext
ctx) (WiringContext -> Maybe HelpMessage
wcHelp WiringContext
ctx)
packumentDepsFor :: WiringContext -> RegistryAdapter -> Mount -> MountConfig -> IO PackumentDeps
packumentDepsFor :: WiringContext
-> RegistryAdapter -> Mount -> MountConfig -> IO PackumentDeps
packumentDepsFor WiringContext
ctx RegistryAdapter
adapter Mount
mount MountConfig
mcfg = do
let ruleDeps :: RuleDeps
ruleDeps = WiringContext -> Ecosystem -> RuleDeps
wcRuleDeps WiringContext
ctx (Mount -> Ecosystem
mountEcosystem Mount
mount)
prepared <- RuleDeps -> [PrecededRule] -> IO [PreparedRule]
prepare RuleDeps
ruleDeps (Mount -> [PrecededRule]
mountPolicy Mount
mount)
let regs = Mount -> MountRegistries
mountRegistries Mount
mount
app = WiringContext -> AppConfig
wcApp WiringContext
ctx
pure
PackumentDeps
{ pdUpstreams =
mountUpstreams
(artifactHosts (adapterArtifact adapter))
(regPrivateUpstream regs)
(regPublicUpstream regs)
(maybe NoMirrorWrite (MirrorOnAdmit . mtUrl) (regMirrorTarget regs))
,
pdFirstParty = maybe (const False) firstPartyName (mntFirstParty mcfg)
, pdMountBaseUrl = mountBaseUrl (srvPublicUrl (cfgServer app)) (mountEcosystem mount)
, pdRules = prepared
,
pdAdditionalBlockedRanges = egrAdditionalBlockedRanges (cfgEgress app)
, pdLimits = wcLimits ctx
, pdInboundToken = srvAuthToken (cfgServer app)
, pdNow = wcClock ctx
, pdAdvisoryEtag = rdCurrentAdvisoryEtag ruleDeps
, pdNoteAdmission = noteAdmission (rdSourceReporter ruleDeps)
, pdHelp = wcHelp ctx
, pdMinIntegrity = intMinPublic (cfgIntegrity app)
,
pdMinTrustedIntegrity = fromMaybe (intMinTrusted (cfgIntegrity app)) (miMinTrusted (mntIntegrity mcfg))
, pdMetadata = adapterMetadata adapter
, pdArtifact = adapterArtifact adapter
, pdEgressUrl = mkRegistryUrl
}
credentialError :: MirrorMintPlan -> CredentialProviders -> Mount -> Maybe BootError
credentialError :: MirrorMintPlan -> CredentialProviders -> Mount -> Maybe BootError
credentialError MirrorMintPlan
mintPlan CredentialProviders
providers Mount
mount = case (MirrorMintPlan
mintPlan, MountRegistries -> Maybe MirrorTarget
regMirrorTarget (Mount -> MountRegistries
mountRegistries Mount
mount)) of
(MirrorMintPlan
SkipMirrorWrite, Maybe MirrorTarget
_) -> Maybe BootError
forall a. Maybe a
Nothing
(MirrorMintPlan
MintMirrorWrite, Maybe MirrorTarget
Nothing) -> Maybe BootError
forall a. Maybe a
Nothing
(MirrorMintPlan
MintMirrorWrite, Just MirrorTarget
_) ->
if Mount -> Ecosystem
mountEcosystem Mount
mount Ecosystem -> Set Ecosystem -> Bool
forall a. Ord a => a -> Set a -> Bool
`Set.member` CredentialProviders -> Set Ecosystem
initializedEcosystems CredentialProviders
providers
then Maybe BootError
forall a. Maybe a
Nothing
else BootError -> Maybe BootError
forall a. a -> Maybe a
Just (Ecosystem -> BootError
UnresolvedCredential (Mount -> Ecosystem
mountEcosystem Mount
mount))
mountBaseUrl :: Maybe Url -> Ecosystem -> Text
mountBaseUrl :: Maybe Url -> Ecosystem -> Text
mountBaseUrl Maybe Url
publicUrl Ecosystem
eco =
case Maybe Url
publicUrl of
Maybe Url
Nothing -> Ecosystem -> Text
mountBasePath Ecosystem
eco
Just Url
public -> Text -> Text
stripTrailingSlash (Url -> Text
unUrl Url
public) Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Ecosystem -> Text
mountBasePath Ecosystem
eco
mountBasePath :: Ecosystem -> Text
mountBasePath :: Ecosystem -> Text
mountBasePath Ecosystem
eco = Text
"/" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> [Text] -> Text
T.intercalate Text
"/" (NonEmpty Text -> [Text]
forall a. NonEmpty a -> [a]
forall (t :: * -> *) a. Foldable t => t a -> [a]
toList (Ecosystem -> NonEmpty Text
prefixFor Ecosystem
eco))
publishDepsFor :: RegistryAdapter -> AppConfig -> Limits -> Maybe PublishBudget -> Maybe HelpMessage -> VettedPublication -> Maybe PublishDeps
publishDepsFor :: RegistryAdapter
-> AppConfig
-> Limits
-> Maybe PublishBudget
-> Maybe HelpMessage
-> VettedPublication
-> Maybe PublishDeps
publishDepsFor RegistryAdapter
adapter AppConfig
app Limits
limits Maybe PublishBudget
publishBudget Maybe HelpMessage
helpMessage VettedPublication
publication = do
budget <- Maybe PublishBudget
publishBudget
publish <- adapterPublish adapter
pure
PublishDeps
{ pubTargetUrl = publicationTargetUrl (vpubTarget publication)
, pubAllowed = firstPartyName (vpubFirstParty publication)
, pubStaticToken = vpubStaticToken publication
, pubInboundToken = srvAuthToken (cfgServer app)
, pubLimits = limits{maxPublishRequestBytes = pbMaxRequestBytes budget}
, pubBodyBudget = pbBodyBudget budget
, pubHelp = helpMessage
, pubProjectName = adapterProjectName adapter
, pubAdapter = publish
}
firstPartyName :: FirstParty -> PackageName -> Bool
firstPartyName :: FirstParty -> PackageName -> Bool
firstPartyName = \case
FirstPartyNpmScopes NonEmpty Scope
scopes -> [Scope] -> PackageName -> Bool
npmPublishAllowed (NonEmpty Scope -> [Scope]
forall a. NonEmpty a -> [a]
forall (t :: * -> *) a. Foldable t => t a -> [a]
toList NonEmpty Scope
scopes)
FirstPartyPyPI NonEmpty PyPIFirstParty
entries -> NonEmpty PyPIFirstParty -> PackageName -> Bool
pypiFirstPartyName NonEmpty PyPIFirstParty
entries
data PublishTarget = PublishTarget
{ PublishTarget -> Ecosystem
ptEcosystem :: Ecosystem
, PublishTarget -> RegistryUrl
ptMirrorUrl :: RegistryUrl
, PublishTarget -> CredentialProvider
ptCredentials :: CredentialProvider
}
planPublishTargets ::
MirrorMintPlan ->
CredentialProviders ->
ValidatedPlan ->
Either [BootError] [PublishTarget]
planPublishTargets :: MirrorMintPlan
-> CredentialProviders
-> ValidatedPlan
-> Either [BootError] [PublishTarget]
planPublishTargets MirrorMintPlan
mintPlan CredentialProviders
providers ValidatedPlan
plan = case MirrorMintPlan
mintPlan of
MirrorMintPlan
SkipMirrorWrite -> [PublishTarget] -> Either [BootError] [PublishTarget]
forall a b. b -> Either a b
Right []
MirrorMintPlan
MintMirrorWrite ->
case [Either [BootError] PublishTarget]
-> ([[BootError]], [PublishTarget])
forall a b. [Either a b] -> ([a], [b])
partitionEithers ((VettedMount -> Maybe (Either [BootError] PublishTarget))
-> [VettedMount] -> [Either [BootError] PublishTarget]
forall a b. (a -> Maybe b) -> [a] -> [b]
mapMaybe (CredentialProviders
-> Mount -> Maybe (Either [BootError] PublishTarget)
publishTargetFor CredentialProviders
providers (Mount -> Maybe (Either [BootError] PublishTarget))
-> (VettedMount -> Mount)
-> VettedMount
-> Maybe (Either [BootError] PublishTarget)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. VettedMount -> Mount
vmMount) (ValidatedPlan -> [VettedMount]
vpMounts ValidatedPlan
plan)) of
([], [PublishTarget]
targets) -> [PublishTarget] -> Either [BootError] [PublishTarget]
forall a b. b -> Either a b
Right [PublishTarget]
targets
([[BootError]]
errs, [PublishTarget]
_) -> [BootError] -> Either [BootError] [PublishTarget]
forall a b. a -> Either a b
Left ([[BootError]] -> [BootError]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [[BootError]]
errs)
publishTargetFor :: CredentialProviders -> Mount -> Maybe (Either [BootError] PublishTarget)
publishTargetFor :: CredentialProviders
-> Mount -> Maybe (Either [BootError] PublishTarget)
publishTargetFor CredentialProviders
providers Mount
mount = do
target <- MountRegistries -> Maybe MirrorTarget
regMirrorTarget (Mount -> MountRegistries
mountRegistries Mount
mount)
pure $ case lookupProvider (mountEcosystem mount) providers of
Just CredentialProvider
provider ->
PublishTarget -> Either [BootError] PublishTarget
forall a b. b -> Either a b
Right
PublishTarget
{ ptEcosystem :: Ecosystem
ptEcosystem = Mount -> Ecosystem
mountEcosystem Mount
mount
, ptMirrorUrl :: RegistryUrl
ptMirrorUrl = MirrorTarget -> RegistryUrl
mtUrl MirrorTarget
target
, ptCredentials :: CredentialProvider
ptCredentials = CredentialProvider
provider
}
Maybe CredentialProvider
Nothing ->
[BootError] -> Either [BootError] PublishTarget
forall a b. a -> Either a b
Left [Ecosystem -> BootError
UnresolvedCredential (Mount -> Ecosystem
mountEcosystem Mount
mount)]