packages feed

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

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

This module provides a Haskell client for Google Cloud Compute Engine Disk operations.
-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

module Google.Cloud.Compute.Disk
  ( listDisks
  , insertDisk
  , DiskResponse (..)
  , InsertDiskOps (..)
  , InsertDiskResponse
  , Operation (..)
  , deleteDisk
  , createDiskSnapshot
  , CreateSnapshotResp 
  , CreateSnapshotOps (..)
  , Warning (..)
  , WarningData (..)
  , defaultCreateSnapshotOps
  , defaultInsertOps
  ) where

import Data.Aeson
import qualified Data.ByteString.Char8 as BS
import Data.Map (Map)
import Google.Cloud.Common.Core
import Google.Cloud.Compute.Common
import Network.HTTP.Simple

-- | Represents a warning object returned by Google Cloud API operations
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 disk listing operations
data DiskResponse = DiskResponse
  { kind :: Maybe String         -- ^ Type identifier ("compute#diskList")
  , id :: Maybe String           -- ^ Unique identifier for the resource
  , items :: Maybe [DiskItem]    -- ^ List of disks in the project/zone
  , 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 Persistent Disk
data DiskItem = DiskItem
  { kind :: Maybe String              -- ^ Type identifier ("compute#disk")
  , id :: Maybe String                -- ^ Unique numeric ID
  , creationTimestamp :: Maybe String -- ^ ISO 8601 creation timestamp
  , name :: Maybe String              -- ^ User-provided disk name
  , description :: Maybe String       -- ^ Optional disk description
  , sizeGb :: Maybe String            -- ^ Disk size in gigabytes
  , zone :: Maybe String              -- ^ Zone where the disk resides
  , status :: Maybe String            -- ^ Current status (READY, CREATING, etc.)
  , sourceSnapshot :: Maybe String    -- ^ Source snapshot URL if applicable
  , sourceSnapshotId :: Maybe String  -- ^ Unique ID of source snapshot
  , sourceImage :: Maybe String       -- ^ Source image URL if applicable
  , sourceImageId :: Maybe String     -- ^ Unique ID of source image
  , type_ :: Maybe String             -- ^ Disk type (e.g., "pd-standard")
  , labels :: Maybe (Map String String) -- ^ User-defined labels
  , labelFingerprint :: Maybe String  -- ^ Fingerprint for label operations
  , licenses :: Maybe [String]        -- ^ License resource URLs
  , users :: Maybe [String]           -- ^ Instances currently using the disk
  , guestOsFeatures :: Maybe [GuestOsFeature] -- ^ OS-specific features
  , diskEncryptionKey :: Maybe EncryptionKey -- ^ Disk encryption details
  , sourceDisk :: Maybe String        -- ^ Source disk URL for regional disks
  , sourceDiskId :: Maybe String      -- ^ Unique ID of source disk
  , physicalBlockSizeBytes :: Maybe String -- ^ Physical block size in bytes
  , provisionedIops :: Maybe String   -- ^ Provisioned IOPS for SSD
  , resourcePolicies :: Maybe [String] -- ^ Resource policy URLs
  , selfLink :: Maybe String          -- ^ Server-defined URL for the disk
  }
  deriving (Show, Eq)

-- | Guest OS features enabled on the disk
data GuestOsFeature = GuestOsFeature
  { type_ :: String  -- ^ Feature type (e.g., "UEFI_COMPATIBLE")
  }
  deriving (Show, Eq)

-- | Disk encryption key information
data EncryptionKey = EncryptionKey
  { rawKey :: Maybe String        -- ^ Raw encryption key (base64)
  , rsaEncryptedKey :: Maybe String -- ^ RSA-wrapped encryption key
  , sha256 :: Maybe String        -- ^ RFC 4648 base64 SHA-256 hash
  }
  deriving (Show, Eq)

-- | Long-running operation resource
data Operation = Operation
  { id :: Maybe String          -- ^ Unique operation ID
  , name :: Maybe String        -- ^ Operation name
  , zone :: Maybe String        -- ^ Zone where operation is executed
  , 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)

-- | Disk creation options
data InsertDiskOps = InsertDiskOps
  { name :: String                -- ^ Required disk name
  , sizeGb :: String              -- ^ Required size in gigabytes
  , description :: Maybe String   -- ^ Optional description
  , type_ :: Maybe String         -- ^ Disk type (default: pd-standard)
  , sourceImage :: Maybe String   -- ^ Source image URL for disk creation
  , sourceSnapshot :: Maybe String -- ^ Source snapshot URL for disk creation
  , labels :: Maybe (Map String String) -- ^ Initial labels
  , guestOsFeatures :: Maybe [GuestOsFeature] -- ^ OS features to enable
  , diskEncryptionKey :: Maybe EncryptionKey -- ^ Encryption configuration
  }
  deriving (Show, Eq)

-- | Alias for operation response from disk insertion
type InsertDiskResponse = Operation

-- | Snapshot creation options
data CreateSnapshotOps = CreateSnapshotOps
  { name :: String                -- ^ Required snapshot name
  , description :: Maybe String   -- ^ Optional description
  , labels :: Maybe (Map String String) -- ^ Labels for the snapshot
  , storageLocations :: Maybe [String] -- ^ Preferred storage locations
  , snapshotEncryptionKey :: Maybe EncryptionKey -- ^ Snapshot encryption
  }
  deriving (Show, Eq)

-- | Alias for operation response from snapshot creation
type CreateSnapshotResp = Operation

data ListDisksQuery = ListDisksQuery
  { filter_ :: Maybe String
  , maxResults :: Maybe Int
  , pageToken :: Maybe String
  , orderBy :: Maybe String
  }
  deriving (Show, Eq)

-- | Query parameters for insertDisk
data InsertDiskQuery = InsertDiskQuery
  { sourceImage :: Maybe String
  , sourceImageEncryptionKey :: Maybe EncryptionKey
  }
  deriving (Show, Eq)

-- | Query parameters for deleteDisk
data DeleteDiskQuery = DeleteDiskQuery
  { force :: Maybe Bool
  }
  deriving (Show, Eq)

-- | Query parameters for createDiskSnapshot
data CreateSnapshotQuery = CreateSnapshotQuery
  { guestFlush :: Maybe Bool
  }
  deriving (Show, Eq)

-- JSON Instances

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

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

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

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

instance FromJSON GuestOsFeature where
  parseJSON = withObject "GuestOsFeature" $ \v ->
    GuestOsFeature
      <$> v .: "type"

instance ToJSON GuestOsFeature where
  toJSON (GuestOsFeature type_) =
    object
      [ "type" .= type_
      ]

instance FromJSON EncryptionKey where
  parseJSON = withObject "EncryptionKey" $ \v ->
    EncryptionKey
      <$> v .:? "rawKey"
      <*> v .:? "rsaEncryptedKey"
      <*> v .:? "sha256"

instance ToJSON EncryptionKey where
  toJSON (EncryptionKey rawKey rsaEncryptedKey sha256) =
    object
      [ "rawKey" .= rawKey
      , "rsaEncryptedKey" .= rsaEncryptedKey
      , "sha256" .= sha256
      ]

instance FromJSON DiskItem where
  parseJSON = withObject "DiskItem" $ \v ->
    DiskItem
      <$> v .:? "kind"
      <*> v .:? "id"
      <*> v .:? "creationTimestamp"
      <*> v .:? "name"
      <*> v .:? "description"
      <*> v .:? "sizeGb"
      <*> v .:? "zone"
      <*> v .:? "status"
      <*> v .:? "sourceSnapshot"
      <*> v .:? "sourceSnapshotId"
      <*> v .:? "sourceImage"
      <*> v .:? "sourceImageId"
      <*> v .:? "type"
      <*> v .:? "labels"
      <*> v .:? "labelFingerprint"
      <*> v .:? "licenses"
      <*> v .:? "users"
      <*> v .:? "guestOsFeatures"
      <*> v .:? "diskEncryptionKey"
      <*> v .:? "sourceDisk"
      <*> v .:? "sourceDiskId"
      <*> v .:? "physicalBlockSizeBytes"
      <*> v .:? "provisionedIops"
      <*> v .:? "resourcePolicies"
      <*> v .:? "selfLink"

instance ToJSON DiskItem where
  toJSON
    ( DiskItem
        kind
        id_
        creationTimestamp
        name
        description
        sizeGb
        zone
        status
        sourceSnapshot
        sourceSnapshotId
        sourceImage
        sourceImageId
        type_
        labels
        labelFingerprint
        licenses
        users
        guestOsFeatures
        diskEncryptionKey
        sourceDisk
        sourceDiskId
        physicalBlockSizeBytes
        provisionedIops
        resourcePolicies
        selfLink
      ) =
      object
        [ "kind" .= kind
        , "id" .= id_
        , "creationTimestamp" .= creationTimestamp
        , "name" .= name
        , "description" .= description
        , "sizeGb" .= sizeGb
        , "zone" .= zone
        , "status" .= status
        , "sourceSnapshot" .= sourceSnapshot
        , "sourceSnapshotId" .= sourceSnapshotId
        , "sourceImage" .= sourceImage
        , "sourceImageId" .= sourceImageId
        , "type" .= type_
        , "labels" .= labels
        , "labelFingerprint" .= labelFingerprint
        , "licenses" .= licenses
        , "users" .= users
        , "guestOsFeatures" .= guestOsFeatures
        , "diskEncryptionKey" .= diskEncryptionKey
        , "sourceDisk" .= sourceDisk
        , "sourceDiskId" .= sourceDiskId
        , "physicalBlockSizeBytes" .= physicalBlockSizeBytes
        , "provisionedIops" .= provisionedIops
        , "resourcePolicies" .= resourcePolicies
        , "selfLink" .= selfLink
        ]

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

instance ToJSON DiskResponse where
  toJSON (DiskResponse 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 .:? "zone"
      <*> 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
        zone
        operationType
        targetLink
        status
        progress
        insertTime
        startTime
        endTime
        error_
        warnings
        selfLink
      ) =
      object
        [ "id" .= id_
        , "name" .= name
        , "zone" .= zone
        , "operationType" .= operationType
        , "targetLink" .= targetLink
        , "status" .= status
        , "progress" .= progress
        , "insertTime" .= insertTime
        , "startTime" .= startTime
        , "endTime" .= endTime
        , "error" .= error_
        , "warnings" .= warnings
        , "selfLink" .= selfLink
        ]

instance ToJSON InsertDiskOps where
  toJSON
    ( InsertDiskOps
        name
        sizeGb
        description
        type_
        sourceImage
        sourceSnapshot
        labels
        guestOsFeatures
        diskEncryptionKey
      ) =
      object
        [ "name" .= name
        , "sizeGb" .= sizeGb
        , "description" .= description
        , "type" .= type_
        , "sourceImage" .= sourceImage
        , "sourceSnapshot" .= sourceSnapshot
        , "labels" .= labels
        , "guestOsFeatures" .= guestOsFeatures
        , "diskEncryptionKey" .= diskEncryptionKey
        ]

instance ToJSON CreateSnapshotOps where
  toJSON (CreateSnapshotOps name description labels storageLocations snapshotEncryptionKey) =
    object
      [ "name" .= name
      , "description" .= description
      , "labels" .= labels
      , "storageLocations" .= storageLocations
      , "snapshotEncryptionKey" .= snapshotEncryptionKey
      ]

-- Functions
-- | List disks in a zone
--
-- Example:
--
-- > listDisks "my-project" "us-central1-a" Nothing
listDisks :: String               -- ^ GCP Project ID
          -> String               -- ^ Zone name
          -> Maybe ListDisksQuery -- ^ Optional query parameters
          -> IO (Either String DiskResponse) -- ^ List result or error}
