首页 > 解决方案 > Jenkins Pipeline 中的 StreamingTemplateEngine 与普通 groovy 中的 StreamingTemplateEngine

问题描述

在 Jenkins 管道中,我们想要创建一个具有可变内容的配置文件,因此我们使用 StreamingTemplateEngine。现在我们必须根据变量映射构建一个带有可选行的配置文件。这个例子是我们的第一次尝试(在开发/测试期间,首先用普通的 groovy 编写):

import groovy.text.StreamingTemplateEngine

def vars=[
    "KEY2": "VAL2",
]

templateText='''
FIXKEY=FIXVAL
<%
if(KEY1) out.print "KEY1="+KEY1+"\\n";
if(KEY2) out.print "KEY2="+KEY2+"\\n";
%>
'''

def engine = new StreamingTemplateEngine()
def template=engine.createTemplate(templateText)
configContent = template.make(vars).toString()
println "CONTENT FROM TEMPLATE IS:"
println configContent;

因此,由于地图中不存在“KEY1”,我们希望生成的配置字符串为:

FIXKEY=FIXVAL
KEY2=VAL2

但是我们得到了这个例外:

    Exception in thread "main" groovy.text.TemplateExecutionException: Template execution error at line 4:
         3: <%
     --> 4: if(KEY1) out.print "KEY1="+KEY1+"\n";
         5: if(KEY2) out.print "KEY2="+KEY2+"\n";

    at main.run(main.groovy:34)
    at main.main(main.groovy)
Caused by: groovy.lang.MissingPropertyException: No such property: KEY1 for class: groovy.tmp.templates.StreamingTemplateScript1

所以我们了解到,模板中使用的每个变量都必须在映射中定义,因为这段代码有效:

import groovy.text.StreamingTemplateEngine

def vars=[
    "KEY1": "VAL1",
    "KEY2": "VAL2",
]

templateText='''
FIXKEY=FIXVAL
<%
if(KEY1) out.print "KEY1="+KEY1+"\\n";
if(KEY2) out.print "KEY2="+KEY2+"\\n";
%>
'''

def engine = new StreamingTemplateEngine()
def template=engine.createTemplate(templateText)
configContent = template.make(vars).toString()
println "CONTENT FROM TEMPLATE IS:"
println configContent;

但结果是:

FIXKEY=FIXVAL
KEY1=VAL1
KEY2=VAL2

现在我们可以在地图中定义 "KEY2":false ,但是对于大量的变量,这将是更多的工作,因为只是定义必要的变量并完全排除不需要的变量。

经过一番搜索,我们发现了这个:

StreamingTemplateEngine 异常 MissingPropertyException

我们尝试了此线程中提到的第二种解决方案:

import groovy.text.StreamingTemplateEngine

def vars=[
    "KEY2": "VAL2",
].withDefault { false }

templateText='''
FIXKEY=FIXVAL
<%
if(KEY1) out.print "KEY1="+KEY1+"\\n";
if(KEY2) out.print "KEY2="+KEY2+"\\n";
%>
'''

def engine = new StreamingTemplateEngine()
def template=engine.createTemplate(templateText)
configContent = template.make(vars).toString()
println "CONTENT FROM TEMPLATE IS:"
println configContent;

并且按预期工作,生成的配置内容是:

FIXKEY=FIXVAL
KEY2=VAL2

完美,我们想,所以现在我们想在 Jenkins 流水线中使用这个片段,但是 Jenkins 在使用这个测试阶段代码时表现得有些不同:

import groovy.text.StreamingTemplateEngine

[...]

           stage('test') {
                def vars=[
                    "KEY2": "VAL2",
                ].withDefault { false }

                templateText='''
    FIXKEY=FIXVAL
    <%
    if(KEY1) out.print "KEY1="+KEY1+"\\n";
    if(KEY2) out.print "KEY2="+KEY2+"\\n";
    %>
    '''
                def engine = new StreamingTemplateEngine()
                def template=engine.createTemplate(templateText)
                configContent = template.make(vars).toString()
                println "CONTENT FROM TEMPLATE IS:"
                println configContent;
            }

但是詹金斯的结果是这样的:

