packages feed

google-cloud-compute-0.1.0.0: src/Google/Cloud/Compute/Firewall.hs

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}

{- |
Module      : Google.Cloud.Compute.Firewall
Copyright   : (c) 2025 
License     : MIT
Maintainer  : 
Stability   : experimental

This module provides a Haskell client for Google Cloud Compute Engine Firewall operations.
-}
module Google.Cloud.Compute.Firewall
  ( listFirewalls
  , FirewallList (..)
  , FirewallMeta (..)
  , AllowedFirewall (..)
  , createFirewall
  , CreateFirewallOps (..)
  , CreateFirewallResp
  , Operation (..)
  , Warning (..)
  , WarningData (..)
  , LogConfig (..)
  , DeniedFirewall (..)
  , defaultCreateFirewallOps
  ) where

import Data.Aeson
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BS8
import Data.Maybe
import Google.Cloud.Common.Core
import Google.Cloud.Compute.Common

-- | Represents a warning object in API responses
data Warning = Warning
  { code :: String
  -- ^ Warning type identifier
  , message :: String
  -- ^ Human-readable warning message
  , data_ :: Maybe [WarningData]
  -- ^ Optional metadata about the warning
  }
  deriving (Show, Eq)

-- | Key-value pair providing additional context for warnings
data WarningData = WarningData
  { key :: String
  -- ^ Metadata key describing the warning aspect
  , value :: String
  -- ^ Metadata value associated with the key
  }
  deriving (Show, Eq)

-- | Response format for firewall listing operations
data FirewallList = FirewallList
  { kind :: Maybe String
  -- ^ Type identifier ("compute#firewallList")
  , id :: Maybe String
  -- ^ Unique identifier for the resource
  , items :: Maybe [FirewallMeta]
  -- ^ List of firewall rules
  , nextPageToken :: Maybe String
  -- ^ Pagination token for next page
  , selfLink :: Maybe String
  -- ^ Server-defined URL for the resource
  , warning :: Maybe Warning
  -- ^ Any warnings about the result
  }
  deriving (Show, Eq)

-- | Represents a Google Cloud Firewall Rule
data FirewallMeta = FirewallMeta
  { kind :: Maybe String
  -- ^ Type identifier ("compute#firewall")
  , id :: Maybe String
  -- ^ Unique numeric ID
  , creationTimestamp :: Maybe String
  -- ^ ISO 8601 creation timestamp
  , name :: Maybe String
  -- ^ User-provided firewall name
  , description :: Maybe String
  -- ^ Optional rule description
  , network :: Maybe String
  -- ^ Network URL the rule applies to
  , priority :: Maybe Int
  -- ^ Rule priority (0-65535, lower=higher)
  , sourceRanges :: Maybe [String]
  -- ^ Allowed source IP ranges (CIDR)
  , sourceTags :: Maybe [String]
  -- ^ Source instance tags
  , targetTags :: Maybe [String]
  -- ^ Target instance tags
  , allowed :: Maybe [AllowedFirewall]
  -- ^ Allow rules
  , denied :: Maybe [DeniedFirewall]
  -- ^ Deny rules
  , direction :: Maybe String
  -- ^ Traffic direction (INGRESS/EGRESS)
  , logConfig :: Maybe LogConfig
  -- ^ Logging configuration
  , disabled :: Maybe Bool
  -- ^ Whether rule is disabled
  , selfLink :: Maybe String
  -- ^ Server-defined URL for the rule
  }
  deriving (Show, Eq)

-- | Allow rule specification
data AllowedFirewall = AllowedFirewall
  { iPProtocol :: String
  -- ^ Protocol name (e.g., "tcp", "udp", "icmp")
  , ports :: Maybe [String]
  -- ^ Optional port numbers/ranges (e.g., "80", "8000-8080")
  }
  deriving (Show, Eq)

-- | Deny rule specification
data DeniedFirewall = DeniedFirewall
  { iPProtocol :: String
  -- ^ Protocol name (e.g., "tcp", "udp")
  , ports :: Maybe [String]
  -- ^ Optional port numbers/ranges to block
  }
  deriving (Show, Eq)

-- | Firewall rule logging configuration
data LogConfig = LogConfig
  { enable :: Bool
  -- ^ Whether to enable logging
  , metadata :: Maybe String
  -- ^ Log metadata level (EXCLUDE_ALL_METADATA or INCLUDE_ALL_METADATA)
  }
  deriving (Show, Eq)

