module Ecluse.Composition (
planMounts,
composeBindings,
validateComposition,
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 Ecluse.Composition.BootError (BootError (..))
import Ecluse.Composition.Credential (CredentialProviders, initializedEcosystems, lookupProvider)
import Ecluse.Config (
AppConfig (..),
Config (..),
EgressSettings (..),
IntegritySettings (..),
MirrorTarget (mtUrl),
Mount (..),
MountConfig (..),
MountRegistries (..),
ServerSettings (..),
Url,
regMirrorTarget,
regPrivateUpstream,
unUrl,
)
import Ecluse.Core.Credential (CredentialProvider, Secret)
import Ecluse.Core.Ecosystem (Ecosystem, prefixFor)
import Ecluse.Core.Registry.Adapter (
RegistryAdapter,
adapterArtifact,
adapterFor,
adapterMetadata,
adapterPublish,
artifactByFile,
artifactByUrl,
artifactHosts,
metadataAssemble,
metadataNewClient,
metadataSerialise,
publishCanonicaliseName,
publishDeclaredNames,
publishRelay,
)
import Ecluse.Core.Rules (RuleDeps, prepare, rdCurrentAdvisoryEtag)
import Ecluse.Core.Security (Limits, tarballHostGate)
import Ecluse.Core.Security.Egress (mkRegistryUrl, registryUrlText)
import Ecluse.Core.Server.Admission.Bytes (ByteAdmission)
import Ecluse.Core.Server.Context (MirrorServePlan (MirrorOnAdmit, NoMirrorWrite), MountBinding, PackumentDeps (..), PublishDeps (..))
import Ecluse.Core.Server.Response (HelpMessage, mkHelpMessage)
planMounts ::
(Ecosystem -> PackumentDeps -> Maybe PublishDeps -> Maybe MountBinding) ->
IO UTCTime ->
(Ecosystem -> RuleDeps) ->
CredentialProviders ->
Limits ->
Maybe PublishBudget ->
Config ->
IO (Either [BootError] [MountBinding])
planMounts :: (Ecosystem
-> PackumentDeps -> Maybe PublishDeps -> Maybe MountBinding)
-> IO UTCTime
-> (Ecosystem -> RuleDeps)
-> CredentialProviders
-> Limits
-> Maybe PublishBudget
-> Config
-> IO (Either [BootError] [MountBinding])
planMounts = (Ecosystem
-> PackumentDeps -> Maybe PublishDeps -> Maybe MountBinding)
-> IO UTCTime
-> (Ecosystem -> RuleDeps)
-> CredentialProviders
-> Limits
-> Maybe PublishBudget
-> Config
-> IO (Either [BootError] [MountBinding])
composeBindings
data PublishBudget = PublishBudget
{ PublishBudget -> ByteAdmission
pbBodyBudget :: ByteAdmission
, PublishBudget -> Int
pbMaxRequestBytes :: Int
}
composeBindings ::
(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])
composeBindings Ecosystem
-> PackumentDeps -> Maybe PublishDeps -> Maybe MountBinding
resolveAdapter IO UTCTime
clock Ecosystem -> RuleDeps
ruleDepsFor CredentialProviders
providers Limits
limits Maybe PublishBudget
publishBudget Config
config = do
let structuralErrs :: [BootError]
structuralErrs = Config -> [BootError]
validateComposition Config
config
pubDepsMap :: Map Ecosystem (Maybe PublishDeps)
pubDepsMap = (Ecosystem -> MountConfig -> Maybe PublishDeps)
-> Map Ecosystem MountConfig -> Map Ecosystem (Maybe PublishDeps)
forall k a b. (k -> a -> b) -> Map k a -> Map k b
Map.mapWithKey (\Ecosystem
eco MountConfig
mcfg -> Maybe RegistryAdapter
-> AppConfig
-> MountConfig
-> Limits
-> Maybe PublishBudget
-> Maybe HelpMessage
-> Maybe PublishDeps
publishDepsFor (Ecosystem -> Maybe RegistryAdapter
adapterFor Ecosystem
eco) AppConfig
app MountConfig
mcfg Limits
limits Maybe PublishBudget
publishBudget Maybe HelpMessage
helpMessage) (AppConfig -> Map Ecosystem MountConfig
cfgMounts AppConfig
app)
let mounts :: [(Mount, MountConfig)]
mounts = Map Ecosystem (Mount, MountConfig) -> [(Mount, MountConfig)]
forall k a. Map k a -> [a]
Map.elems ((Mount -> MountConfig -> (Mount, MountConfig))
-> Map Ecosystem Mount
-> Map Ecosystem MountConfig
-> Map Ecosystem (Mount, MountConfig)
forall k a b c.
Ord k =>
(a -> b -> c) -> Map k a -> Map k b -> Map k c
Map.intersectionWith (,) (Config -> Map Ecosystem Mount
configMounts Config
config) (AppConfig -> Map Ecosystem MountConfig
cfgMounts AppConfig
app))
bindingResults <- ((Mount, MountConfig) -> IO (Either [BootError] MountBinding))
-> [(Mount, MountConfig)] -> 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 (\(Mount
mount, MountConfig
mcfg) -> Maybe PublishDeps
-> Mount -> MountConfig -> IO (Either [BootError] MountBinding)
bindingFor (Maybe (Maybe PublishDeps) -> Maybe PublishDeps
forall (m :: * -> *) a. Monad m => m (m a) -> m a
join (Ecosystem
-> Map Ecosystem (Maybe PublishDeps) -> Maybe (Maybe PublishDeps)
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup (Mount -> Ecosystem
mountEcosystem Mount
mount) Map Ecosystem (Maybe PublishDeps)
pubDepsMap)) Mount
mount MountConfig
mcfg) [(Mount, MountConfig)]
mounts
pure $ case (structuralErrs, partitionEithers bindingResults) of
([], ([], [MountBinding]
bindings)) -> [MountBinding] -> Either [BootError] [MountBinding]
forall a b. b -> Either a b
Right [MountBinding]
bindings
([BootError]
_, ([[BootError]]
errs, [MountBinding]
_)) -> [BootError] -> Either [BootError] [MountBinding]
forall a b. a -> Either a b
Left ([BootError]
structuralErrs [BootError] -> [BootError] -> [BootError]
forall a. Semigroup a => a -> a -> a
<> [[BootError]] -> [BootError]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [[BootError]]
errs)
where
app :: AppConfig
app :: AppConfig
app = Config -> AppConfig
configApp Config
config
helpMessage :: Maybe HelpMessage
helpMessage :: Maybe HelpMessage
helpMessage = 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 :: Maybe PublishDeps -> Mount -> MountConfig -> IO (Either [BootError] MountBinding)
bindingFor :: Maybe PublishDeps
-> Mount -> MountConfig -> IO (Either [BootError] MountBinding)
bindingFor Maybe PublishDeps
pubDeps Mount
mount MountConfig
mcfg =
case Ecosystem -> Maybe RegistryAdapter
adapterFor Ecosystem
eco of
Maybe RegistryAdapter
Nothing -> Either [BootError] MountBinding
-> IO (Either [BootError] MountBinding)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ([BootError] -> Either [BootError] MountBinding
forall a b. a -> Either a b
Left (Maybe BootError -> [BootError]
forall a. Maybe a -> [a]
maybeToList (CredentialProviders -> Mount -> Maybe BootError
credentialError CredentialProviders
providers Mount
mount)))
Just RegistryAdapter
adapter -> do
deps <- RegistryAdapter -> Mount -> MountConfig -> IO PackumentDeps
packumentDepsFor RegistryAdapter
adapter Mount
mount MountConfig
mcfg
pure $ case (credentialError providers mount, resolveAdapter eco deps pubDeps) 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 = Mount -> Ecosystem
mountEcosystem Mount
mount
packumentDepsFor :: RegistryAdapter -> Mount -> MountConfig -> IO PackumentDeps
packumentDepsFor :: RegistryAdapter -> Mount -> MountConfig -> IO PackumentDeps
packumentDepsFor RegistryAdapter
adapter Mount
mount MountConfig
mcfg = do
let ruleDeps :: RuleDeps
ruleDeps = Ecosystem -> RuleDeps
ruleDepsFor (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
pure
PackumentDeps
{ pdPrivateBaseUrl = registryUrlText <$> regPrivateUpstream regs
, pdPublicBaseUrl = registryUrlText (regPublicUpstream regs)
, pdMountBaseUrl = mountBaseUrl (srvPublicUrl (cfgServer app)) (mountEcosystem mount)
, pdMirror = maybe NoMirrorWrite (MirrorOnAdmit . registryUrlText . mtUrl) (regMirrorTarget regs)
, pdRules = prepared
,
pdAdditionalBlockedRanges = egrAdditionalBlockedRanges (cfgEgress app)
,
pdTarballHostGate =
tarballHostGate
(artifactHosts (adapterArtifact adapter))
(registryUrlText <$> regPrivateUpstream regs)
(registryUrlText (regPublicUpstream regs))
(registryUrlText . mtUrl <$> regMirrorTarget regs)
, pdLimits = limits
, pdInboundToken = srvAuthToken (cfgServer app)
, pdNow = clock
, pdAdvisoryEtag = rdCurrentAdvisoryEtag ruleDeps
, pdHelp = helpMessage
,
pdMinIntegrity = intMinPublic (cfgIntegrity app)
,
pdMinTrustedIntegrity = fromMaybe (intMinTrusted (cfgIntegrity app)) (mntMinTrustedIntegrity mcfg)
,
pdDivergencePolicy = fromMaybe (intDivergencePolicy (cfgIntegrity app)) (mntDivergencePolicy mcfg)
, pdNewMetadataClient = metadataNewClient (adapterMetadata adapter)
, pdBuildArtifactRequestByFile = artifactByFile (adapterArtifact adapter)
, pdBuildArtifactRequestByUrl = artifactByUrl (adapterArtifact adapter)
, pdAssemble = metadataAssemble (adapterMetadata adapter)
, pdSerialise = metadataSerialise (adapterMetadata adapter)
, pdEgressUrl = mkRegistryUrl
}
credentialError :: CredentialProviders -> Mount -> Maybe BootError
credentialError :: CredentialProviders -> Mount -> Maybe BootError
credentialError CredentialProviders
providers Mount
mount = case MountRegistries -> Maybe MirrorTarget
regMirrorTarget (Mount -> MountRegistries
mountRegistries Mount
mount) of
Maybe MirrorTarget
Nothing -> Maybe BootError
forall a. Maybe a
Nothing
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 -> (Char -> Bool) -> Text -> Text
T.dropWhileEnd (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'/') (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))
validateComposition :: Config -> [BootError]
validateComposition :: Config -> [BootError]
validateComposition Config
config = [BootError]
missingAdapters [BootError] -> [BootError] -> [BootError]
forall a. Semigroup a => a -> a -> a
<> [BootError]
publishPolicyErrors
where
app :: AppConfig
app = Config -> AppConfig
configApp Config
config
missingAdapters :: [BootError]
missingAdapters =
[Ecosystem -> BootError
MissingAdapter Ecosystem
eco | Ecosystem
eco <- Map Ecosystem Mount -> [Ecosystem]
forall k a. Map k a -> [k]
Map.keys (Config -> Map Ecosystem Mount
configMounts Config
config), Maybe RegistryAdapter -> Bool
forall a. Maybe a -> Bool
isNothing (Ecosystem -> Maybe RegistryAdapter
adapterFor Ecosystem
eco)]
publishPolicyErrors :: [BootError]
publishPolicyErrors =
[[BootError]] -> [BootError]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat
[ Ecosystem -> MountConfig -> Maybe Secret -> [BootError]
publishBootErrors Ecosystem
eco MountConfig
mcfg (ServerSettings -> Maybe Secret
srvAuthToken (AppConfig -> ServerSettings
cfgServer AppConfig
app))
| (Ecosystem
eco, MountConfig
mcfg) <- Map Ecosystem MountConfig -> [(Ecosystem, MountConfig)]
forall k a. Map k a -> [(k, a)]
Map.toAscList (AppConfig -> Map Ecosystem MountConfig
cfgMounts AppConfig
app)
, Maybe RegistryUrl -> Bool
forall a. Maybe a -> Bool
isJust (MountConfig -> Maybe RegistryUrl
mntPublicationTarget MountConfig
mcfg)
]
publishDepsFor :: Maybe RegistryAdapter -> AppConfig -> MountConfig -> Limits -> Maybe PublishBudget -> Maybe HelpMessage -> Maybe PublishDeps
publishDepsFor :: Maybe RegistryAdapter
-> AppConfig
-> MountConfig
-> Limits
-> Maybe PublishBudget
-> Maybe HelpMessage
-> Maybe PublishDeps
publishDepsFor Maybe RegistryAdapter
mAdapter AppConfig
app MountConfig
mcfg Limits
limits Maybe PublishBudget
publishBudget Maybe HelpMessage
helpMessage = do
url <- MountConfig -> Maybe RegistryUrl
mntPublicationTarget MountConfig
mcfg
adapter <- mAdapter
budget <- publishBudget
pure
PublishDeps
{ pubTargetUrl = registryUrlText url
, pubScopes = mntPublishAllow mcfg
, pubStaticToken = mntPublicationTargetToken mcfg
, pubInboundToken = inboundToken
, pubLimits = limits
, pubBodyBudget = pbBodyBudget budget
, pubMaxRequestBytes = pbMaxRequestBytes budget
, pubHelp = helpMessage
, pubRelayPublish = publishRelay (adapterPublish adapter)
, pubCanonicaliseName = publishCanonicaliseName (adapterPublish adapter)
, pubDeclaredNames = publishDeclaredNames (adapterPublish adapter)
}
where
inboundToken :: Maybe Secret
inboundToken :: Maybe Secret
inboundToken = ServerSettings -> Maybe Secret
srvAuthToken (AppConfig -> ServerSettings
cfgServer AppConfig
app)
publishBootErrors :: Ecosystem -> MountConfig -> Maybe Secret -> [BootError]
publishBootErrors :: Ecosystem -> MountConfig -> Maybe Secret -> [BootError]
publishBootErrors Ecosystem
eco MountConfig
mcfg Maybe Secret
inboundToken = [Maybe BootError] -> [BootError]
forall a. [Maybe a] -> [a]
catMaybes [Maybe BootError
scopesError, Maybe BootError
edgeError]
where
scopesError, edgeError :: Maybe BootError
scopesError :: Maybe BootError
scopesError
| [Scope] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null (MountConfig -> [Scope]
mntPublishAllow MountConfig
mcfg) = BootError -> Maybe BootError
forall a. a -> Maybe a
Just (Ecosystem -> BootError
PublishAllowMissing Ecosystem
eco)
| Bool
otherwise = Maybe BootError
forall a. Maybe a
Nothing
edgeError :: Maybe BootError
edgeError
| Maybe Secret -> Bool
forall a. Maybe a -> Bool
isJust (MountConfig -> Maybe Secret
mntPublicationTargetToken MountConfig
mcfg) Bool -> Bool -> Bool
&& Maybe Secret -> Bool
forall a. Maybe a -> Bool
isNothing Maybe Secret
inboundToken = BootError -> Maybe BootError
forall a. a -> Maybe a
Just (Ecosystem -> BootError
PublishStaticCredentialNeedsEdge Ecosystem
eco)
| Bool
otherwise = Maybe BootError
forall a. Maybe a
Nothing
data PublishTarget = PublishTarget
{ PublishTarget -> Ecosystem
ptEcosystem :: Ecosystem
, PublishTarget -> Text
ptMirrorUrl :: Text
, PublishTarget -> CredentialProvider
ptCredentials :: CredentialProvider
}
planPublishTargets ::
CredentialProviders ->
Config ->
Either [BootError] [PublishTarget]
planPublishTargets :: CredentialProviders -> Config -> Either [BootError] [PublishTarget]
planPublishTargets = CredentialProviders -> Config -> Either [BootError] [PublishTarget]
composePublishTargets
composePublishTargets ::
CredentialProviders ->
Config ->
Either [BootError] [PublishTarget]
composePublishTargets :: CredentialProviders -> Config -> Either [BootError] [PublishTarget]
composePublishTargets CredentialProviders
providers Config
config =
case [Either [BootError] PublishTarget]
-> ([[BootError]], [PublishTarget])
forall a b. [Either a b] -> ([a], [b])
partitionEithers ((Mount -> Maybe (Either [BootError] PublishTarget))
-> [Mount] -> [Either [BootError] PublishTarget]
forall a b. (a -> Maybe b) -> [a] -> [b]
mapMaybe (CredentialProviders
-> Mount -> Maybe (Either [BootError] PublishTarget)
publishTargetFor CredentialProviders
providers) (Map Ecosystem Mount -> [Mount]
forall k a. Map k a -> [a]
Map.elems (Config -> Map Ecosystem Mount
configMounts Config
config))) 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 :: Text
ptMirrorUrl = RegistryUrl -> Text
registryUrlText (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)]