build_spec_runner 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +98 -0
- data/Rakefile +15 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/build_spec_runner.gemspec +36 -0
- data/exe/build_spec_runner +7 -0
- data/lib/build_spec_runner.rb +6 -0
- data/lib/build_spec_runner/build_spec/build_spec.rb +148 -0
- data/lib/build_spec_runner/build_spec/buildspec_schema.yml +48 -0
- data/lib/build_spec_runner/cli.rb +181 -0
- data/lib/build_spec_runner/default_images.rb +69 -0
- data/lib/build_spec_runner/runner.rb +373 -0
- data/lib/build_spec_runner/source_provider.rb +26 -0
- data/lib/build_spec_runner/version.rb +3 -0
- metadata +207 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
type: map
|
2
|
+
mapping:
|
3
|
+
"version":
|
4
|
+
type: float
|
5
|
+
required: true
|
6
|
+
assert: val == 0.2
|
7
|
+
"env":
|
8
|
+
type: map
|
9
|
+
mapping:
|
10
|
+
"variables":
|
11
|
+
type: map
|
12
|
+
mapping:
|
13
|
+
=:
|
14
|
+
type: text
|
15
|
+
"parameter-store":
|
16
|
+
type: map
|
17
|
+
mapping:
|
18
|
+
=:
|
19
|
+
type: text
|
20
|
+
|
21
|
+
"phases":
|
22
|
+
type: map
|
23
|
+
required: true
|
24
|
+
mapping:
|
25
|
+
"install": &phase
|
26
|
+
type: map
|
27
|
+
mapping:
|
28
|
+
"commands":
|
29
|
+
type: seq
|
30
|
+
required: true
|
31
|
+
sequence:
|
32
|
+
- type: text
|
33
|
+
"pre_build": *phase
|
34
|
+
"build": *phase
|
35
|
+
"post_build": *phase
|
36
|
+
"artifacts":
|
37
|
+
type: map
|
38
|
+
mapping:
|
39
|
+
"files":
|
40
|
+
type: seq
|
41
|
+
required: true
|
42
|
+
sequence:
|
43
|
+
- type: text
|
44
|
+
required: true
|
45
|
+
"discard-paths":
|
46
|
+
type: bool
|
47
|
+
"base-directory":
|
48
|
+
type: text
|
@@ -0,0 +1,181 @@
|
|
1
|
+
require 'docker'
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
module BuildSpecRunner
|
5
|
+
class CLI
|
6
|
+
|
7
|
+
# Run the CLI object, according to the parsed options.
|
8
|
+
#
|
9
|
+
# @see CLI.optparse
|
10
|
+
|
11
|
+
def run
|
12
|
+
source_provider = get_source_provider
|
13
|
+
image = get_image
|
14
|
+
raise OptionParser::InvalidOption, "Cannot specify both :profile and :no_credentials" if @options[:profile] && @options[:no_credentials]
|
15
|
+
|
16
|
+
BuildSpecRunner::Runner.run image, source_provider, @options
|
17
|
+
end
|
18
|
+
|
19
|
+
# Create a CLI object, parsing the specified argv, or ARGV if none specified.
|
20
|
+
#
|
21
|
+
# @param argv [Array] array of arguments, defaults to ARGV
|
22
|
+
# @return [CLI] a CLI object for running the project in a manner determined by argv.
|
23
|
+
# @see CLI.optparse
|
24
|
+
|
25
|
+
def initialize argv = ARGV
|
26
|
+
@options = {}
|
27
|
+
CLI::optparse(@options).parse argv
|
28
|
+
end
|
29
|
+
|
30
|
+
# Create an OptParse for parsing CLI options
|
31
|
+
#
|
32
|
+
# The options are as follows:
|
33
|
+
# * \-h \-\-help --- Output help message
|
34
|
+
# * \-p \-\-path PATH --- Required argument, path the to the project to run
|
35
|
+
# * \-q \-\-quiet --- Silence debug messages.
|
36
|
+
# * \-\-build_spec_path BUILD_SPEC_PATH --- Alternative path for buildspec file, defaults to {Runner::DEFAULT_BUILD_SPEC_PATH}.
|
37
|
+
# * \-\-profile --- AWS profile of the credentials to provide the container, defaults to the default profile.
|
38
|
+
# This cannot be specified at the same time as \-\-no_credentials.
|
39
|
+
# * \-\-no_credentials --- Don't add AWS credentials to the project's container.
|
40
|
+
# This cannot be specified at the same time as \-\-profile.
|
41
|
+
# * \-\-image_id IMAGE_ID --- Id of alternative docker image to use. This cannot be specified at the same time as \-\-aws_dockerfile_path
|
42
|
+
# * \-\-aws_dockerfile_path AWS_DOCKERFILE_PATH --- Alternative AWS CodeBuild Dockerfile path, defaults to {DefaultImages::DEFAULT_DOCKERFILE_PATH}.
|
43
|
+
# This cannot be specified at the same time as \-\-image_id.
|
44
|
+
# See the {https://github.com/aws/aws-codebuild-docker-images AWS CodeBuild Docker Images repo} for the dockerfiles available through this option.
|
45
|
+
# * \-\-region REGION_NAME --- Name of the AWS region to provide to the container. Will set environment variables to make the container appear like
|
46
|
+
# it is in the specified AWS region. Otherwise it defaults to the default AWS region configured in the profile.
|
47
|
+
#
|
48
|
+
# @param options [Hash] the option hash to populate
|
49
|
+
# @return [OptionParser] the option parser that parses the described options.
|
50
|
+
|
51
|
+
def self.optparse options
|
52
|
+
OptionParser.new do |opts|
|
53
|
+
opts.banner = banner
|
54
|
+
self.add_opt_path opts, options
|
55
|
+
self.add_opt_build_spec_path opts, options
|
56
|
+
self.add_opt_quiet opts, options
|
57
|
+
self.add_opt_image_id opts, options
|
58
|
+
self.add_opt_aws_dockerfile_path opts, options
|
59
|
+
self.add_opt_profile opts, options
|
60
|
+
self.add_opt_no_credentials opts, options
|
61
|
+
self.add_opt_region opts, options
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Banner for the CLI usage.
|
66
|
+
|
67
|
+
def self.banner
|
68
|
+
%|Usage: #{File.basename(__FILE__)} arguments
|
69
|
+
|
70
|
+
Run a build spec locally.
|
71
|
+
|
72
|
+
Arguments:
|
73
|
+
|
|
74
|
+
end
|
75
|
+
|
76
|
+
# Create and execute a CLI with the default ARGV
|
77
|
+
|
78
|
+
def self.main
|
79
|
+
CLI::new.run
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
# Contains the options parsed from the OptParse
|
85
|
+
|
86
|
+
attr_reader :options
|
87
|
+
|
88
|
+
##### Adding Options #####
|
89
|
+
|
90
|
+
def self.add_opt_path opts, options
|
91
|
+
opts.on('-p', '--path PATH',
|
92
|
+
'[REQUIRED] Path to the project to run.') do |project_path|
|
93
|
+
options[:path] = project_path
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.add_opt_build_spec_path opts, options
|
98
|
+
opts.on('--build_spec_path BUILD_SPEC_PATH',
|
99
|
+
'Alternative path for buildspec file, defaults to #{Runner::DEFAULT_BUILD_SPEC_PATH}.') do |build_spec_path|
|
100
|
+
options[:build_spec_path] = build_spec_path
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.add_opt_quiet opts, options
|
105
|
+
opts.on('-q', '--quiet',
|
106
|
+
'Silence debug messages.') do
|
107
|
+
options[:quiet] = true
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.add_opt_image_id opts, options
|
112
|
+
opts.on('--image_id IMAGE_ID',
|
113
|
+
'Id of alternative docker image to use. NOTE: this cannot be specified at the same time as --aws_dockerfile_path') do |image_id|
|
114
|
+
options[:image_id] = image_id
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.add_opt_aws_dockerfile_path opts, options
|
119
|
+
opts.on('--aws_dockerfile_path AWS_DOCKERFILE_PATH',
|
120
|
+
'Alternative AWS CodeBuild DockerFile path, default is "ubuntu/ruby/2.3.1/". '\
|
121
|
+
'NOTE: this cannot be specified at the same time as --image_id . '\
|
122
|
+
'See: https://github.com/aws/aws-codebuild-docker-images') do |aws_dockerfile_path|
|
123
|
+
options[:aws_dockerfile_path] = aws_dockerfile_path
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.add_opt_profile opts, options
|
128
|
+
opts.on('--profile PROFILE',
|
129
|
+
'AWS profile of the credentials to provide the container, defaults to the default profile. '\
|
130
|
+
'This cannot be set at the same time as --no_credentials.') do |profile|
|
131
|
+
options[:profile] = profile
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.add_opt_no_credentials opts, options
|
136
|
+
opts.on('--no_credentials',
|
137
|
+
'Don\'t add AWS credentials to the project\'s container. '\
|
138
|
+
'This cannot be set at the same time as --profile.') do
|
139
|
+
options[:no_credentials] = true
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def self.add_opt_region opts, options
|
144
|
+
opts.on('--region REGION_NAME',
|
145
|
+
'Name of the AWS region to provide to the container. '\
|
146
|
+
'BuildSpecRunner will set environment variables to make the container appear like '\
|
147
|
+
'it is in the specified AWS region. Otherwise it defaults to the default AWS '\
|
148
|
+
'region configured in the profile.') do |region|
|
149
|
+
options[:region] = region
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
##### Parsing #####
|
154
|
+
|
155
|
+
# Create a source provider from the path option.
|
156
|
+
# The path option must be specified.
|
157
|
+
|
158
|
+
def get_source_provider
|
159
|
+
path = @options.delete :path
|
160
|
+
raise OptionParser::MissingArgument, 'Must specify a path (-p, --path PATH)' if path.nil?
|
161
|
+
BuildSpecRunner::SourceProvider::FolderSourceProvider.new path
|
162
|
+
end
|
163
|
+
|
164
|
+
# Choose the image based on the aws_dockerfile_path and image_id options.
|
165
|
+
# Up to one can be specified.
|
166
|
+
|
167
|
+
def get_image
|
168
|
+
image_id = @options.delete :image_id
|
169
|
+
aws_dockerfile_path = @options.delete :aws_dockerfile_path
|
170
|
+
if image_id && aws_dockerfile_path
|
171
|
+
raise OptionParser::InvalidOption, "Cannot specify both :image_id and :aws_dockerfile_path"
|
172
|
+
elsif image_id
|
173
|
+
Docker::Image.get(image_id)
|
174
|
+
elsif aws_dockerfile_path
|
175
|
+
BuildSpecRunner::DefaultImages.build_image :aws_dockerfile_path => aws_dockerfile_path
|
176
|
+
else
|
177
|
+
BuildSpecRunner::DefaultImages.build_image
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'git'
|
2
|
+
require 'docker'
|
3
|
+
|
4
|
+
module BuildSpecRunner
|
5
|
+
|
6
|
+
# Module for building the default AWS CodeBuild images. See {DefaultImages.build_image}
|
7
|
+
|
8
|
+
module DefaultImages
|
9
|
+
|
10
|
+
# The default directory used to clone the AWS CodeBuild Images repo
|
11
|
+
REPO_PATH = '/tmp/build_spec_runner/'
|
12
|
+
# The default CodeBuild Dockerfile
|
13
|
+
DEFAULT_DOCKERFILE_PATH = 'ubuntu/ruby/2.3.1/'
|
14
|
+
|
15
|
+
# Build an AWS CodeBuild Docker image.
|
16
|
+
#
|
17
|
+
# Defaults to the AWS CodeBuild Ruby 2.3.1 image.
|
18
|
+
# Different AWS CodeBuild images can be specified by setting :aws_dockerfile_path
|
19
|
+
# to a different setting, the default is {DEFAULT_DOCKERFILE_PATH}.
|
20
|
+
# This method clones the {https://github.com/aws/aws-codebuild-docker-images AWS CodeBuild Images repo}
|
21
|
+
# locally. The repo will be cloned to {REPO_PATH}, unless a different repo path is
|
22
|
+
# specified by setting :repo_path.
|
23
|
+
#
|
24
|
+
# @param opts [Hash] A hash containing optional values
|
25
|
+
# * *:dockerfile_path* (String) --- override chosen AWS CodeBuild dockerfile.
|
26
|
+
# * *:repo_path* (String) --- override path to clone AWS CodeBuild repo.
|
27
|
+
#
|
28
|
+
# @return [Docker::Image] A docker image with the specified AWS CodeBuild image.
|
29
|
+
#
|
30
|
+
# @see https://github.com/aws/aws-codebuild-docker-images AWS CodeBuild Images Repo
|
31
|
+
|
32
|
+
def self.build_image opts={}
|
33
|
+
|
34
|
+
dockerfile_path = opts[:aws_dockerfile_path]
|
35
|
+
dockerfile_path ||= DEFAULT_DOCKERFILE_PATH
|
36
|
+
repo_path = opts[:repo_path]
|
37
|
+
repo_path ||= REPO_PATH
|
38
|
+
|
39
|
+
repo = self.load_image_repo repo_path
|
40
|
+
docker_dir = File.join(repo.dir.path, dockerfile_path)
|
41
|
+
Docker::Image.build_from_dir(docker_dir)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
REPO_NAME = 'aws-codebuild-docker-images'
|
47
|
+
GIT_LOCATION = File.join("https://github.com/aws/", REPO_NAME)
|
48
|
+
|
49
|
+
# Load a repo that contains the aws codebuild docker images.
|
50
|
+
#
|
51
|
+
# Clone the repo if it hasn't yet been cloned, otherwise pull.
|
52
|
+
#
|
53
|
+
# @param repo_path [String] The path containing the repo.
|
54
|
+
#
|
55
|
+
# @return [Git::Base] A git repo
|
56
|
+
#
|
57
|
+
def self.load_image_repo repo_path
|
58
|
+
begin
|
59
|
+
# pull if it already exists
|
60
|
+
r = Git.open(File.join(repo_path, REPO_NAME))
|
61
|
+
r.pull
|
62
|
+
r
|
63
|
+
rescue ArgumentError # if it hasn't been cloned yet
|
64
|
+
Git.clone(GIT_LOCATION, REPO_NAME, :path => repo_path)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
@@ -0,0 +1,373 @@
|
|
1
|
+
require 'aws-sdk-core'
|
2
|
+
require 'aws-sdk-ssm'
|
3
|
+
require 'docker'
|
4
|
+
require 'pathname'
|
5
|
+
require 'shellwords'
|
6
|
+
|
7
|
+
module BuildSpecRunner
|
8
|
+
|
9
|
+
# Module for running projects on a local docker container.
|
10
|
+
#
|
11
|
+
# It is expected that you have a {BuildSpecRunner::SourceProvider} object that can yield a
|
12
|
+
# path containing a project suitable for AWS Codebuild. The project should have a buildspec
|
13
|
+
# file in its root directory. This module lets you use the default Ruby CodeBuild image or
|
14
|
+
# specify your own. See {Runner#run} and {Runner#run_default}.
|
15
|
+
#
|
16
|
+
# @see Runner#run_default run_default - an easy to use method for running projects on the
|
17
|
+
# default Ruby 2.3.1 image
|
18
|
+
# @see Runner#run run - a more configurable way of running projects locally
|
19
|
+
|
20
|
+
class Runner
|
21
|
+
|
22
|
+
# The default path of the buildspec file in a project.
|
23
|
+
|
24
|
+
DEFAULT_BUILD_SPEC_PATH = 'buildspec.yml'
|
25
|
+
|
26
|
+
# Run the project at the specified directory on the default AWS CodeBuild Ruby 2.3.1 image.
|
27
|
+
#
|
28
|
+
# @param path [String] The path to the project.
|
29
|
+
# @return [Integer] The exit code from running the project.
|
30
|
+
#
|
31
|
+
# @see run
|
32
|
+
# @see BuildSpecRunner::DefaultImages.build_image
|
33
|
+
|
34
|
+
def self.run_default path, opts={}
|
35
|
+
Runner.run(
|
36
|
+
BuildSpecRunner::DefaultImages.build_image,
|
37
|
+
BuildSpecRunner::SourceProvider::FolderSourceProvider.new(path),
|
38
|
+
opts
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Run a project on the specified image.
|
43
|
+
#
|
44
|
+
# Run a project on the specified image, with the source pointed to by
|
45
|
+
# the specified source provider. If the buildspec filename is not buildspec.yml or
|
46
|
+
# is not located in the project root, specify the option :build_spec_path to choose a different
|
47
|
+
# relative path (including filename).
|
48
|
+
#
|
49
|
+
# @param image [Docker::Image] A docker image to run the project on.
|
50
|
+
# @param source_provider [BuildSpecRunner::SourceProvider] A source provider that yields
|
51
|
+
# the source for the project.
|
52
|
+
# @param opts [Hash] A hash containing several optional values:
|
53
|
+
# for redirecting output.
|
54
|
+
# * *:outstream* (StringIO) --- for redirecting the project's stdout output
|
55
|
+
# * *:errstream* (StringIO) --- for redirecting the project's stderr output
|
56
|
+
# * *:build_spec_path* (String) --- Path of the buildspec file (including filename )
|
57
|
+
# relative to the project root. Defaults to {DEFAULT_BUILD_SPEC_PATH}.
|
58
|
+
# * *:quiet* (Boolean) --- suppress debug output
|
59
|
+
# * *:profile* (String) --- Profile to use for AWS clients
|
60
|
+
# * *:no_credentials* (Boolean) --- don't supply AWS credentials to the container
|
61
|
+
# * *:region* (String) --- AWS region to provide to the container.
|
62
|
+
#
|
63
|
+
# @return [Integer] The exit code from running the project.
|
64
|
+
def self.run image, source_provider, opts = {}
|
65
|
+
runner = Runner.new image, source_provider, opts
|
66
|
+
Runner.configure_docker
|
67
|
+
runner.execute
|
68
|
+
end
|
69
|
+
|
70
|
+
# Run the project
|
71
|
+
#
|
72
|
+
# Parse the build_spec, create the environment from the build_spec and any configured credentials,
|
73
|
+
# and build a container. Then execute the build spec's commands on the container.
|
74
|
+
#
|
75
|
+
# This method will close and remove any containers it creates.
|
76
|
+
#
|
77
|
+
def execute
|
78
|
+
build_spec = Runner.make_build_spec(@source_provider, @build_spec_path)
|
79
|
+
env = make_env(build_spec)
|
80
|
+
|
81
|
+
container = nil
|
82
|
+
begin
|
83
|
+
container = Runner.make_container(@image, @source_provider, env)
|
84
|
+
run_commands_on_container(container, build_spec)
|
85
|
+
ensure
|
86
|
+
unless container.nil?
|
87
|
+
container.stop
|
88
|
+
container.remove
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
# Create a Runner instance.
|
96
|
+
#
|
97
|
+
# @param opts [Hash] A hash containing several optional values,
|
98
|
+
# for redirecting output.
|
99
|
+
# * *:outstream* (StringIO) --- for redirecting the project's stdout output
|
100
|
+
# * *:errstream* (StringIO) --- for redirecting the project's stderr output
|
101
|
+
# * *:build_spec_path* (String) --- Path of the buildspec file (including filename )
|
102
|
+
# relative to the project root. Defaults to {DEFAULT_BUILD_SPEC_PATH}.
|
103
|
+
# * *:quiet* (Boolean) --- suppress debug output
|
104
|
+
# * *:profile* (String) --- profile to use for AWS clients
|
105
|
+
# * *:no_credentials* (Boolean) --- don't supply AWS credentials to the container
|
106
|
+
# * *:region* (String) --- name of the AWS region to provide to the container
|
107
|
+
|
108
|
+
def initialize image, source_provider, opts = {}
|
109
|
+
@image = image
|
110
|
+
@source_provider = source_provider
|
111
|
+
@outstream = opts[:outstream]
|
112
|
+
@errstream = opts[:errstream]
|
113
|
+
@build_spec_path = opts[:build_spec_path] || DEFAULT_BUILD_SPEC_PATH
|
114
|
+
@quiet = opts[:quiet] || false
|
115
|
+
@no_credentials = opts[:no_credentials]
|
116
|
+
@profile = opts[:profile]
|
117
|
+
@region = opts[:region]
|
118
|
+
|
119
|
+
raise ArgumentError, "Cannot specify both :no_credentials and :profile" if @profile && @no_credentials
|
120
|
+
end
|
121
|
+
|
122
|
+
DEFAULT_TIMEOUT_SECONDS = 2000
|
123
|
+
REMOTE_SOURCE_VOLUME_PATH_RO="/usr/app_ro/"
|
124
|
+
REMOTE_SOURCE_VOLUME_PATH="/usr/app/"
|
125
|
+
|
126
|
+
# Add region configuration environment variables to the env.
|
127
|
+
# Mutates the env passed to it
|
128
|
+
#
|
129
|
+
# @param env [Array<String>] An array of env variables in the format KEY=FOO, KEY2=BAR, ...
|
130
|
+
|
131
|
+
def add_region_variables env
|
132
|
+
region = @region
|
133
|
+
# This is an awful hack but I can't find the Ruby SDK way of getting the default region....
|
134
|
+
region ||= Aws::SSM::Client.new(profile: @profile).config.region
|
135
|
+
env << "AWS_DEFAULT_REGION=#{region}"
|
136
|
+
env << "AWS_REGION=#{region}"
|
137
|
+
end
|
138
|
+
|
139
|
+
# Make an array that contains environment variables according to the provided
|
140
|
+
# build_spec and sts client configuration.
|
141
|
+
#
|
142
|
+
# @param build_spec [BuildSpecRunner::BuildSpec::BuildSpec]
|
143
|
+
#
|
144
|
+
# @return [Array<String>] An array of env variables in the format KEY=FOO, KEY2=BAR, ...
|
145
|
+
|
146
|
+
def make_env build_spec
|
147
|
+
env = []
|
148
|
+
|
149
|
+
build_spec.env.keys.each { |k| env << "#{k}=#{build_spec.env[k]}" }
|
150
|
+
|
151
|
+
unless @no_credentials
|
152
|
+
sts_client = Aws::STS::Client.new profile: @profile
|
153
|
+
session_token = sts_client.get_session_token
|
154
|
+
credentials = session_token.credentials
|
155
|
+
|
156
|
+
env << "AWS_ACCESS_KEY_ID=#{credentials[:access_key_id]}"
|
157
|
+
env << "AWS_SECRET_ACCESS_KEY=#{credentials[:secret_access_key]}"
|
158
|
+
env << "AWS_SESSION_TOKEN=#{credentials[:session_token]}"
|
159
|
+
|
160
|
+
ssm = Aws::SSM::Client.new credentials: session_token
|
161
|
+
build_spec.parameter_store.keys.each do |k|
|
162
|
+
name = build_spec.parameter_store[k]
|
163
|
+
param_value = ssm.get_parameter(:name => name, :with_decryption => true).parameter.value
|
164
|
+
env << "#{k}=#{param_value}"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
add_region_variables env
|
169
|
+
|
170
|
+
env
|
171
|
+
end
|
172
|
+
|
173
|
+
# Configure docker with some useful defaults.
|
174
|
+
#
|
175
|
+
# Currently this just includes setting the docker read timeout to {DEFAULT_TIMEOUT_SECONDS} if
|
176
|
+
# there is no read timeout already specified. Override this by setting Docker.options[:read_timeout]
|
177
|
+
# to another value.
|
178
|
+
# @return [void]
|
179
|
+
|
180
|
+
def self.configure_docker
|
181
|
+
Docker.options[:read_timeout] = DEFAULT_TIMEOUT_SECONDS if Docker.options[:read_timeout].nil?
|
182
|
+
end
|
183
|
+
|
184
|
+
# Construct a buildspec from the given project provided by the source provider.
|
185
|
+
#
|
186
|
+
# The buildspec file should be located at the root of the source directory
|
187
|
+
# and named "buildspec.yml". An alternate path / filename can be specified by providing build_spec_name.
|
188
|
+
#
|
189
|
+
# @param source_provider [BuildSpecRunner::SourceProvider] A source provider that yields the path for
|
190
|
+
# the desired project.
|
191
|
+
# @param build_spec_path [String] The path and file name for the buildspec file in the project directory.
|
192
|
+
# examples: "buildspec.yml", "./foo/build_spec.yml", "bar/bs.yml", "../../weird/but/ok.yml", "/absolute/paths/too.yml"
|
193
|
+
#
|
194
|
+
# @return [BuildSpecRunner::BuildSpec::BuildSpec] A BuildSpec object representing the information contained
|
195
|
+
# by the specified buildspec.
|
196
|
+
#
|
197
|
+
# @see BuildSpecRunner::BuildSpec::BuildSpec
|
198
|
+
|
199
|
+
def self.make_build_spec(source_provider, build_spec_path="buildspec.yml")
|
200
|
+
if Pathname.new(build_spec_path).absolute?
|
201
|
+
BuildSpecRunner::BuildSpec::BuildSpec.new(build_spec_path)
|
202
|
+
else
|
203
|
+
BuildSpecRunner::BuildSpec::BuildSpec.new(File.join(source_provider.path, build_spec_path))
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# Make a docker container from the specified image for running the project.
|
208
|
+
#
|
209
|
+
# The container:
|
210
|
+
# * is created from the specified image.
|
211
|
+
# * is setup with the specified environment variables.
|
212
|
+
# * has a default command of "/bin/bash" with a tty configured, so that the image stays running when started.
|
213
|
+
# * has the project source provided by the source_provider mounted to a readonly directory
|
214
|
+
# at {REMOTE_SOURCE_VOLUME_PATH_RO}.
|
215
|
+
#
|
216
|
+
# @param image [Docker::Image] The docker image to be used to create the project.
|
217
|
+
# @param source_provider [BuildSpecRunner::SourceProvider] A source provider to provide the location
|
218
|
+
# of the project that will be mounted readonly to the image at the directory {REMOTE_SOURCE_VOLUME_PATH_RO}.
|
219
|
+
# @param env [Hash] the environment to pass along to the container. Should be an array with elements of the
|
220
|
+
# format KEY=VAL, FOO=BAR, etc. See the output of {#make_env}.
|
221
|
+
# @return [Docker::Container] a docker container from the specified image, with the specified settings applied.
|
222
|
+
# See method description.
|
223
|
+
|
224
|
+
def self.make_container(image, source_provider, env)
|
225
|
+
host_source_volume_path = source_provider.path
|
226
|
+
Docker::Container.create(
|
227
|
+
'Image' => image.id,
|
228
|
+
'Env' => env,
|
229
|
+
'Cmd' => '/bin/bash',
|
230
|
+
'Tty' => true,
|
231
|
+
'Volume' => {REMOTE_SOURCE_VOLUME_PATH_RO => {}}, 'Binds' => ["#{host_source_volume_path}:#{REMOTE_SOURCE_VOLUME_PATH_RO}:ro"],
|
232
|
+
)
|
233
|
+
end
|
234
|
+
|
235
|
+
# Bookkeeping bash variable for tracking whether a command has exited unsuccessfully
|
236
|
+
DO_NEXT = "_cbl_do_next_cmd_"
|
237
|
+
# Bookkeeping bash variable for tracking the build phase exit code
|
238
|
+
BUILD_EXIT_CODE = "_cbl_build_exit_code_"
|
239
|
+
# Bookkeeping bash variable for tracking most phases' exit codes
|
240
|
+
EXIT_CODE = "_cbl_exit_code_"
|
241
|
+
# Prepend this to container debug messages
|
242
|
+
DEBUG_HEADER = "[BuildSpecRunner Runner]"
|
243
|
+
|
244
|
+
# Make a conditional shell command
|
245
|
+
#
|
246
|
+
# @param test [String] The variable to test.
|
247
|
+
# @param zero [String] If test equals zero, run this command
|
248
|
+
# @param not_zero [String] If test does not equal zero, run this command
|
249
|
+
|
250
|
+
def make_if test, zero, not_zero
|
251
|
+
noop = ":"
|
252
|
+
"if [ \"0\" -eq \"$#{test}\" ]; then #{zero || noop}; else #{not_zero || noop} ; fi"
|
253
|
+
end
|
254
|
+
|
255
|
+
# Make a shell command that will run if DO_NEXT is 0 (i.e. no errors)
|
256
|
+
|
257
|
+
def maybe_command command
|
258
|
+
make_if DO_NEXT, command, nil
|
259
|
+
end
|
260
|
+
|
261
|
+
# Make a shell command to print a debug message to stderr
|
262
|
+
|
263
|
+
def debug_message message
|
264
|
+
if @quiet
|
265
|
+
# noop
|
266
|
+
":"
|
267
|
+
else
|
268
|
+
">&2 echo #{DEBUG_HEADER} #{message}"
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
# Make a shell script act as the build spec runner agent.
|
273
|
+
#
|
274
|
+
# This implements the running semantics build specs, including phase order, shell session, behavior, etc.
|
275
|
+
# Yes, this is very hacky. I'd love to find a better way that:
|
276
|
+
# * doesn't introduce dependencies on the host system
|
277
|
+
# * allows the build spec commands to run as if they were run consecutively in a single shell session
|
278
|
+
# Better features could include:
|
279
|
+
# * Remote control of agent on container
|
280
|
+
# * Separate streams for output, errors, and debug messages
|
281
|
+
#
|
282
|
+
# @param build_spec [BuildSpecRunner::BuildSpec::BuildSpec] A build spec object containing the commands to run
|
283
|
+
# @return [Array<String>] An array to execute an agent script that runs the project
|
284
|
+
|
285
|
+
def make_agent_script build_spec
|
286
|
+
commands = agent_setup_commands
|
287
|
+
|
288
|
+
BuildSpecRunner::BuildSpec::PHASES.each do |phase|
|
289
|
+
commands.push(*agent_phase_commands(build_spec, phase))
|
290
|
+
end
|
291
|
+
|
292
|
+
["bash", "-c", commands.join("\n")]
|
293
|
+
end
|
294
|
+
|
295
|
+
# Create the setup commands for the shell agent script
|
296
|
+
# The setup commands:
|
297
|
+
# * Copy project to a writable dir
|
298
|
+
# * Move to the dir
|
299
|
+
# * Set bookkeeping vars
|
300
|
+
#
|
301
|
+
# @return [Array<String>] The list of commands to setup the agent
|
302
|
+
|
303
|
+
def agent_setup_commands
|
304
|
+
[
|
305
|
+
"cp -r #{REMOTE_SOURCE_VOLUME_PATH_RO} #{REMOTE_SOURCE_VOLUME_PATH}",
|
306
|
+
"cd #{REMOTE_SOURCE_VOLUME_PATH}",
|
307
|
+
"#{DO_NEXT}=\"0\"",
|
308
|
+
"#{EXIT_CODE}=\"0\"",
|
309
|
+
"#{BUILD_EXIT_CODE}=\"0\"",
|
310
|
+
]
|
311
|
+
end
|
312
|
+
|
313
|
+
# Create shell agent commands for the given phase
|
314
|
+
#
|
315
|
+
# @param build_spec [BuildSpecRunner::BuildSpec::BuildSpec] the build spec object from which to read the commands
|
316
|
+
# @param phase [String] the phase to run
|
317
|
+
# @return [Array<String>] a list of commands to run for the given phase
|
318
|
+
|
319
|
+
def agent_phase_commands build_spec, phase
|
320
|
+
commands = []
|
321
|
+
commands << debug_message("Running phase \\\"#{phase}\\\"")
|
322
|
+
|
323
|
+
build_spec.phases[phase].each do |cmd|
|
324
|
+
# Run the given command, continue if the command exits successfully
|
325
|
+
commands << debug_message("Running command \\\"#{cmd.shellescape}\\\"")
|
326
|
+
commands << maybe_command("#{cmd} ; #{EXIT_CODE}=\"$?\"")
|
327
|
+
commands << maybe_command(
|
328
|
+
make_if(EXIT_CODE, nil, [
|
329
|
+
"#{DO_NEXT}=\"$#{EXIT_CODE}\"",
|
330
|
+
debug_message("Command failed \\\"#{cmd.shellescape}\\\""),
|
331
|
+
].join("\n"))
|
332
|
+
)
|
333
|
+
end
|
334
|
+
|
335
|
+
commands << make_if(
|
336
|
+
EXIT_CODE,
|
337
|
+
debug_message("Completed phase \\\"#{phase}\\\", successful: true"),
|
338
|
+
debug_message("Completed phase \\\"#{phase}\\\", successful: false"),
|
339
|
+
)
|
340
|
+
|
341
|
+
if phase == "build"
|
342
|
+
# If the build phase exits successfully, dont exit, continue onto post_build
|
343
|
+
commands << make_if(EXIT_CODE, nil, "#{BUILD_EXIT_CODE}=$#{EXIT_CODE};#{EXIT_CODE}=\"0\";#{DO_NEXT}=\"0\"")
|
344
|
+
elsif phase == "post_build"
|
345
|
+
# exit BUILD_EXIT_CODE || EXIT_CODE
|
346
|
+
commands << make_if(BUILD_EXIT_CODE, nil, "exit $#{BUILD_EXIT_CODE}")
|
347
|
+
commands << make_if(EXIT_CODE, nil, "exit $#{EXIT_CODE}")
|
348
|
+
else
|
349
|
+
commands << make_if(EXIT_CODE, nil, "exit $#{EXIT_CODE}")
|
350
|
+
end
|
351
|
+
|
352
|
+
commands
|
353
|
+
end
|
354
|
+
|
355
|
+
# Run the commands of the given buildspec on the given container.
|
356
|
+
#
|
357
|
+
# Runs the phases in the order specified by the documentation.
|
358
|
+
#
|
359
|
+
# @see http://docs.aws.amazon.com/codebuild/latest/userguide/view-build-details.html#view-build-details-phases
|
360
|
+
|
361
|
+
def run_commands_on_container(container, build_spec)
|
362
|
+
agent_script = make_agent_script build_spec
|
363
|
+
returned = container.tap(&:start).exec(agent_script, :wait => DEFAULT_TIMEOUT_SECONDS) do |stream, chunk|
|
364
|
+
if stream == :stdout
|
365
|
+
(@outstream || $stdout).print chunk
|
366
|
+
else
|
367
|
+
(@errstream || $stderr).print chunk
|
368
|
+
end
|
369
|
+
end
|
370
|
+
returned[2]
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|