Merge "MuranoPL forms spec"

This commit is contained in:
Jenkins 2016-07-29 14:37:51 +00:00 committed by Gerrit Code Review
commit 17c07134a2
1 changed files with 521 additions and 0 deletions

View File

@ -0,0 +1,521 @@
..
This work is licensed under a Creative Commons Attribution 3.0 Unported
License.
http://creativecommons.org/licenses/by/3.0/legalcode
==============
MuranoPL forms
==============
https://blueprints.launchpad.net/murano/+spec/muranopl-forms
Murano has a YAML-based DSL to describe the UI form required by the
application. However it is completely independent from the application itself.
So the changes made to the application properties doesn't affect the form.
This specification is aimed to make UI definition be driven by the application
code and metadata rather than by a separate language.
Problem description
===================
Current UI definition format and the way Murano handles it comes with a bunch
of problems that are there by design and cannot be solved without making some
significant changes. Most notable problems are:
* It is monolithic. UI form defines all the inputs required not just by the
application but for all its object model subtree except for the other
application references. Thus it is impossible to have UI forms for
individual components. Neither it is possible for a user to choose the
desired implementation of those components without turning them into
applications (and putting in a separate packages).
* It contains yet another yaql-based DSL that defines the mapping between
input fields and application (and inner component) properties. However
this mapping is not bound to the property contracts. So any change made to
the properties or their contracts will not affect the form but only its
validation that happens upon deployment after the form is no longer visible
to the user.
* Because the input fields do not directly correlate to application properties
it is not possible to get the reverse mappings from object model to the
input fields. Because of that once the form is closed it is impossible to
edit entered values because they already transformed to some other structure
that corresponds to application's object model and we don't have any UI
definition for that structure.
* It also means that each property needs to be described in several places in
different syntax thus duplicating the work. It is also easy to fail or get
out of sync with the code if one changes the contract but forgets to update
the UI definition or vice versa.
* It is one more DSL to learn. One more DSL invented in Murano and not covered
by any standard.
* Its object model template part (yaql mappings) use functions that are not
available in MuranoPL and vice versa. And its syntax has its own number of
problems like inability to define an object and reference it from several
places.
* It requires the client be aware of several OpenStack entities like images,
flavors etc. For example murano-dashboard talks to nova to get the list of
available flavors. This brings two additional problems:
a. It cannot be used for application that is not targeting OpenStack or
targeting OpenStack cloud other than the one used to run the dashboard.
b. It is not extensible. Each new selector requires special type in UI
definition that must be know to the client.
* Applications cannot dynamically control content of the form. For example it
is not possible to populate values for a drop-down list if those values are
not constants that are known in advance.
* Neither murano-dashboard nor the UI definition syntax support non-scalar
properties (dictionaries, lists etc.). Thus it is easy to come up with a
class design that is very useful but yet cannot be shown in UI.
* There can be only one UI definition form per package.
Proposed change
===============
Solution overview
-----------------
Proposed solution is to implement two major paradigm changes:
* Switch from the current UI definition format to an auto generated form
definitions that are not supposed to be written manually. Form generator is
going to accept MuranoPL class as an input and produce json-schema based
output that has all the required information to both present the form to a
user and do a client-side validation of the input.
* Make that UI schemas be per-class. There will be a new API call to request
UI definition for particular class or even particular method of that class
rather than for a package.
As a result the UI workflow to add new application is going to be like this:
#. User wants to add application x.y.z identified by the application package
FQN.
#. Client asks for UI definition for that package using existing API call
(the one that returns UI definition in existing "old" format).
If the call is successful and UI definition was obtained then the rest of
workflow is remained as it is now.
#. Otherwise the client (dashboard) requests UI schema for the class x.y.z
using new API call.
#. API sends murano-engine request to generate UI schema for the class x.y.z.
#. Engine loads class definition from appropriate package including attached
meta-values.
#. Taking class property declarations (contracts) and attached meta-values
as an input, engine generates json-schema that has everything needed to
present the UI form and do most of the client-side validation.
#. The same is done for all model builder methods (see below) using the same
algorithm but using their arguments instead of class properties as an input.
#. List of schemas is sent back to the API and, in turn, returned to the
caller (client).
#. The client decides which of the schemas to use (usually by asking the user).
It can always go with default schema for the class or take advantage of
one of the schemas provided by model builder methods.
#. Using the extended json schema client generates UI form. The form is going
to have 1-1 mapping between input controls and class properties or method
arguments.
#. After the form was filled it is validated on the client side using given
json schema (and its optional murano-specific extensions) and then submitted
to the API.
#. If the client opted to go with one of the model builder schemas the form
output is sent to the engine in attempt to call the model builder method
using the form values as an argument values. Model builder will then
return modified object model that is sent back to the client (via API).
Then the client can continue with the form edit using default schema or
apply additional model builder method.
#. Otherwise the constructed object model is inserted into environment
definition into API's database.
To change the application settings later, the same workflow is applied. The
only difference is that it doesn't try to use the old UI definition approach
but instead immediately requests new UI schema for the type specified in the
object model.
Schema generation
-----------------
Schema generation is a special service provided by the API (through the RPC
call to murano-engine) that takes a class FQN (including version or version
spec) and optional method name and produces json-schema compliant JSON. The
schema may have extra attributes for information that cannot be expressed by
standard json-schema attributes alone (for example field order).
If no method was provided the generation code takes class declaration and tries
to produce json-schema records by running YAQL expression contracts for its
properties in a special yaql context where all contract functions are
redefined to produce json-schema rather than validate the input. Thus it
creates a different implementation of the same contract syntax. In addition
the generation code may take into account number of well-known Meta classes
from the core library (to be added) and alter the schema if corresponding Meta
is attached to the properties. Meta values can specify things that cannot be
taken from contracts alone. For example it can be a property description text.
For the check() contract that accepts arbitrary yaql expression the analysis
of the expression AST is performed:
#. If the expression matches EXPR1 and EXPR2 and ... and EXPRN pattern it is
split into several validations (as if it was written as
``check(EXPR1).check(EXPR2)...check(EXPRN))``.
#. Each of (sub)expressions is checked against supported patterns that can
be translated to json-schema:
* comparison of len($) for a string len
* regex match function
* number comparison
* `in` operator that can be converted to enum
All expressions that cannot be translated with this algorithm are ignored and
the value will not be validated on the client side.
If the property/argument has a `Default` specifier it is translated to
`default` schema property.
For the class() the generated schema type is going to be `muranoObject`
with the following attributes:
* `muranoType` - FQN of the base class;
* `version` - version or version-spec that should be used to obtain
`muranoType` (as seen in the manifest);
* `owned` which can be `true`, `false` or null (or missing which means the
same as null). `true` means that the object must be owned by the parent
(thus ID of existing object in object model cannot be provided here),
`false` means that only existing object can be referenced and `null`
means that both options are valid.
Custom json-schema type is needed in order to retain reference semantics that
is it is an object which real type needs to be looked up in the catalog rather
than some plain dictionary or string. Client must understand `muranoObject`
type and know how to get list of valid type inheritors.
When `check()` contract is used to validate MuranoObject value (i.e. the
result of `class()` contract) it may put some constraints on that object's
properties or properties of some inner objects. In this case the schema
generator can emit additional `context` attribute to the property schema.
`context` is set to json-schema for the nested object (or its children).
Upon the input of a referenced object the client should check it against
all the `context` schemas up in the object model tree.
By default the generation algorithm produces the schema for the class and
for each model builder method available.
If the method name was provided to the engine command the same algorithm is
applied to that method only and its arguments are used where the class
properties would be used otherwise. In this case methods can be any methods
that can be invoked by the API which currently are actions and model builder
methods.
Model builders
--------------
Model builders are special MuranoPL methods that take a class definition
(in an object model format dictionary form) and number of optional arguments
and return modified object model.
Model builders are used to simplify object model generation using the template
obtained from its parameters. When generating json schema from such methods
their first parameter (which is current object model) is skipped.
In order for a method to be considered a model builder it must have the
following properties:
#. It must be static (`Usage: Static`)
#. In must have the public scope (`Scope: Public`. I.e. it must be a static
action. See https://blueprints.launchpad.net/murano/+spec/static-actions for
more information on static actions)
#. It must have `io.murano.metadata.ModelBuilder` `Meta` applied to it.
This is a marker class that is going to be introduced to the core library
to distinguish model builders from other static actions.
Caller uses static actions API to invoke the builder and obtain a generated
object model snippet.
UI hints
--------
In addition to the information that can be obtained from contracts some
additional information is needed to produce correct representation for
the property or argument. This information is provided by meta-classes
that need to be introduced to the core library:
`io.murano.metadata.Title`: title of an entity. Can be applied to anything.
The value is in `text` property of a meta-class. Upon schema generation
it is translated to `title` schema key. If no meta is attached then the
property/argument name is used as a title.
`io.murano.metadata.Description`: description of an entity. Can be applied to
anything. The value is in `text` property of the meta-class. Upon schema
generation it is translated to `description` schema key.
`io.murano.metadata.HelpText`: help text of an entity. Can be applied to
anything. The value is in `text` property of the meta-class. Upon schema
generation it is translated to `helpText` schema key.
`io.murano.metadata.forms.Hidden`: marks property or argument to be invisible.
Upon schema generation `"visible": false` is produced.
`io.murano.metadata.Position`: position of the property/argument within the
form. It has two properties:
* `index`: integer by which all of the fields are sorted before rendering.
it doesn't have to be consecutive. If the inherited field has the same
index as the field from the generated class then inherited one goes first.
For this matter property indexes might be re-enumerated upon schema
generation to the consecutive unique indexes. This property is translated
to `formIndex` schema key. If no position specified then the field will be
placed in the list of unordered fields (probably sorted by their title).
* `section': section name for the field. If not provided then it will be
automatically placed in default section for all such fields.
Section name is represented as `formSection` key in the schema of each
field. Additional attributes for the section with that name can be found
in the root schema for the type/method.
`io.murano.metadata.forms.Section`: specifies form sections for the class.
Can be multiple times applied either to the class or to the method.
It has the following properties:
* `name`: name of the section that is going to be used in
`io.murano.metadata.Position` instances.
* `title`: title of the section. In no title provided it is assumed to be
equal to the section mame.
* `index`: index of the section in a section list (e.g. tab number).
Similar to `Position` indexes those numbers doesn't have to be consecutive
and only used to sort sections within the form.
Child classes may redefine sections inherited from their parent classes by
re-declaring section with the same name.
Sections are translated to the
::
"formSections": {
"mySectionName1": {
"title": "text1",
"index": 0
},
"mySectionName2": {
"title": "text2",
"index": 1
}
}
entries in the root schema of the type or method.
Alternatives
------------
Instead of switching to json-schema we could generate UI definition in existing
(or improved) UI definition format.
Data model impact
-----------------
None
REST API impact
---------------
**GET /schemas**
Execute static MuranoPL method. Method must have a Public scope.
*Request*
+---------+--------------------------------+-------------------------------------+
| Method | URI | Description |
+=========+================================+=====================================+
| GET | /schemas/{className} | Obtain json-schema for class |
+---------+--------------------------------+-------------------------------------+
| GET | /schemas/{className}/{methods} | Obtain json-schema for class method |
+---------+--------------------------------+-------------------------------------+
Parameters:
* `className`: name of the class
* `classVersion`: version or version spec for the class. Optional. If not
provided then '=0' is assumed
* `packageName`: optional FQN of the package. If provided the class will only
looked up there instead of full catalog.
* `methods`: model builder method name or list of names which schemas are
requested. Empty string indicates schema of the class rather than of one of
its methods. If the parameter is absent then all the schemas (both class and
model builders) are returned.
*Response*
::
{
"": {
# json-schema for the class
},
"myModelBuilder1": {
# json-schema for the myModelBuilder1 method
}
}
HTTP codes:
+----------------+-----------------------------------------------------------+
| Code | Description |
+================+===========================================================+
| 200 | OK. Schema was generated successfully |
+----------------+-----------------------------------------------------------+
| 400 | Bad request. |
+----------------+-----------------------------------------------------------+
| 401 | User is not allowed to access the schema |
+----------------+-----------------------------------------------------------+
| 404 | Not found. Specified class or doesn't exist |
+----------------+-----------------------------------------------------------+
Versioning impact
-----------------
If we introduce more capabilities to the contracts then a new FormatVersion
should be introduced.
New murano-dashboard/python-client could still talk to an older API service
that lacks new API call and uses old UI definition alone in this case.
New approach is backward compatible so existing applications will still work.
Other end user impact
---------------------
A new python-muranoclient with a method to obtain json-schema for the class
will be required in order to take advantage on the MuranoPL forms.
Deployer impact
---------------
Maximum number of class implementations need to be specified in murano.conf
file. However it is going to have a reasonable default value.
Developer impact
----------------
In order to have rich GUI, an application developer will have to decorate all
his properties with lots of Meta values. Otherwise if no UI definition file
was provided the UI form may present input fields in a random order with a
labels set to a property name which is not very user friendly.
We should design a way to extract all visual hints into a separate per-class
file to separate them from the application code. This is a subject for another
spec.
Murano-dashboard / Horizon impact
---------------------------------
murano-dashboard should present user with the form constructed from a json
schema. The schema would contain all the required visual hints in the extra
attributes (not defined by the json-schema standard).
The dashboard may either construct the form on its own or use 3rd party
libraries that are capable to generate UI from the schema. In the later case
dashboard might become responsible for generating form definition - a structure
describing visual aspects of the form that is provided in addition to the
schema. Such structure might be produced by extracting required information
from extra attributes of the schema. The dashboard might also split it into
several forms/schemas in order to have a wizard UI rather than a single form.
Implementation
==============
Assignee(s)
-----------
Primary assignee:
Stan Lagun <istalker2>
Work Items
----------
* Create new API method;
* Write python-muranoclient method for new API call;
* Implement RPC method in murano-engine that will do schema generation;
* Write json-schema generator for the class/method;
* Define all the mentioned meta-classes and enhance schema generator to make
use of them.
Dependencies
============
* https://blueprints.launchpad.net/murano/+spec/static-actions
Testing
=======
All the path from the MuranoPL code to the rendered UI form can be tested
by the unit tests. Transformation from MuranoPL to json-schema can be tested
independently from the one from json-schema to the form definition or even
HTML layout.
Documentation Impact
====================
The following need to be documented:
* The new UI workflow.
* json-schema specification link along with description of extra fields added
by Murano;
* All changes made to the contracts;
* Standard Meta classes that can be used for visual hints in MuranoPL;
* Model builder methods documentation;
* Developers guide.
References
==========
* http://json-schema.org
* https://blueprints.launchpad.net/murano/+spec/static-actions