athlete 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6d3860c8ddee3f9c32fe243c211833b00ae37488
4
+ data.tar.gz: 47a5c48f169f581acb8c100b9428e69dec334778
5
+ SHA512:
6
+ metadata.gz: a0ee2dfa941b4e3c504b6530bfe344170da34a8cb6500b05cf062c49832d55c07a05c784472034f8258990e2b9169a8e09df95072d050f3e183431863e1b2a2b
7
+ data.tar.gz: 17ca485cf67f9ff3afe517b049b3c104feda65f9b30d63ab3e0e3fa8180e025cf1f3f575478f1ba63497ed64f01e59e120541e6d7265ef1448802ddf405fe32d
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
@@ -0,0 +1 @@
1
+ athlete
@@ -0,0 +1 @@
1
+ 2.1.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in athlete.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Andy Sykes
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,297 @@
1
+ # Athlete
2
+
3
+ Athlete is a Capistrano-like deployment tool for deploying Docker
4
+ containers into [Marathon](https://mesosphere.github.io/marathon/).
5
+
6
+ Warning: Athlete is at best a beta. It may not understand all Marathon responses, it
7
+ lacks some features, and it doesn't do some basic error checking. Use at your own risk.
8
+ Pull requests welcomed. YMMV.
9
+
10
+ ## Why Athlete exists
11
+
12
+ If you're a Ruby developer, Capistrano makes things so fantastically easy that you miss it when
13
+ you start to package apps as Docker containers.
14
+
15
+ Athlete was written to become, internally, a simple Capistrano-like tool that allows developers
16
+ to quickly and easily deploy to our Marathon cluster.
17
+
18
+ ## Features
19
+
20
+ - Simple DSL for defining Docker builds and deployments
21
+ - Detects deployment failures
22
+ - Simple (~750 LoC)
23
+ - Allows Marathon properties to be set outside deployments or forced
24
+
25
+ ## Installation
26
+
27
+ Add these lines to your application's Gemfile, in the `:development` group:
28
+
29
+ group :development do
30
+ ... some other gems ...
31
+ gem 'athlete'
32
+ end
33
+
34
+ And then execute:
35
+
36
+ bundle install --binstubs
37
+
38
+ Athlete isn't required at runtime.
39
+
40
+ ## Usage
41
+
42
+ Athlete performs two actions: building, and deploying.
43
+
44
+ An Athlete build runs `docker build`, tags the image appropriately
45
+ (including the registry name if you're using a private registry),
46
+ and the pushes it to the registry. This push step can be skipped.
47
+
48
+ An Athlete deploy uses Marathon's REST API to deploy one of the Docker
49
+ containers built in the build step or any arbitrary Docker container -
50
+ you are not limited to only deploying containers you built.
51
+
52
+ ### Command line usage
53
+
54
+ (You may want to read the configuration section first, since you
55
+ need a configuration before you can do anything.)
56
+
57
+ To get help with the CLI:
58
+
59
+ bin/athlete help
60
+
61
+ To build all builds in the configuration file:
62
+
63
+ bin/athlete build
64
+
65
+ To build all builds put not push them:
66
+
67
+ bin/athlete build --no-push
68
+
69
+ To deploy all deployments in the configuration file:
70
+
71
+ bin/athlete deploy
72
+
73
+ ### Configuration
74
+
75
+ Athlete is configured using a simple DSL.
76
+
77
+ By default, it expects the configuration file to be called `athlete.rb` and
78
+ to be placed in the `config` directory relative to the base of your app.
79
+
80
+ __Todo: write a command to produce a template athlete.rb file__
81
+
82
+ A build takes some information about the name you want to give your
83
+ container, how it should be versioned, and what registry it should be pushed to.
84
+
85
+ A deployment takes some information about where to deploy to, how many
86
+ instances to run, how much CPU and memory resource to allocate, and so on.
87
+
88
+ Here's an example of a simple image build and deploy:
89
+
90
+ ```ruby
91
+ Athlete::Build.define('my-image') do
92
+ registry 'my-registry:5000'
93
+ version '1'
94
+ end
95
+
96
+ Athlete::Deployment.define('my-app') do
97
+ marathon_url 'http://marathon:8080'
98
+ # image_name 'ubuntu:12.04'
99
+ # command ['/bin/sleep', '600']
100
+ # arguments ['something']
101
+ # environment_variables {'RACK_ENV' => 'production'}
102
+ build_name 'my-image'
103
+ cpus 1, :override
104
+ memory 128, :inherit
105
+ instances 1, :override
106
+ minimum_health_capacity 0.5, :inherit
107
+ end
108
+ ```
109
+
110
+ This will build an image tagged `my-registry:5000/my-image:1`, remembering
111
+ that the Docker image conventions are: `registry_url:registry_port/image_name:image_version`.
112
+
113
+ Once built, it will deploy a container based on that image to the Marathon host
114
+ at `http://marathon:8080`. It will request 1 CPU and 128MB of RAM from Marathon,
115
+ and run one instance of the container. See below for details of what the property
116
+ `minimum_health_capacity` does.
117
+
118
+ #### Build DSL reference
119
+
120
+ | Property | Required | Description |
121
+ | -------- | -------- | ----------- |
122
+ | registry | no | The Docker registry to push to - if unspecified, we use the Docker Hub. |
123
+ | version | yes | How to version the image; this can be any stringifiable object, or the symbol `:git`, which will version using the output of `git rev-parse HEAD` |
124
+
125
+ #### Deployment DSL reference
126
+
127
+ Some properties are defined with an extra parameter which is either
128
+ `:override` or `:inherit` - in the above example, `instances 1, :override`
129
+ is specified with `:override`, and `memory 128, :inherit` is specified with
130
+ `:inherit`.
131
+
132
+ These properties are ones that can be varied in Marathon through other means.
133
+ For example, you may have a separate 'scaling' system that changes the number
134
+ of instances of a container in response to some parameter. Let's say that it
135
+ has acted to increase the number of instances to 5, and our `athlete.rb` has
136
+ this line in the deployment section:
137
+
138
+ instances 1, :override
139
+
140
+ When you deploy, Athlete will see that Marathon's currently running value for
141
+ instances of the app is 5, and that you set it to 1 in your deployment configuration
142
+ with `:override`, and it will _force_ there to be only 1 instance after the deployment.
143
+
144
+ This would be non-ideal, since your scaling system is 'authoritative' for this
145
+ property - it decides how many instances should run. Resetting it when you deploy
146
+ could break production!
147
+
148
+ If you had set the instances property to be `:inherit`, like so:
149
+
150
+ instances 1, :inherit
151
+
152
+ Then when you run a deploy, Athlete will completely ignore the instance value set in
153
+ the configuration file, and just trust whatever is currently set in Marathon.
154
+
155
+ The only time _all parameters_ will be sent to Marathon is when an app
156
+ does not already exist. In that case, we have to supply some initial values to get the app going.
157
+
158
+ ##### `marathon_url`
159
+
160
+ __required__: yes
161
+ __override/inherit__: no
162
+
163
+ The URL to the Marathon REST API endpoint you're using.
164
+
165
+ ##### `build_name`
166
+
167
+ __required__: yes (if not supplying `image_name`)
168
+ __override/inherit__: no
169
+
170
+ The build name to get Docker image information from. You must
171
+ reference a build defined earlier in the `athlete.rb` file. The name of a build
172
+ is the string supplied to the `define` call. E.g.
173
+
174
+ Athlete::Build.define('my-image') do
175
+ registry 'my-registry:5000'
176
+ version '1'
177
+ end
178
+
179
+ You would reference this by setting:
180
+
181
+ build_name 'my-image'
182
+
183
+ in your deployment definition.
184
+
185
+ It is required if you are not specifying `image_name`.
186
+
187
+ ##### `image_name`
188
+
189
+ __required__: yes (if not supplying `build_name`)
190
+ __override/inherit__: no
191
+
192
+ The Docker image name to deploy. You must specify the entire image name,
193
+ including whatever tagged version you want. E.g. `ubuntu:12.04`.
194
+
195
+ It is required if you are not specifying `build_name`.
196
+
197
+ ##### `command`
198
+
199
+ __required__: no
200
+ __override/inherit__: no
201
+
202
+ The command to run inside the Docker container. This will override
203
+ the `CMD` section of the Dockerfile.
204
+
205
+ ##### `arguments`
206
+
207
+ __required__: no
208
+ __override/inherit__: no
209
+
210
+ Arguments to supply to the container's ENTRYPOINT. This should be
211
+ an array of strings.
212
+
213
+ ##### `cpus`
214
+
215
+ __required__: yes (on a cold deploy)
216
+ __override/inherit__: yes
217
+
218
+ CPU resource to request for this app. This can be a fractional value (e.g. 0.1).
219
+
220
+ ##### `memory`
221
+
222
+ __required__: yes (on a cold deploy)
223
+ __override/inherit__: yes
224
+
225
+ Memory to request for this app in MB.
226
+
227
+ ##### `environment_variables`
228
+
229
+ __required__: no
230
+ __override/inherit__: no
231
+
232
+ Environment variables to pass into the container at startup - must be specified
233
+ as a hash of `ENV_VAR_NAME => ENV_VAR_VALUE`.
234
+
235
+ ##### `instances`
236
+
237
+ __required__: no
238
+ __override/inherit__: yes
239
+
240
+ Number of instances of the container to run.
241
+
242
+ ##### `minimum_health_capacity`
243
+
244
+ __required__: no
245
+ __override/inherit__: yes
246
+
247
+ This description is taken from the
248
+ [Marathon documentation](https://mesosphere.github.io/marathon/docs/rest-api.html#post-/v2/apps).
249
+
250
+ > During an upgrade all instances of an application get replaced by a new version.
251
+ > The minimumHealthCapacity defines the minimum number of healthy nodes, that do not sacrifice
252
+ > overall application purpose. It is a number between 0 and 1 which is multiplied with the
253
+ > instance count. The default minimumHealthCapacity is 1, which means no old instance can be stopped,
254
+ > before all new instances are deployed. A value of 0.5 means that an upgrade can be deployed side by side,
255
+ > by taking half of the instances down in the first step, deploy half of the new version and
256
+ > then take the other half down and deploy the rest. A value of 0 means take all instances down
257
+ > immediately and replace with the new application.
258
+
259
+ ## Contributing
260
+
261
+ 1. Fork it ( https://github.com/forward3d/athlete/fork )
262
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
263
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
264
+ 4. Push to the branch (`git push origin my-new-feature`)
265
+ 5. Create a new pull request
266
+
267
+ ## Acknowledgements
268
+
269
+ This repository includes code from the [marathon_client gem](https://github.com/mesosphere/marathon_client).
270
+ It is included with Athlete as Athlete requires some unreleased functionality, and some additional
271
+ functionality, and it is not possible (rightly!) to make gems dependent on git repositories.
272
+
273
+ In accordance with the MIT license, the license information from marathon_client is included here,
274
+ and in the `lib/marathon` directory, which are files covered by the below license:
275
+
276
+ Copyright (c) 2013 Tobi Knaup
277
+
278
+ MIT License
279
+
280
+ Permission is hereby granted, free of charge, to any person obtaining
281
+ a copy of this software and associated documentation files (the
282
+ "Software"), to deal in the Software without restriction, including
283
+ without limitation the rights to use, copy, modify, merge, publish,
284
+ distribute, sublicense, and/or sell copies of the Software, and to
285
+ permit persons to whom the Software is furnished to do so, subject to
286
+ the following conditions:
287
+
288
+ The above copyright notice and this permission notice shall be
289
+ included in all copies or substantial portions of the Software.
290
+
291
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
292
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
293
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
294
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
295
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
296
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
297
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'athlete/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "athlete"
8
+ spec.version = Athlete::VERSION
9
+ spec.authors = ["Andy Sykes"]
10
+ spec.email = ["github@tinycat.co.uk"]
11
+ spec.summary = %q{A deployment tool for Marathon and Mesos}
12
+ spec.description = %q{A deployment tool for building Docker containers for Marathon and Mesos}
13
+ spec.homepage = "https://github.com/forward3d/athlete"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.6"
22
+ spec.add_development_dependency "rake"
23
+
24
+ spec.add_dependency "thor", "~> 0.19"
25
+ spec.add_dependency "httparty", "~> 0.13"
26
+ spec.add_dependency "multi_json", "~> 1.10"
27
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/athlete'
4
+
5
+ Athlete::CLI.start()
@@ -0,0 +1,20 @@
1
+ require 'logger'
2
+ require 'open3'
3
+ require 'httparty'
4
+ require 'multi_json'
5
+ require_relative 'marathon/client'
6
+ require_relative 'marathon/response'
7
+
8
+ require_relative "athlete/version"
9
+ require_relative "athlete/logging"
10
+ require_relative "athlete/utils"
11
+ require_relative "athlete/build"
12
+ require_relative "athlete/cli"
13
+ require_relative "athlete/deployment"
14
+
15
+ module Athlete
16
+ class BuildConfigurationInvalid < Exception; end
17
+ class BuildFailedException < Exception; end
18
+ class CommandExecutionFailed < Exception; end
19
+ class ConfigurationInvalidException < Exception; end
20
+ end
@@ -0,0 +1,130 @@
1
+ module Athlete
2
+ class Build
3
+ include Logging
4
+
5
+ @builds = {}
6
+
7
+ class << self
8
+ attr_accessor :builds
9
+ end
10
+
11
+ # Define valid properties
12
+ @@valid_properties = %w{
13
+ name
14
+ registry
15
+ version
16
+ }
17
+
18
+ def initialize
19
+ @@valid_properties.each do |property|
20
+ self.class.class_eval {
21
+ define_method(property) do |arg|
22
+ instance_variable_set("@#{property}", arg)
23
+ self.class.class_eval{attr_reader property.to_sym}
24
+ end
25
+ }
26
+ end
27
+ end
28
+
29
+ def setup_dsl_methods
30
+ @@valid_properties.each do |property|
31
+ self.class.class_eval {
32
+ define_method(property) do |arg|
33
+ instance_variable_set("@#{property}", arg)
34
+ self.class.class_eval{attr_reader property.to_sym}
35
+ end
36
+ }
37
+ end
38
+ end
39
+
40
+ def self.define(name, &block)
41
+ build = Athlete::Build.new
42
+ build.name name
43
+ build.instance_eval(&block)
44
+ build.fill_default_values
45
+ @builds[build.name] = build
46
+ end
47
+
48
+ def fill_default_values
49
+ @version_method ||= :git_head
50
+ end
51
+
52
+ def final_image_name
53
+ @final_image_name ||= @registry.nil? ? "#{@name}:#{determined_version}" : "#{@registry}/#{@name}:#{determined_version}"
54
+ end
55
+
56
+ def determined_version
57
+ case @version
58
+ when :git_head
59
+ return git_tag
60
+ else
61
+ return @version.to_s
62
+ end
63
+ end
64
+
65
+ # Figure out the short hash of the current HEAD
66
+ def git_tag
67
+ return @git_tag if @git_tag
68
+ @git_tag = `git rev-parse --short HEAD 2>&1`.chomp
69
+ if $? != 0
70
+ raise Athlete::BuildFailedException, "Could not determine git hash of HEAD, output was: #{@git_tag}"
71
+ end
72
+ @git_tag
73
+ end
74
+
75
+ # Create the image name with a specified git tag
76
+ def image_name_with_specified_version(version)
77
+ @registry.nil? ? "#{@name}:#{version}" : "#{@registry}/#{@name}:#{version}"
78
+ end
79
+
80
+ def perform(should_push)
81
+ build
82
+ if should_push
83
+ push
84
+ else
85
+ info "Skipping push of image as --no-push was specified"
86
+ end
87
+ end
88
+
89
+ # Build the Docker image
90
+ def build
91
+ info "Building with image name as: '#{final_image_name}', tagged #{determined_version}"
92
+ command = "docker build -t #{final_image_name} ."
93
+ logged_command = get_loglevel == Logger::INFO ? 'docker build' : command
94
+ retval = Utils::Subprocess.run command do |stdout, stderr, thread|
95
+ info "[#{logged_command}] [stdout] #{stdout}"
96
+ info "[#{logged_command}] [stderr] #{stderr}" if stderr != nil
97
+ end
98
+ if retval.exitstatus != 0
99
+ raise Athlete::CommandExecutionFailed, "The command #{command} exited with non-zero status #{retval.exitstatus}"
100
+ end
101
+ end
102
+
103
+ # Push image to remote registry (Docker Hub or private registry)
104
+ def push
105
+ if @registry.nil?
106
+ info "Preparing to push image to the Docker Hub"
107
+ else
108
+ info "Preparing to push image to '#{@registry}'"
109
+ end
110
+
111
+ command = "docker push #{final_image_name}"
112
+ retval = Utils::Subprocess.run "docker push #{final_image_name}" do |stdout, stderr, thread|
113
+ info "[#{logged_command}] [stdout] #{stdout}"
114
+ info "[#{logged_command}] [stderr] #{stderr}" if stderr != nil
115
+ end
116
+
117
+ end
118
+
119
+ def readable_output
120
+ lines = []
121
+ lines << " Build name: #{@name}"
122
+ @@valid_properties.sort.each do |property|
123
+ next if property == 'name'
124
+ lines << sprintf(" %-10s: %s", property, instance_variable_get("@#{property}")) if instance_variable_get("@#{property}")
125
+ end
126
+ puts lines.join("\n")
127
+ end
128
+
129
+ end
130
+ end