athlete 0.0.1

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.
@@ -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