diff --git a/docs/concepts/pipeline-configuration/overview.md b/docs/concepts/pipeline-configuration/overview.md index 998ac471b..9d8e6ae33 100644 --- a/docs/concepts/pipeline-configuration/overview.md +++ b/docs/concepts/pipeline-configuration/overview.md @@ -35,4 +35,4 @@ Pipeline Configurations can be stored in a couple different locations depending | Job Type | Pipeline Configuration Location | |----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| | Pipeline Job | Either in the Jenkins UI or at the root of a remote source code repository as a file called `pipeline_config.groovy` | -| Multi-Branch Project | At the root of the repository in a file named `pipeline_config.groovy` in the branch job that was created as part of the Multi-Branch Project | +| Multi-Branch Project | At the root of the repository in a file named `pipeline_config.groovy` (or to any arbitrary path in the repository as configured by `configurationPath`) in the branch job that was created as part of the Multi-Branch Project | diff --git a/docs/concepts/pipeline-governance/pipeline-template-selection.md b/docs/concepts/pipeline-governance/pipeline-template-selection.md index 878169fe8..bc60be06e 100644 --- a/docs/concepts/pipeline-governance/pipeline-template-selection.md +++ b/docs/concepts/pipeline-governance/pipeline-template-selection.md @@ -23,7 +23,7 @@ If not, JTE will follow the flow described throughout the rest of this document. ### MultiBranch Project Pipeline Jobs -For MultiBranch Project Pipeline Jobs, if the source code repository has a `Jenkinsfile` at the root **and** `jte.allow_scm_jenkinsfile` is set to `True`, then the repository `Jenkinsfile` will be used as the Pipeline Template. +For MultiBranch Project Pipeline Jobs, if the source code repository has a `Jenkinsfile` at the root (or at any arbitrary path in the repository as configured by `scriptPath`) **and** `jte.allow_scm_jenkinsfile` is set to `True`, then the repository `Jenkinsfile` will be used as the Pipeline Template. !!! important "Disabling Repository Jenkinsfiles" It's important that when trying to enforce a certain set of Pipeline Templates are used that `jte.allow_scm_jenkinsfile` is set to `False`. diff --git a/src/main/groovy/org/boozallen/plugins/jte/init/PipelineConfigurationAggregator.groovy b/src/main/groovy/org/boozallen/plugins/jte/init/PipelineConfigurationAggregator.groovy index 2ab3f6bd3..14510ab9b 100644 --- a/src/main/groovy/org/boozallen/plugins/jte/init/PipelineConfigurationAggregator.groovy +++ b/src/main/groovy/org/boozallen/plugins/jte/init/PipelineConfigurationAggregator.groovy @@ -20,6 +20,7 @@ import org.boozallen.plugins.jte.init.governance.config.ScmPipelineConfiguration import org.boozallen.plugins.jte.init.governance.config.dsl.PipelineConfigurationDsl import org.boozallen.plugins.jte.init.governance.config.dsl.PipelineConfigurationObject import org.boozallen.plugins.jte.job.AdHocTemplateFlowDefinition +import org.boozallen.plugins.jte.job.MultibranchTemplateFlowDefinition import org.boozallen.plugins.jte.util.FileSystemWrapper import org.boozallen.plugins.jte.util.TemplateLogger import org.jenkinsci.plugins.workflow.flow.FlowDefinition @@ -74,7 +75,14 @@ class PipelineConfigurationAggregator { } else { // get job config if present FileSystemWrapper fsw = FileSystemWrapper.createFromJob(flowOwner) - String repoConfigFile = fsw.getFileContents(ScmPipelineConfigurationProvider.CONFIG_FILE, "Template Configuration File", false) + // enable custom path to config file instead of default pipeline_config.groovy at root + String configurationPath + if (flowDefinition instanceof MultibranchTemplateFlowDefinition) { + configurationPath = flowDefinition.getConfigurationPath() + } else { + configurationPath = ScmPipelineConfigurationProvider.CONFIG_FILE + } + String repoConfigFile = fsw.getFileContents(configurationPath, "Template Configuration File", false) if (repoConfigFile){ try{ jobConfig = new PipelineConfigurationDsl(flowOwner).parse(repoConfigFile) diff --git a/src/main/groovy/org/boozallen/plugins/jte/init/PipelineTemplateResolver.groovy b/src/main/groovy/org/boozallen/plugins/jte/init/PipelineTemplateResolver.groovy index 93c55f55d..861da0a49 100644 --- a/src/main/groovy/org/boozallen/plugins/jte/init/PipelineTemplateResolver.groovy +++ b/src/main/groovy/org/boozallen/plugins/jte/init/PipelineTemplateResolver.groovy @@ -18,6 +18,7 @@ package org.boozallen.plugins.jte.init import org.boozallen.plugins.jte.init.governance.GovernanceTier import org.boozallen.plugins.jte.init.governance.config.dsl.PipelineConfigurationObject import org.boozallen.plugins.jte.job.AdHocTemplateFlowDefinition +import org.boozallen.plugins.jte.job.MultibranchTemplateFlowDefinition import org.boozallen.plugins.jte.util.FileSystemWrapper import org.boozallen.plugins.jte.util.TemplateLogger import org.jenkinsci.plugins.workflow.flow.FlowDefinition @@ -50,7 +51,14 @@ class PipelineTemplateResolver { } } else { FileSystemWrapper fs = FileSystemWrapper.createFromJob(flowOwner) - String repoJenkinsfile = fs.getFileContents("Jenkinsfile", "Repository Jenkinsfile", false) + // enable custom path to template file instead of default Jenkinsfile at root + String templatePath + if (flowDefinition instanceof MultibranchTemplateFlowDefinition) { + templatePath = flowDefinition.getScriptPath() + } else { + templatePath = "Jenkinsfile" + } + String repoJenkinsfile = fs.getFileContents(templatePath, "Repository Jenkinsfile", false) if (repoJenkinsfile){ if (jteBlockWrapper.allow_scm_jenkinsfile){ return repoJenkinsfile diff --git a/src/main/groovy/org/boozallen/plugins/jte/job/MultibranchTemplateFlowDefinition.groovy b/src/main/groovy/org/boozallen/plugins/jte/job/MultibranchTemplateFlowDefinition.groovy index fa11257ee..7c243f334 100644 --- a/src/main/groovy/org/boozallen/plugins/jte/job/MultibranchTemplateFlowDefinition.groovy +++ b/src/main/groovy/org/boozallen/plugins/jte/job/MultibranchTemplateFlowDefinition.groovy @@ -21,12 +21,34 @@ import hudson.model.DescriptorVisibilityFilter import org.jenkinsci.plugins.workflow.flow.FlowDefinitionDescriptor import org.jenkinsci.plugins.workflow.job.WorkflowJob import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject +import org.kohsuke.stapler.DataBoundSetter /** * Allows JTE to be used in a MultiBranch Pipeline */ class MultibranchTemplateFlowDefinition extends TemplateFlowDefinition { + String scriptPath + String configurationPath + + @DataBoundSetter + void setScriptPath(String scriptPath){ + this.scriptPath = scriptPath + } + + String getScriptPath(){ + return scriptPath + } + + @DataBoundSetter + void setConfigurationPath(String configurationPath){ + this.configurationPath = configurationPath + } + + String getConfigurationPath(){ + return configurationPath + } + @Extension static class DescriptorImpl extends FlowDefinitionDescriptor { @Override diff --git a/src/main/groovy/org/boozallen/plugins/jte/job/TemplateBranchProjectFactory.groovy b/src/main/groovy/org/boozallen/plugins/jte/job/TemplateBranchProjectFactory.groovy index c498ca9d1..d7f779ff9 100644 --- a/src/main/groovy/org/boozallen/plugins/jte/job/TemplateBranchProjectFactory.groovy +++ b/src/main/groovy/org/boozallen/plugins/jte/job/TemplateBranchProjectFactory.groovy @@ -36,6 +36,7 @@ import javax.annotation.Nonnull */ class TemplateBranchProjectFactory extends WorkflowBranchProjectFactory { + String configurationPath Boolean filterBranches // jenkins requires this be here @@ -44,12 +45,24 @@ class TemplateBranchProjectFactory extends WorkflowBranchProjectFactory { TemplateBranchProjectFactory(){} Object readResolve() { + if (this.configurationPath == null) { + this.configurationPath = ScmPipelineConfigurationProvider.CONFIG_FILE + } if (this.filterBranches == null) { this.filterBranches = false } return this } + @DataBoundSetter + void setConfigurationPath(String configurationPath){ + this.configurationPath = configurationPath + } + + String getConfigurationPath(){ + return configurationPath + } + @DataBoundSetter void setFilterBranches(Boolean filterBranches){ this.filterBranches = filterBranches @@ -61,7 +74,10 @@ class TemplateBranchProjectFactory extends WorkflowBranchProjectFactory { @Override protected FlowDefinition createDefinition() { - return new MultibranchTemplateFlowDefinition() + MultibranchTemplateFlowDefinition definition = new MultibranchTemplateFlowDefinition() + definition.setScriptPath(this.scriptPath) + definition.setConfigurationPath(this.configurationPath) + return definition } @Override @@ -75,20 +91,20 @@ class TemplateBranchProjectFactory extends WorkflowBranchProjectFactory { } // if user chose to filter branches, check for pipeline config file - SCMProbeStat stat = probe.stat(ScmPipelineConfigurationProvider.CONFIG_FILE) + SCMProbeStat stat = probe.stat(configurationPath) switch (stat.getType()) { case SCMFile.Type.NONEXISTENT: if (stat.getAlternativePath() != null) { - listener.getLogger().format(" ‘%s’ not found (but found ‘%s’, search is case sensitive)%n", ScmPipelineConfigurationProvider.CONFIG_FILE, stat.getAlternativePath()) + listener.getLogger().format(" ‘%s’ not found (but found ‘%s’, search is case sensitive)%n", configurationPath, stat.getAlternativePath()) } else { - listener.getLogger().format(" ‘%s’ not found%n", ScmPipelineConfigurationProvider.CONFIG_FILE) + listener.getLogger().format(" ‘%s’ not found%n", configurationPath) } return false case SCMFile.Type.DIRECTORY: - listener.getLogger().format(" ‘%s’ found but is a directory not a file%n", ScmPipelineConfigurationProvider.CONFIG_FILE) + listener.getLogger().format(" ‘%s’ found but is a directory not a file%n", configurationPath) return false default: - listener.getLogger().format(" ‘%s’ found%n", ScmPipelineConfigurationProvider.CONFIG_FILE) + listener.getLogger().format(" ‘%s’ found%n", configurationPath) return true } } diff --git a/src/main/groovy/org/boozallen/plugins/jte/job/TemplateMultiBranchProjectFactory.groovy b/src/main/groovy/org/boozallen/plugins/jte/job/TemplateMultiBranchProjectFactory.groovy index 2a7c2e1ac..f10d3c330 100644 --- a/src/main/groovy/org/boozallen/plugins/jte/job/TemplateMultiBranchProjectFactory.groovy +++ b/src/main/groovy/org/boozallen/plugins/jte/job/TemplateMultiBranchProjectFactory.groovy @@ -27,6 +27,7 @@ import jenkins.branch.OrganizationFolder import jenkins.model.TransientActionFactory import jenkins.scm.api.SCMSource import jenkins.scm.api.SCMSourceCriteria +import org.boozallen.plugins.jte.init.governance.config.ScmPipelineConfigurationProvider import org.jenkinsci.plugins.workflow.cps.Snippetizer import org.jenkinsci.plugins.workflow.multibranch.AbstractWorkflowBranchProjectFactory import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject @@ -40,6 +41,8 @@ import org.kohsuke.stapler.DataBoundSetter */ class TemplateMultiBranchProjectFactory extends MultiBranchProjectFactory.BySCMSourceCriteria { + String scriptPath + String configurationPath Boolean filterBranches // jenkins requires this be here @@ -48,12 +51,36 @@ class TemplateMultiBranchProjectFactory extends MultiBranchProjectFactory.BySCMS TemplateMultiBranchProjectFactory(){} Object readResolve() { + if (this.scriptPath == null) { + this.scriptPath = 'Jenkinsfile' + } + if (this.configurationPath == null) { + this.configurationPath = ScmPipelineConfigurationProvider.CONFIG_FILE + } if (this.filterBranches == null) { this.filterBranches = false } return this } + @DataBoundSetter + void setScriptPath(String scriptPath){ + this.scriptPath = scriptPath + } + + String getScriptPath(){ + return scriptPath + } + + @DataBoundSetter + void setConfigurationPath(String configurationPath){ + this.configurationPath = configurationPath + } + + String getConfigurationPath(){ + return configurationPath + } + @DataBoundSetter void setFilterBranches(Boolean filterBranches){ this.filterBranches = filterBranches @@ -72,6 +99,8 @@ class TemplateMultiBranchProjectFactory extends MultiBranchProjectFactory.BySCMS private AbstractWorkflowBranchProjectFactory newProjectFactory() { TemplateBranchProjectFactory factory = new TemplateBranchProjectFactory() + factory.setScriptPath(this.scriptPath) + factory.setConfigurationPath(this.configurationPath) factory.setFilterBranches(this.filterBranches) return factory } diff --git a/src/main/resources/org/boozallen/plugins/jte/job/TemplateBranchProjectFactory/config.jelly b/src/main/resources/org/boozallen/plugins/jte/job/TemplateBranchProjectFactory/config.jelly index a7b08c8be..8f8aec247 100644 --- a/src/main/resources/org/boozallen/plugins/jte/job/TemplateBranchProjectFactory/config.jelly +++ b/src/main/resources/org/boozallen/plugins/jte/job/TemplateBranchProjectFactory/config.jelly @@ -16,6 +16,12 @@ --> + + + + + + diff --git a/src/main/resources/org/boozallen/plugins/jte/job/TemplateBranchProjectFactory/help-configurationPath.html b/src/main/resources/org/boozallen/plugins/jte/job/TemplateBranchProjectFactory/help-configurationPath.html new file mode 100644 index 000000000..f7f7c23f5 --- /dev/null +++ b/src/main/resources/org/boozallen/plugins/jte/job/TemplateBranchProjectFactory/help-configurationPath.html @@ -0,0 +1,3 @@ +

