marathon_deploy 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.
data/examples/run.sh ADDED
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ ruby -I.:lib deploy.rb -v -f jondeploy.yaml
@@ -0,0 +1,68 @@
1
+ {
2
+ "args": [
3
+ "properties.py && FOO=\"`cat /opt/etc/public-search-germany-webapp.properties | grep -v '^\\s*#' | grep -E '.+=.+'`\"; echo $FOO; echo; env $FOO; echo; env $FOO hostName=$HOSTNAME backend.instance.name=$HOSTNAME java -Dhttp.port=8080 -Dajp.port=8009 -Dinstance.confdir=file:///opt/etc -Dbackend.logdir=$MESOS_SANDBOX -Dlog.rootDir=$MESOS_SANDBOX -DlogDir=$MESOS_SANDBOX $CMD_OPTS -Xmx$JAVA_XMX -Xms$JAVA_XMS -jar $MESOS_SANDBOX/public-search-germany-webapp*.jar"
4
+ ],
5
+ "cmd": null,
6
+ "container": {
7
+ "docker": {
8
+ "image": "dockerregistry.mobile.rz/mobile-java8:latest",
9
+ "network": "BRIDGE",
10
+ "portMappings": [
11
+ {
12
+ "containerPort": 8080,
13
+ "hostPort": 0,
14
+ "protocol": "tcp"
15
+ },
16
+ {
17
+ "containerPort": 8009,
18
+ "hostPort": 0,
19
+ "protocol": "tcp"
20
+ }
21
+ ]
22
+ },
23
+ "type": "DOCKER"
24
+ },
25
+ "cpus": 0.1,
26
+ "env": {
27
+ "APPLICATION_NAME": "public-search-germany-webapp",
28
+ "CMD_OPTS": "",
29
+ "CONFIG_ASSEMBLER_BASE_URL": "http://mobile-config-assembler.service.consul/config-assembler",
30
+ "DATACENTER_NUMBER": "44",
31
+ "JAVA_XMS": "252m",
32
+ "JAVA_XMX": "504m",
33
+ "SERVERCLASS_NAME": "pubse",
34
+ "SERVICE_8009_NAME": "public-search-germany-webapp",
35
+ "SERVICE_8009_TAGS": "ajp",
36
+ "SERVICE_8080_CHECK_SCRIPT": "curl --fail --silent $HOST_IP:$SERVICE_PORT/fz/release-info",
37
+ "SERVICE_8080_NAME": "public-search-germany-webapp",
38
+ "SERVICE_8080_TAGS": "http,haproxy"
39
+ },
40
+ "healthChecks": [
41
+ {
42
+ "gracePeriodSeconds": 30,
43
+ "intervalSeconds": 10,
44
+ "maxConsecutiveFailures": 3,
45
+ "portIndex": 0,
46
+ "protocol": "TCP",
47
+ "timeoutSeconds": 30
48
+ },
49
+ {
50
+ "gracePeriodSeconds": 30,
51
+ "intervalSeconds": 10,
52
+ "maxConsecutiveFailures": 3,
53
+ "path": "/fz/release-info",
54
+ "portIndex": 0,
55
+ "protocol": "HTTP",
56
+ "timeoutSeconds": 30
57
+ }
58
+ ],
59
+ "id": "somename",
60
+ "instances": 4,
61
+ "mem": 512,
62
+ "storeUrls": [
63
+ "http://maven-download.mobile.rz/maven/hosted-mobile-deployment-productive-releases/de/mobile/public-search-germany-webapp/ecs-1558-gb70c745/public-search-germany-webapp-ecs-1558-gb70c745.jar"
64
+ ],
65
+ "uris": [
66
+
67
+ ]
68
+ }
@@ -0,0 +1,54 @@
1
+ ---
2
+ args:
3
+ - properties.py && FOO="`cat /opt/etc/public-search-germany-webapp.properties | grep
4
+ -v '^\s*#' | grep -E '.+=.+'`"; echo $FOO; echo; env $FOO; echo; env $FOO hostName=$HOSTNAME
5
+ backend.instance.name=$HOSTNAME java -Dhttp.port=8080 -Dajp.port=8009 -Dinstance.confdir=file:///opt/etc
6
+ -Dbackend.logdir=$MESOS_SANDBOX -Dlog.rootDir=$MESOS_SANDBOX -DlogDir=$MESOS_SANDBOX
7
+ $CMD_OPTS -Xmx$JAVA_XMX -Xms$JAVA_XMS -jar $MESOS_SANDBOX/public-search-germany-webapp*.jar
8
+ cmd:
9
+ container:
10
+ docker:
11
+ image: dockerregistry.mobile.rz/mobile-java8:latest
12
+ network: BRIDGE
13
+ portMappings:
14
+ - containerPort: 8080
15
+ hostPort: 0
16
+ protocol: tcp
17
+ - containerPort: 8009
18
+ hostPort: 0
19
+ protocol: tcp
20
+ type: DOCKER
21
+ cpus: 0.1
22
+ env:
23
+ APPLICATION_NAME: public-search-germany-webapp
24
+ CMD_OPTS: ''
25
+ CONFIG_ASSEMBLER_BASE_URL: http://mobile-config-assembler.service.consul/config-assembler
26
+ DATACENTER_NUMBER: '44'
27
+ JAVA_XMS: 252m
28
+ JAVA_XMX: 504m
29
+ SERVERCLASS_NAME: pubse
30
+ SERVICE_8009_NAME: public-search-germany-webapp
31
+ SERVICE_8009_TAGS: ajp
32
+ SERVICE_8080_CHECK_SCRIPT: curl --fail --silent $HOST_IP:$SERVICE_PORT/fz/release-info
33
+ SERVICE_8080_NAME: public-search-germany-webapp
34
+ SERVICE_8080_TAGS: http,haproxy
35
+ healthChecks:
36
+ - gracePeriodSeconds: 30
37
+ intervalSeconds: 10
38
+ maxConsecutiveFailures: 3
39
+ portIndex: 0
40
+ protocol: TCP
41
+ timeoutSeconds: 30
42
+ - gracePeriodSeconds: 30
43
+ intervalSeconds: 10
44
+ maxConsecutiveFailures: 3
45
+ path: "/fz/release-info"
46
+ portIndex: 0
47
+ protocol: HTTP
48
+ timeoutSeconds: 30
49
+ id: public-search-germany-webapp
50
+ instances: 99
51
+ mem: 512
52
+ storeUrls:
53
+ - http://maven-download.mobile.rz/maven/hosted-mobile-deployment-productive-releases/de/mobile/public-search-germany-webapp/ecs-1558-gb70c745/public-search-germany-webapp-ecs-1558-gb70c745.jar
54
+ uris: []
data/input.txt ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "args": [
3
+ "properties.py && FOO=\"`cat /opt/etc/public-search-germany-webapp-%FIRST_VAR%.properties | grep -v '^\\s*#' | grep -E '.+=.+'`\"; echo $FOO; echo; env $FOO; echo; env %SECOND_VAR%$FOO hostName=$HOSTNAME backend.instance.name=$HOSTNAME java -Dhttp.port=8080 -Dajp.port=8009 -Dinstance.confdir=file:///opt/etc -Dbackend.logdir=$MESOS_SANDBOX -Dlog.rootDir=$MESOS_SANDBOX -DlogDir=$MESOS_SANDBOX $CMD_OPTS -Xmx$JAVA_XMX -Xms$JAVA_XMS -jar $MESOS_SANDBOX/public-search-germany-webapp*.jar"
4
+ ],
5
+ "cmd": null,
6
+ "container": {
7
+ "docker": {
8
+ "image": "dockerregistry.mobile.rz/mobile-java8:latest",
9
+ "network": "BRIDGE",
10
+ "portMappings": [
11
+ {
12
+ "containerPort": 8080,
13
+ "hostPort": 0,
14
+ "protocol": "tcp"
15
+ },
16
+ {
17
+ "containerPort": 8009,
18
+ "hostPort": 0,
19
+ "protocol": "tcp"
20
+ }
21
+ ]
22
+ },
23
+ "type": "DOCKER"
24
+ },
25
+ "cpus": 0.1,
26
+ "env": {
27
+ "APPLICATION_NAME": "public-search-germany-webapp",
28
+ "CMD_OPTS": "",
29
+ "CONFIG_ASSEMBLER_BASE_URL": "http://mobile-config-assembler.service.consul/config-assembler",
30
+ "DATACENTER_NUMBER": "44",
31
+ "JAVA_XMS": "252m",
32
+ "JAVA_XMX": "504m",
33
+ "SERVERCLASS_NAME": "pubse",
34
+ "SERVICE_8009_NAME": "public-search-germany-webapp",
35
+ "SERVICE_8009_TAGS": "ajp",
36
+ "SERVICE_8080_CHECK_SCRIPT": "curl --fail --silent $HOST_IP:%SERVICE_PORT%/fz/release-info",
37
+ "SERVICE_8080_NAME": "public-search-germany-webapp",
38
+ "SERVICE_8080_TAGS": "http,haproxy"
39
+ },
40
+ "healthChecks": [
41
+ {
42
+ "gracePeriodSeconds": 30,
43
+ "intervalSeconds": 10,
44
+ "maxConsecutiveFailures": 3,
45
+ "portIndex": 0,
46
+ "protocol": "TCP",
47
+ "timeoutSeconds": 30
48
+ },
49
+ {
50
+ "gracePeriodSeconds": 30,
51
+ "intervalSeconds": 10,
52
+ "maxConsecutiveFailures": 3,
53
+ "path": "/%CONTEXT%%TRICKY%/release-info",
54
+ "portIndex": 0,
55
+ "protocol": "HTTP",
56
+ "timeoutSeconds": 30
57
+ }
58
+ ],
59
+ "id": "public-search-germany-webapp",
60
+ "instances": 4,
61
+ "mem": 512,
62
+ "storeUrls": [
63
+ "http://maven-download.mobile.rz/maven/hosted-mobile-deployment-productive-releases/de/mobile/public-search-germany-webapp/ecs-1558-gb70c745/public-search-germany-webapp-ecs-1558-gb70c745.jar"
64
+ ],
65
+ "uris": []
66
+ }
@@ -0,0 +1,5 @@
1
+ require "marathon_deploy/version"
2
+
3
+ module MarathonDeploy
4
+ # Your code goes here...
5
+ end
@@ -0,0 +1,85 @@
1
+ require 'marathon_deploy/marathon_defaults'
2
+ require 'marathon_deploy/yaml_json'
3
+ require 'marathon_deploy/error'
4
+ require 'marathon_deploy/utils'
5
+
6
+ class Application
7
+
8
+ attr_reader :json, :id
9
+ attr_accessor :envs
10
+
11
+ def initialize(deployfile)
12
+
13
+ if (!File.exist?(File.join(Dir.pwd,deployfile)))
14
+ message = "#{deployfile} not found in current directory #{File.join(Dir.pwd)}"
15
+ raise Error::IOError, message, caller
16
+ end
17
+
18
+ extension = File.extname(deployfile)
19
+
20
+ case extension
21
+ when '.json'
22
+ @json = YamlJson.read_json(deployfile)
23
+ when '.yaml'
24
+ @json = YamlJson.yaml2json(deployfile)
25
+ else
26
+ message = "File extension #{extension} is not supported for deployment file #{deployfile}"
27
+ raise Error::UnsupportedFileExtension, message, caller
28
+ end
29
+
30
+ missing_attributes = MarathonDefaults.missing_attributes(@json)
31
+
32
+ if(!missing_attributes.empty?)
33
+ message = "#{deployfile} is missing required marathon API attributes: #{missing_attributes.join(',')}"
34
+ raise Error::MissingMarathonAttributesError, message, caller
35
+ end
36
+
37
+ missing_envs = MarathonDefaults.missing_envs(@json)
38
+ if(!missing_envs.empty?)
39
+ message = "#{deployfile} is missing required environment variables: #{missing_envs.join(',')}"
40
+ raise Error::MissingMarathonAttributesError, message, caller
41
+ end
42
+
43
+ @deployfile = deployfile
44
+ @json = Utils.deep_symbolize(@json)
45
+ add_identifier
46
+
47
+ end
48
+
49
+ def overlay_preproduction_settings
50
+ @json = MarathonDefaults.overlay_preproduction_settings(@json)
51
+ end
52
+
53
+ def add_identifier
54
+ random = Utils.random
55
+ # Time.now.to_i
56
+ json[:env]['UNIQUE_ID'] = "#{id}_#{random}"
57
+ end
58
+
59
+ def to_s
60
+ return id
61
+ end
62
+
63
+ def id
64
+ if (@json[:id])
65
+ return @json[:id]
66
+ end
67
+ end
68
+
69
+ def add_envs(envs)
70
+ if (envs.is_a?(Hash))
71
+ envs.each do |key,value|
72
+ @json[:env][key] = value
73
+ end
74
+ else
75
+ raise Error::BadFormatError, "argument must be a hash", caller
76
+ end
77
+ end
78
+
79
+ def instances
80
+ if (@json[:instances])
81
+ return @json[:instances]
82
+ end
83
+ end
84
+
85
+ end
@@ -0,0 +1,269 @@
1
+ require 'marathon_deploy/http_util'
2
+ require 'marathon_deploy/utils'
3
+ require 'marathon_deploy/marathon_defaults'
4
+ require 'timeout'
5
+
6
+ class Deployment
7
+
8
+ DEPLOYMENT_RECHECK_INTERVAL = MarathonDefaults::DEPLOYMENT_RECHECK_INTERVAL
9
+ DEPLOYMENT_TIMEOUT = MarathonDefaults::DEPLOYMENT_TIMEOUT
10
+ HEALTHY_WAIT_TIMEOUT = MarathonDefaults::HEALTHY_WAIT_TIMEOUT
11
+ HEALTHY_WAIT_RECHECK_INTERVAL = MarathonDefaults::HEALTHY_WAIT_RECHECK_INTERVAL
12
+
13
+ attr_reader :url, :application, :deploymentId
14
+
15
+ def initialize(url, application)
16
+ raise ArgumentError, "second argument to deployment object must be an Application", caller unless (!application.nil? && application.class == Application)
17
+ raise Error::BadURLError, "invalid url => #{url}", caller if (!HttpUtil.valid_url(url))
18
+ @url = HttpUtil.clean_url(url)
19
+ @application = application
20
+ end
21
+
22
+ def timeout
23
+ return DEPLOYMENT_TIMEOUT
24
+ end
25
+
26
+ def healthcheck_timeout
27
+ return HEALTHY_WAIT_TIMEOUT
28
+ end
29
+
30
+ def versions
31
+ if (!applicationExists?)
32
+ response = HttpUtil.get(@url + MarathonDefaults::MARATHON_APPS_REST_PATH + @application.id + '/versions')
33
+ response_body = Utils.response_body(response)
34
+ return response_body[:versions]
35
+ else
36
+ return Array.new
37
+ end
38
+ end
39
+
40
+ def wait_for_deployment_id(message = "Deployment with deploymentId #{@deploymentId} in progress")
41
+ startTime = Time.now
42
+ deployment_seen = false
43
+ Timeout::timeout(DEPLOYMENT_TIMEOUT) do
44
+ while running_for_deployment_id?
45
+
46
+ deployment_seen = true
47
+ #response = list_all
48
+ #STDOUT.print "." if ( $LOG.level == 1 )
49
+ elapsedTime = '%.2f' % (Time.now - startTime)
50
+ $LOG.info(message + " (elapsed time #{elapsedTime}s)")
51
+ deployments = deployments_for_deployment_id
52
+ deployments.each do |item|
53
+ $LOG.debug(deployment_string(item))
54
+ end
55
+ sleep(DEPLOYMENT_RECHECK_INTERVAL)
56
+ end
57
+ #STDOUT.puts "" if ( $LOG.level == 1 )
58
+ if (deployment_seen)
59
+ elapsedTime = '%.2f' % (Time.now - startTime)
60
+ $LOG.info("Deployment with deploymentId #{@deploymentId} ended (Total time #{elapsedTime}s )")
61
+ end
62
+ end
63
+ end
64
+
65
+ def wait_for_application(message = "Deployment of application #{@application.id} in progress")
66
+ deployment_seen = false
67
+ Timeout::timeout(DEPLOYMENT_TIMEOUT) do
68
+ while running_for_application_id?
69
+ deployment_seen = true
70
+ #response = list_all
71
+ #STDOUT.print "." if ( $LOG.level == 1 )
72
+ $LOG.info(message)
73
+ deployments_for_application_id.each do |item|
74
+ $LOG.debug(deployment_string(item))
75
+ end
76
+ #$LOG.debug(JSON.pretty_generate(JSON.parse(response.body)))
77
+ sleep(DEPLOYMENT_RECHECK_INTERVAL)
78
+ end
79
+ #STDOUT.puts "" if ( $LOG.level == 1 )
80
+ if (deployment_seen)
81
+ $LOG.info("Deployment of application #{@application.id} ended")
82
+ end
83
+ end
84
+ end
85
+
86
+ def wait_until_healthy
87
+ Timeout::timeout(HEALTHY_WAIT_TIMEOUT) do
88
+ loop do
89
+ break if (!health_checks_defined?)
90
+ sick = get_alive("false")
91
+ if (!sick.empty?)
92
+ $LOG.info("#{sick.size}/#{@application.instances} instances are not healthy => " + sick.join(','))
93
+ else
94
+ healthy = get_alive("true")
95
+ if (healthy.size == @application.instances)
96
+ $LOG.info("#{healthy.size}/#{@application.instances} instances are healthy => " + healthy.join(','))
97
+ break
98
+ else
99
+ $LOG.info("#{healthy.size}/#{@application.instances} healthy instances seen, retrying")
100
+ end
101
+ end
102
+ sleep(HEALTHY_WAIT_RECHECK_INTERVAL)
103
+ end
104
+ end
105
+ end
106
+
107
+ def cancel(deploymentId,force=false)
108
+ raise Error::BadURLError, "deploymentId must be specified to cancel deployment", caller if (deploymentId.empty?)
109
+ if (running_for_deployment_id?(deploymentId))
110
+ response = HttpUtil.delete(@url + MarathonDefaults::MARATHON_DEPLOYMENT_REST_PATH + deploymentId + "?force=#{force}")
111
+ $LOG.debug("Cancellation response [#{response.code}] => " + JSON.pretty_generate(JSON.parse(response.body)))
112
+ end
113
+ return response
114
+ end
115
+
116
+ def applicationExists?
117
+ response = list_app
118
+ if (response.code.to_i == 200)
119
+ return true
120
+ end
121
+ return false
122
+ end
123
+
124
+ def create_app
125
+ response = HttpUtil.post(@url + MarathonDefaults::MARATHON_APPS_REST_PATH,@application.json)
126
+ @deploymentId = get_deployment_id
127
+ return response
128
+ end
129
+
130
+ def update_app(force=false)
131
+ url = @url + MarathonDefaults::MARATHON_APPS_REST_PATH + @application.id
132
+ url += force ? '?force=true' : ''
133
+ $LOG.debug("Updating app #{@application.id} #{url}")
134
+ response = HttpUtil.put(url,@application.json)
135
+ @deploymentId = Utils.response_body(response)[:deploymentId]
136
+ return response
137
+
138
+ end
139
+
140
+ def rolling_restart
141
+ url = @url + MarathonDefaults::MARATHON_APPS_REST_PATH + @application.id + '/restart'
142
+ $LOG.debug("Calling marathon api with url: #{url}")
143
+ response = HttpUtil.post(url,{})
144
+ $LOG.info("Restart of #{@application.id} returned status code: #{response.code}")
145
+ $LOG.info(JSON.pretty_generate(JSON.parse(response.body)))
146
+ end
147
+
148
+ def health_checks_defined?
149
+ response = list_app
150
+ response_body = Utils.response_body(response)
151
+ return response_body[:app][:healthChecks].size == 0 ? false : true
152
+ end
153
+
154
+ ####### PRIVATE METHODS ##########
155
+ private
156
+
157
+ def get_alive(value)
158
+ state = Array.new
159
+
160
+ if (health_checks_defined?)
161
+ response = list_app
162
+ response_body = Utils.response_body(response)
163
+ if (response_body[:app].empty?)
164
+ raise Error::DeploymentError, "Marathon returned an empty app json object", caller
165
+ else
166
+ get_healthcheck_results.flatten.each do |result|
167
+ next if result.nil?
168
+ alive = result[:alive].to_s
169
+ taskId = result[:taskId].to_s
170
+ if (!alive.nil? && !taskId.nil?)
171
+ state << taskId if (alive == value)
172
+ end
173
+ end
174
+ end
175
+ else
176
+ $LOG.info("No health checks defined. Cannot determine application health of #{@application.id}.")
177
+ end
178
+ return state
179
+ end
180
+
181
+ def get_task_ids
182
+ response = list_app
183
+ response_body = Utils.response_body(response)
184
+ return response_body[:app][:tasks].collect { |task| task[:id]}
185
+ end
186
+
187
+ def get_healthcheck_results
188
+ response = list_app
189
+ response_body = Utils.response_body(response)
190
+ return response_body[:app][:tasks].collect { |task| task[:healthCheckResults]}
191
+ end
192
+
193
+ def get_deployment_id
194
+ response = list_app
195
+ payload = Utils.response_body(response)
196
+ return payload[:app][:deployments].first[:id] unless (payload[:app].nil?)
197
+ return nil
198
+ end
199
+
200
+ def list_all
201
+ HttpUtil.get(@url + MarathonDefaults::MARATHON_DEPLOYMENT_REST_PATH)
202
+ end
203
+
204
+ def running_for_application_id?
205
+ if (deployment_running? && !deployments_for_application_id.empty?)
206
+ return true
207
+ end
208
+ return false
209
+ end
210
+
211
+ def running_for_deployment_id?
212
+ if (deployment_running? && !deployments_for_deployment_id.empty?)
213
+ return true
214
+ end
215
+ return false
216
+ end
217
+
218
+ def deployment_running?
219
+ response = list_all
220
+ body = JSON.parse(response.body)
221
+ return false if body.empty?
222
+ return true
223
+ end
224
+
225
+ def get_deployment_ids
226
+ response = list_all
227
+ payload = JSON.parse(response.body)
228
+ return payload.collect { |d| d['id'] }
229
+ end
230
+
231
+ def list_app
232
+ HttpUtil.get(@url + MarathonDefaults::MARATHON_APPS_REST_PATH + @application.id)
233
+ end
234
+
235
+ # DONT USE: the response seems to be broken in marathon for /v2/apps/application-id/tasks
236
+ #def get_tasks
237
+ # HttpUtil.get(@url + MarathonDefaults::MARATHON_APPS_REST_PATH + @application.id + '/tasks')
238
+ #end
239
+
240
+ def deployment_string(deploymentJsonObject)
241
+ string = "\n" + "+-" * 25 + " DEPLOYMENT INFO " + "+-" * 25 + "\n"
242
+ deploymentJsonObject.sort.each do |k,v|
243
+ case v
244
+ when String
245
+ string += k + " => " + v + "\n"
246
+ when Fixnum
247
+ string += k + " => " + v.to_s + "\n"
248
+ when Array
249
+ string += k + " => " + v.join(',') + "\n"
250
+ else
251
+ string += "#{k} + #{v}\n"
252
+ end
253
+ end
254
+ return string
255
+ end
256
+
257
+ def deployments_for_deployment_id
258
+ response = list_all
259
+ payload = JSON.parse(response.body)
260
+ return payload.find_all { |d| d['id'] == @deploymentId }
261
+ end
262
+
263
+ def deployments_for_application_id
264
+ response = list_all
265
+ payload = JSON.parse(response.body)
266
+ return payload.find_all { |d| d['affectedApps'].include?('/' + @application.id) }
267
+ end
268
+
269
+ end