Distributed Configuration via Multiple Interfaces and Scope

Work In Progress


Overview

An investigation ( FOLIO-2875 - Getting issue details... STATUS ) was conducted into the feasibility of using OKAPI "multiple" interfaces and "scope" to handle request routing for distributed configuration.  This page serves as a place capture the findings of that spike.

Experiment

An experiment was setup to exercise the multiple interface / scope functionality in the context of a distributed configuration scenario

Modules

Two very simple (nearly identical) nodejs/express modules were created for the purposes of this experiment.  They both implement a dummy "distributed_configuration" interface which is of type "multiple", as well as another interface to help distinguish the two modules from one another. 

Note:

  • Both modules define the "distributed_configuration" interface, which is of type "multiple".
  • "scope" is set to correspond with the module (foo/bar)
  • The "permissionsRequired" for the various configuration endpoints is different between the two modules.  This provides greater granularity - e.g. a user could have permissions to CRUD config entries for one but not the other.

 See the module descriptors below for additional details.

mod-hello-foo-1.0.0
{
  "id": "mod-hello-foo-1.0.0",
  "name": "Hello foo module",
  "provides": [{
    "id": "foo",
    "version": "1.0",
    "handlers": [{
      "methods": [
        "GET"
      ],
      "pathPattern": "/foo",
      "permissionsRequired": []
    }]
  },
  {
    "id": "distributed_configuration",
    "interfaceType": "multiple",
    "version": "1.0",
    "handlers": [{
      "methods": [
        "GET"
      ],
      "pathPattern": "/configurations/entries",
      "permissionsRequired": [ "foo.configuration.entries.collection.get" ]
    },
    {
      "methods": [
        "GET"
      ],
      "pathPattern": "/configurations/entries/{id}",
      "permissionsRequired": [ "foo.configuration.entries.item.get" ]
    },
    {
      "methods": [
        "POST"
      ],
      "pathPattern": "/configurations/entries",
      "permissionsRequired": [ "foo.configuration.entries.item.post" ]
    },
    {
      "methods": [
        "PUT"
      ],
      "pathPattern": "/configurations/entries/{id}",
      "permissionsRequired": [ "foo.configuration.entries.item.put" ]
    },
    {
      "methods": [
        "DELETE"
      ],
      "pathPattern": "/configurations/entries/{id}",
      "permissionsRequired": [ "foo.configuration.entries.item.delete" ]
    }],
    "scope": ["foo"]
  }],
  "requires": [],
  "launchDescriptor": {
    "exec": "node hello_foo.js"
  }
}
mod-hello-bar-1.0.0
{
  "id": "mod-hello-bar-1.0.0",
  "name": "Hello bar module",
  "provides": [{
    "id": "bar",
    "version": "1.0",
    "handlers": [{
      "methods": [
        "GET"
      ],
      "pathPattern": "/bar",
      "permissionsRequired": []
    }]
  },
  {
    "id": "distributed_configuration",
    "interfaceType": "multiple",
    "version": "1.0",
    "handlers": [{
      "methods": [
        "GET"
      ],
      "pathPattern": "/configurations/entries",
      "permissionsRequired": [ "bar.configuration.entries.collection.get" ]
    },
    {
      "methods": [
        "GET"
      ],
      "pathPattern": "/configurations/entries/{id}",
      "permissionsRequired": [ "bar.configuration.entries.item.get" ]
    },
    {
      "methods": [
        "POST"
      ],
      "pathPattern": "/configurations/entries",
      "permissionsRequired": [ "bar.configuration.entries.item.post" ]
    },
    {
      "methods": [
        "PUT"
      ],
      "pathPattern": "/configurations/entries/{id}",
      "permissionsRequired": [ "bar.configuration.entries.item.put" ]
    },
    {
      "methods": [
        "DELETE"
      ],
      "pathPattern": "/configurations/entries/{id}",
      "permissionsRequired": [ "bar.configuration.entries.item.delete" ]
    }],
    "scope": ["bar"]
  }],
  "requires": [],
  "launchDescriptor": {
    "exec": "node hello_bar.js"
  }
}

Execution

Both modules were registered and enabled with OKAPI, then several requests were made.  Note:  the permissions module was not enabled for this experiment

