~jameinel/juju-core/api-registry-tracks-type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
* API Design

The overall aim is to make agents and clients connect through a network
API rather than directly to the underlying database as is the case
currently.

This will have a few advantages:

- Operations that involve multiple round trips to the database can be
made more efficient because the API server is likely to be closer to
the database server.

- We can decide on, and enforce, an appropriate authorization policy
for each operation.

- The API can be made easy to use from multiple languages and client
types.

There are two general kinds of operation on the API: simple
requests and watch requests. I'll deal with simple requests
first.

* Simple requests

A simple request takes some parameters, possibly makes some changes to
the state database, and returns some results or an error.

When the request returns no data, it would theoretically be possible to
have the API server operate on the request without returning a reply,
but then the client would not know when the request has completed or if
it completed successfully. Therefore, I think it's better if all requests
return a reply.

Here is the list of all the State requests that are currently used by the
juju agents:
	XXX link
We will need to implement at least these requests (possibly
slightly changed, but hopefully as little as possible, to ensure as
little churn as possible in the agent code when moving to using the API)

41 out of the 59 requests are operating directly on a single state
entity, expressed as the receiver object in the Go API. For this reason
I believe it's appropriate to phrase the API requests in this way -
as requests on particular entities in the state. This leads to certain
implementation advantages (see "Implementation" below) and means there's
a close correspondence between the API protocol and the API as implemented
in Go (and hopefully other languages too).

To make the protocol accessible, we define all messages to be in JSON
format and we use a secure websocket for transport.
For security, we currently rely on a server-side certificate
and passwords sent over the connection to identify the client,
but it should be straightforward to enable the server to
do client certificate checking if desired.

Here's a sample request to change the instance id
associated with a machine, and its reply.  (I'll show JSON in rjson form, to keep the
noise down, see http://godoc.org/launchpad.net/rjson).

Client->Server
	{
		RequestId: 1234
		Type: "Machine"
		Id: "99"
		Request: "SetInstanceId"
		Params: {
			InstanceId: "i-43e55e5"
		}
	}
Server->Client
	{
		RequestId: 1234
		Error: ""
		Result: {
		}
	}

We use the RequestId field to associate the request and its
reply. The API client must not re-use a request id until
it has received the request's reply (the easiest way
to do that is simply to increment the request id each time).
We allow multiple requests to be outstanding on a connection
at once, and their replies can be received in any order.

In the request, the Id field may be omitted to specify
an empty Id, and Params may be omitted
to specify no request parameters. Similarly, in the
response, the Error field may be omitted to
signify no error, and the Result field may be
omitted to signify no result. To save space below,
I've omitted fields accordingly.

The Type field identifies the type of entity to act on,
and the Id field its identifier. Currently I envisage
the following types of entities:

	Admin
		Admin (a singleton) is used by a client when identifying itself
		to the server. It is the only thing that can be accessed
		before the client has authenticated.

	Client
	ClientWatcher
		Client (a singleton) is the access point for all the GUI client
		and other user-facing methods. This is only
		usable by clients, not by agents.

	State
	Machine
	Unit
	Relation
	RelationUnit
	Service
	Pinger
	MachineWatcher
	UnitWatcher
	LifecycleWatcher
	ServiceUnitsWatcher
	ServiceRelationsWatcher
	RelationScopeWatcher
	UnitsWatcher
	ConfigWatcher
	NotifyWatcher
	MachineUnitsWatcher
		These correspond directly to types exported by the
		juju state package.  They are usable only by agents,
		not clients.

The Request field specifies the action to perform, and Params holds the
parameters to that request.

In the reply message, the RequestId field must match that of the
request. If the request failed, then the Error field holds the description
of the error (it is possible we might add a Code field later, to help
diagnosing specific kinds of error).

