build_spec_runner 0.4.0

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