cyclid 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +174 -0
  3. data/README.md +54 -0
  4. data/app/cyclid.rb +61 -0
  5. data/app/cyclid/config.rb +38 -0
  6. data/app/cyclid/controllers.rb +123 -0
  7. data/app/cyclid/controllers/auth.rb +34 -0
  8. data/app/cyclid/controllers/auth/token.rb +78 -0
  9. data/app/cyclid/controllers/health.rb +96 -0
  10. data/app/cyclid/controllers/organizations.rb +104 -0
  11. data/app/cyclid/controllers/organizations/collection.rb +134 -0
  12. data/app/cyclid/controllers/organizations/config.rb +128 -0
  13. data/app/cyclid/controllers/organizations/document.rb +135 -0
  14. data/app/cyclid/controllers/organizations/job.rb +266 -0
  15. data/app/cyclid/controllers/organizations/members.rb +145 -0
  16. data/app/cyclid/controllers/organizations/stages.rb +251 -0
  17. data/app/cyclid/controllers/users.rb +47 -0
  18. data/app/cyclid/controllers/users/collection.rb +131 -0
  19. data/app/cyclid/controllers/users/document.rb +133 -0
  20. data/app/cyclid/health_helpers.rb +40 -0
  21. data/app/cyclid/job.rb +3 -0
  22. data/app/cyclid/job/helpers.rb +67 -0
  23. data/app/cyclid/job/job.rb +164 -0
  24. data/app/cyclid/job/runner.rb +275 -0
  25. data/app/cyclid/job/stage.rb +67 -0
  26. data/app/cyclid/log_buffer.rb +104 -0
  27. data/app/cyclid/models.rb +3 -0
  28. data/app/cyclid/models/job_record.rb +25 -0
  29. data/app/cyclid/models/organization.rb +64 -0
  30. data/app/cyclid/models/plugin_config.rb +25 -0
  31. data/app/cyclid/models/stage.rb +42 -0
  32. data/app/cyclid/models/step.rb +29 -0
  33. data/app/cyclid/models/user.rb +60 -0
  34. data/app/cyclid/models/user_permissions.rb +28 -0
  35. data/app/cyclid/monkey_patches.rb +37 -0
  36. data/app/cyclid/plugin_registry.rb +75 -0
  37. data/app/cyclid/plugins.rb +125 -0
  38. data/app/cyclid/plugins/action.rb +48 -0
  39. data/app/cyclid/plugins/action/command.rb +89 -0
  40. data/app/cyclid/plugins/action/email.rb +207 -0
  41. data/app/cyclid/plugins/action/email/html.erb +58 -0
  42. data/app/cyclid/plugins/action/email/text.erb +13 -0
  43. data/app/cyclid/plugins/action/script.rb +90 -0
  44. data/app/cyclid/plugins/action/slack.rb +129 -0
  45. data/app/cyclid/plugins/action/slack/note.erb +5 -0
  46. data/app/cyclid/plugins/api.rb +195 -0
  47. data/app/cyclid/plugins/api/github.rb +111 -0
  48. data/app/cyclid/plugins/api/github/callback.rb +66 -0
  49. data/app/cyclid/plugins/api/github/methods.rb +201 -0
  50. data/app/cyclid/plugins/api/github/status.rb +67 -0
  51. data/app/cyclid/plugins/builder.rb +80 -0
  52. data/app/cyclid/plugins/builder/mist.rb +107 -0
  53. data/app/cyclid/plugins/dispatcher.rb +89 -0
  54. data/app/cyclid/plugins/dispatcher/local.rb +167 -0
  55. data/app/cyclid/plugins/provisioner.rb +40 -0
  56. data/app/cyclid/plugins/provisioner/debian.rb +90 -0
  57. data/app/cyclid/plugins/provisioner/ubuntu.rb +98 -0
  58. data/app/cyclid/plugins/source.rb +39 -0
  59. data/app/cyclid/plugins/source/git.rb +64 -0
  60. data/app/cyclid/plugins/transport.rb +63 -0
  61. data/app/cyclid/plugins/transport/ssh.rb +155 -0
  62. data/app/cyclid/sinatra/api_helpers.rb +66 -0
  63. data/app/cyclid/sinatra/auth_helpers.rb +127 -0
  64. data/app/cyclid/sinatra/warden/strategies/api_token.rb +62 -0
  65. data/app/cyclid/sinatra/warden/strategies/basic.rb +58 -0
  66. data/app/cyclid/sinatra/warden/strategies/hmac.rb +76 -0
  67. data/app/db.rb +51 -0
  68. data/bin/cyclid-db-init +107 -0
  69. data/db/schema.rb +92 -0
  70. data/lib/cyclid/app.rb +4 -0
  71. metadata +407 -0
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2016 Liqwyd Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ # Top level module for all of the core Cyclid code.
17
+ module Cyclid
18
+ # Module for the Cyclid API
19
+ module API
20
+ # Module for all Organization related API endpoints
21
+ module Organizations
22
+ # API endpoints for Organization specific configuration
23
+ # @api REST
24
+ module Configs
25
+ # rubocop:disable Metrics/LineLength
26
+ # @!group Organizations
27
+
28
+ # @!method get_organizations_organization_configs_type_plugin
29
+ # @overload GET /organizations/:organization/configs/:type/:plugin
30
+ # @macro rest
31
+ # @param [String] organization Name of the organization.
32
+ # @param [String] type The plugin type E.g. 'api' for an API plugin, 'source' for a
33
+ # Source plugin etc.
34
+ # @param [String] plugin Name of the plugin.
35
+ # Get the current configuration for the given plugin.
36
+ # @return The plugin configuration for the given plugin.
37
+ # @return [404] The organization or plugin does not exist.
38
+ # @example Get the 'example' plugin configuration from the 'example' organization
39
+ # GET /organizations/example/configs/type/example => {"id":1,
40
+ # "plugin":"example",
41
+ # "version":"1.0.0",
42
+ # "config":{<plugin specific object>},
43
+ # "organization_id":2,
44
+ # "schema":[<plugin configuration schema>]}
45
+
46
+ # @!method put_organizations_organization_configs_type_plugin
47
+ # @overload PUT /organizations/:organization/configs/:type/:plugin
48
+ # @macro rest
49
+ # @param [String] organization Name of the organization.
50
+ # @param [String] type The plugin type E.g. 'api' for an API plugin, 'source' for a
51
+ # Source plugin etc.
52
+ # @param [String] plugin Name of the plugin.
53
+ # Update the plugin configuration
54
+ # @return [200] The plugin configuration was updated.
55
+ # @return [404] The organization or plugin does not exist.
56
+
57
+ # @!endgroup
58
+ # rubocop:enable Metrics/LineLength
59
+
60
+ # Sinatra callback
61
+ # @private
62
+ def self.registered(app)
63
+ include Errors::HTTPErrors
64
+ include Constants::JobStatus
65
+
66
+ # Get the current configuration for the given plugin.
67
+ app.get '/:plugin' do
68
+ authorized_for!(params[:name], Operations::READ)
69
+
70
+ org = Organization.find_by(name: params[:name])
71
+ halt_with_json_response(404, INVALID_ORG, 'organization does not exist') \
72
+ if org.nil?
73
+
74
+ Cyclid.logger.debug "type=#{params[:type]} plugin=#{params[:plugin]}"
75
+
76
+ # Find the plugin
77
+ plugin = Cyclid.plugins.find(params[:plugin], params[:type])
78
+ halt_with_json_response(404, INVALID_PLUGIN, 'plugin does not exist') \
79
+ if plugin.nil?
80
+
81
+ # Ask the plugin for the current config for this organization. This
82
+ # will include the config schema under the 'schema' attribute.
83
+ begin
84
+ config = plugin.get_config(org)
85
+
86
+ halt_with_json_response(404, INVALID_PLUGIN_CONFIG, 'failed to get plugin config') \
87
+ if config.nil?
88
+ rescue StandardError => ex
89
+ halt_with_json_response(404, \
90
+ INVALID_PLUGIN_CONFIG, \
91
+ "failed to get plugin config: #{ex}") \
92
+ if config.nil?
93
+ end
94
+
95
+ return config.to_json
96
+ end
97
+
98
+ # Update the plugin configuration
99
+ app.put '/:plugin' do
100
+ authorized_for!(params[:name], Operations::ADMIN)
101
+
102
+ payload = parse_request_body
103
+ Cyclid.logger.debug payload
104
+
105
+ org = Organization.find_by(name: params[:name])
106
+ halt_with_json_response(404, INVALID_ORG, 'organization does not exist') \
107
+ if org.nil?
108
+
109
+ # Find the plugin
110
+ plugin = Cyclid.plugins.find(params[:plugin], params[:type])
111
+ halt_with_json_response(404, INVALID_PLUGIN, 'plugin does not exist') \
112
+ if plugin.nil?
113
+
114
+ # Ask the plugin for the current config for this organization. This
115
+ # will include the config schema under the 'schema' attribute.
116
+ begin
117
+ plugin.set_config(payload, org)
118
+ rescue StandardError => ex
119
+ halt_with_json_response(404, \
120
+ INVALID_PLUGIN_CONFIG, \
121
+ "failed to set plugin config: #{ex}") \
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2016 Liqwyd Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ # Top level module for all of the core Cyclid code.
17
+ module Cyclid
18
+ # Module for the Cyclid API
19
+ module API
20
+ # Module for all Organization related API endpoints
21
+ module Organizations
22
+ # API endpoints for a single Organization document
23
+ # @api REST
24
+ module Document
25
+ # @!group Organizations
26
+
27
+ # @!method get_organizations_organization
28
+ # @overload GET /organizations/:organization
29
+ # @macro rest
30
+ # @param [String] organization Name of the organization.
31
+ # Get a specific organization. The RSA public key is in Base64 encoded
32
+ # DER format, and can be used to encrypt secrets that can be
33
+ # decrypted only by the server.
34
+ # @return The organization object.
35
+ # @return [404] The requested organization does not exist.
36
+ # @example Get the 'example' organization
37
+ # GET /organizations/example => [{"id": 1,
38
+ # "name": "example",
39
+ # "owner_email": "admin@example.com",
40
+ # "users": ["user1", "user2"],
41
+ # "public_key": "<RSA public key>"}]
42
+ # @see get_organizations
43
+
44
+ # @!method put_organizations(body)
45
+ # @overload PUT /organizations/:organization
46
+ # @macro rest
47
+ # @param [String] organization Name of the organization.
48
+ # Modify an organization. The organizations name or public key can not
49
+ # be changed.
50
+ # If a list of users is provided, the current list will be *replaced*,
51
+ # so clients should first retrieve the full list of users, modify it,
52
+ # and then use this API to set the final list of users.
53
+ # @param [JSON] body New organization data.
54
+ # @option body [String] owner_email Email address of the organization owner
55
+ # @option body [Array<String>] users List of users who are organization members.
56
+ # @return [200] The organization was changed successfully.
57
+ # @return [404] The organization does not exist
58
+ # @return [404] A user in the list of members does not exist
59
+ # @example Modify the 'example' organization to have user1 & user2 as members
60
+ # POST /organizations/example <= {"users": ["user1", "user2"]}
61
+ # @example Modify the 'example' organization to change the owner email
62
+ # POST /organizations/example <= {"owner_email": "bob@example.com"}
63
+
64
+ # @!endgroup
65
+
66
+ # Sinatra callback
67
+ # @private
68
+ def self.registered(app)
69
+ include Errors::HTTPErrors
70
+
71
+ # Get a specific organization.
72
+ app.get do
73
+ authorized_for!(params[:name], Operations::READ)
74
+
75
+ org = Organization.find_by(name: params[:name])
76
+ halt_with_json_response(404, INVALID_ORG, 'organization does not exist') \
77
+ if org.nil?
78
+
79
+ # Base64 encode the public key
80
+ public_key = Base64.strict_encode64(org.rsa_public_key)
81
+
82
+ # Convert to a Hash, sanitize and inject the Users data & encoded
83
+ # RSA key
84
+ org_hash = sanitize_organization(org.serializable_hash)
85
+ org_hash['users'] = org.users.map(&:username)
86
+ org_hash['public_key'] = public_key
87
+
88
+ return org_hash.to_json
89
+ end
90
+
91
+ # Modify a specific organization.
92
+ app.put do
93
+ authorized_for!(params[:name], Operations::WRITE)
94
+
95
+ payload = parse_request_body
96
+ Cyclid.logger.debug payload
97
+
98
+ org = Organization.find_by(name: params[:name])
99
+ halt_with_json_response(404, INVALID_ORG, 'organization does not exist') \
100
+ if org.nil?
101
+
102
+ begin
103
+ # Change the owner email if one is provided
104
+ org['owner_email'] = payload['owner_email'] if payload.key? 'owner_email'
105
+
106
+ # Change the users if a list of users was provided
107
+ if payload.key? 'users'
108
+ # Add each provided user to the Organization
109
+ org.users = payload['users'].map do |username|
110
+ user = User.find_by(username: username)
111
+
112
+ halt_with_json_response(404, \
113
+ INVALID_USER, \
114
+ "user #{username} does not exist") \
115
+ if user.nil?
116
+
117
+ user
118
+ end
119
+ end
120
+
121
+ org.save!
122
+ rescue ActiveRecord::ActiveRecordError, \
123
+ ActiveRecord::UnknownAttributeError => ex
124
+
125
+ Cyclid.logger.debug ex.message
126
+ halt_with_json_response(400, INVALID_JSON, ex.message)
127
+ end
128
+
129
+ return json_response(NO_ERROR, "organization #{params['name']} updated")
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,266 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2016 Liqwyd Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ # Top level module for all of the core Cyclid code.
17
+ module Cyclid
18
+ # Module for the Cyclid API
19
+ module API
20
+ # Module for all Organization related API endpoints
21
+ module Organizations
22
+ # API endpoints for Organization Jobs
23
+ # @api REST
24
+ module Jobs
25
+ # rubocop:disable Metrics/LineLength
26
+ # @!group Organizations
27
+
28
+ # @!method get_organizations_organization_jobs
29
+ # @overload GET /organizations/:organization/jobs
30
+ # @param [String] organization Name of the organization.
31
+ # @param [Boolean] stats_only Do not return the job records; just the count
32
+ # @param [Integer] limit Maxiumum number of records to return.
33
+ # @param [Integer] offset Offset to start returning records.
34
+ # @param [String] s_name Name of the job to search on.
35
+ # @param [Integer] s_status Status to search on.
36
+ # @param [String] s_from Date & time to search from.
37
+ # @param [String] s_to Date & time to search to.
38
+ # @macro rest
39
+ # Get a list of jobs that have been run for the organization. Jobs can be
40
+ # filtered using the s_name, s_status, s_from and s_to search parameters. If
41
+ # s_from & s_to are given, only jobs which started between the two times will
42
+ # be returned.
43
+ # @return The list of job details.
44
+ # @return [404] The organization does not exist.
45
+
46
+ # @!method post_organizations_organization_jobs(body)
47
+ # @overload POST /organizations/:organization/jobs
48
+ # @macro rest
49
+ # @param [String] organization Name of the organization.
50
+ # Create and run a job. The job definition can be either a JSON or
51
+ # YAML document.
52
+ # @param [JSON] body Job definition
53
+ # @option body [String] name Name of the job.
54
+ # @option body [Object] environment Job runtime environment details. At a minimum this
55
+ # must include the operating system name & version to use.
56
+ # @option body [Object] secrets ({}) Encrypted secret data for use by the job.
57
+ # @option body [Array<Object>] stages ([]) Ad-hoc stage definitions which are local to this job.
58
+ # @option body [Array<Object>] sequence ([]) List of stages to be run.
59
+ # @return [200] The job was created and successfully queued.
60
+ # @return [400] The job definition was invalid.
61
+ # @return [404] The organization does not exist.
62
+ # @example Create a simple job in the 'example' organization with no secrets or ad-hoc stages
63
+ # POST /organizations/example/jobs <= {"name": "example",
64
+ # "environment" : {
65
+ # "os" : "ubuntu_trusty"
66
+ # },
67
+ # "sequence": [
68
+ # {
69
+ # "stage": "example_stage"
70
+ # }
71
+ # ]}
72
+
73
+ # @!method get_organizations_organization_job
74
+ # @overload GET /organizations/:organization/jobs/:job
75
+ # @param [String] organization Name of the organization.
76
+ # @param [Integer] job Job ID.
77
+ # @macro rest
78
+ # Get the complete JobRecord for the given job ID.
79
+ # @return The job record for the job ID.
80
+ # @return [404] The organization or job record does not exist.
81
+
82
+ # @!method get_organizations_organization_job_status
83
+ # @overload GET /organizations/:organization/jobs/:job/status
84
+ # @param [String] organization Name of the organization.
85
+ # @param [Integer] job Job ID.
86
+ # @macro rest
87
+ # Get the current status of the given job ID.
88
+ # @return The current job status for the job ID.
89
+ # @return [404] The organization or job record does not exist.
90
+
91
+ # @!method get_organizations_organization_job_log
92
+ # @overload GET /organizations/:organization/jobs/:job/log
93
+ # @param [String] organization Name of the organization.
94
+ # @param [Integer] job Job ID.
95
+ # @macro rest
96
+ # Get the current complete log of the given job ID.
97
+ # @return The job log for the job ID.
98
+ # @return [404] The organization or job record does not exist.
99
+
100
+ # @!endgroup
101
+ # rubocop:enable Metrics/LineLength
102
+
103
+ # Sinatra callback
104
+ # @private
105
+ def self.registered(app)
106
+ include Errors::HTTPErrors
107
+ include Constants::JobStatus
108
+
109
+ # Return a list of jobs
110
+ app.get do
111
+ authorized_for!(params[:name], Operations::READ)
112
+
113
+ org = Organization.find_by(name: params[:name])
114
+ halt_with_json_response(404, INVALID_ORG, 'organization does not exist') \
115
+ if org.nil?
116
+
117
+ # Get any search terms that we'll need to find the appropriate jobs
118
+ search = {}
119
+ search[:job_name] = URI.decode(params[:s_name]) if params[:s_name]
120
+ search[:status] = params[:s_status] if params[:s_status]
121
+
122
+ # search_from & search_to should be some parsable format
123
+ if params[:s_from] and params[:s_to]
124
+ from = Time.parse(params[:s_from])
125
+ to = Time.parse(params[:s_to])
126
+
127
+ # ActiveRecord understands a range
128
+ search[:started] = from..to
129
+ end
130
+
131
+ Cyclid.logger.debug "search=#{search.inspect}"
132
+
133
+ # Find the number of matching jobs
134
+ count = if search.empty?
135
+ org.job_records.count
136
+ else
137
+ org.job_records
138
+ .where(search)
139
+ .count
140
+ end
141
+
142
+ Cyclid.logger.debug "count=#{count}"
143
+
144
+ stats_only = params[:stats_only] || false
145
+ limit = (params[:limit] || 100).to_i
146
+ offset = (params[:offset] || 0).to_i
147
+
148
+ job_data = { 'total' => count,
149
+ 'offset' => offset,
150
+ 'limit' => limit }
151
+
152
+ unless stats_only
153
+ # Get the available job records, but be terse with the
154
+ # information returned; there is no need to return a full job log
155
+ # with every job, for example.
156
+ job_records = if search.empty?
157
+ org.job_records
158
+ .all
159
+ .select('id, job_name, job_version, started, ended, status')
160
+ .offset(offset)
161
+ .limit(limit)
162
+ else
163
+ org.job_records
164
+ .where(search)
165
+ .select('id, job_name, job_version, started, ended, status')
166
+ .offset(offset)
167
+ .limit(limit)
168
+ end
169
+
170
+ job_data['records'] = job_records
171
+ end
172
+
173
+ return job_data.to_json
174
+ end
175
+
176
+ # Create and run a job.
177
+ app.post do
178
+ authorized_for!(params[:name], Operations::WRITE)
179
+
180
+ payload = parse_request_body
181
+ Cyclid.logger.debug payload
182
+
183
+ halt_with_json_response(400, INVALID_JOB, 'invalid job definition') \
184
+ unless payload.key? 'sequence' and \
185
+ payload.key? 'environment'
186
+
187
+ begin
188
+ job_id = job_from_definition(payload)
189
+ rescue StandardError => ex
190
+ halt_with_json_response(500, INVALID_JOB, "job failed: #{ex}")
191
+ end
192
+
193
+ return { job_id: job_id }.to_json
194
+ end
195
+
196
+ # Get the complete JobRecord for the given job ID.
197
+ app.get '/:id' do
198
+ authorized_for!(params[:name], Operations::READ)
199
+
200
+ org = Organization.find_by(name: params[:name])
201
+ halt_with_json_response(404, INVALID_ORG, 'organization does not exist') \
202
+ if org.nil?
203
+
204
+ begin
205
+ job_record = org.job_records.find(params[:id])
206
+ halt_with_json_response(404, INVALID_JOB, 'job does not exist') \
207
+ if job_record.nil?
208
+ rescue StandardError
209
+ halt_with_json_response(404, INVALID_JOB, 'job does not exist')
210
+ end
211
+
212
+ job = job_record.serializable_hash
213
+ job[:job_id] = job.delete :id
214
+
215
+ # XXX The "job" itself is a serialised internal representation and
216
+ # probably not very useful to the user, so we might want to process
217
+ # it into something more helpful here.
218
+ return job.to_json
219
+ end
220
+
221
+ # Get the current status of the given job ID.
222
+ app.get '/:id/status' do
223
+ authorized_for!(params[:name], Operations::READ)
224
+
225
+ org = Organization.find_by(name: params[:name])
226
+ halt_with_json_response(404, INVALID_ORG, 'organization does not exist') \
227
+ if org.nil?
228
+
229
+ job_record = org.job_records.find(params[:id])
230
+ halt_with_json_response(404, INVALID_JOB, 'job does not exist') \
231
+ if job_record.nil?
232
+
233
+ hash = {}
234
+ hash[:job_id] = job_record.id
235
+ hash[:status] = job_record.status
236
+
237
+ return hash.to_json
238
+ end
239
+
240
+ # Get the current complete log of the given job ID.
241
+ app.get '/:id/log' do
242
+ authorized_for!(params[:name], Operations::READ)
243
+
244
+ org = Organization.find_by(name: params[:name])
245
+ halt_with_json_response(404, INVALID_ORG, 'organization does not exist') \
246
+ if org.nil?
247
+
248
+ job_record = org.job_records.find(params[:id])
249
+ halt_with_json_response(404, INVALID_JOB, 'job does not exist') \
250
+ if job_record.nil?
251
+
252
+ hash = {}
253
+ hash[:job_id] = job_record.id
254
+ hash[:log] = job_record.log
255
+
256
+ return hash.to_json
257
+ end
258
+
259
+ app.helpers do
260
+ include Job::Helpers
261
+ end
262
+ end
263
+ end
264
+ end
265
+ end
266
+ end