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.