+ A Pipeline Configuration file that will be used when loading this Pipeline. +

\ No newline at end of file diff --git a/src/main/resources/org/boozallen/plugins/jte/job/TemplateBranchProjectFactory/help-filterBranches.html b/src/main/resources/org/boozallen/plugins/jte/job/TemplateBranchProjectFactory/help-filterBranches.html index 2ab0ec50e..362f0d2ac 100644 --- a/src/main/resources/org/boozallen/plugins/jte/job/TemplateBranchProjectFactory/help-filterBranches.html +++ b/src/main/resources/org/boozallen/plugins/jte/job/TemplateBranchProjectFactory/help-filterBranches.html @@ -1,5 +1,6 @@
- If checked, the Jenkins Templating Engine will exclude branches that do not have a Pipeline Configuration file (pipeline_config.groovy) at the root of the repository. - - If unchecked, a pipeline will be created for all branches. + If checked, the Jenkins Templating Engine will exclude branches that do not have a Pipeline Configuration file + (pipeline_config.groovy) in the repository at the path specified by configurationPath. + + If unchecked, a pipeline will be created for all branches.
\ No newline at end of file diff --git a/src/main/resources/org/boozallen/plugins/jte/job/TemplateMultiBranchProjectFactory/config.jelly b/src/main/resources/org/boozallen/plugins/jte/job/TemplateMultiBranchProjectFactory/config.jelly index d6fad8be8..d57ca3aff 100644 --- a/src/main/resources/org/boozallen/plugins/jte/job/TemplateMultiBranchProjectFactory/config.jelly +++ b/src/main/resources/org/boozallen/plugins/jte/job/TemplateMultiBranchProjectFactory/config.jelly @@ -16,6 +16,12 @@ --> + + + + + + diff --git a/src/main/resources/org/boozallen/plugins/jte/job/TemplateMultiBranchProjectFactory/help-configurationPath.html b/src/main/resources/org/boozallen/plugins/jte/job/TemplateMultiBranchProjectFactory/help-configurationPath.html new file mode 100644 index 000000000..f7f7c23f5 --- /dev/null +++ b/src/main/resources/org/boozallen/plugins/jte/job/TemplateMultiBranchProjectFactory/help-configurationPath.html @@ -0,0 +1,3 @@ +

