Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

go-runner implementation #1320

Merged
merged 1 commit into from
Jan 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) <!-- and [Go](https://golang.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...
Expand Down
6 changes: 6 additions & 0 deletions src/lambda/handler-runner/HandlerRunner.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
supportedPython,
supportedRuby,
supportedJava,
supportedGo,
} from '../../config/index.js'
import { satisfiesVersionRange } from '../../utils/index.js'

Expand Down Expand Up @@ -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(
Expand Down
153 changes: 153 additions & 0 deletions src/lambda/handler-runner/go-runner/GoRunner.js
Original file line number Diff line number Diff line change
@@ -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'])
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constructors aren't created in an async manner, so using sync here only.

}

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"',
)
Comment on lines +87 to +90
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clone code and swap out lambda import, mock-lambda looks for the LAMBDA_EVENT and LAMBDA_CONTEXT and tries to process it.


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',
Comment on lines +135 to +138
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added initial support for custom authorizers

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)
}
}
1 change: 1 addition & 0 deletions src/lambda/handler-runner/go-runner/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './GoRunner.js'
15 changes: 15 additions & 0 deletions src/utils/checkGoVersion.js
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
11 changes: 10 additions & 1 deletion tests/_setupTeardown/npmInstall.js
Original file line number Diff line number Diff line change
@@ -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']

Expand Down Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions tests/integration/go/go1.x/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bin
9 changes: 9 additions & 0 deletions tests/integration/go/go1.x/go.mod
Original file line number Diff line number Diff line change
@@ -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
)
29 changes: 29 additions & 0 deletions tests/integration/go/go1.x/go.sum
Original file line number Diff line number Diff line change
@@ -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=
40 changes: 40 additions & 0 deletions tests/integration/go/go1.x/go1.x.test.js
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
19 changes: 19 additions & 0 deletions tests/integration/go/go1.x/hello/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
20 changes: 20 additions & 0 deletions tests/integration/go/go1.x/serverless.yml
Original file line number Diff line number Diff line change
@@ -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