Project

General

Profile

Hacking API server » History » Version 20

Joshua Randall, 02/02/2016 02:48 PM
adds a note about how to look up class uuid prefix

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 12 Tom Clegg
First, take care of the dependencies documented at http://doc.arvados.org/install/install-api-server.html.
27
28 1 Tom Clegg
Save something like this at @~/bin/apiserver@, make it executable, make sure ~/bin is in your path:
29
30 7 Tom Clegg
<pre>
31
#!/bin/sh
32 1 Tom Clegg
set -e
33
cd ~/arvados/services/api
34 7 Tom Clegg
if ! [ -e self-signed.key ]
35
then
36 8 Tom Clegg
  # Generate a self-signed SSL key
37
  openssl req -new -x509 -nodes -out ./self-signed.pem -keyout ./self-signed.key -days 3650 -subj /CN=localhost
38 7 Tom Clegg
fi
39
if [ -e /usr/local/rvm/bin/rvm ]
40
then
41
  rvmexec="rvm-exec 2.1.1"
42
else
43
  rvmexec=""
44
fi
45
export ARVADOS_WEBSOCKETS=true
46 1 Tom Clegg
export RAILS_ENV=development
47 7 Tom Clegg
$rvmexec bundle install
48 11 Tom Clegg
exec $rvmexec bundle exec passenger start -p3030 --ssl --ssl-certificate self-signed.pem --ssl-certificate-key self-signed.key
49 7 Tom Clegg
</pre>
50 1 Tom Clegg
51 9 Tom Clegg
Notes:
52
* Here we use passenger instead of webrick (which is what we'd get with "@rails server@") in order to serve websockets from the same process as https. (You also have the option of serving https and wss from separate processes -- see @services/api/config/application.default.yml@ -- but the simplest way is to run both in the same process by setting @ARVADOS_WEBSOCKETS=true@.)
53
* Webrick can make its own self-signed SSL certificate, but passenger expects you to provide a certificate & key yourself. The above script generates a key/certificate pair the first time it runs, and leaves it in @services/api/self-signed.*@ to reuse next time.
54
* @bundle install@ ensures your installed gems satisfy the requirements in Gemfile and (if you have changed it locally) update Gemfile.lock appropriately. This should do the right thing after you change Gemfile yourself, or change Gemfile/Gemfile.lock by updating/merging master.
55
* If you're relying on rvm to provide a suitable version of Ruby, "@rvm-exec@" should do the right thing here. You can change the @2.1.1@ argument to the version you want to use (hopefully >= 2.1.1). If your system Ruby is >= 2.1.1, rvm is unnecessary.
56 14 Radhika Chippada
* You can kill the server by running
57
58 15 Radhika Chippada
<pre>
59 14 Radhika Chippada
passenger stop --pid-file ~/arvados/services/api/tmp/pids/passenger.3030.pid
60 15 Radhika Chippada
</pre>
61 9 Tom Clegg
62 1 Tom Clegg
h2. Headaches to avoid
63
64
If you make a change that affects the discovery document, you need to clear a few caches before your client will see the change.
65
* Restart API server or: @touch tmp/restart.txt@
66
* Clear API server disk cache: @rake tmp:cache:clear@
67
* Clear SDK discovery doc cache on client side: @rm -r ~/.cache/arvados/@
68 4 Tom Clegg
69
Do not store symbol keys (or values) in serialized attributes.
70
* Rails supplies @params@ as a HashWithIndifferentAccess so @params['foo']@ and @params[:foo]@ are equivalent. This is usually convenient. However, here we often copy arrays and hashes from @params@ to the database, and from there to API responses. JSON does not have HashWithIndifferentAccess (or symbols) and we want these serialized attributes to behave predictably everywhere.
71
* API server's policy is that serialized attributes (like @properties@ on a link) always have strings instead of symbols: these attributes look the same in the database, in the API server Rails application, in the JSON response sent to clients, and in the JSON objects received from clients.
72
* There is no validation (yet!) to check for this.
73
74 18 Tom Clegg
When @script/crunch-dispatch.rb@ invokes @arv-run-pipeline-instance@ and @crunch-job@, it uses the version of arvados-cli specified in @Gemfile.lock@. Use @bundle update arvados-cli@ to update @Gemfile.lock@ to use the latest versions.
75
76 1 Tom Clegg
77
h2. Features
78
79
h3. Authentication
80
81
Involves
82
* UserSessionsController (in app/controllers/, not .../arvados/v1): this is an exceptional case where we actually talk to a browser.
83
84
h3. Permissions
85
86
Object-level permissions, aka ownership and sharing
87 19 Tom Clegg
* Writing
88
** Models have their own idea of create/update permissions. Controllers don't worry about this.
89
** ArvadosModel updates/enforces modified_by_* and owner_uuid
90
* Reading
91
** Lookups are not (yet) permission-restricted in the default scope, i.e., when calling Model.where(foo: 'bar').
92
** Controllers need to *use @Model.readable_by(current_user)@ when appropriate.*
93
** The other most important permission method is *@User#groups_i_can(verb)@*. For example, @user_object.groups_i_can(:write)@ returns an array of UUIDs of groups (including projects and other kinds of groups) where @user_object@ has @write@ permission. (For example, @readable_by@ uses this to determine which values of owner_uuid and permission link tail_uuid could establish permission on a given database record for the user in question.)
94 1 Tom Clegg
* 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@.
95
* Unusual cases: KeepDisks and Collections can be looked up by inactive users (otherwise they wouldn't be able to read & clickthrough user agreements).
96
97
Controller-level permissions
98
* ApplicationController#require_auth_scope_all checks token scopes: currently, unless otherwise specified by a subclass controller, nothing is allowed unless scopes includes "all".
99
* ApplicationController has an admin_required filter available (not used by default)
100
101
h3. Error handling
102
103
* "Look up object by uuid, and send 404 if not found" is enabled by default, except for index/create actions.
104
105
h3. Routing
106
107
* API routes are in the @:arvados@ &rarr; @:v1@ namespace.
108
* 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.)
109
* We use the standard Rails routes like @/jobs/:id@ but then we move params[:id] to params[:uuid] in our before_filters.
110
111
h3. Tests
112
113 10 Tom Clegg
* Run tests with @rvm-exec 2.1.1 bundle exec rake test@
114
* If prompted, migrate your test database by running @RAILS_ENV=test rvm-exec 2.1.1 bundle exec rake db:migrate@
115
* As above, you can leave out @rvm-exec 2.1.1@ if your system Ruby version is suitable. But don't leave out @bundle exec@.
116
* Run just the unit tests with @[...] rake test:units@ (or @test:functionals@ or @test:integration@).
117
* Run just a single test class (file) by specifying the file, like @[...] rake TEST=test/unit/owner_test.rb@ (save time in your "did that fix the failing test?" phase!)
118 1 Tom Clegg
* Functional tests need to authenticate themselves with @authorize_with :active@ (where @:active@ refers to an ApiClientAuthorization fixture)
119 10 Tom Clegg
* There is a deficit of tests, especially unit tests. This is a bug! It doesn't mean we don't want to test things.
120
121 1 Tom Clegg
122
h3. Discovery document
123
124
* 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").
125
* Handled by Arvados::V1::SchemaController#index (used to be in #discovery_document before #1750). See @config/routes.rb@
126
* Must be available to anonymous clients.
127
* Has no tests! We test it by trying all of our SDKs against it.
128
129
h2. Development patterns
130
131
h3. Add a model
132
133
In shell:
134
* @rails g model FizzBuzz@
135
136
In @app/models/fizzbuzz.rb@:
137
* Change base class from @ActiveRecord::Base@ to @ArvadosModel@.
138
* Add some more standard behavior.
139
140
<pre><code class="ruby">
141 13 Peter Amstutz
include HasUuid
142 1 Tom Clegg
include KindAndEtag
143
include CommonApiTemplate
144
</code></pre>
145
146
In @db/migrate/{timestamp}_create_fizzbuzzes.rb@:
147
* Add the generic attribute columns.
148
* Run @t.timestamps@ and add (at least!) a @:uuid@ index.
149
150
<pre><code class="ruby">
151
class CreateFizzBuzz < ActiveRecord::Migration
152
  def change
153
    create_table :fizzbuzzes do |t|
154
      t.string :uuid, :null => false
155
      t.string :owner_uuid, :null => false
156
      t.string :modified_by_client_uuid
157
      t.string :modified_by_user_uuid
158
      t.datetime :modified_at
159
      t.text :properties
160
161
      t.timestamps
162
    end
163
    add_index :humans, :uuid, :unique => true
164
  end
165
end
166
</code></pre>
167
168
Apply the migration:
169
* @rake db:migrate@
170
* @RAILS_ENV=test rake db:migrate@ (to migrate your test database too)
171
* Inspect the resulting @db/schema.rb@ and include it in your commit.
172
* Don't forget to @git add@ the new migration and model files.
173
174
h3. Add an attribute to a model
175
176
* Generate migration as usual
177
<pre>
178
rails g migration AddBazQuxToFooBar baz_qux:column_type_goes_here
179
</pre>
180
* 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@:
181
<pre><code class="ruby">, null: false, default: false</code></pre>
182
* Consider adding an index
183 16 Tom Clegg
* You probably want to add it to the API response template(s) so clients can see it: @app/models/model_name.rb@ &rarr; @api_accessible :user ...@
184 1 Tom Clegg
* Sometimes it's only visible to privileged users; see @ping_secret@ in @app/models/keep_disk.rb@
185
* If it's a serialized attribute, add @serialize :the_attribute_name, Hash@ to the model. Always specify Hash or Array!
186 3 Tom Clegg
* 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.
187 1 Tom Clegg
* Run @rake tmp:cache:clear@ and @touch tmp/restart.txt@ in your dev apiserver, to force it to generate a new REST discovery document.
188
189
h3. Add a controller
190
191 17 Tom Clegg
* @rails g controller Arvados::V1::FizzBuzzes@
192 1 Tom Clegg
* Avoid adding top-level controllers like @app/controllers/fizz_buzzes_controller.rb@.
193
* Avoid adding top-level routes. Everything should be in @namespace :arvados@ &rarr; @namespace :v1@ except oddballs like login/logout actions.
194
195
h3. Add a controller action
196
197
Add a route in @config/routes.rb@.
198
* Choose an appropriate HTTP method: GET has no side effects. POST creates something. PUT replaces/updates something.
199
* Use the block form:
200
<pre><code class="ruby">
201
resources :fizz_buzzes do
202
  # If the action operates on an object, i.e., a uuid is required,
203
  # this generates a route /arvados/v1/fizz_buzzes/{uuid}/blurfl
204
  post 'blurfl', on: :member
205
  # If not, this generates a route /arvados/v1/fizz_buzzes/flurbl
206
  get 'flurbl', on: :collection
207
end
208
</code></pre>
209
210
In @app/controllers/arvados/v1/fizz_buzzes_controller.rb@:
211
212
* Add a method to the controller class.
213
* Skip the "find_object" before_filters if it's a collection action.
214
* Specify required/optional parameters using a class method @_action_requires_parameters@.
215
<pre><code class="ruby">
216
skip_before_filter :find_object_by_uuid, only: [:flurbl]
217
skip_before_filter :render_404_if_no_object, only: [:flurbl]
218
219
def blurfl
220
  @object.do_whatever_blurfl_does!
221
  show
222
end
223
224
def self._flurbl_requires_parameters
225
  {
226
    qux: { type: 'integer', required: true, description: 'First flurbl qux must match this qux.' }
227
  }
228
end
229
def flurbl
230
  @object = model_class.where('qux = ?', params[:qux]).first
231
  show
232
end
233
</code></pre>
234
235
h3. Add a configuration parameter
236 6 Tom Clegg
237 1 Tom Clegg
* Add it to @config/application.default.yml@ with a sensible default value. (Don't fall back to default values at time of use, or define defaults in other places!)
238
* 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.
239
* 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!
240 6 Tom Clegg
* 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.
241 2 Tom Clegg
242
243
h3. Add a test fixture
244
245
Generate last part of uuid from command line:
246
<pre><code class="ruby">ruby -e 'puts rand(2**512).to_s(36)[0..14]'
247
j0wqrlny07k1u12</code></pre>
248
Generate uuid from @rails console@:
249
<pre><code class="ruby">Group.generate_uuid
250
=> "xyzzy-j7d0g-8nw4r6gnnkixw1i"
251 1 Tom Clegg
</code></pre>
252 20 Joshua Randall
253
h2. Database notes
254
255
uuids are made up of three parts separated by `-` characters: a system prefix (defined the configuration as uuid_prefix), a class prefix (generated by digesting the ruby Class: https://github.com/curoverse/arvados/blob/master/services/api/lib/has_uuid.rb#L16), and a random string. From the rails console, you can get the current value of the class prefix (second part of the uuid) via `.uuid_prefix`. For example:
256
<pre><code class="ruby">Group.uuid_prefix
257
-> "j7d0g"
258
</code></pre>