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.
@@ -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