drunker 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -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 +281 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/drunker.gemspec +33 -0
- data/exe/drunker +6 -0
- data/lib/drunker.rb +20 -0
- data/lib/drunker/aggregator.rb +9 -0
- data/lib/drunker/aggregator/base.rb +13 -0
- data/lib/drunker/artifact.rb +73 -0
- data/lib/drunker/artifact/layer.rb +30 -0
- data/lib/drunker/cli.rb +74 -0
- data/lib/drunker/config.rb +160 -0
- data/lib/drunker/executor.rb +122 -0
- data/lib/drunker/executor/builder.rb +124 -0
- data/lib/drunker/executor/buildspec.yml.erb +11 -0
- data/lib/drunker/executor/iam.rb +92 -0
- data/lib/drunker/source.rb +72 -0
- data/lib/drunker/version.rb +3 -0
- metadata +182 -0
data/drunker.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'drunker/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "drunker"
|
8
|
+
spec.version = Drunker::VERSION
|
9
|
+
spec.authors = ["Kazuma Watanabe"]
|
10
|
+
spec.email = ["watassbass@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Distributed CLI runner on AWS CodeBuild}
|
13
|
+
spec.description = %q{Distributed CLI runner on AWS CodeBuild}
|
14
|
+
spec.homepage = "https://github.com/wata727/drunker"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
spec.bindir = "exe"
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.14"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
27
|
+
spec.add_development_dependency "timecop", "~> 0.8"
|
28
|
+
|
29
|
+
spec.add_runtime_dependency "thor", "~> 0.19"
|
30
|
+
spec.add_runtime_dependency "rubyzip", "~> 1.2"
|
31
|
+
spec.add_runtime_dependency "aws-sdk", "~> 2"
|
32
|
+
spec.add_runtime_dependency "drunker-aggregator-pretty", "~> 0.1"
|
33
|
+
end
|
data/exe/drunker
ADDED
data/lib/drunker.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require "thor"
|
2
|
+
require "zip"
|
3
|
+
require "pathname"
|
4
|
+
require "aws-sdk"
|
5
|
+
require "json"
|
6
|
+
require "logger"
|
7
|
+
require "erb"
|
8
|
+
require "yaml"
|
9
|
+
|
10
|
+
require "drunker/version"
|
11
|
+
require "drunker/cli"
|
12
|
+
require "drunker/source"
|
13
|
+
require "drunker/artifact"
|
14
|
+
require "drunker/artifact/layer"
|
15
|
+
require "drunker/executor"
|
16
|
+
require "drunker/executor/iam"
|
17
|
+
require "drunker/executor/builder"
|
18
|
+
require "drunker/config"
|
19
|
+
require "drunker/aggregator"
|
20
|
+
require "drunker/aggregator/base"
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Drunker
|
2
|
+
class Aggregator
|
3
|
+
class Base
|
4
|
+
def run(layers)
|
5
|
+
raise NotImplementedError.new("You must implement #{self.class}##{__method__}")
|
6
|
+
end
|
7
|
+
|
8
|
+
def exit_status(layers)
|
9
|
+
raise NotImplementedError.new("You must implement #{self.class}##{__method__}")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Drunker
|
2
|
+
class Artifact
|
3
|
+
attr_reader :bucket
|
4
|
+
attr_reader :stdout
|
5
|
+
attr_reader :stderr
|
6
|
+
attr_reader :exit_status
|
7
|
+
|
8
|
+
def initialize(config:, logger:)
|
9
|
+
timestamp = Time.now.to_i.to_s
|
10
|
+
s3 = Aws::S3::Resource.new(client: Aws::S3::Client.new(config.aws_client_options))
|
11
|
+
|
12
|
+
@bucket = s3.create_bucket(bucket: "drunker-artifact-store-#{timestamp}")
|
13
|
+
logger.info("Created artifact bucket: #{bucket.name}")
|
14
|
+
@name = "drunker_artifact_#{timestamp}"
|
15
|
+
@stdout = "drunker_artifact_#{timestamp}_stdout.txt"
|
16
|
+
@stderr = "drunker_artifact_#{timestamp}_stderr.txt"
|
17
|
+
@exit_status = "drunker_artifact_#{timestamp}_exit_status.txt"
|
18
|
+
@builds = []
|
19
|
+
@logger = logger
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_h
|
23
|
+
{
|
24
|
+
type: "S3",
|
25
|
+
location: bucket.name,
|
26
|
+
namespace_type: "BUILD_ID",
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def layers
|
31
|
+
@layers ||= builds.each_with_object([]) do |build, layers|
|
32
|
+
project_name, build_id = build.split(":")
|
33
|
+
layers << Layer.new(build_id: build).tap do |layer|
|
34
|
+
begin
|
35
|
+
layer.stdout = fetch_content("#{build_id}/#{project_name}/#{stdout}")
|
36
|
+
layer.stderr = fetch_content("#{build_id}/#{project_name}/#{stderr}")
|
37
|
+
layer.exit_status = fetch_content("#{build_id}/#{project_name}/#{exit_status}")
|
38
|
+
rescue Aws::S3::Errors::NoSuchKey
|
39
|
+
logger.debug("Artifact not found")
|
40
|
+
layer.invalid!
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def set_build(build)
|
47
|
+
@builds << build
|
48
|
+
logger.debug("Set build: { build: #{build}, artifact: #{name} }")
|
49
|
+
end
|
50
|
+
|
51
|
+
def replace_build(before:, after:)
|
52
|
+
builds.delete(before)
|
53
|
+
logger.debug("Unset build: { build: #{before}, artifact: #{name} }")
|
54
|
+
set_build(after)
|
55
|
+
end
|
56
|
+
|
57
|
+
def delete
|
58
|
+
bucket.delete!
|
59
|
+
logger.info("Deleted bucket: #{bucket.name}")
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
attr_reader :builds
|
65
|
+
attr_reader :name
|
66
|
+
attr_reader :logger
|
67
|
+
|
68
|
+
def fetch_content(object_id)
|
69
|
+
logger.debug("Get artifact: #{object_id}")
|
70
|
+
bucket.object(object_id).get.body.string
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Drunker
|
2
|
+
class Artifact
|
3
|
+
class Layer
|
4
|
+
attr_reader :build_id
|
5
|
+
attr_reader :exit_status
|
6
|
+
attr_accessor :stdout
|
7
|
+
attr_accessor :stderr
|
8
|
+
|
9
|
+
def initialize(build_id:, stdout: nil, stderr: nil, exit_status: nil)
|
10
|
+
@build_id = build_id
|
11
|
+
@stdout = stdout
|
12
|
+
@stderr = stderr
|
13
|
+
@exit_status = exit_status.to_i
|
14
|
+
@invalid = false
|
15
|
+
end
|
16
|
+
|
17
|
+
def exit_status=(exit_status)
|
18
|
+
@exit_status = exit_status.to_i
|
19
|
+
end
|
20
|
+
|
21
|
+
def invalid?
|
22
|
+
@invalid
|
23
|
+
end
|
24
|
+
|
25
|
+
def invalid!
|
26
|
+
@invalid = true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/drunker/cli.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
module Drunker
|
2
|
+
class CLI < Thor
|
3
|
+
desc "run [IMAGE] [COMMAND]", "Run a command on CodeBuild"
|
4
|
+
method_option :config, :type => :string, :default => ".drunker.yml", :desc => "Location of config file"
|
5
|
+
method_option :concurrency, :type => :numeric, :default => 1, :desc => "Build concurrency"
|
6
|
+
method_option :compute_type, :type => :string, :default => "small", :enum => %w(small medium large), :desc => "Container compute type"
|
7
|
+
method_option :timeout, :type => :numeric, :default => 60, :desc => "Build timeout in minutes, should be between 5 and 480"
|
8
|
+
method_option :env, :type => :hash, :default => {}, :desc => "Environment variables in containers"
|
9
|
+
method_option :buildspec, :type => :string, :desc => "Location of custom buildspec"
|
10
|
+
method_option :file_pattern, :type => :string, :default => "**/*", :desc => "FILES target file pattern, can use glob to specify, but files beginning with a dot are ignored."
|
11
|
+
method_option :aggregator, :type => :string, :default => "pretty", :desc => "Aggregator name. If you want to use custom aggregator, please install that beforehand."
|
12
|
+
method_option :loglevel, :type => :string, :default => "info", :enum => %w(debug info warn error fatal), :desc => "Output log level"
|
13
|
+
method_option :debug, :type => :boolean, :default => false, :desc => "Enable debug mode. This mode does not delete the AWS resources created by Drunker"
|
14
|
+
method_option :access_key, :type => :string, :desc => "AWS access key token used by Drunker"
|
15
|
+
method_option :secret_key, :type => :string, :desc => "AWS secret key token used by Drunker"
|
16
|
+
method_option :region, :type => :string, :desc => "AWS region in which resources is created by Drunker"
|
17
|
+
method_option :profile_name, :type => :string, :desc => "AWS shared credentials profile name used by Drunker"
|
18
|
+
def _run(image, *commands)
|
19
|
+
loglevel = options[:debug] ? "DEBUG" : options[:loglevel].upcase
|
20
|
+
logger = Logger.new(STDERR).tap do |logger|
|
21
|
+
logger.level = Logger.const_get(loglevel)
|
22
|
+
logger.formatter = Proc.new { |severity, _datetime, _progname, message| "#{severity}: #{message}\n" } unless loglevel == "DEBUG"
|
23
|
+
end
|
24
|
+
config = Drunker::Config.new(image: image,
|
25
|
+
commands: commands,
|
26
|
+
config: options[:config],
|
27
|
+
concurrency: options[:concurrency],
|
28
|
+
compute_type: options[:compute_type],
|
29
|
+
timeout: options[:timeout],
|
30
|
+
env: options[:env],
|
31
|
+
buildspec: options[:buildspec],
|
32
|
+
file_pattern: options[:file_pattern],
|
33
|
+
aggregator: options[:aggregator],
|
34
|
+
debug: options[:debug],
|
35
|
+
access_key: options[:access_key],
|
36
|
+
secret_key: options[:secret_key],
|
37
|
+
region: options[:region],
|
38
|
+
profile_name: options[:profile_name],
|
39
|
+
logger: logger)
|
40
|
+
|
41
|
+
logger.info("Creating source....")
|
42
|
+
source = Drunker::Source.new(Pathname.pwd, config: config, logger: logger)
|
43
|
+
|
44
|
+
logger.info("Starting executor...")
|
45
|
+
artifact = Drunker::Executor.new(source: source, config: config, logger: logger).run
|
46
|
+
|
47
|
+
logger.info("Starting aggregator...")
|
48
|
+
aggregator = Drunker::Aggregator.create(config)
|
49
|
+
aggregator.run(artifact.layers)
|
50
|
+
|
51
|
+
unless config.debug?
|
52
|
+
logger.info("Deleting source...")
|
53
|
+
source.delete
|
54
|
+
logger.info("Deleting artifact...")
|
55
|
+
artifact.delete
|
56
|
+
end
|
57
|
+
|
58
|
+
exit aggregator.exit_status(artifact.layers)
|
59
|
+
rescue Drunker::Config::InvalidConfigException => exn
|
60
|
+
logger.error(exn.message)
|
61
|
+
exit 1
|
62
|
+
end
|
63
|
+
map "run" => "_run" # "run" is a Thor reserved word and cannot be defined as command
|
64
|
+
|
65
|
+
desc "version", "Show version"
|
66
|
+
def version
|
67
|
+
puts "Drunker #{VERSION}"
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.exit_on_failure?
|
71
|
+
true
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
module Drunker
|
2
|
+
class Config
|
3
|
+
attr_reader :image
|
4
|
+
attr_reader :commands
|
5
|
+
attr_reader :concurrency
|
6
|
+
attr_reader :compute_type
|
7
|
+
attr_reader :timeout
|
8
|
+
attr_reader :environment_variables
|
9
|
+
attr_reader :buildspec
|
10
|
+
attr_reader :file_pattern
|
11
|
+
attr_reader :aggregator
|
12
|
+
|
13
|
+
class InvalidConfigException < StandardError; end
|
14
|
+
|
15
|
+
def initialize(image:,
|
16
|
+
commands:,
|
17
|
+
config:,
|
18
|
+
concurrency:,
|
19
|
+
compute_type:,
|
20
|
+
timeout:,
|
21
|
+
env:,
|
22
|
+
buildspec:,
|
23
|
+
file_pattern:,
|
24
|
+
aggregator:,
|
25
|
+
access_key:,
|
26
|
+
secret_key:,
|
27
|
+
region:,
|
28
|
+
profile_name:,
|
29
|
+
debug:,
|
30
|
+
logger:)
|
31
|
+
@logger = logger
|
32
|
+
yaml = load!(config)
|
33
|
+
|
34
|
+
@image = image
|
35
|
+
@commands = commands
|
36
|
+
@concurrency = yaml["concurrency"] || concurrency
|
37
|
+
@compute_type = compute_name[ yaml["compute_type"] || compute_type ]
|
38
|
+
@timeout = yaml["timeout"] || timeout
|
39
|
+
@environment_variables = codebuild_environments_format(yaml["environment_variables"] || env)
|
40
|
+
@buildspec = buildspec_body!(yaml["buildspec"] || buildspec)
|
41
|
+
@file_pattern = yaml["file_pattern"] || file_pattern
|
42
|
+
@aggregator = aggregator_gem!(yaml["aggregator"] || aggregator)
|
43
|
+
@credentials = aws_credentials(profile_name: yaml.dig("aws_credentials", "profile_name") || profile_name,
|
44
|
+
access_key: yaml.dig("aws_credentials", "access_key") || access_key,
|
45
|
+
secret_key: yaml.dig("aws_credentials", "secret_key") || secret_key)
|
46
|
+
@region = yaml.dig("aws_credentials", "region") || region
|
47
|
+
@debug = debug
|
48
|
+
|
49
|
+
validate!
|
50
|
+
end
|
51
|
+
|
52
|
+
def debug?
|
53
|
+
debug
|
54
|
+
end
|
55
|
+
|
56
|
+
def aws_client_options
|
57
|
+
{ credentials: credentials, region: region }.delete_if { |_k, v| v.nil? }
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
attr_reader :credentials
|
63
|
+
attr_reader :region
|
64
|
+
attr_reader :debug
|
65
|
+
attr_reader :logger
|
66
|
+
|
67
|
+
def compute_name
|
68
|
+
{
|
69
|
+
"small" => "BUILD_GENERAL1_SMALL",
|
70
|
+
"medium" => "BUILD_GENERAL1_MEDIUM",
|
71
|
+
"large" => "BUILD_GENERAL1_LARGE"
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
def codebuild_environments_format(env)
|
76
|
+
env.map { |k, v| { name: k, value: v } }
|
77
|
+
end
|
78
|
+
|
79
|
+
def buildspec_body!(buildspec)
|
80
|
+
if buildspec
|
81
|
+
buildspec.is_a?(Hash) ? buildspec.to_yaml : Pathname.new(buildspec).read
|
82
|
+
else
|
83
|
+
Pathname.new(__dir__ + "/executor/buildspec.yml.erb").read
|
84
|
+
end
|
85
|
+
rescue Errno::ENOENT
|
86
|
+
raise InvalidConfigException.new("Invalid location of custom buildspec. got: #{buildspec}")
|
87
|
+
end
|
88
|
+
|
89
|
+
def aws_credentials(profile_name:, access_key:, secret_key:)
|
90
|
+
if profile_name
|
91
|
+
Aws::SharedCredentials.new(profile_name: profile_name)
|
92
|
+
elsif access_key && secret_key
|
93
|
+
Aws::Credentials.new(access_key, secret_key)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def aggregator_gem!(name)
|
98
|
+
gem = Gem::Specification.select { |gem| gem.name == "drunker-aggregator-#{name}" }.max { |a, b| a.version <=> b.version }
|
99
|
+
raise InvalidConfigException.new("Invalid aggregator. `drunker-aggregator-#{name}` is already installed?") unless gem
|
100
|
+
gem
|
101
|
+
end
|
102
|
+
|
103
|
+
def load!(config)
|
104
|
+
yaml = YAML.load_file(config)
|
105
|
+
validate_yaml!(yaml)
|
106
|
+
yaml
|
107
|
+
rescue Errno::ENOENT
|
108
|
+
if config == ".drunker.yml"
|
109
|
+
logger.debug("Config file not found. But it ignored because this is default config file.")
|
110
|
+
{}
|
111
|
+
else
|
112
|
+
raise InvalidConfigException.new("Config file not found. got: #{config}")
|
113
|
+
end
|
114
|
+
rescue Psych::SyntaxError => exn
|
115
|
+
raise InvalidConfigException.new("Invalid config file. message: #{exn.message}")
|
116
|
+
end
|
117
|
+
|
118
|
+
def validate_yaml!(yaml)
|
119
|
+
valid_toplevel_keys = %w(concurrency compute_type timeout file_pattern environment_variables buildspec aggregator aws_credentials)
|
120
|
+
invalid_keys = yaml.keys.reject { |k| valid_toplevel_keys.include?(k) }
|
121
|
+
raise InvalidConfigException.new("Invalid config file keys: #{invalid_keys.join(",")}") unless invalid_keys.empty?
|
122
|
+
|
123
|
+
if yaml["aws_credentials"]
|
124
|
+
valid_aws_credentials_keys = %w(profile_name access_key secret_key region)
|
125
|
+
invalid_keys = yaml["aws_credentials"].keys.reject { |k| valid_aws_credentials_keys.include?(k) }
|
126
|
+
raise InvalidConfigException.new("Invalid config file keys: #{invalid_keys.join(",")}") unless invalid_keys.empty?
|
127
|
+
end
|
128
|
+
|
129
|
+
message = case
|
130
|
+
when yaml["concurrency"] && !yaml["concurrency"].is_a?(Numeric)
|
131
|
+
"Invalid concurrency. It should be number (Not string). got: #{yaml["concurrency"]}"
|
132
|
+
when yaml["compute_type"] && !%w(small medium large).include?(yaml["compute_type"])
|
133
|
+
"Invalid compute type. It should be one of small, medium, large. got: #{yaml["compute_type"]}"
|
134
|
+
when yaml["timeout"] && !yaml["timeout"].is_a?(Numeric)
|
135
|
+
"Invalid timeout. It should be number (Not string). got: #{yaml["timeout"]}"
|
136
|
+
when yaml["buildspec"] && !(yaml["buildspec"].is_a?(String) || yaml["buildspec"].is_a?(Hash))
|
137
|
+
"Invalid buildspec. It should be string or hash. got: #{yaml["buildspec"]}"
|
138
|
+
when yaml["environment_variables"] && !yaml["environment_variables"]&.values.all? { |v| v.is_a?(String) || v.is_a?(Numeric) }
|
139
|
+
"Invalid environment variables. It should be flatten hash. got: #{yaml["environment_variables"]}"
|
140
|
+
when yaml["file_pattern"] && !yaml["file_pattern"].is_a?(String)
|
141
|
+
"Invalid file pattern. It should be string. got: #{yaml["file_pattern"]}"
|
142
|
+
when yaml["aggregator"] && !yaml["aggregator"].is_a?(String)
|
143
|
+
"Invalid aggregator. It should be string. got: #{yaml["aggregator"]}"
|
144
|
+
end
|
145
|
+
|
146
|
+
raise InvalidConfigException.new(message) if message
|
147
|
+
end
|
148
|
+
|
149
|
+
def validate!
|
150
|
+
message = case
|
151
|
+
when concurrency <= 0
|
152
|
+
"Invalid concurrency. It should be bigger than 0. got: #{concurrency}"
|
153
|
+
when !timeout.between?(5, 480)
|
154
|
+
"Invalid timeout range. It should be 5 and 480. got: #{timeout}"
|
155
|
+
end
|
156
|
+
|
157
|
+
raise InvalidConfigException.new(message) if message
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Drunker
|
2
|
+
class Executor
|
3
|
+
RETRY_LIMIT = 10
|
4
|
+
|
5
|
+
def initialize(source:, config:, logger:)
|
6
|
+
@project_name = "drunker-executor-#{Time.now.to_i.to_s}"
|
7
|
+
@source = source
|
8
|
+
logger.info("Creating artifact...")
|
9
|
+
@artifact = Drunker::Artifact.new(config: config, logger: logger)
|
10
|
+
@config = config
|
11
|
+
@client = Aws::CodeBuild::Client.new(config.aws_client_options)
|
12
|
+
@builders = []
|
13
|
+
@logger = logger
|
14
|
+
end
|
15
|
+
|
16
|
+
def run
|
17
|
+
setup_project do
|
18
|
+
@builders = parallel_build
|
19
|
+
|
20
|
+
loop do
|
21
|
+
builders.select(&:access_denied?).each do |builder|
|
22
|
+
failed_id = builder.build_id
|
23
|
+
if builder.retriable?
|
24
|
+
build_id = builder.retry
|
25
|
+
artifact.replace_build(before: failed_id ,after: build_id)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
running, finished = builders.partition(&:running?)
|
30
|
+
finished.select(&:failed?).each do |failed|
|
31
|
+
failed.errors.each do |error|
|
32
|
+
logger.warn("Build failed: #{failed.build_id}")
|
33
|
+
logger.warn("\tphase_type: #{error[:phase_type]}")
|
34
|
+
logger.warn("\tphase_status: #{error[:phase_status]}")
|
35
|
+
logger.warn("\tstatus: #{error[:status]}")
|
36
|
+
logger.warn("\tmessage: #{error[:message]}")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
break if running.count.zero?
|
41
|
+
logger.info("Waiting builder: #{finished.count}/#{builders.count}")
|
42
|
+
sleep 5
|
43
|
+
builders.each(&:refresh)
|
44
|
+
end
|
45
|
+
logger.info("Build is completed!")
|
46
|
+
artifact.layers # load artifact layers from S3
|
47
|
+
end
|
48
|
+
|
49
|
+
artifact
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
attr_reader :project_name
|
55
|
+
attr_reader :source
|
56
|
+
attr_reader :artifact
|
57
|
+
attr_reader :config
|
58
|
+
attr_reader :client
|
59
|
+
attr_reader :builders
|
60
|
+
attr_reader :logger
|
61
|
+
|
62
|
+
def setup_project
|
63
|
+
logger.info("Creating IAM resources...")
|
64
|
+
iam = IAM.new(source: source, artifact: artifact, config: config, logger: logger)
|
65
|
+
retry_count = 0
|
66
|
+
|
67
|
+
logger.info("Creating project...")
|
68
|
+
project_info = {
|
69
|
+
name: project_name,
|
70
|
+
source: source.to_h,
|
71
|
+
artifacts: artifact.to_h,
|
72
|
+
environment: {
|
73
|
+
type: "LINUX_CONTAINER",
|
74
|
+
image: config.image,
|
75
|
+
compute_type: config.compute_type,
|
76
|
+
},
|
77
|
+
service_role: iam.role.name,
|
78
|
+
timeout_in_minutes: config.timeout,
|
79
|
+
}
|
80
|
+
project_info[:environment][:environment_variables] = config.environment_variables unless config.environment_variables.empty?
|
81
|
+
begin
|
82
|
+
client.create_project(project_info)
|
83
|
+
logger.info("Created project: #{project_name}")
|
84
|
+
# Sometimes `CodeBuild is not authorized to perform: sts:AssumeRole` error occurs...
|
85
|
+
# We can solve this problem by retrying after a while.
|
86
|
+
rescue Aws::CodeBuild::Errors::InvalidInputException
|
87
|
+
if retry_count < RETRY_LIMIT
|
88
|
+
retry_count += 1
|
89
|
+
sleep 5
|
90
|
+
logger.info("Retrying...")
|
91
|
+
retry
|
92
|
+
else
|
93
|
+
raise
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
yield
|
98
|
+
|
99
|
+
unless config.debug?
|
100
|
+
logger.info("Deleting IAM resources...")
|
101
|
+
iam.delete
|
102
|
+
client.delete_project(name: project_name)
|
103
|
+
logger.info("Deleted project: #{project_name}")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def parallel_build
|
108
|
+
builders = []
|
109
|
+
|
110
|
+
files_list = source.target_files.each_slice(source.target_files.count.quo(config.concurrency).ceil).to_a
|
111
|
+
logger.info("Start parallel build: { files: #{source.target_files.count}, concurrency: #{config.concurrency}, real_concurrency: #{files_list.count} }")
|
112
|
+
files_list.to_a.each do |files|
|
113
|
+
builder = Builder.new(project_name: project_name, targets: files, artifact: artifact, config: config, logger: logger)
|
114
|
+
build_id = builder.run
|
115
|
+
artifact.set_build(build_id)
|
116
|
+
builders << builder
|
117
|
+
end
|
118
|
+
|
119
|
+
builders
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|