-- | Long-running operation resource
data Operation = Operation
  { id :: Maybe String
  -- ^ Unique operation ID
  , name :: Maybe String
  -- ^ Operation name
  , operationType :: Maybe String
  -- ^ Operation type (insert, delete, etc.)
  , targetLink :: Maybe String
  -- ^ URL of resource being modified
  , status :: Maybe String
  -- ^ Current status (DONE, RUNNING, etc.)
  , progress :: Maybe Int
  -- ^ Operation progress (0-100)
  , insertTime :: Maybe String
  -- ^ ISO 8601 creation timestamp
  , startTime :: Maybe String
  -- ^ ISO 8601 start timestamp
  , endTime :: Maybe String
  -- ^ ISO 8601 completion timestamp
  , error_ :: Maybe OperationError
  -- ^ Error details if failed
  , warnings :: Maybe [Warning]
  -- ^ Warnings during operation
  , selfLink :: Maybe String
  -- ^ Server-defined URL for the operation
  }
  deriving (Show, Eq)

-- | Error information from failed operations
data OperationError = OperationError
  { errors :: [ErrorDetail]
  -- ^ List of error details
  }
  deriving (Show, Eq)

-- | Detailed error information
data ErrorDetail = ErrorDetail
  { code :: String
  -- ^ Error type identifier
  , message :: String
  -- ^ Human-readable error message
  , location :: Maybe String
  -- ^ Location in request that triggered error
  }
  deriving (Show, Eq)

-- | Firewall creation options
data CreateFirewallOps = CreateFirewallOps
  { name :: String
  -- ^ Required firewall rule name
  , description :: Maybe String
  -- ^ Optional description
  , network :: Maybe String
  -- ^ Network URL (default: "global/networks/default")
  , priority :: Maybe Int
  -- ^ Priority (default: 1000)
  , sourceRanges :: Maybe [String]
  -- ^ Source IP ranges (CIDR format)
  , sourceTags :: Maybe [String]
  -- ^ Source instance tags
  , targetTags :: Maybe [String]
  -- ^ Target instance tags
  , allowed :: Maybe [AllowedFirewall]
  -- ^ Traffic allow rules
  , denied :: Maybe [DeniedFirewall]
  -- ^ Traffic deny rules
  , direction :: Maybe String
  -- ^ Traffic direction (default: INGRESS)
  , logConfig :: Maybe LogConfig
  -- ^ Logging configuration
  , disabled :: Maybe Bool
  -- ^ Initial disabled state (default: False)
  }
  deriving (Show, Eq)

-- | Alias for operation response from firewall creation
type CreateFirewallResp = Operation

-- JSON Instances

instance FromJSON WarningData where
  parseJSON = withObject "WarningData" $ \v ->
    WarningData
      <$> v .: "key"
      <*> v .: "value"

instance ToJSON WarningData where
  toJSON (WarningData k v) =
    object
      [ "key" .= k
      , "value" .= v
      ]

instance FromJSON Warning where
  parseJSON = withObject "Warning" $ \v ->
    Warning
      <$> v .: "code"
      <*> v .: "message"
      <*> v .:? "data"

instance ToJSON Warning where
  toJSON (Warning c m d) =
    object
      [ "code" .= c
      , "message" .= m
      , "data" .= d
      ]

instance FromJSON LogConfig where
  parseJSON = withObject "LogConfig" $ \v ->
    LogConfig
      <$> v .: "enable"
      <*> v .:? "metadata"

instance ToJSON LogConfig where
  toJSON (LogConfig e m) =
    object
      [ "enable" .= e
      , "metadata" .= m
      ]

instance FromJSON AllowedFirewall where
  parseJSON = withObject "AllowedFirewall" $ \v ->
    AllowedFirewall
      <$> v .: "IPProtocol"
      <*> v .:? "ports"

instance ToJSON AllowedFirewall where
  toJSON (AllowedFirewall i p) =
    object
      [ "IPProtocol" .= i
      , "ports" .= p
      ]

instance FromJSON DeniedFirewall where
  parseJSON = withObject "DeniedFirewall" $ \v ->
    DeniedFirewall
      <$> v .: "IPProtocol"
      <*> v .:? "ports"

instance ToJSON DeniedFirewall where
  toJSON (DeniedFirewall i p) =
    object
      [ "IPProtocol" .= i
      , "ports" .= p
      ]

