build_spec_runner 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|