listDisks project zone_ mbQuery = do
  let queryParams = maybe [] queryToList mbQuery
  doRequestJSON
    RequestOptions
      { reqMethod = GET
      , reqUrl = googleComputeUrl
      , mbQueryParams = Just queryParams
      , mbReqBody = Nothing
      , mbReqHeaders = Nothing
      , mbReqPath = Just $ "/projects/" <> project <> "/zones/" <> zone_ <> "/disks"
      }
  where
    queryToList :: ListDisksQuery -> Query
    queryToList ListDisksQuery {..} =
      [ maybe mempty (\x -> ("filter", Just $ BS.pack x)) filter_
      , maybe mempty (\x -> ("maxResults", Just . BS.pack $ show x)) maxResults
      , maybe mempty (\x -> ("pageToken", Just $ BS.pack x)) pageToken
      , maybe mempty (\x -> ("orderBy", Just $ BS.pack x)) orderBy
      ]

-- | Create a new persistent disk
--
-- Example:
--
-- > insertDisk "my-project" "us-central1-a" (defaultInsertOps "disk1" "100") Nothing
insertDisk :: String               -- ^ GCP Project ID
           -> String               -- ^ Zone name
           -> InsertDiskOps        -- ^ Disk configuration
           -> Maybe InsertDiskQuery -- ^ Optional query parameters
           -> IO (Either String InsertDiskResponse) -- ^ Operation or error}