The Result field holds the results of the request (in this case there
are none, so it's empty).

That completes the overview of simple requests,
so on to watching.

* Watching

To watch something in the state, we invoke a Watch request, which
returns a handle to a watcher object, that can then be used to find
out when changes happen by calling its Next method. To stop a watcher,
we call Stop on it.

For example, if an agent wishes to watch machine 99, the conversation
with the API server looks something like this:

Client->Server
	{
		RequestId: 1000
		Type: "Machine"
		Id: "99"
		Request: "Watch"
	}
Server->Client
	{
		RequestId: 1000
		Response: {
			NotifyWatcherId: "1"
		}
	}

At this point, the watcher is registered.  Subsequent Next calls will
only return when the entity has changed.

Client->Server
	{
		RequestId: 1001
		Type: "NotifyWatcher"
		Id: "1"
		Request: "Next"
	}

This reply will only sent when something has changed.  Note that for this
particular watcher, no data is sent with the Next response. This can vary
according to the particular kind of watcher - some watchers may return
deltas, for example, or the latest value of the thing being watched.

Server->Client
	{
		RequestId: 1001
	}

The client can carry on sending Next requests for
as long as it chooses, each one returning only
when the machine has changed since the previous
Next request.

Client->Server
	{
		RequestId: 1002
		Type: "NotifyWatcher"
		Id: "1"
		Request: "Next"
	}

Finally, the client decides to stop the watcher. This
causes any outstanding Next request to return too -
in no particular order with respect to the Stop reply.

Client->Server
	{
		RequestId: 1003
		Type: "NotifyWatcher"
		Id: "1"
		Request: "Stop"
	}
Server->Client
	{
		RequestId: 1002
	}
Server->Client
	{
		RequestId: 1003
	}

As you can see, we use exactly the same RPC mechanism for watching as
for simple requests. An alternative would have been to push watch change
notifications to clients without waiting for an explicit request.

Both schemes have advantages and disadvantages.  I've gone with the
above scheme mainly because it makes the protocol more obviously correct
in the face of clients that are not reading data fast enough - in the
face of a client with a slow network connection, we will not continue
saturating its link with changes that cannot be passed through the
pipe fast enough. Because Juju is state-based rather than event-based,
the number of possible changes is bounded by the size of the system,
so even if a client is very slow at reading the number of changes pushed
down to it will not grow without bound.

Allocating a watcher per client also implies that the server must
keep some per-client state, but preliminary measurements indicate
that the cost of that is unlikely to be prohibitive.

Using exactly the same mechanism for all interactions with the API has
advantages in simplicity too.

* Authentication and authorization

The API server is authenticated by TLS handshake before the
websocket connection is initiated; the client should check that
the server's certificate is signed by a trusted CA (in particular
the CA that's created as a part of the bootstrap process).

One wrinkle here is that before bootstrapping, we don't
know the DNS name of the API server (in general, from
a high-availability standpoint, we want to be able to serve the API from
any number of servers), so we cannot put it into
the certificate that we generate for the API server.
This doesn't sit well with the way that www authentication
usually works - hopefully there's a way around it in node.

The client authenticates to the server currently by providing
a user name and password in a Login request:

Client->Server
	{
		RequestId: 1
		Type: "Admin"
		Request: "Login"
		Params: {
			"Tag": "machine-1",
			Password: "a2eaa54323ae",
		}
	}
Server->Client
	{
		RequestId: 1
	}

Until the user has successfully logged in, the Login
request is the only one that the server will respond
to - all other requests yield a "permission denied"
error.

The exact form of the Login request is subject to change,
depending on what kind user authentication we might
end up with - it may even end up as two or more requests,
going through different stages of some authentication
process.

When logged in, requests are authorized both at the
type level (to filter out obviously inappropriate requests,
such as a client trying to access the agent API) and
at the request level (allowing a more fine-grained
approach).

* Versioning

I'm not currently sure of the best approach to versioning.
One possibility is to have a Version request that
allows the client to specify a desired version number;
the server could then reply with a lower (or
equal) version. The server would then serve the
version of the protocol that it replied with.

Unfortunately, this adds an extra round trip to the
session setup. This could be mitigated by sending
both the Version and the Login requests at the same
time.

* Implementation

The Go stack consists of the following levels (high to low):

	client interface ("launchpad.net/juju-core/state/api".State)
	rpc package ("launchpad.net/juju-core/rpc".Client)
	----- (json transport over secure websockets, implemented by 3rd party code)
	rpc package ("launchpad.net/juju-core/rpc".Server)
	server implementation ("launchpad.net/juju-core/state/api".Server)
	server backend ("launchpad.net/juju-core/state")
	mongo data store