Class: Athlete::Deployment

Inherits:
Object
  • Object
show all
Includes:
Logging
Defined in:
lib/athlete/deployment.rb

Constant Summary collapse

@@valid_properties =

Define valid properties

%w{
  name
  marathon_url
  build_name
  image_name
  command
  arguments
  cpus
  memory
  environment_variables
  instances
  minimum_health_capacity
}
@@locked_properties =

Define properties that cannot be overridden or inherited

%w{
  name
  marathon_url
  build_name
  image_name
  command
  arguments
  environment_variables
}

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Logging

#debug, #fatal, #get_loglevel, #info, #loglevel, #warn

Constructor Details

#initializeDeployment

Returns a new instance of Deployment.



37
38
39
40
41
# File 'lib/athlete/deployment.rb', line 37

def initialize
  @inherit_properties = []
  @override_properties = []
  setup_dsl_methods
end

Class Attribute Details

.deploymentsObject

Returns the value of attribute deployments.



8
9
10
# File 'lib/athlete/deployment.rb', line 8

def deployments
  @deployments
end

Class Method Details

.define(name, &block) ⇒ Object



68
69
70
71
72
73
74
75
76
# File 'lib/athlete/deployment.rb', line 68

def self.define(name, &block)
  deployment = Athlete::Deployment.new
  deployment.name name
  deployment.instance_eval(&block)
  deployment.fill_default_values
  deployment.validate
  deployment.connect_to_marathon
  @deployments[deployment.name] = deployment
end

Instance Method Details

#app_running?Boolean

Returns:

  • (Boolean)


240
241
242
# File 'lib/athlete/deployment.rb', line 240

def app_running?
  get_running_config != nil
end

#connect_to_marathonObject



113
114
115
# File 'lib/athlete/deployment.rb', line 113

def connect_to_marathon
  @marathon_client = Marathon::Client.new(@marathon_url)
end

#deploy_or_updateObject



142
143
144
145
146
147
148
149
150
151
152
# File 'lib/athlete/deployment.rb', line 142

def deploy_or_update
  if app_running?
    debug "App is running in Marathon; performing a warm deploy"
    prepare_for_warm_deploy
    return @marathon_client.update(@name, marathon_json)
  else
    debug "App is not running in Marathon; performing a cold deploy"
    prepare_for_cold_deploy
    return @marathon_client.start(@name, marathon_json)
  end
end

#deployment_completed?Boolean

Returns:

  • (Boolean)


175
176
177
# File 'lib/athlete/deployment.rb', line 175

def deployment_completed?
  @marathon_client.find_deployment_by_name(@name) == nil
end

#fill_default_valuesObject



78
79
80
81
82
83
# File 'lib/athlete/deployment.rb', line 78

def fill_default_values
  if !@instances
    @instances = 1
    @inherit_properties << 'instances'
  end
end

#get_running_configObject

Find the app if it’s already in Marathon (if it’s not there, we get nil)



229
230
231
232
233
234
235
236
237
238
# File 'lib/athlete/deployment.rb', line 229

def get_running_config
  if @running_config
    return @running_config
  else
    response = @marathon_client.find(@name)
    @running_config = response.error? ? nil : response.parsed_response
    debug "Retrieved running Marathon configuration: #{@running_config}"
    return @running_config
  end
end

#has_task_failures?Boolean

Returns:

  • (Boolean)


188
189
190
191
192
# File 'lib/athlete/deployment.rb', line 188

def has_task_failures?
  app_config = @marathon_client.find(@name)
  return false if app_config.parsed_response['app']['lastTaskFailure'].nil?
  app_config.parsed_response['app']['lastTaskFailure']['version'] == @deploy_response['version']
end

#increment_retryObject



183
184
185
186
# File 'lib/athlete/deployment.rb', line 183

def increment_retry
  @retry_count ||= 0
  @retry_count = @retry_count + 1
end

#linked_buildObject

Locate the linked build



224
225
226
# File 'lib/athlete/deployment.rb', line 224

def linked_build
  @build_name ? Athlete::Build.builds[@build_name] : nil
end

#marathon_jsonObject



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/athlete/deployment.rb', line 244

def marathon_json
  json = {}
  
  json['id'] = @name
  json['cmd'] = @command if @command
  json['args'] = @arguments if @arguments
  json['cpus'] = @cpus if @cpus
  json['mem'] = @memory if @memory
  json['env'] = @environment_variables if @environment_variables
  json['instances'] = @instances if @instances
  if @minimum_health_capacity
    json['upgradeStrategy'] = {
      'minimumHealthCapacity' => @minimum_health_capacity
    }
  end
  
  if @image_name || @build_name
    image = @image_name || linked_build.final_image_name
    json['container'] = {
      'type' => 'DOCKER',
      'docker' => {
        'image' => image,
        'network' => 'BRIDGE'
      }
    }
  end
  debug("Generated Marathon JSON: #{json.to_json}")
  json
end

#performObject



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/athlete/deployment.rb', line 117

