Hacking API server » History » Version 1
Tom Clegg, 04/17/2014 04:08 PM
1 | 1 | Tom Clegg | h1. Hacking API server |
---|---|---|---|
2 | |||
3 | {{toc}} |
||
4 | |||
5 | h2. Source tree layout |
||
6 | |||
7 | Everything is in @/services/api@. |
||
8 | |||
9 | Key pieces to know about before going much further: |
||
10 | |||
11 | |/|Usual Rails project layout| |
||
12 | |/app/controllers/application_controller.rb|Controller superclass with most of the generic API features like CRUD, authentication| |
||
13 | |/app/controllers/arvados/v1/|API methods other than generic CRUD (users#current, jobs#queue, ...)| |
||
14 | |/app/models/arvados_model.rb|Default Arvados model behavior: permissions, etag, uuid| |
||
15 | |||
16 | h2. Unlike a typical Rails project... |
||
17 | |||
18 | * Most responses are JSON. Very few HTML views. We don't normally talk to browsers, except during authentication. |
||
19 | * We assign UUID strings (see lib/assign_uuid.rb and app/models/arvados_model.rb) |
||
20 | * The @Links@ table emulates a graph database a la "RDF":http://www.rdfabout.com/quickintro.xpd. Much of the interesting information in Arvados is recorded as a Link between two other entities. |
||
21 | * For the most part, relations among objects are not expressed with the usual ActiveRelation features like belongs_to and has_many. |
||
22 | * Permissions: see below. |
||
23 | |||
24 | h2. Running in development mode |
||
25 | |||
26 | SDKs really want your server to offer SSL. One way is to generate a self-signed certificate. |
||
27 | |||
28 | openssl req -new -x509 -nodes -out ~/self-signed.pem -keyout ~/self-signed.key -days 3650 -subj '/CN=arvados.example.com' |
||
29 | |||
30 | Save something like this at @~/bin/apiserver@, make it executable, make sure ~/bin is in your path: |
||
31 | |||
32 | #!/bin/sh |
||
33 | set -e |
||
34 | cd ~/arvados/services/api |
||
35 | export RAILS_ENV=development |
||
36 | rvm-exec 2.0.0 bundle install |
||
37 | exec rvm-exec 2.0.0 bundle exec passenger start --ssl --ssl-certificate ~/self-signed.pem --ssl-certificate-key ~/self-signed.key |
||
38 | |||
39 | h2. Headaches to avoid |
||
40 | |||
41 | If you make a change that affects the discovery document, you need to clear a few caches before your client will see the change. |
||
42 | * Restart API server or: @touch tmp/restart.txt@ |
||
43 | * Clear API server disk cache: @rake tmp:cache:clear@ |
||
44 | * Clear SDK discovery doc cache on client side: @rm -r ~/.cache/arvados/@ |
||
45 | |||
46 | h2. Features |
||
47 | |||
48 | h3. Authentication |
||
49 | |||
50 | Involves |
||
51 | * UserSessionsController (in app/controllers/, not .../arvados/v1): this is an exceptional case where we actually talk to a browser. |
||
52 | |||
53 | h3. Permissions |
||
54 | |||
55 | Object-level permissions, aka ownership and sharing |
||
56 | * Models have their own idea of create/update permissions. Controllers don't worry about this. |
||
57 | * ArvadosModel updates/enforces modified_by_* and owner_uuid |
||
58 | * Lookups are not (yet) permission-restricted in the default scope, though. Controllers need to use Model.readable_by(user). |
||
59 | * ApplicationController uses an around_filter that verifies the supplied api_token and makes current_user available everywhere. If you need to override create/update permissions, use @act_as_system_user do ... end@. |
||
60 | * Unusual cases: KeepDisks and Collections can be looked up by inactive users (otherwise they wouldn't be able to read & clickthrough user agreements). |
||
61 | |||
62 | Controller-level permissions |
||
63 | * ApplicationController#require_auth_scope_all checks token scopes: currently, unless otherwise specified by a subclass controller, nothing is allowed unless scopes includes "all". |
||
64 | * ApplicationController has an admin_required filter available (not used by default) |
||
65 | |||
66 | h3. Error handling |
||
67 | |||
68 | * "Look up object by uuid, and send 404 if not found" is enabled by default, except for index/create actions. |
||
69 | |||
70 | h3. Routing |
||
71 | |||
72 | * API routes are in the @:arvados@ → @:v1@ namespace. |
||
73 | * Routes like @/jobs/queue@ have to come before @resources :jobs@ (otherwise @/jobs/queue@ will match @jobs#get(id=queue)@ first). (Better, we should rearrange these to use @resources :jobs do ...@ like in Workbench.) |
||
74 | * We use the standard Rails routes like @/jobs/:id@ but then we move params[:id] to params[:uuid] in our before_filters. |
||
75 | |||
76 | h3. Tests |
||
77 | |||
78 | * Run tests with @rvm-exec 2.0.0 bundle exec rake test RAILS_ENV=test@ |
||
79 | * Functional tests need to authenticate themselves with @authorize_with :active@ (where @:active@ refers to an ApiClientAuthorization fixture) |
||
80 | * Big deficit of tests, especially unit tests. This is a bug! It doesn't mean we don't want to test things. |
||
81 | |||
82 | h3. Discovery document |
||
83 | |||
84 | * Mostly, but not yet completely, generated by introspection (descendants of ArvadosModel are inspected at run time). But some controllers/actions are skipped, and some actions are renamed (e.g., Rails calls it "show" but everyone else calls it "get"). |
||
85 | * Handled by Arvados::V1::SchemaController#index (used to be in #discovery_document before #1750). See @config/routes.rb@ |
||
86 | * Must be available to anonymous clients. |
||
87 | * Has no tests! We test it by trying all of our SDKs against it. |
||
88 | |||
89 | h2. Development patterns |
||
90 | |||
91 | h3. Add a model |
||
92 | |||
93 | In shell: |
||
94 | * @rails g model FizzBuzz@ |
||
95 | |||
96 | In @app/models/fizzbuzz.rb@: |
||
97 | * Change base class from @ActiveRecord::Base@ to @ArvadosModel@. |
||
98 | * Add some more standard behavior. |
||
99 | |||
100 | <pre><code class="ruby"> |
||
101 | include AssignUuid |
||
102 | include KindAndEtag |
||
103 | include CommonApiTemplate |
||
104 | </code></pre> |
||
105 | |||
106 | In @db/migrate/{timestamp}_create_fizzbuzzes.rb@: |
||
107 | * Add the generic attribute columns. |
||
108 | * Run @t.timestamps@ and add (at least!) a @:uuid@ index. |
||
109 | |||
110 | <pre><code class="ruby"> |
||
111 | class CreateFizzBuzz < ActiveRecord::Migration |
||
112 | def change |
||
113 | create_table :fizzbuzzes do |t| |
||
114 | t.string :uuid, :null => false |
||
115 | t.string :owner_uuid, :null => false |
||
116 | t.string :modified_by_client_uuid |
||
117 | t.string :modified_by_user_uuid |
||
118 | t.datetime :modified_at |
||
119 | t.text :properties |
||
120 | |||
121 | t.timestamps |
||
122 | end |
||
123 | add_index :humans, :uuid, :unique => true |
||
124 | end |
||
125 | end |
||
126 | </code></pre> |
||
127 | |||
128 | Apply the migration: |
||
129 | * @rake db:migrate@ |
||
130 | * @RAILS_ENV=test rake db:migrate@ (to migrate your test database too) |
||
131 | * Inspect the resulting @db/schema.rb@ and include it in your commit. |
||
132 | * Don't forget to @git add@ the new migration and model files. |
||
133 | |||
134 | h3. Add an attribute to a model |
||
135 | |||
136 | * Generate migration as usual |
||
137 | <pre> |
||
138 | rails g migration AddBazQuxToFooBar baz_qux:column_type_goes_here |
||
139 | </pre> |
||
140 | * Consider adding null constraints and a default value to the @add_column@ statement in the migration in @db/migrate/timestamp_add_baz_qux_to_foo_bar.rb@: |
||
141 | <pre><code class="ruby">, null: false, default: false</code></pre> |
||
142 | * Consider adding an index |
||
143 | * You probably want to add it to the API response template so clients can see it: @app/models/model_name.rb@ → @api_accessible :user ...@ |
||
144 | * Sometimes it's only visible to privileged users; see @ping_secret@ in @app/models/keep_disk.rb@ |
||
145 | * If it's a serialized attribute, add @serialize :the_attribute_name, Hash@ to the model. Always specify Hash or Array! |
||
146 | * Run @rake db:migrate@ and inspect your @db/schema.rb@ and include the new @schema.rb@ in the *same commit* as your @db/migrate/*.rb@ migration script. |
||
147 | |||
148 | |||
149 | h3. Add a controller |
||
150 | |||
151 | * @rails g controller Arvados::V1::FizzBuzzesController@ |
||
152 | * Avoid adding top-level controllers like @app/controllers/fizz_buzzes_controller.rb@. |
||
153 | * Avoid adding top-level routes. Everything should be in @namespace :arvados@ → @namespace :v1@ except oddballs like login/logout actions. |
||
154 | |||
155 | h3. Add a controller action |
||
156 | |||
157 | Add a route in @config/routes.rb@. |
||
158 | * Choose an appropriate HTTP method: GET has no side effects. POST creates something. PUT replaces/updates something. |
||
159 | * Use the block form: |
||
160 | <pre><code class="ruby"> |
||
161 | resources :fizz_buzzes do |
||
162 | # If the action operates on an object, i.e., a uuid is required, |
||
163 | # this generates a route /arvados/v1/fizz_buzzes/{uuid}/blurfl |
||
164 | post 'blurfl', on: :member |
||
165 | # If not, this generates a route /arvados/v1/fizz_buzzes/flurbl |
||
166 | get 'flurbl', on: :collection |
||
167 | end |
||
168 | </code></pre> |
||
169 | |||
170 | In @app/controllers/arvados/v1/fizz_buzzes_controller.rb@: |
||
171 | |||
172 | * Add a method to the controller class. |
||
173 | * Skip the "find_object" before_filters if it's a collection action. |
||
174 | * Specify required/optional parameters using a class method @_action_requires_parameters@. |
||
175 | <pre><code class="ruby"> |
||
176 | skip_before_filter :find_object_by_uuid, only: [:flurbl] |
||
177 | skip_before_filter :render_404_if_no_object, only: [:flurbl] |
||
178 | |||
179 | def blurfl |
||
180 | @object.do_whatever_blurfl_does! |
||
181 | show |
||
182 | end |
||
183 | |||
184 | def self._flurbl_requires_parameters |
||
185 | { |
||
186 | qux: { type: 'integer', required: true, description: 'First flurbl qux must match this qux.' } |
||
187 | } |
||
188 | end |
||
189 | def flurbl |
||
190 | @object = model_class.where('qux = ?', params[:qux]).first |
||
191 | show |
||
192 | end |
||
193 | </code></pre> |
||
194 | |||
195 | h3. Add a configuration parameter |
||
196 | |||
197 | * Add it to @config/application.default.yml@ with a sensible default value. |
||
198 | * If there is no sensible default value, like @secret_token@: specify @~@ (i.e., nil) in @application.default.yml@ *and* put a default value in the @test@ section of @config/application.yml.example@ that will make tests pass. |
||
199 | * If there is a sensible default value for development/test but not for production, like return address for notification email messages, specify the test/dev default in the @common@ section @application.default.yml@ but specify @~@ (nil) in the @production@ section. This prevents someone from installing or *updating a production server* with defaults that don't make sense in production! |
||
200 | * Use @Rails.configuration.config_setting_name@ to retrieve the configured value. There is no need to check whether it is nil or missing: in those cases, "rake config:check" would have failed and the application would have refused to start. |