initial implementation

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2017-09-04 14:11:30 +02:00
commit 9bbddaa70b
23 changed files with 1309 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
target/
work/
.idea/
*.iml
src/main/resources/org/

33
jenkins.yaml Normal file
View File

@ -0,0 +1,33 @@
jenkins:
# securityRealm: local
securityRealm:
ldap:
configurations:
- server: ldap.acme.com
rootDN: dc=acme,dc=fr
managerPasswordSecret: ${LDAP_PASSWORD}
cache:
size: 100
ttl: 10
userIdStrategy: CaseSensitive
groupIdStrategy: CaseSensitive
authorizationStrategy:
loggedInUsersCanDoAnything:
allowAnonymousRead: false
noUsageStatistics: true
slaveAgentPort: 6666
viewsTabBar: standard
nodes:
- slave:
name: "foo"
remoteFS: '/tmp/2'
launcher: "jnlp"
numExecutors: 1
- slave:
name: "bar"
remoteFS: '/tmp/1'
launcher: "jnlp"
numExecutors: 1

88
pom.xml Normal file
View File

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<version>2.11</version>
<relativePath/>
</parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>configuration-as-code</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>hpi</packaging>
<properties>
<jenkins.version>2.60</jenkins.version>
<java.level>8</java.level>
</properties>
<name>Configuration as Code Plugin</name>
<description>Manage Jenkins master configuration as code</description>
<url>https://wiki.jenkins-ci.org/display/JENKINS/Configuration+as+Code+Plugin</url>
<licenses>
<license>
<name>MIT License</name>
<url>http://opensource.org/licenses/MIT</url>
</license>
</licenses>
<developers>
<developer>
<id>ndeloof</id>
<name>Nicolas De Loof</name>
<email>nicolas.deloof@gmail.com</email>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/jenkinsci/${project.artifactId}-plugin.git</connection>
<developerConnection>scm:git:git@github.com:jenkinsci/${project.artifactId}-plugin.git</developerConnection>
<url>https://github.com/jenkinsci/${project.artifactId}-plugin</url>
</scm>
<repositories>
<repository>
<id>repo.jenkins-ci.org</id>
<url>https://repo.jenkins-ci.org/public/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>repo.jenkins-ci.org</id>
<url>https://repo.jenkins-ci.org/public/</url>
</pluginRepository>
</pluginRepositories>
<dependencies>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.18</version>
</dependency>
<!-- for demo purpose -->
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>ldap</artifactId>
<version>1.16</version>
<type>hpi</type>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<failOnError>false</failOnError>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,56 @@
package org.jenkinsci.plugins.casc;
import org.apache.commons.beanutils.PropertyUtils;
import org.kohsuke.stapler.lang.Klass;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
*/
public class Attribute<T> {
protected final String name;
protected final Class<T> type;
private boolean multiple;
public Attribute(String name, Class<T> type) {
this.name = name;
this.type = type;
}
public String getName() {
return name;
}
public Class<T> getType() {
return type;
}
/** Attribute acutaly is a Collection of documented type */
public boolean isMultiple() {
return multiple;
}
public Attribute<T> withMultiple(boolean multiple) {
this.multiple = multiple;
return this;
}
/** If this attribute is constrained to a limited set of value, here they are */
public List<String> possibleValues() {
if (type.isEnum()) {
Class<Enum> e = (Class<Enum>) type;
return Arrays.stream(e.getEnumConstants()).map(Enum::name).collect(Collectors.toList());
}
return Collections.EMPTY_LIST;
}
public void setValue(Object target, T value) throws Exception {
PropertyUtils.setProperty(target, name, value);
}
}

View File

