diff --git a/README.md b/README.md index acba764a1..0aba3857b 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ To do so, it starts an HTTP server that handles the request's lifecycle like API **Features:** -- [Node.js](https://nodejs.org), [Python](https://www.python.org), [Ruby](https://www.ruby-lang.org) λ runtimes. +- [Node.js](https://nodejs.org), [Python](https://www.python.org), [Ruby](https://www.ruby-lang.org) and [Go](https://golang.org) λ runtimes. - Velocity templates support. - Lazy loading of your handler files. - And more: integrations, authorizers, proxies, timeouts, responseParameters, HTTPS, CORS, etc... diff --git a/src/lambda/handler-runner/HandlerRunner.js b/src/lambda/handler-runner/HandlerRunner.js index 652a9fde7..2e079d616 100644 --- a/src/lambda/handler-runner/HandlerRunner.js +++ b/src/lambda/handler-runner/HandlerRunner.js @@ -5,6 +5,7 @@ import { supportedPython, supportedRuby, supportedJava, + supportedGo, } from '../../config/index.js' import { satisfiesVersionRange } from '../../utils/index.js' @@ -116,6 +117,11 @@ export default class HandlerRunner { ) } + if (supportedGo.has(runtime)) { + const { default: GoRunner } = await import('./go-runner/index.js') + return new GoRunner(this.#funOptions, this.#env, this.v3Utils) + } + if (supportedPython.has(runtime)) { const { default: PythonRunner } = await import('./python-runner/index.js') return new PythonRunner( diff --git a/src/lambda/handler-runner/go-runner/GoRunner.js b/src/lambda/handler-runner/go-runner/GoRunner.js new file mode 100644 index 000000000..4814c9eeb --- /dev/null +++ b/src/lambda/handler-runner/go-runner/GoRunner.js @@ -0,0 +1,153 @@ +import { EOL } from 'os' +import { promises as fsPromises } from 'fs' +import { sep, resolve, parse as pathParse } from 'path' +import execa, { sync } from 'execa' + +const { writeFile, readFile, mkdir, rmdir } = fsPromises +const { parse, stringify } = JSON +const { cwd } = process + +const PAYLOAD_IDENTIFIER = 'offline_payload' + +export default class GoRunner { + #env = null + #handlerPath = null + #tmpPath = null + #tmpFile = null + #goEnv = null + + constructor(funOptions, env, v3Utils) { + const { handlerPath } = funOptions + + this.#env = env + this.#handlerPath = handlerPath + + if (v3Utils) { + this.log = v3Utils.log + this.progress = v3Utils.progress + this.writeText = v3Utils.writeText + this.v3Utils = v3Utils + } + + // Make sure we have the mock-lambda runner + sync('go', ['get', 'github.com/icarus-sullivan/mock-lambda@e065469']) + } + + async cleanup() { + try { + await rmdir(this.#tmpPath, { recursive: true }) + } catch (e) { + // @ignore + } + + this.#tmpFile = null + this.#tmpPath = null + } + + _parsePayload(value) { + const log = [] + let payload + + for (const item of value.split(EOL)) { + if (item.indexOf(PAYLOAD_IDENTIFIER) === -1) { + log.push(item) + } else if (item.indexOf(PAYLOAD_IDENTIFIER) !== -1) { + try { + const { + offline_payload: { success, error }, + } = parse(item) + if (success) { + payload = success + } else if (error) { + payload = error + } + } catch (err) { + // @ignore + } + } + } + + // Log to console in case engineers want to see the rest of the info + if (this.log) { + this.log(log.join(EOL)) + } else { + console.log(log.join(EOL)) + } + + return payload + } + + async run(event, context) { + const { dir } = pathParse(this.#handlerPath) + const handlerCodeRoot = dir.split(sep).slice(0, -1).join(sep) + const handlerCode = await readFile(`${this.#handlerPath}.go`, 'utf8') + this.#tmpPath = resolve(handlerCodeRoot, 'tmp') + this.#tmpFile = resolve(this.#tmpPath, 'main.go') + + const out = handlerCode.replace( + '"github.com/aws/aws-lambda-go/lambda"', + 'lambda "github.com/icarus-sullivan/mock-lambda"', + ) + + try { + await mkdir(this.#tmpPath, { recursive: true }) + } catch (e) { + // @ignore + } + + try { + await writeFile(this.#tmpFile, out, 'utf8') + } catch (e) { + // @ignore + } + + // Get go env to run this locally + if (!this.#goEnv) { + const goEnvResponse = await execa('go', ['env'], { + stdio: 'pipe', + encoding: 'utf-8', + }) + + const goEnvString = goEnvResponse.stdout || goEnvResponse.stderr + this.#goEnv = goEnvString.split(EOL).reduce((a, b) => { + const [k, v] = b.split('="') + // eslint-disable-next-line no-param-reassign + a[k] = v ? v.slice(0, -1) : '' + return a + }, {}) + } + + // Remove our root, since we want to invoke go relatively + const cwdPath = `${this.#tmpFile}`.replace(`${cwd()}${sep}`, '') + const { stdout, stderr } = await execa(`go`, ['run', cwdPath], { + stdio: 'pipe', + env: { + ...this.#env, + ...this.#goEnv, + AWS_LAMBDA_LOG_GROUP_NAME: context.logGroupName, + AWS_LAMBDA_LOG_STREAM_NAME: context.logStreamName, + AWS_LAMBDA_FUNCTION_NAME: context.functionName, + AWS_LAMBDA_FUNCTION_MEMORY_SIZE: context.memoryLimitInMB, + AWS_LAMBDA_FUNCTION_VERSION: context.functionVersion, + LAMBDA_EVENT: stringify(event), + LAMBDA_TEST_EVENT: `${event}`, + LAMBDA_CONTEXT: stringify(context), + IS_LAMBDA_AUTHORIZER: + event.type === 'REQUEST' || event.type === 'TOKEN', + IS_LAMBDA_REQUEST_AUTHORIZER: event.type === 'REQUEST', + IS_LAMBDA_TOKEN_AUTHORIZER: event.type === 'TOKEN', + PATH: process.env.PATH, + }, + encoding: 'utf-8', + }) + + // Clean up after we created the temporary file + await this.cleanup() + + if (stderr) { + return stderr + } + + return this._parsePayload(stdout) + } +} diff --git a/src/lambda/handler-runner/go-runner/index.js b/src/lambda/handler-runner/go-runner/index.js new file mode 100644 index 000000000..1129f2077 --- /dev/null +++ b/src/lambda/handler-runner/go-runner/index.js @@ -0,0 +1 @@ +export { default } from './GoRunner.js' diff --git a/src/utils/checkGoVersion.js b/src/utils/checkGoVersion.js new file mode 100644 index 000000000..fb1b91d4f --- /dev/null +++ b/src/utils/checkGoVersion.js @@ -0,0 +1,15 @@ +import execa from 'execa' + +export default async function checkGoVersion() { + let goVersion + try { + const { stdout } = await execa('go', ['version']) + if (stdout.match(/go1.\d+/g)) { + goVersion = '1.x' + } + } catch (err) { + // @ignore + } + + return goVersion +} diff --git a/src/utils/index.js b/src/utils/index.js index 8ac844a0a..b65419fc0 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -14,6 +14,7 @@ export { default as parseQueryStringParameters } from './parseQueryStringParamet export { default as satisfiesVersionRange } from './satisfiesVersionRange.js' export { default as splitHandlerPathAndName } from './splitHandlerPathAndName.js' export { default as checkDockerDaemon } from './checkDockerDaemon.js' +export { default as checkGoVersion } from './checkGoVersion.js' export { default as generateHapiPath } from './generateHapiPath.js' // export { default as baseImage } from './baseImage.js' diff --git a/tests/_setupTeardown/npmInstall.js b/tests/_setupTeardown/npmInstall.js index 570dffd53..d036e8526 100644 --- a/tests/_setupTeardown/npmInstall.js +++ b/tests/_setupTeardown/npmInstall.js @@ -1,7 +1,11 @@ import { resolve } from 'path' import execa from 'execa' import promiseMap from 'p-map' -import { checkDockerDaemon, detectExecutable } from '../../src/utils/index.js' +import { + checkDockerDaemon, + checkGoVersion, + detectExecutable, +} from '../../src/utils/index.js' const executables = ['python2', 'python3', 'ruby', 'java'] @@ -52,6 +56,11 @@ export default async function npmInstall() { } } + const go = await checkGoVersion() + if (go && go === '1.x') { + process.env.GO1X_DETECTED = true + } + if (python2) { process.env.PYTHON2_DETECTED = true } diff --git a/tests/integration/go/go1.x/.gitignore b/tests/integration/go/go1.x/.gitignore new file mode 100644 index 000000000..ba077a403 --- /dev/null +++ b/tests/integration/go/go1.x/.gitignore @@ -0,0 +1 @@ +bin diff --git a/tests/integration/go/go1.x/go.mod b/tests/integration/go/go1.x/go.mod new file mode 100644 index 000000000..65a851a46 --- /dev/null +++ b/tests/integration/go/go1.x/go.mod @@ -0,0 +1,9 @@ +module serverless-offline-go1.x-test + +go 1.13 + +require ( + github.com/aws/aws-lambda-go v1.28.0 + github.com/icarus-sullivan/mock-lambda v0.0.0-20220115083805-e065469e964a // indirect + github.com/urfave/cli v1.22.1 // indirect +) diff --git a/tests/integration/go/go1.x/go.sum b/tests/integration/go/go1.x/go.sum new file mode 100644 index 000000000..80ba1e7b7 --- /dev/null +++ b/tests/integration/go/go1.x/go.sum @@ -0,0 +1,29 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/aws/aws-lambda-go v1.13.2 h1:8lYuRVn6rESoUNZXdbCmtGB4bBk4vcVYojiHjE4mMrM= +github.com/aws/aws-lambda-go v1.13.2/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-lambda-go v1.28.0 h1:fZiik1PZqW2IyAN4rj+Y0UBaO1IDFlsNo9Zz/XnArK4= +github.com/aws/aws-lambda-go v1.28.0/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/icarus-sullivan/mock-lambda v0.0.0-20220114085425-44091545252e h1:cPv6jHZPqHlu73UmtFEVPRNHGnSrd43OKwpQKVktLcs= +github.com/icarus-sullivan/mock-lambda v0.0.0-20220114085425-44091545252e/go.mod h1:2iuLAENWZqxe/B6XUDWw/3ioQ9d1fwhgFTlwVeIBpzY= +github.com/icarus-sullivan/mock-lambda v0.0.0-20220115083805-e065469e964a h1:gmFO6gLHZkdJlkZ41QiQ5tzH8LORPVJCuKk6YKyquU0= +github.com/icarus-sullivan/mock-lambda v0.0.0-20220115083805-e065469e964a/go.mod h1:2iuLAENWZqxe/B6XUDWw/3ioQ9d1fwhgFTlwVeIBpzY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tests/integration/go/go1.x/go1.x.test.js b/tests/integration/go/go1.x/go1.x.test.js new file mode 100644 index 000000000..4874585c6 --- /dev/null +++ b/tests/integration/go/go1.x/go1.x.test.js @@ -0,0 +1,40 @@ +import { platform } from 'os' +import { resolve } from 'path' +import fetch from 'node-fetch' +import { joinUrl, setup, teardown } from '../../_testHelpers/index.js' + +jest.setTimeout(180000) + +const _describe = + process.env.GO1X_DETECTED && platform() !== 'win32' ? describe : describe.skip + +_describe('Go 1.x with GoRunner', () => { + // init + beforeAll(() => + setup({ + servicePath: resolve(__dirname), + }), + ) + + // cleanup + afterAll(() => teardown()) + + // + ;[ + { + description: 'should work with go1.x', + expected: { + message: 'Hello Go 1.x!', + }, + path: '/dev/hello', + }, + ].forEach(({ description, expected, path }) => { + test(description, async () => { + const url = joinUrl(TEST_BASE_URL, path) + const response = await fetch(url) + const json = await response.json() + + expect(json).toEqual(expected) + }) + }) +}) diff --git a/tests/integration/go/go1.x/hello/main.go b/tests/integration/go/go1.x/hello/main.go new file mode 100644 index 000000000..040a96f4b --- /dev/null +++ b/tests/integration/go/go1.x/hello/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "context" + + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambda" +) + +func Handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + return events.APIGatewayProxyResponse{ + Body: "{\"message\": \"Hello Go 1.x!\"}", + StatusCode: 200, + }, nil +} + +func main() { + lambda.Start(Handler) +} diff --git a/tests/integration/go/go1.x/serverless.yml b/tests/integration/go/go1.x/serverless.yml new file mode 100644 index 000000000..91bd6d38a --- /dev/null +++ b/tests/integration/go/go1.x/serverless.yml @@ -0,0 +1,20 @@ +service: docker-go-1.x-tests + +plugins: + - ../../../../ + +provider: + memorySize: 128 + name: aws + region: us-east-1 # default + runtime: go1.x + stage: dev + versionFunctions: false + +functions: + hello: + events: + - http: + method: get + path: hello + handler: hello/main.go