packages feed

hetzner 0.8.0.0 → 1.0.0.0

raw patch · 5 files changed

+283/−323 lines, 5 filesdep ~basePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: base

API changes (from Hackage documentation)

- Hetzner.Cloud: [serverDeprecated] :: ServerType -> Bool
- Hetzner.Cloud: data WithKey (key :: Symbol) a
- Hetzner.DNS: A :: RecordType
- Hetzner.DNS: AAAA :: RecordType
- Hetzner.DNS: CAA :: RecordType
- Hetzner.DNS: CNAME :: RecordType
- Hetzner.DNS: DANE :: RecordType
- Hetzner.DNS: DNSError :: Response ByteString -> DNSException
- Hetzner.DNS: DS :: RecordType
- Hetzner.DNS: Failed :: ZoneStatus
- Hetzner.DNS: HINFO :: RecordType
- Hetzner.DNS: JSONError :: Response ByteString -> String -> DNSException
- Hetzner.DNS: MX :: RecordType
- Hetzner.DNS: NS :: RecordType
- Hetzner.DNS: PTR :: RecordType
- Hetzner.DNS: Pagination :: Int -> Int -> Maybe Int -> Maybe Int -> Maybe Int -> Maybe Int -> Pagination
- Hetzner.DNS: Pending :: ZoneStatus
- Hetzner.DNS: RP :: RecordType
- Hetzner.DNS: Record :: ZonedTime -> ZonedTime -> RecordID -> Text -> RecordType -> Text -> Int -> ZoneID -> Record
- Hetzner.DNS: RecordID :: Text -> RecordID
- Hetzner.DNS: ResponseMeta :: Pagination -> ResponseMeta
- Hetzner.DNS: SOA :: RecordType
- Hetzner.DNS: SRV :: RecordType
- Hetzner.DNS: TLS :: RecordType
- Hetzner.DNS: TXT :: RecordType
- Hetzner.DNS: Token :: ByteString -> Token
- Hetzner.DNS: Verified :: ZoneStatus
- Hetzner.DNS: WithKey :: a -> WithKey (key :: Symbol) a
- Hetzner.DNS: WithMeta :: ResponseMeta -> a -> WithMeta (key :: Symbol) a
- Hetzner.DNS: Zone :: ZonedTime -> ZonedTime -> ZoneID -> Text -> Bool -> ZoneStatus -> Int -> Int -> Zone
- Hetzner.DNS: ZoneID :: Text -> ZoneID
- Hetzner.DNS: [currentPage] :: Pagination -> Int
- Hetzner.DNS: [itemsPerPage] :: Pagination -> Int
- Hetzner.DNS: [lastPage] :: Pagination -> Maybe Int
- Hetzner.DNS: [nextPage] :: Pagination -> Maybe Int
- Hetzner.DNS: [pagination] :: ResponseMeta -> Pagination
- Hetzner.DNS: [previousPage] :: Pagination -> Maybe Int
- Hetzner.DNS: [recordCreated] :: Record -> ZonedTime
- Hetzner.DNS: [recordID] :: Record -> RecordID
- Hetzner.DNS: [recordModified] :: Record -> ZonedTime
- Hetzner.DNS: [recordName] :: Record -> Text
- Hetzner.DNS: [recordTTL] :: Record -> Int
- Hetzner.DNS: [recordType] :: Record -> RecordType
- Hetzner.DNS: [recordValue] :: Record -> Text
- Hetzner.DNS: [recordZone] :: Record -> ZoneID
- Hetzner.DNS: [responseMeta] :: WithMeta (key :: Symbol) a -> ResponseMeta
- Hetzner.DNS: [totalEntries] :: Pagination -> Maybe Int
- Hetzner.DNS: [withoutKey] :: WithKey (key :: Symbol) a -> a
- Hetzner.DNS: [withoutMeta] :: WithMeta (key :: Symbol) a -> a
- Hetzner.DNS: [zoneCreated] :: Zone -> ZonedTime
- Hetzner.DNS: [zoneID] :: Zone -> ZoneID
- Hetzner.DNS: [zoneIsSecondary] :: Zone -> Bool
- Hetzner.DNS: [zoneModified] :: Zone -> ZonedTime
- Hetzner.DNS: [zoneName] :: Zone -> Text
- Hetzner.DNS: [zoneRecordCount] :: Zone -> Int
- Hetzner.DNS: [zoneStatus] :: Zone -> ZoneStatus
- Hetzner.DNS: [zoneTTL] :: Zone -> Int
- Hetzner.DNS: allRecordTypes :: [RecordType]
- Hetzner.DNS: createRecord :: Token -> ZoneID -> Text -> RecordType -> Text -> Maybe Int -> IO Record
- Hetzner.DNS: data DNSException
- Hetzner.DNS: data Pagination
- Hetzner.DNS: data Record
- Hetzner.DNS: data RecordType
- Hetzner.DNS: data ResponseMeta
- Hetzner.DNS: data WithKey (key :: Symbol) a
- Hetzner.DNS: data WithMeta (key :: Symbol) a
- Hetzner.DNS: data Zone
- Hetzner.DNS: data ZoneStatus
- Hetzner.DNS: deleteRecord :: Token -> RecordID -> IO ()
- Hetzner.DNS: deleteZone :: Token -> ZoneID -> IO ()
- Hetzner.DNS: dnsQuery :: (ToJSON body, FromJSON a) => ByteString -> ByteString -> Maybe body -> Query -> Token -> Maybe Int -> IO a
- Hetzner.DNS: getRecord :: Token -> RecordID -> IO Record
- Hetzner.DNS: getRecords :: Token -> Maybe ZoneID -> IO [Record]
- Hetzner.DNS: getTokenFromEnv :: IO (Maybe Token)
- Hetzner.DNS: getZone :: Token -> ZoneID -> IO Zone
- Hetzner.DNS: getZones :: Token -> Maybe Int -> IO (WithMeta "zones" [Zone])
- Hetzner.DNS: instance Data.Aeson.Types.FromJSON.FromJSON Hetzner.DNS.DNSTime
- Hetzner.DNS: instance Data.Aeson.Types.FromJSON.FromJSON Hetzner.DNS.Record
- Hetzner.DNS: instance Data.Aeson.Types.FromJSON.FromJSON Hetzner.DNS.RecordID
- Hetzner.DNS: instance Data.Aeson.Types.FromJSON.FromJSON Hetzner.DNS.RecordType
- Hetzner.DNS: instance Data.Aeson.Types.FromJSON.FromJSON Hetzner.DNS.Zone
- Hetzner.DNS: instance Data.Aeson.Types.FromJSON.FromJSON Hetzner.DNS.ZoneID
- Hetzner.DNS: instance Data.Aeson.Types.FromJSON.FromJSON Hetzner.DNS.ZoneStatus
- Hetzner.DNS: instance Data.Aeson.Types.ToJSON.ToJSON Hetzner.DNS.RecordID
- Hetzner.DNS: instance Data.Aeson.Types.ToJSON.ToJSON Hetzner.DNS.RecordType
- Hetzner.DNS: instance Data.Aeson.Types.ToJSON.ToJSON Hetzner.DNS.ZoneID
- Hetzner.DNS: instance Data.String.IsString Hetzner.DNS.Token
- Hetzner.DNS: instance GHC.Classes.Eq Hetzner.DNS.RecordID
- Hetzner.DNS: instance GHC.Classes.Eq Hetzner.DNS.RecordType
- Hetzner.DNS: instance GHC.Classes.Eq Hetzner.DNS.Token
- Hetzner.DNS: instance GHC.Classes.Eq Hetzner.DNS.ZoneID
- Hetzner.DNS: instance GHC.Classes.Ord Hetzner.DNS.RecordID
- Hetzner.DNS: instance GHC.Classes.Ord Hetzner.DNS.Token
- Hetzner.DNS: instance GHC.Classes.Ord Hetzner.DNS.ZoneID
- Hetzner.DNS: instance GHC.Enum.Enum Hetzner.DNS.RecordType
- Hetzner.DNS: instance GHC.Exception.Type.Exception Hetzner.DNS.DNSException
- Hetzner.DNS: instance GHC.Show.Show Hetzner.DNS.DNSException
- Hetzner.DNS: instance GHC.Show.Show Hetzner.DNS.Record
- Hetzner.DNS: instance GHC.Show.Show Hetzner.DNS.RecordID
- Hetzner.DNS: instance GHC.Show.Show Hetzner.DNS.RecordType
- Hetzner.DNS: instance GHC.Show.Show Hetzner.DNS.Token
- Hetzner.DNS: instance GHC.Show.Show Hetzner.DNS.Zone
- Hetzner.DNS: instance GHC.Show.Show Hetzner.DNS.ZoneID
- Hetzner.DNS: instance GHC.Show.Show Hetzner.DNS.ZoneStatus
- Hetzner.DNS: newtype RecordID
- Hetzner.DNS: newtype Token
- Hetzner.DNS: newtype ZoneID
- Hetzner.DNS: noBody :: Maybe Void
- Hetzner.DNS: streamPages :: forall (key :: Symbol) f a i (m :: Type -> Type). (Foldable f, MonadIO m) => (Maybe Int -> IO (WithMeta key (f a))) -> ConduitT i a m ()
- Hetzner.DNS: streamToList :: Monad m => ConduitT () a m () -> m [a]
- Hetzner.DNS: updateRecord :: Token -> RecordID -> ZoneID -> Text -> RecordType -> Text -> Maybe Int -> IO Record
- Hetzner.DNS: updateZone :: Token -> ZoneID -> Text -> Maybe Int -> IO Zone
+ Hetzner.Cloud: A :: RRSetType
+ Hetzner.Cloud: AAAA :: RRSetType
+ Hetzner.Cloud: AddRRSetRecords :: ActionCommand
+ Hetzner.Cloud: CAA :: RRSetType
+ Hetzner.Cloud: CNAME :: RRSetType
+ Hetzner.Cloud: CreateRRSet :: ActionCommand
+ Hetzner.Cloud: DNSRecord :: Text -> Text -> DNSRecord
+ Hetzner.Cloud: DS :: RRSetType
+ Hetzner.Cloud: DeleteRRSet :: ActionCommand
+ Hetzner.Cloud: DeleteRRSetRecords :: ActionCommand
+ Hetzner.Cloud: DisableRescue :: ActionCommand
+ Hetzner.Cloud: EnableRescue :: ActionCommand
+ Hetzner.Cloud: HINFO :: RRSetType
+ Hetzner.Cloud: HTTPS :: RRSetType
+ Hetzner.Cloud: MX :: RRSetType
+ Hetzner.Cloud: NS :: RRSetType
+ Hetzner.Cloud: PTR :: RRSetType
+ Hetzner.Cloud: RP :: RRSetType
+ Hetzner.Cloud: RRSet :: RRSetID -> ZoneID -> [DNSRecord] -> Maybe Int -> LabelMap -> RRSet
+ Hetzner.Cloud: RRSetID :: Text -> RRSetType -> RRSetID
+ Hetzner.Cloud: RescueModeEnabled :: Text -> Action -> RescueModeEnabled
+ Hetzner.Cloud: ResourceZoneID :: ZoneID -> ResourceID
+ Hetzner.Cloud: SOA :: RRSetType
+ Hetzner.Cloud: SRV :: RRSetType
+ Hetzner.Cloud: SVCB :: RRSetType
+ Hetzner.Cloud: SetRRSetRecords :: ActionCommand
+ Hetzner.Cloud: SetRRSetTTL :: ActionCommand
+ Hetzner.Cloud: TLSA :: RRSetType
+ Hetzner.Cloud: TXT :: RRSetType
+ Hetzner.Cloud: UpdateRRSetRecords :: ActionCommand
+ Hetzner.Cloud: Zone :: ZoneID -> LabelMap -> Text -> Int -> Int -> Zone
+ Hetzner.Cloud: ZoneID :: Int -> ZoneID
+ Hetzner.Cloud: [dnsRecordComment] :: DNSRecord -> Text
+ Hetzner.Cloud: [dnsRecordValue] :: DNSRecord -> Text
+ Hetzner.Cloud: [rescueModeAction] :: RescueModeEnabled -> Action
+ Hetzner.Cloud: [rescueModePassword] :: RescueModeEnabled -> Text
+ Hetzner.Cloud: [rrSetID] :: RRSet -> RRSetID
+ Hetzner.Cloud: [rrSetLabels] :: RRSet -> LabelMap
+ Hetzner.Cloud: [rrSetRecords] :: RRSet -> [DNSRecord]
+ Hetzner.Cloud: [rrSetTTL] :: RRSet -> Maybe Int
+ Hetzner.Cloud: [rrSetZoneID] :: RRSet -> ZoneID
+ Hetzner.Cloud: [zoneDefaultTTL] :: Zone -> Int
+ Hetzner.Cloud: [zoneID] :: Zone -> ZoneID
+ Hetzner.Cloud: [zoneLabels] :: Zone -> LabelMap
+ Hetzner.Cloud: [zoneName] :: Zone -> Text
+ Hetzner.Cloud: [zoneRecordCount] :: Zone -> Int
+ Hetzner.Cloud: addRRSetRecords :: Token -> ZoneID -> RRSetID -> [DNSRecord] -> IO Action
+ Hetzner.Cloud: createRRSet :: Token -> RRSet -> IO Action
+ Hetzner.Cloud: data DNSRecord
+ Hetzner.Cloud: data RRSet
+ Hetzner.Cloud: data RRSetID
+ Hetzner.Cloud: data RRSetType
+ Hetzner.Cloud: data RescueModeEnabled
+ Hetzner.Cloud: data Zone
+ Hetzner.Cloud: deleteRRSet :: Token -> ZoneID -> RRSetID -> IO Action
+ Hetzner.Cloud: deleteRRSetRecords :: Token -> ZoneID -> RRSetID -> [Text] -> IO Action
+ Hetzner.Cloud: disableRescueMode :: Token -> ServerID -> IO Action
+ Hetzner.Cloud: downloadZoneFile :: Token -> ZoneID -> IO Text
+ Hetzner.Cloud: enableRescueMode :: Token -> ServerID -> [SSHKeyID] -> IO RescueModeEnabled
+ Hetzner.Cloud: getRRSet :: Token -> ZoneID -> RRSetID -> IO RRSet
+ Hetzner.Cloud: getRRSets :: Token -> ZoneID -> Maybe Int -> IO (WithMeta "rrsets" [RRSet])
+ Hetzner.Cloud: getZone :: Token -> ZoneID -> IO Zone
+ Hetzner.Cloud: getZones :: Token -> Maybe Int -> IO (WithMeta "zones" [Zone])
+ Hetzner.Cloud: instance (GHC.TypeLits.KnownSymbol key, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (Hetzner.Cloud.WithKey key a)
+ Hetzner.Cloud: instance Data.Aeson.Types.FromJSON.FromJSON Hetzner.Cloud.DNSRecord
+ Hetzner.Cloud: instance Data.Aeson.Types.FromJSON.FromJSON Hetzner.Cloud.RRSet
+ Hetzner.Cloud: instance Data.Aeson.Types.FromJSON.FromJSON Hetzner.Cloud.RRSetType
+ Hetzner.Cloud: instance Data.Aeson.Types.FromJSON.FromJSON Hetzner.Cloud.RescueModeEnabled
+ Hetzner.Cloud: instance Data.Aeson.Types.FromJSON.FromJSON Hetzner.Cloud.Zone
+ Hetzner.Cloud: instance Data.Aeson.Types.FromJSON.FromJSON Hetzner.Cloud.ZoneID
+ Hetzner.Cloud: instance Data.Aeson.Types.ToJSON.ToJSON Hetzner.Cloud.DNSRecord
+ Hetzner.Cloud: instance Data.Aeson.Types.ToJSON.ToJSON Hetzner.Cloud.RRSet
+ Hetzner.Cloud: instance Data.Aeson.Types.ToJSON.ToJSON Hetzner.Cloud.RRSetType
+ Hetzner.Cloud: instance Data.Aeson.Types.ToJSON.ToJSON Hetzner.Cloud.ZoneID
+ Hetzner.Cloud: instance GHC.Classes.Eq Hetzner.Cloud.RRSetID
+ Hetzner.Cloud: instance GHC.Classes.Eq Hetzner.Cloud.RRSetType
+ Hetzner.Cloud: instance GHC.Classes.Eq Hetzner.Cloud.ZoneID
+ Hetzner.Cloud: instance GHC.Classes.Ord Hetzner.Cloud.ZoneID
+ Hetzner.Cloud: instance GHC.Show.Show Hetzner.Cloud.DNSRecord
+ Hetzner.Cloud: instance GHC.Show.Show Hetzner.Cloud.RRSet
+ Hetzner.Cloud: instance GHC.Show.Show Hetzner.Cloud.RRSetID
+ Hetzner.Cloud: instance GHC.Show.Show Hetzner.Cloud.RRSetType
+ Hetzner.Cloud: instance GHC.Show.Show Hetzner.Cloud.Zone
+ Hetzner.Cloud: instance GHC.Show.Show Hetzner.Cloud.ZoneID
+ Hetzner.Cloud: newtype WithKey (key :: Symbol) a
+ Hetzner.Cloud: newtype ZoneID
+ Hetzner.Cloud: setRRSetRecords :: Token -> ZoneID -> RRSetID -> [DNSRecord] -> IO Action
+ Hetzner.Cloud: setRRSetTTL :: Token -> ZoneID -> RRSetID -> Int -> IO Action
+ Hetzner.Cloud: updateRRSetRecords :: Token -> ZoneID -> RRSetID -> [DNSRecord] -> IO Action
- Hetzner.Cloud: ServerType :: Architecture -> Int -> CPUType -> Bool -> Text -> Int -> ServerTypeID -> Int -> Text -> [PriceInLocation] -> StorageType -> ServerType
+ Hetzner.Cloud: ServerType :: Architecture -> Int -> CPUType -> Text -> Int -> ServerTypeID -> Int -> Text -> [PriceInLocation] -> StorageType -> ServerType

Files

changelog.md view
@@ -1,3 +1,13 @@+## 1.0.0.0+* Require `base >= 4.18`.+* Remove module `Hetzner.DNS`. Instead, the DNS API now lives under+  `Hetzner.Cloud`. It's also the new DNS API offered by Hetzner,+  which is incompatible with the previous one. Not complete yet.+* Make `WithKey` a newtype and add a `ToJSON` instance.+* Implement rescue mode enabling/disabling.+* Remove server type "deprecated" field. No longer returned by Hetzner.+* Fix `Image` parser.+ ## 0.8.0.0 * Remove data centers. These have been deprecated by Hetzner. 
hetzner.cabal view
@@ -2,7 +2,7 @@ category: Cloud synopsis: Hetzner Cloud and DNS library. description: Hetzner Cloud and DNS library. Check the readme and documentation for more details.-version: 0.8.0.0+version: 1.0.0.0 cabal-version: 1.18 build-type: Simple author: Daniel Casanueva (coding `at` danielcasanueva.eu)@@ -19,7 +19,6 @@   exposed-modules:     Hetzner.Cloud     Hetzner.Cloud.Fingerprint-    Hetzner.DNS   ghc-options: -Wall -Wunused-packages   default-extensions:       ImportQualifiedPost@@ -31,7 +30,7 @@     , TypeApplications     , CPP   build-depends:-      base >= 4.16 && < 4.23+      base >= 4.18 && < 4.23     , text, aeson, containers, conduit     , http-conduit, bytestring, yaml, time     , country, ip, megaparsec, scientific
readme.md view
@@ -2,8 +2,7 @@  ## Description -Client interface for the [Hetzner Cloud API](https://docs.hetzner.cloud)-and the [Hetzner DNS API](https://dns.hetzner.com/api-docs),+Client interface for the [Hetzner Cloud API](https://docs.hetzner.cloud). written in the [Haskell](https://www.haskell.org) programming language.  ## Completeness
src/Hetzner/Cloud.hs view
@@ -44,8 +44,7 @@   , getMetadata     -- * Hetzner Cloud API -    -- | Sections are in the same order as in the-    --   [official documentation](https://docs.hetzner.cloud).+    -- | Sections appear in alphabetic order.      -- ** Actions   , ActionStatus (..)@@ -56,6 +55,27 @@   , waitForAction   , HasActions (..)   , waitForActions+    -- ** DNS+    -- *** Zones+  , ZoneID (..)+  , Zone (..)+  , getZones+  , getZone+  , downloadZoneFile+    -- *** RRSets+  , RRSetID (..)+  , RRSet (..)+  , RRSetType (..)+  , DNSRecord (..)+  , getRRSets+  , getRRSet+  , createRRSet+  , deleteRRSet+  , setRRSetTTL+  , setRRSetRecords+  , addRRSetRecords+  , updateRRSetRecords+  , deleteRRSetRecords     -- ** Firewalls   , FirewallID (..)   , TrafficDirection (..)@@ -94,12 +114,6 @@   , Location (..)   , getLocations   , getLocation-    -- ** Primary IPs-  , PrimaryIPID (..)-  , PrimaryIP (..)-  , getPrimaryIPs-  , getPrimaryIP-  , setReverseDNS     -- ** Networks   , NetworkID (..)   , Route (..)@@ -116,6 +130,12 @@     -- ** Pricing   , Price (..)   , PriceInLocation (..)+    -- ** Primary IPs+  , PrimaryIPID (..)+  , PrimaryIP (..)+  , getPrimaryIPs+  , getPrimaryIP+  , setReverseDNS     -- ** Servers   , ServerStatus (..)   , ServerID (..)@@ -135,6 +155,9 @@   , shutdownServer   , rebootServer   , changeServerType+  , RescueModeEnabled (..)+  , enableRescueMode+  , disableRescueMode     -- ** Server types   , Architecture (..)   , StorageType (..)@@ -214,7 +237,7 @@ import Control.Monad.IO.Class (MonadIO, liftIO) import Data.Foldable (forM_) import Data.Traversable (forM)-import Data.Maybe (isNothing, fromMaybe)+import Data.Maybe (isNothing) import System.Environment qualified as System import Data.List.NonEmpty (NonEmpty ((:|))) -- ip@@ -227,6 +250,7 @@ -- text import Data.Text (Text) import Data.Text qualified as Text+import Data.Text.Encoding qualified as Text -- aeson import Data.Aeson   ( FromJSON, ToJSON@@ -236,6 +260,7 @@ import Data.Aeson qualified as JSON import Data.Aeson.Types qualified as JSON import Data.Aeson.Key qualified as JSONKey+import Data.Aeson.KeyMap qualified as JSONKeyMap import Data.Aeson.Encoding qualified as JSONEncoding -- yaml import Data.Yaml qualified as Yaml@@ -685,7 +710,7 @@ streamToList = Conduit.sourceToList  -- | Wrap a value with the key of the value within a JSON object.-data WithKey (key :: Symbol) a = WithKey { withoutKey :: a } deriving Show+newtype WithKey (key :: Symbol) a = WithKey { withoutKey :: a } deriving Show  instance Functor (WithKey key) where   fmap f (WithKey x) = WithKey (f x)@@ -699,6 +724,9 @@     in  JSON.withObject ("WithKey:" ++ key) $ \o ->           WithKey <$> o .: fromString key +instance (KnownSymbol key, ToJSON a) => ToJSON (WithKey key a) where+  toJSON (WithKey a) = JSON.object [ fromString (symbolVal (Proxy @key)) .= a ]+ -- | A value together with response metadata. --   The type is annotated with the JSON key of the value. data WithMeta (key :: Symbol) a = WithMeta@@ -780,6 +808,15 @@   | CreateVolume   | AttachVolume   | ChangeDNSPtr+  | CreateRRSet+  | DeleteRRSet+  | SetRRSetTTL+  | SetRRSetRecords+  | AddRRSetRecords+  | UpdateRRSetRecords+  | DeleteRRSetRecords+  | EnableRescue+  | DisableRescue     deriving Show  instance FromJSON ActionCommand where@@ -795,6 +832,15 @@     "create_volume" -> pure CreateVolume     "attach_volume" -> pure AttachVolume     "change_dns_ptr" -> pure ChangeDNSPtr+    "create_rrset" -> pure CreateRRSet+    "delete_rrset" -> pure DeleteRRSet+    "change_rrset_ttl" -> pure SetRRSetTTL+    "set_rrset_records" -> pure SetRRSetRecords+    "add_rrset_records" -> pure AddRRSetRecords+    "update_rrset_records" -> pure UpdateRRSetRecords+    "remove_rrset_records" -> pure DeleteRRSetRecords+    "enable_rescue" -> pure EnableRescue+    "disable_rescue" -> pure DisableRescue     _ -> fail $ "Unknown action command " ++ Text.unpack t  -- | Action identifier.@@ -812,6 +858,8 @@   | ResourceFirewallID FirewallID     -- | Image ID.   | ResourceImageID ImageID+    -- | Zone ID.+  | ResourceZoneID ZoneID     deriving Show  instance FromJSON ResourceID where@@ -823,6 +871,7 @@       "primary_ip" -> ResourcePrimaryIPID <$> o .: "id"       "firewall" -> ResourceFirewallID <$> o .: "id"       "image" -> ResourceImageID <$> o .: "id"+      "zone" -> ResourceZoneID <$> o .: "id"       _ -> fail $ "Unknown resource type: " ++ Text.unpack t  -- | Action.@@ -893,6 +942,180 @@   actionsOf = foldMap actionsOf  ----------------------------------------------------------------------------------------------------+-- DNS+----------------------------------------------------------------------------------------------------++-- | DNS zone identifier.+newtype ZoneID = ZoneID Int deriving (Eq, Ord, Show, FromJSON, ToJSON)++-- | DNS zone.+data Zone = Zone+  { zoneID :: ZoneID+  , zoneLabels :: LabelMap+  , zoneName :: Text+  , zoneRecordCount :: Int+  , zoneDefaultTTL :: Int+    } deriving Show++instance FromJSON Zone where+  parseJSON = JSON.withObject "Zone" $ \o -> Zone+    <$> o .: "id"+    <*> o .: "labels"+    <*> o .: "name"+    <*> o .: "record_count"+    <*> o .: "ttl"++getZones :: Token -> Maybe Int -> IO (WithMeta "zones" [Zone])+getZones = cloudQuery "GET" "/zones" noBody++getZone :: Token -> ZoneID -> IO Zone+getZone token (ZoneID i) = withoutKey @"zone" <$>+  cloudQuery "GET" ("/zones/" <> fromString (show i)) noBody token Nothing++-- | Download zone file in BIND format.+downloadZoneFile :: Token -> ZoneID -> IO Text+downloadZoneFile token (ZoneID i) = withoutKey @"zonefile" <$>+  cloudQuery "GET" ("/zones/" <> fromString (show i) <> "/zonefile") noBody token Nothing++data RRSetType =+  A | AAAA | CAA | CNAME | DS | HINFO | HTTPS | MX | NS | PTR | RP | SOA | SRV | SVCB | TLSA | TXT+  deriving (Eq, Show)++instance FromJSON RRSetType where+  parseJSON = JSON.withText "RRSetType" $ \t ->+    case t of+      "A" -> pure A+      "AAAA" -> pure AAAA+      "CAA" -> pure CAA+      "CNAME" -> pure CNAME+      "DS" -> pure DS+      "HINFO" -> pure HINFO+      "HTTPS" -> pure HTTPS+      "MX" -> pure MX+      "NS" -> pure NS+      "PTR" -> pure PTR+      "RP" -> pure RP+      "SOA" -> pure SOA+      "SRV" -> pure SRV+      "SVCB" -> pure SVCB+      "TLSA" -> pure TLSA+      "TXT" -> pure TXT+      _ -> fail $ "Invalid RRSetType: " <> Text.unpack t++instance ToJSON RRSetType where+  toJSON = JSON.toJSON . show++data RRSetID = RRSetID Text RRSetType deriving (Eq, Show)++data DNSRecord = DNSRecord+  { dnsRecordValue :: Text+  , dnsRecordComment :: Text+    } deriving Show++instance FromJSON DNSRecord where+  parseJSON = JSON.withObject "DNSRecord" $ \o ->+    liftA2 DNSRecord (o .: "value") (o .: "comment")++instance ToJSON DNSRecord where+  toJSON (DNSRecord v c) = JSON.object [ "value" .= v, "comment" .= c ]++data RRSet = RRSet+  { rrSetID :: RRSetID+  , rrSetZoneID :: ZoneID+  , rrSetRecords :: [DNSRecord]+  , rrSetTTL :: Maybe Int+  , rrSetLabels :: LabelMap+    } deriving Show++instance FromJSON RRSet where+  parseJSON = JSON.withObject "RRSet" $ \o -> RRSet+    <$> liftA2 RRSetID (o .: "name") (o .: "type")+    <*> o .: "zone"+    <*> o .: "records"+    <*> o .:? "ttl"+    <*> o .: "labels"++instance ToJSON RRSet where+  toJSON rrset =+    let RRSetID name typ = rrSetID rrset+    in  JSON.object+          [ "name" .= name+          , "type" .= typ+          , "zone" .= rrSetZoneID rrset+          , "records" .= rrSetRecords rrset+          , "ttl" .= rrSetTTL rrset+          , "labels" .= rrSetLabels rrset+            ]++-- | Get RRSets from a zone.+getRRSets :: Token -> ZoneID -> Maybe Int -> IO (WithMeta "rrsets" [RRSet])+getRRSets token (ZoneID i) =+  cloudQuery "GET" ("/zones/" <> fromString (show i) <> "/rrsets") noBody token++-- | Get an RRSet.+getRRSet :: Token -> ZoneID -> RRSetID -> IO RRSet+getRRSet token (ZoneID i) (RRSetID name typ) = withoutKey @"rrset" <$>+  cloudQuery "GET"+    ("/zones/" <> fromString (show i)+               <> "/rrsets/" <> Text.encodeUtf8 name+               <> "/" <> fromString (show typ)) noBody token Nothing++-- | Create an RRSet.+createRRSet :: Token -> RRSet -> IO Action+createRRSet token rrset = withoutKey @"action" <$> do+  body <-+    case JSON.toJSON rrset of+      JSON.Object km -> pure $ JSON.Object $ JSONKeyMap.delete "zone" km+      _ -> fail "toJSON rrset returned a non-object!"+  let ZoneID i = rrSetZoneID rrset+      path = "/zones/" <> fromString (show i) <> "/rrsets"+  cloudQuery "POST" path (Just body) token Nothing++-- | Delete an RRSet.+deleteRRSet :: Token -> ZoneID -> RRSetID -> IO Action+deleteRRSet token (ZoneID i) (RRSetID name typ) = withoutKey @"action" <$>+  let path = "/zones/" <> fromString (show i)+          <> "/rrsets/" <> Text.encodeUtf8 name+          <> "/" <> fromString (show typ)+  in  cloudQuery "DELETE" path noBody token Nothing++genericRRSetUpdate+  :: forall key a+   . (KnownSymbol key, ToJSON a)+  => ByteString -- ^ Route name+  -> Proxy key+  -> Token -> ZoneID -> RRSetID -> a -> IO Action+genericRRSetUpdate rname _ token (ZoneID i) (RRSetID name typ) a = withoutKey @"action" <$>+  let path = "/zones/" <> fromString (show i)+          <> "/rrsets/" <> Text.encodeUtf8 name+          <> "/" <> fromString (show typ)+          <> "/actions/" <> rname+  in  cloudQuery "POST" path (Just $ WithKey @key a) token Nothing++-- | Set the TTL of an RRSet.+setRRSetTTL :: Token -> ZoneID -> RRSetID -> Int -> IO Action+setRRSetTTL = genericRRSetUpdate "change_ttl" (Proxy @"ttl")++-- | Set the DNS records of an RRSet. Any previoulsy held records are deleted.+setRRSetRecords :: Token -> ZoneID -> RRSetID -> [DNSRecord] -> IO Action+setRRSetRecords = genericRRSetUpdate "set_records" (Proxy @"records")++-- | Add DNS records to an RRSet. If the RRSet does not exist, it's created.+addRRSetRecords :: Token -> ZoneID -> RRSetID -> [DNSRecord] -> IO Action+addRRSetRecords = genericRRSetUpdate "add_records" (Proxy @"records")++-- | Update DNS records from an RRSet.+updateRRSetRecords :: Token -> ZoneID -> RRSetID -> [DNSRecord] -> IO Action+updateRRSetRecords = genericRRSetUpdate "update_records" (Proxy @"records")++-- | Delete DNS records from an RRSet. If after deleting the records the+--   RRSet becomes empty, the RRSet is deleted as well.+deleteRRSetRecords :: Token -> ZoneID -> RRSetID -> [Text] -> IO Action+deleteRRSetRecords token zid rid xs =+  genericRRSetUpdate "remove_records" (Proxy @"records") token zid rid $+    WithKey @"value" <$> xs++---------------------------------------------------------------------------------------------------- -- Firewalls ---------------------------------------------------------------------------------------------------- @@ -1299,7 +1522,7 @@       <*> o .: "disk_size"       <*> o .: "created"       <*> o .: "deleted"-      <*> o .: "deprecated"+      <*> (fmap (withoutKey @"unavailable_after") <$> o .: "deprecation")       <*> o .: "labels"  -- | Get images.@@ -1414,7 +1637,11 @@       <*> o .: "created"       <*> o .: "id"       <*> (case iptype :: Text of-             "ipv4" -> Left <$> (o .: "dns_ptr" >>= JSON.parseJSON . head)+             "ipv4" -> do+               xs <- o .: "dns_ptr"+               case xs of+                 [] -> fail "Empty dns_ptr list."+                 x : _ -> Left <$> JSON.parseJSON x              "ipv6" -> Right <$> JSON.parseJSON (JSON.Object o)              _ -> fail $ "Invalid ip type: " ++ Text.unpack iptype              )@@ -1779,7 +2006,7 @@   , newServerNetworks = []   , newServerEnableIPv4 = True   , newServerEnableIPv6 = True-  , newServerType = ServerTypeID 114 -- CX23+  , newServerType = ServerTypeID 118 -- CPX02   , newServerSSHKeys = []   , newServerStart = True   , newServerVolumes = []@@ -1900,6 +2127,36 @@         ("/servers/" <> fromString (show i) <> "/actions/change_type")         (Just body) token Nothing +-- | Values returned when enabling rescue mode on as server.+data RescueModeEnabled = RescueModeEnabled+  { -- | A password that can be used to access the rescue system.+    rescueModePassword :: Text+    -- | The action of enabling rescue mode.+  , rescueModeAction :: Action+    }++instance FromJSON RescueModeEnabled where+  parseJSON = JSON.withObject "RescueModeEnabled" $ \o ->+    liftA2 RescueModeEnabled (o .: "root_password") (o .: "action")++-- | Enable rescue mode on a server. Once enabled, the next time the server boots,+--   it will do so into the rescue system.+enableRescueMode :: Token -> ServerID -> [SSHKeyID] -> IO RescueModeEnabled+enableRescueMode token (ServerID i) keys =+  cloudQuery "POST"+    ("/servers/" <> fromString (show i) <> "/actions/enable_rescue")+    (Just $ WithKey @"ssh-keys" keys) token Nothing++-- | Disable rescue mode so that next time the server boots it does so from disk.+--+--   Note that rescue mode is automatically disabled after 60 minutes or after+--   booting into the rescue system.+disableRescueMode :: Token -> ServerID -> IO Action+disableRescueMode token (ServerID i) = withoutKey @"action" <$>+  cloudQuery "POST"+    ("/servers/" <> fromString (show i) <> "/actions/disable_rescue")+    noBody token Nothing+ ---------------------------------------------------------------------------------------------------- -- Server Types ----------------------------------------------------------------------------------------------------@@ -1939,7 +2196,6 @@   { serverArchitecture :: Architecture   , serverCores :: Int   , serverCPUType :: CPUType-  , serverDeprecated :: Bool   , serverTypeDescription :: Text     -- | Disk size a server of this type has in GB.   , serverDisk :: Int@@ -1956,7 +2212,6 @@     <$> o .: "architecture"     <*> o .: "cores"     <*> o .: "cpu_type"-    <*> (fromMaybe False <$> o .: "deprecated")     <*> o .: "description"     <*> o .: "disk"     <*> o .: "id"
− src/Hetzner/DNS.hs
@@ -1,303 +0,0 @@--------------------------------------------------------------------------------------------------------- | Client for the Hetzner DNS API.------   /This module is currently outdated. Hetzner deprecated this API./---   /Its replacement has not been implemented in this library as of yet./---   /Contributions are welcome./-module Hetzner.DNS {-# DEPRECATED "DNS functionality moved to Cloud." #-} (-    -- * Tokens-    Token (..)-  , getTokenFromEnv-    -- * Hetzner DNS-    -- ** Zones-  , ZoneID (..)-  , ZoneStatus (..)-  , Zone (..)-  , getZones-  , getZone-  , updateZone-  , deleteZone-    -- ** Records-  , RecordID (..)-  , RecordType (..)-  , allRecordTypes-  , Record (..)-  , getRecords-  , getRecord-  , createRecord-  , updateRecord-  , deleteRecord-    -- * Exceptions-  , DNSException (..)-    -- * Streaming-  , streamPages-  , streamToList-    -- * Generic interface-    -- ** Generic queries-  , dnsQuery-  , noBody-    -- ** JSON Wrappers-  , WithKey (..)-  , WithMeta (..)-    -- ** Response metadata-  , ResponseMeta (..)-  , Pagination (..)-  ) where--import Hetzner.Cloud-  ( WithKey (..), WithMeta (..)-  , ResponseMeta (..), Pagination (..), noBody-  , streamPages, streamToList-    )--- base-import Data.String (IsString, fromString)-import Data.Maybe (maybeToList)-import System.Environment qualified as System-import Control.Exception (Exception, throwIO)-import Data.Foldable (find)--- bytestring-import Data.ByteString (ByteString)--- aeson-import Data.Aeson (FromJSON, ToJSON, (.:), (.:?), (.!=), (.=))-import Data.Aeson qualified as JSON--- http-conduit-import Network.HTTP.Simple as HTTP--- time-import Data.Time (ZonedTime, parseTimeM, defaultTimeLocale)--- text-import Data.Text (Text)-import Data.Text qualified as Text-import Data.Text.Encoding (encodeUtf8)---- | A token used to authenticate requests.------   You can create one in the [Hetzner DNS Console](https://dns.hetzner.com/settings/api-token).-newtype Token = Token ByteString deriving (Show, Eq, Ord)--instance IsString Token where-  fromString = Token . fromString---- | Lookup 'Token' from the environment variable @HETZNER_DNS_TOKEN@.-getTokenFromEnv :: IO (Maybe Token)-getTokenFromEnv = fmap fromString <$> System.lookupEnv "HETZNER_DNS_TOKEN"---- | Exception produced while performing a request to Hetzner DNS.-data DNSException =-    DNSError (HTTP.Response ByteString)-  | JSONError (HTTP.Response ByteString) String-    deriving Show--instance Exception DNSException---- | Generic Hetzner DNS query.-dnsQuery-  :: (ToJSON body, FromJSON a)-  => ByteString -- ^ Method.-  -> ByteString -- ^ Path.-  -> Maybe body -- ^ Request body. You may use 'noBody' to skip.-  -> HTTP.Query -- ^ Additional query options.-  -> Token -- ^ Authorization token.-  -> Maybe Int -- ^ Page.-  -> IO a-dnsQuery method path mbody query (Token token) mpage = do-  let req = HTTP.setRequestMethod method-          $ HTTP.setRequestSecure True-          $ HTTP.setRequestHost "dns.hetzner.com"-          $ HTTP.setRequestPort 443-          $ HTTP.setRequestPath ("/api/v1" <> path)-          $ maybe id HTTP.setRequestBodyJSON mbody-          $ HTTP.addRequestHeader "Auth-API-Token" token-          $ HTTP.addToRequestQueryString query-          $ maybe id (\page -> HTTP.addToRequestQueryString-                                 [("page", Just $ fromString $ show page)]) mpage-          $ HTTP.defaultRequest-  resp <- HTTP.httpBS req-  let body = HTTP.getResponseBody resp-  case divMod (HTTP.getResponseStatusCode resp) 100 of-    (2,m) ->-      let body' = if m == 4 then "{}" else body-      in  case JSON.eitherDecodeStrict body' of-            Left err -> throwIO $ JSONError resp err-            Right x -> pure x-    _ -> throwIO $ DNSError resp--------------------------------------------------------------------------------------------------------- Time parser-------------------------------------------------------------------------------------------------------newtype DNSTime = DNSTime { dnsTime :: ZonedTime }--instance FromJSON DNSTime where-  parseJSON = JSON.withText "DNSTime" $-    let format = "%F %T%Q %z %Z"-    in  fmap DNSTime . parseTimeM False defaultTimeLocale format . Text.unpack--------------------------------------------------------------------------------------------------------- Zones--------------------------------------------------------------------------------------------------------- | Zone identifier.-newtype ZoneID = ZoneID Text deriving (Eq, Ord, Show, FromJSON, ToJSON)---- | Status of a 'Zone'.-data ZoneStatus = Verified | Failed | Pending deriving Show--instance FromJSON ZoneStatus where-  parseJSON = JSON.withText "ZoneStatus" $ \t -> case t of-    "verified" -> pure Verified-    "failed" -> pure Failed-    "pending" -> pure Pending-    _ -> fail $ "Invalid zone status: " ++ Text.unpack t---- | DNS zone.-data Zone = Zone-  { zoneCreated :: ZonedTime-  , zoneModified :: ZonedTime-  , zoneID :: ZoneID-  , zoneName :: Text-  , zoneIsSecondary :: Bool-  , zoneStatus :: ZoneStatus-  , zoneRecordCount :: Int-  , zoneTTL :: Int-    } deriving Show--instance FromJSON Zone where-  parseJSON = JSON.withObject "Zone" $ \o -> Zone-    <$> (dnsTime <$> o .: "created")-    <*> (dnsTime <$> o .: "modified")-    <*> o .: "id"-    <*> o .: "name"-    <*> o .: "is_secondary_dns"-    <*> o .: "status"-    <*> o .: "records_count"-    <*> o .: "ttl"---- | Get zones.-getZones :: Token -> Maybe Int -> IO (WithMeta "zones" [Zone])-getZones = dnsQuery "GET" "/zones" noBody []---- | Get a single zone.-getZone :: Token -> ZoneID -> IO Zone-getZone token (ZoneID i) = withoutKey @"zone" <$>-  dnsQuery "GET" ("/zones/" <> encodeUtf8 i) noBody [] token Nothing---- | Update a zone's name and TTL.-updateZone-  :: Token-  -> ZoneID -- ^ ID of zone to update.-  -> Text -- ^ New zone name.-  -> Maybe Int -- ^ New TTL. If not provided, it won't change.-  -> IO Zone-updateZone token (ZoneID i) name mttl = withoutKey @"zone" <$>-  let body = JSON.object $ ("name" .= name) : maybeToList (fmap ("ttl" .=) mttl)-  in  dnsQuery "PUT" ("/zones/" <> encodeUtf8 i) (Just body) [] token Nothing---- | Delete a zone.-deleteZone :: Token -> ZoneID -> IO ()-deleteZone token (ZoneID i) = dnsQuery "DELETE" ("/zones/" <> encodeUtf8 i) noBody [] token Nothing--------------------------------------------------------------------------------------------------------- Records--------------------------------------------------------------------------------------------------------- | A record identifier.-newtype RecordID = RecordID Text deriving (Eq, Ord, Show, FromJSON, ToJSON)---- | Record type.-data RecordType =-  A | AAAA | CAA | CNAME | DANE | DS | HINFO | MX | NS | PTR | RP | SOA | SRV | TLS | TXT-  deriving (Eq, Show, Enum)---- | List with all supported record types.-allRecordTypes :: [RecordType]-allRecordTypes = [A ..]--instance FromJSON RecordType where-  parseJSON = JSON.withText "RecordType" $ \t ->-    case find ((==) t . Text.pack . show) allRecordTypes of-      Just rtype -> pure rtype-      _ -> fail $ "Invalid record type: " ++ Text.unpack t--instance ToJSON RecordType where-  toJSON = JSON.String . Text.pack . show---- | A DNS record.-data Record = Record-  { recordCreated :: ZonedTime-  , recordModified :: ZonedTime-  , recordID :: RecordID-  , recordName :: Text-  , recordType :: RecordType-  , recordValue :: Text-  , recordTTL :: Int-    -- | ID of the zone this record is associated with.-  , recordZone :: ZoneID-    } deriving Show--instance FromJSON Record where-  parseJSON = JSON.withObject "Record" $ \o -> Record-    <$> (dnsTime <$> o .: "created")-    <*> (dnsTime <$> o .: "modified")-    <*> o .: "id"-    <*> o .: "name"-    <*> o .: "type"-    <*> o .: "value"-    <*> o .:? "ttl" .!= 86400-    <*> o .: "zone_id"---- | Get DNS records.-getRecords-  :: Token-  -> Maybe ZoneID -- ^ Optionally filter by zone.-  -> IO [Record]-getRecords token mzone = withoutKey @"records" <$>-  let query = maybe [] (\(ZoneID zone) -> [("zone_id", Just $ encodeUtf8 zone)]) mzone-  in  dnsQuery "GET" "/records" noBody query token Nothing---- | Get a single DNS record.-getRecord :: Token -> RecordID -> IO Record-getRecord token (RecordID i) = withoutKey @"record" <$>-  dnsQuery "GET" ("/records/" <> encodeUtf8 i) noBody [] token Nothing---- | Create a DNS record.-createRecord-  :: Token-  -> ZoneID -- ^ Zone to add the record to.-  -> Text -- ^ Record name.-  -> RecordType -- ^ Record type.-  -> Text -- ^ Record value.-  -> Maybe Int -- ^ Optional TTL.-  -> IO Record-createRecord token zone name rtype value mttl = withoutKey @"record" <$>-  let body = JSON.object $-        [ "zone_id" .= zone-        , "name" .= name-        , "type" .= rtype-        , "value" .= value-          ] ++ maybe [] (pure . ("ttl" .=)) mttl-  in  dnsQuery "POST" "/records" (Just body) [] token Nothing---- | Update a DNS record.-updateRecord-  :: Token-  -> RecordID -- ^ Record to update.-  -> ZoneID -- ^ Zone for the record.-  -> Text -- ^ New record name.-  -> RecordType -- ^ New recored type.-  -> Text -- ^ New record value.-  -> Maybe Int -- ^ Optinally, a new TTL.-  -> IO Record-updateRecord token (RecordID i) zone name rtype value mttl = withoutKey @"record" <$>-  let body = JSON.object $-        [ "zone_id" .= zone-        , "name" .= name-        , "type" .= rtype-        , "value" .= value-          ] ++ maybe [] (pure . ("ttl" .=)) mttl-  in  dnsQuery "PUT" ("/records/" <> encodeUtf8 i) (Just body) [] token Nothing---- | Delete a DNS record.-deleteRecord :: Token -> RecordID -> IO ()-deleteRecord token (RecordID i) =-  dnsQuery "DELETE" ("/records/" <> encodeUtf8 i) noBody [] token Nothing