spot_build 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +39 -0
- data/LICENSE +21 -0
- data/README.md +16 -0
- data/bin/spot_build +5 -0
- data/lib/spot_build/buildkite_agent.rb +77 -0
- data/lib/spot_build/spot_instance.rb +25 -0
- data/lib/spot_build/sqs_event.rb +24 -0
- data/lib/spot_build.rb +56 -0
- data/spot_build.gemspec +21 -0
- metadata +96 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 58c259e6f3b6c9e4ccbc8682426d3ab23b257406
|
4
|
+
data.tar.gz: 9a6d4d033c24ab2778946281c967e359968e9f12
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bd29f93748e8113a7c31ad592eeef931cecdd6b75dc40884760013261e62d0138fd3dad58ec556f2852fbf4900173c432acb32f2b0405566f2bdee1db582d758
|
7
|
+
data.tar.gz: 9db1142b19e9fcd33367394df0bccafad2bb655e8ba37ff9d7ac46864687726bfcdb60f9ccf53c2c51352cd8d3129cf5d20b93cccd2271bb297bdeb9e93fec05
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.3.0
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
spot_build (0.0.2)
|
5
|
+
aws-sdk (~> 2)
|
6
|
+
buildkit (~> 0.4)
|
7
|
+
link_header (~> 0.0.2)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
addressable (2.3.8)
|
13
|
+
aws-sdk (2.6.38)
|
14
|
+
aws-sdk-resources (= 2.6.38)
|
15
|
+
aws-sdk-core (2.6.38)
|
16
|
+
aws-sigv4 (~> 1.0)
|
17
|
+
jmespath (~> 1.0)
|
18
|
+
aws-sdk-resources (2.6.38)
|
19
|
+
aws-sdk-core (= 2.6.38)
|
20
|
+
aws-sigv4 (1.0.0)
|
21
|
+
buildkit (0.4.0)
|
22
|
+
sawyer (~> 0.6.0)
|
23
|
+
faraday (0.9.2)
|
24
|
+
multipart-post (>= 1.2, < 3)
|
25
|
+
jmespath (1.3.1)
|
26
|
+
link_header (0.0.8)
|
27
|
+
multipart-post (2.0.0)
|
28
|
+
sawyer (0.6.0)
|
29
|
+
addressable (~> 2.3.5)
|
30
|
+
faraday (~> 0.8, < 0.10)
|
31
|
+
|
32
|
+
PLATFORMS
|
33
|
+
ruby
|
34
|
+
|
35
|
+
DEPENDENCIES
|
36
|
+
spot_build!
|
37
|
+
|
38
|
+
BUNDLED WITH
|
39
|
+
1.13.6
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2016 Envato
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# Managing Spot Fleets with Buildkite
|
2
|
+
|
3
|
+
AWS EC2 Spot instances are cheaper, Buildkite Agents are a natural fit for Spot instnaces as the workload is interruptable.
|
4
|
+
|
5
|
+
SpotBuild makes it easier to use Spot instances and Spot fleets with Buildkite Agents by providing an agent that will shutdown the agent when the instance is scheduled for termination, preventing it from starting any new jobs and retry the current job it's working on.
|
6
|
+
|
7
|
+
# Running
|
8
|
+
|
9
|
+
Run this gem as a daemon on your buildkite agents and supply it the Organisation Slug and a Buildkite API token with the following permissions:
|
10
|
+
- read_agents
|
11
|
+
- read_builds
|
12
|
+
- write_builds
|
13
|
+
|
14
|
+
## Development Status
|
15
|
+
|
16
|
+
Very early stages of development
|
data/bin/spot_build
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'buildkit'
|
2
|
+
require 'socket'
|
3
|
+
require 'link_header'
|
4
|
+
|
5
|
+
module SpotBuild
|
6
|
+
class BuildkiteAgent
|
7
|
+
def initialize(token, org_slug)
|
8
|
+
@client = Buildkit.new(token: token)
|
9
|
+
@org_slug = org_slug
|
10
|
+
end
|
11
|
+
|
12
|
+
def the_end_is_nigh
|
13
|
+
return unless agent_running?
|
14
|
+
job = current_job
|
15
|
+
stop(true)
|
16
|
+
reschedule_job(job)
|
17
|
+
end
|
18
|
+
|
19
|
+
def stop(force="false")
|
20
|
+
return unless agent_running?
|
21
|
+
@client.stop_agent(@org_slug, agent_id, "{\"force\": #{force}}")
|
22
|
+
end
|
23
|
+
|
24
|
+
def agent_running?
|
25
|
+
!agent.nil?
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def reschedule_job(job)
|
31
|
+
return if job.nil?
|
32
|
+
@client.retry_job(@org_slug, job_pipeline(job[:build_url]), job_build(job[:build_url]), job[:id])
|
33
|
+
end
|
34
|
+
|
35
|
+
# build_url: https://api.buildkite.com/v2/organizations/my-great-org/pipelines/sleeper/builds/50
|
36
|
+
def job_pipeline(build_url)
|
37
|
+
build_url[%r{organizations/#{@org_slug}/pipelines/([^/]*)}, 1]
|
38
|
+
end
|
39
|
+
|
40
|
+
def job_build(build_url)
|
41
|
+
build_url[%r{organizations/#{@org_slug}/pipelines/[^/]*/builds/([0-9]*)}, 1]
|
42
|
+
end
|
43
|
+
|
44
|
+
def current_job
|
45
|
+
agent[:job]
|
46
|
+
end
|
47
|
+
|
48
|
+
def agent_id
|
49
|
+
@agent_id ||= agent[:id]
|
50
|
+
end
|
51
|
+
|
52
|
+
def agent
|
53
|
+
agents.select { |agent| agent.hostname == Socket.gethostname }.first
|
54
|
+
end
|
55
|
+
|
56
|
+
def agents
|
57
|
+
with_pagination do |options = {}|
|
58
|
+
@client.agents(@org_slug, options)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# This is definately not thread safe
|
63
|
+
def with_pagination(&block)
|
64
|
+
results = yield
|
65
|
+
while next_ref = next_link_ref(@client.last_response.headers["link"])
|
66
|
+
uri = URI.parse(next_ref.href)
|
67
|
+
next_page = uri.query.split("=")[1]
|
68
|
+
results.push(yield page: next_page)
|
69
|
+
end
|
70
|
+
results.flatten
|
71
|
+
end
|
72
|
+
|
73
|
+
def next_link_ref(header)
|
74
|
+
LinkHeader.parse(header).find_link(["rel", "next"])
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module SpotBuild
|
5
|
+
class SpotInstance
|
6
|
+
def shutdown_if_required(&block)
|
7
|
+
return false unless self.class.scheduled_for_termination?
|
8
|
+
yield
|
9
|
+
true
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.scheduled_for_termination?
|
13
|
+
!time_until_termination.nil?
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.time_until_termination
|
17
|
+
uri = URI('http://169.254.169.254/latest/meta-data/spot/termination-time')
|
18
|
+
response = Net::HTTP.get_response(uri)
|
19
|
+
return nil if response.code == "404"
|
20
|
+
Time.parse(response.body) - Time.now
|
21
|
+
rescue ArgumentError
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
module SpotBuild
|
5
|
+
class SqsEvent
|
6
|
+
def initialize(url:, timeout:, region: ENV['AWS_REGION'])
|
7
|
+
@queue = Aws::SQS::Queue.new(url: url, region: region)
|
8
|
+
@timeout = timeout
|
9
|
+
end
|
10
|
+
|
11
|
+
def shutdown_if_required(&block)
|
12
|
+
# Any message to this queue is treated as a "I should shutdown"
|
13
|
+
message = @queue.receive_messages(
|
14
|
+
attribute_names: ["All"],
|
15
|
+
max_number_of_messages: 1,
|
16
|
+
visibility_timeout: (@timeout - 5),
|
17
|
+
).first
|
18
|
+
return false if message.nil?
|
19
|
+
yield
|
20
|
+
message.delete
|
21
|
+
true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/spot_build.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'spot_build/buildkite_agent'
|
2
|
+
require 'spot_build/spot_instance'
|
3
|
+
require 'spot_build/sqs_event'
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
module SpotBuild
|
7
|
+
DEFAULT_TIMEOUT = 300
|
8
|
+
|
9
|
+
def self.run
|
10
|
+
options = parse_options
|
11
|
+
options[:timeout] ||= DEFAULT_TIMEOUT
|
12
|
+
|
13
|
+
checks = [SpotInstance.new]
|
14
|
+
if options[:queue_url]
|
15
|
+
checks.push(SqsEvent.new(url: options[:queue_url], timeout: options[:timeout], region: options[:aws_region]))
|
16
|
+
end
|
17
|
+
|
18
|
+
agent = BuildkiteAgent.new(options[:token], options[:org_slug])
|
19
|
+
loop do
|
20
|
+
checks.each do |check|
|
21
|
+
terminating = check.shutdown_if_required do
|
22
|
+
timeout = SpotInstance.scheduled_for_termination? ? (SpotInstance.time_until_termination - 30) : options[:timeout]
|
23
|
+
|
24
|
+
agent.stop
|
25
|
+
Timeout::timeout(timeout) do
|
26
|
+
while agent.agent_running?
|
27
|
+
sleep 5
|
28
|
+
end
|
29
|
+
end rescue Timeout::Error
|
30
|
+
agent.the_end_is_nigh
|
31
|
+
end
|
32
|
+
%x(shutdown -h now) if terminating
|
33
|
+
end
|
34
|
+
sleep 2
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.parse_options
|
39
|
+
options = {}
|
40
|
+
parser = OptionParser.new do |opts|
|
41
|
+
opts.banner = "Usage: #{__FILE__} [options]"
|
42
|
+
opts.on("-t", "--token TOKEN", "Buildkite API token") { |v| options[:token] = v }
|
43
|
+
opts.on("-o", "--org-slug ORGANISATION-SLUG", "The Buildkite Organisation Slug") { |v| options[:org_slug] = v }
|
44
|
+
opts.on("-s", "--sqs-queue SQS-QUEUE-URL", "The SQS Queue URL we should monitor for events that tell us to shutdown") { |v| options[:queue_url] = v }
|
45
|
+
opts.on("--timeout TIMEOUT", "The amount of time to wait for the buildkite agent to stop before shutting down. Only used if --sqs-queue is specified") { |v| options[:timeout] = v.to_i }
|
46
|
+
opts.on("-r", "--aws-region REGION", "The AWS Region the SQS queue resides in") { |v| options[:aws_region] = v }
|
47
|
+
end
|
48
|
+
parser.parse!
|
49
|
+
|
50
|
+
if options[:token].nil? || options[:org_slug].nil?
|
51
|
+
raise OptionParser::MissingArgument, "You must specify Token and Organisational Slug.\n#{parser.help}"
|
52
|
+
end
|
53
|
+
|
54
|
+
options
|
55
|
+
end
|
56
|
+
end
|
data/spot_build.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.name = 'spot_build'
|
6
|
+
gem.version = '0.0.4'
|
7
|
+
gem.authors = ['Patrick Robinson']
|
8
|
+
gem.email = []
|
9
|
+
gem.description = 'Helps manage Buildkite Agents running on EC2 Spot instances'
|
10
|
+
gem.summary = gem.description
|
11
|
+
gem.homepage = 'https://github.com/envato/spot_build'
|
12
|
+
|
13
|
+
gem.files = `git ls-files`.split($/)
|
14
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
15
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
16
|
+
gem.require_paths = ['lib']
|
17
|
+
|
18
|
+
gem.add_dependency 'buildkit', '~> 0.4'
|
19
|
+
gem.add_dependency 'aws-sdk', '~> 2'
|
20
|
+
gem.add_dependency 'link_header', '~> 0.0.2'
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: spot_build
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Patrick Robinson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-05-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: buildkit
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.4'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.4'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: aws-sdk
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: link_header
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.0.2
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.0.2
|
55
|
+
description: Helps manage Buildkite Agents running on EC2 Spot instances
|
56
|
+
email: []
|
57
|
+
executables:
|
58
|
+
- spot_build
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- .ruby-version
|
63
|
+
- Gemfile
|
64
|
+
- Gemfile.lock
|
65
|
+
- LICENSE
|
66
|
+
- README.md
|
67
|
+
- bin/spot_build
|
68
|
+
- lib/spot_build.rb
|
69
|
+
- lib/spot_build/buildkite_agent.rb
|
70
|
+
- lib/spot_build/spot_instance.rb
|
71
|
+
- lib/spot_build/sqs_event.rb
|
72
|
+
- spot_build.gemspec
|
73
|
+
homepage: https://github.com/envato/spot_build
|
74
|
+
licenses: []
|
75
|
+
metadata: {}
|
76
|
+
post_install_message:
|
77
|
+
rdoc_options: []
|
78
|
+
require_paths:
|
79
|
+
- lib
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - '>='
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
requirements: []
|
91
|
+
rubyforge_project:
|
92
|
+
rubygems_version: 2.0.14.1
|
93
|
+
signing_key:
|
94
|
+
specification_version: 4
|
95
|
+
summary: Helps manage Buildkite Agents running on EC2 Spot instances
|
96
|
+
test_files: []
|