insertDisk project zone_ insertDiskOps mbQuery = do
  let queryParams = maybe [] queryToList mbQuery
  doRequestJSON
    RequestOptions
      { reqMethod = POST
      , reqUrl = googleComputeUrl
      , mbQueryParams = Just queryParams
      , mbReqBody = Just $ encode insertDiskOps
      , mbReqHeaders = Nothing
      , mbReqPath = Just $ "/projects/" <> project <> "/zones/" <> zone_ <> "/disks"
      }
  where
    queryToList :: InsertDiskQuery -> Query
    queryToList InsertDiskQuery {sourceImage, sourceImageEncryptionKey} =
      [ maybe mempty (\x -> ("sourceImage", Just $ BS.pack x)) sourceImage
      , maybe
          mempty
          (\x -> ("sourceImageEncryptionKey", Just . BS.toStrict $ encode x))
          sourceImageEncryptionKey
      ]

-- | Deletes a disk with optional query parameters

-- | Delete a persistent disk
--
-- Example:
--
-- > deleteDisk "my-project" "us-central1-a" "old-disk" (Just DeleteDiskQuery { force = True })
deleteDisk :: String               -- ^ GCP Project ID
           -> String               -- ^ Zone name
           -> String               -- ^ Disk name to delete
           -> Maybe DeleteDiskQuery -- ^ Optional force deletion
           -> IO (Either String Operation) -- ^ Operation or error