$ curl "localhost:9130/_/proxy/modules?provide=distributed_configuration&scope=foo" -w'\n'
[ {
  "id" : "mod-hello-foo-1.0.0",
  "name" : "Hello foo module"
} ]

$ curl localhost:9130/configurations/entries -H "X-Okapi-Tenant: cmcnally" -w'\n' -H "X-Okapi-Module-Id: mod-hello-foo-1.0.0"
[
  {
    "configName": "format",
    "value": "xml"
  }
]

$ curl "localhost:9130/_/proxy/modules?provide=distributed_configuration&scope=bar" -w'\n'
[ {
  "id" : "mod-hello-bar-1.0.0",
  "name" : "Hello bar module"
} ]

$ curl localhost:9130/configurations/entries -H "X-Okapi-Tenant: cmcnally" -w'\n' -H "X-Okapi-Module-Id: mod-hello-bar-1.0.0"
[
  {
    "configName": "format",
    "value": "json"
  },
  {
    "configName": "timeoutMs",
    "value": 5000
  }
]

Other Considerations

Is there anything that RMB/Folio-spring-base could provide to make this easier?

  • I personally don't see any problem with have modules explicitly define the "distributed_configuration" interface in their module descriptors, but I suppose it could be viewed as verbose / repetitive.  
  • I don't see any reason why it wouldn't be possible for RMB/Folio-spring-base to provide default implementation(s) to reduce the amount of code required to be written for modules adopting this. (info) JIRA needed

Required changes for Stripes?

  • Assuming the distributed_configuration interface mimics/mirrors the existing "configuration" interface, Stripes would need to make calls to OKAPI, providing the appropriate scope to learn which value to send in X-Okapi-Module-Id when making calls to CRUD configuration entries.
  • I think this can be done in a stripes-smart-component like https://github.com/folio-org/stripes-smart-components/tree/master/lib/ConfigManager (info) JIRA needed 
  • NOTE:  This is also true for backend modules which consume configuration entries which are managed by other modules
    • Is this something that is done today?  Possibly in some places like circulation (info) Possible JIRA needed (RMB/Folio-spring-base)? 

What happens when modules provide different versions of the distributed_configuration multiple interface?  Is that allowed?  Is it a problem?

  • Apparently it's not possible to specify a dependency on a multiple interface, so clients will need to. 
  • It might be helpful to extend/enhance some of the OKAPI APIs for querying interfaces
    • Extending the provides query to allow the specification of a version could be helpful, e.g. /_/proxy/modules?provide=distributed_configuration:2.0&scope=orders (info) Possible JIRA needed 
    • Extending /_/proxy/tenants/{tenant}/interfaces may also make sense (info) Possible JIRA needed
  • If the interface is being changed often it could become a pain for modules which require the distributed_configuration interface.  As a point of reference, mod-configuration's configuration interface has been at 2.0 for 4 years now, so it seems unlikely that we'll need to change this frequently.

Guidance on usage of scope?

  • A naming convention will likely help here.  
    • scope is an array, so maybe it should be a list of the interfaces provided by the module, or at least those which store configuration
  • Should a uniqueness constraint be placed upon "scope"?  How should clients handle the case when multiple module IDs are returned from a query using scope?

What about business logic modules?

  • One reason I think mod-configuration has been so widely adopted is that there's a low barrier to entry - just make a couple API calls.  You don't need to deal with databases, etc.  This is ideal for UI modules as well as business logic modules.  Pulling postgres into all those business logic modules is probably not desirable.  However, most of the business logic modules have a corresponding storage module.  These are a more natural place to store the configuration.  The BL module would call the storage module to get config entries instead of calling mod-configuration - seems simple enough.  The UI could either call the storage module directly (I know some UI modules do this already), or the BL module could also implement the distributed_configuration interface, and act as a proxy to the storage layer.  It could be handy to have a default implementation of this too.  (info) Possible JIRA needed.

Summary

As demonstrated through experimentation, it's possible to implement distributed configuration in a way which provides a consistent interface across modules via multiple interfaces.  Okapi's scope functionality helps clients determine which module Id to provide in the call, and removes the need for clients to know about particular module implementations (e.g. module names/versions - mod-hello-foo-1.0.0), but instead only need to know about an interface-level scope (e.g. foo).  There are possibly enhancements that can/should be made in Stripes-smart-components/Okapi/RMB/folio-spring-base to make adoption of this easier.

JIRAs