def perform
  response = deploy_or_update
  @deploy_response = response.parsed_response
  debug "Entire deployment response: #{response.inspect}"
  
  # Check to see if the deployment actually happened
  if response.code == 409
    fatal "Deployment did not start; another deployment is in progress"
    exit 1
  end
  
  info "Polling for deployment state"
  state = poll_for_deploy_state
  case state
  when :retry_exceeded
    fatal "App failed to start on Marathon; cancelling deploy"
    exit 1
  when :complete
    info "App is running on Marathon; deployment complete"
  else
    fatal "App is in unknown state on Marathon"
    exit 1
  end
end

#poll_for_deploy_stateObject

Poll Marathon to see if the deploy has completed for the given deployed version



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/athlete/deployment.rb', line 156

def poll_for_deploy_state
  debug "Entering deploy state polling"
  while (not deployment_completed?) && (not retry_exceeded?)
    if has_task_failures?
      warn "Task failures have occurred during the deploy attempt - this deploy may not succeed"
      sleep 1
      increment_retry
    else
      debug "Deploy still in progress with no task failures; sleeping and retrying"
      sleep 1
      increment_retry
    end
  end
  
  # We bailed because we exceeded retry or the deploy completed, determine
  # which of these states it is
  deployment_completed? ? :complete : :retry_exceeded
end

#prepare_for_cold_deployObject

A ‘cold’ deploy is one where the app is not running in Marathon. We have to do additional validation to ensure we can deploy the app, since we don’t have a set of valid parameters in Marathon.



214
215
216
217
218
219
220
221
# File 'lib/athlete/deployment.rb', line 214

def prepare_for_cold_deploy
  errors = []
  errors << "You must specify the parameter 'cpus'" unless @cpus
  errors << "You must specify the parameter 'memory'" unless @memory
  unless errors.empty?
    raise ConfigurationInvalidException, @errors
  end
end

#prepare_for_warm_deployObject

A ‘warm’ deploy is one where the app is running in Marathon and we’re making changes to it. For each declared configuration property, determine whether it will be always inserted into the remote configuration (:override) or not (:inherit). Think of :override as “Athlete is authoritative for this property”, and :inherit as “Marathon is authoritative for this property”. The way this works in practice is we unset any instance variables that are specified as “inherit”, so that when the Marathon JSON is generated by ‘to_marathon_json` they do not appear in the final deployment JSON.



204
205
206
207
208
209
# File 'lib/athlete/deployment.rb', line 204

def prepare_for_warm_deploy
  @inherit_properties.each do |property|
    debug "Property '#{property}' is specified as :inherit; not supplying to Marathon"
    instance_variable_set("@#{property}", nil)
  end
end

#readable_outputObject



274
275
276
277
278
279
280
281
282
# File 'lib/athlete/deployment.rb', line 274

def readable_output
  lines = []
  lines << "  Deployment name: #{@name}"
  @@valid_properties.sort.each do |property|
    next if property == 'name'
    lines << sprintf("    %-26s: %s", property, instance_variable_get("@#{property}")) if instance_variable_get("@#{property}")
  end
  puts lines.join("\n")
end

#retry_exceeded?Boolean

Returns:

  • (Boolean)


179
180
181
# File 'lib/athlete/deployment.rb', line 179

def retry_exceeded?
  @retry_count == 10
end

#setup_dsl_methodsObject



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/athlete/deployment.rb', line 43

def setup_dsl_methods
  @@valid_properties.each do |property|
    self.class.class_eval {
      
      # Define property settings methods for the DSL
      define_method(property) do |property_value, override_or_inherit = nil|
        instance_variable_set("@#{property}", property_value)
        if not @@locked_properties.include?(property)
          case override_or_inherit
          when :override
            @override_properties << property
          when :inherit
            @inherit_properties << property
          else
            raise Athlete::ConfigurationInvalidException, 
              "Property '#{property}' of deployment '#{@name}' specified behaviour as '#{override_or_inherit}', which is not one of :override or :inherit"
          end
        end
        self.class.class_eval{attr_reader property.to_sym}
      end
      
    }
  end
end

#validateObject



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/athlete/deployment.rb', line 85

def validate
  errors = []
  
  # Must specify a Docker image (either from a build or some upstream source)
  errors << "You must set one of image_name or build_name" unless @build_name || @image_name
  
  # If a build name is specified, it must match something in the file
  if @build_name && linked_build.nil?
    errors << "Build name '#{@build_name}' doesn't match a build in the config file"
  end
  
  # Marathon URL is required
  errors << "You must specify marathon_url" unless @marathon_url
  
  # Environment variables must be a hash
  errors << "environment_variables must be a hash" if @environment_variables && !@environment_variables.kind_of?(Hash)
  
  # Can't supply both command and args
  errors << "You must specify only one of command or arguments" if @command && @arguments
  
  # Arguments must be in an array
  error << "The arguments parameter must be specified as an array" if @arguments && !@arguments.kind_of?(Array)
  
  unless errors.empty?
    raise ConfigurationInvalidException, errors
  end
end