From 8dc78689bb417e7a8ba0c6e6eb0f1b2ecec0f07d Mon Sep 17 00:00:00 2001 From: Cyril MARIN Date: Wed, 13 Nov 2019 15:25:39 +0100 Subject: [PATCH 1/7] Add secret variables expansion from CNode value --- pom.xml | 2 +- .../cascgroovy/GroovyScriptCaller.java | 92 ++++++++----------- 2 files changed, 41 insertions(+), 53 deletions(-) diff --git a/pom.xml b/pom.xml index a591af2..50a364f 100644 --- a/pom.xml +++ b/pom.xml @@ -58,7 +58,7 @@ io.jenkins configuration-as-code - 1.0 + 1.32 diff --git a/src/main/java/io/jenkins/plugins/cascgroovy/GroovyScriptCaller.java b/src/main/java/io/jenkins/plugins/cascgroovy/GroovyScriptCaller.java index 86a9325..78256e0 100644 --- a/src/main/java/io/jenkins/plugins/cascgroovy/GroovyScriptCaller.java +++ b/src/main/java/io/jenkins/plugins/cascgroovy/GroovyScriptCaller.java @@ -1,36 +1,28 @@ 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.RootElementConfigurator; +import io.jenkins.plugins.casc.*; import io.jenkins.plugins.casc.impl.attributes.MultivaluedAttribute; import io.jenkins.plugins.casc.model.CNode; -import io.jenkins.plugins.casc.model.Sequence; +import io.jenkins.plugins.casc.model.Mapping; +import jenkins.model.Jenkins; 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 java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; +import java.util.*; + +import static io.vavr.API.Try; +import static io.vavr.API.unchecked; /** * @author Tomasz Szandala */ -@Extension(optional = true) +@Extension(optional = true, ordinal = -50) @Restricted(NoExternalUse.class) public class GroovyScriptCaller implements RootElementConfigurator { @@ -56,41 +48,36 @@ public Boolean[] getTargetComponent(ConfigurationContext context) { @Override 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, "Configuration-as-Code-Groovy", 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 -> Try(() -> c.configure(source, context).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(), + SecretSourceResolver.resolve(context, unchecked(() -> entry.getValue().asScalar().getValue()).apply()) + ); + return m; + } + + private void runGroovyShell(String script) { + final GroovyShell s = new GroovyShell(Jenkins.getActiveInstance().getPluginManager().uberClassLoader, new Binding()); + unchecked(() -> s.run(script, "Configuration-as-Code-Groovy", new ArrayList())); + } + + @Override public Boolean[] check(CNode config, ConfigurationContext context) throws ConfiguratorException { // Any way to dry-run a Groovy script ? @@ -99,7 +86,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 +95,5 @@ public List getConfigurators(ConfigurationContext context) { public CNode describe(Boolean[] instance, ConfigurationContext context) throws Exception { return null; } + } From 9f64797ab0a00222cbcce18a3877900d66cf0753 Mon Sep 17 00:00:00 2001 From: Cyril MARIN Date: Thu, 30 Jan 2020 14:47:55 +0100 Subject: [PATCH 2/7] Add demo file for variable/secret expansion feature as requested --- demos/variables-expansion.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 demos/variables-expansion.yml 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}"; From 8609c45b1334a7f7821a624fbfe67753508c49c3 Mon Sep 17 00:00:00 2001 From: Joseph Petersen Date: Mon, 19 Oct 2020 22:53:46 +0200 Subject: [PATCH 3/7] use ordinal -60 to load after JobDSL JCasC configurator --- .../java/io/jenkins/plugins/cascgroovy/GroovyScriptCaller.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/jenkins/plugins/cascgroovy/GroovyScriptCaller.java b/src/main/java/io/jenkins/plugins/cascgroovy/GroovyScriptCaller.java index 708e082..eb68da6 100644 --- a/src/main/java/io/jenkins/plugins/cascgroovy/GroovyScriptCaller.java +++ b/src/main/java/io/jenkins/plugins/cascgroovy/GroovyScriptCaller.java @@ -22,7 +22,7 @@ /** * @author Tomasz Szandala */ -@Extension(optional = true, ordinal = -50) +@Extension(optional = true, ordinal = -60) // Ordinal -60 Ensure it is loaded after JobDSL @Restricted(NoExternalUse.class) public class GroovyScriptCaller implements RootElementConfigurator { From 6c1c6783d097d37361f94a824cc8855bf1682d75 Mon Sep 17 00:00:00 2001 From: Joseph Petersen Date: Mon, 19 Oct 2020 23:28:10 +0200 Subject: [PATCH 4/7] bump parent pom, jenkins core and jcasc --- pom.xml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pom.xml b/pom.xml index fc97ade..b261f13 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,8 @@ 1.2-SNAPSHOT hpi - 2.60.3 + 2.222 8 - true - true Configuration as Code Plugin - Groovy Scripting Extension Plugin that extends JCasC with Groovy scripts execution @@ -26,6 +24,10 @@ Tomasz Szandala tomasz.szandala@gmail.com + + jetersen + Joseph Petersen + @@ -33,14 +35,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 +58,7 @@ io.jenkins configuration-as-code - 1.32 + 1.42 From 316f5bf1622f9a45e027c97ac8ad346fbc461452 Mon Sep 17 00:00:00 2001 From: Joseph Petersen Date: Mon, 19 Oct 2020 23:28:38 +0200 Subject: [PATCH 5/7] fix deprecated method and improve readability --- .../cascgroovy/GroovyScriptCaller.java | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/cascgroovy/GroovyScriptCaller.java b/src/main/java/io/jenkins/plugins/cascgroovy/GroovyScriptCaller.java index eb68da6..edd8828 100644 --- a/src/main/java/io/jenkins/plugins/cascgroovy/GroovyScriptCaller.java +++ b/src/main/java/io/jenkins/plugins/cascgroovy/GroovyScriptCaller.java @@ -3,18 +3,25 @@ import groovy.lang.Binding; import groovy.lang.GroovyShell; import hudson.Extension; -import io.jenkins.plugins.casc.*; +import io.jenkins.plugins.casc.Attribute; +import io.jenkins.plugins.casc.ConfigurationContext; +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.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 javax.annotation.CheckForNull; -import javax.annotation.Nonnull; -import java.util.*; - import static io.vavr.API.Try; import static io.vavr.API.unchecked; @@ -27,6 +34,7 @@ public class GroovyScriptCaller implements RootElementConfigurator { @Override + @Nonnull public String getName() { return "groovy"; } @@ -37,6 +45,7 @@ public Class getTarget() { } @Override + @Nonnull public Set> describe() { return Collections.singleton(new MultivaluedAttribute("", GroovyScriptSource.class)); } @@ -47,13 +56,13 @@ public Boolean[] getTargetComponent(ConfigurationContext context) { } @Override + @Nonnull public Boolean[] configure(CNode config, ConfigurationContext context) throws ConfiguratorException { final Configurator c = context.lookup(GroovyScriptSource.class); return config.asSequence().stream() .map(source -> getActualValue(source, context)) - .map(source -> Try(() -> c.configure(source, context).getScript()) - .onSuccess(GroovyScriptCaller.this::runGroovyShell) - .isSuccess()) + .map(source -> getScriptFromSource(source, context, c)) + .map(script -> Try(script::getScript).onSuccess(GroovyScriptCaller.this::runGroovyShell).isSuccess()) .toArray(Boolean[]::new); } @@ -67,13 +76,21 @@ private CNode resolveSourceOrGetValue(Map.Entry entry, Configurat final Mapping m = new Mapping(); m.put( entry.getKey(), - SecretSourceResolver.resolve(context, unchecked(() -> entry.getValue().asScalar().getValue()).apply()) + 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.getActiveInstance().getPluginManager().uberClassLoader, new Binding()); + final GroovyShell s = new GroovyShell(Jenkins.get().getPluginManager().uberClassLoader, new Binding()); unchecked(() -> s.run(script, "ConfigurationAsCodeGroovy", new ArrayList())); } From c5cc52702ca0363b1df76ace95c08f5e4b523285 Mon Sep 17 00:00:00 2001 From: Joseph Petersen Date: Mon, 19 Oct 2020 23:35:21 +0200 Subject: [PATCH 6/7] update Jenkinsfile --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index cb50db3..8966783 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1 +1 @@ -buildPlugin(jenkinsVersions: [null, "2.107.1"], timeout: 180) +buildPlugin(jenkinsVersions: [null], timeout: 180) From 9fee9aa725cd57395796f2ec3471336ac58327a7 Mon Sep 17 00:00:00 2001 From: Joseph Petersen Date: Tue, 20 Oct 2020 00:26:58 +0200 Subject: [PATCH 7/7] add test --- pom.xml | 8 ++++++- .../plugins/cascgroovy/GroovyScriptTest.java | 24 +++++++++++++++++++ .../io/jenkins/plugins/cascgroovy/casc.yaml | 8 +++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 src/test/java/io/jenkins/plugins/cascgroovy/GroovyScriptTest.java create mode 100644 src/test/resources/io/jenkins/plugins/cascgroovy/casc.yaml diff --git a/pom.xml b/pom.xml index b261f13..bc272d0 100644 --- a/pom.xml +++ b/pom.xml @@ -14,6 +14,7 @@ 2.222 8 + 1.42 Configuration as Code Plugin - Groovy Scripting Extension Plugin that extends JCasC with Groovy scripts execution @@ -58,7 +59,12 @@ io.jenkins configuration-as-code - 1.42 + ${configuration-as-code.version} + + + io.jenkins.configuration-as-code + test-harness + ${configuration-as-code.version} 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();