Personal Programming Notes

To err is human; to debug, divine.

Groovy Hook Script and Jenkins Configuration as Code

This post discusses Groovy Hook Scripts and how to use them for full configuration-as-code in Jenkins with Docker, Pipeline. This can help us to set up local environment for developing Jenkins Pipeline libraries and to evaluate various Jenkins features.

Groovy Hook Scripts

These scripts are written in Groovy, and get executed inside the same JVM as Jenkins, allowing full access to the domain model of Jenkins. For a given hook HOOK, the following locations are searched:

1
2
3
4
WEB-INF/HOOK.groovy in jenkins.war
WEB-INF/HOOK.groovy.d/*.groovy in the lexical order in jenkins.war
$JENKINS_HOME/HOOK.groovy
$JENKINS_HOME/HOOK.groovy.d/*.groovy in the lexical order

The init is the most commonly used hook (i.e., HOOK=init). The following sections show how some of the most common tasks and configurations in Jenkins can be achieved by using such Groovy scripts. For example, in this project, many of such scripts are added into a Dockerized Jenkins master and executed when starting a container to replicate configurations of the Jenkins instance in production. It will give us ability to quickly spin up local Jenkins instances for development or troubleshooting issues in production Jenkins.

On a side note, IntelliJ IDEA is probably the best development tool for working with these Groovy Scripts. Check out these instructions on how to set it up in IntelliJ. UPDATED ON 2018/09/29: More on IntelliJ setup is discussed in this blog post.

Authorization

This section shows how to enable different authorization strategies in Groovy code.

"Logged-in users can do anything"
1
2
3
4
5
6
7
8
9
10
11
12
import jenkins.model.*
def instance = Jenkins.getInstance()

import hudson.security.*
def realm = new HudsonPrivateSecurityRealm(false)
instance.setSecurityRealm(realm)

def strategy = new hudson.security.FullControlOnceLoggedInAuthorizationStrategy()
strategy.setAllowAnonymousRead(false)
instance.setAuthorizationStrategy(strategy)

instance.save()

Matrix-based authorization: Gives all authenticated users admin access:

Matrix-based authorization
1
2
3
4
5
6
7
8
9
10
11
12
import jenkins.model.*
def instance = Jenkins.getInstance()

import hudson.security.*
def realm = new HudsonPrivateSecurityRealm(false)
instance.setSecurityRealm(realm)

def strategy = new hudson.security.GlobalMatrixAuthorizationStrategy()
strategy.add(Jenkins.ADMINISTER, 'authenticated')
instance.setAuthorizationStrategy(strategy)

instance.save()

For importing GlobalMatrixAuthorizationStrategy class, make sure that matrix-auth plugin is installed. For full list of standard permissions in the matrix, see this code snippet. Note that the matrix can be different if different plugins are installed. For example, the “Replay” permission for Runs is not simply hudson.model.Run.REPLAY since there is no such static constant. Such permission is only available after Workflow CPS plugin is installed. Therefore, we can only set “Replay” permission for Runs with the following:

1
strategy.add(org.jenkinsci.plugins.workflow.cps.replay.ReplayAction.REPLAY,USER)

References

Basic Jenkins security

In addition to enable authorization strategy, we should also set some basic configurations for hardening Jenkins. Those includes various options that you see in Jenkins UI when going to Manage Jenkins > Configure Global Security.

  • Disable Jenkins CLI
  • Limit Jenkins agent protocols.
  • “Enable Slave -> Master Access Control”
  • “Prevent Cross Site Request Forgery exploits”
Basic Jenkins security
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import hudson.security.csrf.DefaultCrumbIssuer
import jenkins.model.Jenkins
import jenkins.model.JenkinsLocationConfiguration
import jenkins.security.s2m.AdminWhitelistRule
import org.kohsuke.stapler.StaplerProxy
import hudson.tasks.Mailer

println("--- Configuring Remoting (JNLP4 only, no Remoting CLI)")
Jenkins.instance.getDescriptor("jenkins.CLI").get().setEnabled(false)
Jenkins.instance.agentProtocols = new HashSet<String>(["JNLP4-connect"])

println("--- Enable Slave -> Master Access Control")
Jenkins.instance.getExtensionList(StaplerProxy.class)
    .get(AdminWhitelistRule.class)
    .masterKillSwitch = false

println("--- Checking the CSRF protection")
if (Jenkins.instance.crumbIssuer == null) {
    println "CSRF protection is disabled, Enabling the default Crumb Issuer"
    Jenkins.instance.crumbIssuer = new DefaultCrumbIssuer(true)
}

println("--- Configuring Quiet Period")
// We do not wait for anything
Jenkins.instance.quietPeriod = 0
Jenkins.instance.save()

println("--- Configuring Email global settings")
JenkinsLocationConfiguration.get().adminAddress = "admin@non.existent.email"
Mailer.descriptor().defaultSuffix = "@non.existent.email"

Some are not working for versions before 2.46, according to this. For disabling Jenkins CLI, you can simply add the java argument -Djenkins.CLI.disabled=true on Jenkins startup.

References

Create Jobs and Items

Create "Pipeline script from SCM" job
1
2
3
4
5
6
7
8
9
10
import hudson.plugins.git.*;

def scm = new GitSCM("git@github.com:dermeister0/Tests.git")
scm.branches = [new BranchSpec("*/develop")];

