2014-10-06

New Brooklyn Blueprint Features

This post describes a new feature added to Apache Brooklyn in a recent pull request. The feature was based in part on user requirements and partly to support some work being done with Clocker placement strategies. Previously, to create an object for use by a Brooklyn Entity there had to be a specific piece of syntax in the DSL, with associated code to instantiate that type of object or the blueprint had to be written in Java. There was a mix of complex type coercions from String to various classes, such as the ProxySslConfig class for Nginx, which were not extensible or re-useable, and DSL methods like $brooklyn:entity("id") to reference specific types of object.

Blueprint Object Method

The code in pull request #182 adds another DSL method that can create and inject any object required into a Brooklyn blueprint. This means any Java DTO, whether defined as part of the Brooklyn code or externally in some library and passed on to a running entity. Additionally it extends the ConfigKey typed configuration mechanism to allow arbitrary objects to use the same configuration definition and documentation classes as entities, policies, enrichers and locations. This makes Brooklyn much more consistent and blueprints much easier to create and much more powerful as well!

The new DSL method is named object and is invoked using the $brooklyn: prefix as $brooklyn:object: followed by a map of arguments. It can be used anywhere this syntax is valid, including in its own argument map, and it is possible to create nested trees of objects in this way. The method recognises three keys in the map of arguments it is passed: the object type, and either a map of fields or a map of configuration keys. Any left over and unmatched keys here, or in the field and configuration maps will be ignored.

The following example shows how the DSL method could be used to instantiate a brooklyn.example.TestObject object, which is to be configured using Brooklyn ConfigKeys. Note that one of the keys, connectionProperties, is itself an object definition, this time of a com.example.ConnectionProperties object, which could be part of the configuration for a database client. Here, the hostname field of this object is being set using the attributeWhenReady method on a referenced entity, to resolve the host.name sensor value. This shows the complex assemblages of objects and configuration that can be created using the Brooklyn CAMP blueprint DSL.

$brooklyn:object:
  objectType:
    brooklyn.example.TestObject
  brooklyn.config:
    database:
      $brooklyn:component("database")
    connectionProperties:
      $brooklyn:object:
        objectType: com.example.ConnectionProperties
        object.fields:
          username: "guest"
          password: "gu3st"
          port: 31337
          hostname: $brooklyn:component("database").attributeWhenReady("host.name")
    wars.list:
    - "http://example.org/test1.war"
    - "http://example.org/test2.war"

Object Configuration

This section documents the configuration used to create an object with the Brooklyn CAMP DSL, describing each of the required values.

First the class name used to instantiate the object must be set. This can be done using any of the keys type, objectType or object_type. The value should be a string with the full package and class name of the required object. Brooklyn will use its default classpath for resolution of the class name, looking in the lib/brooklyn, lib/patch and lib/dropins directories. Extra catalog classpath entries, versioning and OSGi will be supported at a later date. Any class on the classpath can be used, as long as it has a public zero-argument constructor.

The object.fields key is used to specify a map of fields to set on the new object. These are processed using the Apache Commons BeanUtils project, and should consist of keys representing the field name (which should be accessible via a public setter using the JavaBean conventions) and an appropriate value to set. As noted earlier, the value could be another object construction, any supported YAML primitive value or another DSL method such as an entity or sensor reference, or a deferred attribute access. The deferred accessors will be created as Tasks and resolved at runtime as the blueprint application is started.

Finally, brooklyn.config can also be specified with another map of data. This is used in the same way as an entitySpec definition, and will attempt to set fields annotated with @SetFromFlag using reflection. Any keys matching ConfigKeys defined on the object will be set, using the name of the ConfigKey or any @SetFromFlag annotation on them. This assumes that the object implements the Configurable interface, and will be ignored if it does not, as they will cause setConfig(key, value) to be called for each matched map entry. To make this easy to use, a new BasicConfigurableObject class is available, which can be extended and appropriate ConfigKeys added, which will be accessible through its getConfig(key) method. Again, any DSL method or deferred attribute access can be set as a value for a key, and will be parsed and resolved appropriately. Another feature, not available with the object.fields map, is type coercion using the TypeCoercions.coerce(object, class) method, which will be called when setting or accessing ConfigKey values.

If the ManagementContextInjectable interface is implemented by the class specified for the object, the DSL will also detect this, and call the injectManagementContext(context) method after construction. Any other object initialisation is left to the user, although possible use cases such as auto detection of init() methods or annotation driven construction are future possibilities.

Example

Here is another example of a YAML blueprint, this time describing a Clocker application configuration. We are using the new object creation methods to set up some placement strategies. Lines 6-13 are highlighted, and show how both the object.fields and brooklyn.config keys can be used together to give more flexibility configuring an object.

services:
- serviceType: brooklyn.entity.container.docker.DockerInfrastructure
  id: infrastructure
  brooklyn.config:
    docker.container.strategies:
    - $brooklyn:object:
        type: brooklyn.location.docker.strategy.CpuUsagePlacementStrategy
        object.fields:
          maxContainers: 6
          maxCpu: 0.33
        brooklyn.config:
          infrastructure: $brooklyn:component("infrastructure")
          cpuUsage.sensor: $brooklyn:sensor("machine.cpu.usage")
    - $brooklyn:object:
        type: brooklyn.location.docker.strategy.BreadthFirstPlacementStrategy
        object.fields:
          maxContainers: 6
        brooklyn.config:
          infrastructure: $brooklyn:component("infrastructure")
    - $brooklyn:object:
        type: brooklyn.location.docker.strategy.AffinityStrategy
        brooklyn.config:
          infrastructure: $brooklyn:component("infrastructure")
          iso3166.code: "en-UK"

Other places where the $brooklyn:object DSL notation will be useful are when configuring Apache jclouds location customizer classes:

ConfigKey<Collection<JcloudsLocationCustomizer>> JCLOUDS_LOCATION_CUSTOMIZERS =
    ConfigKeys.newConfigKey(
        new TypeToken<Collection<JcloudsLocationCustomizer>>() {},
        "customizers", "Optional location customizers");

Previously blueprints using YAML were restricted to using pre-defined classes with fixed configuration, now it is possible to define a JcloudsLocationCustomizer that is dynamically configurable per blueprint. The placement strategy example given earlier also shows how much more flexible this approach is.

Map Configuration Alternative

Entities that use MapConfigKey<Object> to store a map of configuration data, where some values are references to other DSL objects, can now use the more obvious ConfigKey<Map<String,Object>> instead, because resolution of DSL objects now occurs on any collection typed ConfigKey. However, a map of data will discard all type information, and prevent you from taking advantage of some of the more interesting and powerful Brooklyn type coercions. Also, all maps are rendered the same way in the console, which may not be very useful or friendly. Using the $brooklyn:object syntax and defining a POJO data holder object can give strong typing for individual data items, and will allow a custom RendererHint to be provided for the specific Java type you define, which can display more useful information in the console UI.

More detailed documentation is available, and further information can be found at the main Apache Brooklyn site or on GitHub.