+ A Pipeline Configuration file that will be used when loading this Pipeline. +

\ No newline at end of file diff --git a/src/main/resources/org/boozallen/plugins/jte/job/TemplateMultiBranchProjectFactory/help-filterBranches.html b/src/main/resources/org/boozallen/plugins/jte/job/TemplateMultiBranchProjectFactory/help-filterBranches.html index 2ab0ec50e..362f0d2ac 100644 --- a/src/main/resources/org/boozallen/plugins/jte/job/TemplateMultiBranchProjectFactory/help-filterBranches.html +++ b/src/main/resources/org/boozallen/plugins/jte/job/TemplateMultiBranchProjectFactory/help-filterBranches.html @@ -1,5 +1,6 @@
- If checked, the Jenkins Templating Engine will exclude branches that do not have a Pipeline Configuration file (pipeline_config.groovy) at the root of the repository. - - If unchecked, a pipeline will be created for all branches. + If checked, the Jenkins Templating Engine will exclude branches that do not have a Pipeline Configuration file + (pipeline_config.groovy) in the repository at the path specified by configurationPath. + + If unchecked, a pipeline will be created for all branches.
\ No newline at end of file diff --git a/src/main/resources/org/boozallen/plugins/jte/job/TemplateMultiBranchProjectFactory/help-scriptPath.html b/src/main/resources/org/boozallen/plugins/jte/job/TemplateMultiBranchProjectFactory/help-scriptPath.html new file mode 100644 index 000000000..7cb5add4b --- /dev/null +++ b/src/main/resources/org/boozallen/plugins/jte/job/TemplateMultiBranchProjectFactory/help-scriptPath.html @@ -0,0 +1,3 @@ +