instance FromJSON FirewallMeta where
  parseJSON = withObject "FirewallMeta" $ \v ->
    FirewallMeta
      <$> v .:? "kind"
      <*> v .:? "id"
      <*> v .:? "creationTimestamp"
      <*> v .:? "name"
      <*> v .:? "description"
      <*> v .:? "network"
      <*> v .:? "priority"
      <*> v .:? "sourceRanges"
      <*> v .:? "sourceTags"
      <*> v .:? "targetTags"
      <*> v .:? "allowed"
      <*> v .:? "denied"
      <*> v .:? "direction"
      <*> v .:? "logConfig"
      <*> v .:? "disabled"
      <*> v .:? "selfLink"

instance ToJSON FirewallMeta where
  toJSON
    ( FirewallMeta
        k
        i
        c
        name_
        description_
        network_
        priority_
        sourceRanges_
        sourceTags_
        targetTags_
        allowed_
        denied_
        direction_
        logConfig_
        disabled_
        selfLink_
      ) =
      object
        [ "kind" .= k
        , "id" .= i
        , "creationTimestamp" .= c
        , "name" .= name_
        , "description" .= description_
        , "network" .= network_
        , "priority" .= priority_
        , "sourceRanges" .= sourceRanges_
        , "sourceTags" .= sourceTags_
        , "targetTags" .= targetTags_
        , "allowed" .= allowed_
        , "denied" .= denied_
        , "direction" .= direction_
        , "logConfig" .= logConfig_
        , "disabled" .= disabled_
        , "selfLink" .= selfLink_
        ]

instance FromJSON FirewallList where
  parseJSON = withObject "FirewallList" $ \v ->
    FirewallList
      <$> v .:? "kind"
      <*> v .:? "id"
      <*> v .:? "items"
      <*> v .:? "nextPageToken"
      <*> v .:? "selfLink"
      <*> v .:? "warning"

instance ToJSON FirewallList where
  toJSON (FirewallList kind_ id_ items_ nextPageToken_ selfLink_ warning_) =
    object
      [ "kind" .= kind_
      , "id" .= id_
      , "items" .= items_
      , "nextPageToken" .= nextPageToken_
      , "selfLink" .= selfLink_
      , "warning" .= warning_
      ]

instance FromJSON ErrorDetail where
  parseJSON = withObject "ErrorDetail" $ \v ->
    ErrorDetail
      <$> v .: "code"
      <*> v .: "message"
      <*> v .:? "location"

instance ToJSON ErrorDetail where
  toJSON (ErrorDetail code_ message_ location_) =
    object
      [ "code" .= code_
      , "message" .= message_
      , "location" .= location_
      ]

instance FromJSON OperationError where
  parseJSON = withObject "OperationError" $ \v ->
    OperationError
      <$> v .: "errors"

instance ToJSON OperationError where
  toJSON (OperationError errors_) =
    object
      [ "errors" .= errors_
      ]

instance FromJSON Operation where
  parseJSON = withObject "Operation" $ \v ->
    Operation
      <$> v .:? "id"
      <*> v .:? "name"
      <*> v .:? "operationType"
      <*> v .:? "targetLink"
      <*> v .:? "status"
      <*> v .:? "progress"
      <*> v .:? "insertTime"
      <*> v .:? "startTime"
      <*> v .:? "endTime"
      <*> v .:? "error"
      <*> v .:? "warnings"
      <*> v .:? "selfLink"

instance ToJSON Operation where
  toJSON
    ( Operation
        id_
        name_
        operationType_
        targetLink_
        status_
        progress_
        insertTime_
        startTime_
        endTime_
        error1
        warnings_
        selfLink_
      ) =
      object
        [ "id" .= id_
        , "name" .= name_
        , "operationType" .= operationType_
        , "targetLink" .= targetLink_
        , "status" .= status_
        , "progress" .= progress_
        , "insertTime" .= insertTime_
        , "startTime" .= startTime_
        , "endTime" .= endTime_
        , "error" .= error1
        , "warnings" .= warnings_
        , "selfLink" .= selfLink_
        ]

