Search code examples
jsonjenkinsgroovyyamlstring-interpolation

Groovy Yaml string interpolation with a list of maps


I am building a yaml string within Jenkins scripted pipeline that is parametrized. The problem is that I get error interpolating string with a variable that is a list of maps. I don't think I can use external libraries in the jenkins pipeline. There are some Jenkins plugins that are installed. Not sure which libs are available to serialize these objects properly into json or yaml. FWIW I am very inexperienced in groovy. I usually write Python.

def yaml, volumeMounts

volumeMounts = [
    ['mountPath': '/build/toolchain',      'name': 'volume-0'],
    ['mountPath': '/build/apps',           'name': 'volume-1'],
    ['mountPath': '/horizonci/automation', 'name': 'volume-2'],
    ['mountPath': '/home/jenkins',         'name': 'workspace-volume']
]

volumes = [
    ['name': 'volume-0', 'nfs': ['path': '/toolchain',           'readOnly': true, 'server': 'tools.company.com']],
    ['name': 'volume-1', 'nfs': ['path': '/apps',                'readOnly': true, 'server': 'tools.company.com']],
    ['name': 'volume-2', 'nfs': ['path': '/ifs/standard/devops', 'readOnly': true, 'server': 'my.server.company.com']]
]

yaml = """
kind: Pod
spec:
  containers:
  - volumeMounts: ${volumeMounts}
  volumes: ${volumesYml.toString()}
"""

println(yaml)

ERROR:

com.fasterxml.jackson.databind.exc.MismatchedInputException:
Cannot deserialize value of type `io.fabric8.kubernetes.api.model.VolumeMount`
from Array value (token `JsonToken.START_ARRAY`)
   at [Source: (String)"{"apiVersion":"v1","kind":"Pod","spec":{"containers":[{"name":"app","image":"jenkins/agent:latest","volumeMounts":[["mountPath:/build/toolchain","name:volume-0"],
        ["mountPath:/build/apps","name:vol"[truncated 1028 chars]; line: 1, column: 420] (through reference chain: io.fabric8.kubernetes.api.model.Pod["spec"]->io.fabric8.kubernetes.api.model.PodSpec["containers"]->java.util.ArrayList[0]->io.fabric8.kubernetes.api.model.Container["volumeMounts"]->java.util.ArrayList[0])

      at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:59)
      at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1601)
      at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:324)
      at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:187)
      at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:322)
      at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4593)
      at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3548)
      at io.fabric8.kubernetes.client.utils.Serialization.unmarshalJsonStr(Serialization.java:311)
      at io.fabric8.kubernetes.client.utils.Serialization.unmarshalYaml(Serialization.java:306)
      at io.fabric8.kubernetes.client.utils.Serialization.unmarshal(Serialization.java:245)
  Caused: io.fabric8.kubernetes.client.KubernetesClientException: An error has occurred.
      at io.fabric8.kubernetes.client.KubernetesClientException.launderThrowable(KubernetesClientException.java:64)
      at io.fabric8.kubernetes.client.KubernetesClientException.launderThrowable(KubernetesClientException.java:53)
      at io.fabric8.kubernetes.client.utils.Serialization.unmarshal(Serialization.java:249)
      at io.fabric8.kubernetes.client.dsl.base.OperationSupport.unmarshal(OperationSupport.java:656)
      at io.fabric8.kubernetes.client.dsl.base.BaseOperation.load(BaseOperation.java:315)
      at io.fabric8.kubernetes.client.dsl.base.BaseOperation.load(BaseOperation.java:86)
      at org.csanchez.jenkins.plugins.kubernetes.PodTemplateUtils.parseFromYaml(PodTemplateUtils.java:566)
      at org.csanchez.jenkins.plugins.kubernetes.PodTemplateUtils.validateYamlContainerNames(PodTemplateUtils.java:595)
      at org.csanchez.jenkins.plugins.kubernetes.pipeline.PodTemplateStepExecution.start(PodTemplateStepExecution.java:142)
      at org.jenkinsci.plugins.workflow.cps.DSL.invokeStep(DSL.java:319)
      at org.jenkinsci.plugins.workflow.cps.DSL.invokeMethod(DSL.java:193)
      at org.jenkinsci.plugins.workflow.cps.CpsScript.invokeMethod(CpsScript.java:122)
      at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:48)
      at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48)
      at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:113)
      at com.cloudbees.groovy.cps.sandbox.DefaultInvoker.methodCall(DefaultInvoker.java:20)
  Caused: java.lang.RuntimeException: Failed to parse yaml:

I tried using literal strings but the code looks ugly. Hard to read and maintain.


Solution

  • Solution is to convert Groovy objects to JSON and serialize JSON objects using built-in JSON library that is importable in Jenkins pipelines. Working with yaml strings was tricky and gave up on that. Here is the solution:

    import groovy.json.JsonOutput
    
    def yaml, volumeMounts
    
    volumeMounts = [
        ['mountPath': '/build/toolchain',      'name': 'volume-0'],
        ['mountPath': '/build/apps',           'name': 'volume-1'],
        ['mountPath': '/horizonci/automation', 'name': 'volume-2'],
        ['mountPath': '/home/jenkins',         'name': 'workspace-volume']
    ]
    
    volumes = [
        ['name': 'volume-0', 'nfs': ['path': '/toolchain',           'readOnly': true, 'server': 'tools.company.com']],
        ['name': 'volume-1', 'nfs': ['path': '/apps',                'readOnly': true, 'server': 'tools.company.com']],
        ['name': 'volume-2', 'nfs': ['path': '/ifs/standard/devops', 'readOnly': true, 'server': 'my.server.company.com']]
    ]
    
    yaml = """
    kind: Pod
    spec:
      containers:
      - volumeMounts: ${JsonOutput.toJson(volumeMounts)}
      volumes: ${JsonOutput.toJson(volumes)}
    """
    
    println(yaml)