diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 8c9f406d4..3493a9de1 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -26,7 +26,7 @@ jobs: - uses: eskatos/gradle-command-action@v1 with: arguments: --no-daemon codenarc - - uses: actions/upload-artifact@v1 + - uses: actions/upload-artifact@v4 if: failure() with: name: codenarc-results @@ -45,7 +45,7 @@ jobs: runs-on: ubuntu-latest if: github.repository == 'jenkinsci/templating-engine-plugin' container: - image: jdkato/vale:v2.18.0 + image: jdkato/vale:v2.21.3 options: --user root steps: - uses: actions/checkout@v2 @@ -62,7 +62,7 @@ jobs: - uses: eskatos/gradle-command-action@v1 with: arguments: --no-daemon test jacocoTestReport - - uses: actions/upload-artifact@v1 + - uses: actions/upload-artifact@v4 if: failure() with: name: test-results @@ -78,7 +78,7 @@ jobs: - uses: eskatos/gradle-command-action@v1 with: arguments: --no-daemon jpi - - uses: actions/upload-artifact@v1 + - uses: actions/upload-artifact@v4 with: name: jpi path: build/libs/templating-engine.hpi diff --git a/docs/styles/Vocab/JTE/accept.txt b/docs/styles/Vocab/JTE/accept.txt index 897d1e364..c8687e434 100644 --- a/docs/styles/Vocab/JTE/accept.txt +++ b/docs/styles/Vocab/JTE/accept.txt @@ -33,3 +33,5 @@ Splunk [Tt]emplat(e|es|ed|ing)\b truthy [Ww]alkthroughs? +JTE's +stageContext \ No newline at end of file diff --git a/src/main/groovy/org/boozallen/plugins/jte/init/primitives/TemplatePrimitiveInjector.groovy b/src/main/groovy/org/boozallen/plugins/jte/init/primitives/TemplatePrimitiveInjector.groovy index cd7a0d4da..099926305 100644 --- a/src/main/groovy/org/boozallen/plugins/jte/init/primitives/TemplatePrimitiveInjector.groovy +++ b/src/main/groovy/org/boozallen/plugins/jte/init/primitives/TemplatePrimitiveInjector.groovy @@ -20,6 +20,7 @@ import hudson.ExtensionPoint import jenkins.model.Jenkins import org.boozallen.plugins.jte.init.governance.config.dsl.PipelineConfigurationObject import org.boozallen.plugins.jte.util.AggregateException +import org.boozallen.plugins.jte.util.FileSystemWrapperFactory import org.boozallen.plugins.jte.util.JTEException import org.codehaus.groovy.reflection.CachedMethod import org.jenkinsci.plugins.workflow.cps.CpsFlowExecution @@ -84,14 +85,18 @@ abstract class TemplatePrimitiveInjector implements ExtensionPoint{ * @param config the aggregated pipeline configuration */ static void orchestrate(CpsFlowExecution exec, PipelineConfigurationObject config){ - invoke("validateConfiguration", exec, config) - FlowExecutionOwner flowOwner = exec.getOwner() - WorkflowRun run = flowOwner.run() - TemplatePrimitiveCollector collector = new TemplatePrimitiveCollector() - run.addOrReplaceAction(collector) // may be used by one of the injectors - invoke(run, collector, "injectPrimitives", exec, config) - invoke("validatePrimitives", exec, config, collector) - run.addOrReplaceAction(collector) + try { + invoke("validateConfiguration", exec, config) + FlowExecutionOwner flowOwner = exec.getOwner() + WorkflowRun run = flowOwner.run() + TemplatePrimitiveCollector collector = new TemplatePrimitiveCollector() + run.addOrReplaceAction(collector) // may be used by one of the injectors + invoke(run, collector, "injectPrimitives", exec, config) + invoke("validatePrimitives", exec, config, collector) + run.addOrReplaceAction(collector) + } finally { + FileSystemWrapperFactory.clearCache(exec.getOwner()) + } } /** 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 885e69808..fe2061287 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 @@ -90,6 +90,7 @@ import org.jenkinsci.plugins.workflow.job.WorkflowJob // load all the libraries // this will copy their contents to ${jteDir} for the run LinkedHashMap aggregatedConfig = config.getConfig() + aggregatedConfig[KEY].each{ libName, libConfig -> LibraryProvider provider = providers.find{ provider -> provider.hasLibrary(flowOwner, libName) diff --git a/src/main/groovy/org/boozallen/plugins/jte/job/TemplateFlowDefinition.groovy b/src/main/groovy/org/boozallen/plugins/jte/job/TemplateFlowDefinition.groovy index d869c26cb..98294d509 100644 --- a/src/main/groovy/org/boozallen/plugins/jte/job/TemplateFlowDefinition.groovy +++ b/src/main/groovy/org/boozallen/plugins/jte/job/TemplateFlowDefinition.groovy @@ -83,7 +83,6 @@ abstract class TemplateFlowDefinition extends FlowDefinition { CpsFlowExecution exec = new CpsFlowExecution(template, true, owner, hint) // Step 3: Invoke TemplatePrimitiveInjectors TemplatePrimitiveInjector.orchestrate(exec, config) - return exec } diff --git a/src/main/groovy/org/boozallen/plugins/jte/util/FileSystemCacheKey.groovy b/src/main/groovy/org/boozallen/plugins/jte/util/FileSystemCacheKey.groovy new file mode 100644 index 000000000..320d0e396 --- /dev/null +++ b/src/main/groovy/org/boozallen/plugins/jte/util/FileSystemCacheKey.groovy @@ -0,0 +1,134 @@ +/* + 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.util + +import hudson.scm.SCM +import jenkins.scm.api.SCMHead +import jenkins.scm.api.SCMRevision +import jenkins.scm.api.SCMSource +import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner + +/** + * The FileSystemCacheKey class is a utility for creating cache keys based on various + * SCM (Source Code Management) related objects within the Jenkins Pipeline ecosystem. + * This key is intended to uniquely identify a cache entry based on SCM configurations + * and the owner of the pipeline execution. + * + *

