JCasC requirement for plugins developers

62 views
Skip to first unread message

nicolas de loof

unread,
Oct 4, 2018, 11:21:16 AM10/4/18
to jenkin...@googlegroups.com
Hi there,

after huge interest for JCasC at Jenkins World, I wrote some minimalist guide for plugin developers to understand requirements so your plugin is well supported.

can read online here, but here's a copy so you can't say you didn't know :P

JCasC Requirements - guide for plugin maintainers

JCasC is designed so any plugin can be managed without the need to implement any custom API, but still require plugins to respect some contract, aka "convention over extension". This documentation is here to explain plugin maintainers those conventions and provide guidance on expected design.

CasC is comming

Overview

JCasC relies on ability to introspect jenkins configurable components to build a "data model" from a live jenkins instance. For this purpose it relies on web UI data-binding conventions.

For legacy reasons, Jenkins do offer multiple ways to support UI data-binding, but the sole one to be introspection friendly is to offer @DataBoundSetter fields or setters in your code.

Surprisingly, this is well adopted by most plugins for Describable components, but not for Descriptors, despite the exact same mechanism can be used for both. And unfortunately, in many case the interesting components to offer configuration one want to expose to JCasC is attached to a Descriptors.

Check-list

Rule 1: don't write code for data-binding

Check implementation of Descriptor#configure(StaplerRequest,JSONObject) in your descriptors. This one should not use any of the JSONObject.get*() methods to set value for an internal field. Prefer exposing javabean setter methods, and use req.bindJSON(this,JSONObject) to rely on introspection-friendly data-binding.

Within a Descriptor such setters don't have to be annotated as @DataBoundSetter but we suggest to do anyway, as it make it clear about the intent for such public methods.

sample :

public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
    smtpHost = nullify(json.getString("smtpHost"));
    replyToAddress = json.getString("replyToAddress");
    ...
    save();
    return true;
}

to be replaced by :

public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
    req.bindJSON(this, json);
    save();
    return true;
}

@DataBoundSetter
public void setSmtpHost(String smtpHost) {
    this.smtpHost = nullify(smtpHost);
}

@DataBoundSetter
public void setReplyToAddress(String address) {
    this.replyToAddress = Util.fixEmpty(address);
}

note: you might not even need to implement configure once #3669 is merged.

Rule 2: don't use pseudo-properties for optional

You might have a set of fields which only make sense when set altogether, and have jelly view to use <f:optionalBlock>based on some boolean pseudo-property to show/hidde the matching section in web UI.

Doing so require had-written data-binding code, so based on rule 1 should be prohibited.

Hopefully there's a simple (and arguably better) way to handle this, by just using nested components and group all related fields into an optional sub-element.

sample :

<f:optionalBlock name="useAuth" title="${%Use Authentication}" 
                 checked="${descriptor.username!=null}">
    <f:entry title="${%User Name}" field="username">	
          <f:textbox />	
    </f:entry>
    ...
private String username;
private Secret password;

public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
    if(json.has("useAuth")) {
        JSONObject auth = json.getJSONObject("useAuth");
        username = nullify(auth.getString("username"));
        password = Secret.fromString(nullify(auth.getString("password")));	
    }
}

to be replaced by :`

<f:optionalProperty field="Authentication" title="${%Use Authentication}"/>
private Authentication authentication;

With a fresh new Authentication Describable class to host username and password, all the <f:optionalBlock> body being moved Authentication/config.jelly view.

note: this also require some data migration logic, please read PLUGINS for a step by step migration guide.

Rule 3: define a test case if you can

Checking support for JCasC is easy as long as your plugin required java 8 / jenkins 2.60+.

You just need CasC as a test dependency and a sample yaml file for your component

<dependency>
      <groupId>io.jenkins</groupId>
      <artifactId>configuration-as-code</artifactId>
      <version>1.0</version>
      <scope>test</scope>
</dependency>
public class ConfigAsCodeTest {

    @Rule public JenkinsRule r = new JenkinsRule();

    @Test public void should_support_configuration_as_code() throws Exception {
        ConfigurationAsCode.get().configure(ConfigAsCodeTest.class.getResource("configuration-as-code.yml").toString());
        assertTrue( /* check plugin has been configured as expected */ );
    }

Benefits for you to write such a testcase :

  • You confirm your plugin is well design regarding JCasC conventions
  • You offer users a sample configuration file
  • You will be able to detect breaking changes that may impact your users

Rule 4: ping us in case of doubts

Really, if you need any assistance getting your plugin to support JCasC, want code review or anything, ping us on Gitter.



--
Nicolas De Loof

Jesse Glick

unread,
Oct 4, 2018, 11:49:53 AM10/4/18
to Jenkins Dev
On Thu, Oct 4, 2018 at 11:21 AM nicolas de loof
<nicolas...@gmail.com> wrote:
> after huge interest for JCasC at Jenkins World, I wrote some minimalist guide for plugin developers to understand requirements so your plugin is well supported.

Please link from

https://jenkins.io/doc/developer/guides/

Jesse Glick

unread,
Oct 4, 2018, 11:53:52 AM10/4/18
to Jenkins Dev
On Thu, Oct 4, 2018 at 11:21 AM nicolas de loof
<nicolas...@gmail.com> wrote:
> Check implementation of Descriptor#configure(StaplerRequest,JSONObject) in your descriptors.

Note that `GlobalConfiguration` subtypes need to override at all.
Arguably that default impl should just be pulled up into `Descriptor`.

> public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
> req.bindJSON(this, json);
> save();

Normally the `save()` call should be within the setter, not here. If
you wish to avoid repeated saves in close succession, that is what
`BulkChange` is for. (Should probably be used from within the standard
impl.)

> @DataBoundSetter

You should remind readers that they also need corresponding getters.

The Pipeline compatibility guide covers some of the same topics BTW.
Reply all
Reply to author
Forward
0 new messages