git.iko.soy/typeable/bloodhound
on master help sign in
WIP snapshot-restore API

Snapshots API partly implemented. First trivial test case is passing. This could get tricky as our test suite spans many versions. For instance, 1.7 is when patterns got introduced.

commit06819a6b44411bf54ed0e1349fdd2244db470353 authorMichael Xavier <michael@michaelxavier.net> dateTue 19 Jul 2016, 16:15 (10 years ago) parent26d4e54
6 files changed+291−10
bloodhound.cabal+3−1 src/Database/Bloodhound/Client.hs+79−2 src/Database/Bloodhound/Types.hs+191−4 src/Database/Bloodhound/Types/Internal.hs+2−2 stack-7.10.yaml+1 tests/tests.hs+15−1
bloodhound.cabal+3−1
@@ -73,7 +73,9 @@ test-suite tests
7373 mtl,
7474 quickcheck-properties,
7575 derive,
76− errors
76+ errors,
77+ resourcet,
78+ temporary-resourcet
7779 default-language: Haskell2010
7880
7981test-suite doctests
src/Database/Bloodhound/Client.hs+79−2
@@ -59,6 +59,10 @@ module Database.Bloodhound.Client
5959 , mkShardCount
6060 , mkReplicaCount
6161 , getStatus
62+ , getSnapshotRepos
63+ , updateSnapshotRepo
64+ , verifySnapshotRepo
65+ , deleteSnapshotRepo
6266 , encodeBulkOperations
6367 , encodeBulkOperation
6468 -- * Authentication
@@ -72,7 +76,7 @@ module Database.Bloodhound.Client
7276 where
7377
7478import qualified Blaze.ByteString.Builder as BB
75−import Control.Applicative
79+import Control.Applicative as A
7680import Control.Monad
7781import Control.Monad.Catch
7882import Control.Monad.IO.Class
@@ -183,7 +187,7 @@ dispatch :: MonadBH m
183187 -> m Reply
184188dispatch dMethod url body = do
185189 initReq <- liftIO $ parseUrl' url
186− reqHook <- bhRequestHook <$> getBHEnv
190+ reqHook <- bhRequestHook A.<$> getBHEnv
187191 let reqBody = RequestBodyLBS $ fromMaybe emptyBody body
188192 req <- liftIO $ reqHook $ setRequestIgnoreStatus $ initReq { method = dMethod
189193 , requestBody = reqBody }
@@ -260,6 +264,79 @@ getStatus = do
260264 return $ decode (responseBody response)
261265 where url = joinPath []
262266
267+-- | 'getSnapshotRepos' gets the definitions of a subset of the
268+-- defined snapshot repos.
269+getSnapshotRepos
270+ :: ( MonadBH m
271+ , MonadThrow m
272+ )
273+ => SnapshotRepoSelection
274+ -> m (Either EsError [GenericSnapshotRepo])
275+getSnapshotRepos sel = fmap (fmap unGSRs) . parseEsResponse =<< get =<< url
276+ where
277+ url = joinPath ["_snapshot", selectorSeg]
278+ selectorSeg = case sel of
279+ AllSnapshotRepos -> "_all"
280+ SnapshotRepoList (p :| ps) -> T.intercalate "," (renderPat <$> (p:ps))
281+ renderPat (RepoPattern t) = t
282+ renderPat (ExactRepo (SnapshotRepoName t)) = t
283+
284+
285+-- | Wrapper to extract the list of 'GenericSnapshotRepo' in the
286+-- format they're returned in
287+newtype GSRs = GSRs { unGSRs :: [GenericSnapshotRepo] }
288+
289+
290+instance FromJSON GSRs where
291+ parseJSON = withObject "Collection of GenericSnapshotRepo" parse
292+ where
293+ parse = fmap GSRs . mapM (uncurry go) . HM.toList
294+ go rawName = withObject "GenericSnapshotRepo" $ \o ->
295+ GenericSnapshotRepo (SnapshotRepoName rawName) <$> o .: "type"
296+ <*> o .: "settings"
297+
298+
299+-- | Create or update a snapshot repo
300+updateSnapshotRepo
301+ :: ( MonadBH m
302+ , SnapshotRepo repo
303+ )
304+ => SnapshotRepoUpdateSettings
305+ -- ^ Use 'defaultSnapshotRepoUpdateSettings' if unsure
306+ -> repo
307+ -> m Reply --TODO: more specific?
308+updateSnapshotRepo SnapshotRepoUpdateSettings {..} repo = bindM2 post url (return (Just body))
309+ where
310+ url = addQuery params <$> joinPath ["_snapshots", snapshotRepoName gSnapshotRepoName]
311+ params
312+ | repoUpdateVerify = []
313+ | otherwise = [("verify", Just "false")]
314+ body = encode $ object [ "types" .= gSnapshotRepoName
315+ , "settings" .= gSnapshotRepoSettings
316+ ]
317+ GenericSnapshotRepo {..} = toGSnapshotRepo repo
318+
319+
320+
321+-- | Verify if a snapshot repo is working.
322+verifySnapshotRepo
323+ :: ( MonadBH m
324+ , MonadThrow m
325+ )
326+ => SnapshotRepoName
327+ -> m (Either EsError SnapshotVerification)
328+verifySnapshotRepo (SnapshotRepoName n) =
329+ parseEsResponse =<< bindM2 post url (return Nothing)
330+ where
331+ url = joinPath ["_snapshot", n, "_verify"]
332+
333+
334+deleteSnapshotRepo :: MonadBH m => SnapshotRepoName -> m Reply
335+deleteSnapshotRepo (SnapshotRepoName n) = delete =<< url
336+ where
337+ url = joinPath ["_snapshot", n]
338+
339+
263340-- | 'createIndex' will create an index given a 'Server', 'IndexSettings', and an 'IndexName'.
264341--
265342-- >>> response <- runBH' $ createIndex defaultIndexSettings (IndexName "didimakeanindex")
src/Database/Bloodhound/Types.hs+191−4
@@ -5,10 +5,10 @@
55{-# LANGUAGE GeneralizedNewtypeDeriving #-}
66{-# LANGUAGE MultiParamTypeClasses #-}
77-- {-# LANGUAGE NoMonomorphismRestriction #-}
8+{-# LANGUAGE NamedFieldPuns #-}
89{-# LANGUAGE OverloadedStrings #-}
910{-# LANGUAGE RecordWildCards #-}
1011{-# LANGUAGE UndecidableInstances #-}
11−{-# LANGUAGE NamedFieldPuns #-}
1212
1313-------------------------------------------------------------------------------
1414-- |
@@ -69,6 +69,9 @@ module Database.Bloodhound.Types
6969 , AllocationPolicy(..)
7070 , ReplicaBounds(..)
7171 , Bytes(..)
72+ , gigabytes
73+ , megabytes
74+ , kilobytes
7275 , FSType(..)
7376 , InitialShardCount(..)
7477 , NodeAttrFilter(..)
@@ -235,6 +238,21 @@ module Database.Bloodhound.Types
235238 , CollectionMode(..)
236239 , TermOrder(..)
237240 , TermInclusion(..)
241+ , SnapshotRepoSelection(..)
242+ , GenericSnapshotRepo(..)
243+ , SnapshotRepo(..)
244+ , SnapshotRepoConversionError(..)
245+ , SnapshotRepoType(..)
246+ , GenericSnapshotRepoSettings(..)
247+ , SnapshotRepoUpdateSettings(..)
248+ , defaultSnapshotRepoUpdateSettings
249+ , SnapshotRepoName(..)
250+ , SnapshotRepoPattern(..)
251+ , SnapshotVerification(..)
252+ , SnapshotNodeVerification(..)
253+ , FullNodeId(..)
254+ , NodeName(..)
255+ , FsSnapshotRepo(..)
238256
239257 , Aggregation(..)
240258 , Aggregations
@@ -275,7 +293,7 @@ module Database.Bloodhound.Types
275293 , EsPassword(..)
276294 ) where
277295
278−import Control.Applicative
296+import Control.Applicative as A
279297import Control.Monad.Catch
280298import Control.Monad.Except
281299import Control.Monad.Reader
@@ -283,7 +301,7 @@ import Control.Monad.State
283301import Control.Monad.Writer
284302import Data.Aeson
285303import Data.Aeson.Types (Pair, Parser, emptyObject,
286− parseMaybe)
304+ parseEither, parseMaybe)
287305import qualified Data.ByteString.Lazy.Char8 as L
288306import Data.Char
289307import Data.Hashable (Hashable)
@@ -332,7 +350,7 @@ mkBHEnv s m = BHEnv s m return
332350newtype BH m a = BH {
333351 unBH :: ReaderT BHEnv m a
334352 } deriving ( Functor
335− , Applicative
353+ , A.Applicative
336354 , Monad
337355 , MonadIO
338356 , MonadState s
@@ -483,8 +501,31 @@ data ReplicaBounds = ReplicasBounded Int Int
483501 | ReplicasUnbounded
484502 deriving (Eq, Read, Show, Generic, Typeable)
485503
504+-- | A measure of bytes used for various configurations. You may want
505+-- to use smart constructors like 'gigabytes' for larger values.
506+--
507+-- >>> gigabytes 9
508+-- Bytes 9000000000
509+--
510+-- >>> megabytes 9
511+-- Bytes 9000000
512+--
513+-- >>> kilobytes 9
514+-- Bytes 9000
486515newtype Bytes = Bytes Int deriving (Eq, Read, Show, Generic, Typeable, Ord, ToJSON, FromJSON)
487516
517+gigabytes :: Int -> Bytes
518+gigabytes n = megabytes (1000 * n)
519+
520+
521+megabytes :: Int -> Bytes
522+megabytes n = kilobytes (1000 * n)
523+
524+
525+kilobytes :: Int -> Bytes
526+kilobytes n = Bytes (1000 * n)
527+
528+
488529data FSType = FSSimple
489530 | FSBuffered deriving (Eq, Read, Show, Generic, Typeable, Ord)
490531
@@ -3625,3 +3666,149 @@ newtype EsUsername = EsUsername { esUsername :: Text } deriving (Read, Show, Eq)
36253666
36263667-- | Password type used for HTTP Basic authentication. See 'basicAuthHook'.
36273668newtype EsPassword = EsPassword { esPassword :: Text } deriving (Read, Show, Eq)
3669+
3670+
3671+data SnapshotRepoSelection = SnapshotRepoList (NonEmpty SnapshotRepoPattern)
3672+ | AllSnapshotRepos deriving (Eq, Generic, Show, Typeable)
3673+
3674+
3675+-- | Either specifies an exact repo name or one with globs in it,
3676+-- e.g. @RepoPattern "foo*"@ __NOTE__: Patterns are not supported on ES < 1.7
3677+data SnapshotRepoPattern = ExactRepo SnapshotRepoName
3678+ | RepoPattern Text
3679+ deriving (Eq, Generic, Show, Typeable)
3680+
3681+-- | The unique name of a snapshot repository.
3682+newtype SnapshotRepoName = SnapshotRepoName { snapshotRepoName :: Text }
3683+ deriving (Eq, Generic, Show, Typeable, ToJSON, FromJSON)
3684+
3685+
3686+-- | A generic representation of a snapshot repo. This is what gets
3687+-- sent to and parsed from the server. For repo types enabled by
3688+-- plugins that aren't exported by this library, consider making a
3689+-- custom type which implements 'SnapshotRepo'. If it is a common repo
3690+-- type, consider submitting a pull request to have it included in the
3691+-- library proper
3692+data GenericSnapshotRepo = GenericSnapshotRepo {
3693+ gSnapshotRepoName :: SnapshotRepoName
3694+ , gSnapshotRepoType :: SnapshotRepoType
3695+ , gSnapshotRepoSettings :: GenericSnapshotRepoSettings
3696+ } deriving (Eq, Generic, Show, Typeable)
3697+
3698+
3699+newtype SnapshotRepoType = SnapshotRepoType { snapshotRepoType :: Text }
3700+ deriving (Eq, Generic, Show, Typeable, ToJSON, FromJSON)
3701+
3702+
3703+-- | Opaque representation of snapshot repo settings. Instances of
3704+-- 'SnapshotRepo' will produce this.
3705+newtype GenericSnapshotRepoSettings = GenericSnapshotRepoSettings { gSnapshotRepoSettingsObject :: Object }
3706+ deriving (Eq, Generic, Show, Typeable, ToJSON, FromJSON)
3707+
3708+
3709+-- | The result of running 'verifySnapshotRepo'. --TODO: more detail once you know what a failure looks like
3710+newtype SnapshotVerification = SnapshotVerification { snapshotNodeVerifications :: [SnapshotNodeVerification] }
3711+ deriving (Eq, Generic, Show, Typeable)
3712+
3713+
3714+instance FromJSON SnapshotVerification where
3715+ parseJSON = withObject "SnapshotVerification" parse
3716+ where
3717+ parse o = do
3718+ o2 <- o .: "nodes"
3719+ SnapshotVerification <$> mapM (uncurry parse') (HM.toList o2)
3720+ parse' rawFullId = withObject "SnapshotNodeVerification" $ \o ->
3721+ SnapshotNodeVerification (FullNodeId rawFullId) <$> o .: "name"
3722+
3723+
3724+data SnapshotNodeVerification = SnapshotNodeVerification {
3725+ snvFullId :: FullNodeId
3726+ , snvNodeName :: NodeName
3727+ } deriving (Eq, Generic, Show, Typeable)
3728+
3729+
3730+-- | Unique, automatically-generated name assigned to nodes that are
3731+-- usually returned in node-oriented APIs.
3732+newtype FullNodeId = FullNodeId { fullNodeId :: Text }
3733+ deriving (Eq, Generic, Show, Typeable, FromJSON)
3734+
3735+
3736+newtype NodeName = NodeName { nodeName :: Text }
3737+ deriving (Eq, Generic, Show, Typeable, FromJSON)
3738+
3739+
3740+data SnapshotRepoUpdateSettings = SnapshotRepoUpdateSettings {
3741+ repoUpdateVerify :: Bool
3742+ -- ^ After creation/update, synchronously check that nodes can
3743+ -- write to this repo. Defaults to True. You may use False if you
3744+ -- need a faster response and plan on verifying manually later
3745+ -- with 'verifySnapshotRepo'.
3746+ } deriving (Eq, Show, Generic, Typeable)
3747+
3748+
3749+-- | Reasonable defaults for repo creation/update
3750+--
3751+-- * repoUpdateVerify True
3752+defaultSnapshotRepoUpdateSettings :: SnapshotRepoUpdateSettings
3753+defaultSnapshotRepoUpdateSettings = SnapshotRepoUpdateSettings True
3754+
3755+
3756+-- | A filesystem-based snapshot repo that ships with elasticsearch.
3757+data FsSnapshotRepo = FsSnapshotRepo {
3758+ fsrName :: SnapshotRepoName
3759+ , fsrLocation :: FilePath
3760+ , fsrCompressMetadata :: Bool
3761+ , fsrChunkSize :: Maybe Bytes
3762+ , fsrMaxRestoreBytesPerSec :: Maybe Bytes
3763+ , fsrMaxSnapshotBytesPerSec :: Maybe Bytes
3764+ } deriving (Eq, Generic, Show, Typeable)
3765+
3766+
3767+instance SnapshotRepo FsSnapshotRepo where
3768+ toGSnapshotRepo FsSnapshotRepo {..} =
3769+ GenericSnapshotRepo fsrName fsRepoType (GenericSnapshotRepoSettings settings)
3770+ where
3771+ Object settings = object $ [ "location" .= fsrLocation
3772+ , "compress" .= fsrCompressMetadata
3773+ ] ++ optionalPairs
3774+ optionalPairs = catMaybes [ ("chunk_size" .=) <$> fsrChunkSize
3775+ , ("max_restore_bytes_per_sec" .=) <$> fsrMaxRestoreBytesPerSec
3776+ , ("max_snapshot_bytes_per_sec" .=) <$> fsrMaxSnapshotBytesPerSec
3777+ ]
3778+ fromGSnapshotRepo GenericSnapshotRepo {..}
3779+ | gSnapshotRepoType == fsRepoType = do
3780+ let o = gSnapshotRepoSettingsObject gSnapshotRepoSettings
3781+ parseRepo $ do
3782+ FsSnapshotRepo gSnapshotRepoName <$> o .: "location"
3783+ <*> o .:? "compress" .!= False
3784+ <*> o .:? "chunk_size"
3785+ <*> o .:? "max_restore_bytes_per_sec"
3786+ <*> o .:? "max_snapshot_bytes_per_sec"
3787+ | otherwise = Left (RepoTypeMismatch fsRepoType gSnapshotRepoType)
3788+
3789+
3790+--TODO: test what error case looks like
3791+parseRepo :: Parser a -> Either SnapshotRepoConversionError a
3792+parseRepo parser = case parseEither (const parser) () of
3793+ Left e -> Left (OtherRepoConversionError (T.pack e))
3794+ Right a -> Right a
3795+
3796+
3797+fsRepoType :: SnapshotRepoType
3798+fsRepoType = SnapshotRepoType "fs"
3799+
3800+-- | Law: fromGSnapshotRepo (toGSnapshotRepo r) == Right r
3801+class SnapshotRepo r where
3802+ toGSnapshotRepo :: r -> GenericSnapshotRepo
3803+ fromGSnapshotRepo :: GenericSnapshotRepo -> Either SnapshotRepoConversionError r
3804+
3805+
3806+data SnapshotRepoConversionError = RepoTypeMismatch SnapshotRepoType
3807+ -- ^ Expected type
3808+ SnapshotRepoType
3809+ -- ^ Actual type
3810+ | OtherRepoConversionError Text
3811+ deriving (Show, Eq, Generic, Typeable)
3812+
3813+
3814+instance Exception SnapshotRepoConversionError
src/Database/Bloodhound/Types/Internal.hs+2−2
@@ -22,7 +22,7 @@ module Database.Bloodhound.Types.Internal
2222 ) where
2323
2424
25−import Control.Applicative
25+import Control.Applicative as A
2626import Control.Monad.Reader
2727import Data.Text (Text)
2828import Data.Typeable (Typeable)
@@ -51,6 +51,6 @@ newtype Server = Server Text deriving (Eq, Show, Generic, Typeable)
5151 own monad transformer stack. A default instance for a ReaderT and
5252 alias 'BH' is provided for the simple case.
5353-}
54−class (Functor m, Applicative m, MonadIO m) => MonadBH m where
54+class (Functor m, A.Applicative m, MonadIO m) => MonadBH m where
5555 getBHEnv :: m BHEnv
5656
stack-7.10.yaml+1
@@ -13,4 +13,5 @@ extra-deps:
1313- quickcheck-properties-0.1
1414- semigroups-0.18.0.1
1515- uri-bytestring-0.1.9
16+- temporary-resourcet-0.1.0.0
1617resolver: lts-5.1
tests/tests.hs+15−1
@@ -14,6 +14,7 @@ import Control.Error
1414import Control.Exception
1515import Control.Monad
1616import Control.Monad.Reader
17+import Control.Monad.Trans.Resource
1718import Data.Aeson
1819import Data.Aeson.Types (parseEither)
1920import qualified Data.ByteString.Lazy.Char8 as BL8
@@ -38,6 +39,7 @@ import GHC.Generics as G
3839import Network.HTTP.Client hiding (Proxy)
3940import qualified Network.HTTP.Types.Status as NHTS
4041import Prelude hiding (filter)
42+import System.IO.Temp
4143import Test.Hspec
4244import Test.QuickCheck.Property.Monoid (T (..), eq, prop_Monoid)
4345
@@ -773,7 +775,8 @@ $(derive makeArbitrary ''AllocationPolicy)
773775$(derive makeArbitrary ''InitialShardCount)
774776$(derive makeArbitrary ''FSType)
775777$(derive makeArbitrary ''CompoundFormat)
776−
778+$(derive makeArbitrary ''FsSnapshotRepo)
779+$(derive makeArbitrary ''SnapshotRepoName)
777780
778781main :: IO ()
779782main = hspec $ do
@@ -1365,6 +1368,17 @@ main = hspec $ do
13651368 (dv <= maxBound) .&&.
13661369 docVersionNumber dv === i
13671370
1371+ describe "FsSnapshotRepo" $ do
1372+ prop "SnapshotRepo laws" $ \fsr ->
1373+ fromGSnapshotRepo (toGSnapshotRepo fsr) === Right (fsr :: FsSnapshotRepo)
1374+
1375+ describe "snapshot repos" $ do
1376+ it "always parses all snapshots API" $ withTestEnv $ do
1377+ res <- getSnapshotRepos AllSnapshotRepos
1378+ liftIO $ case res of
1379+ Left e -> expectationFailure ("Expected a right but got Left " <> show e)
1380+ Right _ -> return ()
1381+
13681382 describe "Enum DocVersion" $ do
13691383 it "follows the laws of Enum, Bounded" $ do
13701384 evaluate (succ maxBound :: DocVersion) `shouldThrow` anyErrorCall