Skip to content

Commit c15c6b0

Browse files
committed
Add the Go Modules builder
Add a builder for Go that assumes the given go project uses Go Modules, and so can be built using standard go tooling.
1 parent f53dc86 commit c15c6b0

File tree

21 files changed

+485
-2
lines changed

21 files changed

+485
-2
lines changed

aws_lambda_builders/actions.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,16 @@ class CopySourceAction(BaseAction):
9292

9393
PURPOSE = Purpose.COPY_SOURCE
9494

95-
def __init__(self, source_dir, dest_dir, excludes=None):
95+
def __init__(self, source_dir, dest_dir, only=None, excludes=None):
9696
self.source_dir = source_dir
9797
self.dest_dir = dest_dir
98+
self.only = only
9899
self.excludes = excludes or []
99100

100101
def execute(self):
101-
copytree(self.source_dir, self.dest_dir, ignore=shutil.ignore_patterns(*self.excludes))
102+
if self.only:
103+
def ignore(source, names):
104+
return [name for name in names if name not in self.only]
105+
else:
106+
ignore = shutil.ignore_patterns(*self.excludes)
107+
copytree(self.source_dir, self.dest_dir, ignore=ignore)

aws_lambda_builders/workflows/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
import aws_lambda_builders.workflows.nodejs_npm
77
import aws_lambda_builders.workflows.ruby_bundler
88
import aws_lambda_builders.workflows.go_dep
9+
import aws_lambda_builders.workflows.go_modules
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
## Go - Go Modules Lambda Builder
2+
3+
### Scope
4+
5+
This package leverages standard Go tooling available as of Go1.11 to build Go
6+
applications to be deployed in an AWS Lambda environment. The scope of this
7+
builder is to take an existing directory containing customer code, and a
8+
top-level `go.mod` file specifying third party dependencies. The builder will
9+
run `go build` on the project and put the resulting binary in the given
10+
artifacts folder.
11+
12+
### Interface
13+
14+
The top level interface is presented by the `GoModulesBuilder` class. There
15+
will be one public method `build`, which takes the provided arguments and
16+
builds a static binary using standard go tools.
17+
18+
```python
19+
def build(self, source_dir_path, artifacts_dir_path, ui=None, config=None):
20+
"""Builds a go project into an artifact directory.
21+
22+
:type source_dir_path: str
23+
:param source_dir_path: Directory with the source files.
24+
25+
:type artifacts_dir_path: str
26+
:param artifacts_dir_path: Directory to write dependencies into.
27+
28+
:type executable_name: str
29+
:param executable_name: Name of the executable to create from the build.
30+
31+
:type ui: :class:`lambda_builders.utils.UI` or None
32+
:param ui: A class that traps all progress information such as status
33+
and errors. If injected by the caller, it can be used to monitor
34+
the status of the build process or forward this information
35+
elsewhere.
36+
37+
:type config: :class:`lambda_builders.utils.Config` or None
38+
:param config: To be determined. This is an optional config object
39+
we can extend at a later date to add more options to how modules is
40+
called.
41+
"""
42+
```
43+
44+
### Implementation
45+
46+
The general algorithm for preparing a Go package for use on AWS Lambda
47+
is very simple. It's as follows:
48+
49+
Pass in GOOS=linux and GOARCH=amd64 to the `go build` command to target the
50+
OS and architecture used on AWS Lambda. Let go tooling handle the
51+
cross-compilation, regardless of the build environment. Move the resulting
52+
static binary to the artifacts folder to be shipped as a single-file zip
53+
archive.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""
2+
Builds Go Lambda functions using standard Go tooling
3+
"""
4+
5+
from .workflow import GoModulesWorkflow
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""
2+
Action to build a Go project using standard Go tooling
3+
"""
4+
5+
from aws_lambda_builders.actions import BaseAction, Purpose, ActionFailedError
6+
from .builder import BuilderError
7+
8+
9+
class GoModulesBuildAction(BaseAction):
10+
11+
NAME = "Build"
12+
DESCRIPTION = "Building Go package with Go Modules"
13+
PURPOSE = Purpose.RESOLVE_DEPENDENCIES
14+
15+
def __init__(self, source_dir, artifacts_dir, executable_name, builder):
16+
self.source_dir = source_dir
17+
self.artifacts_dir = artifacts_dir
18+
self.executable_name = executable_name
19+
self.builder = builder
20+
21+
def execute(self):
22+
try:
23+
self.builder.build(
24+
self.source_dir,
25+
self.artifacts_dir,
26+
self.executable_name
27+
)
28+
except BuilderError as ex:
29+
raise ActionFailedError(str(ex))
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""
2+
Build a Go project using standard Go tooling
3+
"""
4+
import logging
5+
6+
7+
LOG = logging.getLogger(__name__)
8+
9+
10+
class BuilderError(Exception):
11+
MESSAGE = "Builder Failed: {message}"
12+
13+
def __init__(self, **kwargs):
14+
Exception.__init__(self, self.MESSAGE.format(**kwargs))
15+
16+
17+
class GoModulesBuilder(object):
18+
def __init__(self, osutils):
19+
"""Initialize a GoModulesBuilder.
20+
21+
:type osutils: :class:`lambda_builders.utils.OSUtils`
22+
:param osutils: A class used for all interactions with the
23+
outside OS.
24+
"""
25+
self.osutils = osutils
26+
27+
def build(self, source_dir_path, artifacts_dir_path, executable_name):
28+
"""Builds a go project into an artifact directory.
29+
30+
:type source_dir_path: str
31+
:param source_dir_path: Directory with the source files.
32+
33+
:type artifacts_dir_path: str
34+
:param artifacts_dir_path: Directory to write dependencies into.
35+
36+
:type executable_name: str
37+
:param executable_name: Name of the executable to create from the build.
38+
39+
:type ui: :class:`lambda_builders.utils.UI` or None
40+
:param ui: A class that traps all progress information such as status
41+
and errors. If injected by the caller, it can be used to monitor
42+
the status of the build process or forward this information
43+
elsewhere.
44+
45+
:type config: :class:`lambda_builders.utils.Config` or None
46+
:param config: To be determined. This is an optional config object
47+
we can extend at a later date to add more options to how modules is
48+
called.
49+
"""
50+
env = {}
51+
env.update(self.osutils.environ)
52+
env.update({"GOOS": "linux", "GOARCH": "amd64"})
53+
cmd = ["go", "build", "-o", self.osutils.joinpath(artifacts_dir_path, executable_name), source_dir_path]
54+
55+
p = self.osutils.popen(
56+
cmd,
57+
cwd=source_dir_path,
58+
env=env,
59+
stdout=self.osutils.pipe,
60+
stderr=self.osutils.pipe,
61+
)
62+
out, err = p.communicate()
63+
64+
if p.returncode != 0:
65+
raise BuilderError(message=err.decode("utf8").strip())
66+
67+
return out.decode("utf8").strip()
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""
2+
Commonly used utilities
3+
"""
4+
5+
import os
6+
import subprocess
7+
8+
9+
class OSUtils(object):
10+
"""
11+
Wrapper around file system functions, to make it easy to
12+
unit test actions in memory
13+
"""
14+
@property
15+
def environ(self):
16+
return os.environ
17+
18+
def joinpath(self, *args):
19+
return os.path.join(*args)
20+
21+
def basename(self, *args):
22+
return os.path.basename(*args)
23+
24+
def popen(self, command, stdout=None, stderr=None, env=None, cwd=None):
25+
p = subprocess.Popen(command, stdout=stdout, stderr=stderr, env=env, cwd=cwd)
26+
return p
27+
28+
@property
29+
def pipe(self):
30+
return subprocess.PIPE
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""
2+
Go Modules Workflow
3+
"""
4+
from aws_lambda_builders.workflow import BaseWorkflow, Capability
5+
from aws_lambda_builders.actions import CopySourceAction
6+
7+
from .actions import GoModulesBuildAction
8+
from .builder import GoModulesBuilder
9+
from .utils import OSUtils
10+
11+
12+
class GoModulesWorkflow(BaseWorkflow):
13+
14+
NAME = "GoModulesBuilder"
15+
16+
CAPABILITY = Capability(language="go",
17+
dependency_manager="modules",
18+
application_framework=None)
19+
20+
def __init__(self,
21+
source_dir,
22+
artifacts_dir,
23+
scratch_dir,
24+
manifest_path,
25+
runtime=None,
26+
osutils=None,
27+
**kwargs):
28+
29+
super(GoModulesWorkflow, self).__init__(
30+
source_dir,
31+
artifacts_dir,
32+
scratch_dir,
33+
manifest_path,
34+
runtime=runtime,
35+
**kwargs)
36+
37+
if osutils is None:
38+
osutils = OSUtils()
39+
40+
executable_name = osutils.basename(source_dir)
41+
builder = GoModulesBuilder(osutils)
42+
self.actions = [
43+
GoModulesBuildAction(source_dir, artifacts_dir, executable_name, builder),
44+
CopySourceAction(source_dir, artifacts_dir, only=[executable_name]),
45+
]
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import os
2+
import sys
3+
4+
from unittest import TestCase
5+
6+
from aws_lambda_builders.workflows.go_modules import utils
7+
8+
9+
class TestOSUtils(TestCase):
10+
11+
def setUp(self):
12+
self.osutils = utils.OSUtils()
13+
14+
def test_environ_returns_environment(self):
15+
result = self.osutils.environ
16+
self.assertEqual(result, os.environ)
17+
18+
def test_joinpath_joins_path_components(self):
19+
result = self.osutils.joinpath('a', 'b', 'c')
20+
self.assertEqual(result, os.path.join('a', 'b', 'c'))
21+
22+
def test_basename_returns_path_basename(self):
23+
result = self.osutils.basename(os.path.dirname(__file__))
24+
self.assertEqual(result, 'go_modules')
25+
26+
def test_popen_runs_a_process_and_returns_outcome(self):
27+
cwd_py = os.path.join(os.path.dirname(__file__), '..', '..', 'testdata', 'cwd.py')
28+
p = self.osutils.popen([sys.executable, cwd_py],
29+
stdout=self.osutils.pipe,
30+
stderr=self.osutils.pipe)
31+
out, err = p.communicate()
32+
self.assertEqual(p.returncode, 0)
33+
self.assertEqual(out.decode('utf8').strip(), os.getcwd())
34+
35+
def test_popen_can_accept_cwd(self):
36+
testdata_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'testdata')
37+
p = self.osutils.popen([sys.executable, 'cwd.py'],
38+
stdout=self.osutils.pipe,
39+
stderr=self.osutils.pipe,
40+
cwd=testdata_dir)
41+
out, err = p.communicate()
42+
self.assertEqual(p.returncode, 0)
43+
self.assertEqual(out.decode('utf8').strip(), os.path.abspath(testdata_dir))
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import os
2+
import shutil
3+
import tempfile
4+
5+
from unittest import TestCase
6+
7+
from aws_lambda_builders.builder import LambdaBuilder
8+
from aws_lambda_builders.exceptions import WorkflowFailedError
9+
10+
11+
class TestGoWorkflow(TestCase):
12+
"""
13+
Verifies that `go` workflow works by building a Lambda using Go Modules
14+
"""
15+
16+
TEST_DATA_FOLDER = os.path.join(os.path.dirname(__file__), "testdata")
17+
18+
def setUp(self):
19+
self.artifacts_dir = tempfile.mkdtemp()
20+
self.scratch_dir = tempfile.mkdtemp()
21+
self.builder = LambdaBuilder(language="go",
22+
dependency_manager="modules",
23+
application_framework=None)
24+
25+
def tearDown(self):
26+
shutil.rmtree(self.artifacts_dir)
27+
shutil.rmtree(self.scratch_dir)
28+
29+
def test_builds_project_without_dependencies(self):
30+
source_dir = os.path.join(self.TEST_DATA_FOLDER, "no-deps")
31+
self.builder.build(source_dir, self.artifacts_dir, self.scratch_dir,
32+
os.path.join(source_dir, "go.mod"))
33+
expected_files = {"no-deps"}
34+
output_files = set(os.listdir(self.artifacts_dir))
35+
print(output_files)
36+
self.assertEquals(expected_files, output_files)
37+
38+
def test_builds_project_with_dependencies(self):
39+
source_dir = os.path.join(self.TEST_DATA_FOLDER, "with-deps")
40+
self.builder.build(source_dir, self.artifacts_dir, self.scratch_dir,
41+
os.path.join(source_dir, "go.mod"))
42+
expected_files = {"with-deps"}
43+
output_files = set(os.listdir(self.artifacts_dir))
44+
self.assertEquals(expected_files, output_files)
45+
46+
def test_fails_if_modules_cannot_resolve_dependencies(self):
47+
source_dir = os.path.join(self.TEST_DATA_FOLDER, "broken-deps")
48+
with self.assertRaises(WorkflowFailedError) as ctx:
49+
self.builder.build(source_dir, self.artifacts_dir, self.scratch_dir,
50+
os.path.join(source_dir, "go.mod"))
51+
self.assertIn("GoModulesBuilder:Build - Builder Failed: ",
52+
str(ctx.exception))
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module github.com/awslabs/aws-lambda-builders
2+
3+
require (
4+
github.com/BurntSushi/toml v0.3.1 // indirect
5+
github.com/aws/aws-lambda-go v0.9999.0 // doesn't exist, broken dependency
6+
github.com/davecgh/go-spew v1.1.1 // indirect
7+
github.com/pmezard/go-difflib v1.0.0 // indirect
8+
github.com/stretchr/objx v0.1.1 // indirect
9+
github.com/stretchr/testify v1.2.2 // indirect
10+
gopkg.in/urfave/cli.v1 v1.20.0 // indirect
11+
gopkg.in/yaml.v2 v2.2.2 // indirect
12+
)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package main
2+
3+
func main() {
4+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module github.com/awslabs/aws-lambda-builders
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package main
2+
3+
func main() {
4+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module github.com/awslabs/aws-lambda-builders
2+
3+
require (
4+
github.com/BurntSushi/toml v0.3.1 // indirect
5+
github.com/aws/aws-lambda-go v1.8.0
6+
github.com/davecgh/go-spew v1.1.1 // indirect
7+
github.com/pmezard/go-difflib v1.0.0 // indirect
8+
github.com/stretchr/objx v0.1.1 // indirect
9+
github.com/stretchr/testify v1.2.2 // indirect
10+
gopkg.in/urfave/cli.v1 v1.20.0 // indirect
11+
gopkg.in/yaml.v2 v2.2.2 // indirect
12+
)

0 commit comments

Comments
 (0)