From f02e5ed4fba5f5ad0273ee8e84c1c8f44615fb54 Mon Sep 17 00:00:00 2001 From: Nicolas De loof Date: Thu, 23 Nov 2017 15:25:20 +0100 Subject: [PATCH] Create PLUGINS.md --- PLUGINS.md | 166 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 PLUGINS.md diff --git a/PLUGINS.md b/PLUGINS.md new file mode 100644 index 00000000..64729d34 --- /dev/null +++ b/PLUGINS.md @@ -0,0 +1,166 @@ +# Guide to plugin developers + +Configuration as Code relies on `Describable` and `DataBound` mechanism Jenkins plugin developers are probably already using. +As long as you follow [best practices](https://jenkins.io/doc/developer/plugin-development/pipeline-integration/#constructor-vs-setters) +using those annotations for data-binding, same attributes will be usable for configuration-as-code. + +## Descriptors global configuration + +Most ot the interesting plugin's configuration you want ot expose to end users with configuration-as-code is managed by your plugin's +Descriptor(s) and exposed on web UI with a `global.jelly` view. This is fully supported by configuration-as-code as long as you rely on +the exact same `DataBound` mechanism, which isn't a common practice (yet). + +In many plugins, `Descriptor#configure()` is implemented by lookup for attributes values from the `JSONObject`. To make your Descriptor +compliant with configuration-as-code, you'll need to expose your configuration attributes as `@DataBoundSetters`. + +Here's the recommended approach : + +Let's consider this Descriptor : + +```java +public static final class DescriptorImpl extends Descriptor { + private String charset; + + /** optional password */ + private Secret password; + + public boolean configure(StaplerRequest req, JSONObject json) throws FormException { + charset = json.getString("charset"); + if (json.has("usePassword")) { + password = Secret.fromString(nullify(auth.getString("password"))); + } else { + password = null; + } + save(); + return true; + } + + public String getCharset() { return charset; } +} +``` + +with global.jelly view : +```xml + + + + + + + + + + + +``` + +### Step 1 + +Define `@DataBoundSetters` javabean setters for your Descriptor's properties. They should match the getters you already have for +global.jelly data-binding. + +```java + @DataBoundSetter + public void setCharset(String charset) { + this.charset = charset; + } +``` + +### Step 2 +Create a new Describable object with a `config.jelly` view to own optional attributes. + +```java +public class Authentication extends AbstractDescribableImpl { + + private Secret password; + + @DataBoundConstructor + public Authentication(Secret password) { this.password = password; } + + public Secret getPassword() { return password; } + + @Extension + public static class DescriptorImpl extends Descriptor { + } +} +``` +```xml + + + + + +``` + +### Step 3 +Define a new attribute in your Descriptor to own optional attributes. +For binary compatibility you'll need to maintain the legacy getters as delegates to this new sub-component. +For backward compatibility, use `readResolve` method to create the new nested component from legacy attributes. + +```java +public static final class DescriptorImpl extends Descriptor { + private String charset; + + /** @deprecated use {@link #authentication} */ + private transient Secret password; + + @CheckForNull + private Authentication authentication; + + // --- backward compatibility + + /** @deprecated use {@link #getAuthentication()} */ + public Secret getPassword() { return authentication != null ? authentication.getPassword() : null; } + + private Object readResolve() { + if (this.password != null) { + this.authentication = new Authentication(password); + } + return this; + } +``` + +### Step 4 + +Replace `optionalBlocks` in your jelly view with `optionalProperty` and add the required DataBound accessors +```xml + + + + + +``` + +```java + public Authentication getAuthentication() { return this.authentication; } + + @DataBoundSetter + public void setAuthentication(Authentication authentication) { this.authentication = authentication; } +``` + +### Step 5 + +Rewrite `Descriptor#configure()` implementation to rely on `request.bindJson(this, json)`. You will have to reset attributes to their +default values as a Desciptor is a mutable object, i.e. data-binding won't reset values if they are not present in JSON payload. + +```java + public boolean configure(StaplerRequest req, JSONObject json) throws FormException { + // reset optional authentication to default before data-binding + this.authentication = null; + req.bindJSON(this, json); + save(); + return true; + } +``` + +### Step 6 +If you don't have one already, define a `@Symbol` annotation on your descriptor. This is the name end-user will be able to use to access +your Descriptor for configuration. To avoid collisions with other plugin, prefer using your plugin's artifactId as a symbolic name for your +descriptor. + +```java +@Symbol("foo") +public static final class DescriptorImpl extends Descriptor { +``` + +See [mailer plugin#39](https://github.com/jenkinsci/mailer-plugin/pull/39) for a sample on required changes.