Skip to content

Commit 77a50f2

Browse files
sthulbjfuss
authored andcommitted
feat(go): Golang Dep Builder (#54)
1 parent 7182b75 commit 77a50f2

File tree

25 files changed

+708
-1
lines changed

25 files changed

+708
-1
lines changed

.appveyor.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ version: 1.0.{build}
22
image: Visual Studio 2017
33

44
environment:
5+
GOPATH: c:\gopath
6+
GOVERSION: 1.11
57

68
matrix:
79

@@ -24,6 +26,15 @@ install:
2426
- "gem install bundler -v 1.17.3 --no-ri --no-rdoc"
2527
- "bundler --version"
2628

29+
# setup go
30+
- rmdir c:\go /s /q
31+
- "choco install golang"
32+
- "choco install bzr"
33+
- "choco install dep"
34+
- setx PATH "C:\go\bin;C:\gopath\bin;C:\Program Files (x86)\Bazaar\;C:\Program Files\Mercurial;%PATH%;"
35+
- "go version"
36+
- "go env"
37+
2738
test_script:
2839
- "%PYTHON%\\python.exe -m pytest --cov aws_lambda_builders --cov-report term-missing tests/unit tests/functional"
2940
- "%PYTHON%\\python.exe -m pytest tests/integration"

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,4 +383,6 @@ $RECYCLE.BIN/
383383

384384
/Dockerfile
385385

386-
# End of https://www.gitignore.io/api/osx,node,macos,linux,python,windows,pycharm,intellij,sublimetext,visualstudiocode
386+
tests/integration/workflows/go_dep/data/src/*/vendor/*
387+
388+
# End of https://www.gitignore.io/api/osx,node,macos,linux,python,windows,pycharm,intellij,sublimetext,visualstudiocode

.travis.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ install:
1616
- nvm install 8.10.0
1717
- nvm use 8.10.0
1818

19+
# Go workflow integ tests require Go 1.11+
20+
- eval "$(gimme 1.11.2)"
21+
- go version
22+
23+
- go get -u github.com/golang/dep/cmd/dep
24+
1925
# Install the code requirements
2026
- make init
2127
script:

aws_lambda_builders/workflows/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
import aws_lambda_builders.workflows.python_pip
66
import aws_lambda_builders.workflows.nodejs_npm
77
import aws_lambda_builders.workflows.ruby_bundler
8+
import aws_lambda_builders.workflows.go_dep
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 the `dep` dependency manager
3+
"""
4+
5+
from .workflow import GoDepWorkflow
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""
2+
Actions for Go dependency resolution with dep
3+
"""
4+
5+
import logging
6+
import os
7+
8+
from aws_lambda_builders.actions import BaseAction, Purpose, ActionFailedError
9+
10+
from .subproc_exec import ExecutionError
11+
12+
13+
LOG = logging.getLogger(__name__)
14+
15+
class DepEnsureAction(BaseAction):
16+
17+
"""
18+
A Lambda Builder Action which runs dep to install dependencies from Gopkg.toml
19+
"""
20+
21+
NAME = "DepEnsure"
22+
DESCRIPTION = "Ensures all dependencies are installed for a project"
23+
PURPOSE = Purpose.RESOLVE_DEPENDENCIES
24+
25+
def __init__(self, base_dir, subprocess_dep):
26+
super(DepEnsureAction, self).__init__()
27+
28+
self.base_dir = base_dir
29+
self.subprocess_dep = subprocess_dep
30+
31+
def execute(self):
32+
try:
33+
self.subprocess_dep.run(["ensure"],
34+
cwd=self.base_dir)
35+
except ExecutionError as ex:
36+
raise ActionFailedError(str(ex))
37+
38+
class GoBuildAction(BaseAction):
39+
40+
"""
41+
A Lambda Builder Action which runs `go build` to create a binary
42+
"""
43+
44+
NAME = "GoBuild"
45+
DESCRIPTION = "Builds final binary"
46+
PURPOSE = Purpose.COMPILE_SOURCE
47+
48+
def __init__(self, base_dir, source_path, output_path, subprocess_go, env=None):
49+
super(GoBuildAction, self).__init__()
50+
51+
self.base_dir = base_dir
52+
self.source_path = source_path
53+
self.output_path = output_path
54+
55+
self.subprocess_go = subprocess_go
56+
self.env = env if not env is None else {}
57+
58+
def execute(self):
59+
env = self.env
60+
env.update({"GOOS": "linux", "GOARCH": "amd64"})
61+
62+
try:
63+
self.subprocess_go.run(["build", "-o", self.output_path, self.source_path],
64+
cwd=self.source_path, env=env)
65+
except ExecutionError as ex:
66+
raise ActionFailedError(str(ex))
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""
2+
Wrapper around calling dep through a subprocess.
3+
"""
4+
5+
import logging
6+
7+
LOG = logging.getLogger(__name__)
8+
9+
10+
class ExecutionError(Exception):
11+
"""
12+
Exception raised in case binary execution fails.
13+
It will pass on the standard error output from the binary console.
14+
"""
15+
16+
MESSAGE = "Exec Failed: {}"
17+
18+
def __init__(self, message):
19+
raw_message = message
20+
if isinstance(message, bytes):
21+
message = message.decode('utf-8')
22+
23+
try:
24+
Exception.__init__(self, self.MESSAGE.format(message.strip()))
25+
except UnicodeError:
26+
Exception.__init__(self, self.MESSAGE.format(raw_message.strip()))
27+
28+
class SubprocessExec(object):
29+
30+
"""
31+
Wrapper around the Dep command line utility, making it
32+
easy to consume execution results.
33+
"""
34+
35+
def __init__(self, osutils, binary=None):
36+
"""
37+
:type osutils: aws_lambda_builders.workflows.go_dep.utils.OSUtils
38+
:param osutils: An instance of OS Utilities for file manipulation
39+
40+
:type binary: str
41+
:param binary: Path to the binary. If not set,
42+
the default executable path will be used
43+
"""
44+
self.osutils = osutils
45+
46+
self.binary = binary
47+
48+
49+
def run(self, args, cwd=None, env=None):
50+
51+
"""
52+
Runs the action.
53+
54+
:type args: list
55+
:param args: Command line arguments to pass to the binary
56+
57+
:type cwd: str
58+
:param cwd: Directory where to execute the command (defaults to current dir)
59+
60+
:rtype: str
61+
:return: text of the standard output from the command
62+
63+
:raises aws_lambda_builders.workflows.go_dep.dep.ExecutionError:
64+
when the command executes with a non-zero return code. The exception will
65+
contain the text of the standard error output from the command.
66+
67+
:raises ValueError: if arguments are not provided, or not a list
68+
"""
69+
70+
if not isinstance(args, list):
71+
raise ValueError("args must be a list")
72+
73+
if not args:
74+
raise ValueError("requires at least one arg")
75+
76+
invoke_bin = [self.binary] + args
77+
78+
LOG.debug("executing binary: %s", invoke_bin)
79+
80+
p = self.osutils.popen(invoke_bin,
81+
stdout=self.osutils.pipe,
82+
stderr=self.osutils.pipe,
83+
cwd=cwd,
84+
env=env)
85+
86+
out, err = p.communicate()
87+
88+
if p.returncode != 0:
89+
raise ExecutionError(message=err)
90+
91+
out = out.decode('utf-8') if isinstance(out, bytes) else out
92+
93+
return out.strip()
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""
2+
Commonly used utilities
3+
"""
4+
5+
import os
6+
import platform
7+
import tarfile
8+
import subprocess
9+
10+
11+
class OSUtils(object):
12+
13+
"""
14+
Wrapper around file system functions, to make it easy to
15+
unit test actions in memory
16+
17+
TODO: move to somewhere generic
18+
"""
19+
20+
def joinpath(self, *args):
21+
return os.path.join(*args)
22+
23+
def popen(self, command, stdout=None, stderr=None, env=None, cwd=None):
24+
p = subprocess.Popen(command, stdout=stdout, stderr=stderr, env=env, cwd=cwd)
25+
return p
26+
27+
@property
28+
def pipe(self):
29+
return subprocess.PIPE
30+
31+
@property
32+
def environ(self):
33+
return os.environ.copy()
34+
35+
def dirname(self, path):
36+
return os.path.dirname(path)
37+
38+
def abspath(self, path):
39+
return os.path.abspath(path)
40+
41+
def is_windows(self):
42+
return platform.system().lower() == 'windows'
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""
2+
Go Dep Workflow
3+
"""
4+
5+
import logging
6+
import os
7+
8+
from aws_lambda_builders.actions import CopySourceAction
9+
from aws_lambda_builders.workflow import BaseWorkflow, Capability
10+
11+
from .actions import DepEnsureAction, GoBuildAction
12+
from .utils import OSUtils
13+
from .subproc_exec import SubprocessExec
14+
15+
LOG = logging.getLogger(__name__)
16+
17+
class GoDepWorkflow(BaseWorkflow):
18+
"""
19+
A Lambda builder workflow that knows how to build
20+
Go projects using `dep`
21+
"""
22+
23+
NAME = "GoDepBuilder"
24+
25+
CAPABILITY = Capability(language="go",
26+
dependency_manager="dep",
27+
application_framework=None)
28+
29+
EXCLUDED_FILES = (".aws-sam")
30+
31+
def __init__(self,
32+
source_dir,
33+
artifacts_dir,
34+
scratch_dir,
35+
manifest_path,
36+
runtime=None,
37+
osutils=None,
38+
**kwargs):
39+
40+
super(GoDepWorkflow, self).__init__(source_dir,
41+
artifacts_dir,
42+
scratch_dir,
43+
manifest_path,
44+
runtime=runtime,
45+
**kwargs)
46+
47+
options = kwargs["options"] if "options" in kwargs else {}
48+
handler = options.get("handler", None)
49+
50+
if osutils is None:
51+
osutils = OSUtils()
52+
53+
# project base name, where the Gopkg.toml and vendor dir are.
54+
base_dir = osutils.abspath(osutils.dirname(manifest_path))
55+
output_path = osutils.joinpath(osutils.abspath(artifacts_dir), handler)
56+
57+
subprocess_dep = SubprocessExec(osutils, "dep")
58+
subprocess_go = SubprocessExec(osutils, "go")
59+
60+
self.actions = [
61+
DepEnsureAction(base_dir, subprocess_dep),
62+
GoBuildAction(base_dir, osutils.abspath(source_dir), output_path, subprocess_go, env=osutils.environ)
63+
]
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import os
2+
import shutil
3+
import sys
4+
import tempfile
5+
6+
from unittest import TestCase
7+
8+
from aws_lambda_builders.workflows.go_dep import utils
9+
10+
11+
class TestGoDepOSUtils(TestCase):
12+
13+
def setUp(self):
14+
15+
self.osutils = utils.OSUtils()
16+
17+
def test_dirname_returns_directory_for_path(self):
18+
dirname = self.osutils.dirname(sys.executable)
19+
20+
self.assertEqual(dirname, os.path.dirname(sys.executable))
21+
22+
def test_abspath_returns_absolute_path(self):
23+
24+
result = self.osutils.abspath('.')
25+
26+
self.assertTrue(os.path.isabs(result))
27+
28+
self.assertEqual(result, os.path.abspath('.'))
29+
30+
def test_joinpath_joins_path_components(self):
31+
32+
result = self.osutils.joinpath('a', 'b', 'c')
33+
34+
self.assertEqual(result, os.path.join('a', 'b', 'c'))
35+
36+
def test_popen_runs_a_process_and_returns_outcome(self):
37+
38+
cwd_py = os.path.join(os.path.dirname(__file__), '..', '..', 'testdata', 'cwd.py')
39+
40+
p = self.osutils.popen([sys.executable, cwd_py],
41+
stdout=self.osutils.pipe,
42+
stderr=self.osutils.pipe)
43+
44+
out, err = p.communicate()
45+
46+
self.assertEqual(p.returncode, 0)
47+
48+
self.assertEqual(out.decode('utf8').strip(), os.getcwd())
49+
50+
def test_popen_can_accept_cwd(self):
51+
52+
testdata_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'testdata')
53+
54+
p = self.osutils.popen([sys.executable, 'cwd.py'],
55+
stdout=self.osutils.pipe,
56+
stderr=self.osutils.pipe,
57+
cwd=testdata_dir)
58+
59+
out, err = p.communicate()
60+
61+
self.assertEqual(p.returncode, 0)
62+
63+
self.assertEqual(out.decode('utf8').strip(), os.path.abspath(testdata_dir))

0 commit comments

Comments
 (0)