instance ToJSON CreateFirewallOps where
  toJSON
    ( CreateFirewallOps
        name_
        description_
        network_
        priority_
        sourceRanges_
        sourceTags_
        targetTags_
        allowed_
        denied_
        direction_
        logConfig_
        disabled_
      ) =
      object
        [ "name" .= name_
        , "description" .= description_
        , "network" .= network_
        , "priority" .= priority_
        , "sourceRanges" .= sourceRanges_
        , "sourceTags" .= sourceTags_
        , "targetTags" .= targetTags_
        , "allowed" .= allowed_
        , "denied" .= denied_
        , "direction" .= direction_
        , "logConfig" .= logConfig_
        , "disabled" .= disabled_
        ]

-- | Query parameters for listFirewalls
data ListFirewallsQuery = ListFirewallsQuery
  { filter0 :: Maybe String
  , maxResults :: Maybe Int
  , pageToken :: Maybe String
  , orderBy :: Maybe String
  }
  deriving (Show, Eq)

-- | Query parameters for createFirewall
data CreateFirewallQuery = CreateFirewallQuery
  { requestId :: Maybe String
  }
  deriving (Show, Eq)

-- Helper function to convert query to list of (ByteString, Maybe ByteString)
queryToList :: (a -> [(String, String)]) -> a -> [(BS.ByteString, Maybe BS.ByteString)]
queryToList f q = map (\(k, v) -> (BS8.pack k, Just $ BS8.pack v)) (f q)

-- Functions

{- | List firewall rules in a project

Example:

> listFirewalls "my-project" (Just ListFirewallsQuery { filter0 = Just "name=default-allow-*" })
-}
listFirewalls ::
  -- | GCP Project ID
  String ->
  -- | Optional query parameters
  Maybe ListFirewallsQuery ->
  -- | List result or error
  IO (Either String FirewallList)
listFirewalls project mbQuery = do
  let queryParams = maybe [] (queryToList queryToListFunc) mbQuery
  doRequestJSON
    RequestOptions
      { reqMethod = GET
      , reqUrl = googleComputeUrl
      , mbQueryParams = Just queryParams
      , mbReqBody = Nothing
      , mbReqHeaders = Nothing
      , mbReqPath = Just $ "/projects/" <> project <> "/global/firewalls"
      }
  where
    queryToListFunc :: ListFirewallsQuery -> [(String, String)]
    queryToListFunc q =
      catMaybes
        [ ("filter",) <$> filter0 q
        , ("maxResults",) . show <$> maxResults q
        , ("pageToken",) <$> pageToken q
        , ("orderBy",) <$> orderBy q
        ]

{- | Create a new firewall rule

Example:

> createFirewall "my-project"
>   (defaultCreateFirewallOps "allow-http"
>     { allowed = Just [AllowedFirewall "tcp" (Just ["80"])]
>     , sourceRanges = Just ["0.0.0.0/0"]
>   }) Nothing
-}
createFirewall ::
  -- | GCP Project ID
  String ->
  -- | Firewall configuration
  CreateFirewallOps ->
  -- | Optional request ID
  Maybe CreateFirewallQuery ->
  -- | Operation or error
  IO (Either String CreateFirewallResp)
createFirewall project createFirewallOps mbQuery = do
  let queryParams = maybe [] (queryToList queryToListFunc) mbQuery
  doRequestJSON
    RequestOptions
      { reqMethod = POST
      , reqUrl = googleComputeUrl
      , mbQueryParams = Just queryParams
      , mbReqBody = Just $ encode createFirewallOps
      , mbReqHeaders = Nothing
      , mbReqPath = Just $ "/projects/" <> project <> "/global/firewalls"
      }
  where
    queryToListFunc :: CreateFirewallQuery -> [(String, String)]
    queryToListFunc q =
      catMaybes
        [("requestId",) <$> requestId q]

-- | Default firewall creation options
--
-- Creates minimal valid configuration with required name.
-- Additional fields can be added using record update syntax:
--
-- > defaultCreateFirewallOps "web-allow" 
-- >   { description = Just "Allow web traffic"
-- >   , allowed = Just [AllowedFirewall "tcp" (Just ["80","443"])]
-- >   }
defaultCreateFirewallOps :: String -> CreateFirewallOps
defaultCreateFirewallOps firewallName =
  CreateFirewallOps
    { name = firewallName
    , description = Nothing
    , network = Nothing
    , priority = Nothing
    , sourceRanges = Nothing
    , sourceTags = Nothing
    , targetTags = Nothing
    , allowed = Nothing
    , denied = Nothing
    , direction = Nothing
    , logConfig = Nothing
    , disabled = Nothing
    }