aws-lambda-haskell-runtime 1.1.0 → 2.0.1
raw patch · 17 files changed
+140/−273 lines, 17 filesdep −aws-lambda-haskell-runtimedep −optparse-genericdep −process
Dependencies removed: aws-lambda-haskell-runtime, optparse-generic, process, uuid
Files
- ChangeLog.md +0/−3
- LICENSE +1/−1
- README.md +5/−46
- app/Main.hs +0/−21
- aws-lambda-haskell-runtime.cabal +3/−22
- src/Aws/Lambda.hs +1/−0
- src/Aws/Lambda/Configuration.hs +5/−10
- src/Aws/Lambda/Meta/Common.hs +5/−0
- src/Aws/Lambda/Meta/Discover.hs +18/−2
- src/Aws/Lambda/Meta/Dispatch.hs +17/−5
- src/Aws/Lambda/Meta/Main.hs +9/−15
- src/Aws/Lambda/Meta/Run.hs +0/−6
- src/Aws/Lambda/Runtime.hs +52/−18
- src/Aws/Lambda/Runtime/Common.hs +23/−0
- src/Aws/Lambda/Runtime/IPC.hs +0/−116
- src/Aws/Lambda/Runtime/Publish.hs +1/−1
- src/Aws/Lambda/Runtime/Result.hs +0/−7
− ChangeLog.md
@@ -1,3 +0,0 @@-# Changelog for aws-lambda-haskell-runtime--## Unreleased changes
LICENSE view
@@ -1,4 +1,4 @@-Copyright 2018 The Agile Monkeys+Copyright 2019 The Agile Monkeys Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
README.md view
@@ -1,48 +1,7 @@-# Haskell Runtime for AWS Lambda-[](https://hackage.haskell.org/package/aws-lambda-haskell-runtime)+[](https://circleci.com/gh/theam/aws-lambda-haskell-runtime)+[](http://makeapullrequest.com)+[](https://hackage.haskell.org/package/aws-lambda-haskell-runtime)+[](https://github.com/ellerbrock/open-source-badges/) [](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-13.0 --omit-packages-cd my-haskell-lambda-stack docker pull-```--Then add--```-packages:-- .--extra-deps:-- aws-lambda-haskell-runtime-1.0.10-```--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:aws-haskell-runtime:5-````--## 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.+[Proceed to site](https://theam.github.io/aws-lambda-haskell-runtime) to know how to use the AWS Lambda Haskell Runtime.
− app/Main.hs
@@ -1,21 +0,0 @@--- | Main entry point for the layer-module Main- ( main- ) where--import Aws.Lambda.Runtime-import Control.Monad-import qualified Network.HTTP.Client as Http---httpManagerSettings :: Http.ManagerSettings-httpManagerSettings =- -- We set the timeout to none, as AWS Lambda freezes the containers.- Http.defaultManagerSettings- { Http.managerResponseTimeout = Http.responseTimeoutNone- }--main :: IO ()-main = do- manager <- Http.newManager httpManagerSettings- forever (runLambda manager)
aws-lambda-haskell-runtime.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: a4a6b9dc1371218a01d228e551520a6e1296955192cb06ca15f14ebe0923badd+-- hash: 938688bc6c929451cf9326354de6c4b1e8575302e64a2bd413d0b921cd6b3549 name: aws-lambda-haskell-runtime-version: 1.1.0+version: 2.0.1 synopsis: Haskell runtime for AWS Lambda description: Please see the README on GitHub at <https://github.com/theam/aws-lambda-haskell-runtime#readme> category: AWS@@ -21,7 +21,6 @@ build-type: Simple extra-source-files: README.md- ChangeLog.md source-repository head type: git@@ -41,12 +40,11 @@ Aws.Lambda.Runtime.API.Endpoints Aws.Lambda.Runtime.API.Version Aws.Lambda.Runtime.ApiInfo+ Aws.Lambda.Runtime.Common Aws.Lambda.Runtime.Context Aws.Lambda.Runtime.Environment Aws.Lambda.Runtime.Error- Aws.Lambda.Runtime.IPC Aws.Lambda.Runtime.Publish- Aws.Lambda.Runtime.Result Paths_aws_lambda_haskell_runtime hs-source-dirs: src@@ -58,28 +56,11 @@ , bytestring , http-client , http-types- , optparse-generic , path , path-io- , process , safe-exceptions-checked , template-haskell , text- , uuid- default-language: Haskell2010--executable bootstrap- main-is: Main.hs- other-modules:- Paths_aws_lambda_haskell_runtime- hs-source-dirs:- app- default-extensions: TemplateHaskell OverloadedStrings RecordWildCards ScopedTypeVariables DeriveGeneric TypeApplications FlexibleContexts DeriveAnyClass QuasiQuotes- ghc-options: -Wall -fno-warn-orphans -optP-Wno-nonportable-include-path -Wincomplete-uni-patterns -Wincomplete-record-updates -Wcompat -Widentities -Wredundant-constraints -Wmissing-export-lists -Wpartial-fields -fhide-source-paths -freverse-errors- build-depends:- aws-lambda-haskell-runtime- , base >=4.7 && <5- , http-client default-language: Haskell2010 test-suite aws-lambda-haskell-runtime-test
src/Aws/Lambda.hs view
@@ -3,4 +3,5 @@ ) where import Aws.Lambda.Configuration as Reexported+import Aws.Lambda.Runtime as Reexported import Aws.Lambda.Runtime.Context as Reexported
src/Aws/Lambda/Configuration.hs view
@@ -1,12 +1,9 @@ {-# OPTIONS_GHC -fno-warn-unused-pattern-binds #-} module Aws.Lambda.Configuration ( Main.LambdaOptions(..)- , Main.generate- , Main.getRecord- , configureLambda- , IPC.returnAndFail- , IPC.returnAndSucceed+ , generateLambdaDispatcher , Dispatch.decodeObj+ , Dispatch.encodeObj ) where @@ -15,13 +12,11 @@ import qualified Aws.Lambda.Meta.Dispatch as Dispatch import qualified Aws.Lambda.Meta.Main as Main import qualified Aws.Lambda.Meta.Run as Run-import qualified Aws.Lambda.Runtime.IPC as IPC -{-| Generates a @main@ function to be used with the-AWS Lambda layer.+{-| Generates a @main@ function that acts as a dispatcher -}-configureLambda :: Meta.DecsQ-configureLambda = do+generateLambdaDispatcher :: Meta.DecsQ+generateLambdaDispatcher = do main <- Main.generate run <- Run.generate return (main <> [run])
src/Aws/Lambda/Meta/Common.hs view
@@ -3,6 +3,7 @@ ( declarationName , expressionName , getFieldsFrom+ , constructorName ) where import Data.Text (Text)@@ -19,6 +20,10 @@ expressionName :: Text -> Q Exp expressionName = pure . VarE . mkName . Text.unpack +-- | Helper for defining names for constructors+-- think of @Foo@ in @quux = Foo 3@+constructorName :: Text -> Q Exp+constructorName = pure . ConE . mkName . Text.unpack -- | Helper for extracting fields of a specified record -- it expects the constructor name as the first parameter,
src/Aws/Lambda/Meta/Discover.hs view
@@ -1,6 +1,6 @@ {-| Discovery of AWS Lambda handlers A handler is basically a function that has a type definition that-starts with "handler :: ".+starts with "handler " and two colons. -} module Aws.Lambda.Meta.Discover ( handlers@@ -15,6 +15,14 @@ import Path import qualified Path.IO as PathIO +-- | Paths to ignore during compilation+ignoredPaths :: [Text]+ignoredPaths =+ [ "node_modules"+ , ".stack-work"+ , ".serverless"+ ]+ {-| Returns a list of handler paths that look like @src/Foo/Bar/Quux.handler@@@ -35,7 +43,12 @@ where isHaskellModule file = fileExtension file == ".hs"+ && isNotIgnoredPath file + isNotIgnoredPath file =+ filter (\ignoredPath -> ignoredPath `Text.isInfixOf` (Text.pack $ toFilePath file)) ignoredPaths+ & null+ handlerNames :: [Path Rel File] -> [Text] handlerNames modules = fmap changeExtensionToHandler modules@@ -48,4 +61,7 @@ containsHandler :: Path Rel File -> IO Bool containsHandler file = do fileContents <- readFile $ toFilePath file- pure $ "handler :: " `Text.isInfixOf` Text.pack fileContents+ lines fileContents+ & filter (Text.isPrefixOf "handler :: " . Text.pack)+ & (not . null)+ & pure
src/Aws/Lambda/Meta/Dispatch.hs view
@@ -2,17 +2,21 @@ module Aws.Lambda.Meta.Dispatch ( generate , decodeObj+ , encodeObj+ , Runtime.LambdaResult(..) ) where import Data.Function ((&)) import Data.Text (Text) import qualified Data.Text as Text+import qualified Data.Char as Char import Data.Aeson import qualified Data.ByteString.Lazy.Char8 as LazyByteString import qualified Language.Haskell.TH as Meta import Aws.Lambda.Meta.Common+import qualified Aws.Lambda.Runtime.Common as Runtime {-| Helper function that the dispatcher will use to decode the JSON that comes as an AWS Lambda event into the@@ -24,6 +28,14 @@ Left e -> error e Right v -> v +{-| Helper function that the dispatcher will use to+decode the JSON that comes as an AWS Lambda event into the+appropriate type expected by the handler.+-}+encodeObj :: ToJSON a => a -> String+encodeObj x = LazyByteString.unpack (encode x)++ {-| Generates the dispatcher out of a list of handler names in the form @src/Foo/Bar.handler@ @@ -43,19 +55,19 @@ let pat = Meta.LitP (Meta.StringL $ Text.unpack lambdaHandler) body <- [e|do result <- $(expressionName qualifiedName) (decodeObj $(expressionName "eventObject")) (decodeObj $(expressionName "contextObject"))- either (returnAndFail $(expressionName "executionUuid")) (returnAndSucceed $(expressionName "executionUuid")) result |]+ either (pure . Left . encodeObj) (pure . Right . $(constructorName "LambdaResult") . encodeObj) result |] pure $ Meta.Match pat (Meta.NormalB body) [] where qualifiedName = lambdaHandler- & Text.dropWhile (/= '/')- & Text.drop 1- & Text.replace "/" "."+ & Text.splitOn "/"+ & filter (Char.isUpper . Text.head)+ & Text.intercalate "." unmatchedCase :: Meta.MatchQ unmatchedCase = do let pattern = Meta.WildP body <- [e|- returnAndFail $(expressionName "executionUuid") ("Handler " <> $(expressionName "functionHandler") <> " does not exist on project")+ pure $ Left ("Handler " <> $(expressionName "functionHandler") <> " does not exist on project") |] pure $ Meta.Match pattern (Meta.NormalB body) []
src/Aws/Lambda/Meta/Main.hs view
@@ -1,27 +1,21 @@ {-| main function generation for interoperation with the layer -} module Aws.Lambda.Meta.Main- ( LambdaOptions(..)+ ( Runtime.LambdaOptions(..) , generate- , Options.getRecord ) where -import GHC.Generics (Generic)- import qualified Language.Haskell.TH as Meta-import qualified Options.Generic as Options import Aws.Lambda.Meta.Common---- | Options that the generated main expects-data LambdaOptions = LambdaOptions- { eventObject :: !String- , contextObject :: !String- , functionHandler :: !String- , executionUuid :: !String- } deriving (Generic, Options.ParseRecord)+import qualified Aws.Lambda.Runtime.Common as Runtime --- | Generate the main function that the layer will call+-- | Generate the main function with the dispatcher generate :: Meta.DecsQ generate = [d|- $(declarationName "main") = getRecord "" >>= run+ $(declarationName "main") = $(directCallBody) |]+ where+ directCallBody =+ [e|do+ runLambda run+ |]
src/Aws/Lambda/Meta/Run.hs view
@@ -8,12 +8,6 @@ import qualified Aws.Lambda.Meta.Discover as Discover import qualified Aws.Lambda.Meta.Dispatch as Dispatch -{-| Generate the run function--It will create a dispatcher that is a huge @case@ expression that-expects the name of the handler provided by AWS Lambda, and will-execute the appropriate user function- -} generate :: Meta.DecQ generate = do handlers <- Meta.runIO Discover.handlers
src/Aws/Lambda/Runtime.hs view
@@ -1,44 +1,78 @@ module Aws.Lambda.Runtime ( runLambda+ , Runtime.LambdaResult(..) ) where import Control.Exception.Safe.Checked+import Control.Monad (forever) import qualified Network.HTTP.Client as Http +import Data.Aeson+import qualified Data.ByteString.Lazy.Char8 as LazyByteString+ import qualified Aws.Lambda.Runtime.ApiInfo as ApiInfo+import qualified Aws.Lambda.Runtime.Common as Runtime import qualified Aws.Lambda.Runtime.Context as Context import qualified Aws.Lambda.Runtime.Environment as Environment import qualified Aws.Lambda.Runtime.Error as Error-import qualified Aws.Lambda.Runtime.IPC as IPC import qualified Aws.Lambda.Runtime.Publish as Publish -- | Runs the user @haskell_lambda@ executable and posts back the--- results-runLambda- :: Http.Manager- -> IO ()-runLambda manager = do- lambdaApi <- Environment.apiEndpoint `catch` variableNotSet- event <- ApiInfo.fetchEvent manager lambdaApi `catch` errorParsing- context <- Context.initialize event `catch` errorParsing `catch` variableNotSet- ((invokeAndRun manager lambdaApi event context- `catch` \err -> Publish.parsingError err lambdaApi context manager)- `catch` \err -> Publish.invocationError err lambdaApi context manager)- `catch` \(err :: Error.EnvironmentVariableNotSet) -> Publish.runtimeInitError err lambdaApi context manager+-- results. This is called from the layer's @main@ function.+runLambda :: Runtime.RunCallback -> IO ()+runLambda callback = do+ manager <- Http.newManager httpManagerSettings+ forever $ do+ lambdaApi <- Environment.apiEndpoint `catch` variableNotSet+ event <- ApiInfo.fetchEvent manager lambdaApi `catch` errorParsing+ context <- Context.initialize event `catch` errorParsing `catch` variableNotSet+ ((invokeAndRun callback manager lambdaApi event context+ `catch` \err -> Publish.parsingError err lambdaApi context manager)+ `catch` \err -> Publish.invocationError err lambdaApi context manager)+ `catch` \(err :: Error.EnvironmentVariableNotSet) -> Publish.runtimeInitError err lambdaApi context manager +httpManagerSettings :: Http.ManagerSettings+httpManagerSettings =+ -- We set the timeout to none, as AWS Lambda freezes the containers.+ Http.defaultManagerSettings+ { Http.managerResponseTimeout = Http.responseTimeoutNone+ }+ invokeAndRun- :: Throws Error.Parsing- => Throws Error.Invocation+ :: Throws Error.Invocation => Throws Error.EnvironmentVariableNotSet- => Http.Manager+ => Runtime.RunCallback+ -> Http.Manager -> String -> ApiInfo.Event -> Context.Context -> IO ()-invokeAndRun manager lambdaApi event context = do- result <- IPC.invoke (ApiInfo.event event) context+invokeAndRun callback manager lambdaApi event context = do+ result <- invokeWithCallback callback event context Publish.result result lambdaApi context manager `catch` \err -> Publish.invocationError err lambdaApi context manager++invokeWithCallback+ :: Throws Error.Invocation+ => Throws Error.EnvironmentVariableNotSet+ => Runtime.RunCallback+ -> ApiInfo.Event+ -> Context.Context+ -> IO Runtime.LambdaResult+invokeWithCallback callback event context = do+ handlerName <- Environment.handlerName+ let lambdaOptions = Runtime.LambdaOptions+ { eventObject = LazyByteString.unpack $ ApiInfo.event event+ , contextObject = LazyByteString.unpack . encode $ context+ , functionHandler = handlerName+ , executionUuid = "" -- DirectCall doesnt use UUID+ }+ result <- callback lambdaOptions+ case result of+ Left err ->+ throw $ Error.Invocation err+ Right value ->+ pure value variableNotSet :: Error.EnvironmentVariableNotSet -> IO a variableNotSet (Error.EnvironmentVariableNotSet env) =
+ src/Aws/Lambda/Runtime/Common.hs view
@@ -0,0 +1,23 @@+module Aws.Lambda.Runtime.Common+ ( RunCallback+ , LambdaResult(..)+ , LambdaOptions(..)+ ) where++import GHC.Generics (Generic)++-- | Callback that we pass to the dispatcher function+type RunCallback =+ LambdaOptions -> IO (Either String LambdaResult)++-- | Options that the generated main expects+data LambdaOptions = LambdaOptions+ { eventObject :: !String+ , contextObject :: !String+ , functionHandler :: !String+ , executionUuid :: !String+ } deriving (Generic)++-- | Wrapper type to handle the result of the user+newtype LambdaResult =+ LambdaResult String
− src/Aws/Lambda/Runtime/IPC.hs
@@ -1,116 +0,0 @@-{-| Inter-Process Communication--Used for when the user project is called from a layer.--This is used to call the @haskell_lambda@ executable, which is-provided by the user, when they want to use the layer.--This IPC protocol is based on printing an UUID that is-created by the layer, and then the result. So everything that-is printed before the UUID, is considered STDOUT printed by-the lambda, while what comes after the UUID is considered.--In the case that the lambda execution fails, the exit code-won't be 0 (exit-success), so it will use the STDERR.--}-module Aws.Lambda.Runtime.IPC- ( invoke- , returnAndFail- , returnAndSucceed- ) where---import Data.Function ((&))-import qualified Data.Maybe as Maybe-import qualified Data.String as String-import qualified System.Exit as Exit-import qualified System.IO as IO-import qualified System.Process as Process--import Control.Exception.Safe.Checked-import Data.Aeson-import qualified Data.ByteString.Lazy.Char8 as ByteString-import qualified Data.UUID as UUID-import qualified Data.UUID.V4 as UUID--import Aws.Lambda.Runtime.Context (Context (..))-import qualified Aws.Lambda.Runtime.Environment as Environment-import qualified Aws.Lambda.Runtime.Error as Error-import Aws.Lambda.Runtime.Result (LambdaResult (..))---- | Returns the JSON value failing, according to the protocol-returnAndFail :: ToJSON a => String -> a -> IO ()-returnAndFail uuid v = do- IO.hFlush IO.stdout- putStrLn uuid- IO.hFlush IO.stdout- putStrLn (ByteString.unpack $ encode v)- IO.hFlush IO.stdout- IO.hFlush IO.stderr- Exit.exitFailure---- | Returns the JSON value succeeding, according to the protocol-returnAndSucceed :: ToJSON a => String -> a -> IO ()-returnAndSucceed uuid v = do- IO.hFlush IO.stdout- putStrLn uuid- IO.hFlush IO.stdout- putStrLn (ByteString.unpack $ encode v)- IO.hFlush IO.stdout- Exit.exitSuccess---- | Invokes a function defined by the user as the @haskell_lambda@ executable-invoke- :: Throws Error.Invocation- => Throws Error.Parsing- => Throws Error.EnvironmentVariableNotSet- => ByteString.ByteString- -> Context- -> IO LambdaResult-invoke event context = do- handlerName <- Environment.handlerName- runningDirectory <- Environment.taskRoot- let contextJSON = ByteString.unpack $ encode context- uuid <- UUID.nextRandom- out <- Process.readProcessWithExitCode (runningDirectory <> "/haskell_lambda")- [ "--eventObject", ByteString.unpack event- , "--contextObject", contextJSON- , "--functionHandler", handlerName- , "--executionUuid", UUID.toString uuid- ]- ""- case out of- (Exit.ExitSuccess, stdOut, _) -> do- res <- getFunctionResult uuid stdOut- case res of- Nothing -> throw (Error.Parsing "parsing result" stdOut)- Just value -> pure (LambdaResult value)- (_, stdOut, stdErr) ->- if stdErr /= ""- then throw (Error.Invocation stdErr)- else do- res <- getFunctionResult uuid stdOut- case res of- Nothing -> throw (Error.Parsing "parsing error" stdOut)- Just value -> throw (Error.Invocation value)--getFunctionResult :: UUID.UUID -> String -> IO (Maybe String)-getFunctionResult u stdOut = do- let out = String.lines stdOut- let uuid = UUID.toString u- printAfterUuid uuid out- returnAfterUuid uuid out- where- printAfterUuid uuid out =- out- & takeWhile (/= uuid)- & mapM_ ( \t -> do- putStrLn t- IO.hFlush IO.stdout )-- returnAfterUuid uuid out =- out- & dropWhile (/= uuid)- & dropWhile (== uuid)- & Maybe.listToMaybe- & pure
src/Aws/Lambda/Runtime/Publish.hs view
@@ -13,9 +13,9 @@ import qualified Network.HTTP.Client as Http import qualified Aws.Lambda.Runtime.API.Endpoints as Endpoints+import Aws.Lambda.Runtime.Common import Aws.Lambda.Runtime.Context (Context (..)) import qualified Aws.Lambda.Runtime.Error as Error-import Aws.Lambda.Runtime.Result (LambdaResult (..)) -- | Publishes the result back to AWS Lambda result :: LambdaResult -> String -> Context -> Http.Manager -> IO ()
− src/Aws/Lambda/Runtime/Result.hs
@@ -1,7 +0,0 @@-module Aws.Lambda.Runtime.Result- ( LambdaResult(..)- ) where---- | Wrapper type to handle the result of the user-newtype LambdaResult =- LambdaResult String