deleteDisk project zone_ diskName mbQuery = do
  let queryParams = maybe [] queryToList mbQuery
  doRequestJSON
    RequestOptions
      { reqMethod = DELETE
      , reqUrl = googleComputeUrl
      , mbQueryParams = Just queryParams
      , mbReqBody = Nothing
      , mbReqHeaders = Nothing
      , mbReqPath = Just $ "/projects/" <> project <> "/zones/" <> zone_ <> "/disks/" <> diskName
      }
  where
    queryToList :: DeleteDiskQuery -> Query
    queryToList q =
      [ maybe mempty (\x -> ("force", Just . BS.pack $ show x)) (force q)
      ]

-- | Creates a snapshot of a disk with optional query parameters
-- | Create disk snapshot
--
-- Example:
--
-- > createDiskSnapshot "my-project" "us-central1-a" "source-disk" 
-- >   (defaultCreateSnapshotOps "daily-backup") Nothing
createDiskSnapshot :: String               -- ^ GCP Project ID
                   -> String               -- ^ Zone name
                   -> String               -- ^ Source disk name
                   -> CreateSnapshotOps    -- ^ Snapshot configuration
                   -> Maybe CreateSnapshotQuery -- ^ Optional guest flush
                   -> IO (Either String CreateSnapshotResp) -- ^ Operation or error
createDiskSnapshot project zone_ diskName createSnapshotOps mbQuery = do
  let queryParams = maybe [] queryToList mbQuery
  doRequestJSON
    RequestOptions
      { reqMethod = POST
      , reqUrl = googleComputeUrl
      , mbQueryParams = Just queryParams
      , mbReqBody = Just $ encode createSnapshotOps
      , mbReqHeaders = Nothing
      , mbReqPath =
          Just $ "/projects/" <> project <> "/zones/" <> zone_ <> "/disks/" <> diskName <> "/createSnapshot"
      }
  where
    queryToList :: CreateSnapshotQuery -> Query
    queryToList q =
      [ maybe mempty (\x -> ("guestFlush", Just . BS.pack $ show x)) (guestFlush q)
      ]

-- | Default disk creation options
--
-- Creates minimal valid configuration with required name and size.
-- Other fields can be added using record update syntax:
--
-- > defaultInsertOps "new-disk" "50" { type_ = Just "pd-ssd" }
defaultInsertOps :: String -> String -> InsertDiskOps
defaultInsertOps diskName diskSizeGb =
  InsertDiskOps
    { name = diskName
    , sizeGb = diskSizeGb
    , description = Nothing
    , type_ = Nothing
    , sourceImage = Nothing
    , sourceSnapshot = Nothing
    , labels = Nothing
    , guestOsFeatures = Nothing
    , diskEncryptionKey = Nothing
    }

-- | Default snapshot creation options
--
-- Creates minimal valid configuration with required snapshot name.
-- Additional fields can be added using record update syntax:
--
-- > defaultCreateSnapshotOps "backup" { description = Just "Daily backup" }
defaultCreateSnapshotOps :: String -> CreateSnapshotOps
defaultCreateSnapshotOps snapshotName =
  CreateSnapshotOps
    { name = snapshotName
    , description = Nothing
    , labels = Nothing
    , storageLocations = Nothing
    , snapshotEncryptionKey = Nothing
    }