19929: Remove long-gone List properties from the discovery document
[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   ActionNameMap = {
27     'destroy' => 'delete',
28     'index' => 'list',
29     'show' => 'get',
30   }
31
32   HttpMethodDescriptionMap = {
33     "DELETE" => "delete",
34     "GET" => "query",
35     "POST" => "update",
36     "PUT" => "create",
37   }
38
39   ModelHumanNameMap = {
40     # The discovery document has code to humanize most model names.
41     # These are exceptions that require some capitalization.
42     "ApiClientAuthorization" => "API client authorization",
43     "KeepService" => "Keep service",
44   }
45
46   SchemaDescriptionMap = {
47     # This hash contains descriptions for everything in the schema.
48     # Schemas are looked up by their model name.
49     # Schema properties are looked up by "{model_name}.{property_name}"
50     # and fall back to just the property name if that doesn't exist.
51     "ApiClientAuthorization" => "Arvados API client authorization token
52
53 This resource represents an API token a user may use to authenticate an
54 Arvados API request.",
55     "AuthorizedKey" => "Arvados authorized public key
56
57 This resource represents a public key a user may use to authenticate themselves
58 to services on the cluster. Its primary use today is to store SSH keys for
59 virtual machines (\"shell nodes\"). It may be extended to store other keys in
60 the future.",
61     "Collection" => "Arvados data collection
62
63 A collection describes how a set of files is stored in data blocks in Keep,
64 along with associated metadata.",
65     "ComputedPermission" => "Arvados computed permission
66
67 Computed permissions do not correspond directly to any Arvados resource, but
68 provide a simple way to query the entire graph of permissions granted to
69 users and groups.",
70     "ContainerRequest" => "Arvados container request
71
72 A container request represents a user's request that Arvados do some compute
73 work, along with full details about what work should be done. Arvados will
74 attempt to fulfill the request by mapping it to a matching container record,
75 running the work on demand if necessary.",
76     "Container" => "Arvados container record
77
78 A container represents compute work that has been or should be dispatched,
79 along with its results. A container can satisfy one or more container requests.",
80     "Group" => "Arvados group
81
82 Groups provide a way to organize users or data together, depending on their
83 `group_class`.",
84     "KeepService" => "Arvados Keep service
85
86 This resource stores information about a single Keep service in this Arvados
87 cluster that clients can contact to retrieve and store data.",
88     "Link" => "Arvados object link
89
90 A link provides a way to define relationships between Arvados objects,
91 depending on their `link_class`.",
92     "Log" => "Arvados log record
93
94 This resource represents a single log record about an event in this Arvados
95 cluster. Some individual Arvados services create log records. Users can also
96 create custom logs.",
97     "UserAgreement" => "Arvados user agreement
98
99 A user agreement is a collection with terms that users must agree to before
100 they can use this Arvados cluster.",
101     "User" => "Arvados user
102
103 A user represents a single individual or role who may be authorized to access
104 this Arvados cluster.",
105     "VirtualMachine" => "Arvados virtual machine (\"shell node\")
106
107 This resource stores information about a virtual machine or \"shell node\"
108 hosted on this Arvados cluster where users can log in and use preconfigured
109 Arvados client tools.",
110     "Workflow" => "Arvados workflow
111
112 A workflow contains workflow definition source code that Arvados can execute
113 along with associated metadata for users.",
114
115     # This section contains:
116     # * attributes shared across most resources
117     # * attributes shared across Collections and UserAgreements
118     # * attributes shared across Containers and ContainerRequests
119     "command" =>
120     "An array of strings that defines the command that the dispatcher should
121 execute inside this container.",
122     "container_image" =>
123     "The portable data hash of the Arvados collection that contains the image
124 to use for this container.",
125     "created_at" => "The time this %s was created.",
126     "current_version_uuid" => "The UUID of the current version of this %s.",
127     "cwd" =>
128     "A string that the defines the working directory that the dispatcher should
129 use when it executes the command inside this container.",
130     "delete_at" => "The time this %s will be permanently deleted.",
131     "description" => "A longer description of this %s assigned by a user.",
132     "environment" =>
133     "A hash of string keys and values that defines the environment variables
134 for the dispatcher to set when it executes this container.",
135     "file_count" =>
136     "The number of files represented in this %s's `manifest_text`.
137 This attribute is read-only.",
138     "file_size_total" =>
139     "The total size in bytes of files represented in this %s's `manifest_text`.
140 This attribute is read-only.",
141     "is_trashed" => "A boolean flag to indicate whether or not this %s is trashed.",
142     "manifest_text" =>
143     "The manifest text that describes how files are constructed from data blocks
144 in this %s. Refer to the [manifest format][] reference for details.
145
146 [manifest format]: https://doc.arvados.org/architecture/manifest-format.html
147
148 ",
149     "modified_at" => "The time this %s was last updated.",
150     "modified_by_user_uuid" => "The UUID of the user that last updated this %s.",
151     "mounts" =>
152     "A hash where each key names a directory inside this container, and its
153 value is an object that defines the mount source for that directory. Refer
154 to the [mount types reference][] for details.
155
156 [mount types reference]: https://doc.arvados.org/api/methods/containers.html#mount_types
157
158 ",
159     "name" => "The name of this %s assigned by a user.",
160     "output_glob" =>
161     "An array of strings of shell-style glob patterns that define which file(s)
162 and subdirectory(ies) under the `output_path` directory should be recorded in
163 the container's final output. Refer to the [glob patterns reference][] for details.
164
165 [glob patterns reference]: https://doc.arvados.org/api/methods/containers.html#glob_patterns
166
167 ",
168     "output_path" =>
169     "A string that defines the file or directory path where the command
170 writes output that should be saved from this container.",
171     "output_properties" =>
172 "A hash of arbitrary metadata to set on the output collection of this %s.
173 Some keys may be reserved by Arvados or defined by a configured vocabulary.
174 Refer to the [metadata properties reference][] for details.
175
176 [metadata properties reference]: https://doc.arvados.org/api/properties.html
177
178 ",
179     "output_storage_classes" =>
180     "An array of strings identifying the storage class(es) that should be set
181 on the output collection of this %s. Storage classes are configured by
182 the cluster administrator.",
183     "owner_uuid" => "The UUID of the user or group that owns this %s.",
184     "portable_data_hash" =>
185     "The portable data hash of this %s. This string provides a unique
186 and stable reference to these contents.",
187     "preserve_version" =>
188     "A boolean flag to indicate whether this specific version of this %s
189 should be persisted in cluster storage.",
190     "priority" =>
191     "An integer between 0 and 1000 (inclusive) that represents this %s's
192 scheduling priority. 0 represents a request to be cancelled. Higher
193 values represent higher priority. Refer to the [priority reference][] for details.
194
195 [priority reference]: https://doc.arvados.org/api/methods/container_requests.html#priority
196
197 ",
198     "properties" =>
199     "A hash of arbitrary metadata for this %s.
200 Some keys may be reserved by Arvados or defined by a configured vocabulary.
201 Refer to the [metadata properties reference][] for details.
202
203 [metadata properties reference]: https://doc.arvados.org/api/properties.html
204
205 ",
206     "replication_confirmed" =>
207     "The number of copies of data in this %s that the cluster has confirmed
208 exist in storage.",
209     "replication_confirmed_at" =>
210     "The last time the cluster confirmed that it met `replication_confirmed`
211 for this %s.",
212     "replication_desired" =>
213     "The number of copies that should be made for data in this %s.",
214     "runtime_auth_scopes" =>
215     "The `scopes` from the API client authorization token used to run this %s.",
216     "runtime_constraints" =>
217     "A hash that identifies compute resources this container requires to run
218 successfully. See the [runtime constraints reference][] for details.
219
220 [runtime constraints reference]: https://doc.arvados.org/api/methods/containers.html#runtime_constraints
221
222 ",
223     "runtime_token" =>
224     "The `api_token` from an Arvados API client authorization token that a
225 dispatcher should use to set up this container.",
226     "runtime_user_uuid" =>
227     "The UUID of the Arvados user associated with the API client authorization
228 token used to run this container.",
229     "secret_mounts" =>
230     "A hash like `mounts`, but this attribute is only available through a
231 dedicated API before the container is run.",
232     "scheduling_parameters" =>
233     "A hash of scheduling parameters that should be passed to the underlying
234 dispatcher when this container is run.
235 See the [scheduling parameters reference][] for details.
236
237 [scheduling parameters reference]: https://doc.arvados.org/api/methods/containers.html#scheduling_parameters
238
239 ",
240     "storage_classes_desired" =>
241     "An array of strings identifying the storage class(es) that should be used
242 for data in this %s. Storage classes are configured by the cluster administrator.",
243     "storage_classes_confirmed" =>
244     "An array of strings identifying the storage class(es) the cluster has
245 confirmed have a copy of this %s's data.",
246     "storage_classes_confirmed_at" =>
247     "The last time the cluster confirmed that data was stored on the storage
248 class(es) in `storage_classes_confirmed`.",
249     "trash_at" => "The time this %s will be trashed.",
250
251     "ApiClientAuthorization.api_token" =>
252     "The secret token that can be used to authorize Arvados API requests.",
253     "ApiClientAuthorization.created_by_ip_address" =>
254     "The IP address of the client that created this token.",
255     "ApiClientAuthorization.expires_at" =>
256     "The time after which this token is no longer valid for authorization.",
257     "ApiClientAuthorization.last_used_at" =>
258     "The last time this token was used to authorize a request.",
259     "ApiClientAuthorization.last_used_by_ip_address" =>
260     "The IP address of the client that last used this token.",
261     "ApiClientAuthorization.scopes" =>
262     "An array of strings identifying HTTP methods and API paths this token is
263 authorized to use. Refer to the [scopes reference][] for details.
264
265 [scopes reference]: https://doc.arvados.org/api/tokens.html#scopes
266
267 ",
268     "version" =>
269     "An integer that counts which version of a %s this record
270 represents. Refer to [collection versioning][] for details. This attribute is
271 read-only.
272
273 [collection versioning]: https://doc.arvados.org/user/topics/collection-versioning.html
274
275 ",
276
277     "AuthorizedKey.authorized_user_uuid" =>
278     "The UUID of the Arvados user that is authorized by this key.",
279     "AuthorizedKey.expires_at" =>
280     "The time after which this key is no longer valid for authorization.",
281     "AuthorizedKey.key_type" =>
282     "A string identifying what type of service uses this key. Supported values are:
283
284   * `\"SSH\"`
285
286 ",
287     "AuthorizedKey.public_key" =>
288     "The full public key, in the format referenced by `key_type`.",
289
290     "ComputedPermission.user_uuid" =>
291     "The UUID of the Arvados user who has this permission.",
292     "ComputedPermission.target_uuid" =>
293     "The UUID of the Arvados object the user has access to.",
294     "ComputedPermission.perm_level" =>
295     "A string representing the user's level of access to the target object.
296 Possible values are:
297
298   * `\"can_read\"`
299   * `\"can_write\"`
300   * `\"can_manage\"`
301
302 ",
303
304     "Container.auth_uuid" =>
305     "The UUID of the Arvados API client authorization token that a dispatcher
306 should use to set up this container. This token is automatically created by
307 Arvados and this attribute automatically assigned unless a container is
308 created with `runtime_token`.",
309     "Container.cost" =>
310     "A float with the estimated cost of the cloud instance used to run this
311 container. The value is `0` if cost estimation is not available on this cluster.",
312     "Container.exit_code" =>
313     "An integer that records the Unix exit code of the `command` from a
314 finished container.",
315     "Container.gateway_address" =>
316     "A string with the address of the Arvados gateway server, in `HOST:PORT`
317 format. This is for internal use only.",
318     "Container.interactive_session_started" =>
319     "This flag is set true if any user starts an interactive shell inside the
320 running container.",
321     "Container.lock_count" =>
322     "The number of times this container has been locked by a dispatcher. This
323 may be greater than 1 if a dispatcher locks a container but then execution is
324 interrupted for any reason.",
325     "Container.locked_by_uuid" =>
326     "The UUID of the Arvados API client authorization token that successfully
327 locked this container in preparation to execute it.",
328     "Container.log" =>
329     "The portable data hash of the Arvados collection that contains this
330 container's logs.",
331     "Container.output" =>
332     "The portable data hash of the Arvados collection that contains this
333 container's output file(s).",
334     "Container.progress" =>
335     "A float between 0.0 and 1.0 (inclusive) that represents the container's
336 execution progress. This attribute is not implemented yet.",
337     "Container.runtime_status" =>
338     "A hash with status updates from a running container.
339 Refer to the [runtime status reference][] for details.
340
341 [runtime status reference]: https://doc.arvados.org/api/methods/containers.html#runtime_status
342
343 ",
344     "Container.subrequests_cost" =>
345     "A float with the estimated cost of all cloud instances used to run this
346 container and all its subrequests. The value is `0` if cost estimation is not
347 available on this cluster.",
348     "Container.state" =>
349     "A string representing the container's current execution status. Possible
350 values are:
351
352   * `\"Queued\"` --- This container has not been dispatched yet.
353   * `\"Locked\"` --- A dispatcher has claimed this container in preparation to run it.
354   * `\"Running\"` --- A dispatcher is running this container.
355   * `\"Cancelled\"` --- Container execution has been cancelled by user request.
356   * `\"Complete\"` --- A dispatcher ran this container to completion and recorded the results.
357
358 ",
359
360     "ContainerRequest.auth_uuid" =>
361     "The UUID of the Arvados API client authorization token that a
362 dispatcher should use to set up a corresponding container. This token is
363 automatically created by Arvados and this attribute automatically assigned
364 unless a container request is created with `runtime_token`.",
365     "ContainerRequest.container_count" =>
366     "An integer that records how many times Arvados has attempted to dispatch
367 a container to fulfill this container request.",
368     "ContainerRequest.container_count_max" =>
369     "An integer that defines the maximum number of times Arvados should attempt
370 to dispatch a container to fulfill this container request.",
371     "ContainerRequest.container_uuid" =>
372     "The UUID of the container that fulfills this container request, if any.",
373     "ContainerRequest.cumulative_cost" =>
374     "A float with the estimated cost of all cloud instances used to run
375 container(s) to fulfill this container request and their subrequests.
376 The value is `0` if cost estimation is not available on this cluster.",
377     "ContainerRequest.expires_at" =>
378     "The time after which this %s will no longer be fulfilled.",
379     "ContainerRequest.filters" =>
380     "An array of filters that limit which existing containers are eligible to
381 satisfy this container request. Refer to the [filters reference][] for details.
382
383 [filters reference]: https://doc.arvados.org/api/methods.html#filters
384
385 ",
386     "ContainerRequest.log_uuid" =>
387     "The UUID of the Arvados collection that contains logs for all the
388 container(s) that were dispatched to fulfill this container request.",
389     "ContainerRequest.output_name" =>
390     "The name to set on the output collection of this container request.",
391     "ContainerRequest.output_ttl" =>
392     "An integer in seconds. If greater than zero, when an output collection is
393 created for this container request, its `expires_at` attribute will be set this
394 far in the future.",
395     "ContainerRequest.output_uuid" =>
396     "The UUID of the Arvados collection that contains output for all the
397 container(s) that were dispatched to fulfill this container request.",
398     "ContainerRequest.requesting_container_uuid" =>
399     "The UUID of the container that created this container request, if any.",
400     "ContainerRequest.state" =>
401     "A string indicating where this container request is in its lifecycle.
402 Possible values are:
403
404   * `\"Uncommitted\"` --- The container request has not been finalized and can still be edited.
405   * `\"Committed\"` --- The container request is ready to be fulfilled.
406   * `\"Final\"` --- The container request has been fulfilled or cancelled.
407
408 ",
409     "ContainerRequest.use_existing" =>
410     "A boolean flag. If set, Arvados may choose to satisfy this container
411 request with an eligible container that already exists. Otherwise, Arvados will
412 satisfy this container request with a newer container, which will usually result
413 in the container running again.",
414
415     "Group.group_class" =>
416     "A string representing which type of group this is. One of:
417
418   * `\"filter\"` --- A virtual project whose contents are selected dynamically by filters.
419   * `\"project\"` --- An Arvados project that can contain collections,
420     container records, workflows, and subprojects.
421   * `\"role\"` --- A group of users that can be granted permissions in Arvados.
422
423 ",
424     "Group.frozen_by_uuid" =>
425     "The UUID of the user that has frozen this group, if any. Frozen projects
426 cannot have their contents or metadata changed, even by admins.",
427
428     "KeepService.service_host" => "The DNS hostname of this %s.",
429     "KeepService.service_port" => "The TCP port where this %s listens.",
430     "KeepService.service_ssl_flag" =>
431     "A boolean flag that indicates whether or not this %s uses TLS/SSL.",
432     "KeepService.service_type" =>
433     "A string that describes which type of %s this is. One of:
434
435   * `\"disk\"` --- A service that stores blocks on a local filesystem.
436   * `\"blob\"` --- A service that stores blocks in a cloud object store.
437   * `\"proxy\"` --- A keepproxy service.
438
439 ",
440     "KeepService.read_only" =>
441     "A boolean flag. If set, this %s does not accept requests to write data
442 blocks; it only serves blocks it already has.",
443
444     "Link.head_uuid" =>
445     "The UUID of the Arvados object that is the originator or actor in this
446 relationship. May be null.",
447     "Link.link_class" =>
448     "A string that defines which kind of link this is. One of:
449
450   * `\"permission\"` --- This link grants a permission to the user or group
451     referenced by `head_uuid` to the object referenced by `tail_uuid`. The
452     access level is set by `name`.
453   * `\"star\"` --- This link represents a \"favorite.\" The user referenced
454     by `head_uuid` wants quick access to the object referenced by `tail_uuid`.
455   * `\"tag\"` --- This link represents an unstructured metadata tag. The object
456     referenced by `tail_uuid` has the tag defined by `name`.
457
458 ",
459     "Link.name" =>
460     "The primary value of this link. For `\"permission\"` links, this is one of
461 `\"can_read\"`, `\"can_write\"`, or `\"can_manage\"`.",
462     "Link.tail_uuid" =>
463     "The UUID of the Arvados object that is the target of this relationship.",
464
465     "Log.id" =>
466     "The serial number of this log. You can use this in filters to query logs
467 that were created before/after another.",
468     "Log.event_type" =>
469     "An arbitrary short string that classifies what type of log this is.",
470     "Log.object_owner_uuid" =>
471     "The `owner_uuid` of the object referenced by `object_uuid` at the time
472 this log was created.",
473     "Log.object_uuid" =>
474     "The UUID of the Arvados object that this log pertains to, such as a user
475 or container.",
476     "Log.summary" =>
477     "A text string that describes the logged event. This is the primary
478 attribute for simple logs.",
479
480     "User.email" => "This user's email address.",
481     "User.first_name" => "This user's first name.",
482     "User.identity_url" =>
483     "A URL that represents this user with the cluster's identity provider.",
484     "User.is_active" =>
485     "A boolean flag. If unset, this user is not permitted to make any Arvados
486 API requests.",
487     "User.is_admin" =>
488     "A boolean flag. If set, this user is an administrator of the Arvados
489 cluster, and automatically passes most permissions checks.",
490     "User.last_name" => "This user's last name.",
491     "User.prefs" => "A hash that stores cluster-wide user preferences.",
492     "User.username" => "This user's Unix username on virtual machines.",
493
494     "VirtualMachine.hostname" =>
495     "The DNS hostname where users should access this %s.",
496
497     "Workflow.definition" => "A string with the CWL source of this %s.",
498   }
499
500   def discovery_doc
501     Rails.application.eager_load!
502     remoteHosts = {}
503     Rails.configuration.RemoteClusters.each {|k,v| if k != :"*" then remoteHosts[k] = v["Host"] end }
504     discovery = {
505       kind: "discovery#restDescription",
506       discoveryVersion: "v1",
507       id: "arvados:v1",
508       name: "arvados",
509       version: "v1",
510       # format is YYYYMMDD, must be fixed width (needs to be lexically
511       # sortable), updated manually, may be used by clients to
512       # determine availability of API server features.
513       revision: "20240627",
514       source_version: AppVersion.hash,
515       sourceVersion: AppVersion.hash, # source_version should be deprecated in the future
516       packageVersion: AppVersion.package_version,
517       generatedAt: db_current_time.iso8601,
518       title: "Arvados API",
519       description: "The API to interact with Arvados.",
520       documentationLink: "http://doc.arvados.org/api/index.html",
521       defaultCollectionReplication: Rails.configuration.Collections.DefaultReplication,
522       protocol: "rest",
523       baseUrl: root_url + "arvados/v1/",
524       basePath: "/arvados/v1/",
525       rootUrl: root_url,
526       servicePath: "arvados/v1/",
527       batchPath: "batch",
528       uuidPrefix: Rails.configuration.ClusterID,
529       defaultTrashLifetime: Rails.configuration.Collections.DefaultTrashLifetime,
530       blobSignatureTtl: Rails.configuration.Collections.BlobSigningTTL,
531       maxRequestSize: Rails.configuration.API.MaxRequestSize,
532       maxItemsPerResponse: Rails.configuration.API.MaxItemsPerResponse,
533       dockerImageFormats: Rails.configuration.Containers.SupportedDockerImageFormats.keys,
534       crunchLogUpdatePeriod: Rails.configuration.Containers.Logging.LogUpdatePeriod,
535       crunchLogUpdateSize: Rails.configuration.Containers.Logging.LogUpdateSize,
536       remoteHosts: remoteHosts,
537       remoteHostsViaDNS: Rails.configuration.RemoteClusters["*"].Proxy,
538       websocketUrl: Rails.configuration.Services.Websocket.ExternalURL.to_s,
539       workbenchUrl: Rails.configuration.Services.Workbench1.ExternalURL.to_s,
540       workbench2Url: Rails.configuration.Services.Workbench2.ExternalURL.to_s,
541       keepWebServiceUrl: Rails.configuration.Services.WebDAV.ExternalURL.to_s,
542       parameters: {
543         alt: {
544           type: "string",
545           description: "Data format for the response.",
546           default: "json",
547           enum: [
548             "json"
549           ],
550           enumDescriptions: [
551             "Responses with Content-Type of application/json"
552           ],
553           location: "query"
554         },
555         fields: {
556           type: "string",
557           description: "Selector specifying which fields to include in a partial response.",
558           location: "query"
559         },
560         key: {
561           type: "string",
562           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.",
563           location: "query"
564         },
565         oauth_token: {
566           type: "string",
567           description: "OAuth 2.0 token for the current user.",
568           location: "query"
569         }
570       },
571       auth: {
572         oauth2: {
573           scopes: {
574             "https://api.arvados.org/auth/arvados" => {
575               description: "View and manage objects"
576             },
577             "https://api.arvados.org/auth/arvados.readonly" => {
578               description: "View objects"
579             }
580           }
581         }
582       },
583       schemas: {},
584       resources: {}
585     }
586
587     ActiveRecord::Base.descendants.reject(&:abstract_class?).sort_by(&:to_s).each do |k|
588       begin
589         ctl_class = "Arvados::V1::#{k.to_s.pluralize}Controller".constantize
590       rescue
591         # No controller -> no discovery.
592         next
593       end
594       human_name = ModelHumanNameMap[k.to_s] || k.to_s.underscore.humanize.downcase
595       object_properties = {}
596       k.columns.
597         select { |col| k.selectable_attributes.include? col.name }.
598         collect do |col|
599         if k.serialized_attributes.has_key? col.name
600           col_type = k.serialized_attributes[col.name].object_class.to_s
601         elsif k.attribute_types[col.name].is_a? JsonbType::Hash
602           col_type = Hash.to_s
603         elsif k.attribute_types[col.name].is_a? JsonbType::Array
604           col_type = Array.to_s
605         else
606           col_type = col.type
607         end
608         desc_fmt =
609           SchemaDescriptionMap["#{k}.#{col.name}"] ||
610           SchemaDescriptionMap[col.name] ||
611           ""
612         if k.attribute_types[col.name].type == :datetime
613           desc_fmt += " The string encodes a UTC date and time in ISO 8601 format."
614         end
615         object_properties[col.name] = {
616           description: desc_fmt % human_name,
617           type: col_type,
618         }
619       end
620       discovery[:schemas][k.to_s + 'List'] = {
621         id: k.to_s + 'List',
622         description: "A list of #{k} objects.",
623         type: "object",
624         properties: {
625           kind: {
626             type: "string",
627             description: "Object type. Always arvados##{k.to_s.camelcase(:lower)}List.",
628             default: "arvados##{k.to_s.camelcase(:lower)}List"
629           },
630           etag: {
631             type: "string",
632             description: "List cache version."
633           },
634           items: {
635             type: "array",
636             description: "An array of matching #{k} objects.",
637             items: {
638               "$ref" => k.to_s
639             }
640           },
641         }
642       }
643       discovery[:schemas][k.to_s] = {
644         id: k.to_s,
645         description: SchemaDescriptionMap[k.to_s] || "Arvados #{human_name}.",
646         type: "object",
647         uuidPrefix: nil,
648         properties: {
649           etag: {
650             type: "string",
651             description: "Object cache version."
652           }
653         }.merge(object_properties)
654       }
655       if k.respond_to? :uuid_prefix
656         discovery[:schemas][k.to_s][:uuidPrefix] ||= k.uuid_prefix
657         discovery[:schemas][k.to_s][:properties][:uuid] ||= {
658           type: "string",
659           description: "This #{human_name}'s Arvados UUID, like `zzzzz-#{k.uuid_prefix}-12345abcde67890`."
660         }
661       end
662       discovery[:resources][k.to_s.underscore.pluralize] = {
663         methods: {
664           get: {
665             id: "arvados.#{k.to_s.underscore.pluralize}.get",
666             path: "#{k.to_s.underscore.pluralize}/{uuid}",
667             httpMethod: "GET",
668             description: "Get a #{k.to_s} record by UUID.",
669             parameters: {
670               uuid: {
671                 type: "string",
672                 description: "The UUID of the #{k.to_s} to return.",
673                 required: true,
674                 location: "path"
675               }
676             },
677             parameterOrder: [
678               "uuid"
679             ],
680             response: {
681               "$ref" => k.to_s
682             },
683             scopes: [
684               "https://api.arvados.org/auth/arvados",
685               "https://api.arvados.org/auth/arvados.readonly"
686             ]
687           },
688           list: {
689             id: "arvados.#{k.to_s.underscore.pluralize}.list",
690             path: k.to_s.underscore.pluralize,
691             httpMethod: "GET",
692             description: "Retrieve a #{k.to_s}List.",
693             parameters: {
694             },
695             response: {
696               "$ref" => "#{k.to_s}List"
697             },
698             scopes: [
699               "https://api.arvados.org/auth/arvados",
700               "https://api.arvados.org/auth/arvados.readonly"
701             ]
702           },
703           create: {
704             id: "arvados.#{k.to_s.underscore.pluralize}.create",
705             path: "#{k.to_s.underscore.pluralize}",
706             httpMethod: "POST",
707             description: "Create a new #{k.to_s}.",
708             parameters: {},
709             request: {
710               required: true,
711               properties: {
712                 k.to_s.underscore => {
713                   "$ref" => k.to_s
714                 }
715               }
716             },
717             response: {
718               "$ref" => k.to_s
719             },
720             scopes: [
721               "https://api.arvados.org/auth/arvados"
722             ]
723           },
724           update: {
725             id: "arvados.#{k.to_s.underscore.pluralize}.update",
726             path: "#{k.to_s.underscore.pluralize}/{uuid}",
727             httpMethod: "PUT",
728             description: "Update attributes of an existing #{k.to_s}.",
729             parameters: {
730               uuid: {
731                 type: "string",
732                 description: "The UUID of the #{k.to_s} to update.",
733                 required: true,
734                 location: "path"
735               }
736             },
737             request: {
738               required: true,
739               properties: {
740                 k.to_s.underscore => {
741                   "$ref" => k.to_s
742                 }
743               }
744             },
745             response: {
746               "$ref" => k.to_s
747             },
748             scopes: [
749               "https://api.arvados.org/auth/arvados"
750             ]
751           },
752           delete: {
753             id: "arvados.#{k.to_s.underscore.pluralize}.delete",
754             path: "#{k.to_s.underscore.pluralize}/{uuid}",
755             httpMethod: "DELETE",
756             description: "Delete an existing #{k.to_s}.",
757             parameters: {
758               uuid: {
759                 type: "string",
760                 description: "The UUID of the #{k.to_s} to delete.",
761                 required: true,
762                 location: "path"
763               }
764             },
765             response: {
766               "$ref" => k.to_s
767             },
768             scopes: [
769               "https://api.arvados.org/auth/arvados"
770             ]
771           }
772         }
773       }
774       # Check for Rails routes that don't match the usual actions
775       # listed above
776       d_methods = discovery[:resources][k.to_s.underscore.pluralize][:methods]
777       Rails.application.routes.routes.each do |route|
778         action = route.defaults[:action]
779         httpMethod = ['GET', 'POST', 'PUT', 'DELETE'].map { |method|
780           method if route.verb.match(method)
781         }.compact.first
782         if httpMethod &&
783           route.defaults[:controller] == 'arvados/v1/' + k.to_s.underscore.pluralize &&
784           ctl_class.action_methods.include?(action)
785           method_name = ActionNameMap[action] || action
786           method_key = method_name.to_sym
787           if !d_methods[method_key]
788             method = {
789               id: "arvados.#{k.to_s.underscore.pluralize}.#{method_name}",
790               path: route.path.spec.to_s.sub('/arvados/v1/','').sub('(.:format)','').sub(/:(uu)?id/,'{uuid}'),
791               httpMethod: httpMethod,
792               description: ctl_class.send("_#{method_name}_method_description".to_sym),
793               parameters: {},
794               response: {
795                 "$ref" => (method_name == 'list' ? "#{k.to_s}List" : k.to_s)
796               },
797               scopes: [
798                 "https://api.arvados.org/auth/arvados"
799               ]
800             }
801             route.segment_keys.each do |key|
802               case key
803               when :format
804                 next
805               when :id, :uuid
806                 key = :uuid
807                 description = "The UUID of the #{k} to #{HttpMethodDescriptionMap[httpMethod]}."
808               else
809                 description = ""
810               end
811               method[:parameters][key] = {
812                 type: "string",
813                 description: description,
814                 required: true,
815                 location: "path",
816               }
817             end
818           else
819             # We already built a generic method description, but we
820             # might find some more required parameters through
821             # introspection.
822             method = d_methods[method_key]
823           end
824           if ctl_class.respond_to? "_#{action}_requires_parameters".to_sym
825             ctl_class.send("_#{action}_requires_parameters".to_sym).each do |l, v|
826               if v.is_a? Hash
827                 method[:parameters][l] = v
828               else
829                 method[:parameters][l] = {}
830               end
831               if !method[:parameters][l][:default].nil?
832                 # The JAVA SDK is sensitive to all values being strings
833                 method[:parameters][l][:default] = method[:parameters][l][:default].to_s
834               end
835               method[:parameters][l][:type] ||= 'string'
836               method[:parameters][l][:description] ||= ''
837               method[:parameters][l][:location] = (route.segment_keys.include?(l) ? 'path' : 'query')
838               if method[:parameters][l][:required].nil?
839                 method[:parameters][l][:required] = v != false
840               end
841             end
842           end
843           d_methods[method_key] = method
844         end
845       end
846     end
847
848     # The computed_permissions controller does not offer all of the
849     # usual methods and attributes.  Modify discovery doc accordingly.
850     discovery[:resources]['computed_permissions'][:methods].select! do |method|
851       method == :list
852     end
853     discovery[:resources]['computed_permissions'][:methods][:list][:parameters].reject! do |param|
854       [:cluster_id, :bypass_federation, :offset].include?(param)
855     end
856     discovery[:schemas]['ComputedPermission'].delete(:uuidPrefix)
857     discovery[:schemas]['ComputedPermission'][:properties].reject! do |prop|
858       [:uuid, :etag].include?(prop)
859     end
860     discovery[:schemas]['ComputedPermission'][:properties]['perm_level'][:type] = 'string'
861
862     # The 'replace_files' option is implemented in lib/controller,
863     # not Rails -- we just need to add it here so discovery-aware
864     # clients know how to validate it.
865     [:create, :update].each do |action|
866       discovery[:resources]['collections'][:methods][action][:parameters]['replace_files'] = {
867         type: 'object',
868         description:
869           "Add, delete, and replace files and directories with new content
870 and/or content from other collections. Refer to the
871 [replace_files reference][] for details.
872
873 [replace_files reference]: https://doc.arvados.org/api/methods/collections.html#replace_files
874
875 ",
876         required: false,
877         location: 'query',
878         properties: {},
879         additionalProperties: {type: 'string'},
880       }
881     end
882
883     discovery[:resources]['configs'] = {
884       methods: {
885         get: {
886           id: "arvados.configs.get",
887           path: "config",
888           httpMethod: "GET",
889           description: "Get this cluster's public configuration settings.",
890           parameters: {
891           },
892           parameterOrder: [
893           ],
894           response: {
895           },
896           scopes: [
897             "https://api.arvados.org/auth/arvados",
898             "https://api.arvados.org/auth/arvados.readonly"
899           ]
900         },
901       }
902     }
903
904     discovery[:resources]['vocabularies'] = {
905       methods: {
906         get: {
907           id: "arvados.vocabularies.get",
908           path: "vocabulary",
909           httpMethod: "GET",
910           description: "Get this cluster's configured vocabulary definition.
911
912 Refer to [metadata vocabulary documentation][] for details.
913
914 [metadata vocabulary documentation]: https://doc.aravdos.org/admin/metadata-vocabulary.html
915
916 ",
917           parameters: {
918           },
919           parameterOrder: [
920           ],
921           response: {
922           },
923           scopes: [
924             "https://api.arvados.org/auth/arvados",
925             "https://api.arvados.org/auth/arvados.readonly"
926           ]
927         },
928       }
929     }
930
931     discovery[:resources]['sys'] = {
932       methods: {
933         get: {
934           id: "arvados.sys.trash_sweep",
935           path: "sys/trash_sweep",
936           httpMethod: "POST",
937           description:
938             "Run scheduled data trash and sweep operations across this cluster's Keep services.",
939           parameters: {
940           },
941           parameterOrder: [
942           ],
943           response: {
944           },
945           scopes: [
946             "https://api.arvados.org/auth/arvados",
947             "https://api.arvados.org/auth/arvados.readonly"
948           ]
949         },
950       }
951     }
952
953     Rails.configuration.API.DisabledAPIs.each do |method, _|
954       ctrl, action = method.to_s.split('.', 2)
955       next if ctrl.in?(['api_clients', 'job_tasks', 'jobs', 'keep_disks', 'nodes', 'pipeline_instances', 'pipeline_templates', 'repositories'])
956       discovery[:resources][ctrl][:methods].delete(action.to_sym)
957     end
958     discovery
959   end
960 end