@ -0,0 +1,73 @@
package org.jenkinsci.plugins.casc;
import hudson.model.Describable;
import org.apache.commons.beanutils.PropertyUtils;
import org.jenkinsci.Symbol;
import org.kohsuke.accmod.Restricted;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
/**
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
*/
public abstract class BaseConfigurator<T> extends Configurator<T> {
public Set<Attribute> describe() {
Set<Attribute> attributes = new HashSet<>();
final PropertyDescriptor[] properties = PropertyUtils.getPropertyDescriptors(getTarget());
for (PropertyDescriptor p : properties) {
final String name = p.getName();
final Method setter = p.getWriteMethod();
if (setter == null) continue; // read only
if (setter.getAnnotation(Deprecated.class) != null) continue; // not actually public
if (setter.getAnnotation(Restricted.class) != null) continue; // not actually public - require access-modifier 1.12
Class c;
final Type type = setter.getGenericParameterTypes()[0];
if (type instanceof ParameterizedType) {
// List<? extends Foo>
ParameterizedType pt = (ParameterizedType) type;
c = (Class) pt.getRawType();
} else {
c = (Class) type;
}
boolean multiple = false;
if (Collection.class.isAssignableFrom(c)) {
multiple = true;
ParameterizedType pt = (ParameterizedType) type;
Type actualType = pt.getActualTypeArguments()[0];
if (actualType instanceof WildcardType) {
actualType = ((WildcardType) actualType).getUpperBounds()[0];
}
if (!(actualType instanceof Class)) {
throw new IllegalStateException("Can't handle "+type);
}
c = (Class) actualType;
}
Attribute attribute;
if (Describable.class.isAssignableFrom(c)) {
attribute = new DescribableAttribute(p.getName(), c);
} else {
attribute = new Attribute(p.getName(), c);
}
attributes.add(attribute.withMultiple(multiple));
// See https://github.com/jenkinsci/structs-plugin/pull/18
final Symbol s = setter.getAnnotation(Symbol.class);
// TODO record symbol as preferred name / alias for this attribute
}
return attributes;
}
}

View File

@ -0,0 +1,7 @@
package org.jenkinsci.plugins.casc;
/**
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
*/
public class Config {
}

View File

@ -0,0 +1,124 @@
package org.jenkinsci.plugins.casc;
import hudson.Plugin;
import hudson.init.InitMilestone;
import hudson.init.Initializer;
import jenkins.model.Jenkins;
import org.yaml.snakeyaml.Yaml;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
*/
public class ConfigurationAsCode extends Plugin {
@Initializer(after = InitMilestone.EXTENSIONS_AUGMENTED)
public static void configure() throws Exception {
final File f = new File("./jenkins.yaml");
if (f.exists()) {
configure(new FileInputStream(f));
}
}
public static void configure(InputStream in) throws Exception {
Map<String, Object> config = new Yaml().loadAs(in, Map.class);
for (Map.Entry<String, Object> e : config.entrySet()) {
final Configurator configurator = Configurator.lookupRootElement(e.getKey());
if (configurator == null) {
throw new IllegalArgumentException("no configurator for root element "+e.getKey());
}
configurator.configure(e.getValue());
}
}
public List<?> getConfigurators() {
List<Object> elements = new ArrayList<>();
for (RootElementConfigurator c : Jenkins.getInstance().getExtensionList(RootElementConfigurator.class)) {
elements.add(c);
listElements(elements, c.describe());
}
return elements;
}
private void listElements(List<Object> elements, Set<Attribute> attributes) {
for (Attribute attribute : attributes) {
final Class type = attribute.type;
Configurator configurator = Configurator.lookup(type);
if (configurator == null ) {
continue;
}
for (Object o : configurator.getConfigurators()) {
if (!elements.contains(o)) {
elements.add(o);
}
}
listElements(elements, configurator.describe());
}
}
private final List<Class> documented = new ArrayList<>();
{
documented.add(int.class);
documented.add(String.class);
documented.add(boolean.class);
documented.add(Integer.class);
// ...
for (RootElementConfigurator c : Jenkins.getInstance().getExtensionList(RootElementConfigurator.class)) {
final String name = c.getName();
document(name, c.describe());
}
}
private void document(String name, Set<Attribute> attributes) {
Set<Class> next = new HashSet<>();
System.out.println();
System.out.println("## " + name);
for (Attribute attribute : attributes) {
// FIXME filter attribute to target a component without anything configurable
final Class type = attribute.getType();
System.out.print("**"+attribute.getName() + "** (");
if (attribute.isMultiple())
System.out.print("list of ");
System.out.println(type+")");
if (!attribute.possibleValues().isEmpty()) {
System.out.println("possible values :");
for (Object o : attribute.possibleValues()) {
System.out.println(" - " + o);
}
}
if (! documented.contains(type)) {
next.add(type);
}
}
for (Class type : next) {
Configurator configurator = Configurator.lookup(type);
if (configurator == null) continue;
document(type.getName(), configurator.describe());
}
}
}

