diff --git a/demos/variables-expansion.yml b/demos/variables-expansion.yml new file mode 100644 index 0000000..e16ac18 --- /dev/null +++ b/demos/variables-expansion.yml @@ -0,0 +1,20 @@ + +# Using jCasC secret expansion feature +# Secrets are loaded from : +# - Docker secrets +# - Kubernetes/Openshift secrets +# - HashiCorp Vault +# - Environment variables +# +# cf: https://github.com/jenkinsci/configuration-as-code-plugin/blob/master/docs/features/secrets.adoc + +groovy: + + # Asuming secret value is stored in variable named API_KEY + - url: https://my.web.site.com/path/to/my/resource?api_key=${API_KEY} + # Same with basic auth example + - url: https://${USER}:${PASS}@my.web.site.com/path/to/my/resource + + # Asuming secret value is stored in variable named SECRET + - script: > + println "Displaying my secret : ${SECRET}"; diff --git a/pom.xml b/pom.xml index cbd2303..bc272d0 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.jenkins-ci.plugins plugin - 3.18 + 4.12 io.jenkins.plugins @@ -12,10 +12,9 @@ 1.2-SNAPSHOT hpi - 2.60.3 + 2.222 8 - true - true + 1.42 Configuration as Code Plugin - Groovy Scripting Extension Plugin that extends JCasC with Groovy scripts execution @@ -26,6 +25,10 @@ Tomasz Szandala tomasz.szandala@gmail.com + + jetersen + Joseph Petersen + @@ -33,14 +36,12 @@ http://opensource.org/licenses/MIT - - - scm:git:git://github.com/jenkinsci/${project.artifactId}-plugin.git - scm:git:git@github.com:jenkinsci/${project.artifactId}-plugin.git - http://github.com/jenkinsci/${project.artifactId}-plugin - HEAD - + scm:git:git://github.com/jenkinsci/${project.artifactId}-plugin.git + scm:git:git@github.com:jenkinsci/${project.artifactId}-plugin.git + http://github.com/jenkinsci/${project.artifactId}-plugin + HEAD + @@ -58,7 +59,12 @@ io.jenkins configuration-as-code - 1.0 + ${configuration-as-code.version} + + + io.jenkins.configuration-as-code + test-harness + ${configuration-as-code.version} diff --git a/src/main/java/io/jenkins/plugins/cascgroovy/GroovyScriptCaller.java b/src/main/java/io/jenkins/plugins/cascgroovy/GroovyScriptCaller.java index fbd9312..edd8828 100644 --- a/src/main/java/io/jenkins/plugins/cascgroovy/GroovyScriptCaller.java +++ b/src/main/java/io/jenkins/plugins/cascgroovy/GroovyScriptCaller.java @@ -1,40 +1,40 @@ package io.jenkins.plugins.cascgroovy; -import jenkins.model.Jenkins; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import hudson.EnvVars; +import groovy.lang.Binding; +import groovy.lang.GroovyShell; import hudson.Extension; import io.jenkins.plugins.casc.Attribute; import io.jenkins.plugins.casc.ConfigurationContext; -import io.jenkins.plugins.casc.ConfiguratorException; import io.jenkins.plugins.casc.Configurator; +import io.jenkins.plugins.casc.ConfiguratorException; import io.jenkins.plugins.casc.RootElementConfigurator; import io.jenkins.plugins.casc.impl.attributes.MultivaluedAttribute; import io.jenkins.plugins.casc.model.CNode; -import io.jenkins.plugins.casc.model.Sequence; -import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.NoExternalUse; -import groovy.lang.GroovyShell; -import groovy.lang.Binding; - -import java.io.PrintWriter; -import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; -import java.io.IOException; +import io.jenkins.plugins.casc.model.Mapping; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Set; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import jenkins.model.Jenkins; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import static io.vavr.API.Try; +import static io.vavr.API.unchecked; /** * @author Tomasz Szandala */ -@Extension(optional = true) +@Extension(optional = true, ordinal = -60) // Ordinal -60 Ensure it is loaded after JobDSL @Restricted(NoExternalUse.class) public class GroovyScriptCaller implements RootElementConfigurator { @Override + @Nonnull public String getName() { return "groovy"; } @@ -45,6 +45,7 @@ public Class getTarget() { } @Override + @Nonnull public Set> describe() { return Collections.singleton(new MultivaluedAttribute("", GroovyScriptSource.class)); } @@ -55,42 +56,45 @@ public Boolean[] getTargetComponent(ConfigurationContext context) { } @Override + @Nonnull public Boolean[] configure(CNode config, ConfigurationContext context) throws ConfiguratorException { - //JenkinsJobManagement mng = new JenkinsJobManagement(System.out, new EnvVars(), null, null, LookupStrategy.JENKINS_ROOT); - final Sequence sources = config.asSequence(); - final Configurator con = context.lookup(GroovyScriptSource.class); - List generated = new ArrayList<>(); - for (CNode source : sources) { - final String script; - try { - script = con.configure(source, context).getScript(); - } catch (IOException e) { - throw new ConfiguratorException(this, "Failed to retrieve Groovy script", e); - } - try { - //Binding binding = new Binding(); - //binding.setVariable("foo", new Integer(2)); - //GroovyShell shell = new GroovyShell(); - //shell.evaluate(script); - - Binding binding = new Binding(); - //binding.setProperty("out",new PrintWriter(stdout,true)); - //binding.setProperty("stdin",stdin); - //binding.setProperty("stdout",stdout); - //binding.setProperty("stderr",stderr); - - GroovyShell groovy = new GroovyShell(Jenkins.getActiveInstance().getPluginManager().uberClassLoader, binding); - groovy.run(script, "ConfigurationAsCodeGroovy", new ArrayList()); - - generated.add(true); - - } catch (Exception ex) { - throw new ConfiguratorException(this, "Failed to execute script with hash " + script.hashCode(), ex); - } - } - return generated.toArray(new Boolean[generated.size()]); + final Configurator c = context.lookup(GroovyScriptSource.class); + return config.asSequence().stream() + .map(source -> getActualValue(source, context)) + .map(source -> getScriptFromSource(source, context, c)) + .map(script -> Try(script::getScript).onSuccess(GroovyScriptCaller.this::runGroovyShell).isSuccess()) + .toArray(Boolean[]::new); + } + + private CNode getActualValue(CNode source, ConfigurationContext context) { + return unchecked(() -> source.asMapping().entrySet().stream().findFirst()).apply() + .map(entry -> resolveSourceOrGetValue(entry, context)) + .orElse(source); } + private CNode resolveSourceOrGetValue(Map.Entry entry, ConfigurationContext context) { + final Mapping m = new Mapping(); + m.put( + entry.getKey(), + context.getSecretSourceResolver().resolve(unchecked(() -> entry.getValue().asScalar().getValue()).apply()) + ); + return m; + } + + private GroovyScriptSource getScriptFromSource(CNode source, ConfigurationContext context, + Configurator configurator) { + return unchecked(() -> + Try(() -> configurator.configure(source, context)) + .getOrElseThrow(t -> new ConfiguratorException(this, + "Failed to retrieve groovy script", t))).apply(); + } + + private void runGroovyShell(String script) { + final GroovyShell s = new GroovyShell(Jenkins.get().getPluginManager().uberClassLoader, new Binding()); + unchecked(() -> s.run(script, "ConfigurationAsCodeGroovy", new ArrayList())); + } + + @Override public Boolean[] check(CNode config, ConfigurationContext context) throws ConfiguratorException { // Any way to dry-run a Groovy script ? @@ -99,7 +103,7 @@ public Boolean[] check(CNode config, ConfigurationContext context) throws Config @Nonnull @Override - public List getConfigurators(ConfigurationContext context) { + public List> getConfigurators(ConfigurationContext context) { return Collections.singletonList(context.lookup(GroovyScriptSource.class)); } @@ -108,4 +112,5 @@ public List getConfigurators(ConfigurationContext context) { public CNode describe(Boolean[] instance, ConfigurationContext context) throws Exception { return null; } + } diff --git a/src/test/java/io/jenkins/plugins/cascgroovy/GroovyScriptTest.java b/src/test/java/io/jenkins/plugins/cascgroovy/GroovyScriptTest.java new file mode 100644 index 0000000..20ff911 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/cascgroovy/GroovyScriptTest.java @@ -0,0 +1,24 @@ +package io.jenkins.plugins.cascgroovy; + +import io.jenkins.plugins.casc.misc.ConfiguredWithCode; +import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule; +import jenkins.model.Jenkins; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +public class GroovyScriptTest { + @Rule + public JenkinsConfiguredWithCodeRule j = new JenkinsConfiguredWithCodeRule(); + + @Test + @ConfiguredWithCode("casc.yaml") + @Ignore + public void configure() throws Exception { + Jenkins jenkins = Jenkins.get(); + assertThat(jenkins.getSystemMessage(), is("Hello World")); + } +} diff --git a/src/test/resources/io/jenkins/plugins/cascgroovy/casc.yaml b/src/test/resources/io/jenkins/plugins/cascgroovy/casc.yaml new file mode 100644 index 0000000..7dd60fc --- /dev/null +++ b/src/test/resources/io/jenkins/plugins/cascgroovy/casc.yaml @@ -0,0 +1,8 @@ +groovy: + - script: | + import jenkins.model.Jenkins; + + def systemMessage = "Hello World"; + def jenkins = Jenkins.get(); + jenkins.setSystemMessage(systemMessage); + jenkins.save();