on master help sign in
Merge branch 'db-envvar-configs' of github.com:obsidiansystems/obelisk into dep/obelisk/project-master
commit16e0ebc52428c563fef09f40ca8e096e69d49e81 authorJonathan D.K. Gibbons <jonored@gmail.com> dateThu 24 Oct 2019, 13:48 (7 years ago) parent9bf5b9358cf9bd
3 files changed+129−2
lib/executable-config/lookup/obelisk-executable-config-lookup.cabal+3 lib/executable-config/lookup/src-other/Obelisk/Configs/Internal/Environment.hs+116 lib/executable-config/lookup/src-other/Obelisk/ExecutableConfig/Lookup.hs+10−2
lib/executable-config/lookup/obelisk-executable-config-lookup.cabal+3
@@ -24,7 +24,10 @@ library
2424 else
2525 hs-source-dirs: src-other, src
2626 other-modules: Obelisk.Configs.Internal.Directory
27+ , Obelisk.Configs.Internal.Environment
2728 build-depends: directory
29+ , attoparsec
30+ , witherable
2831
2932 build-depends:
3033 base,
lib/executable-config/lookup/src-other/Obelisk/Configs/Internal/Environment.hsnew file+116
@@ -0,0 +1,116 @@
1+{-# LANGUAGE TypeApplications #-}
2+{-# LANGUAGE PatternGuards #-}
3+{-# LANGUAGE OverloadedStrings #-}
4+
5+module Obelisk.Configs.Internal.Environment where
6+
7+import Control.Applicative
8+import Control.Monad
9+import qualified Data.Attoparsec.ByteString as P
10+import qualified Data.Attoparsec.ByteString.Char8 as P (hexadecimal)
11+import qualified Data.Attoparsec.Combinator as P
12+import Data.Bifunctor (second)
13+import Data.ByteString (ByteString)
14+import qualified Data.ByteString as BS
15+import qualified Data.ByteString.Base64 as Base64
16+import qualified Data.ByteString.Builder as Builder
17+import qualified Data.ByteString.Char8 as Char8
18+import qualified Data.ByteString.Lazy as LBS
19+import qualified Data.Char as Char
20+import Data.Map (Map)
21+import qualified Data.Map as Map
22+import Data.Text (Text)
23+import qualified Data.Text as T
24+import qualified Data.Text.Lazy as LT
25+import Data.Tuple (swap)
26+import Data.Witherable
27+import Data.Word
28+import System.Environment
29+import qualified Text.Printf as Printf
30+
31+
32+data EnvCodec
33+ = EnvCodec_Char8
34+ | EnvCodec_Base64
35+ deriving (Eq, Ord, Show)
36+
37+envVarEscapes :: [(Char, Word8)]
38+envVarEscapes = fmap (second f)
39+ [ ('-', 'h')
40+ , ('_', 'u')
41+ , ('.', 'd')
42+ ]
43+ where
44+ f :: Char -> Word8
45+ f = fromIntegral . Char.ord
46+
47+escapeVarName :: EnvCodec -> Text -> ByteString
48+escapeVarName codec x = LBS.toStrict . Builder.toLazyByteString $ T.foldr ((<>) . f) mempty x <> codec'
49+ where
50+ escapeCode = Map.fromList envVarEscapes
51+
52+ codec' = case codec of
53+ EnvCodec_Base64 -> "__base64"
54+ EnvCodec_Char8 -> ""
55+
56+ f :: Char -> Builder.Builder
57+ f c
58+ | not (Char.isAscii c) = error "bad"
59+ | c == '/' = "_"
60+ | Char.isAlphaNum c = Builder.char7 c
61+ | Just c' <- Map.lookup c escapeCode = "__" <> Builder.word8 c'
62+ | otherwise = "__x" <> (Builder.string7 $ Printf.formatInt (Char.ord c) (Printf.FieldFormat (Just 2) Nothing (Just Printf.ZeroPad) Nothing False "" 'x') "")
63+
64+unescapeVarName :: ByteString -> Either String (Text, EnvCodec)
65+unescapeVarName x = P.parseOnly ((,) <$> fileName <*> codec <* P.endOfInput) x
66+ where
67+ escapeCode = Map.fromList $ fmap swap envVarEscapes
68+
69+ codec :: P.Parser EnvCodec
70+ codec = P.option EnvCodec_Char8 $ (EnvCodec_Base64 <$ "__base64")
71+
72+ -- | parse ps. succeed only if both ps and qs succeeds
73+ guardLookahead ps qs = do
74+ p <- ps
75+ _ <- P.lookAhead qs
76+ return p
77+
78+ parseEscapeCode =
79+ ("__" *> (do
80+ c <- P.anyWord8
81+ case Map.lookup c escapeCode of
82+ Nothing -> mzero
83+ Just c' -> return c')
84+ ) <|> ( "__x" *> (Char.chr . fromIntegral <$> P.hexadecimal @Word8))
85+
86+ parseEnvChar = Char.chr . fromIntegral <$> P.satisfy (P.inClass "a-zA-Z0-9")
87+
88+ fileName = LT.toStrict . LT.pack <$> P.many' (P.choice
89+ [ parseEnvChar
90+ , parseEscapeCode
91+ , guardLookahead ('/' <$ "_") (parseEnvChar <|> parseEscapeCode)
92+ ])
93+
94+
95+getConfigsFromEnvironment :: ByteString -> Map Text ByteString -> IO (Map Text ByteString)
96+getConfigsFromEnvironment varNamePrefix dirConfigs = do
97+ let
98+ prefix needle haystack
99+ | needle `BS.isPrefixOf` haystack = Just (BS.drop (BS.length needle) haystack)
100+ | otherwise = Nothing
101+
102+ makeEnv (k0, v0) = case prefix varNamePrefix (Char8.pack k0) of
103+ Nothing -> return Nothing
104+ Just k -> case unescapeVarName k of
105+ Left bad -> error bad
106+ Right (k', codec) ->
107+ let
108+ v = case codec of
109+ EnvCodec_Char8 -> Char8.pack v0
110+ EnvCodec_Base64 -> Base64.decodeLenient (Char8.pack v0)
111+ in case Map.lookup k' dirConfigs of
112+ Just v' | v /= v' -> error $ "conflicting obelisk config: env " <> k0 <> " /= ./config/" <> (T.unpack k')
113+ _ -> return (Just (k', v))
114+
115+ env <- wither makeEnv =<< getEnvironment
116+ return $ (Map.fromList env) <> dirConfigs
lib/executable-config/lookup/src-other/Obelisk/ExecutableConfig/Lookup.hs+10−2
@@ -1,6 +1,12 @@
11{-# LANGUAGE LambdaCase #-}
22{-# LANGUAGE OverloadedStrings #-}
3−module Obelisk.ExecutableConfig.Lookup where
3+module Obelisk.ExecutableConfig.Lookup
4+ ( getConfigs
5+ , escapeVarName
6+ , EnvCodec(..)
7+ , unescapeVarName
8+ )
9+where
410
511import Data.Map (Map)
612import qualified Data.Map as Map
@@ -14,5 +20,7 @@ import Control.Monad
1420
1521import Obelisk.Configs.Internal.Directory
1622
23+import Obelisk.Configs.Internal.Environment
24+
1725getConfigs :: IO (Map Text ByteString)
18−getConfigs = getConfigsFromDirectory "config"
26+getConfigs = getConfigsFromDirectory "config" >>= getConfigsFromEnvironment "OBELISK_CONFIG_"