View File

@ -0,0 +1,170 @@
package org.jenkinsci.plugins.casc;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.model.Describable;
import jenkins.model.Jenkins;
import org.apache.commons.io.IOUtils;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.lang.Klass;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
/**
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
*/
public abstract class Configurator<T> implements ExtensionPoint {
public static Configurator lookupRootElement(String name) {
final ExtensionList<Configurator> l = Jenkins.getInstance().getExtensionList(Configurator.class);
for (Configurator c : l) {
if (c instanceof RootElementConfigurator
&& ((RootElementConfigurator)c).getName().equals(name)) {
return c;
}
}
return null;
}
public static Configurator lookup(Type type) {
Class clazz = asClass(type);
final ExtensionList<Configurator> l = Jenkins.getInstance().getExtensionList(Configurator.class);
for (Configurator c : l) {
if (clazz == c.getTarget()) {
// this type has a dedicated Configurator implementation
return c;
}
}
if (Collection.class.isAssignableFrom(clazz)) {
ParameterizedType pt = (ParameterizedType) type;
Type actualType = pt.getActualTypeArguments()[0];
if (actualType instanceof WildcardType) {
actualType = ((WildcardType) actualType).getUpperBounds()[0];
}
if (!(actualType instanceof Class)) {
throw new IllegalStateException("Can't handle "+type);
}
return lookup(actualType);
}
if (Describable.class.isAssignableFrom(clazz)) {
if (Modifier.isAbstract(clazz.getModifiers())) {
return new HeteroDescribableConfigurator(clazz);
}
return new DescribableConfigurator(clazz);
}
if (Extension.class.isAssignableFrom(clazz)) {
return new ExtensionConfigurator(clazz);
}
return new PrimitiveConfigurator(clazz);
}
private static Class asClass(Type type) {
Class clazz;
if (type instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) type;
clazz = (Class) pt.getRawType();
} else {
clazz = (Class) type;
}
return clazz;
}
// ---
/**
*
* @return the list of Configurator to be considered so one can fully configure this component.
* Typically, an extension point with multiple implementations will return Configurators for available implementations.
*/
public List<Configurator> getConfigurators() {
return Collections.singletonList(this);
}
/**
* @return short name for this component when used in a configuration.yaml file
*/
public String getName() {
final Symbol annotation = getTarget().getAnnotation(Symbol.class);
if (annotation != null) return annotation.value()[0];
return getTarget().getSimpleName();
}
/**
* The actual component being managed by this Configurator
*/
public abstract Class<T> getTarget();
/**
* The extension point being implemented by this configurator. Can be null
*/
public Class getExtensionpoint() {
return ExtensionPoint.class.isAssignableFrom(getTarget()) ? getTarget() : null;
}
/**
* Human friendly display name for this component.
*/
public String getDisplayName() { return ""; }
private Klass getKlass() {
return Klass.java(getTarget());
}
public abstract T configure(Object config) throws Exception;
/**
* Ordered version of {@link #describe()} for documentation generation
*/
public List<Attribute> getAttributes() {
final ArrayList<Attribute> attributes = new ArrayList<>(describe());
Collections.sort(attributes, (a,b) -> a.name.compareTo(b.name));
return attributes;
}
/**
* Determine the list of Attribute available for configuration of the managed component.
*/
public abstract Set<Attribute> describe();
/**
* Retrieve the html help tip associated to an attribute.
* FIXME would prefer <st:include page="help-${a.name}.html" class="${c.target}" optional="true"/>
*/
public String getHtmlHelp(String attribute) throws IOException {
final URL resource = getKlass().getResource("help-" + attribute + ".html");
if (resource != null) {
return IOUtils.toString(resource);
}
return "";
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Configurator) {
return getTarget() == ((Configurator) obj).getTarget();
}
return false;
}
@Override
public int hashCode() {
return getTarget().hashCode();
}
}

