-- | B9 has a concept of 'B9.DiskImages.SharedImaged'. Shared images can be pulled and -- pushed to/from remote locations via rsync+ssh. B9 also maintains a local cache; -- the whole thing is supposed to be build-server-safe, that means no two builds -- shall interfere with each other. This is accomplished by refraining from -- automatic cache updates from/to remote repositories. module B9.Repository ( RepoCacheReader, getRepoCache, withSelectedRemoteRepo, getSelectedRemoteRepo, SelectedRemoteRepoReader, Repository (..), RepoImagesMap, toRemoteRepository, SelectedRemoteRepo (..), remoteRepoCacheDir, localRepoDir, lookupRemoteRepo, filterRepoImagesMap, lookupCachedImages, allCachedSharedImages, allSharedImagesWithRepo, maxSharedImageOfAllRepos, allSharedImagesInRepo, allSharedImages, allRepositories, groupBySharedImageName, keepNLatestSharedImages, dropAllButNLatestSharedImages, module X, ) where import B9.B9Config import B9.B9Config.Repository as X import B9.B9Error import B9.DiskImages import Control.Eff import Control.Eff.Reader.Lazy import Data.Foldable import Data.Map (Map) import qualified Data.Map as Map import Data.Maybe import Data.Set (Set) import qualified Data.Set as Set import GHC.Generics import System.FilePath import Test.QuickCheck import Text.Printf data Repository = Cache | Remote String -- TODO use a newtype deriving (Eq, Ord, Read, Show, Generic) instance Arbitrary Repository where arbitrary = Test.QuickCheck.oneof [ pure Cache, Remote . printf "remote-repo-%0X" <$> choose (0, 31 :: Int) ] instance Function Repository instance CoArbitrary Repository -- | Convert a `RemoteRepo` down to a mere `Repository` toRemoteRepository :: RemoteRepo -> Repository toRemoteRepository = Remote . remoteRepoRepoId -- | Alias for a 'Reader' 'Eff'ect that reads a list of 'RemoteRepo's. -- -- @since 0.5.65 type RepoCacheReader = Reader RepoCache -- | Ask for the 'RepoCache' initialized by 'withRemoteRepos'. -- -- @since 0.5.65 getRepoCache :: Member RepoCacheReader e => Eff e RepoCache getRepoCache = ask -- | Run a 'SelectedRemoteRepoReader' with the 'SelectedRemoteRepo' selected -- in the 'B9Config'. -- -- If the selected repo does not exist, and exception is thrown. -- -- @since 0.5.65 withSelectedRemoteRepo :: (Member B9ConfigReader e, Member ExcB9 e) => Eff (SelectedRemoteRepoReader ': e) a -> Eff e a withSelectedRemoteRepo e = do remoteRepos' <- _remoteRepos <$> getB9Config mSelectedRepoName <- _repository <$> getB9Config case mSelectedRepoName of Nothing -> runReader (MkSelectedRemoteRepo Nothing) e Just selectedRepoName -> case lookupRemoteRepo remoteRepos' selectedRepoName of Nothing -> throwB9Error ( printf "selected remote repo '%s' not configured, valid remote repos are: '%s'" (show selectedRepoName) (show remoteRepos') ) Just r -> runReader (MkSelectedRemoteRepo (Just r)) e -- | Contains the 'Just' the 'RemoteRepo' selected by the 'B9Config' value '_repository', -- or 'Nothing' of no 'RemoteRepo' was selected in the 'B9Config'. -- -- @since 0.5.65 newtype SelectedRemoteRepo = MkSelectedRemoteRepo {fromSelectedRemoteRepo :: Maybe RemoteRepo} -- | Alias for a 'Reader' 'Eff'ect that reads the 'RemoteRepo' -- selected by the 'B9Config' value '_repository'. See 'withSelectedRemoteRepo'. -- -- @since 0.5.65 type SelectedRemoteRepoReader = Reader SelectedRemoteRepo -- | Ask for the 'RemoteRepo' -- selected by the 'B9Config' value '_repository'. See 'withSelectedRemoteRepo'. -- -- @since 0.5.65 getSelectedRemoteRepo :: Member SelectedRemoteRepoReader e => Eff e SelectedRemoteRepo getSelectedRemoteRepo = ask -- | Return the cache directory for a remote repository relative to the root -- cache dir. remoteRepoCacheDir :: -- | The repository cache directory RepoCache -> -- | Id of the repository String -> -- | The existing, absolute path to the -- cache directory FilePath remoteRepoCacheDir (RepoCache cacheDir) repoId = cacheDir "remote-repos" repoId -- | Return the local repository directory. localRepoDir :: -- | The repository cache directory RepoCache -> -- | The existing, absolute path to the -- directory FilePath localRepoDir (RepoCache cacheDir) = cacheDir "local-repo" -- | Select the first 'RemoteRepo' with a given @repoId@. lookupRemoteRepo :: [RemoteRepo] -> String -> Maybe RemoteRepo lookupRemoteRepo repos repoId = lookup repoId repoIdRepoPairs where repoIdRepoPairs = map (\r@(RemoteRepo rid _ _ _ _) -> (rid, r)) repos -- | A 'Map' that maps 'Repository's to the 'SharedImage's they hold. -- -- @since 1.1.0 type RepoImagesMap = Map Repository (Set SharedImage) -- | Filter the 'SharedImage's returned by 'getSharedImages' -- using a 'Repository'-, and a 'SharedImage' predicate. -- -- @since 1.1.0 filterRepoImagesMap :: (Repository -> Bool) -> (SharedImage -> Bool) -> RepoImagesMap -> RepoImagesMap filterRepoImagesMap repoPred imgPred = Map.map (Set.filter imgPred) . Map.filterWithKey (const . repoPred) -- | Return the versions of a shared image named 'name' from the local cache. -- -- @since 1.1.0 lookupCachedImages :: SharedImageName -> RepoImagesMap -> Set SharedImage lookupCachedImages name = allSharedImages . filterRepoImagesMap (== Cache) ((== name) . sharedImageName) -- | Return a 'Set' of 'Repository' names from a 'RepoImagesMap' -- -- @since 1.1.0 allRepositories :: RepoImagesMap -> Set Repository allRepositories = Map.keysSet -- | Get a 'Set' of all 'SharedImage's in all 'Repository's. -- -- @since 1.1.0 allSharedImages :: RepoImagesMap -> Set SharedImage allSharedImages = fold -- | Fetch all 'SharedImage's like 'allSharedImages' but attach -- the 'Repository' that the image belongs to. -- -- Usage example: In combination with 'filterRepoImagesMap' to find -- the latest version of a certain image in all known repositories. -- -- @since 1.1.0 allSharedImagesWithRepo :: RepoImagesMap -> Set (SharedImage, Repository) allSharedImagesWithRepo = Map.foldMapWithKey (Set.map . flip (,)) -- | Return the maximum with regard to the 'Ord' instance of 'SharedImage' -- from an 'RepoImagesMap' -- -- @since 1.1.0 maxSharedImageOfAllRepos :: RepoImagesMap -> Maybe (SharedImage, Repository) maxSharedImageOfAllRepos = Set.lookupMax . allSharedImagesWithRepo -- | Return the 'SharedImage's, that are contained in a 'Repository'. -- -- @since 1.1.0 allSharedImagesInRepo :: Repository -> RepoImagesMap -> Set SharedImage allSharedImagesInRepo repo = fromMaybe Set.empty . Map.lookup repo -- | Keep 'SharedImage's that are in the 'Cache' 'Repository'. -- -- @since 1.1.0 allCachedSharedImages :: RepoImagesMap -> Set SharedImage allCachedSharedImages = allSharedImagesInRepo Cache -- | Take a subset that contains the @n@ -- latest versions of 'SharedImage's with the same name. -- -- For example, if the input contains: -- -- @@@ -- fromList -- [ SharedImage "foo" "2020-07-07 13:34:31" -- , SharedImage "foo" "2020-07-07 13:34:32" -- , SharedImage "foo" "2020-07-07 13:34:33" -- , SharedImage "bar" "2020-07-07 13:34:34" -- , SharedImage "bar" "2020-07-07 13:34:35" -- , SharedImage "bar" "2020-07-07 13:34:36" -- ] -- @@@ -- -- The output of @keepNLatestSharedImages 2@ will be: -- -- @@@ -- fromList -- [ SharedImage "foo" "2020-07-07 13:34:32" -- , SharedImage "foo" "2020-07-07 13:34:33" -- , SharedImage "bar" "2020-07-07 13:34:35" -- , SharedImage "bar" "2020-07-07 13:34:36" -- ] -- @@@ -- -- @since 1.1.0 keepNLatestSharedImages :: Int -> Set SharedImage -> Set SharedImage keepNLatestSharedImages n = fold . Map.map ( \s -> let nOld = max 0 (length s - n) in Set.drop nOld s ) . groupBySharedImageName -- | Take a subset that contains obsolete images. -- -- Do the opposite of 'keepNLatestSharedImages', -- and return all **but** the @n@ -- latest versions of 'SharedImage's with the same name. -- -- For example, if the input contains: -- -- @@@ -- fromList -- [ SharedImage "foo" "2020-07-07 13:34:31" -- , SharedImage "foo" "2020-07-07 13:34:32" -- , SharedImage "foo" "2020-07-07 13:34:33" -- , SharedImage "bar" "2020-07-07 13:34:34" -- , SharedImage "bar" "2020-07-07 13:34:35" -- , SharedImage "bar" "2020-07-07 13:34:36" -- ] -- @@@ -- -- The output of @keepNLatestSharedImages 2@ will be: -- -- @@@ -- fromList -- [ SharedImage "foo" "2020-07-07 13:34:31" -- , SharedImage "bar" "2020-07-07 13:34:34" -- ] -- @@@ -- -- @since 1.1.0 dropAllButNLatestSharedImages :: Int -> Set SharedImage -> Set SharedImage dropAllButNLatestSharedImages n = fold . Map.map ( \s -> let nOld = max 0 (length s - n) in Set.take nOld s ) . groupBySharedImageName -- | Group by 'SharedImageName'. -- -- @since 1.1.0 groupBySharedImageName :: Set SharedImage -> Map SharedImageName (Set SharedImage) groupBySharedImageName = foldr ( \img -> Map.alter ( Just . maybe (Set.singleton img) (Set.insert img) ) (sharedImageName img) ) Map.empty