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

Define properties that cannot be overridden or inherited

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

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.



39
40
41
42
43
# File 'lib/athlete/deployment.rb', line 39

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



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

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)


245
246
247
# File 'lib/athlete/deployment.rb', line 245

def app_running?
  get_running_config != nil
end

#connect_to_marathonObject



118
119
120
# File 'lib/athlete/deployment.rb', line 118

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

#deploy_or_updateObject



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

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)


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

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

#fill_default_valuesObject



80
81
82
83
84
85
# File 'lib/athlete/deployment.rb', line 80

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)



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

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)


193
194
195
196
197
# File 'lib/athlete/deployment.rb', line 193

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



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

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

#linked_buildObject

Locate the linked build



229
230
231
# File 'lib/athlete/deployment.rb', line 229

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

#marathon_jsonObject



249
250
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
# File 'lib/athlete/deployment.rb', line 249

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 @port_mappings && !@port_mappings.empty?
    json['portMappings'] = @port_mappings
  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



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

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



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

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.



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

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.



209
210
211
212
213
214
# File 'lib/athlete/deployment.rb', line 209

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



283
284
285
286
287
288
289
290
291
# File 'lib/athlete/deployment.rb', line 283

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)


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

def retry_exceeded?
  @retry_count == 10
end

#setup_dsl_methodsObject



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

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



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
112
113
114
115
116
# File 'lib/athlete/deployment.rb', line 87

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