cyclid 0.2.0

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.
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