aws-larpi (empty) → 0.1.0.0
raw patch · 5 files changed
+303/−0 lines, 5 filesdep +aesondep +basedep +bytestring
Dependencies added: aeson, base, bytestring, modern-uri, req, text
Files
- CHANGELOG.md +5/−0
- Dockerfile +36/−0
- LICENSE +21/−0
- aws-larpi.cabal +41/−0
- src/AWS/LARPI.hs +200/−0
+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for AWS-LARPI++## 0.1.0.0 -- 2021-01-25++* First version. Released on an unsuspecting world.
+ Dockerfile view
@@ -0,0 +1,36 @@+FROM frolvlad/alpine-ghc as build+# install build tools+# cache dependencies+# build+RUN apk add cabal wget zlib zlib-dev+RUN mkdir /cabal-build+RUN mkdir /cabal-bins+RUN cabal update+RUN cabal install --global req warp++ADD aws-larpi.cabal .+ADD LICENSE .+ADD CHANGELOG.md .+ADD Dockerfile .+ADD src/ src/++RUN cabal build --builddir=/cabal-build --enable-static --enable-executable-static aws-larpi++ARG PKG=test++ADD $PKG/$PKG.cabal $PKG/+RUN cabal build --builddir=/cabal-build --enable-static --enable-executable-static --only-dependencies $PKG+ADD $PKG/ $PKG/++RUN cabal install --builddir=/cabal-build --installdir=/cabal-bins --install-method=copy\+ --enable-static --enable-executable-static $PKG++# copy artifacts to a clean image+FROM alpine+RUN apk add libffi gmp zlib zlib-dev+ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/bin/aws-lambda-rie+RUN chmod 755 /usr/bin/aws-lambda-rie+COPY entry.sh /+RUN chmod 755 /entry.sh+COPY --from=build /cabal-bins /cabal-bins+ENTRYPOINT [ "/entry.sh" ]
+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2021 Matthías Páll Gissurarson++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ aws-larpi.cabal view
@@ -0,0 +1,41 @@+cabal-version: >=1.10+-- Initial package description 'AWS-LARPI.cabal' generated by 'cabal init'.+-- For further documentation, see http://haskell.org/cabal/users-guide/++name: aws-larpi+version: 0.1.0.0+synopsis: Package Haskell functions for easy use on AWS Lambda.+description: The AWS LARPI package allows you to run Haskell code on AWS+ by defining a simple request handler for AWS Lambda requests, and then+ packaging it using the Dockerfile included. Uses the + <https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html AWS Lambda runtime API>.+-- bug-reports:+license: MIT+license-file: LICENSE+author: Matthías Páll Gissurarson+maintainer: mpg@mpg.is+-- copyright:+category: AWS+homepage: https://github.com/Tritlo/AWS-LARPI+build-type: Simple+extra-source-files: CHANGELOG.md,+ Dockerfile++tested-with: GHC == 8.8.3++source-repository head+ type: git+ location: git://github.com/Tritlo/AWS-LARPI.git++library+ hs-source-dirs: src/+ exposed-modules: AWS.LARPI+ -- other-extensions:+ build-depends: base >=4.13 && <4.14,+ req >= 3.8 && < 3.9,+ bytestring >= 0.9 && < 0.11,+ text >= 1.2 && < 1.3,+ modern-uri >= 0.3 && < 0.4,+ aeson >= 1.5 && < 1.6+ -- hs-source-dirs:+ default-language: Haskell2010
+ src/AWS/LARPI.hs view
@@ -0,0 +1,200 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}+{-# LANGUAGE RankNTypes, ExistentialQuantification #-}++-- | The AWS.LARPI library defines the interface and the associated data types+-- for running a Haskell program directly on AWS Lambda, and handles most of+-- associated overheads, leaving the user free to focus on the functionality+-- itself.+module AWS.LARPI (+ runLambdaInterface,++ LambdaInterface(..), LambdaInvocation(..), LambdaError(..)++) where+++import System.Environment++import Network.HTTP.Req+import Text.URI++import Data.ByteString.Char8 (ByteString)+import qualified Data.ByteString.Char8 as B+import qualified Data.ByteString.Lazy as BL++import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T++import Data.Maybe++import Data.Aeson+import GHC.Generics++import Control.Monad (when)+++-- CONSTANTS+version :: Text+version = "2018-06-01"++invPath :: Url 'Http -> Url 'Http+invPath api = api /~ version /: "runtime" /: "invocation"++errHeader :: Option 'Http+errHeader = header "Lambda-Runtime-Function-Error-Type" "Unhandled"+++data Config = Conf { c_base :: Url 'Http+ , c_opts :: Option 'Http }++++invocationResponse :: Config -> Text -> ByteString -> IO ()+invocationResponse Conf{..} req_id resp =+ runReq defaultHttpConfig $ do+ req POST+ (invPath c_base /: req_id /: "response" )+ (ReqBodyBs resp)+ ignoreResponse+ c_opts+ return ()+++invocationError :: Config -> Text -> LambdaError -> IO ()+invocationError Conf{..} req_id lerr =+ runReq defaultHttpConfig $ do+ res <- req POST+ (invPath c_base /: req_id /: "error" )+ (ReqBodyJson lerr)+ ignoreResponse+ (c_opts <> errHeader)+ return ()++initializationError :: Config -> LambdaError -> IO ()+initializationError Conf{..} lerr =+ runReq defaultHttpConfig $ do+ res <- req POST+ (c_base /~ version /: "runtime" /: "init" /: "error" )+ (ReqBodyJson lerr)+ ignoreResponse+ (c_opts <> errHeader)+ return ()+++nextInvocation :: Config -> IO LambdaInvocation+nextInvocation Conf{..} = runReq defaultHttpConfig $ do+ res <- req GET+ (invPath c_base /: "next")+ NoReqBody+ bsResponse+ (c_opts <> responseTimeout maxBound)+ let rH = responseHeader res++ return $ LambdaInvocation {+ li_body = responseBody res+ , li_aws_request_id = fromJust $+ T.decodeUtf8 <$> (rH "Lambda-Runtime-Aws-Request-Id")+ , li_deadline_ms = rH "Lambda-Runtime-Deadline-Ms"+ , li_invoked_function_arn = rH "Lambda-Runtime-Invoked-Function-Arn"+ , li_trace_id = rH "Lambda-Runtime-Trace-Id"+ , li_client_context = rH "Lambda-Runtime-Client-Context"+ , li_cognito_identity = rH "Lambda-Runtime-Cognito-Identity" }++--- EXPORTS+++-- | A Lambda error consists of a message and an error type, both of which+-- the user can define.+data LambdaError =+ LambdaErr { errorMessage :: String+ , errorType :: String }+ deriving (Show, Generic, ToJSON)++-- | A Lambda invocaton contains the body of the request and a few headers+-- defined by AWS. Only the AWS-Request-Id header is guaranteed to be defined.+-- The headers are the following:+--+-- * li_aws_request_id – The request ID, which identifies the request that triggered+-- the function invocation.+--+-- For example, 8476a536-e9f4-11e8-9739-2dfe598c3fcd.+--+-- * li_deadline_ms – The date that the function times out in Unix time milliseconds.+--+-- For example, 1542409706888.+--+-- * li_invoked_function_arn – The ARN of the Lambda function, version, or alias+-- that's specified in the invocation.+--+-- For example, arn:aws:lambda:us-east-2:123456789012:function:custom-runtime.+--+-- * li_trace_id – The AWS X-Ray tracing+-- header.+--+-- For example, Root=1-5bef4de7-ad49b0e87f6ef6c87fc2e700;Parent=9a9197af755a6419;Sampled=1.+--+-- * li_client_context – For invocations from the AWS Mobile SDK, data about the+-- client application and device.+--+-- * li_cognito_identity – For invocations from the AWS Mobile SDK, data about the+-- Amazon Cognito identity provider.+data LambdaInvocation =+ LambdaInvocation {+ li_body :: ByteString+ , li_aws_request_id :: Text+ , li_deadline_ms :: Maybe ByteString+ , li_invoked_function_arn :: Maybe ByteString+ , li_trace_id :: Maybe ByteString+ , li_client_context :: Maybe ByteString+ , li_cognito_identity :: Maybe ByteString}+ deriving (Show, Eq)+++-- | A Lambda interface consists of two functions:+--+-- The li_handler is the function that is run on every Lambda Invocation+-- the results will be dumped as JSON, as this is what most AWS APIs+-- expect. The first argument is the result of the li_init function, such+-- as a database connection or an IORef or similar. Existentially+-- quantified in the same manner as the ST monad to avoid the state+-- escaping.+--+-- The li_init function is called at the start of the program to initialize+-- the Lambda function, and some optional state. The state is then passed+-- to the handler at each invocation, e.g. a database connection or similar.+data LambdaInterface a = forall s.+ LambdaInterface {+ li_handler :: ToJSON a => s+ -> LambdaInvocation+ -> IO (Either LambdaError a)+ , li_init :: IO (Either LambdaError s)+ }++-- | The runLambdaInteface should be provided with the definition of the+-- interface to run, and handles the interaction with the AWS API, such as the+-- waiting for next request, responding to the right request, setting the+-- X-Ray trace id and initializing the function at the start.+runLambdaInterface :: ToJSON a => LambdaInterface a -> IO ()+runLambdaInterface (LambdaInterface {..}) =+ do Just api <- fmap T.pack <$> lookupEnv "AWS_LAMBDA_RUNTIME_API"+ Just (c_base, c_opts) <- useHttpURI <$> mkURI ("http://" <> api)+ let conf = Conf{..}+ i_res <- li_init+ case i_res of+ Left err -> initializationError conf err+ Right init_state -> loop conf init_state li_handler+ where loop conf init_state handler = do+ li@LambdaInvocation{..} <- nextInvocation conf+ when (isJust li_trace_id) $+ setEnv "_X_AMZN_TRACE_ID" $ B.unpack $ fromJust li_trace_id+ res <- handler init_state li+ case res of+ Left lerr -> invocationError conf li_aws_request_id lerr+ Right resp -> invocationResponse conf li_aws_request_id+ (BL.toStrict $ encode resp)+ loop conf init_state handler+