1

I want to access Jenkins Credentials to authenticate with Git before the pipeline script - to fill Active Choices parameter.

In the pipeline I simply do it with withCredentials. But during pre-pipeline stage (i.e. Active Choices parameter) Hudson extensions such as withCredentials or sshAgent are not accessible. Currently I execute groovy git ls-remote -h [email protected]:repo.git with unencrypted key in jenkins_home directory (which is not a good idea).

I know that there is some possibility to import and construct Java objects by reading Jenkin's and its plugins' source code - but this seems endless.

So: is it possible to somehow easily import and apply Hudson extensions to current Groovy script or does anyone have some ready solution for this (apparently nobody published anything about such case)?

1

2 Answers 2

0

It sounds to me like you're already aware of this, but there are a couple easy ways to do this, and a couple hard ways to do this.

The easy ways:

  • Make your repo available for checkout without SSH keys or authentication credentials.
  • Manually add an SSH keypair for repo checkout to the Jenkins user on your Jenkins master node (since Active Choice Groovy scripts run on the master node) (i.e. don't use the Credentials store).

The hard ways:

  • Use the Jenkins Java API in your Active Choices code to extract credentials from the credentials store. API docs are available for the credentials store and the ssh credentials type. There is no documentation on how to use the Java classes available in these plugins, so it's up to you to figure it out yourself.
  • Create a secondary "meta" job to update the parameters in the original Jenkins job (including Active Choices scripts) - I describe this a bit below.

If you can't do any of the easy solutions for security reasons, there isn't an easy, straightforward way to update Active Choice parameters based on the contents of a git repo. This is because:

  1. Like you said, "during pre-pipeline stage (i.e. Active Choices parameter) Hudson extensions such as withCredentials or sshAgent are not accessible".
  2. The configuration for parameters, including Active Choice parameters, is taken from the previous run of that Pipeline, not the current one. Meaning that, to update your parameters from within your Pipeline code, you'll need to run your Pipeline, which defeats the point of what you want to do.

There is a solution for 1., which is to use Scripted Pipelines instead of Declarative, but that won't get around the problems of 2.

The solution that I came up with when I ran into this issue is to create a second Jenkins job (I call it a "meta" job) which updates the parameters of the original job, including Active Choices scripts. This second meta job is triggered by a git hook on the repository that I use to fill the options in my Active Choices parameters. This has the advantage that you do not need to run the original job's Pipeline in order to update its parameters, solving the issues in 2. Also, because it's still a Pipeline job, you can use normal Pipeline steps, thus allowing you to access the credentials store.

My job looks (roughly) like this - I simplified it a lot to make it fit here, so you'll need to do a bunch of tweaking to get it working the way you want. There are probably a ton of errors and bugs in this because I ripped apart an existing script to make this. A few things to note:

  • This makes heavy use of Jenkins APIs, so you'll need to either disable the script sandbox or whitelist a ton of functions in the Jenkins security manager in order to get this to run
  • This is still technically a Scripted Pipeline even though it looks much more like a normal Groovy script than a Pipeline file
  • Because this is a Pipeline, you can use steps like withCredentials to check out repositories using the credentials store.
import groovy.json.JsonOutput

import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript 

import org.biouno.unochoice.*
import org.biouno.unochoice.model.*

def metadataUrl = 'ssh://git@**************/metadata.git'
def metadataBranch = 'master'

def metadata = [:]

node('master') {
  git(
    url: metadataUrl,
    branch: metadataBranch,
    credentialsId: 'jenkins',
    changelog: false,
  )

  def dataDir = "${pwd()}/myJsonFiles"

  // can't use File.eachFileMatch() due to:
  // https://wiki.jenkins.io/display/JENKINS/Pipeline+CPS+method+mismatches
  def files = new File(dataDir).list().findAll() { f->
    FilenameUtils.getExtension(f) == 'json'
  }

  files.each() { f->
    // TODO: nice error messages if files are misformatted
    def name = FilenameUtils.removeExtension(f)
    def data = readJson(file: "${dataDir}/${f}")
    metadata[name] = data
  }
}


// ChoiceParameterDefinition is the class name for the built-in static choice parameter.
// CascadeChoiceParameter is the class name for the Active Choices dynamic choice parameter.


// begin first parameter
def fileParam = new ChoiceParameterDefinition(
  'file',
  'The JSON file to use',
)
fileParam.setChoices(metadata.keySet())
// end first parameter


// begin second parameter
def metadataJson = JsonOutput.toJson(metadata)

def scriptText = """
import groovy.json.JsonSlurper

def slurper = new JsonSlurper()
def metadata = slurper.parseText('${metadataJson}')

return metadata[file]['versions']
"""
def mainScript = new SecureGroovyScript(scriptText, true)
def fallbackScript = new SecureGroovyScript('return ["Error encountered - see logs"]', true)
def activeChoiceScript = new GroovyScript(mainScript, fallbackScript)

def versionParam = new CascadeChoiceParameter(
  'version', 
  'The version to use',
  activeChoiceScript,
  'PT_SINGLE_SELECT',
  'file',
  false,
)
// end second parameter


def paramProp = new ParametersDefinitionProperty(fileParam, versionParam)


// this is the full name of the actual job which needs dynamic parameters
def job = Jenkins.instance.getItemByFullName('UNIX/deploy_to_environment/master')


// There is no "setProperty" - we need to remove and replace.
// removeProperty is safe to run even if no matching properties are set on the job.
// However, this method only removes one property per method call.
// Theoretically, this means the job could accumulate multiple conflicting
// ParametersDefinitionProperty properties.
// TODO: run removeProperty in a while loop to eliminate all matching properties for extra safety.
job.removeProperty(ParametersDefinitionProperty)

job.addProperty(paramProp)
1
  • Amazing solution! I modified your script to match my liking and scheduled it every 15 minutes to always have fresh branches.
    – pfoof
    Commented Oct 8, 2020 at 6:21
0

I'm not using the git credential type, but we use a secret stored in the credential store as an API token, that is used to call away to an endpoint to populate a select dropdown.
I'm sure you could modify to pull the creds from a different secret. (Also I'm not a groovy/Java dev so there might be a cleaner way to do this).

