21224: merged main to pass int tests
[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_select_param
12   skip_before_action :load_read_auths
13   skip_before_action :load_where_param
14   skip_before_action :render_404_if_no_object
15   skip_before_action :require_auth_scope
16
17   include DbCurrentTime
18
19   def index
20     expires_in 24.hours, public: true
21     send_json discovery_doc
22   end
23
24   protected
25
26   def discovery_doc
27     Rails.application.eager_load!
28     remoteHosts = {}
29     Rails.configuration.RemoteClusters.each {|k,v| if k != :"*" then remoteHosts[k] = v["Host"] end }
30     discovery = {
31       kind: "discovery#restDescription",
32       discoveryVersion: "v1",
33       id: "arvados:v1",
34       name: "arvados",
35       version: "v1",
36       # format is YYYYMMDD, must be fixed width (needs to be lexically
37       # sortable), updated manually, may be used by clients to
38       # determine availability of API server features.
39       revision: "20231117",
40       source_version: AppVersion.hash,
41       sourceVersion: AppVersion.hash, # source_version should be deprecated in the future
42       packageVersion: AppVersion.package_version,
43       generatedAt: db_current_time.iso8601,
44       title: "Arvados API",
45       description: "The API to interact with Arvados.",
46       documentationLink: "http://doc.arvados.org/api/index.html",
47       defaultCollectionReplication: Rails.configuration.Collections.DefaultReplication,
48       protocol: "rest",
49       baseUrl: root_url + "arvados/v1/",
50       basePath: "/arvados/v1/",
51       rootUrl: root_url,
52       servicePath: "arvados/v1/",
53       batchPath: "batch",
54       uuidPrefix: Rails.configuration.ClusterID,
55       defaultTrashLifetime: Rails.configuration.Collections.DefaultTrashLifetime,
56       blobSignatureTtl: Rails.configuration.Collections.BlobSigningTTL,
57       maxRequestSize: Rails.configuration.API.MaxRequestSize,
58       maxItemsPerResponse: Rails.configuration.API.MaxItemsPerResponse,
59       dockerImageFormats: Rails.configuration.Containers.SupportedDockerImageFormats.keys,
60       crunchLogUpdatePeriod: Rails.configuration.Containers.Logging.LogUpdatePeriod,
61       crunchLogUpdateSize: Rails.configuration.Containers.Logging.LogUpdateSize,
62       remoteHosts: remoteHosts,
63       remoteHostsViaDNS: Rails.configuration.RemoteClusters["*"].Proxy,
64       websocketUrl: Rails.configuration.Services.Websocket.ExternalURL.to_s,
65       workbenchUrl: Rails.configuration.Services.Workbench1.ExternalURL.to_s,
66       workbench2Url: Rails.configuration.Services.Workbench2.ExternalURL.to_s,
67       keepWebServiceUrl: Rails.configuration.Services.WebDAV.ExternalURL.to_s,
68       parameters: {
69         alt: {
70           type: "string",
71           description: "Data format for the response.",
72           default: "json",
73           enum: [
74             "json"
75           ],
76           enumDescriptions: [
77             "Responses with Content-Type of application/json"
78           ],
79           location: "query"
80         },
81         fields: {
82           type: "string",
83           description: "Selector specifying which fields to include in a partial response.",
84           location: "query"
85         },
86         key: {
87           type: "string",
88           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.",
89           location: "query"
90         },
91         oauth_token: {
92           type: "string",
93           description: "OAuth 2.0 token for the current user.",
94           location: "query"
95         }
96       },
97       auth: {
98         oauth2: {
99           scopes: {
100             "https://api.arvados.org/auth/arvados" => {
101               description: "View and manage objects"
102             },
103             "https://api.arvados.org/auth/arvados.readonly" => {
104               description: "View objects"
105             }
106           }
107         }
108       },
109       schemas: {},
110       resources: {}
111     }
112
113     ActiveRecord::Base.descendants.reject(&:abstract_class?).sort_by(&:to_s).each do |k|
114       begin
115         ctl_class = "Arvados::V1::#{k.to_s.pluralize}Controller".constantize
116       rescue
117         # No controller -> no discovery.
118         next
119       end
120       object_properties = {}
121       k.columns.
122         select { |col| k.selectable_attributes.include? col.name }.
123         collect do |col|
124         if k.serialized_attributes.has_key? col.name
125           object_properties[col.name] = {
126             type: k.serialized_attributes[col.name].object_class.to_s
127           }
128         elsif k.attribute_types[col.name].is_a? JsonbType::Hash
129           object_properties[col.name] = {
130             type: Hash.to_s
131           }
132         elsif k.attribute_types[col.name].is_a? JsonbType::Array
133           object_properties[col.name] = {
134             type: Array.to_s
135           }
136         else
137           object_properties[col.name] = {
138             type: col.type
139           }
140         end
141       end
142       discovery[:schemas][k.to_s + 'List'] = {
143         id: k.to_s + 'List',
144         description: k.to_s + ' list',
145         type: "object",
146         properties: {
147           kind: {
148             type: "string",
149             description: "Object type. Always arvados##{k.to_s.camelcase(:lower)}List.",
150             default: "arvados##{k.to_s.camelcase(:lower)}List"
151           },
152           etag: {
153             type: "string",
154             description: "List version."
155           },
156           items: {
157             type: "array",
158             description: "The list of #{k.to_s.pluralize}.",
159             items: {
160               "$ref" => k.to_s
161             }
162           },
163           next_link: {
164             type: "string",
165             description: "A link to the next page of #{k.to_s.pluralize}."
166           },
167           next_page_token: {
168             type: "string",
169             description: "The page token for the next page of #{k.to_s.pluralize}."
170           },
171           selfLink: {
172             type: "string",
173             description: "A link back to this list."
174           }
175         }
176       }
177       discovery[:schemas][k.to_s] = {
178         id: k.to_s,
179         description: k.to_s,
180         type: "object",
181         uuidPrefix: (k.respond_to?(:uuid_prefix) ? k.uuid_prefix : nil),
182         properties: {
183           uuid: {
184             type: "string",
185             description: "Object ID."
186           },
187           etag: {
188             type: "string",
189             description: "Object version."
190           }
191         }.merge(object_properties)
192       }
193       discovery[:resources][k.to_s.underscore.pluralize] = {
194         methods: {
195           get: {
196             id: "arvados.#{k.to_s.underscore.pluralize}.get",
197             path: "#{k.to_s.underscore.pluralize}/{uuid}",
198             httpMethod: "GET",
199             description: "Gets a #{k.to_s}'s metadata by UUID.",
200             parameters: {
201               uuid: {
202                 type: "string",
203                 description: "The UUID of the #{k.to_s} in question.",
204                 required: true,
205                 location: "path"
206               }
207             },
208             parameterOrder: [
209               "uuid"
210             ],
211             response: {
212               "$ref" => k.to_s
213             },
214             scopes: [
215               "https://api.arvados.org/auth/arvados",
216               "https://api.arvados.org/auth/arvados.readonly"
217             ]
218           },
219           index: {
220             id: "arvados.#{k.to_s.underscore.pluralize}.index",
221             path: k.to_s.underscore.pluralize,
222             httpMethod: "GET",
223             description:
224               %|Index #{k.to_s.pluralize}.
225
226                    The <code>index</code> method returns a
227                    <a href="/api/resources.html">resource list</a> of
228                    matching #{k.to_s.pluralize}. For example:
229
230                    <pre>
231                    {
232                     "kind":"arvados##{k.to_s.camelcase(:lower)}List",
233                     "etag":"",
234                     "self_link":"",
235                     "next_page_token":"",
236                     "next_link":"",
237                     "items":[
238                        ...
239                     ],
240                     "items_available":745,
241                     "_profile":{
242                      "request_time":0.157236317
243                     }
244                     </pre>|,
245             parameters: {
246             },
247             response: {
248               "$ref" => "#{k.to_s}List"
249             },
250             scopes: [
251               "https://api.arvados.org/auth/arvados",
252               "https://api.arvados.org/auth/arvados.readonly"
253             ]
254           },
255           create: {
256             id: "arvados.#{k.to_s.underscore.pluralize}.create",
257             path: "#{k.to_s.underscore.pluralize}",
258             httpMethod: "POST",
259             description: "Create a new #{k.to_s}.",
260             parameters: {},
261             request: {
262               required: true,
263               properties: {
264                 k.to_s.underscore => {
265                   "$ref" => k.to_s
266                 }
267               }
268             },
269             response: {
270               "$ref" => k.to_s
271             },
272             scopes: [
273               "https://api.arvados.org/auth/arvados"
274             ]
275           },
276           update: {
277             id: "arvados.#{k.to_s.underscore.pluralize}.update",
278             path: "#{k.to_s.underscore.pluralize}/{uuid}",
279             httpMethod: "PUT",
280             description: "Update attributes of an existing #{k.to_s}.",
281             parameters: {
282               uuid: {
283                 type: "string",
284                 description: "The UUID of the #{k.to_s} in question.",
285                 required: true,
286                 location: "path"
287               }
288             },
289             request: {
290               required: true,
291               properties: {
292                 k.to_s.underscore => {
293                   "$ref" => k.to_s
294                 }
295               }
296             },
297             response: {
298               "$ref" => k.to_s
299             },
300             scopes: [
301               "https://api.arvados.org/auth/arvados"
302             ]
303           },
304           delete: {
305             id: "arvados.#{k.to_s.underscore.pluralize}.delete",
306             path: "#{k.to_s.underscore.pluralize}/{uuid}",
307             httpMethod: "DELETE",
308             description: "Delete an existing #{k.to_s}.",
309             parameters: {
310               uuid: {
311                 type: "string",
312                 description: "The UUID of the #{k.to_s} in question.",
313                 required: true,
314                 location: "path"
315               }
316             },
317             response: {
318               "$ref" => k.to_s
319             },
320             scopes: [
321               "https://api.arvados.org/auth/arvados"
322             ]
323           }
324         }
325       }
326       # Check for Rails routes that don't match the usual actions
327       # listed above
328       d_methods = discovery[:resources][k.to_s.underscore.pluralize][:methods]
329       Rails.application.routes.routes.each do |route|
330         action = route.defaults[:action]
331         httpMethod = ['GET', 'POST', 'PUT', 'DELETE'].map { |method|
332           method if route.verb.match(method)
333         }.compact.first
334         if httpMethod and
335           route.defaults[:controller] == 'arvados/v1/' + k.to_s.underscore.pluralize and
336           ctl_class.action_methods.include? action
337           if !d_methods[action.to_sym]
338             method = {
339               id: "arvados.#{k.to_s.underscore.pluralize}.#{action}",
340               path: route.path.spec.to_s.sub('/arvados/v1/','').sub('(.:format)','').sub(/:(uu)?id/,'{uuid}'),
341               httpMethod: httpMethod,
342               description: "#{action} #{k.to_s.underscore.pluralize}",
343               parameters: {},
344               response: {
345                 "$ref" => (action == 'index' ? "#{k.to_s}List" : k.to_s)
346               },
347               scopes: [
348                 "https://api.arvados.org/auth/arvados"
349               ]
350             }
351             route.segment_keys.each do |key|
352               if key != :format
353                 key = :uuid if key == :id
354                 method[:parameters][key] = {
355                   type: "string",
356                   description: "",
357                   required: true,
358                   location: "path"
359                 }
360               end
361             end
362           else
363             # We already built a generic method description, but we
364             # might find some more required parameters through
365             # introspection.
366             method = d_methods[action.to_sym]
367           end
368           if ctl_class.respond_to? "_#{action}_requires_parameters".to_sym
369             ctl_class.send("_#{action}_requires_parameters".to_sym).each do |l, v|
370               if v.is_a? Hash
371                 method[:parameters][l] = v
372               else
373                 method[:parameters][l] = {}
374               end
375               if !method[:parameters][l][:default].nil?
376                 # The JAVA SDK is sensitive to all values being strings
377                 method[:parameters][l][:default] = method[:parameters][l][:default].to_s
378               end
379               method[:parameters][l][:type] ||= 'string'
380               method[:parameters][l][:description] ||= ''
381               method[:parameters][l][:location] = (route.segment_keys.include?(l) ? 'path' : 'query')
382               if method[:parameters][l][:required].nil?
383                 method[:parameters][l][:required] = v != false
384               end
385             end
386           end
387           d_methods[action.to_sym] = method
388
389           if action == 'index'
390             list_method = method.dup
391             list_method[:id].sub!('index', 'list')
392             list_method[:description].sub!('Index', 'List')
393             list_method[:description].sub!('index', 'list')
394             d_methods[:list] = list_method
395           end
396         end
397       end
398     end
399
400     # The 'replace_files' option is implemented in lib/controller,
401     # not Rails -- we just need to add it here so discovery-aware
402     # clients know how to validate it.
403     [:create, :update].each do |action|
404       discovery[:resources]['collections'][:methods][action][:parameters]['replace_files'] = {
405         type: 'object',
406         description: 'Files and directories to initialize/replace with content from other collections.',
407         required: false,
408         location: 'query',
409         properties: {},
410         additionalProperties: {type: 'string'},
411       }
412     end
413
414     discovery[:resources]['configs'] = {
415       methods: {
416         get: {
417           id: "arvados.configs.get",
418           path: "config",
419           httpMethod: "GET",
420           description: "Get public config",
421           parameters: {
422           },
423           parameterOrder: [
424           ],
425           response: {
426           },
427           scopes: [
428             "https://api.arvados.org/auth/arvados",
429             "https://api.arvados.org/auth/arvados.readonly"
430           ]
431         },
432       }
433     }
434
435     discovery[:resources]['vocabularies'] = {
436       methods: {
437         get: {
438           id: "arvados.vocabularies.get",
439           path: "vocabulary",
440           httpMethod: "GET",
441           description: "Get vocabulary definition",
442           parameters: {
443           },
444           parameterOrder: [
445           ],
446           response: {
447           },
448           scopes: [
449             "https://api.arvados.org/auth/arvados",
450             "https://api.arvados.org/auth/arvados.readonly"
451           ]
452         },
453       }
454     }
455
456     discovery[:resources]['sys'] = {
457       methods: {
458         get: {
459           id: "arvados.sys.trash_sweep",
460           path: "sys/trash_sweep",
461           httpMethod: "POST",
462           description: "apply scheduled trash and delete operations",
463           parameters: {
464           },
465           parameterOrder: [
466           ],
467           response: {
468           },
469           scopes: [
470             "https://api.arvados.org/auth/arvados",
471             "https://api.arvados.org/auth/arvados.readonly"
472           ]
473         },
474       }
475     }
476
477     Rails.configuration.API.DisabledAPIs.each do |method, _|
478       ctrl, action = method.to_s.split('.', 2)
479       next if ctrl.in?(['job_tasks', 'jobs', 'keep_disks', 'nodes', 'pipeline_instances', 'pipeline_templates', 'repositories'])
480       discovery[:resources][ctrl][:methods].delete(action.to_sym)
481     end
482     discovery
483   end
484 end