Hi guys, it's been a while but here comes the latest revision of UI Plugins proof-of-concept patch (please find it attached).
This revision was originally meant to focus solely on server-side components of
the plugin infrastructure. However, I ended up implementing all the major
concepts and ideas as discussed on engine-devel mailing list, impacting both
client-side and server-side parts of the plugin infrastructure. As a result, UI
plugin infrastructure should be pretty much complete now, so we can focus on
specific plugin API features in upcoming PoC revisions.
There's a whole bunch of changes and improvements in this revision, so I'll try
to cover all the relevant parts step by step. If you have any comments,
questions or ideas, please let me know!
So here we go... (or if you just want to get the patch, find the link at the
end of this message)
0. Added new Engine configuration values
UI plugin data path is represented by ConfigValues.UIPluginDataPath enum option
("UIPluginDataPath" in vdc_options table), and resolved relative to
ConfigValues.DataDir if possible. Following (default) values:
* UIPluginDataPath = ui-plugins
* DataDir = /usr/share/ovirt-engine
result in UI plugin data path: /usr/share/ovirt-engine/ui-plugins
UI plugin config path is represented by ConfigValues.UIPluginConfigPath enum
option ("UIPluginConfigPath" in vdc_options table), and resolved relative to
ConfigValues.ConfigDir if possible. Following (default) values:
* UIPluginConfigPath = ui-plugins
* ConfigDir = /etc/ovirt-engine
result in UI plugin config path: /etc/ovirt-engine/ ui-plugins
1. Processing UI plugin data on the server
PluginDataManager is the class responsible for reading, validating and caching
UI plugin descriptor/configuration data on the server (Engine). It has two main
responsibilities:
* return a snapshot of currently valid plugin data ( getCurrentData method
)
* reload plugin data from local file system if necessary ( reloadData
method)
The reloadData method doesn't modify "live" plugin data directly. Instead, it
creates a local working copy of current plugin data, updates this copy as it
reads/validates plugin descriptor and configuration files, and attempts to
update "live" plugin data through conditional reference re-assignment (using
java.util.concurrent.atomic.AtomicReference.compareAndSet method).
In other words, reloadData method makes no attempts with regard to Java
lock-based synchronization, in favor of dealing with "live" data through
AtomicReference (reference that involves atomic volatile reads and writes):
* In the best case, a thread will succeed in updating "live" data (
AtomicReference.compareAndSet == true), which means that "live" data remained
unchanged since this thread acquired a reference of current plugin data.
* In the worst case, a thread will NOT succeed in updating "live" data (
AtomicReference.compareAndSet == false), which means that "live" data was
already changed by another thread since this thread acquired a reference of
current plugin data.
In my opinion, when dealing with external resources like the local file system,
this is a good compromise between performance and up-to-date data. While we
might not get "completely-up-to-date" data at the given point in time (
reloadData + getCurrentData ), we are guaranteed to get "recently-up-to-date"
and consistent data. In other words, the requirement of "completely-up-to-date"
data would involve synchronized statements that would hurt performance. In my
(very humble) opinion, the benefit of having "completely-up-to-date" data , at
the cost of reduced performance, is not really worth it, especially in our case
when the user can just hit refresh (F5) to reload WebAdmin and its plugin data.
Plugin descriptor files are expected to be placed in UI plugin data path , for
example: /usr/share/ovirt-engine/ui-plugins/foo.json
Following descriptor file attributes are implemented and recognized by the
plugin infrastructure:
* name : A name that uniquely identifies the plugin (required attribute).
* url : URL of plugin host page that invokes the plugin code (required
attribute).
* config : Default configuration object associated with the plugin
(optional attribute).
* resourcePath : Path to plugin static resources, relative to UI plugin
data path (optional attribute). This is used when serving plugin files through
Engine PluginResourceServlet (more on this below).
Plugin configuration files are expected to be placed in UI plugin config path,
for example: /etc/engine/ui-plugins/foo-config.json
Note that plugin configuration files follow the
"<descriptorFileName>-config.json" convention.
Following configuration file attributes are implemented and recognized by the
plugin infrastructure:
* config : Custom configuration object associated with the plugin (optional
attribute). This overrides the default plugin descriptor configuration, if any.
* enabled : Indicates whether the plugin should be loaded on WebAdmin
startup (optional attribute). Default value is 'true'.
* order : Defines the relative order in which the plugin will be loaded on
WebAdmin startup (optional attribute). Default value is Integer.MAX_VALUE
(lowest order).
The concept of merging custom configuration ( config attribute in
foo-config.json ), if any, on top of default configuration ( config attribute
in foo.json ), if any, remains unchanged. This makes the plugin configuration
quite flexible - in my opinion, the added complexity of handling/merging such
configuration is definitely worth the effort.
The enabled attribute is straight-forward, allowing users to turn the given
plugin off, if necessary. In future, users should still be able to load such
plugins through WebAdmin GUI.
The order attribute controls the order in which plugins are loaded on WebAdmin
startup. Since plugin resources are fetched asynchronously by the browser, this
is basically a way of imposing some degree of determinism in the
"generally-non-deterministic" plugin environment, which is helpful when
troubleshooting problems with multiple plugins. This attribute is also helpful
due to file listing methods in java.io.File giving no guarantees that files
would be listed in any particular order (otherwise we could just go for the
"NN-<descriptorFileName>.json" convention, with NN being the order number).
2. Modified behavior of WebadminDynamicHostingServlet
WebadminDynamicHostingServlet is the servlet used to serve WebAdmin application
host page (HTML page that bootstraps WebAdmin JavaScript code).
In addition to its former behavior, as part of handling the given request,
WebadminDynamicHostingServlet :
* reloads descriptor/configuration data from local file system if
necessary, and obtains a snapshot of currently valid plugin data (
PluginDataManager.reloadAndGetCurrentData )
* embeds all plugin meta-data, suitable for use in client-side plugin
infrastructure , into WebAdmin host page as "pluginDefinitions" JavaScript a
rray ( PluginDefinitions )
As a result, reloading UI plugin descriptor/configuration data is as simple as
refreshing (F5) WebAdmin application in the browser (no need to restart
Engine).
3. Added servlet for serving plugin static resources
PluginResourceServlet is the servlet used to serve UI plugin static files
(plugin host page, 3rd party JavaScript, etc.) from the local file system.
For example, requesting URL:
*
http://<EngineManagerHost>:8700/webadmin/webadmin/plugin/foo/content/start.html
will send the content of:
* /usr/share/ovirt-engine/ui-plugins/<resourcePath>/ content/start.html
to the client.
As shown in the above example:
* /webadmin/webadmin/plugin/ is the servlet root path for
PluginResourceServlet
* in the extra path beyond the servlet root path ( /foo/content/start.html
):
* /foo represents the name of the plugin
* /content/start.html represents the path to requested resource,
relative to " UIPluginDataPath / < resourcePath >"
Note that each plugin using PluginResourceServlet to serve its static files
must declare non-empty resourcePath attribute in within the plugin descriptor.
Also note that PluginResourceServlet , unlike WebadminDynamicHostingServlet ,
does NOT reload descriptor/configuration data from local file system as part of
handling the given request. In other words, it's assumed that plugin data has
already been (re)loaded when serving WebAdmin application host page, with
subsequent requests to PluginResourceServlet reading the current plugin
information.
Until we solve the cross-origin issue in a clean way, PluginResourceServlet
should be used to serve all plugin resources from local file system.
4. Plugin lifecycle improved to deal with misbehaving plugins
PluginState enum has been modified to deal with plugins that allow uncaught
exceptions to escape from plugin event handler functions (e.g. "UiInit"):
* removed state INITIALIZED
* added state INITIALIZING : The plugin is (currently) being initialized by
calling UiInit event handler function.
* added state IN_USE : Plugin's UiInit event handler function has completed
successfully, we can now call other event handler functions as necessary. The
plugin is in use now.
* added state FAILED : An uncaught exception escaped while calling an event
handler function, which indicates internal error within the plugin code. The
plugin is removed from service.
I've attached a simple state diagram that illustrates different states and
transitions between them (green color is initial state, red color is end
state).
Uncaught exceptions in plugin event handler functions will be caught and
handled by the plugin infrastructure. This prevents a misbehaving plugin from
breaking WebAdmin application, since WebAdmin is the caller (initiator) of the
function call. In such case, the plugin will be removed from service.
Update on cross-origin issue (consequence of same-origin policy)
In order for the plugin to access WebAdmin plugin API, plugin host page (e.g.
start.html ) must be served from URL on same origin as Engine origin.
Otherwise, plugin code running in the context of an iframe'd host page will
fail to evaluate "parent.pluginApi" expression, with "parent" being top-level
(WebAdmin) window, and "pluginApi" being the global plugin API object exposed
by WebAdmin.
This is why PluginResourceServlet , available on Engine origin, should be used
to serve all plugin resources from local file system.
There's only one issue that remains to be solved: cross-origin "plugin vs.
remote service" communication, with "remote service" being anything other than
Engine (REST API). In future, we'll address this with Apache reverse proxy
configuration, so that users can configure Apache server (placed in front of
Engine JBoss AS) to put arbitrary (local or remote non-Engine) services on same
origin. However, this requires a change in current Apache configuration. Until
then, users can manually edit the Engine Apache configuration file (
/etc/httpd/conf.d/ovirt-engine.conf ).
I've attached some sample plugin files for you to experiment with. Instead of
attaching actual patch file (92 kB) to this email, I've submitted the patch to
oVirt Gerrit: http://gerrit.ovirt.org/8120
Let me know what you think!
Cheers,
Vojtech
ui-plugin-sample-files.tar.gz
Description: application/compressed-tar
<<attachment: ui-plugin-lifecycle.png>>
_______________________________________________ Engine-devel mailing list [email protected] http://lists.ovirt.org/mailman/listinfo/engine-devel