[Pipeline] {
[Pipeline] stage
[Pipeline] { (test)
[Pipeline] echo
15:07:10  CONTENT FROM TEMPLATE IS:
[Pipeline] echo
15:07:10  false
[Pipeline] }

注意单个“假”??!!

当像这样“完成”地图时:

import groovy.text.StreamingTemplateEngine

[...]

           stage('test') {
                def vars=[
                    "KEY1": "VAL1",
                    "KEY2": "VAL2",
                ].withDefault { false }

                templateText='''
    FIXKEY=FIXVAL
    <%
    if(KEY1) out.print "KEY1="+KEY1+"\\n";
    if(KEY2) out.print "KEY2="+KEY2+"\\n";
    %>
    '''
                def engine = new StreamingTemplateEngine()
                def template=engine.createTemplate(templateText)
                configContent = template.make(vars).toString()
                println "CONTENT FROM TEMPLATE IS:"
                println configContent;
            }

内容字符串符合预期:

[Pipeline] stage
[Pipeline] { (test)
[Pipeline] echo
15:09:06  CONTENT FROM TEMPLATE IS:
[Pipeline] echo
15:09:06  
15:09:06  FIXKEY=FIXVAL
15:09:06  KEY1=VAL1
15:09:06  KEY2=VAL2
15:09:06  
15:09:06  
[Pipeline] }

那么,为什么 Jenkins Pipeline Groovy 使用相同的代码片段时的行为与“普通”Groovy 不同呢?

或者甚至有完全不同的方法来解决“基于 var 的可变行 - 地图中存在”的请求?

感谢任何提示!
T0mcat

标签: jenkinsgroovyjenkins-pipelinejenkins-groovy

解决方案


您面临的问题的根本原因是以下表达式:

def vars=[
    "KEY1": "VAL1",
    "KEY2": "VAL2",
].withDefault { false }

返回一个类的实例MapWithDefault<K,V>。此对象在 Jenkins 管道内产生问题,因为该管道使用 Groovy CPS 库进行连续传递样式转换。这种模式有一些限制。例如,它要求您在管道中使用的所有对象都必须是Serializable.

管道脚本可以用注解标记指定的方法@NonCPS。然后它们会被正常编译(除了沙盒安全检查),因此其行为很像来自 Java 平台、Groovy 运行时或 Jenkins 核心或插件代码的“二进制”方法。@NonCPS方法可以安全地使用非Serializable对象作为局部变量,尽管它们不应接受不可序列化的参数或返回或存储不可序列化的值。您不能从@NonCPS方法调用常规(CPS 转换)方法或流水线步骤,因此它们最好用于在将摘要传递回主脚本之前执行一些计算。特别注意@Overrides二进制类中定义的方法,例如 Object.toString(),通常应该被标记@NonCPS因为它通常是调用它们的二进制代码。

来源:https ://github.com/jenkinsci/workflow-cps-plugin#technical-design

对于 Groovy 类,这个要求是开箱即用的,因为每个 Groovy 类都隐式地实现了Serializable接口。在 Java 类的情况下,这个接口必须显式地实现。正如你所看到的,这个MapWithDefault<K,V>类是一个 Java 类,它没有实现Serializable接口。

解决方案1:将逻辑提取到@NonCPS方法中

考虑以下示例:

import groovy.text.StreamingTemplateEngine

node {

   stage('test') {
        def vars=[
            "KEY2": "VAL2",
        ]

        String templateText='''
        FIXKEY=FIXVAL
        <%
        if(KEY1) out.print "KEY1="+KEY1+"\\n";
        if(KEY2) out.print "KEY2="+KEY2+"\\n";
        %>
    '''

        configContent = parseAsConfigString(templateText, vars)
        println "CONTENT FROM TEMPLATE IS:"
        println configContent;
    }
}

@NonCPS
def parseAsConfigString(String templateText, Map vars) {
    def engine = new StreamingTemplateEngine()
    def template=engine.createTemplate(templateText)
    return template.make(vars.withDefault { false }).toString()
}

在这种情况下,一个方法parseAsConfigString处理配置字符串的生成。请记住,它接受普通的哈希映射(可序列化)并将其转换到方法MapWithDefault内部@NonCPS,因此不可序列化对象不会在@NonCPS方法的上下文之外使用。该StreamingTemplateEngine对象也在方法内部使用,因为这个类没有实现Serializable接口,所以它也会导致一些奇怪的问题。

解决方案2:ConfigObject改用

即使带有模板引擎的解决方案可能对您有用,我还是建议您ConfigObject改用。这个类是为表示配置对象而设计的,它有一些有用的方法。您可以从任何地图创建一个实例ConfigObject,然后您可以调用prettyPrint()方法来生成配置的字符串表示。考虑以下示例:

node {
   stage('test') {
       def map = [
                KEYVAL1: "VAL2",
                FIXKEY: "FIXVAL"
        ]

        def config = new ConfigObject()
        config.putAll(map)

        println config.prettyPrint()
   }
}

输出:

[Pipeline] stage
[Pipeline] { (test)
[Pipeline] echo
KEYVAL1='VAL2'
FIXKEY='FIXVAL'

[Pipeline] }
[Pipeline] // stage
[Pipeline] }

这两种方法的主要区别在于,a 的漂亮打印中的字符串ConfigObject用单引号括起来,这是我真正期望的。在这种方法中,您所要做的就是准备一个适当的地图来存储配置选项并将其转换为ConfigObject允许您以所需的形式打印它。


推荐阅读