diff --git a/infra/api_gateway.tf b/infra/api_gateway.tf index e38f97941..fccbcf65b 100644 --- a/infra/api_gateway.tf +++ b/infra/api_gateway.tf @@ -2,6 +2,10 @@ resource "aws_api_gateway_rest_api" "lambda-api" { name = "deckdeckgo-handler-rest-api" } +### +### HANDLER +### + resource "aws_api_gateway_resource" "proxy-api" { rest_api_id = "${aws_api_gateway_rest_api.lambda-api.id}" parent_id = "${aws_api_gateway_rest_api.lambda-api.root_resource_id}" @@ -16,7 +20,7 @@ resource "aws_api_gateway_resource" "proxy" { resource "aws_api_gateway_method" "proxy" { rest_api_id = "${aws_api_gateway_rest_api.lambda-api.id}" - resource_id = "${aws_api_gateway_resource.proxy.id}" + resource_id = "${aws_api_gateway_resource.proxy.id}" # TODO: -api? http_method = "ANY" authorization = "NONE" } @@ -32,31 +36,83 @@ resource "aws_api_gateway_integration" "lambda-api" { uri = "${aws_lambda_function.api.invoke_arn}" } -resource "aws_api_gateway_deployment" "lambda-api" { +resource "aws_lambda_permission" "lambda_permission" { + action = "lambda:InvokeFunction" + function_name = "${aws_lambda_function.api.function_name}" + principal = "apigateway.amazonaws.com" + source_arn = "${aws_api_gateway_rest_api.lambda-api.execution_arn}/*/*/*" + depends_on = [ - "aws_api_gateway_integration.lambda-api", - "aws_api_gateway_resource.proxy-api", - "aws_api_gateway_resource.proxy", + "aws_lambda_function.api", ] +} + +### +### UNSPLASH +### +resource "aws_api_gateway_resource" "unsplash-proxy-root" { rest_api_id = "${aws_api_gateway_rest_api.lambda-api.id}" - stage_name = "beta" + parent_id = "${aws_api_gateway_rest_api.lambda-api.root_resource_id}" + path_part = "unsplash" } -resource "aws_lambda_permission" "lambda_permission" { +resource "aws_api_gateway_resource" "unsplash-proxy" { + rest_api_id = "${aws_api_gateway_rest_api.lambda-api.id}" + parent_id = "${aws_api_gateway_resource.unsplash-proxy-root.id}" + path_part = "{proxy+}" +} + +resource "aws_api_gateway_method" "unsplash-proxy" { + rest_api_id = "${aws_api_gateway_rest_api.lambda-api.id}" + resource_id = "${aws_api_gateway_resource.unsplash-proxy.id}" + http_method = "ANY" + authorization = "NONE" +} + +# XXX: when redeploying, tweak the stage name +resource "aws_api_gateway_integration" "lambda-unsplash" { + rest_api_id = "${aws_api_gateway_rest_api.lambda-api.id}" + resource_id = "${aws_api_gateway_method.unsplash-proxy.resource_id}" + http_method = "${aws_api_gateway_method.unsplash-proxy.http_method}" + + integration_http_method = "POST" + type = "AWS_PROXY" + uri = "${aws_lambda_function.unsplash.invoke_arn}" +} + +resource "aws_lambda_permission" "lambda_permission_unsplash" { action = "lambda:InvokeFunction" - function_name = "${aws_lambda_function.api.function_name}" + function_name = "${aws_lambda_function.unsplash.function_name}" principal = "apigateway.amazonaws.com" source_arn = "${aws_api_gateway_rest_api.lambda-api.execution_arn}/*/*/*" depends_on = [ - "aws_lambda_function.api", + "aws_lambda_function.unsplash", + ] +} + +### +### GATEWAY GENERAL +### + +resource "aws_api_gateway_deployment" "lambda-api" { + depends_on = [ + "aws_api_gateway_integration.lambda-api", + "aws_api_gateway_resource.proxy-api", + "aws_api_gateway_resource.proxy", + "aws_api_gateway_resource.unsplash-proxy", + "aws_api_gateway_resource.unsplash-proxy-root", ] + + rest_api_id = "${aws_api_gateway_rest_api.lambda-api.id}" + stage_name = "beta" } ############### # Enable CORS # ############### + # https://medium.com/@MrPonath/terraform-and-aws-api-gateway-a137ee48a8ac resource "aws_api_gateway_method" "options_method" { rest_api_id = "${aws_api_gateway_rest_api.lambda-api.id}" diff --git a/infra/default.nix b/infra/default.nix index 6f47e3ce5..58250fb22 100644 --- a/infra/default.nix +++ b/infra/default.nix @@ -1,7 +1,7 @@ with { pkgs = import ./nix {}; }; rec -{ function = +{ function = # TODO: rename to handler pkgs.runCommand "build-lambda" {} '' cp ${pkgs.wai-lambda.wai-lambda-js-wrapper} main.js @@ -12,9 +12,22 @@ rec ${pkgs.zip}/bin/zip -r $out/function.zip main.js main_hs google-public-keys.json ''; + function-unsplash = + pkgs.runCommand "build-lambda" {} + '' + cp ${pkgs.wai-lambda.wai-lambda-js-wrapper} main.js + # Can't be called 'main' otherwise lambda tries to load it + cp "${unsplashProxyStatic}/bin/unsplash-proxy" main_hs + mkdir $out + ${pkgs.zip}/bin/zip -r $out/function.zip main.js main_hs + ''; + handlerStatic = pkgs.haskellPackagesStatic.deckdeckgo-handler; handler = pkgs.haskellPackages.deckdeckgo-handler; + unsplashProxyStatic = pkgs.haskellPackagesStatic.unsplash-proxy; + unsplashProxy = pkgs.haskellPackages.unsplash-proxy; + dynamoJar = pkgs.runCommand "dynamodb-jar" { buildInputs = [ pkgs.gnutar ]; } '' mkdir -p $out diff --git a/infra/nix/default.nix b/infra/nix/default.nix index 29f34f546..61989297e 100644 --- a/infra/nix/default.nix +++ b/infra/nix/default.nix @@ -56,6 +56,7 @@ with rec mkPackage "deckdeckgo-handler" ../handler // ( mkPackage "wai-lambda" wai-lambda.wai-lambda-source ) // ( mkPackage "firebase-login" ../firebase-login ) // + ( mkPackage "unsplash-proxy" ../unsplash-proxy ) // { jose = super.callCabal2nix "jose" sources.hs-jose {}; } // { port-utils = super.callCabal2nix "port-utils" sources.port-utils {}; } ; }; diff --git a/infra/script/build-function b/infra/script/build-function index 63f8759bd..78e6b5941 100755 --- a/infra/script/build-function +++ b/infra/script/build-function @@ -1,5 +1,6 @@ #!/usr/bin/env bash # vim: filetype=sh +# TODO: rename to build-handler set -euo pipefail diff --git a/infra/script/build-unsplash-proxy b/infra/script/build-unsplash-proxy new file mode 100755 index 000000000..53929779c --- /dev/null +++ b/infra/script/build-unsplash-proxy @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# vim: filetype=sh +# TODO: rename to build-handler + +set -euo pipefail + +out=$(nix-build --no-out-link -A function-unsplash) + +cat < ClientAPI + +type ClientAPI = + "search" :> + "photos" :> + QueryParam "query" UnsplashQuery :> + QueryParam "client_id" UnsplashCliendId :> + QueryParam "page" T.Text :> + Get '[JSON] Aeson.Value :<|> + "photos" :> + Capture "photoId" UnsplashPhotoId :> + "download" :> + QueryParam "client_id" UnsplashCliendId :> + Get '[JSON] Aeson.Value + +runClient + :: MonadIO io + => Client.ClientM a + -> io (Either Client.ServantError a) +runClient act = liftIO $ do + mgr <- HTTP.newManager HTTP.tlsManagerSettings + Client.runClientM act (clientEnv mgr) + where + clientEnv mgr = + Client.mkClientEnv + mgr (Client.BaseUrl Client.Https "api.unsplash.com" 443 "") + +getUnsplashClientId :: IO UnsplashCliendId +getUnsplashClientId = + (UnsplashCliendId . T.pack) <$> getEnv "UNSPLASH_CLIENT_ID" + +proxySearch + :: Maybe UnsplashQuery + -> Maybe UnsplashCliendId + -> Maybe T.Text + -> Servant.Handler Aeson.Value +proxySearch mq Nothing mPage = do + c <- liftIO getUnsplashClientId + liftIO $ putStrLn "proxySearch: calling" + runClient (proxySearch' mq (Just c) mPage) >>= \case + Left e -> do + liftIO $ print e + Servant.throwError Servant.err500 + Right bs -> pure bs +proxySearch mq (Just c) mPage = do + liftIO $ putStrLn "proxySearch: calling" + runClient (proxySearch' mq (Just c) mPage) >>= \case + Left e -> do + liftIO $ print e + Servant.throwError Servant.err500 + Right bs -> pure bs + +proxySearch' + :: Maybe UnsplashQuery + -> Maybe UnsplashCliendId + -> Maybe T.Text + -> Client.ClientM Aeson.Value +proxyDownload' + :: UnsplashPhotoId + -> Maybe UnsplashCliendId + -> Client.ClientM Aeson.Value +proxySearch' :<|> proxyDownload' = Client.client clientApi + +proxyDownload + :: UnsplashPhotoId + -> Maybe UnsplashCliendId + -> Servant.Handler Aeson.Value +proxyDownload photId Nothing = do + c <- liftIO getUnsplashClientId + liftIO $ putStrLn "proxyDownload: calling" + runClient (proxyDownload' photId (Just c)) >>= \case + Left e -> do + liftIO $ print e + Servant.throwError Servant.err500 + Right bs -> pure bs +proxyDownload photId (Just c) = do + liftIO $ putStrLn "proxyDownload: calling" + runClient (proxyDownload' photId (Just c)) >>= \case + Left e -> do + liftIO $ print e + Servant.throwError Servant.err500 + Right bs -> pure bs + +serverApi :: Proxy ServerAPI +serverApi = Proxy + +clientApi :: Proxy ClientAPI +clientApi = Proxy + +server :: Servant.Server ServerAPI +server = proxySearch :<|> proxyDownload + +application :: Wai.Application +application = Servant.serve serverApi server + +main :: IO () +main = do + hSetBuffering stdin LineBuffering + hSetBuffering stdout LineBuffering + Lambda.runSettings settings $ cors $ application + where + settings = Lambda.defaultSettings + { Lambda.timeoutValue = 9 * 1000 * 1000 } + +cors :: Wai.Middleware +cors = Cors.cors $ + const $ + Just Cors.simpleCorsResourcePolicy { Cors.corsMethods = methods } + +methods :: [HTTP.Method] +methods = + [ "GET" + , "HEAD" + ] diff --git a/infra/unsplash-proxy/package.yaml b/infra/unsplash-proxy/package.yaml new file mode 100644 index 000000000..15dac7ec4 --- /dev/null +++ b/infra/unsplash-proxy/package.yaml @@ -0,0 +1,22 @@ +name: unsplash-proxy +license: AGPL-3 + +executable: + main: Main.hs + +dependencies: + - aeson + - base + - bytestring + - http-client + - http-client-tls + - http-conduit + - http-types + - servant + - servant-client + - servant-server + - text + - wai + - wai-cors + - wai-lambda + - warp diff --git a/infra/unsplash_lambda.tf b/infra/unsplash_lambda.tf new file mode 100644 index 000000000..9967c944f --- /dev/null +++ b/infra/unsplash_lambda.tf @@ -0,0 +1,92 @@ +variable "unsplash_name" { + type = "string" + default = "deckdeckgo-unsplash-lambda" +} + +resource "aws_lambda_function" "unsplash" { + function_name = "${var.unsplash_name}" + filename = "${data.external.build-function-unsplash.result.build_function_zip_path}" + handler = "main.handler" + runtime = "nodejs8.10" + timeout = 10 + + role = "${aws_iam_role.iam_for_unsplash_lambda.arn}" + + environment { + variables = { + UNSPLASH_CLIENT_ID = "${data.external.unsplash-client-id.result.unsplash-client-id}" + } + } + + depends_on = + [ "aws_iam_role_policy_attachment.unsplash_lambda_logs", + "aws_cloudwatch_log_group.unsplash_group" + ] +} + +resource "aws_iam_role" "iam_for_unsplash_lambda" { + name = "deckdeckgo-unsplash-lambda-iam" + + assume_role_policy = <