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
  port_mappings
  volumes
}
@@locked_properties =

Define properties that cannot be overridden or inherited

%w{
  name
  marathon_url
  build_name
  image_name
  command
  arguments
  environment_variables
  port_mappings
  volumes
}

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.



41
42
43
44
45
# File 'lib/athlete/deployment.rb', line 41

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



72
73
74
75
76
77
78
79
80
# File 'lib/athlete/deployment.rb', line 72

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)


247
248
249
# File 'lib/athlete/deployment.rb', line 247

def app_running?
  get_running_config != nil
end

#connect_to_marathonObject



120
121
122
# File 'lib/athlete/deployment.rb', line 120

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

#deploy_or_updateObject



149
150
151
152
153
154
155
156
157
158
159
# File 'lib/athlete/deployment.rb', line 149

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)


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

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

#fill_default_valuesObject



82
83
84
85
86
87
# File 'lib/athlete/deployment.rb', line 82

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)



236
237
238
239
240
241
242
243
244
245
# File 'lib/athlete/deployment.rb', line 236

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)


195
196
197
198
199
# File 'lib/athlete/deployment.rb', line 195

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



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

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

#linked_buildObject

Locate the linked build



231
232
233
# File 'lib/athlete/deployment.rb', line 231

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

#marathon_jsonObject



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/athlete/deployment.rb', line 251

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'
      }
    }
    
    if @port_mappings && !@port_mappings.empty?
      json['container']['docker']['portMappings'] = @port_mappings
    end
    
    if @volumes
      json['container']['volumes'] = @volumes
    end
    
  end
  
  debug("Generated Marathon JSON: #{json.to_json}")
  json
end

#performObject



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/athlete/deployment.rb', line 124

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



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/athlete/deployment.rb', line 163

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.



221
222
223
224
225
226
227
228
# File 'lib/athlete/deployment.rb', line 221

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.



211
212
213
214
215
216
# File 'lib/athlete/deployment.rb', line 211

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



291
292
293
294
295
296
297
298
299
# File 'lib/athlete/deployment.rb', line 291

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)


186
187
188
# File 'lib/athlete/deployment.rb', line 186

def retry_exceeded?
  @retry_count == 10
end

#setup_dsl_methodsObject



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/athlete/deployment.rb', line 47

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



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/athlete/deployment.rb', line 89

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)
  
  # Port mappings must be an array (of hashes but let's do a basic check)
  error << "The port_mappings parameter must be an array of hashes" if @port_mappings && !@port_mappings.kind_of?(Array)
  
  unless errors.empty?
    raise ConfigurationInvalidException, errors
  end
end