Skip to content

Make invocation URL dynamic and fix default function name #46

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

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ The rest of these Environment Variables can be set to match AWS Lambda's environ
* `AWS_LAMBDA_FUNCTION_NAME`
* `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`

By default `aws-lambda-rie` sets the value of the `AWS_LAMBDA_FUNCTION_NAME` environment variable to `test_function`, while the
function name in the endpoint URL is `function`. If you want the RIE to behave like AWS Lambda, where the function name in the
endpoint matches the value of the environment variable, set the value of the `AWS_LAMBDA_RIE_DYNAMIC_FUNCTION_URL`
environment variable to `"TRUE"`.

## Level of support

You can use the emulator to test if your function code is compatible with the Lambda environment, executes successfully
Expand Down
14 changes: 11 additions & 3 deletions cmd/aws-lambda-rie/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ func GetenvWithDefault(key string, defaultValue string) string {
return envValue
}

func GetFunctionName() string {
defaultValue := "function"
if GetenvWithDefault("AWS_LAMBDA_RIE_DYNAMIC_FUNCTION_URL", "FALSE") == "FALSE" {
defaultValue = "test_function"
}
return GetenvWithDefault("AWS_LAMBDA_FUNCTION_NAME", defaultValue)
}

func printEndReports(invokeId string, initDuration string, memorySize string, invokeStart time.Time, timeoutDuration time.Duration) {
// Calcuation invoke duration
invokeDuration := math.Min(float64(time.Now().Sub(invokeStart).Nanoseconds()),
Expand Down Expand Up @@ -118,7 +126,7 @@ func InvokeHandler(w http.ResponseWriter, r *http.Request, sandbox Sandbox, bs i
invokeStart := time.Now()
invokePayload := &interop.Invoke{
ID: uuid.New().String(),
InvokedFunctionArn: fmt.Sprintf("arn:aws:lambda:us-east-1:012345678912:function:%s", GetenvWithDefault("AWS_LAMBDA_FUNCTION_NAME", "test_function")),
InvokedFunctionArn: fmt.Sprintf("arn:aws:lambda:us-east-1:012345678912:function:%s", GetFunctionName()),
TraceID: r.Header.Get("X-Amzn-Trace-Id"),
LambdaSegmentID: r.Header.Get("X-Amzn-Segment-Id"),
Payload: bytes.NewReader(bodyBytes),
Expand Down Expand Up @@ -198,7 +206,7 @@ func InitHandler(sandbox Sandbox, functionVersion string, timeout int64, bs inte
additionalFunctionEnvironmentVariables["AWS_LAMBDA_LOG_STREAM_NAME"] = "$LATEST"
additionalFunctionEnvironmentVariables["AWS_LAMBDA_FUNCTION_VERSION"] = "$LATEST"
additionalFunctionEnvironmentVariables["AWS_LAMBDA_FUNCTION_MEMORY_SIZE"] = "3008"
additionalFunctionEnvironmentVariables["AWS_LAMBDA_FUNCTION_NAME"] = "test_function"
additionalFunctionEnvironmentVariables["AWS_LAMBDA_FUNCTION_NAME"] = GetFunctionName()

// Forward Env Vars from the running system (container) to what the function can view. Without this, Env Vars will
// not be viewable when the function runs.
Expand All @@ -216,7 +224,7 @@ func InitHandler(sandbox Sandbox, functionVersion string, timeout int64, bs inte
AwsSecret: os.Getenv("AWS_SECRET_ACCESS_KEY"),
AwsSession: os.Getenv("AWS_SESSION_TOKEN"),
XRayDaemonAddress: "0.0.0.0:0", // TODO
FunctionName: GetenvWithDefault("AWS_LAMBDA_FUNCTION_NAME", "test_function"),
FunctionName: GetFunctionName(),
FunctionVersion: functionVersion,
RuntimeInfo: interop.RuntimeInfo{
ImageJSON: "{}",
Expand Down
16 changes: 11 additions & 5 deletions cmd/aws-lambda-rie/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,21 @@ import (
"go.amzn.com/lambda/rapidcore"
)

func startHTTPServer(ipport string, sandbox *rapidcore.SandboxBuilder, bs interop.Bootstrap) {
func startHTTPServer(ipport string, sandbox *rapidcore.SandboxBuilder, bs interop.Bootstrap, funcName string) {
srv := &http.Server{
Addr: ipport,
}

// Pass a channel
http.HandleFunc("/2015-03-31/functions/function/invocations", func(w http.ResponseWriter, r *http.Request) {
InvokeHandler(w, r, sandbox.LambdaInvokeAPI(), bs)
})
var functions = []string{funcName}
if funcName != "function" {
functions = []string{"function", funcName}
}
for _, funcName := range functions {
// Pass a channel
http.HandleFunc("/2015-03-31/functions/"+funcName+"/invocations", func(w http.ResponseWriter, r *http.Request) {
InvokeHandler(w, r, sandbox.LambdaInvokeAPI(), bs)
})
}

// go routine (main thread waits)
if err := srv.ListenAndServe(); err != nil {
Expand Down
4 changes: 3 additions & 1 deletion cmd/aws-lambda-rie/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,15 @@ func main() {
sandbox.SetRuntimeAPIAddress(opts.RuntimeAPIAddress)
}

funcName := GetenvWithDefault("AWS_LAMBDA_FUNCTION_NAME", "function")

sandboxContext, internalStateFn := sandbox.Create()
// Since we have not specified a custom interop server for standalone, we can
// directly reference the default interop server, which is a concrete type
sandbox.DefaultInteropServer().SetSandboxContext(sandboxContext)
sandbox.DefaultInteropServer().SetInternalStateGetter(internalStateFn)

startHTTPServer(opts.RuntimeInterfaceEmulatorAddress, sandbox, bootstrap)
startHTTPServer(opts.RuntimeInterfaceEmulatorAddress, sandbox, bootstrap, funcName)
}

func getCLIArgs() (options, []string) {
Expand Down
26 changes: 23 additions & 3 deletions test/integration/local_lambda/test_end_to_end.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ def run_command(self, cmd):
def sleep_1s(self):
time.sleep(SLEEP_TIME)

def invoke_function(self, json={}, headers={}):
def invoke_function(self, json={}, headers={}, function_name="function"):
return requests.post(
f"http://localhost:{self.PORT}/2015-03-31/functions/function/invocations",
f"http://localhost:{self.PORT}/2015-03-31/functions/{function_name}/invocations",
json=json,
headers=headers,
)
Expand Down Expand Up @@ -257,5 +257,25 @@ def test_custom_client_context(self):
self.assertEqual(123, content["baz"])


def test_function_name_is_overriden_consistent(self):
image, rie, image_name = self.tagged_name("assert_overwritten_consistent")

params = f"--name {image} -d --env AWS_LAMBDA_FUNCTION_NAME=MyCoolName --env AWS_LAMBDA_RIE_DYNAMIC_FUNCTION_URL=TRUE -v {self.path_to_binary}:/local-lambda-runtime-server -p {self.PORT}:8080 --entrypoint /local-lambda-runtime-server/{rie} {image_name} {DEFAULT_1P_ENTRYPOINT} main.assert_env_var_is_overwritten"

with self.create_container(params, image):
r = self.invoke_function(function_name="MyCoolName")
self.assertEqual(b'"My lambda ran succesfully"', r.content)


def test_lambda_function_arn_exists(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

This one should be test_lambda_function_arn_exists_consistent

image, rie, image_name = self.tagged_name("arnexists_consistent")

params = f"--name {image} -d --env AWS_LAMBDA_FUNCTION_NAME=MyCoolName --env AWS_LAMBDA_RIE_DYNAMIC_FUNCTION_URL=TRUE -v {self.path_to_binary}:/local-lambda-runtime-server -p {self.PORT}:8080 --entrypoint /local-lambda-runtime-server/{rie} {image_name} {DEFAULT_1P_ENTRYPOINT} main.assert_lambda_arn_in_context"

with self.create_container(params, image):
r = self.invoke_function(function_name="MyCoolName")
self.assertEqual(b'"My lambda ran succesfully"', r.content)


if __name__ == "__main__":
main()
main()
4 changes: 2 additions & 2 deletions test/integration/testdata/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ def check_env_var_handler(event, context):

def assert_env_var_is_overwritten(event, context):
print(os.environ.get("AWS_LAMBDA_FUNCTION_NAME"))
if os.environ.get("AWS_LAMBDA_FUNCTION_NAME") == "test_function":
if os.environ.get("AWS_LAMBDA_FUNCTION_NAME") == "function":
raise("Function name was not overwritten")
else:
return "My lambda ran succesfully"

def assert_lambda_arn_in_context(event, context):
if context.invoked_function_arn == f"arn:aws:lambda:us-east-1:012345678912:function:{os.environ.get('AWS_LAMBDA_FUNCTION_NAME', 'test_function')}":
if context.invoked_function_arn == f"arn:aws:lambda:us-east-1:012345678912:function:{os.environ.get('AWS_LAMBDA_FUNCTION_NAME', 'function')}":
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I found an issue with our tests thanks to this change.
Because the integration tests are passing but here they should fail.

Your new test works correctly with this code, but there was already a previous test test_lambda_function_arn_exists() -> arnexists-x86_64 that doesn't pass the new env var, so this ARN should have test_function instead (the old value).

Looking at the output of the test run arnexists-x86_64 doesn't appear on the list of tests that were run, so it looks it might be ignoring the failing tests instead of fail overall.

I'll have to figure out this issue, but in the meantime you should change this to have different cases depending if the new env var was passed or not (probably just create a new handler here to be called form the new integ test).

The previous handler in this file also has the same issue, but because it's the branch that shouldn't happen, then it's not relevant and the test still succeed, but you should change that too.

Everything else looks good

Copy link
Contributor

Choose a reason for hiding this comment

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

Digging a little deeper, it looks like this shouldn't really be a problem, because when running this we will always have a value for AWS_LAMBDA_FUNCTION_NAME, so in this code it will never go to the second parameter of the os.environ.get(),

It's still weird that the previous test that uses this code doesn't show up on the output for the tests, so I'm still investigating what's going on there.

Did you make this change because the tests failed if you didn't? Or did you change it before actually running the tests? (My point is that: either this change is making the old test fail, or this change shouldn't be needed).

Copy link
Contributor

Choose a reason for hiding this comment

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

Of course it had to be a simple thing. The old test is not running because the new test you added has the same name! (def test_lambda_function_arn_exists) so it's actually overwriting the old test, but it succeeds.

What I would say is: don't change this file. This change doesn't actually affect the test because the env var is always set. We could probably just remove that second parameter in the test, but for now, it's better if you don't modify the file, so it doesn't get mixed up as a needed change.

return "My lambda ran succesfully"
else:
raise("Function Arn was not there")
Expand Down