+ Relative location within the checkout of your Pipeline script. +

\ No newline at end of file diff --git a/src/test/groovy/org/boozallen/plugins/jte/job/TemplateBranchProjectFactorySpec.groovy b/src/test/groovy/org/boozallen/plugins/jte/job/TemplateBranchProjectFactorySpec.groovy new file mode 100644 index 000000000..7dd9df174 --- /dev/null +++ b/src/test/groovy/org/boozallen/plugins/jte/job/TemplateBranchProjectFactorySpec.groovy @@ -0,0 +1,105 @@ +/* + 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.job + +import jenkins.branch.BranchSource +import jenkins.branch.OrganizationFolder +import jenkins.plugins.git.GitSCMSource +import jenkins.plugins.git.GitSampleRepoRule +import jenkins.scm.impl.SingleSCMNavigator +import org.jenkinsci.plugins.workflow.job.WorkflowRun +import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject +import org.junit.ClassRule +import org.junit.Rule +import org.jvnet.hudson.test.JenkinsRule +import spock.lang.Shared +import spock.lang.Specification + +class TemplateBranchProjectFactorySpec extends Specification { + + @Shared @ClassRule JenkinsRule jenkins = new JenkinsRule() + @Rule GitSampleRepoRule repo = new GitSampleRepoRule() + + String templatePath = 'dir/Jenkinsfile' + String configPath = 'dir/pipeline_config.groovy' + + def setup() { + String pipelineConfigContents = ''' + block{ + custom = 'hello' + } + ''' + String pipelineTemplateContents = ''' + node { + stage('Build') { + echo "build number: ${env.BUILD_NUMBER}" + echo "branch: ${env.BRANCH_NAME}" + echo "custom: ${pipelineConfig.block.custom}" + } + } + ''' + repo.init() + repo.write(templatePath, pipelineTemplateContents) + repo.write(configPath, pipelineConfigContents) + repo.git('add', '*') + repo.git('commit', '--message=init') + } + + def "Specify custom template and configuration path with TemplateBranchProjectFactory"() { + given: + WorkflowMultiBranchProject mp = jenkins.createProject(WorkflowMultiBranchProject) + mp.getSourcesList().add(new BranchSource(new GitSCMSource(null, repo.toString(), "", "*", "", false))) + TemplateBranchProjectFactory factory = new TemplateBranchProjectFactory() + factory.setConfigurationPath(configPath) + factory.setScriptPath(templatePath) + factory.setFilterBranches(true) + mp.setProjectFactory(factory) + + when: + mp.scheduleBuild2(0).getFuture().get() + jenkins.waitUntilNoActivity() + WorkflowRun run = mp.getItem('master').getLastBuild() + + then: + jenkins.assertBuildStatusSuccess(run) + jenkins.assertLogContains('custom: hello', run) + jenkins.assertLogContains('branch: master', run) + jenkins.assertLogContains('build number: 1', run) + } + + def "Specify custom template and configuration path with TemplateMultiBranchProjectFactory"() { + given: + OrganizationFolder folder = jenkins.createProject(OrganizationFolder) + folder.getNavigators().add(new SingleSCMNavigator('repo', [new GitSCMSource(null, repo.toString(), "", "*", "", false)])) + TemplateMultiBranchProjectFactory factory = new TemplateMultiBranchProjectFactory() + factory.setConfigurationPath(configPath) + factory.setScriptPath(templatePath) + factory.setFilterBranches(true) + folder.getProjectFactories().add(factory) + + when: + folder.scheduleBuild2(0).getFuture().get() + jenkins.waitUntilNoActivity() + WorkflowRun run = folder.getItem('repo').getItem('master').getLastBuild() + + then: + jenkins.assertBuildStatusSuccess(run) + jenkins.assertLogContains('custom: hello', run) + jenkins.assertLogContains('branch: master', run) + jenkins.assertLogContains('build number: 1', run) + } + +}