|
| 1 | +{-# LANGUAGE DataKinds #-} |
| 2 | +{-# LANGUAGE DeriveGeneric #-} |
| 3 | +{-# LANGUAGE DuplicateRecordFields #-} |
| 4 | +{-# LANGUAGE KindSignatures #-} |
| 5 | +{-# LANGUAGE OverloadedStrings #-} |
| 6 | +{-# LANGUAGE RecordWildCards #-} |
| 7 | +{-# LANGUAGE ScopedTypeVariables #-} |
| 8 | + |
| 9 | +{-| |
| 10 | +Module : Kubernetes.KubeConfig |
| 11 | +Description : Data model for the kubeconfig. |
| 12 | +
|
| 13 | +This module contains the definition of the data model of the kubeconfig. |
| 14 | +
|
| 15 | +The official definition of the kubeconfig is defined in https://github.com/kubernetes/client-go/blob/master/tools/clientcmd/api/v1/types.go. |
| 16 | +
|
| 17 | +This is a mostly straightforward translation into Haskell, with 'FromJSON' and 'ToJSON' instances defined. |
| 18 | +-} |
| 19 | +module Kubernetes.KubeConfig where |
| 20 | + |
| 21 | +import Data.Aeson (FromJSON (..), Options, ToJSON (..), |
| 22 | + Value (..), camelTo2, defaultOptions, |
| 23 | + fieldLabelModifier, genericParseJSON, |
| 24 | + genericToJSON, object, omitNothingFields, |
| 25 | + withObject, (.:), (.=)) |
| 26 | +import qualified Data.Map as Map |
| 27 | +import Data.Proxy |
| 28 | +import Data.Semigroup ((<>)) |
| 29 | +import Data.Text (Text) |
| 30 | +import qualified Data.Text as T |
| 31 | +import Data.Typeable |
| 32 | +import GHC.Generics |
| 33 | +import GHC.TypeLits |
| 34 | + |
| 35 | +camelToWithOverrides :: Char -> Map.Map String String -> Options |
| 36 | +camelToWithOverrides c overrides = defaultOptions |
| 37 | + { fieldLabelModifier = modifier |
| 38 | + , omitNothingFields = True |
| 39 | + } |
| 40 | + where modifier s = Map.findWithDefault (camelTo2 c s) s overrides |
| 41 | + |
| 42 | +-- |Represents a kubeconfig. |
| 43 | +data Config = Config |
| 44 | + { kind :: Maybe Text |
| 45 | + , apiVersion :: Maybe Text |
| 46 | + , preferences :: Preferences |
| 47 | + , clusters :: [NamedEntity Cluster "cluster"] |
| 48 | + , authInfos :: [NamedEntity AuthInfo "user"] |
| 49 | + , contexts :: [NamedEntity Context "context"] |
| 50 | + , currentContext :: Text |
| 51 | + } deriving (Eq, Generic, Show) |
| 52 | + |
| 53 | +configJSONOptions = camelToWithOverrides |
| 54 | + '-' |
| 55 | + (Map.fromList [("apiVersion", "apiVersion"), ("authInfos", "users")]) |
| 56 | + |
| 57 | +instance ToJSON Config where |
| 58 | + toJSON = genericToJSON configJSONOptions |
| 59 | + |
| 60 | +instance FromJSON Config where |
| 61 | + parseJSON = genericParseJSON configJSONOptions |
| 62 | + |
| 63 | +newtype Preferences = Preferences |
| 64 | + { colors :: Maybe Bool |
| 65 | + } deriving (Eq, Generic, Show) |
| 66 | + |
| 67 | +instance ToJSON Preferences where |
| 68 | + toJSON = genericToJSON $ camelToWithOverrides '-' Map.empty |
| 69 | + |
| 70 | +instance FromJSON Preferences where |
| 71 | + parseJSON = genericParseJSON $ camelToWithOverrides '-' Map.empty |
| 72 | + |
| 73 | +data Cluster = Cluster |
| 74 | + { server :: Text |
| 75 | + , insecureSkipTLSVerify :: Maybe Bool |
| 76 | + , certificateAuthority :: Maybe Text |
| 77 | + , certificateAuthorityData :: Maybe Text |
| 78 | + } deriving (Eq, Generic, Show, Typeable) |
| 79 | + |
| 80 | +instance ToJSON Cluster where |
| 81 | + toJSON = genericToJSON $ camelToWithOverrides '-' Map.empty |
| 82 | + |
| 83 | +instance FromJSON Cluster where |
| 84 | + parseJSON = genericParseJSON $ camelToWithOverrides '-' Map.empty |
| 85 | + |
| 86 | +data NamedEntity a (typeKey :: Symbol) = NamedEntity |
| 87 | + { name :: Text |
| 88 | + , entity :: a } deriving (Eq, Generic, Show) |
| 89 | + |
| 90 | +instance (FromJSON a, Typeable a, KnownSymbol s) => |
| 91 | + FromJSON (NamedEntity a s) where |
| 92 | + parseJSON = withObject ("Named" <> (show $ typeOf (undefined :: a))) $ \v -> |
| 93 | + NamedEntity <$> v .: "name" <*> v .: T.pack (symbolVal (Proxy :: Proxy s)) |
| 94 | + |
| 95 | +instance (ToJSON a, KnownSymbol s) => |
| 96 | + ToJSON (NamedEntity a s) where |
| 97 | + toJSON (NamedEntity {..}) = object |
| 98 | + ["name" .= toJSON name, T.pack (symbolVal (Proxy :: Proxy s)) .= toJSON entity] |
| 99 | + |
| 100 | +toMap :: [NamedEntity a s] -> Map.Map Text a |
| 101 | +toMap = Map.fromList . fmap (\NamedEntity {..} -> (name, entity)) |
| 102 | + |
| 103 | +data AuthInfo = AuthInfo |
| 104 | + { clientCertificate :: Maybe FilePath |
| 105 | + , clientCertificateData :: Maybe Text |
| 106 | + , clientKey :: Maybe FilePath |
| 107 | + , clientKeyData :: Maybe Text |
| 108 | + , token :: Maybe Text |
| 109 | + , tokenFile :: Maybe FilePath |
| 110 | + , impersonate :: Maybe Text |
| 111 | + , impersonateGroups :: Maybe [Text] |
| 112 | + , impersonateUserExtra :: Maybe (Map.Map Text [Text]) |
| 113 | + , username :: Maybe Text |
| 114 | + , password :: Maybe Text |
| 115 | + , authProvider :: Maybe AuthProviderConfig |
| 116 | + } deriving (Eq, Generic, Show, Typeable) |
| 117 | + |
| 118 | +authInfoJSONOptions = camelToWithOverrides |
| 119 | + '-' |
| 120 | + ( Map.fromList |
| 121 | + [ ("tokenFile" , "tokenFile") |
| 122 | + , ("impersonate" , "as") |
| 123 | + , ("impersonateGroups" , "as-groups") |
| 124 | + , ("impersonateUserExtra", "as-user-extra") |
| 125 | + ] |
| 126 | + ) |
| 127 | + |
| 128 | +instance ToJSON AuthInfo where |
| 129 | + toJSON = genericToJSON authInfoJSONOptions |
| 130 | + |
| 131 | +instance FromJSON AuthInfo where |
| 132 | + parseJSON = genericParseJSON authInfoJSONOptions |
| 133 | + |
| 134 | +data Context = Context |
| 135 | + { cluster :: Text |
| 136 | + , authInfo :: Text |
| 137 | + , namespace :: Maybe Text |
| 138 | + } deriving (Eq, Generic, Show, Typeable) |
| 139 | + |
| 140 | +contextJSONOptions = |
| 141 | + camelToWithOverrides '-' (Map.fromList [("authInfo", "user")]) |
| 142 | + |
| 143 | +instance ToJSON Context where |
| 144 | + toJSON = genericToJSON contextJSONOptions |
| 145 | + |
| 146 | +instance FromJSON Context where |
| 147 | + parseJSON = genericParseJSON contextJSONOptions |
| 148 | + |
| 149 | +data AuthProviderConfig = AuthProviderConfig |
| 150 | + { name :: Text |
| 151 | + , config :: Maybe (Map.Map Text Text) |
| 152 | + } deriving (Eq, Generic, Show) |
| 153 | + |
| 154 | +instance ToJSON AuthProviderConfig where |
| 155 | + toJSON = genericToJSON $ camelToWithOverrides '-' Map.empty |
| 156 | + |
| 157 | +instance FromJSON AuthProviderConfig where |
| 158 | + parseJSON = genericParseJSON $ camelToWithOverrides '-' Map.empty |
| 159 | + |
| 160 | +-- |Returns the currently active context. |
| 161 | +getContext :: Config -> Either String Context |
| 162 | +getContext Config {..} = |
| 163 | + let maybeContext = Map.lookup currentContext (toMap contexts) |
| 164 | + in case maybeContext of |
| 165 | + Just ctx -> Right ctx |
| 166 | + Nothing -> Left ("No context named " <> T.unpack currentContext) |
| 167 | + |
| 168 | +-- |Returns the currently active user. |
| 169 | +getAuthInfo :: Config -> Either String (Text, AuthInfo) |
| 170 | +getAuthInfo cfg@Config {..} = do |
| 171 | + Context {..} <- getContext cfg |
| 172 | + let maybeAuth = Map.lookup authInfo (toMap authInfos) |
| 173 | + case maybeAuth of |
| 174 | + Just auth -> Right (authInfo, auth) |
| 175 | + Nothing -> Left ("No user named " <> T.unpack authInfo) |
| 176 | + |
| 177 | +-- |Returns the currently active cluster. |
| 178 | +getCluster :: Config -> Either String Cluster |
| 179 | +getCluster cfg@Config {clusters=clusters} = do |
| 180 | + Context {cluster=clusterName} <- getContext cfg |
| 181 | + let maybeCluster = Map.lookup clusterName (toMap clusters) |
| 182 | + case maybeCluster of |
| 183 | + Just cluster -> Right cluster |
| 184 | + Nothing -> Left ("No cluster named " <> T.unpack clusterName) |
0 commit comments