github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/doc/design/juju-api-implementation-guide.md (about) 1 # Juju API Implementation Guide 2 3 ## Status 4 5 *Work in Progress* 6 7 ## Contents 8 9 1. [Introduction](#introduction) 10 2. [Organization](#organization) 11 3. [Versioning](#versioning) 12 4. [Patterns](#patterns) 13 14 ## Introduction 15 16 ### Purpose 17 18 The *Juju API* is the central interface to control the functionality of Juju. It 19 is used by the command line tools as well as by the Juju GUI. This document 20 describes how functionality has to be added, modified, or removed. 21 22 ### Scope 23 24 The [Juju API Design Specification](juju-api-design-specificaion.md) shows how the 25 core packages of the API are implemented. They provide a communication between client 26 and server using WebSockets and a JSON marshalling. To expose methods as RPC, the API 27 server uses a registry of Facades, their public name, and their version to determine 28 how requests are dispatched to the associated method. 29 30 This document covers how those *factories*, *facades*, and *methods* have to be 31 implemented and maintained. The goal here is a clean organization following common 32 paterns while preserving compatability to older versions. 33 34 ### Overview 35 36 This document provides guidance on 37 38 - how the code should be organized into packages and types, 39 - how the versioning should be implemented, and 40 - recommended patterns for implementing new API types and methods. 41 42 ## Organization 43 44 Access to the business logic provided by the API is done through *facades*. Those 45 are grouping *connected functionality*, so that everything a worker or a client 46 needs can be provided by a single facade. The facade is specified in the 47 `Type` field of each request. Each facade is represented by a package which 48 implements and registers a *factory* for each version it supports. When a request 49 for a given type is received the factory for the requested version is used to create 50 a facade instance. This type implements a set of *methods* for handling of the 51 requests as described in the [specification document](juju-api-design-specification.md). 52 53 All API facade packages are located inside the package 54 [apiserver](https://github.com/juju/juju/tree/master/apiserver). For example, here you'll 55 find the package [agent](https://github.com/juju/juju/tree/master/apiserver/agent), which 56 implements the API interface used by Machine and Unit agents. 57 58 You can see that in the `init()` function it registers the factory for version 0 of the 59 facade with: 60 61 ``` 62 func init() { 63 common.RegisterStandardFacade("Agent", 0, NewAgentAPIV0) 64 } 65 ``` 66 67 This initial step has to be done for each factory/facade pair in each package. In case 68 When there are multiple versions, all are registered here. So when the next version is 69 added, the `init()` function will include the line: 70 71 ``` 72 common.RegisterStandardFacade("Agent", 1, NewAgentAPIV1) 73 ``` 74 75 The signature for factories inside the `apiserver` package is: 76 77 ``` 78 func(st *state.State, resources *common.Resources, auth common.Authorizer) (facade interface{}, err error) 79 ``` 80 81 Real implementations here don't return their API facades as empty interface but as 82 a typed instance: 83 84 ``` 85 func NewAgentAPIV0(st *state.State, resources *common.Resources, auth common.Authorizer) (*AgentAPIV0, error) 86 ``` 87 88 The usage of `common.RegisterStandardFacade()` takes care of wrapping this function before 89 registering the facade. The concrete facade types implement their methods according to the 90 allowed signatures described in the specification so that the RPC package can distribute 91 the requests to them. 92 93 ## Versioning 94 95 ***Remark*** 96 97 > Most of the current facade packages so far only implement the initial version and so 98 > don't contain the postfix `V0`. These versions represent the API that has been released 99 > with Juju 1.18. They will be refactored step-by-step when adding a `V1`. 100 101 ### Scenario 102 103 The folling description uses a fictional API for monitoring purposes. So a monitoring worker 104 on a machine can store vital data in state while other functions exist to retrieve them. We 105 start with version 1 here because the API didn't exist in Juju 1.18. 106 107 The initial version provides the function `WriteCPU()` and `WriteDisk()`, in version 2 the 108 function `WriteRAM()` is added while the arguments of `WriteCPU()` are changed. In version 3 109 `WriteCPU()`is then dropped in favor of `WriteLoad()`. 110 111 ### Server 112 113 #### Implementation 114 115 The implementation of version 1 of the monitoring API is done in a file named `monitoring_v1.go`. 116 Here the type `MonitoringAPIV1` is defined and its factory function registered like decribed 117 above. Also the initial functions are implemented here: 118 119 ``` 120 func (api *MonitoringAPIV1) WriteCPU(args params.CPUMeasurings) (params.ErrorResults, error) { 121 ... 122 } 123 124 func (api *MonitoringAPIV1) WriteDisk(args params.DiskMeasurings) (params.ErrorResults, error) { 125 ... 126 } 127 ``` 128 129 When implementing version 1 we want to reuse the already written code. So in a new file 130 named `monitoring_v2.go` in the same package the new type is defined by embedding the version 131 1: 132 133 ``` 134 type MonitoringAPIV2 struct { 135 MonitoringAPIV1 136 } 137 ``` 138 139 This way the new version already provides the functions of version 1 and can be registered 140 like its predecessor. Now the new function can be added: 141 142 ``` 143 func (api *MonitoringAPIV2) WriteRAM(args params.RAMMeasurings) (params.ErrorResults, error) { 144 ... 145 } 146 ``` 147 148 But beside adding a new functionality we also have to change our CPU monitoring functions 149 as descibed. It now expects different arguments, so those have to be versioned too. The 150 way Go embedds functions and the RPC mechanism resolves function calls we now can overload 151 the initial version by defining the function new on the version 2: 152 153 ``` 154 func (api *MonitoringAPIV2) WriteCPU(args params.CPUMeasuringsV2) (params.ErrorResults, error) { 155 ... 156 } 157 ``` 158 159 As said above the version 3 renames a function. Technically this means dropping an existing 160 one and adding a new one. The latter is no problem, it's like the adding of a new function 161 shown above, even if the functionality stays the same. Dropping a function is the more 162 complicated part and larger changes have to be done. First a private base type containing 163 the fields of version 1 has to be implemented in the file `monitoring.go`: 164 165 ``` 166 type monitoringAPIBase struct { 167 ... 168 } 169 ``` 170 171 Now in `monitoring_v1.go` the code of the API functions has to be moved into versioned private 172 functions of the newly created base. This base now has to be embedded and the versioned moved 173 code be called: 174 175 ``` 176 func (api *monitoringAPIBase) writeCPUV1(args params.CPUMeasurings) (params.ErrorResults, error) { 177 ... 178 } 179 180 type MonitoringAPIV1 struct { 181 monitoringAPIBase 182 } 183 184 func (api *MonitoringAPIV1) WriteCPU(args params.CPUMeasurings) (params.ErrorResults, error) { 185 return api.writeCPUV1(args) 186 } 187 ``` 188 189 Now also in the version 2 file move the code into according version functions of the base, 190 embed it, and call it from inside the public functions. Also remove the embedding of the 191 version 1. Instead the functions have to be implemented on the type itself and call the 192 embedded code like in version 1. Thankfully this is a quick task. 193 194 The coding of version 3 is now done the same way. First the new load monitoring function 195 is implemented as private base function. Then the according public function added on the 196 version 3 type. Again all exported functions are added like in version 2, only the removed 197 function will be left out: 198 199 ``` 200 func (api *monitoringAPIBase) writeLoadV3(args params.LoadMeasurings) (params.ErrorResults, error) { 201 ... 202 } 203 204 type MonitoringAPIV3 struct { 205 monitoringAPIBase 206 } 207 208 func (api *MonitoringAPIV3) WriteLoad(args params.LoadMeasurings) (params.ErrorResults, error) { 209 return api.writeLoadV3(args) 210 } 211 ``` 212 213 This way new or changed logic is implemented in their versioned files and can easily be 214 reused, changed, or dropped. 215 216 #### Testing 217 218 The testing of versioned APIs differs from most other unit tests. Each provided function 219 of each version of a facade has to be tested, but while some tests don't differ between 220 versions because the function didn't change others have to be reimplemented. So the idea 221 of organizing the tests and reuse the code is very similar to the solution for the 222 implementation described above. 223 224 Once again the tests are split into one base file and one file per version. The base file 225 `monitoring_test.go`contains a test suite with no pubic test functions. It's the container 226 for test variables and private prepared test functions. As long as there are no changes 227 needed it also may contain the definitions of `SetUpSuite()`/`TearDownSuite()` and 228 `SetUpTest()`/`TearDownTest()`. Otherwise those have to be implemented as private versioned 229 methods like the test methods themselves. 230 231 ``` 232 type baseSuite struct { 233 ... 234 } 235 236 func (s *baseSuite) SetUpTest(c *gc.C) { 237 ... 238 } 239 ``` 240 241 Now in `monitoring_v1_test.go` the tests for version 1 of the provided API functions 242 have to be implemented. Additionally, types to wrap the factory functions as well as 243 interfaces containing only one of the API functions to test have to be declared. Both are 244 used to inject the real versions later. 245 246 ``` 247 func factoryV1 func(st *state.State, resources *common.Resources, auth common.Authorizer) (interface{}, error) 248 249 func (s *baseSuite) testNewMonitorSucceedsV1(c *gc.C, factory factoryV1) { 250 ... 251 api, err := factory(s.State, s.resources, s.authorizer) 252 c.Assert(err, jc.ErrorIsNil) 253 ... 254 } 255 256 func (s *baseSuite) testNewMonitorFailsV1(c *gc.C, factory factoryV1) { 257 ... 258 } 259 260 type writeCPUV1 interface { 261 WriteCPU(args params.CPUMeasurings) (params.ErrorResults, error) 262 } 263 264 func (s *baseSuite) testWriteCPUSucceedsV1(c *gc.C, api writeCPUV1) { 265 ... 266 results, err := api.WriteCPU(args) 267 c.Assert(err, jc.ErrorIsNil) 268 ... 269 } 270 ``` 271 272 As long as the functionality of the versions doesn't change those tests can 273 be reused in future test. First the have to be integrated into the test suite 274 for version 1 in the same file. First we need a factory for this version: 275 276 ``` 277 func factoryWrapperV1(st *state.State, resources *common.Resources, auth common.Authorizer) (interface{}, error) { 278 return monitoring.NewMonitorAPIV0(st, resources, auth) 279 } 280 ``` 281 282 Now the versioned suite itself can be implemented: 283 284 ``` 285 type monitoringSuiteV1 struct { 286 baseSuite 287 } 288 289 var _ = gc.Suite(&monitoringSuiteV1{}) 290 291 func (s *monitoringSuiteV1) TestNewMonitorSucceeds(c *gc.C) { 292 s.testNewMonitorSucceedsV0(c, factoryWrapperV1) 293 } 294 295 func (s *monitoringSuiteV1) TestWriteCPUSucceeds(c *gc.C) { 296 s.testWriteCPUSucceedsV1(c, s.newAPI(c)) 297 } 298 ``` 299 300 Here `newAPI()` is a little but useful helper: 301 302 ``` 303 func (s *monitoringSuiteV1) newAPI(c *gc.C) *monitoring.MonitoringAPIV1 { 304 api, err := monitoring.NewMonitorAPIV1(s.State, s.resources, s.authorizer) 305 c.Assert(err, jc.ErrorIsNil) 306 return api 307 } 308 ``` 309 310 When now implementing the version 2 of the monitoring suite the factory wrapper 311 has to be reimplemented to return an instance of version 2 as well as the 312 private tests for the changed or added functions have to be written. The interface 313 for the `WriteCPU()` function needs a new version too because its signature changed. 314 315 ``` 316 func factoryWrapperV2(st *state.State, resources *common.Resources, auth common.Authorizer) (interface{}, error) { 317 return monitoring.NewMonitorAPIV2(st, resources, auth) 318 } 319 320 func (s *baseSuite) testWriteCPUSucceedsV2(c *gc.C, api writeCPUV2) { 321 ... 322 } 323 324 func (s *baseSuite) testWriteRAMFailsV2(c *gc.C, api writeRAMV2) { 325 ... 326 } 327 ``` 328 329 Now like already in version 1 the suite for version 1 can be implemented. It mostly 330 looks like its predecessor and the tests for `WriteDisk()` can reuse the version 1 331 tests of the base suite. The tests for `WriteRAM()` have to use the new base tests 332 instead while the tests for `WriteRAM()` are added. Also the little `newAPI()` helper 333 now has to return an instance of the version 2 API. 334 335 ``` 336 func (s *monitoringSuiteV2) TestWriteCPUSucceeds(c *gc.C) { 337 s.testWriteCPUSucceedsV2(c, s.newAPI(c)) 338 } 339 340 func (s *monitoringSuiteV2) TestWriteDiskSucceeds(c *gc.C) { 341 s.testWriteDiskSucceedsV1(c, s.newAPI(c)) 342 } 343 344 func (s *monitoringSuiteV2) TestWriteRAMFails(c *gc.C) { 345 s.testWriteRAMFailsV2(c, s.newAPI(c)) 346 } 347 ``` 348 349 Finally for version 3 the steps again are similar: 350 351 - Create the file `monitoring_v3_test.go` 352 - Add a factory wrapper returning the version 3 of the API 353 - Add an interface for the new `WriteLoad()` function 354 - Implement the private base tests for this new function 355 - Add the `monitoringSuiteV3` 356 - Implement `newAPI()` returning a version 3 API instance 357 - Add all known test methods but those for `WriteCPU()` tests 358 - Add tests for the new `WriteLoad()` using their according base tests 359 360 This way test suites can easily grow version by version. The code is distributed 361 to the versioned tests in a natural way, adding, changing, and removing is no 362 problem. 363 364 **Take care:** Don't forget tests for existing functions when implementing the 365 suite for a new version! 366 367 ### Client 368 369 #### Implementation 370 371 TBD. 372 373 #### Testing 374 375 TBD. 376 377 ## Patterns 378 379 ### Bulk Requests 380 381 When developing an API function always have in mind that it may not only be interesting 382 to use it for a single operation. Sometimes it's useful to perform the same operation 383 for a number of entities, e.g. instead of retrieving the information of one machine it 384 could make sense to read them at once. Here you surely could perform the function for 385 each instance individually, but this also creates a large overhead. 386 387 Another aspect of a too narrow design of an API function is when it could possibly used 388 in other facades too. It only depends on a small number of parameters controlling its 389 behavior, e.g. like the authorization in case of the `LifeGetter` in `apiserver/common`. 390 391 So even if there's only one use case regarding only a single operation for an API function 392 is known during implementation *always* design it to operate as *bulk request* for a larger 393 number of operations. Additionally check if the same logic could be reused in different 394 facades by exchanging only few parameters, like e.g. the authentication. 395 396 As an example take the monitoring API of above. Surely all functions could be implemented 397 for only one machine: 398 399 ``` 400 // Package "params". 401 type CPUMeasuring struct { 402 Id string `json:"id"` 403 Time int `json:"time"` 404 User int `json:"user"` 405 System int `json:"system"` 406 Nice int `json:"nice"` 407 Idle int `json:"idle"` 408 } 409 410 // Package "monitoring". 411 func (api *MonitoringAPIV0) WriteCPU(arg params.CPUMeasuring) (params.ErrorResult, error) { 412 ... 413 } 414 ``` 415 416 So in case of a temporary not available API server all enqueued measurings would have to 417 be written value by value, call by call (*OK, maybe not the best example, but think of 418 the enabling of the monitoring for 100 machines at once.*). To change that simply create 419 a wrapper type containing a slice of the interesting types: 420 421 ``` 422 type CPUMeasurings struct { 423 Measurings []CPUMeasuring `json:"measurings"` 424 } 425 ``` 426 427 Now use this as argument as well as the according bulk return type: 428 429 ``` 430 func (api *MonitoringAPIV0) WriteCPU(args params.CPUMeasurings) (params.ErrorResults, error) { 431 ... 432 } 433 ``` 434 435 In case of writing only one value it is still no problem. But this way we win the possibility 436 to also do bulk requests. 437 438 When defining the arguments and the result also *always* take care for an explicit serialication 439 like shown above. The naming scheme for arguments and the according results is: 440 441 ``` 442 type Thing struct { ... } 443 444 type Things struct { 445 Things []Thing `json:"things"` 446 } 447 448 type ThingResult struct { ... } 449 450 type ThingResults struct { 451 Results []ThingResult `json:"results"` 452 } 453 ``` 454 455 As we're talking about items of our cloud world the arguments as well as the result *must* have 456 nouns as identifiers, no combination of verb and noun according to the function. So in case of 457 the scenerio example above a type named `WriteDisk` would be a bad name for reusage. The name 458 `DiskMeasuring` instead also makes sense for a reusage when retrieving those values from state.