drunker 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +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
|