spot_build 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
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,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'spot_build'
4
+
5
+ SpotBuild.run
@@ -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
@@ -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: []