Skip to content

Commit

Permalink
feat(debugging): Fixing issues around debugging Golang functions. (aw…
Browse files Browse the repository at this point in the history
  • Loading branch information
jfuss authored and sanathkr committed Jul 25, 2018
1 parent 81d9ac7 commit 0e0f815
Show file tree
Hide file tree
Showing 26 changed files with 652 additions and 188 deletions.
50 changes: 50 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,56 @@ port to your host machine.
issue <https://github.com/Microsoft/vscode-python/issues/71>`__ for
updates.

Debugging Golang functions
^^^^^^^^^^^^^^^^^^^^^^^^^^

Golang function debugging is slightly different when compared to Node.JS,
Java, and Python. We require `delve <https://github.com/derekparker/delve>`__
as the debugger, and wrap your function with it at runtime. The debugger
is run in headless mode, listening on the debug port.

When debugging, you must compile your function in debug mode:

`GOARCH=amd64 GOOS=linux go build -gcflags='-N -l' -o <output path> <path to code directory>

You must compile `delve` to run in the container and provide its local path
via the `--debugger-path` argument. Build delve locally as follows:

`GOARCH=amd64 GOOS=linux go build -o <delve folder path>/dlv github.com/derekparker/delve/cmd/dlv`

NOTE: The output path needs to end in `/dlv`. The docker container will expect the dlv binary to be in the <delve folder path>
and will cause mounting issue otherwise.

Then invoke `sam` similar to the following:

`sam local start-api -d 5986 --debugger-path <delve folder path>`

NOTE: The `--debugger-path` is the path to the directory that contains the `dlv` binary compiled from the above.

The following is an example launch configuration for Visual Studio Code to
attach to a debug session.

.. code:: json
{
"version": "0.2.0",
"configurations": [
{
"name": "Connect to Lambda container",
"type": "go",
"request": "launch",
"mode": "remote",
"remotePath": "",
"port": <debug port>,
"host": "127.0.0.1",
"program": "${workspaceRoot}",
"env": {},
"args": [],
},
]
}
Passing Additional Runtime Debug Arguments
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
1 change: 1 addition & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ aws-sam-translator==1.6.0
docker>=3.3.0
dateparser~=0.7
python-dateutil~=2.6
pathlib2~=2.3.2; python_version<"3.4"
2 changes: 1 addition & 1 deletion requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ pylint==1.7.2
pytest==3.0.7
py==1.4.33
mock==2.0.0
requests>=2.19.0
requests==2.19.0
parameterized==0.6.1
pathlib2==2.3.2; python_version<"3.4"
futures==3.2.0; python_version<"3.2.3"
Expand Down
112 changes: 91 additions & 21 deletions samcli/commands/local/cli_common/invoke_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,31 @@
Reads CLI arguments and performs necessary preparation to be able to run the function
"""

import os
import sys
import errno
import json
import sys
import os
import yaml
import docker

import docker
import requests

from samcli.yamlhelper import yaml_parse
from samcli.commands.local.lib.local_lambda import LocalLambdaRunner
from samcli.commands.local.lib.debug_context import DebugContext
from samcli.local.lambdafn.runtime import LambdaRuntime
from samcli.local.docker.manager import ContainerManager
from .user_exceptions import InvokeContextException
from .user_exceptions import InvokeContextException, DebugContextException
from ..lib.sam_function_provider import SamFunctionProvider

# This is an attempt to do a controlled import. pathlib is in the
# Python standard library starting at 3.4. This will import pathlib2,
# which is a backport of the Python Standard Library pathlib
try:
from pathlib import Path
except ImportError:
from pathlib2 import Path


class InvokeContext(object):
"""
Expand All @@ -38,43 +48,60 @@ def __init__(self,
template_file,
function_identifier=None,
env_vars_file=None,
debug_port=None,
debug_args=None,
docker_volume_basedir=None,
docker_network=None,
log_file=None,
skip_pull_image=None,
aws_profile=None):
aws_profile=None,
debug_port=None,
debug_args=None,
debugger_path=None):
"""
Initialize the context
:param string template_file: Name or path to template
:param string function_identifier: Identifier of the function to invoke
:param string env_vars_file: Path to a file containing values for environment variables
:param int debug_port: Port to bind the debugger
:param string docker_volume_basedir: Directory for the Docker volume
:param string docker_network: Docker network identifier
:param string log_file: Path to a file to send container output to. If the file does not exist, it will be
Parameters
----------
template_file str
Name or path to template
function_identifier str
Identifier of the function to invoke
env_vars_file str
Path to a file containing values for environment variables
docker_volume_basedir str
Directory for the Docker volume
docker_network str
Docker network identifier
log_file str
Path to a file to send container output to. If the file does not exist, it will be
created
:param bool skip_pull_image: Should we skip pulling the Docker container image?
:param string aws_profile: Name of the profile to fetch AWS credentials from
skip_pull_image bool
Should we skip pulling the Docker container image?
aws_profile str
Name of the profile to fetch AWS credentials from
debug_port int
Port to bind the debugger to
debug_args str
Additional arguments passed to the debugger
debugger_path str
Path to the directory of the debugger to mount on Docker
"""

self._template_file = template_file
self._function_identifier = function_identifier
self._env_vars_file = env_vars_file
self._debug_port = debug_port
self._debug_args = debug_args
self._docker_volume_basedir = docker_volume_basedir
self._docker_network = docker_network
self._log_file = log_file
self._skip_pull_image = skip_pull_image
self._aws_profile = aws_profile
self._debug_port = debug_port
self._debug_args = debug_args
self._debugger_path = debugger_path

self._template_dict = None
self._function_provider = None
self._env_vars_value = None
self._log_file_handle = None
self._debug_context = None

def __enter__(self):
"""
Expand All @@ -90,6 +117,10 @@ def __enter__(self):
self._env_vars_value = self._get_env_vars_value(self._env_vars_file)
self._log_file_handle = self._setup_log_file(self._log_file)

self._debug_context = self._get_debug_context(self._debug_port,
self._debug_args,
self._debugger_path)

self._check_docker_connectivity()

return self
Expand Down Expand Up @@ -146,8 +177,7 @@ def local_lambda_runner(self):
function_provider=self._function_provider,
cwd=self.get_cwd(),
env_vars_values=self._env_vars_value,
debug_port=self._debug_port,
debug_args=self._debug_args,
debug_context=self._debug_context,
aws_profile=self._aws_profile)

@property
Expand Down Expand Up @@ -271,6 +301,46 @@ def _setup_log_file(log_file):

return open(log_file, 'wb')

@staticmethod
def _get_debug_context(debug_port, debug_args, debugger_path):
"""
Creates a DebugContext if the InvokeContext is in a debugging mode
Parameters
----------
debug_port int
Port to bind the debugger to
debug_args str
Additional arguments passed to the debugger
debugger_path str
Path to the directory of the debugger to mount on Docker
Returns
-------
samcli.commands.local.lib.debug_context.DebugContext
Object representing the DebugContext
Raises
------
samcli.commands.local.cli_common.user_exceptions.DebugContext
When the debugger_path is not valid
"""
if debug_port and debugger_path:
try:
debugger = Path(debugger_path).resolve(strict=True)
except OSError as error:
if error.errno == errno.ENOENT:
raise DebugContextException("'{}' could not be found.".format(debugger_path))
else:
raise error

# We turn off pylint here due to https://github.com/PyCQA/pylint/issues/1660
if not debugger.is_dir(): # pylint: disable=no-member
raise DebugContextException("'{}' should be a directory with the debugger in it.".format(debugger_path))
debugger_path = str(debugger)

return DebugContext(debug_port=debug_port, debug_args=debug_args, debugger_path=debugger_path)

@staticmethod
def _check_docker_connectivity(docker_client=None):
"""
Expand Down
3 changes: 3 additions & 0 deletions samcli/commands/local/cli_common/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ def invoke_common_options(f):
"port on localhost.",
envvar="SAM_DEBUG_PORT"),

click.option('--debugger-path',
help="Host path to a debugger that will be mounted into the Lambda container."),

click.option('--debug-args',
help="Additional arguments to be passed to the debugger.",
envvar="DEBUGGER_ARGS"),
Expand Down
7 changes: 7 additions & 0 deletions samcli/commands/local/cli_common/user_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,10 @@ class SamTemplateNotFoundException(UserException):
The SAM Template provided could not be found
"""
pass


class DebugContextException(UserException):
"""
Something went wrong when creating the DebugContext
"""
pass
19 changes: 10 additions & 9 deletions samcli/commands/local/invoke/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,17 @@
@cli_framework_options
@click.argument('function_identifier', required=False)
@pass_context
def cli(ctx, function_identifier, template, event, env_vars, debug_port, debug_args, docker_volume_basedir,
docker_network, log_file, skip_pull_image, profile):
def cli(ctx, function_identifier, template, event, env_vars, debug_port, debug_args, debugger_path,
docker_volume_basedir, docker_network, log_file, skip_pull_image, profile):

# All logic must be implemented in the ``do_cli`` method. This helps with easy unit testing

do_cli(ctx, function_identifier, template, event, env_vars, debug_port, debug_args, docker_volume_basedir,
docker_network, log_file, skip_pull_image, profile) # pragma: no cover
do_cli(ctx, function_identifier, template, event, env_vars, debug_port, debug_args, debugger_path,
docker_volume_basedir, docker_network, log_file, skip_pull_image, profile) # pragma: no cover


def do_cli(ctx, function_identifier, template, event, env_vars, debug_port, debug_args, docker_volume_basedir,
docker_network, log_file, skip_pull_image, profile):
def do_cli(ctx, function_identifier, template, event, env_vars, debug_port, debug_args, # pylint: disable=R0914
debugger_path, docker_volume_basedir, docker_network, log_file, skip_pull_image, profile):
"""
Implementation of the ``cli`` method, just separated out for unit testing purposes
"""
Expand All @@ -64,13 +64,14 @@ def do_cli(ctx, function_identifier, template, event, env_vars, debug_port, debu
with InvokeContext(template_file=template,
function_identifier=function_identifier,
env_vars_file=env_vars,
debug_port=debug_port,
debug_args=debug_args,
docker_volume_basedir=docker_volume_basedir,
docker_network=docker_network,
log_file=log_file,
skip_pull_image=skip_pull_image,
aws_profile=profile) as context:
aws_profile=profile,
debug_port=debug_port,
debug_args=debug_args,
debugger_path=debugger_path) as context:

# Invoke the function
context.local_lambda_runner.invoke(context.function_name,
Expand Down
21 changes: 21 additions & 0 deletions samcli/commands/local/lib/debug_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""
Information and debug options for a specific runtime.
"""


class DebugContext(object):

def __init__(self,
debug_port=None,
debugger_path=None,
debug_args=None):

self.debug_port = debug_port
self.debugger_path = debugger_path
self.debug_args = debug_args

def __bool__(self):
return bool(self.debug_port)

def __nonzero__(self):
return self.__bool__()
Loading

0 comments on commit 0e0f815

Please sign in to comment.