It overrides the {@link #equals(Object)} and {@link #hashCode()} methods to ensure + * that cache keys are properly compared and can be used effectively in hash-based collections.

+ * + *

This class is used in caching scenarios to avoid re-fetching or recalculating information + * that has already been retrieved from the SCM system during a Jenkins pipeline execution.

+ * + *

Fields:

+ * + * + *

Methods:

+ * + * + *

Copyright: 2018 Booz Allen Hamilton

+ * + *

License: This class is licensed under the Apache License, Version 2.0. See the LICENSE file or + * visit http://www.apache.org/licenses/LICENSE-2.0 for details.

+ * + * @see FlowExecutionOwner + * @see SCM + * @see SCMSource + * @see SCMHead + * @see SCMRevision + */ +class FileSystemCacheKey { + + private static final int MULTIPLIER = 31 + + FlowExecutionOwner owner + SCM scm + SCMSource scmSource + SCMHead scmHead + SCMRevision scmRevision + + /** + * Compares this FileSystemCacheKey instance with another object to determine if they are equal. + * The comparison is based on the equality of the following fields: {@code owner}, {@code scm}, + * {@code scmSource}, {@code scmHead}, and {@code scmRevision}. + * + *

If the provided object is not an instance of {@code FileSystemCacheKey}, this method returns {@code false}.

+ * + * @param o the object to be compared with this instance. + * @return {@code true} if the provided object is equal to this instance, {@code false} otherwise. + */ + @Override + boolean equals(Object o) { + if (this.is(o)) { + return true + } + if (!(o instanceof FileSystemCacheKey)) { + return false + } + + FileSystemCacheKey that = (FileSystemCacheKey) o + + boolean result = true + + if (owner != that.owner) { + result = false + } + if (scm != that.scm) { + result = false + } + if (scmHead != that.scmHead) { + result = false + } + if (scmRevision != that.scmRevision) { + result = false + } + if (scmSource != that.scmSource) { + result = false + } + + return result + } +/** + * Generates a hash code for this FileSystemCacheKey instance. + * The hash code is computed based on the {@code owner}, {@code scm}, {@code scmSource}, + * {@code scmHead}, and {@code scmRevision} fields. + * + *

This method is used to efficiently store and retrieve instances of {@code FileSystemCacheKey} + * in hash-based collections such as {@link java.util.HashMap} or {@link java.util.HashSet}.

+ * + * @return the hash code value for this instance. + */ + @Override + int hashCode() { + int result + result = (owner != null ? owner.hashCode() : 0) + result = MULTIPLIER * result + (scm != null ? scm.hashCode() : 0) + result = MULTIPLIER * result + (scmSource != null ? scmSource.hashCode() : 0) + result = MULTIPLIER * result + (scmHead != null ? scmHead.hashCode() : 0) + result = MULTIPLIER * result + (scmRevision != null ? scmRevision.hashCode() : 0) + return result + } + +} diff --git a/src/main/groovy/org/boozallen/plugins/jte/util/FileSystemWrapperFactory.groovy b/src/main/groovy/org/boozallen/plugins/jte/util/FileSystemWrapperFactory.groovy index 01c703253..69d1e2353 100644 --- a/src/main/groovy/org/boozallen/plugins/jte/util/FileSystemWrapperFactory.groovy +++ b/src/main/groovy/org/boozallen/plugins/jte/util/FileSystemWrapperFactory.groovy @@ -36,6 +36,40 @@ import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject */ class FileSystemWrapperFactory { + /** + * A static cache that maps {@link FileSystemCacheKey} instances to their corresponding + * {@link FileSystemWrapper} objects. + * + *

This cache is used to avoid redundant filesystem operations by reusing {@code FileSystemWrapper} + * instances that have already been created for a given {@link FileSystemCacheKey}. It helps + * improve performance by preventing repeated lookups or calculations for the same SCM data + * during Jenkins pipeline executions.

+ * + *

The cache is implemented as a {@code Map}, where:

+ * + * + *

This cache is a static attribute of the class, shared across all instances of {@code FileSystemCacheKey}, + * and is initialized as an empty map, represented by {@code [:]}. It can grow dynamically as new entries are added during runtime.

+ */ + private final static Map CACHE = [:] + + static void clearCache(FlowExecutionOwner owner) { + if (!CACHE.isEmpty()) { + TemplateLogger logger = new TemplateLogger(owner.getListener()) + ArrayList msg = [ + "Values were found in the cache, so this helped reduce pipeline initiation time.", + ] + CACHE.entrySet().find { entry -> entry.getKey().getOwner() == owner }.each { entry -> + msg.add("-- scm ${entry.getValue().getScmKey()}") + } + logger.print(msg) + CACHE.entrySet().removeIf { entry -> entry.getKey().getOwner() == owner } + } + } + /** * Creates a FileSystemWrapper. Can either be provided an SCM directly * or try to infer the SCM from the current job @@ -73,16 +107,23 @@ class FileSystemWrapperFactory { throw new IllegalStateException("Unable to build a FileSystemWrapper") } - private static FileSystemWrapper fromSCM(FlowExecutionOwner owner, WorkflowJob job, SCM scm){ - SCMFileSystem fs - fs = SCMFileSystem.of(job, scm) - FileSystemWrapper fsw = new FileSystemWrapper(fs: fs, scmKey: scm.getKey(), owner: owner) + private static FileSystemWrapper fromSCM(FlowExecutionOwner owner, WorkflowJob job, SCM scm) { + FileSystemWrapper fsw + FileSystemCacheKey cacheKey = new FileSystemCacheKey(owner: owner, scm: scm) + if (CACHE.containsKey(cacheKey)) { + fsw = CACHE.get(cacheKey) + } else { + SCMFileSystem fs + fs = SCMFileSystem.of(job, scm) + fsw = new FileSystemWrapper(fs: fs, scmKey: scm.getKey(), owner: owner) + CACHE.put(cacheKey, fsw) + } + return fsw } private static FileSystemWrapper fromMultiBranchProject(FlowExecutionOwner owner, WorkflowJob job, TaskListener listener){ ItemGroup parent = job.getParent() - BranchJobProperty property = job.getProperty(BranchJobProperty) if (!property) { throw new JTEException("BranchJobProperty is somehow missing") @@ -96,19 +137,32 @@ class FileSystemWrapperFactory { SCMHead head = branch.getHead() SCMRevision tip = scmSource.fetch(head, listener) - + FileSystemWrapper fsw SCMFileSystem fs String scmKey if (tip) { scmKey = branch.getScm().getKey() SCMRevision rev = scmSource.getTrustedRevision(tip, listener) - fs = SCMFileSystem.of(scmSource, head, rev) + FileSystemCacheKey cacheKey = new FileSystemCacheKey(owner: owner, scmSource: scmSource, scmHead: head, scmRevision: rev) + if (CACHE.containsKey(cacheKey)) { + fsw = CACHE.get(cacheKey) + } else { + fs = SCMFileSystem.of(scmSource, head, rev) + fsw = new FileSystemWrapper(fs: fs, scmKey: scmKey, owner: owner) + CACHE.put(cacheKey, fsw) + } } else { SCM jobSCM = branch.getScm() - fs = SCMFileSystem.of(job, jobSCM) scmKey = jobSCM.getKey() + FileSystemCacheKey cacheKey = new FileSystemCacheKey(owner: owner, scm: jobSCM) + if (CACHE.containsKey(cacheKey)) { + fsw = CACHE.get(cacheKey) + } else { + fs = SCMFileSystem.of(job, jobSCM) + fsw = new FileSystemWrapper(fs: fs, scmKey: scmKey, owner: owner) + CACHE.put(cacheKey, fsw) + } } - FileSystemWrapper fsw = new FileSystemWrapper(fs: fs, scmKey: scmKey, owner: owner) return fsw } @@ -116,8 +170,15 @@ class FileSystemWrapperFactory { FlowDefinition definition = job.getDefinition() SCM jobSCM = definition.getScm() String scmKey = jobSCM.getKey() - SCMFileSystem fs = SCMFileSystem.of(job, jobSCM) - FileSystemWrapper fsw = new FileSystemWrapper(fs: fs, scmKey: scmKey, owner: owner) + FileSystemCacheKey cacheKey = new FileSystemCacheKey(owner: owner, scm: jobSCM) + FileSystemWrapper fsw + if (CACHE.containsKey(cacheKey)) { + fsw = CACHE.get(cacheKey) + } else { + SCMFileSystem fs = SCMFileSystem.of(job, jobSCM) + fsw = new FileSystemWrapper(fs: fs, scmKey: scmKey, owner: owner) + CACHE.put(cacheKey, fsw) + } return fsw }