View File

@ -0,0 +1,32 @@
package org.jenkinsci.plugins.casc;
import hudson.model.Describable;
import hudson.model.Descriptor;
import jenkins.model.Jenkins;
import org.jenkinsci.Symbol;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
*/
public class DescribableAttribute extends Attribute {
public DescribableAttribute(String name, Class<? extends Describable> type) {
super(name, type);
}
@Override
public List<String> possibleValues() {
final List<Descriptor> descriptors = Jenkins.getInstance().getDescriptorList(type);
return descriptors.stream()
.map(d -> {
Symbol s = (Symbol) d.getClass().getAnnotation(Symbol.class);
if (s != null) return s.value()[0];
// TODO truncate extension class name, so LegacyAuthorizationStrategy => "Legacy"
else return d.getKlass().toJavaClass().getSimpleName();
})
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,167 @@
package org.jenkinsci.plugins.casc;
import com.google.common.base.Defaults;
import hudson.DescriptorExtensionList;
import hudson.model.Describable;
import hudson.model.Descriptor;
import jenkins.model.Jenkins;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.ClassDescriptor;
import org.kohsuke.stapler.DataBoundConstructor;
import javax.annotation.Nonnull;
import java.lang.reflect.Constructor;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* A generic {@link Configurator} to configure {@link Describable} which offer a
* {@link org.kohsuke.stapler.DataBoundConstructor}
*
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
*/
public class DescribableConfigurator extends BaseConfigurator<Describable> {
private final Class target;
public DescribableConfigurator(Class clazz) {
this.target = clazz;
}
@Override
public Class<Describable> getTarget() {
return target;
}
@Override
public Describable configure(Object c) throws Exception {
Map config = c instanceof Map ? (Map) c : Collections.EMPTY_MAP;
final Constructor constructor = getDataBoundConstructor(target);
if (constructor == null) {
throw new IllegalStateException(target.getName() + " is missing a @DataBoundConstructor");
}
final Parameter[] parameters = constructor.getParameters();
final String[] names = ClassDescriptor.loadParameterNames(constructor);
Object[] args = new Object[names.length];
if (parameters.length > 0) {
// Many jenkins components haven't been migrated to @DataBoundSetter vs @NotNull constructor parameters
// as a result it might be valid to reference a describable without parameters
for (int i = 0; i < names.length; i++) {
final Object value = config.remove(names[i]);
if (value == null && parameters[i].getAnnotation(Nonnull.class) != null) {
throw new IllegalArgumentException(names[i] + " is required to configure " + target);
}
final Class t = parameters[i].getType();
if (value != null) {
if (Collection.class.isAssignableFrom(t)) {
if (!(value instanceof List)) {
throw new IllegalArgumentException(names[i] + " should be a list");
}
final Type pt = parameters[i].getParameterizedType();
final Configurator lookup = Configurator.lookup(pt);
final ArrayList<Object> list = new ArrayList<>();
for (Object o : (List) value) {
list.add(lookup.configure(o));
}
args[i] = list;
} else {
final Type pt = parameters[i].getParameterizedType();
args[i] = Configurator.lookup(pt != null ? pt : t).configure(value);
}
System.out.println("Setting " + target + "." + names[i] + " = " + value);
} else if (t.isPrimitive()) {
args[i] = Defaults.defaultValue(t);
}
}
}
Describable object = (Describable) constructor.newInstance(args);
final Set<Attribute> attributes = describe();
for (Attribute attribute : attributes) {
final String name = attribute.getName();
if (config.containsKey(name)) {
final Object value = Configurator.lookup(attribute.getType()).configure(config.get(name));
attribute.setValue(object, value);
}
}
return object;
}
public Constructor getDataBoundConstructor(Class type) {
for (Constructor c : type.getConstructors()) {
if (c.getAnnotation(DataBoundConstructor.class) != null) return c;
}
return null;
}
public String getName() {
final Descriptor d = getDescriptor();
final Symbol annotation = d.getClass().getAnnotation(Symbol.class);
if (annotation != null) return annotation.value()[0];
return getTarget().getSimpleName();
}
private Descriptor getDescriptor() {
return Jenkins.getInstance().getDescriptor(getTarget());
}
public Class getExtensionpoint() {
// detect common pattern DescriptorImpl extends Descriptor<ExtensionPoint>
final Type superclass = getDescriptor().getClass().getGenericSuperclass();
if (superclass instanceof ParameterizedType) {
final ParameterizedType genericSuperclass = (ParameterizedType) superclass;
Type type = genericSuperclass.getActualTypeArguments()[0];
if (type instanceof ParameterizedType) {
type = ((ParameterizedType) type).getRawType();
}
if (type instanceof Class) {
return (Class) type;
}
}
return super.getExtensionpoint();
}
@Override
public Set<Attribute> describe() {
final Set<Attribute> attributes = super.describe();
final Constructor constructor = getDataBoundConstructor(target);
if (constructor != null) {
final Parameter[] parameters = constructor.getParameters();
final String[] names = ClassDescriptor.loadParameterNames(constructor);
for (int i = 0; i < parameters.length; i++) {
final Parameter p = parameters[i];
final Attribute a = new Attribute(names[i], p.getType());
attributes.add(a);
}
}
return attributes;
}
public String getDisplayName() {
final List<Descriptor> list = Jenkins.getInstance().getDescriptorList(target);
return list.get(0).getDisplayName();
}
}

View File

@ -0,0 +1,72 @@
package org.jenkinsci.plugins.casc;
import jenkins.model.Jenkins;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* TODO rely on some JenkinsRule or comparable to generate this statically from jenkins-core version + set of plugins
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
*/
public class DocumentationGeneration {
// @Initializer(after = InitMilestone.EXTENSIONS_AUGMENTED)
public static void init() {
new DocumentationGeneration().generate();
}
private final List<Class> documented = new ArrayList<>();
public void generate() {
documented.add(int.class);
documented.add(String.class);
documented.add(boolean.class);
documented.add(Integer.class);
// ...
for (RootElementConfigurator c : Jenkins.getInstance().getExtensionList(RootElementConfigurator.class)) {
final String name = c.getName();
document(name, c.describe());
}
}
private void document(String name, Set<Attribute> attributes) {
Set<Class> next = new HashSet<>();
System.out.println();
System.out.println("## " + name);
for (Attribute attribute : attributes) {
// FIXME filter attribute to target a component without anything configurable
final Class type = attribute.getType();
System.out.print("**"+attribute.getName() + "** (");
if (attribute.isMultiple())
System.out.print("list of ");
System.out.println(type+")");
if (!attribute.possibleValues().isEmpty()) {
System.out.println("possible values :");
for (Object o : attribute.possibleValues()) {
System.out.println(" - " + o);
}
}
if (! documented.contains(type)) {
next.add(type);
}
}
for (Class type : next) {
Configurator configurator = Configurator.lookup(type);
if (configurator == null) continue;
document(type.getName(), configurator.describe());
}
}
}

View File

@ -0,0 +1,54 @@
package org.jenkinsci.plugins.casc;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.model.Descriptor;
import jenkins.model.Jenkins;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* A generic {@link Configurator} for {@link hudson.Extension} singletons
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
*/
public class ExtensionConfigurator extends BaseConfigurator {
private final Class target;
public ExtensionConfigurator(Class clazz) {
this.target = clazz;
}
@Override
public Class getTarget() {
return target;
}
@Override
public Object configure(Object c) throws Exception {
final ExtensionList list = Jenkins.getInstance().getExtensionList(target);
if (list.size() != 1) {
throw new IllegalStateException();
}
final Object o = list.get(0);
if (c instanceof Map) {
Map config = (Map) c;
final Set<Attribute> attributes = describe();
for (Attribute attribute : attributes) {
final String name = attribute.getName();
if (config.containsKey(name)) {
final Object value = Configurator.lookup(attribute.getType()).configure(config.get(name));
attribute.setValue(o, value);
}
}
}
return o;
}
}

View File

@ -0,0 +1,105 @@
package org.jenkinsci.plugins.casc;
import hudson.model.Describable;
import hudson.model.Descriptor;
import jenkins.model.Jenkins;
import org.jenkinsci.Symbol;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
*/
public class HeteroDescribableConfigurator extends Configurator<Describable> {
private final Class target;
public HeteroDescribableConfigurator(Class clazz) {
this.target = clazz;
}
@Override
public Class<Describable> getTarget() {
return target;
}
public List<Configurator> getConfigurators() {
final List<Descriptor> candidates = Jenkins.getInstance().getDescriptorList(target);
return candidates.stream()
.map(d -> Configurator.lookup(d.getKlass().toJavaClass()))
.collect(Collectors.toList());
}
@Override
public Describable configure(Object config) throws Exception {
String shortname;
Object subconfig = null;
if (config instanceof String) {
shortname = (String) config;
} else if (config instanceof Map) {
Map<String, ?> map = (Map) config;
if (map.size() != 1) {
throw new IllegalArgumentException("single entry map expected to configure a "+target.getName());
}
shortname = map.keySet().iterator().next();
subconfig = map.get(shortname);
} else {
throw new IllegalArgumentException("Unexpected configuration type "+config);
}
final List<Descriptor> candidates = Jenkins.getInstance().getDescriptorList(target);
Class<? extends Describable> k = findDescribableBySymbol(shortname, candidates);
return (Describable) Configurator.lookup(k).configure(subconfig);
}
private Class findDescribableBySymbol(String shortname, List<Descriptor> candidates) {
// Search for @Symbol annotation on Descriptor to match shortName
for (Descriptor d : candidates) {
final Symbol symbol = d.getClass().getAnnotation(Symbol.class);
if (symbol == null) continue;
for (String s : symbol.value()) {
if (s.equals(shortname)) {
return d.getKlass().toJavaClass();
}
}
}
// Search for Fully qualified class name
for (Descriptor d : candidates) {
final String fqcn = d.getKlass().toJavaClass().getName();
if (shortname.equals(fqcn)) {
return d.getKlass().toJavaClass();
}
}
// Search for class name
for (Descriptor d : candidates) {
final String cn = d.getKlass().toJavaClass().getSimpleName();
if (shortname.equalsIgnoreCase(cn)) {
return d.getKlass().toJavaClass();
}
}
// Search for implicit symbol, i.e "ldap" for LdapSecurityRealm implementing SecurityRealm
String s = shortname + target.getSimpleName();
for (Descriptor d : candidates) {
final String cn = d.getKlass().toJavaClass().getSimpleName();
if (s.equalsIgnoreCase(cn)) {
return d.getKlass().toJavaClass();
}
}
throw new IllegalArgumentException("No "+target.getName()+ "implementation found for "+shortname);
}
@Override
public Set<Attribute> describe() {
return Collections.EMPTY_SET;
}
}

View File

@ -0,0 +1,102 @@
package org.jenkinsci.plugins.casc;
import hudson.Extension;
import hudson.ExtensionPoint;
import hudson.model.Descriptor;
import jenkins.model.Jenkins;
import org.apache.commons.beanutils.PropertyUtils;
import org.jenkinsci.Symbol;
import org.kohsuke.accmod.Restricted;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
*/
@Extension
public class JenkinsConfigurator extends BaseConfigurator<Jenkins> implements RootElementConfigurator {
@Override
public Class<Jenkins> getTarget() {
return Jenkins.class;
}
@Override
public Jenkins configure(Object c) throws Exception {
Map config = (Map) c;
Jenkins jenkins = Jenkins.getInstance();
final Set<Attribute> attributes = describe();
for (Attribute attribute : attributes) {
final String name = attribute.getName();
if (config.containsKey(name)) {
final Object sub = config.get(name);
if (attribute.isMultiple()) {
List values = new ArrayList<>();
for (Object o : (List) sub) {
Object value = Configurator.lookup(attribute.getType()).configure(o);
values.add(value);
}
attribute.setValue(jenkins, values);
} else {
Object value = Configurator.lookup(attribute.getType()).configure(sub);
attribute.setValue(jenkins, value);
}
}
}
return jenkins;
}
@Override
public Set<Attribute> describe() {
final Set<Attribute> attributes = super.describe();
final List<ExtensionPoint> all = Jenkins.getInstance().getExtensionList(ExtensionPoint.class);
for (Object e : all) {
if (e instanceof Descriptor) continue;
final Symbol symbol = e.getClass().getAnnotation(Symbol.class);
if (symbol == null) {
continue; // This extension doesn't even have a shortname
}
if (Configurator.lookup(e.getClass()).describe().isEmpty()) {
// There's nothing on can configure on this extension
// So this useless to expose it - this probably is some technical stuff
continue;
}
attributes.add(new ExtensionAttribute(symbol.value()[0], e.getClass()));
}
return attributes;
}
@Override
public String getName() {
return "jenkins";
}
private class ExtensionAttribute extends Attribute {
public ExtensionAttribute(String name, Class type) {
super(name, type);
}
@Override
public void setValue(Object target, Object value) throws Exception {
// nop
}
}
}

View File

@ -0,0 +1,53 @@
package org.jenkinsci.plugins.casc;
import org.kohsuke.stapler.Stapler;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
*/
public class PrimitiveConfigurator extends Configurator {
private final Class target;
public PrimitiveConfigurator(Class clazz) {
this.target = clazz;
}
@Override
public Class getTarget() {
return target;
}
@Override
public Set<Attribute> describe() {
return Collections.EMPTY_SET;
}
public static final Pattern ENV_VARIABLE = Pattern.compile("\\$\\{(.*)\\}");
@Override
public Object configure(Object config) throws Exception {
if (config instanceof String) {
String s = (String) config;
// TODO I Wonder this could be done during parsing with some snakeyml extension
final Matcher matcher = ENV_VARIABLE.matcher(s);
if (matcher.matches()) {
final String var = matcher.group(1);
config = System.getenv(var);
if (config == null) throw new IllegalStateException("Environment variable not set: "+var);
}
}
return Stapler.CONVERT_UTILS.convert(config, target);
}
@Override
public List<Configurator> getConfigurators() {
return Collections.EMPTY_LIST;
}
}

View File

@ -0,0 +1,15 @@
package org.jenkinsci.plugins.casc;
import java.util.Set;
/**
* Define a {@link Configurator} which handles a root configuration element, identified by name.
* Note: we assume any configurator here will use a unique name for root element.
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
*/
public interface RootElementConfigurator {
String getName();
Set<Attribute> describe();
}

View File

@ -0,0 +1,7 @@
<?jelly escape-by-default='true'?>
<!--
This view is used to render the installed plugins page.
-->
<div>
This plugin is a sample to explain how to write a Jenkins plugin.
</div>

View File

@ -0,0 +1,49 @@
package org.jenkinsci.plugins.casc;
import hudson.model.AbstractDescribableImpl;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.kohsuke.stapler.DataBoundConstructor;
import java.util.HashMap;
import java.util.Map;
import static org.junit.Assert.*;
/**
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
*/
public class DescribableConfiguratorTest {
@Rule
public JenkinsRule j = new JenkinsRule();
@Test
public void _databound() throws Exception {
Map<String, Object> config = new HashMap<>();
config.put("foo", "foo");
config.put("bar", true);
config.put("qix", 123);
final Foo configured = (Foo) Configurator.lookup(Foo.class).configure(config);
assertEquals("foo", configured.foo);
assertEquals(true, configured.bar);
assertEquals(123, configured.qix);
}
public static class Foo extends AbstractDescribableImpl<Foo> {
final String foo;
final boolean bar;
final int qix;
@DataBoundConstructor
public Foo(String foo, boolean bar, int qix) {
this.foo = foo;
this.bar = bar;
this.qix = qix;
}
}
}

View File

@ -0,0 +1,41 @@
package org.jenkinsci.plugins.casc;
import hudson.security.FullControlOnceLoggedInAuthorizationStrategy;
import hudson.security.HudsonPrivateSecurityRealm;
import jenkins.model.Jenkins;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import static org.junit.Assert.*;
/**
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
*/
public class JenkinsConfiguratorTest {
@Rule
public JenkinsRule j = new JenkinsRule();
@Test
public void jenkins_primitive_attributes() throws Exception {
ConfigurationAsCode.configure(getClass().getResourceAsStream("Primitives.yml"));
final Jenkins jenkins = Jenkins.getInstance();
assertEquals(6666, jenkins.getSlaveAgentPort());
assertEquals(false, jenkins.isUsageStatisticsCollected());
}
@Test
public void jenkins_abstract_describable_attributes() throws Exception {
ConfigurationAsCode.configure(getClass().getResourceAsStream("HeteroDescribable.yml"));
final Jenkins jenkins = Jenkins.getInstance();
assertTrue(jenkins.getSecurityRealm() instanceof HudsonPrivateSecurityRealm);
assertTrue(jenkins.getAuthorizationStrategy() instanceof FullControlOnceLoggedInAuthorizationStrategy);
assertFalse(((FullControlOnceLoggedInAuthorizationStrategy) jenkins.getAuthorizationStrategy()).isAllowAnonymousRead());
}
}

View File

@ -0,0 +1,44 @@
package org.jenkinsci.plugins.casc;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import static org.junit.Assert.*;
/**
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
*/
public class PrimitiveConfiguratorTest {
@Rule
public JenkinsRule j = new JenkinsRule();
@Test
public void _boolean() throws Exception {
Configurator c = Configurator.lookup(boolean.class);
final Object value = c.configure("true");
assertTrue((Boolean) value);
}
@Test
public void _int() throws Exception {
Configurator c = Configurator.lookup(int.class);
final Object value = c.configure("123");
assertEquals(123, (int) value);
}
@Test
public void _Integer() throws Exception {
Configurator c = Configurator.lookup(Integer.class);
final Object value = c.configure("123");
assertTrue(123 == ((Integer) value).intValue());
}
@Test
public void _string() throws Exception {
Configurator c = Configurator.lookup(String.class);
final Object value = c.configure("abc");
assertEquals("abc", value);
}
}

View File

@ -0,0 +1,3 @@
securityConfig:
markupFormatter: plainText

View File

@ -0,0 +1,5 @@
jenkins:
securityRealm: local
authorizationStrategy:
loggedInUsersCanDoAnything:
allowAnonymousRead: false

View File

@ -0,0 +1,4 @@
jenkins:
noUsageStatistics: true
slaveAgentPort: 6666
authorizationStrategy: loggedInUsersCanDoAnything