diff --git a/Version_2.adoc b/Version_2.adoc index 932ff7da2..57d69b5fb 100644 --- a/Version_2.adoc +++ b/Version_2.adoc @@ -50,12 +50,17 @@ As JTE grows in adoption, there is plenty that can be done to improve our abilit == Feature Development +* [] https://github.com/jenkinsci/templating-engine-plugin/pull/132[Allow for reversal of library sources traversal] +* [x] https://github.com/jenkinsci/templating-engine-plugin/issues/111[move JTE behavior feature flags into a root level `jte` block in the pipeline configuration] +* [x] https://github.com/jenkinsci/templating-engine-plugin/issues/100[Pipeline Job: added SCM pipeline configuration and template] +* [x] https://github.com/jenkinsci/templating-engine-plugin/pull/101[JTE pipelines can survive an ungraceful restart now] +* [x] https://github.com/jenkinsci/templating-engine-plugin/pull/98[Added pipelineConfig reserved variable] * [x] https://github.com/jenkinsci/templating-engine-plugin/issues/46[Add support for Library Resources] * [x] https://github.com/jenkinsci/templating-engine-plugin/issues/97[Field-level pipeline configuration governance] * [x] https://github.com/jenkinsci/templating-engine-plugin/issues/79[Standardize on autowired variables - remove the Lifecycle Hook context parameter in favor of an autowired variable] * [x] https://github.com/jenkinsci/templating-engine-plugin/issues/72[Document the StageContext functionality to pass arguments to steps run as part of a Stage] -* [ ] https://github.com/jenkinsci/templating-engine-plugin/issues/62[Introduce step namespacing so that multiple steps with the same name can be loaded via a less-strict initialization process configurable by the user] -* [ ] https://github.com/jenkinsci/templating-engine-plugin/issues/84[Improve overall initialization logging for JTE to help users debug issues] +* [x] https://github.com/jenkinsci/templating-engine-plugin/issues/62[Introduce step namespacing so that multiple steps with the same name can be loaded via a less-strict initialization process configurable by the user] +* [x] https://github.com/jenkinsci/templating-engine-plugin/issues/84[Improve overall initialization logging for JTE to help users debug issues] * [ ] https://github.com/jenkinsci/templating-engine-plugin/issues/23[Investigate whether or not JTE can integrate with Jenkins Pipeline's Declarative Syntax] diff --git a/docs/modules/pipeline-templating/pages/configuration_files.adoc b/docs/modules/pipeline-templating/pages/configuration_files.adoc index dd7e6db91..06f708fd1 100644 --- a/docs/modules/pipeline-templating/pages/configuration_files.adoc +++ b/docs/modules/pipeline-templating/pages/configuration_files.adoc @@ -22,6 +22,7 @@ jte{ <1> allow_scm_jenkinsfile = true pipeline_template = "some_template" permissive_initialization = false + reverse_library_resolution = false } template_methods{} <2> @@ -59,6 +60,10 @@ keywords{} <7> | Whether or not JTE will allow a naming collision within the binding. For example, two library's that both contribute a step by the same name or a keyword and application environment by the same name. | `false` +| `reverse_library_resolution` +| Normally, JTE resolves libraries based on the library sources from the highest governance tier down towards the job, xref:governance:library_selection.adoc[default library selection]. This flag, if enabled, reverses the library search order +| `false` + |=== [WARNING] diff --git a/src/main/groovy/org/boozallen/plugins/jte/init/PipelineDecorator.groovy b/src/main/groovy/org/boozallen/plugins/jte/init/PipelineDecorator.groovy index 0daa238f9..dc2621d41 100644 --- a/src/main/groovy/org/boozallen/plugins/jte/init/PipelineDecorator.groovy +++ b/src/main/groovy/org/boozallen/plugins/jte/init/PipelineDecorator.groovy @@ -106,10 +106,9 @@ class PipelineDecorator extends InvisibleAction { } String determinePipelineTemplate(){ - LinkedHashMap pipelineConfig = config.getConfig() WorkflowJob job = getJob() FlowDefinition flowDefinition = job.getDefinition() - JteBlockWrapper jteBlockWrapper = (pipelineConfig.jte ?: [:]) as JteBlockWrapper + JteBlockWrapper jteBlockWrapper = config.jteBlockWrapper if (flowDefinition instanceof AdHocTemplateFlowDefinition){ String template = flowDefinition.getTemplate(flowOwner) if(template){ @@ -172,6 +171,7 @@ class PipelineDecorator extends InvisibleAction { String pipeline_template = null Boolean allow_scm_jenkinsfile = true Boolean permissive_initialization = false + Boolean reverse_library_resolution = false static LinkedHashMap getSchema(){ return [ @@ -179,7 +179,8 @@ class PipelineDecorator extends InvisibleAction { optional: [ allow_scm_jenkinsfile: Boolean, pipeline_template: String, - permissive_initialization: Boolean + permissive_initialization: Boolean, + reverse_library_resolution: Boolean ] ] ] diff --git a/src/main/groovy/org/boozallen/plugins/jte/init/governance/config/dsl/PipelineConfigurationObject.groovy b/src/main/groovy/org/boozallen/plugins/jte/init/governance/config/dsl/PipelineConfigurationObject.groovy index 7930a1cb3..d78585c78 100644 --- a/src/main/groovy/org/boozallen/plugins/jte/init/governance/config/dsl/PipelineConfigurationObject.groovy +++ b/src/main/groovy/org/boozallen/plugins/jte/init/governance/config/dsl/PipelineConfigurationObject.groovy @@ -15,6 +15,7 @@ */ package org.boozallen.plugins.jte.init.governance.config.dsl +import org.boozallen.plugins.jte.init.PipelineDecorator import org.boozallen.plugins.jte.util.TemplateLogger import org.codehaus.groovy.runtime.InvokerHelper import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner @@ -38,6 +39,10 @@ class PipelineConfigurationObject implements Serializable{ this.flowOwner = flowOwner } + PipelineDecorator.JteBlockWrapper getJteBlockWrapper(){ + return (config.jte ?: [:]) as PipelineDecorator.JteBlockWrapper + } + PipelineConfigurationObject plus(PipelineConfigurationObject child){ /* If this is the first call to join, then there is no pre-existing diff --git a/src/main/groovy/org/boozallen/plugins/jte/init/primitives/TemplateBindingFactory.groovy b/src/main/groovy/org/boozallen/plugins/jte/init/primitives/TemplateBindingFactory.groovy index 3975c8445..dd6ab8387 100644 --- a/src/main/groovy/org/boozallen/plugins/jte/init/primitives/TemplateBindingFactory.groovy +++ b/src/main/groovy/org/boozallen/plugins/jte/init/primitives/TemplateBindingFactory.groovy @@ -39,7 +39,7 @@ class TemplateBindingFactory { static TemplateBinding create(FlowExecutionOwner flowOwner, PipelineConfigurationObject config){ invoke("validateConfiguration", flowOwner, config) - JteBlockWrapper jte = (config.getConfig().jte ?: [:]) as JteBlockWrapper + JteBlockWrapper jte = config.jteBlockWrapper TemplateBinding templateBinding = new TemplateBinding(flowOwner, jte.permissive_initialization) invoke("injectPrimitives", flowOwner, config, templateBinding) invoke("validateBinding", flowOwner, config, templateBinding) diff --git a/src/main/groovy/org/boozallen/plugins/jte/init/primitives/injectors/LibraryStepInjector.groovy b/src/main/groovy/org/boozallen/plugins/jte/init/primitives/injectors/LibraryStepInjector.groovy index 0de03cfe9..f232d2d69 100644 --- a/src/main/groovy/org/boozallen/plugins/jte/init/primitives/injectors/LibraryStepInjector.groovy +++ b/src/main/groovy/org/boozallen/plugins/jte/init/primitives/injectors/LibraryStepInjector.groovy @@ -54,6 +54,10 @@ import org.jenkinsci.plugins.workflow.job.WorkflowJob LinkedHashMap aggregatedConfig = config.getConfig() AggregateException errors = new AggregateException() List providers = getLibraryProviders(flowOwner) + boolean reverseProviders = config.jteBlockWrapper.reverse_library_resolution + if(reverseProviders) { + providers = providers.reverse() + } ConfigValidator validator = new ConfigValidator(flowOwner) aggregatedConfig[KEY].each { libName, libConfig -> LibraryProvider provider = providers.find{ provider -> @@ -86,7 +90,12 @@ import org.jenkinsci.plugins.workflow.job.WorkflowJob @Override void injectPrimitives(FlowExecutionOwner flowOwner, PipelineConfigurationObject config, TemplateBinding binding){ LinkedHashMap aggregatedConfig = config.getConfig() + List providers = getLibraryProviders(flowOwner) + boolean reverseProviders = config.jteBlockWrapper.reverse_library_resolution + if(reverseProviders) { + providers = providers.reverse() + } aggregatedConfig[KEY].each{ libName, libConfig -> LibraryProvider provider = providers.find{ provider -> provider.hasLibrary(flowOwner, libName) diff --git a/src/test/groovy/org/boozallen/plugins/jte/init/PipelineDecoratorSpec.groovy b/src/test/groovy/org/boozallen/plugins/jte/init/PipelineDecoratorSpec.groovy index 935765b42..1b64e2c76 100644 --- a/src/test/groovy/org/boozallen/plugins/jte/init/PipelineDecoratorSpec.groovy +++ b/src/test/groovy/org/boozallen/plugins/jte/init/PipelineDecoratorSpec.groovy @@ -63,12 +63,14 @@ class PipelineDecoratorSpec extends Specification{ then: jte.allow_scm_jenkinsfile == expected_allow jte.pipeline_template == expected_template + jte.permissive_initialization == permissive + jte.reverse_library_resolution == reverse_libs where: - config | expected_allow | expected_template - [:] | true | null - [allow_scm_jenkinsfile:false] | false | null - [allow_scm_jenkinsfile:false, pipeline_template:'dev_template'] | false | 'dev_template' + config | expected_allow | expected_template | permissive | reverse_libs + [:] | true | null | false | false + [allow_scm_jenkinsfile:false, reverse_library_resolution:true] | false | null | false | true + [allow_scm_jenkinsfile:false, pipeline_template:'dev_template', permissive_initialization:true] | false | 'dev_template' | true | false } } diff --git a/src/test/groovy/org/boozallen/plugins/jte/init/primitives/injectors/LibraryStepInjectorSpec.groovy b/src/test/groovy/org/boozallen/plugins/jte/init/primitives/injectors/LibraryStepInjectorSpec.groovy new file mode 100644 index 000000000..3d3593cec --- /dev/null +++ b/src/test/groovy/org/boozallen/plugins/jte/init/primitives/injectors/LibraryStepInjectorSpec.groovy @@ -0,0 +1,332 @@ +/* + Copyright 2018 Booz Allen Hamilton + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package org.boozallen.plugins.jte.init.primitives.injectors + +import org.boozallen.plugins.jte.init.PipelineDecorator +import org.boozallen.plugins.jte.init.governance.GovernanceTier +import org.boozallen.plugins.jte.init.governance.config.dsl.PipelineConfigurationObject +import org.boozallen.plugins.jte.init.governance.libs.LibraryProvider +import org.boozallen.plugins.jte.init.governance.libs.LibrarySource +import org.boozallen.plugins.jte.init.primitives.TemplateBinding +import org.boozallen.plugins.jte.util.AggregateException +import org.jenkinsci.plugins.workflow.cps.CpsScript +import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner +import org.jenkinsci.plugins.workflow.job.WorkflowJob +import org.junit.ClassRule +import org.jvnet.hudson.test.JenkinsRule +import org.jvnet.hudson.test.WithoutJenkins +import spock.lang.Shared +import spock.lang.Specification + +class LibraryStepInjectorSpec extends Specification{ + + class JobChild { + WorkflowJob getParent(){ return null } + } + + @Shared @ClassRule JenkinsRule jenkins = new JenkinsRule() + CpsScript script = Mock() + PrintStream logger = Mock() + + WorkflowJob job = GroovyMock() + LibraryStepInjector injector = new LibraryStepInjector() + FlowExecutionOwner flowExecutionOwner = GroovyMock{ + run() >> GroovyMock(JobChild){ + getParent() >> job + } + } + + TemplateBinding templateBinding = Mock() + LinkedHashMap config = [ + jte: [:], + libraries: [:] + ] + + PipelineConfigurationObject pipelineConfigurationObject = pipelineConfigurationObject = Mock{ + getConfig() >> config + + getJteBlockWrapper() >> { return config.jte as PipelineDecorator.JteBlockWrapper } + } + + class MockLibraryProvider extends LibraryProvider{ + + @Override + Boolean hasLibrary(FlowExecutionOwner flowOwner, String libraryName) { + return false + } + + @Override + String getLibrarySchema(FlowExecutionOwner flowOwner, String libraryName) { + return null + } + + @Override + void loadLibrary(FlowExecutionOwner flowOwner, Binding binding, String libName, Map libConfig) { + } + + } + + @WithoutJenkins + def "when library source has library, loadLibrary is called"(){ + setup: + String libraryName = "libA" + config.libraries["libA"] = [:] + + MockLibraryProvider p1 = Mock{ + hasLibrary(flowExecutionOwner, libraryName) >> true + } + + LibrarySource s1 = Mock{ + getLibraryProvider() >> p1 + } + + GovernanceTier t1 = GroovyMock(global:true){ + getLibrarySources() >> [ s1 ] + } + + GovernanceTier.getHierarchy(_) >> [ t1 ] + + when: + injector.injectPrimitives(flowExecutionOwner, pipelineConfigurationObject, templateBinding) + + then: + 1 * p1.loadLibrary(flowExecutionOwner, templateBinding, libraryName, _) + } + + @WithoutJenkins + def "Libraries can be loaded across library sources in a governance tier"(){ + setup: + config.libraries["libA"] = [:] + config.libraries["libB"] = [:] + + MockLibraryProvider p1 = Mock{ + hasLibrary(flowExecutionOwner, "libA") >> true + } + MockLibraryProvider p2 = Mock{ + hasLibrary(flowExecutionOwner, "libB") >> true + } + + LibrarySource s1 = Mock{ + getLibraryProvider() >> p1 + } + LibrarySource s2 = Mock{ + getLibraryProvider() >> p2 + } + + GovernanceTier t1 = GroovyMock(global:true){ + getLibrarySources() >> [ s1, s2 ] + } + + GovernanceTier.getHierarchy(_) >> [ t1 ] + + when: + injector.injectPrimitives(flowExecutionOwner, pipelineConfigurationObject, templateBinding) + + then: + 1 * p1.loadLibrary(flowExecutionOwner, templateBinding, "libA", _) + 0 * p1.loadLibrary(flowExecutionOwner, templateBinding, "libB", _) + 1 * p2.loadLibrary(flowExecutionOwner, templateBinding, "libB", _) + 0 * p2.loadLibrary(flowExecutionOwner, templateBinding, "libA", _) + } + + @WithoutJenkins + def "Libraries can be loaded across library sources in different governance tiers"(){ + setup: + config.libraries["libA"] = [:] + config.libraries["libB"] = [:] + + MockLibraryProvider p1 = Mock{ + hasLibrary(flowExecutionOwner, "libA") >> true + } + MockLibraryProvider p2 = Mock{ + hasLibrary(flowExecutionOwner, "libB") >> true + } + + LibrarySource s1 = Mock{ + getLibraryProvider() >> p1 + } + LibrarySource s2 = Mock{ + getLibraryProvider() >> p2 + } + + GovernanceTier tier1 = Mock{ + getLibrarySources() >> [ s1, s2 ] + } + + GovernanceTier tier2 = GroovyMock(global:true){ + getLibrarySources() >> [ s1, s2 ] + } + + GovernanceTier.getHierarchy(_) >> [ tier1, tier2 ] + + when: + injector.injectPrimitives(flowExecutionOwner, pipelineConfigurationObject, templateBinding) + + then: + 1 * p1.loadLibrary(flowExecutionOwner, templateBinding, "libA", _) + 0 * p1.loadLibrary(flowExecutionOwner, templateBinding, "libB", _) + 0 * p2.loadLibrary(flowExecutionOwner, templateBinding, "libA", _) + 1 * p2.loadLibrary(flowExecutionOwner, templateBinding, "libB", _) + } + + @WithoutJenkins + def "library on more granular governance tier gets loaded"(){ + setup: + config.libraries["libA"] = [:] + + MockLibraryProvider p1 = Mock{ + hasLibrary(flowExecutionOwner, "libA") >> true + } + MockLibraryProvider p2 = Mock{ + hasLibrary(flowExecutionOwner, "libA") >> true + } + + LibrarySource s1 = Mock{ + getLibraryProvider() >> p1 + } + LibrarySource s2 = Mock{ + getLibraryProvider() >> p2 + } + + GovernanceTier tier1 = Mock{ + getLibrarySources() >> [ s1 ] + } + + GovernanceTier tier2 = GroovyMock(global:true){ + getLibrarySources() >> [ s2 ] + } + + GovernanceTier.getHierarchy(_) >> [ tier1, tier2 ] + + when: + injector.injectPrimitives(flowExecutionOwner, pipelineConfigurationObject, templateBinding) + + then: + 1 * p1.loadLibrary(flowExecutionOwner, templateBinding, "libA", _) + 0 * p2.loadLibrary(flowExecutionOwner, templateBinding, "libA", _) + } + + @WithoutJenkins + def "library on higher governance tier (last in hierarchy array) gets loaded if library override set to false"(){ + setup: + config.jte['reverse_library_resolution'] = true + config.libraries["libA"] = [:] + + MockLibraryProvider p1 = Mock{ + hasLibrary(flowExecutionOwner, "libA") >> true + } + MockLibraryProvider p2 = Mock{ + hasLibrary(flowExecutionOwner, "libA") >> true + } + + LibrarySource s1 = Mock{ + getLibraryProvider() >> p1 + } + LibrarySource s2 = Mock{ + getLibraryProvider() >> p2 + } + + GovernanceTier t1 = Mock{ + getLibrarySources() >> [ s1 ] + } + + GovernanceTier t2 = GroovyMock(global:true){ + getLibrarySources() >> [ s2 ] + } + + GovernanceTier.getHierarchy(_) >> [ t1, t2 ] + + when: + injector.injectPrimitives(flowExecutionOwner, pipelineConfigurationObject, templateBinding) + + then: + 0 * p1.loadLibrary(flowExecutionOwner, templateBinding, "libA", _) + 1 * p2.loadLibrary(flowExecutionOwner, templateBinding, "libA", _) + } + + @WithoutJenkins + def "library loader correctly passes step config"(){ + setup: + config.libraries = [ + libA: [ + fieldA: "A" + ], + libB: [ + fieldB: "B" + ] + ] + + MockLibraryProvider p1 = Mock{ + hasLibrary(flowExecutionOwner, "libA") >> true + hasLibrary(flowExecutionOwner, "libB") >> true + } + + LibrarySource s1 = Mock{ + getLibraryProvider() >> p1 + } + + GovernanceTier t1 = GroovyMock(global:true){ + getLibrarySources() >> [ s1 ] + } + + GovernanceTier.getHierarchy(_) >> [ t1 ] + + when: + injector.injectPrimitives(flowExecutionOwner, pipelineConfigurationObject, templateBinding) + + then: + 1 * p1.loadLibrary(flowExecutionOwner, templateBinding, "libA", [fieldA: "A"]) + 1 * p1.loadLibrary(flowExecutionOwner, templateBinding, "libB", [fieldB: "B"]) + } + + @WithoutJenkins + def "Missing library throws exception"(){ + // now, when a library isn't found, we push a message onto the `libConfigErrors` array + // and throw the exception later after validating all the libraries. + // so this test represents making sure that an exception is thrown if a library does not exist. + setup: + config.libraries = [ + libA: [ + fieldA: "A" + ], + libB: [ + fieldB: "B" + ] + ] + + MockLibraryProvider p = Mock{ + 1 * hasLibrary(flowExecutionOwner, "libA") >> true + 1 * hasLibrary(flowExecutionOwner, "libB") >> false + } + + LibrarySource s = Mock{ + getLibraryProvider() >> p + } + + GovernanceTier tier = GroovyMock(global:true){ + getLibrarySources() >> [ s ] + } + + GovernanceTier.getHierarchy(_) >> [ tier ] + + when: + injector.validateConfiguration(flowExecutionOwner, pipelineConfigurationObject) + + then: + thrown(AggregateException) + } + +}