aws-lambda-haskell-runtime 1.0.4 → 1.0.5
raw patch · 7 files changed
+193/−119 lines, 7 filesdep +uuiddep −QuickCheck
Dependencies added: uuid
Dependencies removed: QuickCheck
Files
- LICENSE +10/−27
- README.md +51/−0
- aws-lambda-haskell-runtime.cabal +6/−33
- src/Aws/Lambda/Configuration.hs +32/−16
- src/Aws/Lambda/Runtime.hs +80/−26
- src/Aws/Lambda/ThHelpers.hs +10/−5
- test/Spec.hs +4/−12
LICENSE view
@@ -1,30 +1,13 @@-Copyright Author name here (c) 2018--All rights reserved.--Redistribution and use in source and binary forms, with or without-modification, are permitted provided that the following conditions are met:-- * Redistributions of source code must retain the above copyright- notice, this list of conditions and the following disclaimer.+Copyright 2018 The Agile Monkeys - * Redistributions in binary form must reproduce the above- copyright notice, this list of conditions and the following- disclaimer in the documentation and/or other materials provided- with the distribution.+Licensed under the Apache License, Version 2.0 (the "License");+you may not use this file except in compliance with the License.+You may obtain a copy of the License at - * Neither the name of Author name here nor the names of other- contributors may be used to endorse or promote products derived- from this software without specific prior written permission.+ http://www.apache.org/licenses/LICENSE-2.0 -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.+Unless required by applicable law or agreed to in writing, software+distributed under the License is distributed on an "AS IS" BASIS,+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+See the License for the specific language governing permissions and+limitations under the License.
README.md view
@@ -1,3 +1,54 @@ # Haskell Runtime for AWS Lambda+[](https://hackage.haskell.org/package/aws-lambda-haskell-runtime)+[](https://github.com/ndmitchell/hlint)+[](https://circleci.com/gh/theam/aws-lambda-haskell-runtime/tree/master) + This package provides a way of running Haskell projects on AWS Lambda.++## Sample lambda function++```+stack new my-haskell-lambda https://github.com/theam/aws-lambda-haskell-runtime/raw/master/stack-template.hsfiles --resolver=lts-12.13 --omit-packages+cd my-haskell-lambda+stack docker pull+```++Then add ++```+packages:+- .++extra-deps:+- aws-lambda-haskell-runtime-1.0.4+```++to your `stack.yaml`++Then run ++```+make+```++Now you should have a `build/function.zip` file that you can upload to your lambda.++## Lambda function configuration++When creating your lambda function you need to provide a layer with the Haskell runtime. We have deployed the layer to our AWS account, you can use it or deploy it to your own AWS account.++The ARN of the runtime layer is:+```+arn:aws:lambda:<YOUR REGION>:785355572843:layer:haskell-runtime:<VERSION>+````++The version of the layer may be different depending of the region, the current values are:++| us-east-2 | Other regions |+|-----------|---------------|+| 10 | 2 |++## Full user guide++Take a look at the [Getting Started with the Haskell AWS Lambda Runtime](https://medium.com/the-theam-journey/getting-started-with-the-haskell-aws-lambda-runtime-951b2322c7a3) guide.
aws-lambda-haskell-runtime.cabal view
@@ -4,12 +4,12 @@ -- -- see: https://github.com/sol/hpack ----- hash: eb0c1a0fc5528fcd3765757dd2c1e92a71d28843c032385383b0cd05e2ca1f58+-- hash: 2ef45c75a139565b825b94e2a0dfb5a1ba2cb921f85ce064de99be29e2c60cfb name: aws-lambda-haskell-runtime-version: 1.0.4+version: 1.0.5 synopsis: Haskell runtime for AWS Lambda-description: Please see the README on GitHub at <https://github.com/githubuser/aws-lambda-haskell-runtime#readme>+description: Please see the README on GitHub at <https://github.com/theam/aws-lambda-haskell-runtime#readme> category: AWS homepage: https://github.com/theam/aws-lambda-haskell-runtime#readme bug-reports: https://github.com/theam/aws-lambda-haskell-runtime/issues@@ -53,6 +53,7 @@ , relude , template-haskell , text+ , uuid , wreq default-language: Haskell2010 @@ -65,22 +66,9 @@ default-extensions: TemplateHaskell OverloadedStrings RecordWildCards ScopedTypeVariables NoImplicitPrelude DeriveGeneric TypeApplications ghc-options: -Wall -optP-Wno-nonportable-include-path build-depends:- aeson- , aws-lambda-haskell-runtime+ aws-lambda-haskell-runtime , base >=4.7 && <5- , bytestring- , case-insensitive- , conduit- , directory- , filepath- , microlens-platform- , mtl- , optparse-generic- , process , relude- , template-haskell- , text- , wreq default-language: Haskell2010 test-suite aws-lambda-haskell-runtime-test@@ -93,22 +81,7 @@ default-extensions: TemplateHaskell OverloadedStrings RecordWildCards ScopedTypeVariables NoImplicitPrelude DeriveGeneric TypeApplications ghc-options: -Wall -optP-Wno-nonportable-include-path -threaded -rtsopts -with-rtsopts=-N build-depends:- QuickCheck- , aeson- , aws-lambda-haskell-runtime- , base >=4.7 && <5- , bytestring- , case-insensitive- , conduit- , directory- , filepath+ base >=4.7 && <5 , hspec- , microlens-platform- , mtl- , optparse-generic- , process , relude- , template-haskell- , text- , wreq default-language: Haskell2010
src/Aws/Lambda/Configuration.hs view
@@ -1,3 +1,4 @@+{-# OPTIONS_GHC -fno-warn-unused-pattern-binds #-} module Aws.Lambda.Configuration ( LambdaOptions (..) , configureLambda@@ -18,6 +19,7 @@ import qualified System.Directory as Directory import System.FilePath ((</>)) import System.IO.Error+import System.IO (hFlush) import Aws.Lambda.ThHelpers @@ -26,10 +28,12 @@ { eventObject :: Text , contextObject :: Text , functionHandler :: Text+ , executionUuid :: Text } deriving (Generic) instance Options.ParseRecord LambdaOptions +-- This function is the reason why we disable the warning on top of the module mkMain :: Q [Dec] mkMain = [d| $(pName "main") = getRecord "" >>= run@@ -38,7 +42,7 @@ mkRun :: Q Dec mkRun = do handlers <- runIO getHandlers- clause' <- recordQ "LambdaOptions" ["functionHandler", "contextObject", "eventObject"]+ clause' <- recordQ "LambdaOptions" ["functionHandler", "contextObject", "eventObject", "executionUuid"] body <- dispatcherCaseQ handlers pure $ FunD (mkName "run") [Clause [clause'] (NormalB body) []] @@ -56,7 +60,7 @@ let pattern = LitP (StringL $ toString lambdaHandler) body <- [e|do result <- $(eName qualifiedName) (decodeObj $(eName "eventObject")) (decodeObj $(eName "contextObject"))- either returnAndFail returnAndSucceed result+ either (returnAndFail $(eName "executionUuid")) (returnAndSucceed $(eName "executionUuid")) result |] pure $ Match pattern (NormalB body) [] where@@ -71,7 +75,7 @@ unmatchedCaseQ = do let pattern = WildP body <- [e|- returnAndFail ("Handler " <> $(eName "functionHandler") <> " does not exist on project")+ returnAndFail $(eName "executionUuid") ("Handler " <> $(eName "functionHandler") <> " does not exist on project") |] pure $ Match pattern (NormalB body) [] @@ -82,29 +86,41 @@ return $ main <> [run] -returnAndFail :: ToJSON a => a -> IO ()-returnAndFail v = do- putTextLn (decodeUtf8 $ encode v)- exitFailure+returnAndFail :: ToJSON a => Text -> a -> IO ()+returnAndFail uuid v = do+ hFlush stdout+ putTextLn uuid+ hFlush stdout+ putTextLn (decodeUtf8 $ encode v)+ hFlush stdout+ hFlush stderr+ exitFailure -returnAndSucceed :: ToJSON a => a -> IO ()-returnAndSucceed v = do- putTextLn (decodeUtf8 $ encode v)- exitSuccess+returnAndSucceed :: ToJSON a => Text -> a -> IO ()+returnAndSucceed uuid v = do+ hFlush stdout+ putTextLn uuid+ hFlush stdout+ putTextLn (decodeUtf8 $ encode v)+ hFlush stdout+ exitSuccess decodeObj :: FromJSON a => Text -> a-decodeObj x = (decode $ encodeUtf8 x) ?: error $ "Could not decode event " <> x+decodeObj x =+ case (eitherDecode $ encodeUtf8 x) of+ Left e -> error $ toText e+ Right v -> v data DirContent = DirList [FilePath] [FilePath] | DirError IOError data DirData = DirData FilePath DirContent -- Produces directory data-walk :: FilePath -> Conduit.Source IO DirData+walk :: FilePath -> Conduit.ConduitT () DirData IO () walk path = do result <- lift $ tryIOError listdir case result of- Right dl@(DirList subdirs files) -> do+ Right dl@(DirList subdirs _) -> do Conduit.yield (DirData path dl) forM_ subdirs (walk . (path </>)) Right e -> Conduit.yield (DirData path e)@@ -122,14 +138,14 @@ -- Consume directories-myVisitor :: Conduit.Sink DirData IO [FilePath]+myVisitor :: Conduit.ConduitT DirData Void IO [FilePath] myVisitor = loop [] where loop n = do r <- Conduit.await case r of Nothing -> return n- Just r -> loop (process r <> n)+ Just result -> loop (process result <> n) process (DirData _ (DirError _)) = [] process (DirData dir (DirList _ files)) = map (\f -> dir <> "/" <> f) files
src/Aws/Lambda/Runtime.hs view
@@ -11,6 +11,9 @@ import qualified Network.Wreq as Wreq import qualified System.Environment as Environment import qualified System.Process as Process+import qualified Data.UUID as UUID+import qualified Data.UUID.V4 as UUID+import System.IO (hFlush) type App a =@@ -71,6 +74,30 @@ LambdaResult Text +awsLambdaVersion :: String+awsLambdaVersion = "2018-06-01"+++nextInvocationEndpoint :: Text -> String+nextInvocationEndpoint endpoint =+ "http://" <> toString endpoint <> "/"<> awsLambdaVersion <>"/runtime/invocation/next"+++responseEndpoint :: Text -> Text -> String+responseEndpoint lambdaApi requestId =+ "http://"<> toString lambdaApi <> "/" <> awsLambdaVersion <> "/runtime/invocation/"<> toString requestId <> "/response"+++invocationErrorEndpoint :: Text -> Text -> String+invocationErrorEndpoint lambdaApi requestId =+ "http://"<> toString lambdaApi <> "/" <> awsLambdaVersion <> "/runtime/invocation/"<> toString requestId <> "/error"+++runtimeInitErrorEndpoint :: Text -> String+runtimeInitErrorEndpoint lambdaApi =+ "http://"<> toString lambdaApi <> "/" <> awsLambdaVersion <> "/runtime/init/error"++ readEnvironmentVariable :: Text -> App Text readEnvironmentVariable envVar = do v <- lift (Environment.lookupEnv $ toString envVar)@@ -91,22 +118,14 @@ getApiData :: Text -> App (Wreq.Response LByteString) getApiData endpoint =- tryIO (Wreq.get nextInvocationEndpoint)+ keepRetrying (Wreq.get $ nextInvocationEndpoint endpoint) where- nextInvocationEndpoint :: String- nextInvocationEndpoint =- "http://" <> toString endpoint <> "/2018-06-01/runtime/invocation/next"-- tryIO :: IO a -> App a- tryIO f =- try f- & catchApiException-- catchApiException :: IO (Either IOException a) -> App a- catchApiException action =- action- & fmap (first $ const ApiConnectionError)- & ExceptT+ keepRetrying :: IO (Wreq.Response LByteString) -> App (Wreq.Response LByteString)+ keepRetrying f = do+ result <- (liftIO $ try f) :: App (Either IOException (Wreq.Response LByteString))+ case result of+ Right x -> return x+ _ -> keepRetrying f extractHeader :: Wreq.Response LByteString -> Text -> Text@@ -157,26 +176,60 @@ } +getFunctionResult :: UUID.UUID -> Text -> App (Maybe Text)+getFunctionResult u stdOut = do+ let out = stdOut+ & toText+ & lines++ out+ & takeWhile (/= uuid)+ & traverse_ ( \t -> do+ liftIO $ putTextLn t+ liftIO $ hFlush stdout)++ out+ & dropWhile (/= uuid)+ & dropWhile (== uuid)+ & nonEmpty+ & fmap head+ & return+ where+ uuid = toText $ UUID.toString u++ invoke :: Text -> Context -> App LambdaResult invoke event context = do handlerName <- readEnvironmentVariable "_HANDLER" runningDirectory <- readEnvironmentVariable "LAMBDA_TASK_ROOT" let contextJSON = decodeUtf8 $ encode context+ uuid <- liftIO UUID.nextRandom out <- liftIO $ Process.readProcessWithExitCode (toString runningDirectory <> "/haskell_lambda") [ "--eventObject", toString event , "--contextObject", contextJSON , "--functionHandler", toString handlerName+ , "--executionUuid", UUID.toString uuid ] "" case out of- (ExitSuccess, stdOut, _) -> pure (LambdaResult $ toText stdOut)- (_, stdOut, _) -> throwError (InvocationError $ toText stdOut)+ (ExitSuccess, stdOut, _) -> do+ res <- getFunctionResult uuid (toText stdOut)+ case res of+ Nothing -> throwError (ParseError "parsing result" $ toText stdOut)+ Just value -> pure (LambdaResult value)+ (_, stdOut, stdErr) ->+ if stdErr /= ""+ then throwError (InvocationError $ toText stdErr)+ else do+ res <- getFunctionResult uuid (toText stdOut)+ case res of+ Nothing -> throwError (ParseError "parsing error" $ toText stdOut)+ Just value -> throwError (InvocationError value) publishResult :: Context -> Text -> LambdaResult -> App ()-publishResult Context {..} lambdaApi (LambdaResult result) = do- let endpoint = "http://"<> lambdaApi <> "/2018-06-01/runtime/invocation/"<> awsRequestId <> "/response"- void $ liftIO $ Wreq.post (toString endpoint) (encodeUtf8 @Text @ByteString result)+publishResult Context {..} lambdaApi (LambdaResult result) =+ void $ liftIO $ Wreq.post (responseEndpoint lambdaApi awsRequestId) (encodeUtf8 @Text @ByteString result) invokeAndPublish :: Context -> Text -> Text -> App ()@@ -186,13 +239,14 @@ publishError :: Context -> Text -> RuntimeError -> App ()-publishError Context {..} lambdaApiEndpoint (InvocationError err) = do- let endpoint = "http://"<> lambdaApiEndpoint <> "/2018-06-01/runtime/invocation/"<> awsRequestId <> "/error"- void (liftIO $ Wreq.post (toString endpoint) (encodeUtf8 @Text @ByteString err))+publishError Context {..} lambdaApiEndpoint (InvocationError err) =+ void (liftIO $ Wreq.post (invocationErrorEndpoint lambdaApiEndpoint awsRequestId) (encodeUtf8 @Text @ByteString err)) -publishError Context {..} lambdaApiEndpoint err = do- let endpoint = "http://"<> lambdaApiEndpoint <> "/2018-06-01/runtime/init/error"- void (liftIO $ Wreq.post (toString endpoint) (toJSON err))+publishError Context {..} lambdaApiEndpoint (ParseError t t2) =+ void (liftIO $ Wreq.post (invocationErrorEndpoint lambdaApiEndpoint awsRequestId) (toJSON $ ParseError t t2))++publishError Context {..} lambdaApiEndpoint err =+ void (liftIO $ Wreq.post (runtimeInitErrorEndpoint lambdaApiEndpoint) (toJSON err)) lambdaRunner :: App ()
src/Aws/Lambda/ThHelpers.hs view
@@ -1,4 +1,8 @@-module Aws.Lambda.ThHelpers where+module Aws.Lambda.ThHelpers+ ( pName+ , eName+ , recordQ+ ) where import Relude import Language.Haskell.TH@@ -13,10 +17,6 @@ eName :: Text -> Q Exp eName = pure . VarE . mkName . toString --- | Helper for extracting fields of records--- think of @personAge@ in @myFunction Person { personAge = personAge } = ...@-fName :: Text -> Q FieldPat-fName name = pure (mkName $ toString name, VarP $ mkName $ toString name) -- | Helper for extracting fields of a specified record -- it expects the constructor name as the first parameter,@@ -27,3 +27,8 @@ recordQ name fields = do extractedFields <- traverse fName fields pure $ RecP (mkName $ toString name) extractedFields+ where+ -- | Helper for extracting fields of records+ -- think of @personAge@ in @myFunction Person { personAge = personAge } = ...@+ fName :: Text -> Q FieldPat+ fName n = pure (mkName $ toString n, VarP $ mkName $ toString n)
test/Spec.hs view
@@ -1,16 +1,8 @@-import Relude hiding ( head )-+import Relude import Test.Hspec -import Aws.Lambda.Runtime-import Aws.Lambda.Function --handler :: Text -> c -> IO (Either String Int)-handler t _ = do- putTextLn t- return $ Right (42 :: Int)- main :: IO ()-main = hspec $ describe "Impure lambda handler" $ do- it "runs" $ lambda handler+main = hspec $ describe "Useless test spec" $ do+ it "runs" $ do+ (1 + 1 :: Int) `shouldBe` (2 :: Int)