packages feed

powerdns (empty) → 0.1

raw patch · 13 files changed

+1445/−0 lines, 13 filesdep +aesondep +basedep +base64-bytestringsetup-changed

Dependencies added: aeson, base, base64-bytestring, bytestring, case-insensitive, containers, deepseq, http-client, powerdns, servant, servant-client, servant-client-core, tasty, tasty-hunit, text, time

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for powerdns-api++## 0.1 -- 2021-05-28++* Initial release
+ LICENSE view
@@ -0,0 +1,11 @@+Copyright (c) 2021 Victor Nawothnig++Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.++3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ PowerDNS/API.hs view
@@ -0,0 +1,46 @@+-- |+-- Module: PowerDNS.API+-- Description: Servant based wrapper for PowerDNS.+--+-- This module exports a complete servant API description of the PowerDNS. May be useful to some.++{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE DeriveGeneric #-}+module PowerDNS.API+  ( API+  , api+  , PowerDNS(..)+  , module PowerDNS.API.Zones+  , module PowerDNS.API.Servers+  , module PowerDNS.API.Cryptokeys+  , module PowerDNS.API.Metadata+  , module PowerDNS.API.TSIGKeys+  )+where++import           GHC.Generics (Generic)+import           Data.Proxy (Proxy(..))++import           Servant.API+import           Servant.API.Generic ((:-), ToServantApi)++import           PowerDNS.API.Zones+import           PowerDNS.API.Servers+import           PowerDNS.API.Cryptokeys+import           PowerDNS.API.Metadata+import           PowerDNS.API.TSIGKeys++api :: Proxy API+api = Proxy++type API = "api" :> "v1" :> ToServantApi PowerDNS++data PowerDNS f = PowerDNS+  { servers    :: f :- ToServantApi ServersAPI+  , zones      :: f :- ToServantApi ZonesAPI+  , cryptokeys :: f :- ToServantApi CryptokeysAPI+  , metadata   :: f :- ToServantApi MetadataAPI+  , tsigkeys   :: f :- ToServantApi TSIGKeysAPI+  } deriving Generic+
+ PowerDNS/API/Cryptokeys.hs view
@@ -0,0 +1,73 @@+-- |+-- Module: PowerDNS.API.Cryptokeys+-- Description: Cryptokeys endpoints for PowerDNS API+--+-- This module implements the endpoints described at [Cryptokeys API](https://doc.powerdns.com/authoritative/http-api/cryptokey.html)++{-# LANGUAGE DataKinds          #-}+{-# LANGUAGE DeriveAnyClass     #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveGeneric      #-}+{-# LANGUAGE TypeOperators      #-}+module PowerDNS.API.Cryptokeys+  (+  -- * API+    CryptokeysAPI(..)++  -- * Data types+  , Cryptokey(..)+  )+where++import           Data.Data (Data)++import           Control.DeepSeq (NFData)+import           Data.Aeson (FromJSON(..), ToJSON(..), defaultOptions,+                             fieldLabelModifier, genericParseJSON,+                             genericToJSON)+import qualified Data.Text as T+import           Servant.API+import           Servant.API.Generic++import           PowerDNS.Internal.Utils (Empty(..), strip)++data CryptokeysAPI f = Cryptokeys+    { apiListCryptokeys :: f  :- "servers" :> Capture "server_id" T.Text :> "zones" :> Capture "zone_id" T.Text :> "cryptokeys"+                              :> Get '[JSON] [Cryptokey]++    , apiCreateCryptokey :: f :- "servers" :> Capture "server_id" T.Text :> "zones" :> Capture "zone_id" T.Text :> "cryptokeys"+                              :> ReqBody '[JSON] Cryptokey+                              :> PostCreated '[JSON] Cryptokey++    , apiGetCryptokey :: f    :- "servers" :> Capture "server_id" T.Text :> "zones" :> Capture "zone_id" T.Text :> "cryptokeys" :> Capture "cryptokey_id" T.Text+                              :> Get '[JSON] Cryptokey++    , apiUpdateCryptokey :: f :- "servers" :> Capture "server_id" T.Text :> "zones" :> Capture "zone_id" T.Text :> "cryptokeys" :> Capture "cryptokey_id" T.Text+                              :> ReqBody '[JSON] Cryptokey+                              :> PutNoContent++    , apiDeleteCryptokey :: f :- "servers" :> Capture "server_id" T.Text :> "zones" :> Capture "zone_id" T.Text :> "cryptokeys" :> Capture "cryptokey_id" T.Text+                              :> DeleteNoContent++    } deriving Generic++----------------------------------------------------------------------------------------++data Cryptokey = Cryptokey+    { ck_type :: Maybe T.Text+    , ck_id :: Maybe Integer+    , ck_keytype :: Maybe T.Text+    , ck_active :: Maybe Bool+    , ck_published :: Maybe Bool+    , ck_dnskey :: Maybe T.Text+    , ck_ds :: Maybe [T.Text]+    , ck_privatekey :: Maybe T.Text+    , ck_algorithm :: Maybe T.Text+    , ck_bits :: Maybe Integer+    } deriving (Eq, Ord, Show, Generic, NFData, Data, Empty)++instance ToJSON Cryptokey where+  toJSON = genericToJSON defaultOptions { fieldLabelModifier = strip "ck_"}++instance FromJSON Cryptokey where+  parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = strip "ck_"}
+ PowerDNS/API/Metadata.hs view
@@ -0,0 +1,63 @@+-- |+-- Module: PowerDNS.API.Metadata+-- Description: Metadata endpoints for PowerDNS API+--+-- This module implements the endpoints described at [Metadata API](https://doc.powerdns.com/authoritative/http-api/metadata.html#)++{-# LANGUAGE DataKinds          #-}+{-# LANGUAGE DeriveAnyClass     #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveGeneric      #-}+{-# LANGUAGE TypeOperators      #-}+module PowerDNS.API.Metadata+  (+  -- * API+    MetadataAPI(..)++  -- * Data types+  , Metadata(..)+  )+where++import           Control.DeepSeq (NFData)+import           Data.Aeson ( FromJSON(..), ToJSON(..)+                            , defaultOptions, fieldLabelModifier+                            , genericParseJSON, genericToJSON+                            )++import           Data.Data (Data)+import qualified Data.Text as T+import           Servant.API+import           Servant.API.Generic++import           PowerDNS.Internal.Utils (strip)++data MetadataAPI f = MetadataAPI+  { apiListMetadata   :: f :- "servers" :> Capture "server_id" T.Text :> "zones" :> Capture "zone_id" T.Text :> "metadata"+                           :> Get '[JSON] [Metadata]++  , apiCreateMetadata :: f :- "servers" :> Capture "server_id" T.Text :> "zones" :> Capture "zone_id" T.Text :> "metadata"+                           :> ReqBody '[JSON] Metadata+                           :> PostNoContent++  , apiGetMetadata    :: f :- "servers" :> Capture "server_id" T.Text :> "zones" :> Capture "zone_id" T.Text :> "metadata" :> Capture "kind" T.Text+                           :> Get '[JSON] Metadata++  , apiUpdateMetadata    :: f :- "servers" :> Capture "server_id" T.Text :> "zones" :> Capture "zone_id" T.Text :> "metadata" :> Capture "kind" T.Text+                           :> ReqBody '[JSON] Metadata+                           :> Put '[JSON] Metadata++  , apiDeleteMetadata    :: f :- "servers" :> Capture "server_id" T.Text :> "zones" :> Capture "zone_id" T.Text :> "metadata" :> Capture "kind" T.Text+                           :> DeleteNoContent+  } deriving Generic++data Metadata = Metadata+  { md_kind :: T.Text+  , md_metadata :: [T.Text]+  } deriving (Eq, Ord, Show, Generic, NFData, Data)++instance ToJSON Metadata where+  toJSON = genericToJSON defaultOptions { fieldLabelModifier = strip "md_"}++instance FromJSON Metadata where+  parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = strip "md_"}
+ PowerDNS/API/Servers.hs view
@@ -0,0 +1,215 @@+-- |+-- Module: PowerDNS.API.Servers+-- Description: Servers endpoints for PowerDNS API+--+-- This module implements the endpoints described at [Servers API](https://doc.powerdns.com/authoritative/http-api/server.html)++{-# LANGUAGE DataKinds          #-}+{-# LANGUAGE DeriveAnyClass     #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveGeneric      #-}+{-# LANGUAGE TypeOperators      #-}+{-# LANGUAGE OverloadedStrings  #-}+module PowerDNS.API.Servers+  (+  -- * API+    ServersAPI(..)++  -- * Data types+  , Server(..)+  , ObjectType(..)+  , SearchResult(..)+  , CacheFlushResult(..)+  , AnyStatisticItem(..)+  , StatisticItem(..)+  , MapStatisticItem(..)+  , RingStatisticItem(..)+  , SimpleStatisticItem(..)+  )+where+++import           Data.Char (toLower)+import           Data.Data (Data)+import           Text.Read (readMaybe)++import           Control.DeepSeq (NFData)+import           Data.Aeson (FromJSON(..), ToJSON(..), defaultOptions+                            , Value(String)+                            , fieldLabelModifier, genericParseJSON+                            , allNullaryToStringTag+                            , genericToJSON+                            , (.:), (.=)+                            , withObject, object+                            )+import           Data.Aeson.Types (Parser)+import qualified Data.Text as T+import           Servant.API+import           Servant.API.Generic++import           PowerDNS.Internal.Utils (Empty(..), strip)++----------------------------------------------------------------------------------------++type QueryParamReq = QueryParam' [Required, Strict]+data ServersAPI f = ServersAPI+  { apiListServers :: f :- "servers"+                        :> Get '[JSON] [Server]++  , apiGetServer   :: f :- "servers" :> Capture "server_id" T.Text+                        :> Get '[JSON] Server++  , apiSearch      :: f :- "servers" :> Capture "server_id" T.Text :> "search-data"+                        :> QueryParamReq "q" T.Text+                        :> QueryParamReq "max" Integer+                        :> QueryParam "object_type" ObjectType+                        :> Get '[JSON] [SearchResult]++  , apiFlushCache  :: f :- "servers" :> Capture "server_id" T.Text+                        :> QueryParamReq "domain" T.Text+                        :> Put '[JSON] CacheFlushResult++  , apiStatistics  :: f :- "servers" :> Capture "server_id" T.Text :> "statistics"+                        :> QueryParam "statistic" T.Text+                        :> QueryParam "includerings" Bool+                        :> Get '[JSON] [AnyStatisticItem]+  } deriving Generic++----------------------------------------------------------------------------------------++data Server = Server+  { server_type :: Maybe T.Text+  , server_id :: Maybe T.Text+  , server_daemon_type :: Maybe T.Text+  , server_version :: Maybe T.Text+  , server_url :: Maybe T.Text+  , server_config_url :: Maybe T.Text+  , server_zones_url :: Maybe T.Text+  } deriving (Eq, Ord, Show, Generic, NFData, Data, Empty)++instance ToJSON Server where+  toJSON = genericToJSON defaultOptions { fieldLabelModifier = strip "server_"}++instance FromJSON Server where+  parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = strip "server_"}++----------------------------------------------------------------------------------------++data SearchResult = SearchResult+  { sr_content :: T.Text+  , sr_disabled :: Bool+  , sr_name :: T.Text+  , sr_object_type :: ObjectType+  , sr_zone_id :: T.Text+  , sr_zone :: T.Text+  , sr_type :: T.Text+  , sr_ttl :: Integer+  } deriving (Eq, Ord, Show, Generic, NFData, Data)++instance ToJSON SearchResult where+  toJSON = genericToJSON defaultOptions { fieldLabelModifier = strip "sr_"}++instance FromJSON SearchResult where+  parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = strip "sr_"}++----------------------------------------------------------------------------------------+data ObjectType = TyAll+                | TyZone+                | TyRecord+                | TyComment+                deriving (Eq, Ord, Show, Generic, NFData, Data)++instance ToJSON ObjectType where+  toJSON = genericToJSON defaultOptions { fieldLabelModifier = fmap toLower . strip "Ty"+                                        , allNullaryToStringTag = True }++instance FromJSON ObjectType where+  parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = fmap toLower . strip "Ty"+                                              , allNullaryToStringTag = True }++instance ToHttpApiData ObjectType where+  toQueryParam TyAll = "all"+  toQueryParam TyZone = "zone"+  toQueryParam TyRecord = "all"+  toQueryParam TyComment = "comment"++----------------------------------------------------------------------------------------++data CacheFlushResult = CacheFlushResult+  { cfr_count :: Integer+  , cfr_result :: T.Text+  } deriving (Eq, Ord, Show, Generic, NFData, Data)++instance ToJSON CacheFlushResult where+  toJSON = genericToJSON defaultOptions { fieldLabelModifier = strip "cfr_"}++instance FromJSON CacheFlushResult where+  parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = strip "cfr_"}++----------------------------------------------------------------------------------------++data AnyStatisticItem = AnyStatisticItem StatisticItem+                       | AnyMapStatisticItem MapStatisticItem+                       | AnyRingStatisticItem RingStatisticItem+                       deriving (Eq, Ord, Show, Generic, NFData, Data)++instance ToJSON AnyStatisticItem where+  toJSON (AnyStatisticItem si) = object [ "type" .= String "StatisticItem"+                                        , "name" .= si_name si+                                        , "value" .= si_value si ]+  toJSON (AnyMapStatisticItem si) = object [ "type" .= String "MapStatisticItem"+                                           , "name" .= msi_name si+                                           , "value" .= msi_value si ]+  toJSON (AnyRingStatisticItem si) = object [ "type" .= String "RingStatisticItem"+                                            , "name" .= rsi_name si+                                            , "size" .= String (showT (rsi_size si))+                                            , "value" .= rsi_value si ]++showT :: Show a => a -> T.Text+showT = T.pack . show++thruRead :: Read a => T.Text -> Parser a+thruRead = maybe (fail "failed to parse") pure . readMaybe . T.unpack++instance FromJSON AnyStatisticItem where+  parseJSON = withObject "Any StatisticItem" $ \o -> do+    r <- o .: "type"+    case r of+      "StatisticItem" -> fmap AnyStatisticItem $+                         StatisticItem <$> o .: "name"+                                       <*> o .: "value"+      "MapStatisticItem" -> fmap AnyMapStatisticItem $+                            MapStatisticItem <$> o .: "name"+                                             <*> o .: "value"+      "RingStatisticItem" -> fmap AnyRingStatisticItem $+                             RingStatisticItem <$> o .: "name"+                                               <*> (o .: "size" >>= thruRead)+                                               <*> o .: "value"+      _ -> fail ("Unknown type: " <> T.unpack r)++data StatisticItem = StatisticItem+  { si_name :: T.Text+  , si_value :: T.Text+  } deriving (Eq, Ord, Show, Generic, NFData, Data)++data MapStatisticItem = MapStatisticItem+  { msi_name :: T.Text+  , msi_value :: [SimpleStatisticItem]+  } deriving (Eq, Ord, Show, Generic, NFData, Data)++data RingStatisticItem = RingStatisticItem+  { rsi_name :: T.Text+  , rsi_size :: Integer+  , rsi_value :: [SimpleStatisticItem]+  } deriving (Eq, Ord, Show, Generic, NFData, Data)++data SimpleStatisticItem = SimpleStatisticItem+  { ssi_name :: T.Text+  , ssi_value :: T.Text+  } deriving (Eq, Ord, Show, Generic, NFData, Data)++instance ToJSON SimpleStatisticItem where+  toJSON = genericToJSON defaultOptions { fieldLabelModifier = strip "ssi_"}++instance FromJSON SimpleStatisticItem where+  parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = strip "ssi_"}
+ PowerDNS/API/TSIGKeys.hs view
@@ -0,0 +1,124 @@+-- |+-- Module: PowerDNS.API.TSIGKeys+-- Description: TSIGKeys endpoints for PowerDNS API+--+-- Implementation of the API endpoints described at [PowerDNS TSIGKeys API](https://doc.powerdns.com/authoritative/http-api/tsigkey.html)++{-# LANGUAGE DataKinds          #-}+{-# LANGUAGE DeriveAnyClass     #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveGeneric      #-}+{-# LANGUAGE RecordWildCards    #-}+{-# LANGUAGE OverloadedStrings  #-}+{-# LANGUAGE TypeOperators      #-}+{-# LANGUAGE CPP                #-}+module PowerDNS.API.TSIGKeys+  (+  -- * API+    TSIGKeysAPI(..)++  -- * Data types+  , TSIGKey(..)+  , TSIGAlgorithm(..)+  )+where++import           Data.Char (toLower)+import           Data.Functor ((<&>))+import           Data.Data (Data)+#if !(MIN_VERSION_base(4,11,0))+import           Control.Monad.Fail (MonadFail)+#endif++import           Control.DeepSeq (NFData)+import           Data.Aeson ( FromJSON(..), ToJSON(..), Value(String), allNullaryToStringTag+                            , constructorTagModifier, defaultOptions+                            , genericParseJSON+                            , genericToJSON, withObject, object+                            , (.:), (.:?), (.=)+                            )+import qualified Data.ByteString as BS+import qualified Data.ByteString.Base64 as BS64+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import           Servant.API+import           Servant.API.Generic++---------------------------------------------------------------------------------------++data TSIGKeysAPI f = TSIGKeysAPI+  { apiListTSIGKeys  :: f :- "servers" :> Capture "server_id" T.Text :> "tsigkeys"+                          :> Get '[JSON] [TSIGKey]++  , apiCreateTSIGKey :: f :- "servers" :> Capture "server_id" T.Text :> "tsigkeys"+                          :> ReqBody '[JSON] TSIGKey+                          :> PostCreated '[JSON] TSIGKey++  , apiGetTSIGKey    :: f :- "servers" :> Capture "server_id" T.Text :> "tsigkeys" :> Capture "tsigkey_id" T.Text+                          :> Get '[JSON] TSIGKey++  , apiUpdateTSIGKey :: f :- "servers" :> Capture "server_id" T.Text :> "tsigkeys" :> Capture "tsigkey_id" T.Text+                          :> ReqBody '[JSON] TSIGKey+                          :> Put '[JSON] TSIGKey++  , apiDeleteTSIGKey :: f :- "servers" :> Capture "server_id" T.Text :> "tsigkeys" :> Capture "tsigkey_id" T.Text+                          :> DeleteNoContent+  } deriving Generic++----------------------------------------------------------------------------------------++data TSIGKey = TSIGKey+  { tsk_name :: T.Text+  , tsk_id :: T.Text+  , tsk_algorithm :: Maybe TSIGAlgorithm+  , tsk_key :: Maybe BS.ByteString+  -- ^ Unlike the original PowerDNS API we do not require the key to be base64 encoded.+  } deriving (Eq, Ord, Show, Generic, NFData, Data)++instance ToJSON TSIGKey where+  toJSON key = object [ "name" .= tsk_name key+                      , "id" .= tsk_id key+                      , "type" .= String "TSIGKey"+                      , "algorithm" .= tsk_algorithm key+                      , "key" .= (tsk_key key <&> encode64)+                      ]++instance FromJSON TSIGKey where+  parseJSON = withObject "TSIGKey" $ \o -> do+    tsk_name      <- o .: "name"+    tsk_id        <- o .: "id"+    tsk_algorithm <- o .:? "algorithm"+    tsk_key       <- o .:? "key" >>= traverse decode64+    pure TSIGKey{..}++encode64 :: BS.ByteString -> T.Text+encode64 = T.decodeUtf8 . BS64.encode++decode64 :: MonadFail m => T.Text -> m BS.ByteString+decode64 i = case BS64.decode (T.encodeUtf8 i) of+  Left err -> fail err+  Right k  -> pure k  ++----------------------------------------------------------------------------------------++-- | Supported algorithms according to [PowerDNS TSIG Documentation](https://doc.powerdns.com/authoritative/tsig.html#tsig)+data TSIGAlgorithm = HMAC_MD5+                   | HMAC_SHA1+                   | HMAC_SHA224+                   | HMAC_SHA256+                   | HMAC_SHA384+                   | HMAC_SHA512+                   deriving (Eq, Ord, Show, Generic, NFData, Data)++instance ToJSON TSIGAlgorithm where+  toJSON = genericToJSON defaultOptions { allNullaryToStringTag = True+                                        , constructorTagModifier = algo+                                        }++instance FromJSON TSIGAlgorithm where+  parseJSON = genericParseJSON defaultOptions { allNullaryToStringTag = True+                                              , constructorTagModifier = algo+                                              }++algo :: String -> String+algo = fmap (\c -> if c == '_' then '-' else toLower c)
+ PowerDNS/API/Zones.hs view
@@ -0,0 +1,271 @@+-- |+-- Module: PowerDNS.API.Zones+-- Description: Zones endpoints for PowerDNS API+--+-- Implementation of the API endpoints described at [PowerDNS Zones API](https://doc.powerdns.com/authoritative/http-api/zone.html)++{-# LANGUAGE DataKinds          #-}+{-# LANGUAGE DeriveAnyClass     #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveGeneric      #-}+{-# LANGUAGE TypeOperators      #-}+module PowerDNS.API.Zones+  (+  -- * API+    ZonesAPI(..)++  -- * Data types+  , Zone(..)+  , Kind(..)+  , RRSets(..)+  , RRSet(..)+  , Record(..)+  , Comment(..)+  , ChangeType(..)+  , RecordType(..)+  )+where++import           Data.Char (toUpper)+import           Data.Data (Data)+import           Data.Word (Word32)++import           Control.DeepSeq (NFData)+import           Data.Aeson (FromJSON(..), ToJSON(..), allNullaryToStringTag,+                             defaultOptions, fieldLabelModifier,+                             genericParseJSON, genericToJSON, omitNothingFields)+import qualified Data.Text as T+import           Data.Time.Clock.POSIX (POSIXTime)+import           Servant.API+import           Servant.API.Generic++import           PowerDNS.Internal.Utils (Empty(..), strip)++----------------------------------------------------------------------------------------++data ZonesAPI f = ZonesAPI+  { apiListZones     :: f :- "servers" :> Capture "server_id" T.Text :> "zones"+                          :> QueryParam "zone" T.Text+                          :> QueryParam "dnssec" Bool+                          :> Get '[JSON] [Zone]++  , apiCreateZone    :: f :- "servers" :> Capture "server_id" T.Text :> "zones"+                          :> QueryParam "rrset" Bool+                          :> ReqBody '[JSON] Zone+                          :> PostCreated '[JSON] Zone++  , apiGetZone       :: f :- "servers" :> Capture "server_id" T.Text :> "zones" :> Capture "zone_id" T.Text+                          :> QueryParam "rrsets" Bool+                          :> Get '[JSON] Zone++  , apiDeleteZone    :: f :- "servers" :> Capture "server_id" T.Text :> "zones" :> Capture "zone_id" T.Text+                          :> DeleteNoContent++  , apiUpdateRecords :: f :- "servers" :> Capture "server_id" T.Text :> "zones" :> Capture "zone_id" T.Text+                          :> ReqBody '[JSON] RRSets+                          :> PatchNoContent++  , apiUpdateZone    :: f :- "servers" :> Capture "server_id" T.Text :> "zones" :> Capture "zone_id" T.Text+                          :> ReqBody '[JSON] Zone+                          :> PutNoContent++  , apiTriggerAxfr   :: f :- "servers" :> Capture "server_id" T.Text :> "zones" :> Capture "zone_id" T.Text :> "axfr-retrieve"+                          :> Put '[JSON] NoContent++  , apiNotifySlaves  :: f :- "servers" :> Capture "server_id" T.Text :> "zones" :> Capture "zone_id" T.Text :> "notify"+                          :> Put '[JSON] NoContent++  , apiGetZoneAxfr   :: f :- "servers" :> Capture "server_id" T.Text :> "zones" :> Capture "zone_id" T.Text :> "export"+                          :> Get '[JSON] T.Text++  , apiRectifyZone   :: f :- "servers" :> Capture "server_id" T.Text :> "zones" :> Capture "zone_id" T.Text :> "rectify"+                          :> Put '[JSON] T.Text++  } deriving Generic+++----------------------------------------------------------------------------------------++-- | Zone according to [PowerDNS Documentation](https://doc.powerdns.com/authoritative/http-api/zone.html#zone).+-- All fields are optional because the PowerDNS API differs on which fields are required depending on the endpoint.+data Zone = Zone+  { zone_id :: Maybe T.Text+  , zone_name :: Maybe T.Text+  , zone_type :: Maybe T.Text+  , zone_url :: Maybe T.Text+  , zone_kind :: Maybe Kind+  , zone_rrsets :: Maybe [RRSet]+  , zone_serial :: Maybe Integer+  , zone_notified_serial :: Maybe Integer+  , zone_edited_serial :: Maybe Integer+  , zone_masters :: Maybe [T.Text]+  , zone_dnssec :: Maybe Bool+  , zone_nsec3param :: Maybe T.Text+  , zone_nsec3narrow :: Maybe Bool+  , zone_presigned :: Maybe Bool+  , zone_soa_edit :: Maybe T.Text+  , zone_soa_edit_api :: Maybe T.Text+  , zone_api_rectify :: Maybe Bool+  , zone_zone :: Maybe T.Text+  , zone_account :: Maybe T.Text+  , zone_nameservers :: Maybe [T.Text]+  , zone_master_tsig_key_ids :: Maybe [T.Text]+  , zone_slave_tsig_key_ids :: Maybe [T.Text]+  } deriving (Eq, Ord, Show, Generic, NFData, Data, Empty)++instance ToJSON Zone where+  toJSON = genericToJSON defaultOptions { fieldLabelModifier = strip "zone_"}++instance FromJSON Zone where+  parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = strip "zone_"}++----------------------------------------------------------------------------------------++data Kind = Native | Master | Slave+  deriving (Eq, Ord, Show, Generic, NFData, Data)++instance ToJSON Kind where+  toJSON = genericToJSON defaultOptions { fieldLabelModifier = fmap toUpper+                                        , allNullaryToStringTag = True }++instance FromJSON Kind where+  parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = fmap toUpper+                                              , allNullaryToStringTag = True }++----------------------------------------------------------------------------------------++-- | A list of RRSets+data RRSets = RRSets+  { rrsets :: [RRSet]+  } deriving (Eq, Ord, Show, Generic, NFData, Data)++instance ToJSON RRSets+instance FromJSON RRSets++----------------------------------------------------------------------------------------++-- | RRSet according to [PowerDNS Documentation](https://doc.powerdns.com/authoritative/http-api/zone.html#rrset).+data RRSet = RRSet+  { rrset_name :: T.Text+  , rrset_type :: RecordType+  , rrset_ttl :: Word32+  , rrset_changetype :: Maybe ChangeType+  , rrset_records :: Maybe [Record]+  , rrset_comments :: Maybe [Comment]+  } deriving (Eq, Ord, Show, Generic, NFData, Data)++instance ToJSON RRSet where+  toJSON = genericToJSON defaultOptions { fieldLabelModifier = strip "rrset_"+                                        , omitNothingFields = True }++instance FromJSON RRSet where+  parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = strip "rrset_"+                                              , omitNothingFields = True }++----------------------------------------------------------------------------------------++data RecordType = A+                | AAAA+                | AFSDB+                | ALIAS+                | APL+                | CAA+                | CERT+                | CDNSKEY+                | CDS+                | CNAME+                | DNSKEY+                | DNAME+                | DS+                | HINFO+                | KEY+                | LOC+                | MX+                | NAPTR+                | NS+                | NSEC+                | NSEC3+                | NSEC3PARAM+                | OPENPGPKEY+                | PTR+                | RP+                | RRSIG+                | SOA+                | SPF+                | SSHFP+                | SRV+                | TKEY+                | TSIG+                | TLSA+                | SMIMEA+                | TXT+                | URI+                | A6+                | DHCID+                | DLV+                | EUI48+                | EUI64+                | IPSECKEY+                | KX+                | MAILA+                | MAILB+                | MINFO+                | MR+                | RKEY+                | SIG+                | WKS+                deriving (Eq, Ord, Show, Generic, NFData, Data)++instance ToJSON RecordType where+  toJSON = genericToJSON defaultOptions { allNullaryToStringTag = True+                                        }++instance FromJSON RecordType where+  parseJSON = genericParseJSON defaultOptions { allNullaryToStringTag = True+                                              }++----------------------------------------------------------------------------------------++-- | Whether or not an 'RRSet' replace or delete an existing entry.+-- If the 'ChangeType' is left at @Nothing@ it will create a new domain entry.+data ChangeType = Replace+                | Delete+                deriving (Eq, Ord, Show, Generic, NFData, Data)++instance ToJSON ChangeType where+  toJSON = genericToJSON defaultOptions { fieldLabelModifier = fmap toUpper+                                        , allNullaryToStringTag = True }++instance FromJSON ChangeType where+  parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = fmap toUpper+                                              , allNullaryToStringTag = True }++----------------------------------------------------------------------------------------+-- | Record according to [PowerDNS Documentation](https://doc.powerdns.com/authoritative/http-api/zone.html#record)+data Record = Record+  { record_content :: T.Text+  , record_disabled :: Bool+  } deriving (Eq, Ord, Show, Generic, NFData, Data)++instance ToJSON Record where+  toJSON = genericToJSON defaultOptions { fieldLabelModifier = strip "record_"+                                        , omitNothingFields = True }++instance FromJSON Record where+  parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = strip "record_"+                                              , omitNothingFields = True }++----------------------------------------------------------------------------------------++-- | Comment according to [PowerDNS Documentation](https://doc.powerdns.com/authoritative/http-api/zone.html#comment)+data Comment = Comment+  { comment_content :: Maybe T.Text+  , comment_account :: Maybe T.Text+  , commant_modified_at :: Maybe POSIXTime+  } deriving (Eq, Ord, Show, Generic, NFData, Data, Empty)++instance ToJSON Comment where+  toJSON = genericToJSON defaultOptions { fieldLabelModifier = strip "comment_" }++instance FromJSON Comment where+  parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = strip "comment_" }
+ PowerDNS/Client.hs view
@@ -0,0 +1,354 @@+-- |+-- Module: PowerDNS.Client+-- Description: Servant client functions and utilities to interact with a PowerDNS Server+--+-- All the endpoints provide only a very slim wrapper around the PowerDNS API preserving+-- its idiosyncracies.++{-# LANGUAGE OverloadedStrings #-}+module PowerDNS.Client+  (+  -- * Authentication+    applyXApiKey++  -- * Zones+  --+  -- | See documentation at [PowerDNS Zones API](https://doc.powerdns.com/authoritative/http-api/zone.html)+  --+  -- Because the required fields of a 'Zone' differs between various requests and responses, every field+  -- is wrapped with Maybe. It is the users responsibility to check with the PowerDNS API to know when a+  -- field must be specified. For convenience 'empty' can be used to generate a value of 'Zone' with all+  -- fields set to 'Nothing'.+  --+  -- @+  --    let zone = empty { zone_name = Just "some.domain."+  --                     , zone_kind = Just Native+  --                     , zone_type = Just "zone" }+  --+  --    in createZone "localhost" -- Server ID+  --                  Nothing     -- Show RRsets in response+  --                  zone        -- The zone we are creating+  -- @+  , listZones+  , createZone+  , getZone+  , deleteZone+  , updateRecords+  , updateZone+  , triggerAxfr+  , notifySlaves+  , getZoneAxfr+  , rectifyZone+  -- ** Data types+  , Zone(..)+  , Kind(..)+  , RRSets(..)+  , RRSet(..)+  , Record(..)+  , Comment(..)+  , ChangeType(..)+  , RecordType(..)++  -- * Cryptokeys+  -- | See documentation at [PowerDNS Cryptokeys API](https://doc.powerdns.com/authoritative/http-api/cryptokey.html)+  , listCryptoKeys+  , createCryptokey+  , getCryptokey+  , updateCryptokey+  , deleteCryptokey+  -- ** Data types+  , Cryptokey++  -- * Servers+  -- | See documentation at [PowerDNS Servers API](https://doc.powerdns.com/authoritative/http-api/server.html)+  --+  -- Also contains [PowerDNS Search API](https://doc.powerdns.com/authoritative/http-api/search.html)+  -- and [PowerDNS Cache API](https://doc.powerdns.com/authoritative/http-api/cache.html)+  , listServers+  , getServer+  , search+  , flushCache+  , statistics+  -- ** Data types+  , Server(..)+  , SearchResult(..)+  , ObjectType(..)+  , CacheFlushResult(..)++  -- * Metadata+  -- | See documentation at [PowerDNS Metadata API](https://doc.powerdns.com/authoritative/http-api/metadata.html)+  , listMetadata+  , createMetadata+  , getMetadata+  , updateMetadata+  , deleteMetadata+  -- ** Data types+  , Metadata(..)++  -- * TSIGKeys+  -- | See documentation at [PowerDNS TSIGKeys API](https://doc.powerdns.com/authoritative/http-api/metadata.html)+  -- as well as related [TSIG documentation](https://doc.powerdns.com/authoritative/tsig.html)+  , listTSIGKeys+  , createTSIGKey+  , getTSIGKey+  , updateTSIGKey+  , deleteTSIGKey++  -- ** Data types+  , TSIGKey(..)+  , TSIGAlgorithm(..)++  -- * Utilities+  , empty+  )+where++import qualified Data.CaseInsensitive as CI+import           Data.Sequence ((|>))+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import           Servant.API (NoContent)+import           Servant.API.Generic (fromServant)+import           Servant.Client (ClientEnv(..), ClientM, client)+import           Servant.Client.Core.Request (Request, requestHeaders)+import           Servant.Client.Generic (AsClientT)++import           PowerDNS.API+import           PowerDNS.Internal.Utils (empty)++-- | Causes all requests with this 'ClientEnv' to be sent with the specified key embedded in a X-API-Key header.+applyXApiKey :: T.Text -- ^ API key+             -> ClientEnv+             -> ClientEnv+applyXApiKey key env = env { makeClientRequest = modified }+  where+    modified url = makeClientRequest env url . applyXApiKeyHeader key++applyXApiKeyHeader :: T.Text -> Request -> Request+applyXApiKeyHeader key req =+    req { requestHeaders = requestHeaders req |> authHeader }+  where+    authHeader = (CI.mk "X-API-Key", T.encodeUtf8 key)++----------------------------------------------------------------------------------------++----------------------------------------------------------------------------------------++pdnsClient :: PowerDNS (AsClientT ClientM)+pdnsClient = fromServant (client api)++zonesClient :: ZonesAPI (AsClientT ClientM)+zonesClient = fromServant (zones pdnsClient)++-- | List all zones for the server. See [Zones API Documentation](https://doc.powerdns.com/authoritative/http-api/zone.html#get--servers-server_id-zones)+listZones :: T.Text       -- ^ Server name+          -> Maybe T.Text -- ^ Limit to zone+          -> Maybe Bool   -- ^ Whether or not to include dnssec and edited_serial fields+          -> ClientM [Zone]+listZones = apiListZones zonesClient++-- | Create a new zone. See [Zones API Documentation](https://doc.powerdns.com/authoritative/http-api/zone.html#post--servers-server_id-zones)+--+-- 'empty' with record update syntax is useful avoid having to specify Nothing+-- for unwanted fields.+createZone :: T.Text       -- ^ Server name+           -> Maybe Bool   -- ^ Whether or not to include RRsets in the response+           -> Zone         -- ^ The zone to create+           -> ClientM Zone+createZone = apiCreateZone zonesClient++-- | Get details for zone. See [Zones API Documentation](https://doc.powerdns.com/authoritative/http-api/zone.html#get--servers-server_id-zones-zone_id)+getZone :: T.Text       -- ^ Server name+        -> T.Text       -- ^ Zone ID+        -> Maybe Bool   -- ^ Wheher or not to include RRsets in the response+        -> ClientM Zone+getZone = apiGetZone zonesClient++-- | Delete a given zone by id. See [Zones API Documentation](https://doc.powerdns.com/authoritative/http-api/zone.html#delete--servers-server_id-zones-zone_id)+deleteZone :: T.Text       -- ^ Server name+           -> T.Text       -- ^ Zone ID+           -> ClientM NoContent+deleteZone = apiDeleteZone zonesClient++-- | Update records of a zone. See [Zones API Documentation](https://doc.powerdns.com/authoritative/http-api/zone.html#patch--servers-server_id-zones-zone_id)+--+-- __Caution__: If rrset_records or rrset_comments is set to @Just []@ on a 'Delete' changetype,+-- this will delete all existing records or comments respectively for the domain.+updateRecords :: T.Text       -- ^ Server name+              -> T.Text       -- ^ Zone ID+              -> RRSets       -- ^ The RRsets to create, update or delete.+              -> ClientM NoContent+updateRecords = apiUpdateRecords zonesClient++-- | Modify zone. See [Zones API Documentation](https://doc.powerdns.com/authoritative/http-api/zone.html#put--servers-server_id-zones-zone_id)+--+-- 'empty' with record update syntax is useful avoid having to specify Nothing+-- for unwanted fields.+updateZone :: T.Text       -- ^ Server name+           -> T.Text       -- ^ Zone ID+           -> Zone         -- ^ Patch record. Fields with Just are changed, Nothing are ignored+           -> ClientM NoContent+updateZone = apiUpdateZone zonesClient++-- | Trigger zone transfer on a slave. See [Zones API Documentation](https://doc.powerdns.com/authoritative/http-api/zone.html#put--servers-server_id-zones-zone_id-axfr-retrieve)+triggerAxfr :: T.Text       -- ^ Server name+            -> T.Text       -- ^ Zone ID+            -> ClientM NoContent+triggerAxfr = apiTriggerAxfr zonesClient++-- | Send DNS notify to slaves. See [Zones API Documentation](https://doc.powerdns.com/authoritative/http-api/zone.html#put--servers-server_id-zones-zone_id-notify)+notifySlaves :: T.Text       -- ^ Server name+             -> T.Text       -- ^ Zone ID+             -> ClientM NoContent+notifySlaves = apiNotifySlaves zonesClient++-- | Return zone in AXFR format. See [Zones API Documentation](https://doc.powerdns.com/authoritative/http-api/zone.html#get--servers-server_id-zones-zone_id-export)+getZoneAxfr :: T.Text         -- ^ Server name+            -> T.Text         -- ^ Zone ID+            -> ClientM T.Text -- ^ Zone in AXFR format+getZoneAxfr = apiGetZoneAxfr zonesClient++-- | Rectify the zone data. See [Zones API Documentation](https://doc.powerdns.com/authoritative/http-api/zone.html#put--servers-server_id-zones-zone_id-rectify)+rectifyZone :: T.Text       -- ^ Server name+            -> T.Text       -- ^ Zone ID+            -> ClientM T.Text+rectifyZone = apiRectifyZone zonesClient++----------------------------------------------------------------------------------------++cryptokeysClient :: CryptokeysAPI (AsClientT ClientM)+cryptokeysClient = fromServant (cryptokeys pdnsClient)++-- | List all crypto keys. See [Cryptokeys API Documentation](https://doc.powerdns.com/authoritative/http-api/cryptokey.html#get--servers-server_id-zones-zone_id-cryptokeys)+listCryptoKeys :: T.Text -- ^ Server name+               -> T.Text -- ^ Zone ID+               -> ClientM [Cryptokey]+listCryptoKeys = apiListCryptokeys cryptokeysClient++-- | Create a new crypto key. See [Cryptokeys API Documentation](https://doc.powerdns.com/authoritative/http-api/cryptokey.html#post--servers-server_id-zones-zone_id-cryptokeys)+createCryptokey :: T.Text            -- ^ Server name+                -> T.Text            -- ^ Zone ID+                -> Cryptokey         -- ^ Cryptokey to create+                -> ClientM Cryptokey -- ^ Created cryptokey+createCryptokey = apiCreateCryptokey cryptokeysClient++-- | Get existing crypto key. See [Cryptokeys API Documentation](https://doc.powerdns.com/authoritative/http-api/cryptokey.html#post--servers-server_id-zones-zone_id-cryptokeys)+getCryptokey :: T.Text -- ^ Server name+             -> T.Text -- ^ Zone ID+             -> T.Text -- ^ Cryptokey ID+             -> ClientM Cryptokey+getCryptokey = apiGetCryptokey cryptokeysClient++-- | Update existing crypto key. See [Cryptokeys API Documentation](https://doc.powerdns.com/authoritative/http-api/cryptokey.html#put--servers-server_id-zones-zone_id-cryptokeys-cryptokey_id)+updateCryptokey :: T.Text    -- ^ Server name+                -> T.Text    -- ^ Zone ID+                -> T.Text    -- ^ Cryptokey ID+                -> Cryptokey -- ^ Patch record. Fields with Just are changed, Nothing are ignored+                -> ClientM NoContent+updateCryptokey = apiUpdateCryptokey cryptokeysClient++-- | Delete existing crypto key. See [Cryptokeys API Documentation](https://doc.powerdns.com/authoritative/http-api/cryptokey.html#delete--servers-server_id-zones-zone_id-cryptokeys-cryptokey_id)+deleteCryptokey :: T.Text -- ^ Server name+                -> T.Text -- ^ Zone ID+                -> T.Text -- ^ Cryptokey ID+                -> ClientM NoContent+deleteCryptokey = apiDeleteCryptokey cryptokeysClient++----------------------------------------------------------------------------------------+serversClient :: ServersAPI (AsClientT ClientM)+serversClient = fromServant (servers pdnsClient)++-- | List available servers. See [Servers API Documentation](https://doc.powerdns.com/authoritative/http-api/server.html#get--servers)+listServers :: ClientM [Server]+listServers = apiListServers serversClient++-- | Get existing server. See [Servers API Documentation](https://doc.powerdns.com/authoritative/http-api/server.html#get--servers-server_id)+getServer :: T.Text -- ^ Server ID+          -> ClientM Server+getServer = apiGetServer serversClient++-- | Searches in various object types for an arbitrary string. See [Search API Documentation](https://doc.powerdns.com/authoritative/http-api/search.html#get--servers-server_id-search-data)+search :: T.Text           -- ^ Server ID+       -> T.Text           -- ^ String to search for+       -> Integer          -- ^ Maximum number of results+       -> Maybe ObjectType -- ^ Limit results to specified object type, if any.+       -> ClientM [SearchResult]+search = apiSearch serversClient++-- | Flushes a domain from the cache. See [Cache API Documentation](https://doc.powerdns.com/authoritative/http-api/cache.html#put--servers-server_id-cache-flush)+flushCache :: T.Text -- ^ Server ID+           -> T.Text -- ^ Domain+           -> ClientM CacheFlushResult+flushCache = apiFlushCache serversClient++-- | Get server wide statistics. See [Cache API Documentation](https://doc.powerdns.com/authoritative/http-api/statistics.html)+statistics :: T.Text       -- ^ Server ID+           -> Maybe T.Text -- ^ Only return statistic items with this name.+           -> Maybe Bool   -- ^ Whether or not to return ring items.+           -> ClientM [AnyStatisticItem]+statistics = apiStatistics serversClient++----------------------------------------------------------------------------------------+metadataClient :: MetadataAPI (AsClientT ClientM)+metadataClient = fromServant (metadata pdnsClient)++-- | List metadata for existing zone. See [Metadata API Documentation](https://doc.powerdns.com/authoritative/http-api/metadata.html#get--servers-server_id-zones-zone_id-metadata)+listMetadata :: T.Text -- ^ Server ID+             -> T.Text -- ^ Zone ID+             -> ClientM [Metadata]+listMetadata = apiListMetadata metadataClient++-- | Create metadata for zone. See [Metadata API Documentation](https://doc.powerdns.com/authoritative/http-api/metadata.html#post--servers-server_id-zones-zone_id-metadata)+createMetadata :: T.Text -- ^ Server ID+               -> T.Text -- ^ Zone ID+               -> Metadata+               -> ClientM NoContent+createMetadata = apiCreateMetadata metadataClient++-- | Get metadata for zone by kind. See [Metadata API Documentation](https://doc.powerdns.com/authoritative/http-api/metadata.html#get--servers-server_id-zones-zone_id-metadata-metadata_kind)+getMetadata :: T.Text -- ^ Server ID+            -> T.Text -- ^ Zone ID+            -> T.Text -- ^ Kind+            -> ClientM Metadata+getMetadata = apiGetMetadata metadataClient++-- | Update metadata for zone by kind. See [Metadata API Documentation](https://doc.powerdns.com/authoritative/http-api/metadata.html#put--servers-server_id-zones-zone_id-metadata-metadata_kind)+updateMetadata :: T.Text -- ^ Server ID+               -> T.Text -- ^ Zone ID+               -> T.Text -- ^ Kind+               -> Metadata+               -> ClientM Metadata+updateMetadata = apiUpdateMetadata metadataClient++-- | Delete metadata for zone by kind. See [Metadata API Documentation](https://doc.powerdns.com/authoritative/http-api/metadata.html#delete--servers-server_id-zones-zone_id-metadata-metadata_kind)+deleteMetadata :: T.Text -- ^ Server ID+               -> T.Text -- ^ Zone ID+               -> T.Text -- ^ Kind+               -> ClientM NoContent+deleteMetadata = apiDeleteMetadata metadataClient++----------------------------------------------------------------------------------------+tsigkeysClient :: TSIGKeysAPI (AsClientT ClientM)+tsigkeysClient = fromServant (tsigkeys pdnsClient)++-- | List all TSIG keys. See [TSIGKeys API Documentation](https://doc.powerdns.com/authoritative/http-api/tsigkey.html#get--servers-server_id-tsigkeys)+listTSIGKeys :: T.Text -> ClientM [TSIGKey]+listTSIGKeys = apiListTSIGKeys tsigkeysClient++-- | Create a new TSIG key. If the key is left empty, the server will generate one. See [TSIGKeys API Documentation](https://doc.powerdns.com/authoritative/http-api/tsigkey.html#post--servers-server_id-tsigkeys)+createTSIGKey :: T.Text -> TSIGKey -> ClientM TSIGKey+createTSIGKey = apiCreateTSIGKey tsigkeysClient++-- | Get TSIG key by its id. See [TSIGKeys API Documentation](https://doc.powerdns.com/authoritative/http-api/tsigkey.html#get--servers-server_id-tsigkeys-tsigkey_id)+getTSIGKey :: T.Text -> T.Text -> ClientM TSIGKey+getTSIGKey = apiGetTSIGKey tsigkeysClient++-- | Update existig TSIG key. See [TSIGKeys API Documentation](https://doc.powerdns.com/authoritative/http-api/tsigkey.html#put--servers-server_id-tsigkeys-tsigkey_id)+updateTSIGKey :: T.Text -> T.Text -> TSIGKey -> ClientM TSIGKey+updateTSIGKey = apiUpdateTSIGKey tsigkeysClient++-- | Delete existing TSIG key. See [TSIGKeys API Documentation](https://doc.powerdns.com/authoritative/http-api/tsigkey.html#delete--servers-server_id-tsigkeys-tsigkey_id)+deleteTSIGKey :: T.Text -> T.Text -> ClientM NoContent+deleteTSIGKey = apiDeleteTSIGKey tsigkeysClient++----------------------------------------------------------------------------------------
+ PowerDNS/Internal/Utils.hs view
@@ -0,0 +1,56 @@+-- |+-- Module      : PowerDNS.Internal.Utils+-- Description : Assorted utilities for the PowerDNS API++{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE OverloadedStrings #-}+module PowerDNS.Internal.Utils+  ( strip+  , Empty(..)+  , GEmpty(..)+  )+where++import           Data.List (stripPrefix)+import           GHC.Generics++-- | A variant of 'stripPrefix' that defaults to id if the prefix is not found.+strip :: Eq a => [a] -> [a] -> [a]+strip p xs = case stripPrefix p xs of+                  Just ys -> ys+                  Nothing -> xs+++-- | Typeclass of things we can generate empty values of.+-- This is used to quickly build values from parameters to PowerDNS, because+-- you often only need a few fields.+-- @+--   empty { someField = Just 1+--         , otherField = Just "foo" }+-- @+class Empty a where+  -- | Produce an empty value+  empty :: a+  default empty :: (Generic a, GEmpty (Rep a)) => a+  empty = to gempty++instance Empty (Maybe a) where+  empty = Nothing++class GEmpty f where+  gempty :: f p++instance GEmpty U1 where+  gempty = U1++instance GEmpty f => GEmpty (M1 i t f) where+  gempty = M1 gempty++instance Empty a => GEmpty (K1 i a) where+  gempty = K1 empty++instance (GEmpty f, GEmpty g) => GEmpty (f :*: g) where+  gempty = gempty :*: gempty
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ powerdns.cabal view
@@ -0,0 +1,77 @@+cabal-version:      >=1.10+name:               powerdns+version:            0.1+license:            BSD3+license-file:       LICENSE+copyright:          (c) 2021 Victor Nawothnig+maintainer:         Victor Nawothnig (dminuoso@icloud.com)+author:             Victor Nawothnig, Julian Jacobi++bug-reports:        https://gitlab.com/wobcom/haskell/powerdns/issues+synopsis:           PowerDNS API bindings for api/v1+description:+    PowerDNS API allows programmatic manipulation of zones and other metadata. This+    library is a thin wrapper, exposing most of the idiosyncracies directly.+    .+    Users are advised to refer to the PowerDNS documentation and its source code for+    the exact semantics.+    .+    All exposed API endpoints are provided.+    .+    > import qualified PowerDNS.Client as P+    > import           Network.HTTP.Client (newManager, defaultManagerSettings)+    > import           Servant.Client (runClientM, mkClientEnv, parseBarseUrl)+    >+    > main :: IO+    > main = do+    >   uri <- parseBaseUrl "http://localhost:8081"+    >   mgr <- newManager defaultManagerSettings+    >   env <- P.applyXApiKey "secret" <$> mkClientEnv+    >+    >   Right r <- runClientM (P.statistics "localhost" Nothing Nothing) env)+    >   traverse_ print r++category:           Network+build-type:         Simple+extra-source-files: CHANGELOG.md++library+    exposed-modules:+        PowerDNS.API+        PowerDNS.API.Zones+        PowerDNS.API.Servers+        PowerDNS.API.Cryptokeys+        PowerDNS.API.Metadata+        PowerDNS.API.TSIGKeys+        PowerDNS.Client+        PowerDNS.Internal.Utils++    default-language: Haskell2010+    ghc-options:      -Wall -Wcompat+    build-depends:+        base >=4.12 && <4.14,+        aeson >=1.5.6 && <1.6,+        bytestring >=0.10.10 && <0.11,+        deepseq >=1.4.4 && <1.5,+        containers >=0.6.2 && <0.7,+        text >=1.2.4 && <1.3,+        time >=1.9.3 && <1.10,+        base64-bytestring >=1.1.0 && <1.2,+        case-insensitive >=1.2.1 && <1.3,+        servant >=0.18.2 && <0.19,+        servant-client >=0.18.2 && <0.19,+        servant-client-core >=0.18.2 && <0.19++test-suite powerdns-test+    type:             exitcode-stdio-1.0+    main-is:          Spec.hs+    hs-source-dirs:   spec+    default-language: Haskell2010+    build-depends:+        base >=4.12 && <4.14,+        powerdns -any,+        tasty >=1.4.1 && <1.5,+        tasty-hunit >=0.10.0.3 && <0.11,+        servant-client >=0.18.2 && <0.19,+        servant-client-core >=0.18.2 && <0.19,+        http-client >=0.6.4.1 && <0.7
+ spec/Spec.hs view
@@ -0,0 +1,148 @@+{-# LANGUAGE OverloadedStrings #-}+import Data.Either (isRight)+import Control.Monad (unless)++import GHC.Stack (HasCallStack)++import System.IO.Unsafe (unsafePerformIO)+import Test.Tasty+import Test.Tasty.HUnit+import Network.HTTP.Client (newManager, defaultManagerSettings, Manager)+import Servant.Client (runClientM, mkClientEnv, ClientEnv, ClientM)+import Servant.Client.Core (parseBaseUrl, BaseUrl, ClientError(..), responseStatusCode)++import PowerDNS.Client++{-# NOINLINE mgr #-}+mgr :: Manager+mgr = unsafePerformIO (newManager defaultManagerSettings)++{-# NOINLINE baseUrl #-}+baseUrl :: BaseUrl+baseUrl = unsafePerformIO (parseBaseUrl "localhost:8081")++envNoAuth :: ClientEnv+envNoAuth = mkClientEnv mgr baseUrl++envAuth :: ClientEnv+envAuth = applyXApiKey "secret" envNoAuth++main :: IO ()+main = defaultMain tree++tree :: TestTree+tree = testGroup "All specs" [ authSpecs+                             , zoneSpecs+                             ]++run :: HasCallStack => ClientM a -> IO (Either ClientError a)+run = flip runClientM envAuth++runOk :: HasCallStack => ClientM a -> IO a+runOk c = do+  r <- run c+  assertRight "ClientM result" r++zoneSpecs :: TestTree+zoneSpecs = testCaseSteps "Verifies zones can be created, displayed and deleted" $ \step -> do+    step "Ensure no zone is currently found"+    r1 <- run (listZones "localhost" Nothing Nothing)+    assertEqual "list of zones" (Right []) r1++    step "Create a new zone"+    r2 <- runOk (createZone "localhost" (Just True) new)+    zoneId <- assertJust "zone id" (zone_id r2)++    step "Check if zone is fetchable"+    runOk (getZone "localhost" zoneId (Just False))++    step "Add record to zone"+    runOk (updateZone "localhost" zoneId patch)++    step "Delete zone"+    runOk (deleteZone "localhost" zoneId)++    step "Ensure no zone is left over"+    r <- run (listZones "localhost" Nothing Nothing)+    assertEqual "list of zones" (Right []) r+    +    step "Done"++  where+    new = empty { zone_name   = Just "test.space."+                , zone_kind   = Just Native+                , zone_type   = Just "zone"+                , zone_rrsets = Just init }++    init = [ RRSet { rrset_name = "magic.test.space."+                   , rrset_type = A+                   , rrset_ttl = 86003+                   , rrset_changetype = Nothing+                   , rrset_records = Just [Record "127.0.0.1" Nothing]+                   , rrset_comments = Nothing+                   }+           ]++    patch = empty { zone_type = Just "zone"+                  , zone_rrsets = Just added+                  }+    added = [ RRSet { rrset_name = "foo.test.space."+                    , rrset_type = AAAA+                    , rrset_ttl = 1234+                    , rrset_changetype = Nothing+                    , rrset_records = Just [Record "::1" Nothing]+                    , rrset_comments = Nothing+                    } +            ]++-- | Erases all server generated data from a zone. Used to test for equality.+cleanse :: Zone -> Zone+cleanse z = z { zone_id = Nothing+              , zone_url = Nothing+              , zone_serial = Nothing+              , zone_soa_edit = Nothing+              , zone_soa_edit_api = Nothing+              , zone_notified_serial = Nothing+              , zone_edited_serial = Nothing+              , zone_rrsets = Nothing+              , zone_master_tsig_key_ids = Nothing+              , zone_slave_tsig_key_ids = Nothing+              }++authSpecs :: TestTree+authSpecs = testGroup "Authentication specs"+  [ testCase "X-API-Key header is correctly set" $+      assertIsSuccess =<< runClientM listServers envAuth++  , testCase "Access without X-API-Key is rejected" $+      assertIs401 =<< runClientM listServers envNoAuth+  ]++assertPredicate :: Show s => String -> (s -> Bool) -> s -> Assertion+assertPredicate name p v =+  unless (p v) (assertFailure msg)+ where msg = "failed to satisfy predicate: " ++ name ++ "\n" +++             "with value: " ++ show v++assertIs401 :: Show a => Either ClientError a -> Assertion+assertIs401 = assertPredicate "has HTTP status 401" (hasStatus 401)++hasStatus :: Int -> Either ClientError a -> Bool+hasStatus i (Left (FailureResponse _req resp)) |+   responseStatusCode resp == toEnum i = True+hasStatus _i _        = False++assertIsSuccess :: Show a => Either ClientError a -> Assertion+assertIsSuccess = assertPredicate "successful HTTP response" isRight+  +assertJust :: HasCallStack => String -> Maybe a -> IO a+assertJust preface = maybe (assertFailure msg) pure+ where msg = (if null preface then "" else preface ++ "\n") +++             "Unexpected Nothing"++assertRight :: (Show a, HasCallStack) => String -> Either a b -> IO b+assertRight preface e = case e of+  Left e -> assertFailure (msg e)+  Right r -> pure r+ where msg v = (if null preface then "" else preface ++ "\n") +++               "Unexpected Left: " ++ show v