Merge branch 'master' into 14873-api-rails5-upgrade
[arvados.git] / services / api / app / controllers / arvados / v1 / schema_controller.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 class Arvados::V1::SchemaController < ApplicationController
6   skip_before_action :catch_redirect_hint
7   skip_before_action :find_objects_for_index
8   skip_before_action :find_object_by_uuid
9   skip_before_action :load_filters_param
10   skip_before_action :load_limit_offset_order_params
11   skip_before_action :load_read_auths
12   skip_before_action :load_where_param
13   skip_before_action :render_404_if_no_object
14   skip_before_action :require_auth_scope
15
16   include DbCurrentTime
17
18   def index
19     expires_in 24.hours, public: true
20     send_json discovery_doc
21   end
22
23   protected
24
25   def discovery_doc
26     Rails.cache.fetch 'arvados_v1_rest_discovery' do
27       Rails.application.eager_load!
28       discovery = {
29         kind: "discovery#restDescription",
30         discoveryVersion: "v1",
31         id: "arvados:v1",
32         name: "arvados",
33         version: "v1",
34         revision: "20131114",
35         source_version: AppVersion.hash,
36         sourceVersion: AppVersion.hash, # source_version should be deprecated in the future
37         packageVersion: AppVersion.package_version,
38         generatedAt: db_current_time.iso8601,
39         title: "Arvados API",
40         description: "The API to interact with Arvados.",
41         documentationLink: "http://doc.arvados.org/api/index.html",
42         defaultCollectionReplication: Rails.configuration.default_collection_replication,
43         protocol: "rest",
44         baseUrl: root_url + "arvados/v1/",
45         basePath: "/arvados/v1/",
46         rootUrl: root_url,
47         servicePath: "arvados/v1/",
48         batchPath: "batch",
49         uuidPrefix: Rails.application.config.uuid_prefix,
50         defaultTrashLifetime: Rails.application.config.default_trash_lifetime,
51         blobSignatureTtl: Rails.application.config.blob_signature_ttl,
52         maxRequestSize: Rails.application.config.max_request_size,
53         maxItemsPerResponse: Rails.application.config.max_items_per_response,
54         dockerImageFormats: Rails.application.config.docker_image_formats,
55         crunchLogBytesPerEvent: Rails.application.config.crunch_log_bytes_per_event,
56         crunchLogSecondsBetweenEvents: Rails.application.config.crunch_log_seconds_between_events,
57         crunchLogThrottlePeriod: Rails.application.config.crunch_log_throttle_period,
58         crunchLogThrottleBytes: Rails.application.config.crunch_log_throttle_bytes,
59         crunchLogThrottleLines: Rails.application.config.crunch_log_throttle_lines,
60         crunchLimitLogBytesPerJob: Rails.application.config.crunch_limit_log_bytes_per_job,
61         crunchLogPartialLineThrottlePeriod: Rails.application.config.crunch_log_partial_line_throttle_period,
62         crunchLogUpdatePeriod: Rails.application.config.crunch_log_update_period,
63         crunchLogUpdateSize: Rails.application.config.crunch_log_update_size,
64         remoteHosts: Rails.configuration.remote_hosts,
65         remoteHostsViaDNS: Rails.configuration.remote_hosts_via_dns,
66         websocketUrl: Rails.application.config.websocket_address,
67         workbenchUrl: Rails.application.config.workbench_address,
68         keepWebServiceUrl: Rails.application.config.keep_web_service_url,
69         gitUrl: case Rails.application.config.git_repo_https_base
70                 when false
71                   ''
72                 when true
73                   'https://git.%s.arvadosapi.com/' % Rails.configuration.uuid_prefix
74                 else
75                   Rails.application.config.git_repo_https_base
76                 end,
77         parameters: {
78           alt: {
79             type: "string",
80             description: "Data format for the response.",
81             default: "json",
82             enum: [
83                    "json"
84                   ],
85             enumDescriptions: [
86                                "Responses with Content-Type of application/json"
87                               ],
88             location: "query"
89           },
90           fields: {
91             type: "string",
92             description: "Selector specifying which fields to include in a partial response.",
93             location: "query"
94           },
95           key: {
96             type: "string",
97             description: "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.",
98             location: "query"
99           },
100           oauth_token: {
101             type: "string",
102             description: "OAuth 2.0 token for the current user.",
103             location: "query"
104           }
105         },
106         auth: {
107           oauth2: {
108             scopes: {
109               "https://api.curoverse.com/auth/arvados" => {
110                 description: "View and manage objects"
111               },
112               "https://api.curoverse.com/auth/arvados.readonly" => {
113                 description: "View objects"
114               }
115             }
116           }
117         },
118         schemas: {},
119         resources: {}
120       }
121
122       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
123         begin
124           ctl_class = "Arvados::V1::#{k.to_s.pluralize}Controller".constantize
125         rescue
126           # No controller -> no discovery.
127           next
128         end
129         object_properties = {}
130         k.columns.
131           select { |col| col.name != 'id' && !col.name.start_with?('secret_') }.
132           collect do |col|
133           if k.serialized_attributes.has_key? col.name
134             object_properties[col.name] = {
135               type: k.serialized_attributes[col.name].object_class.to_s
136             }
137           else
138             object_properties[col.name] = {
139               type: col.type
140             }
141           end
142         end
143         discovery[:schemas][k.to_s + 'List'] = {
144           id: k.to_s + 'List',
145           description: k.to_s + ' list',
146           type: "object",
147           properties: {
148             kind: {
149               type: "string",
150               description: "Object type. Always arvados##{k.to_s.camelcase(:lower)}List.",
151               default: "arvados##{k.to_s.camelcase(:lower)}List"
152             },
153             etag: {
154               type: "string",
155               description: "List version."
156             },
157             items: {
158               type: "array",
159               description: "The list of #{k.to_s.pluralize}.",
160               items: {
161                 "$ref" => k.to_s
162               }
163             },
164             next_link: {
165               type: "string",
166               description: "A link to the next page of #{k.to_s.pluralize}."
167             },
168             next_page_token: {
169               type: "string",
170               description: "The page token for the next page of #{k.to_s.pluralize}."
171             },
172             selfLink: {
173               type: "string",
174               description: "A link back to this list."
175             }
176           }
177         }
178         discovery[:schemas][k.to_s] = {
179           id: k.to_s,
180           description: k.to_s,
181           type: "object",
182           uuidPrefix: (k.respond_to?(:uuid_prefix) ? k.uuid_prefix : nil),
183           properties: {
184             uuid: {
185               type: "string",
186               description: "Object ID."
187             },
188             etag: {
189               type: "string",
190               description: "Object version."
191             }
192           }.merge(object_properties)
193         }
194         discovery[:resources][k.to_s.underscore.pluralize] = {
195           methods: {
196             get: {
197               id: "arvados.#{k.to_s.underscore.pluralize}.get",
198               path: "#{k.to_s.underscore.pluralize}/{uuid}",
199               httpMethod: "GET",
200               description: "Gets a #{k.to_s}'s metadata by UUID.",
201               parameters: {
202                 uuid: {
203                   type: "string",
204                   description: "The UUID of the #{k.to_s} in question.",
205                   required: true,
206                   location: "path"
207                 }
208               },
209               parameterOrder: [
210                                "uuid"
211                               ],
212               response: {
213                 "$ref" => k.to_s
214               },
215               scopes: [
216                        "https://api.curoverse.com/auth/arvados",
217                        "https://api.curoverse.com/auth/arvados.readonly"
218                       ]
219             },
220             index: {
221               id: "arvados.#{k.to_s.underscore.pluralize}.index",
222               path: k.to_s.underscore.pluralize,
223               httpMethod: "GET",
224               description:
225                  %|Index #{k.to_s.pluralize}.
226
227                    The <code>index</code> method returns a
228                    <a href="/api/resources.html">resource list</a> of
229                    matching #{k.to_s.pluralize}. For example:
230
231                    <pre>
232                    {
233                     "kind":"arvados##{k.to_s.camelcase(:lower)}List",
234                     "etag":"",
235                     "self_link":"",
236                     "next_page_token":"",
237                     "next_link":"",
238                     "items":[
239                        ...
240                     ],
241                     "items_available":745,
242                     "_profile":{
243                      "request_time":0.157236317
244                     }
245                     </pre>|,
246               parameters: {
247               },
248               response: {
249                 "$ref" => "#{k.to_s}List"
250               },
251               scopes: [
252                        "https://api.curoverse.com/auth/arvados",
253                        "https://api.curoverse.com/auth/arvados.readonly"
254                       ]
255             },
256             create: {
257               id: "arvados.#{k.to_s.underscore.pluralize}.create",
258               path: "#{k.to_s.underscore.pluralize}",
259               httpMethod: "POST",
260               description: "Create a new #{k.to_s}.",
261               parameters: {},
262               request: {
263                 required: true,
264                 properties: {
265                   k.to_s.underscore => {
266                     "$ref" => k.to_s
267                   }
268                 }
269               },
270               response: {
271                 "$ref" => k.to_s
272               },
273               scopes: [
274                        "https://api.curoverse.com/auth/arvados"
275                       ]
276             },
277             update: {
278               id: "arvados.#{k.to_s.underscore.pluralize}.update",
279               path: "#{k.to_s.underscore.pluralize}/{uuid}",
280               httpMethod: "PUT",
281               description: "Update attributes of an existing #{k.to_s}.",
282               parameters: {
283                 uuid: {
284                   type: "string",
285                   description: "The UUID of the #{k.to_s} in question.",
286                   required: true,
287                   location: "path"
288                 }
289               },
290               request: {
291                 required: true,
292                 properties: {
293                   k.to_s.underscore => {
294                     "$ref" => k.to_s
295                   }
296                 }
297               },
298               response: {
299                 "$ref" => k.to_s
300               },
301               scopes: [
302                        "https://api.curoverse.com/auth/arvados"
303                       ]
304             },
305             delete: {
306               id: "arvados.#{k.to_s.underscore.pluralize}.delete",
307               path: "#{k.to_s.underscore.pluralize}/{uuid}",
308               httpMethod: "DELETE",
309               description: "Delete an existing #{k.to_s}.",
310               parameters: {
311                 uuid: {
312                   type: "string",
313                   description: "The UUID of the #{k.to_s} in question.",
314                   required: true,
315                   location: "path"
316                 }
317               },
318               response: {
319                 "$ref" => k.to_s
320               },
321               scopes: [
322                        "https://api.curoverse.com/auth/arvados"
323                       ]
324             }
325           }
326         }
327         # Check for Rails routes that don't match the usual actions
328         # listed above
329         d_methods = discovery[:resources][k.to_s.underscore.pluralize][:methods]
330         Rails.application.routes.routes.each do |route|
331           action = route.defaults[:action]
332           httpMethod = ['GET', 'POST', 'PUT', 'DELETE'].map { |method|
333             method if route.verb.match(method)
334           }.compact.first
335           if httpMethod and
336               route.defaults[:controller] == 'arvados/v1/' + k.to_s.underscore.pluralize and
337               ctl_class.action_methods.include? action
338             if !d_methods[action.to_sym]
339               method = {
340                 id: "arvados.#{k.to_s.underscore.pluralize}.#{action}",
341                 path: route.path.spec.to_s.sub('/arvados/v1/','').sub('(.:format)','').sub(/:(uu)?id/,'{uuid}'),
342                 httpMethod: httpMethod,
343                 description: "#{action} #{k.to_s.underscore.pluralize}",
344                 parameters: {},
345                 response: {
346                   "$ref" => (action == 'index' ? "#{k.to_s}List" : k.to_s)
347                 },
348                 scopes: [
349                          "https://api.curoverse.com/auth/arvados"
350                         ]
351               }
352               route.segment_keys.each do |key|
353                 if key != :format
354                   key = :uuid if key == :id
355                   method[:parameters][key] = {
356                     type: "string",
357                     description: "",
358                     required: true,
359                     location: "path"
360                   }
361                 end
362               end
363             else
364               # We already built a generic method description, but we
365               # might find some more required parameters through
366               # introspection.
367               method = d_methods[action.to_sym]
368             end
369             if ctl_class.respond_to? "_#{action}_requires_parameters".to_sym
370               ctl_class.send("_#{action}_requires_parameters".to_sym).each do |l, v|
371                 if v.is_a? Hash
372                   method[:parameters][l] = v
373                 else
374                   method[:parameters][l] = {}
375                 end
376                 if !method[:parameters][l][:default].nil?
377                   # The JAVA SDK is sensitive to all values being strings
378                   method[:parameters][l][:default] = method[:parameters][l][:default].to_s
379                 end
380                 method[:parameters][l][:type] ||= 'string'
381                 method[:parameters][l][:description] ||= ''
382                 method[:parameters][l][:location] = (route.segment_keys.include?(l) ? 'path' : 'query')
383                 if method[:parameters][l][:required].nil?
384                   method[:parameters][l][:required] = v != false
385                 end
386               end
387             end
388             d_methods[action.to_sym] = method
389
390             if action == 'index'
391               list_method = method.dup
392               list_method[:id].sub!('index', 'list')
393               list_method[:description].sub!('Index', 'List')
394               list_method[:description].sub!('index', 'list')
395               d_methods[:list] = list_method
396             end
397           end
398         end
399       end
400       Rails.configuration.disable_api_methods.each do |method|
401         ctrl, action = method.split('.', 2)
402         discovery[:resources][ctrl][:methods].delete(action.to_sym)
403       end
404       discovery
405     end
406   end
407 end