This is how it looks in our pipeline script

properties(
    [
        parameters(
            [
                [
                  $class: 'ChoiceParameter',
                  choiceType: 'PT_SINGLE_SELECT',
                  name: 'Environment',
                  description: 'The environment to deploy to',
                  filterLength: 1,
                  filterable: true,
                  script: [
                    $class: 'GroovyScript',
                    fallbackScript: [
                        classpath: [],
                        sandbox: false,
                        script:
                            'return[\'Could not load envs\']'
                    ],
                    script: [
                        classpath: [],
                        sandbox: false,
                        script:
                            '''
                            #!/usr/bin/env groovy
                            import jenkins.model.*
                            import groovy.json.JsonSlurper

                            def creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
                                org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl,
                                Jenkins.instance,
                                null,
                                null
                            );

                            def apiToken = ''
                            for (c in creds){
                              if(c.getId() == '[CREDENTIAL ID FOR API TOKEN]'){
                                apiToken = c.getSecret()
                                break
                              }
                            }

                            def get = new URL("https://example.com/envs").openConnection()
                            get.setRequestProperty("Accept", "application/json")
                            get.setRequestProperty("Authorization", "Bearer "+apiToken)
                            get.setUseCaches(false)
                            get.setDoOutput(true)
                            def getRC = get.getResponseCode()
                            def response = get.getInputStream().getText()
                            def jsonSlurper = new JsonSlurper()
                            def responseObject = jsonSlurper.parseText(response)
                            return responseObject['data']
                            '''
                    ]
                  ]
                ]
            ]
        )
    ]
)

Hope it helps!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.