def flowDefinition = new org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition(scm, "Jenkinsfile")

def parent = Jenkins.instance
def job = new org.jenkinsci.plugins.workflow.job.WorkflowJob(parent, "New Job")
job.definition = flowDefinition

Create different kinds of Credentials

Adding Credentials to a new, local Jenkins for development or troubleshooting can be a daunting task. However, with the following scripts and the right setup (NEVER commit your secrets into VCS), developers can automate adding the required Credentials into the new Jenkins.

Preamble
1
2
3
4
5
6
7
8
9
10
11
12
13
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl
import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl
import org.jenkinsci.plugins.plaincredentials.impl.FileCredentialsImpl
import com.cloudbees.plugins.credentials.domains.Domain
import com.cloudbees.plugins.credentials.CredentialsScope
import jenkins.model.Jenkins
import hudson.util.Secret
import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey
import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl
import com.cloudbees.plugins.credentials.SecretBytes

def domain = Domain.global()
def store = Jenkins.instance.getExtensionList('com.cloudbees.plugins.credentials.SystemCredentialsProvider')[0].getStore()
"Username with Password" type
1
2
3
4
5
6
def githubAccount = new UsernamePasswordCredentialsImpl(
        CredentialsScope.GLOBAL, "test-github", "Test Github Account",
        "testuser",
        "testpassword"
)
store.addCredentials(domain, githubAccount)
"Secret text" type
1
2
3
4
5
def secretString = new StringCredentialsImpl(
        CredentialsScope.GLOBAL, "test-secret-string", "Test Secret String",
        Secret.fromString("testpassword")
)
store.addCredentials(domain, secretString)
"Secret file" type
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Text file
def secret = '''Hi,
This is the content of the file.
'''

def secretBytes = SecretBytes.fromBytes(secret.getBytes())
def secretFile = new FileCredentialsImpl(
  CredentialsScope.GLOBAL,
  'text-secret-file',
  'description',
  'file.txt',
  secretBytes)
store.addCredentials(domain, secretFile)

// Binary file
Path fileLocation = Paths.get("/path/to/some/file.tar");
def secretBytes = SecretBytes.fromBytes(Files.readAllBytes(fileLocation))
def secretFile = new FileCredentialsImpl(
  CredentialsScope.GLOBAL,
  'binary-secret-file',
  'description',
  'file.tar',
  secretBytes)
store.addCredentials(domain, secretFile)
"SSH Username with private key" type
1
2
3
4
5
6
7
8
9
10
String keyfile = "/var/jenkins_home/.ssh/id_rsa"
def privateKey = new BasicSSHUserPrivateKey(
        CredentialsScope.GLOBAL,
        "jenkins_ssh_key",
        "git",
        new BasicSSHUserPrivateKey.FileOnMasterPrivateKeySource(keyfile),
        "",
        ""
)
store.addCredentials(domain, privateKey)
"Certificate" type
1
2
3
4
5
6
7
8
String minikubeKeyfile = "/var/jenkins_home/secret_data/minikube.pfx"
def minikubeCreds = new CertificateCredentialsImpl(
        CredentialsScope.GLOBAL,
        "minikube",
        "Minikube client certificate",
        "secret",
        new CertificateCredentialsImpl.FileOnMasterKeyStoreSource(minikubeKeyfile))
store.addCredentials(domain, minikubeCreds)

Notifications

Configure Slack
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import jenkins.model.*
def instance = Jenkins.getInstance()

// configure slack
def slack = Jenkins.instance.getExtensionList(
  jenkins.plugins.slack.SlackNotifier.DescriptorImpl.class
)[0]
def params = [
  slackTeamDomain: "domain",
  slackToken: "token",
  slackRoom: "",
  slackBuildServerUrl: "$JENKINS_URL",
  slackSendAs: ""
]
def req = [
  getParameter: { name -> params[name] }
] as org.kohsuke.stapler.StaplerRequest
slack.configure(req, null)
slack.save()
Global email settings
1
2
3
4
5
6
import jenkins.model.*
def instance = Jenkins.getInstance()

// set email
def location_config = JenkinsLocationConfiguration.get()
location_config.setAdminAddress("jenkins@skynet.net")