diff --git a/src/main/java/org/jenkinsci/plugins/envinject/AliasInitializer.java b/src/main/java/org/jenkinsci/plugins/envinject/AliasInitializer.java index b41b38d4..cbbd881e 100644 --- a/src/main/java/org/jenkinsci/plugins/envinject/AliasInitializer.java +++ b/src/main/java/org/jenkinsci/plugins/envinject/AliasInitializer.java @@ -12,6 +12,7 @@ public class AliasInitializer { @Initializer(before = InitMilestone.PLUGINS_STARTED) @SuppressWarnings("unused") public static void addAliases() { + Items.XSTREAM.alias("EnvInjectPrebuildJobProperty", EnvInjectPrebuildJobProperty.class); Items.XSTREAM.alias("EnvInjectJobProperty", EnvInjectJobProperty.class); Items.XSTREAM.alias("EnvInjectBuildWrapper", EnvInjectBuildWrapper.class); Items.XSTREAM.alias("EnvInjectPasswordWrapper", EnvInjectPasswordWrapper.class); diff --git a/src/main/java/org/jenkinsci/plugins/envinject/EnvInjectPrebuildJobProperty.java b/src/main/java/org/jenkinsci/plugins/envinject/EnvInjectPrebuildJobProperty.java new file mode 100644 index 00000000..fe5b8627 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/envinject/EnvInjectPrebuildJobProperty.java @@ -0,0 +1,390 @@ +package org.jenkinsci.plugins.envinject; + +import hudson.DescriptorExtensionList; +import hudson.Extension; +import hudson.FilePath; +import hudson.model.BuildListener; +import hudson.model.Environment; +import hudson.model.JobProperty; +import hudson.model.JobPropertyDescriptor; +import hudson.model.AbstractBuild; +import hudson.model.Computer; +import hudson.model.Descriptor; +import hudson.model.Job; +import hudson.model.Node; +import hudson.model.Result; +import hudson.model.Run; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.annotation.CheckForNull; + +import jenkins.model.Jenkins; +import net.sf.json.JSON; +import net.sf.json.JSONException; +import net.sf.json.JSONObject; + +import org.jenkinsci.lib.envinject.EnvInjectException; +import org.jenkinsci.lib.envinject.EnvInjectLogger; +import org.jenkinsci.plugins.envinject.model.EnvInjectJobPropertyContributor; +import org.jenkinsci.plugins.envinject.model.EnvInjectJobPropertyContributorDescriptor; +import org.jenkinsci.plugins.envinject.service.EnvInjectActionSetter; +import org.jenkinsci.plugins.envinject.service.EnvInjectContributorManagement; +import org.jenkinsci.plugins.envinject.service.EnvInjectEnvVars; +import org.jenkinsci.plugins.envinject.service.EnvInjectVariableGetter; +import org.kohsuke.stapler.StaplerRequest; + +/** + * @author Arcadiy Ivanov (arcivanov) + * @author Gregory Boissinot + */ +public class EnvInjectPrebuildJobProperty> extends JobProperty { + + private EnvInjectJobPropertyInfo info = new EnvInjectJobPropertyInfo(); + private boolean on; + private boolean keepJenkinsSystemVariables; + private boolean keepBuildVariables; + private boolean overrideBuildParameters; + private EnvInjectJobPropertyContributor[] contributors; + + private transient EnvInjectJobPropertyContributor[] contributorsComputed; + + @SuppressWarnings("unused") + public EnvInjectJobPropertyInfo getInfo() { + return info; + } + + @SuppressWarnings("unused") + public boolean isOn() { + return on; + } + + @SuppressWarnings("unused") + public boolean isKeepJenkinsSystemVariables() { + return keepJenkinsSystemVariables; + } + + @SuppressWarnings("unused") + public boolean isKeepBuildVariables() { + return keepBuildVariables; + } + + @SuppressWarnings("unused") + public boolean isOverrideBuildParameters() { + return overrideBuildParameters; + } + + @SuppressWarnings("unused") + public EnvInjectJobPropertyContributor[] getContributors() { + if (contributorsComputed == null) { + try { + contributorsComputed = computeEnvInjectContributors(); + } catch (org.jenkinsci.lib.envinject.EnvInjectException e) { + e.printStackTrace(); + } + contributors = contributorsComputed; + } + + return Arrays.copyOf(contributors, contributors.length); + } + + private EnvInjectJobPropertyContributor[] computeEnvInjectContributors() throws org.jenkinsci.lib.envinject.EnvInjectException { + + DescriptorExtensionList + descriptors = EnvInjectJobPropertyContributor.all(); + + // If the config are loaded with success (this step) and the descriptors size doesn't have change + // we considerate, they are the same, therefore we retrieve instances + if (contributors != null && contributors.length == descriptors.size()) { + return contributors; + } + + EnvInjectContributorManagement envInjectContributorManagement = new EnvInjectContributorManagement(); + EnvInjectJobPropertyContributor[] contributorsInstance = envInjectContributorManagement.getNewContributorsInstance(); + + //No jobProperty Contributors ==> new configuration + if (contributors == null || contributors.length == 0) { + return contributorsInstance; + } + + List result = new ArrayList(); + for (EnvInjectJobPropertyContributor contributor1 : contributorsInstance) { + for (EnvInjectJobPropertyContributor contributor2 : contributors) { + if (contributor1.getDescriptor().getClass() == contributor2.getDescriptor().getClass()) { + result.add(contributor2); + } else { + result.add(contributor1); + } + } + } + return result.toArray(new EnvInjectJobPropertyContributor[result.size()]); + } + + public void setInfo(EnvInjectJobPropertyInfo info) { + this.info = info; + } + + public void setOn(boolean on) { + this.on = on; + } + + public void setKeepJenkinsSystemVariables(boolean keepJenkinsSystemVariables) { + this.keepJenkinsSystemVariables = keepJenkinsSystemVariables; + } + + public void setKeepBuildVariables(boolean keepBuildVariables) { + this.keepBuildVariables = keepBuildVariables; + } + + public void setOverrideBuildParameters(boolean overrideBuildParameters) { + this.overrideBuildParameters = overrideBuildParameters; + } + + public void setContributors(EnvInjectJobPropertyContributor[] jobPropertyContributors) { + this.contributors = jobPropertyContributors; + } + + @Override + public JobProperty reconfigure(StaplerRequest req, JSONObject form) throws Descriptor.FormException { + EnvInjectPrebuildJobProperty property = (EnvInjectPrebuildJobProperty) super.reconfigure(req, form); + if (property != null && property.info != null && !Jenkins.getInstance().hasPermission(Jenkins.RUN_SCRIPTS)) { + // Don't let non RUN_SCRIPT users set arbitrary groovy script + property.info = new EnvInjectJobPropertyInfo(property.info.propertiesFilePath, property.info.propertiesContent, + property.info.getScriptFilePath(), property.info.getScriptContent(), + this.info != null ? this.info.getGroovyScriptContent() : "", + property.info.isLoadFilesFromMaster()); + } + return property; + } + + @Extension + @SuppressWarnings("unused") + public static final class DescriptorImpl extends JobPropertyDescriptor { + + @Override + public String getDisplayName() { + return "[Environment Inject (Prebuild)] -" + Messages.envinject_set_displayName(); + } + + @Override + public boolean isApplicable(Class jobType) { + return true; + } + + @Override + public String getHelpFile() { + return "/plugin/envinject/help.html"; + } + + @Override + public EnvInjectPrebuildJobProperty newInstance(StaplerRequest req, JSONObject formData) throws FormException { + Object onObject = formData.get("on"); + + if (onObject != null) { + EnvInjectPrebuildJobProperty envInjectJobProperty = new EnvInjectPrebuildJobProperty(); + EnvInjectJobPropertyInfo info = req.bindParameters(EnvInjectJobPropertyInfo.class, "envInjectInfoPrebuildJobProperty."); + envInjectJobProperty.setInfo(info); + envInjectJobProperty.setOn(true); + if (onObject instanceof JSONObject) { + JSONObject onJSONObject = (JSONObject) onObject; + envInjectJobProperty.setKeepJenkinsSystemVariables(onJSONObject.getBoolean("keepJenkinsSystemVariables")); + envInjectJobProperty.setKeepBuildVariables(onJSONObject.getBoolean("keepBuildVariables")); + envInjectJobProperty.setOverrideBuildParameters(onJSONObject.getBoolean("overrideBuildParameters")); + + //Process contributions + setContributors(req, envInjectJobProperty, onJSONObject); + + return envInjectJobProperty; + } + } + + return null; + } + + private void setContributors(StaplerRequest req, EnvInjectPrebuildJobProperty envInjectJobProperty, JSONObject onJSONObject) { + if (!onJSONObject.containsKey("contributors")) { + envInjectJobProperty.setContributors(new EnvInjectJobPropertyContributor[0]); + } else { + JSON contribJSON; + try { + contribJSON = onJSONObject.getJSONArray("contributors"); + } catch (JSONException jsone) { + contribJSON = onJSONObject.getJSONObject("contributors"); + } + List contributions = req.bindJSONToList(EnvInjectJobPropertyContributor.class, contribJSON); + EnvInjectJobPropertyContributor[] contributionsArray = contributions.toArray(new EnvInjectJobPropertyContributor[contributions.size()]); + envInjectJobProperty.setContributors(contributionsArray); + } + } + + public DescriptorExtensionList getEnvInjectContributors() { + return EnvInjectJobPropertyContributor.all(); + } + + public @CheckForNull EnvInjectJobPropertyContributor[] getContributorsInstance() { + EnvInjectContributorManagement envInjectContributorManagement = new EnvInjectContributorManagement(); + try { + return envInjectContributorManagement.getNewContributorsInstance(); + } catch (org.jenkinsci.lib.envinject.EnvInjectException e) { + e.printStackTrace(); + } + return null; + } + + public boolean isEnvInjectContributionActivated() { + EnvInjectContributorManagement envInjectContributorManagement = new EnvInjectContributorManagement(); + return envInjectContributorManagement.isEnvInjectContributionActivated(); + } + + } + + /* + * (non-Javadoc) + * + * @see hudson.model.JobProperty#prebuild(hudson.model.AbstractBuild, + * hudson.model.BuildListener) + */ + @Override + public boolean prebuild(AbstractBuild build, BuildListener listener) { + if(!isOn()) { + return true; + } + EnvInjectLogger logger = new EnvInjectLogger(listener); + FilePath ws = build.getWorkspace(); + + try { + logger.info("Preparing an environment for the build (prebuild phase)."); + + EnvInjectVariableGetter variableGetter = new EnvInjectVariableGetter(); + EnvInjectJobPropertyInfo info = getInfo(); + assert isOn(); + + // Init infra env vars + Map previousEnvVars = variableGetter.getEnvVarsPreviousSteps(build, logger); + Map infraEnvVarsNode = new LinkedHashMap(previousEnvVars); + Map infraEnvVarsMaster = new LinkedHashMap(previousEnvVars); + + // Add workspace if not set + if (ws != null) { + if (infraEnvVarsNode.get("WORKSPACE") == null) { + infraEnvVarsNode.put("WORKSPACE", ws.getRemote()); + } + } + + //Add Jenkins System variables + if (isKeepJenkinsSystemVariables()) { + logger.info("Keeping Jenkins system variables."); + infraEnvVarsMaster.putAll(variableGetter.getJenkinsSystemVariables(true)); + infraEnvVarsNode.putAll(variableGetter.getJenkinsSystemVariables(false)); + } + + //Add build variables + if (isKeepBuildVariables()) { + logger.info("Keeping Jenkins build variables."); + Map buildVariables = variableGetter.getBuildVariables(build, logger); + infraEnvVarsMaster.putAll(buildVariables); + infraEnvVarsNode.putAll(buildVariables); + } + + final FilePath rootPath = getNodeRootPath(); + if (rootPath != null) { + + final EnvInjectEnvVars envInjectEnvVarsService = new EnvInjectEnvVars(logger); + + //Execute script + int resultCode = envInjectEnvVarsService.executeScript(info.isLoadFilesFromMaster(), + info.getScriptContent(), + rootPath, info.getScriptFilePath(), infraEnvVarsMaster, infraEnvVarsNode, rootPath.createLauncher(listener), listener); + if (resultCode != 0) { + build.setResult(Result.FAILURE); + throw new Run.RunnerAbortedException(); + } + + //Evaluate Groovy script + Map groovyMapEnvVars = envInjectEnvVarsService.executeAndGetMapGroovyScript(logger, info.getGroovyScriptContent(), infraEnvVarsNode); + + final Map propertiesVariables = envInjectEnvVarsService.getEnvVarsPropertiesJobProperty(rootPath, + logger, info.isLoadFilesFromMaster(), + info.getPropertiesFilePath(), info.getPropertiesContentMap(previousEnvVars), + infraEnvVarsMaster, infraEnvVarsNode); + + //Get variables get by contribution + Map contributionVariables = getEnvVarsByContribution(build, this, logger, listener); + + final Map mergedVariables = envInjectEnvVarsService.getMergedVariables( + infraEnvVarsNode, + propertiesVariables, + groovyMapEnvVars, + contributionVariables); + + //Add an action to share injected environment variables + new EnvInjectActionSetter(rootPath).addEnvVarsToEnvInjectBuildAction(build, mergedVariables); + } + } catch (Throwable t) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + t.printStackTrace(pw); + logger.error("Failure while executing scripts in a prebuild phase: " + sw.toString()); + pw.close(); + return false; + } + return true; + } + + private Node getNode() { + Computer computer = Computer.currentComputer(); + if (computer == null) { + return null; + } + return computer.getNode(); + } + + private FilePath getNodeRootPath() { + Node node = getNode(); + if (node != null) { + return node.getRootPath(); + } + return null; + } + + private Map getEnvVarsByContribution(AbstractBuild build, EnvInjectPrebuildJobProperty envInjectJobProperty, + EnvInjectLogger logger, BuildListener listener) throws EnvInjectException { + + assert envInjectJobProperty != null; + Map contributionVariables = new HashMap(); + + EnvInjectJobPropertyContributor[] contributors = envInjectJobProperty.getContributors(); + if (contributors != null) { + logger.info("Injecting contributions."); + for (EnvInjectJobPropertyContributor contributor : contributors) { + contributionVariables.putAll(contributor.getEnvVars(build, listener)); + } + } + return contributionVariables; + } + + + @Deprecated + private transient boolean injectGlobalPasswords; + @Deprecated + private transient EnvInjectPasswordEntry[] passwordEntries; + @Deprecated + private transient boolean keepSystemVariables; + + @Deprecated + public boolean isInjectGlobalPasswords() { + return injectGlobalPasswords; + } + + @Deprecated + public EnvInjectPasswordEntry[] getPasswordEntries() { + return passwordEntries; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/envinject/service/EnvInjectVariableGetter.java b/src/main/java/org/jenkinsci/plugins/envinject/service/EnvInjectVariableGetter.java index 6bf81172..9f21e82b 100644 --- a/src/main/java/org/jenkinsci/plugins/envinject/service/EnvInjectVariableGetter.java +++ b/src/main/java/org/jenkinsci/plugins/envinject/service/EnvInjectVariableGetter.java @@ -144,7 +144,12 @@ public Map getEnvVarsPreviousSteps(AbstractBuild build, EnvInjec if (environmentList != null) { for (Environment e : environmentList) { if (e != null) { - e.buildEnvVars(result); + try { + e.buildEnvVars(result); + } + catch(Exception ex) { + logger.error("Environment " + e + " could not provide it's env variables: " + ex); + } } } } diff --git a/src/main/resources/org/jenkinsci/plugins/envinject/EnvInjectPrebuildJobProperty/config.jelly b/src/main/resources/org/jenkinsci/plugins/envinject/EnvInjectPrebuildJobProperty/config.jelly new file mode 100644 index 00000000..2123ceac --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/envinject/EnvInjectPrebuildJobProperty/config.jelly @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/webapp/help_prebuild.html b/src/main/webapp/help_prebuild.html new file mode 100644 index 00000000..6155aec6 --- /dev/null +++ b/src/main/webapp/help_prebuild.html @@ -0,0 +1,10 @@ +
+

+ If this option is checked, Jenkins makes it possible to set an environment + for the build job (or for each run on a matrix project) by executing a script + (a setup script).
+ These actions will be executed in a JobProperty "prebuild" phase, after a SCM checkout but before the build-proper starts. + This allows, for example, to setup build environment for Maven project based on SCM data, which would otherwise be impossible, + since Maven project requires Maven to be available even before any of the build wrappers are executed.
+

+
diff --git a/src/test/java/org/jenkinsci/plugins/envinject/EnvInjectPrebuildJobPropertyTest.java b/src/test/java/org/jenkinsci/plugins/envinject/EnvInjectPrebuildJobPropertyTest.java new file mode 100644 index 00000000..91e57ff8 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/envinject/EnvInjectPrebuildJobPropertyTest.java @@ -0,0 +1,130 @@ +package org.jenkinsci.plugins.envinject; + +import hudson.model.Cause; +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; +import hudson.model.ParametersAction; +import hudson.model.StringParameterValue; +import hudson.model.TaskListener; +import hudson.model.queue.QueueTaskFuture; +import java.io.IOException; +import javax.annotation.Nonnull; +import org.junit.Rule; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.junit.Assert.*; +import org.junit.Ignore; +import org.junit.Test; +import org.jvnet.hudson.test.CaptureEnvironmentBuilder; +import org.jvnet.hudson.test.Issue; + +/** + * Tests for {@link EnvInjectPrebuildJobProperty} + * @author Oleg Nenashev + */ +public class EnvInjectPrebuildJobPropertyTest { + + @Rule + public JenkinsRule jenkinsRule = new JenkinsRule(); + + @Test + public void shouldInjectBuildVarsFromPropertiesContent() throws Exception { + FreeStyleProject project = jenkinsRule.createFreeStyleProject(); + EnvInjectPrebuildJobProperty prop = forPropertiesContent(project, "FOO=BAR"); + + FreeStyleBuild build = jenkinsRule.buildAndAssertSuccess(project); + assertEquals("The FOO variable has not been injected properly", "BAR", + build.getEnvironment(TaskListener.NULL).get("FOO")); + } + + @Test + public void shouldNotInjectVariablesIfPropertyIsDisabled() throws Exception { + FreeStyleProject project = jenkinsRule.createFreeStyleProject(); + + EnvInjectPrebuildJobProperty prop = new EnvInjectPrebuildJobProperty(); + prop.setInfo(new EnvInjectJobPropertyInfo(null, "FOO=BAR", null, null, null, false)); + // prop.setOn(false); // It is default + project.addProperty(prop); + + FreeStyleBuild build = jenkinsRule.buildAndAssertSuccess(project); + assertNull("The plugin should not inject properties by default", + build.getEnvironment(TaskListener.NULL).get("FOO")); + } + + @Test + public void shouldKeepBuildVariablesByDefault() throws Exception { + FreeStyleProject project = jenkinsRule.createFreeStyleProject(); + // We assign a value to another parameter just to enable the engine + EnvInjectPrebuildJobProperty prop = forPropertiesContent(project, "PARAM2=Overridden"); + + QueueTaskFuture scheduled = project.scheduleBuild2(0, new Cause.UserIdCause(), + new ParametersAction(new StringParameterValue("PARAM", "Value"))); + assertNotNull(scheduled); + FreeStyleBuild build = scheduled.get(); + jenkinsRule.assertBuildStatusSuccess(build); + assertEquals("The build parameter has been overridden", "Value", + build.getEnvironment(TaskListener.NULL).get("PARAM")); + } + + @Test + @Ignore("The value is being actually contributed by other env contributors. The feature seems to be obsolete") + public void shouldNotKeepBuildVariablesIfDisabled() throws Exception { + FreeStyleProject project = jenkinsRule.createFreeStyleProject(); + + EnvInjectPrebuildJobProperty prop = forPropertiesContent(project, "PARAM2=Overridden"); + prop.setKeepBuildVariables(false); + + QueueTaskFuture scheduled = project.scheduleBuild2(0, new Cause.UserIdCause(), + new ParametersAction(new StringParameterValue("PARAM", "Value"))); + assertNotNull(scheduled); + FreeStyleBuild build = scheduled.get(); + jenkinsRule.assertBuildStatusSuccess(build); + assertNull("We expect that the PARAM is not specified", + build.getEnvironment(TaskListener.NULL).get("PARAM")); + } + + @Test + @Ignore("It should not override vars according to the manual testing. But it does... " + + "Manual tests also show the wrong value in InjectedVarsAction") + @Issue("JENKINS-29905") + public void shouldNotOverrideBuildParametersByDefault() throws Exception { + FreeStyleProject project = jenkinsRule.createFreeStyleProject(); + EnvInjectPrebuildJobProperty prop = forPropertiesContent(project, "PARAM=Overridden"); + prop.setOverrideBuildParameters(false); + + final CaptureEnvironmentBuilder envCapture = new CaptureEnvironmentBuilder(); + project.getBuildersList().add(envCapture); + + QueueTaskFuture scheduled = project.scheduleBuild2(0, new Cause.UserIdCause(), + new ParametersAction(new StringParameterValue("PARAM", "ValueFromParameter"))); + assertNotNull(scheduled); + FreeStyleBuild build = scheduled.get(); + jenkinsRule.assertBuildStatusSuccess(build); + assertEquals("The variable has been overridden in the environment", "ValueFromParameter", envCapture.getEnvVars().get("PARAM")); + assertEquals("The variable has been overridden in the API", "ValueFromParameter", build.getEnvironment(TaskListener.NULL).get("PARAM")); + } + + @Test + public void shouldOverrideBuildParametersIfEnabled() throws Exception { + FreeStyleProject project = jenkinsRule.createFreeStyleProject(); + EnvInjectPrebuildJobProperty prop = forPropertiesContent(project, "PARAM=Overridden"); + prop.setOverrideBuildParameters(true); + + QueueTaskFuture scheduled = project.scheduleBuild2(0, new Cause.UserIdCause(), + new ParametersAction(new StringParameterValue("PARAM", "ValueFromParameter"))); + assertNotNull(scheduled); + FreeStyleBuild build = scheduled.get(); + jenkinsRule.assertBuildStatusSuccess(build); + assertEquals("The build parameter value has not been overridden", "Overridden", build.getEnvironment(TaskListener.NULL).get("PARAM")); + } + + @Nonnull + public EnvInjectPrebuildJobProperty + forPropertiesContent(@Nonnull FreeStyleProject job, @Nonnull String content) throws IOException { + final EnvInjectPrebuildJobProperty prop = new EnvInjectPrebuildJobProperty(); + prop.setInfo(new EnvInjectJobPropertyInfo(null, content, null, null, null, false)); + prop.setOn(true); // Property becomes enabled by default + job.addProperty(prop); + return prop; + } +}