Skip to content

Commit

Permalink
Initial go-runner implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
icarus-sullivan committed Jan 19, 2022
1 parent 8d61bde commit 0cb9256
Show file tree
Hide file tree
Showing 13 changed files with 344 additions and 2 deletions.
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
194 changes: 194 additions & 0 deletions src/lambda/handler-runner/go-runner/GoRunner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { EOL } from 'os'
import { promises as fsPromises } from 'fs'
import { sep, resolve, parse as pathParse } from 'path'
import execa, { sync } from 'execa'
import STS from 'aws-sdk/clients/sts'

const { writeFile, readFile, mkdir, rmdir } = fsPromises
const { parse, stringify } = JSON
const { cwd } = process

const MIN_ALLOWED_SESSION_DURATION_S = 900
const SESSION_TIMEOUT_MS = 850 * 1000
const PAYLOAD_IDENTIFIER = 'offline_payload'

export default class GoRunner {
#env = null
#handlerPath = null
#tmpPath = null
#tmpFile = null
#profile = null
#handle = null
#credentials = null
#goEnv = null

constructor(funOptions, env, v3Utils) {
const { handlerPath, provider } = funOptions

this.#env = env
this.#handlerPath = handlerPath
this.#profile = provider.profile || 'default'

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() {
// Clean up session timeout
if (this.#handle !== null) {
clearTimeout(this.#handle)
}

await this.cleanArtifacts()
}

_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 cleanArtifacts() {
try {
await rmdir(this.#tmpPath, { recursive: true })
} catch (e) {
// @ignore
}

this.#tmpFile = null
this.#tmpPath = null
}

clearCredentials() {
this.#credentials = null
this.#handle = null
}

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
}, {})
}

// Get session credentials to pass to go
if (!this.#credentials) {
const sts = new STS()

const { Credentials } = await sts
.getSessionToken({
DurationSeconds: MIN_ALLOWED_SESSION_DURATION_S, // 900-inf
})
.promise()

this.#credentials = Credentials
this.#handle = setTimeout(
this.clearCredentials.bind(this),
SESSION_TIMEOUT_MS,
)
}

// Remove our root, since we want to invoke go relatively
const cwdPath = `${this.#tmpFile}`.replace(`${cwd()}${sep}`, '')
const cp = await execa(`go`, ['run', cwdPath], {
stdio: 'pipe',
env: {
...this.#env,
...this.#goEnv,
AWS_ACCESS_KEY_ID: this.#credentials.AccessKeyId,
AWS_SECRET_ACCESS_KEY: this.#credentials.SecretAccessKey,
AWS_SESSION_TOKEN: this.#credentials.SessionToken,
AWS_PROFILE: this.#profile,
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_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.cleanArtifacts()

if (cp.stderr) {
throw new Error(cp.stderr)
}

return this._parsePayload(cp.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-20220114085425-44091545252e // indirect
github.com/urfave/cli v1.22.1 // indirect
)
27 changes: 27 additions & 0 deletions tests/integration/go/go1.x/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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/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 without docker',
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

0 comments on commit 0cb9256

Please sign in to comment.