StreamingTemplateEngine в конвейере Jenkins и в простом исполнении - PullRequest
2 голосов
/ 12 марта 2019

В конвейере Jenkins мы хотим создать файл конфигурации с переменным содержимым, поэтому мы используем StreamingTemplateEngine. Теперь нам нужно создать файл конфигурации с необязательными строками в зависимости от карты переменных. Этот пример был нашей первой попыткой (во время разработки / тестирования, сначала написанной простым шрифтом):

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

1 Ответ

0 голосов
/ 12 марта 2019

Основной причиной проблемы, с которой вы сталкиваетесь, является то, что следующее выражение:

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

возвращает экземпляр класса MapWithDefault<K,V>. Этот объект создает проблему внутри конвейера Jenkins, поскольку конвейер использует библиотеку Groovy CPS для непрерывного преобразования стиля. Этот режим имеет некоторые ограничения. Например, он требует, чтобы все объекты, которые вы используете в конвейере, были Serializable.

Конвейерные сценарии могут помечать назначенные методы аннотацией @NonCPS. Затем они компилируются нормально (за исключением проверок безопасности в песочнице) и, таким образом, ведут себя подобно «двоичным» методам из платформы Java, среды исполнения Groovy или кода ядра или плагина Jenkins. Методы @NonCPS могут безопасно использовать не Serializable объекты в качестве локальных переменных, хотя они не должны принимать несериализуемые параметры или возвращать или хранить несериализуемые значения. Вы не можете вызывать обычные (преобразованные в CPS) методы или шаги конвейера из метода @NonCPS, поэтому их лучше всего использовать для выполнения некоторых вычислений перед передачей сводки в основной сценарий. В частности, обратите внимание, что @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] }

Основное различие между этими двумя подходами состоит в том, что строки в симпатичном шрифте ConfigObject заключены в одинарные кавычки, чего я и ожидал. В этом подходе все, что вам нужно сделать, это подготовить правильную карту, в которой хранятся параметры конфигурации, и преобразовав ее в ConfigObject, вы сможете распечатать ее в нужной форме.

...