Using a proxy server with groovy script in Jenkins

Posted on February 9, 2022 by Adrian Wyssmann ‐ 4 min read

Sitting behind a corporate proxy may give you some additional security but it's often challenging to get things done. I really had some troubles to get the groovy script in jenkins to use the proxy server.

Problem

We currently still use Jenkins for Continuous Delivery. This is usually fine as most stuff is internally. However with the adoption of Azure, things get slightly more difficult. Mainly cause connections to Azure from Jenkins have to be routed via the the corporate proxy. Our Jenkins slave in question is running on Windows where we have configured the proxy as part of the operating system. This seems to work well, even in Jenkins.

Looking at our code, which runs azcopy to deploy static web pages to a CDN looks as follows:

def String call(String currentNode, Map<String, String> deploymentParams, Map<String, Object> applicationInfo) {
    artifacts.downloadArtifact(deploymentParams.artifactUrl)

    String environment = deploymentParams.env.toLowerCase()
    String deployCommand = ""
    source = "./"
    withCredentials([string(credentialsId: "${environment}.cwa.${applicationInfo.application}.token", variable: 'TOKEN')]) {
        String target = "https://mytarget.blob.core.windows.net/%24web${env.TOKEN}"
        target = target.replaceAll('&', '^&')
        target = target.replaceAll('%', '%%')
        deployCommand = "azcopy sync $source $target --recursive --delete-destination=true --mirror-mode=true --exclude-pattern=\"*.zip;*.tar.gz\""
        this.executeCommand(deployCommand, deploymentParams.dryRun)
        this.purgeAzureCache(environment, applicationInfo.application)
    }
    return "SUCCESS"
}

def executeCommand(String deployCommand, boolean isDryRun) {
    if (isDryRun) {
        echo deployCommand
    } else {
        status = 0
        status = runCommand(deployCommand, false)
        if (status != 0) {
            error("Build failed, see log above")
        }
    }
}

The runCommand will execute the command on the available shell, in case of Windows, on a cmd-shell. The route to the target goes over the proxy, which works fine. Probably cause it uses the proxy settings from Windows, as it does when you execute the command manually on a cmd-shell.

As an extension to the above, we also need to regurarily purge the cache, as otherwise the deployment is very slow. So we extended the groovy code to do that:

def purgeAzureCache(String environment, String application){
    def subscriptions = [dev: 'xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx',
                         test: 'xxxxxxxxx-xxxx-xxxx-xxxx-yyyyyyyyyy',
                         prd: 'xxxxxxxxx-xxxx-xxxx-xxxx-zzzzzzzzzz']

    def baseUrl = "https://management.azure.com/subscriptions/${subscriptions[environment]}/resourceGroups/rg-cwa${environment}-euw-services/providers/Microsoft.Cdn/profiles/cwa${environment}/endpoints/cwa${environment}.azureedge.net/purge?api-version=2019-12-31"
    println("[INFO] purging cdn content on $baseUrl")

    withCredentials([string(credentialsId: "${environment}.cwa.${application}.token", variable: 'TOKEN')]) {    
        def post = new URL(baseUrl).openConnection();
        post.setRequestMethod("POST")

        def postRC = post.getResponseCode()
        restResponse = post.getInputStream().getText()
        println("[INFO] azurePurge returned code $postRC")
        if (postRC.equals(200)) {
            return restResponse
        }
    }
    throw new AbortException("Error while doing azurePurge:\n$restResponse")
}

However when executing the code, the proxy settings seems to be ignored:

13:52:51  [INFO] runCommand returned 0
[Pipeline] echo
13:52:51  [INFO] purging cdn content on https://management.azure.com/subscriptions/xxxxx/resourceGroups/rg-example-services/providers/Microsoft.Cdn/profiles/example/endpoints/example.azureedge.net/purge?api-version=2019-12-31
[Pipeline] withCredentials
13:52:51  Masking supported pattern matches of %TOKEN%
[Pipeline] {
[Pipeline] }
[Pipeline] // withCredentials
[Pipeline] }
[Pipeline] // withCredentials
[Pipeline] echo
13:53:12  [ERROR] Error was: Connection timed out: connect

Using jenkins arguments

I though, maybe one has to also pass the proxy settings as part of the jenkins setup. Hence I modified jenkins-slave.xml and pass the

<arguments>-Xrs -Dhttp.proxyHost="http://myproxy.intra" -Dhttp.proxyPort=8080 -Dhttp.nonProxyHosts="localhost|*.intra" ... </arguments>

Still, I have the same result:

13:53:12  [ERROR] Error was: Connection timed out: connect

Using System.getProperties()

After searching, I found this, which recommends to use System.getProperties():

System.getProperties().put("proxySet", "true");
System.getProperties().put("proxyHost", "some.proxyserver.com");
System.getProperties().put("proxyPort", "8080");

Unfortunately this made things even worse as it looks like the settings are applied to the Jenkins master, cause after running the pipeline once, subsequent runs did not even start, as checkout was not possible:

ERROR: [Error] [Wed Feb 09 07:36:18 CET 2022] Giving up, deployment aborted
[Bitbucket] Notifying commit build result
ERROR: Could not send notifications
com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRequestException: HTTP request error. Status: 403: AuthorizedOnly.
HttpResponseProxy{HTTP/1.1 403 AuthorizedOnly [Via: 1.1 x.x.x.x, Connection: Keep-Alive, Content-Type: text/html, Cache-Control: no-cache, Content-Length: 3054, X-Frame-Options: deny] ResponseEntityProxy{[Content-Type: text/html,Content-Length: 3054,Chunked: false]}}
...

Which makes sense, cause I did not provide any nonProxyHosts, so everything is routed trough the proxy - well Bitbucket is internal, so this will not work trough the proxy. However, we don’t want the proxy settings changed on master but only on the slave. So let’s revert the changes, you can - within Jenkins - open a console on the built-in host, i.e. https://cd.intra/jenkins/computer/(built-in)/script and run

System.getProperties().put("proxyHost", "");
System.getProperties().put("proxyPort", "");

Groovy Proxy Object

Further searching brought me to Micro Focus Community, which recommends to us Proxy Object

Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("10.0.0.1", 8080));
conn = new URL(urlString).openConnection(proxy);

This seems to work as expected, so my final code looks like this:

withCredentials([string(credentialsId: "${environment}.cwa.${application}.token", variable: 'TOKEN')]) {    
    Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("myproxy.intra", 8080));
    def post = new URL(baseUrl).openConnection(proxy);
    post.setRequestMethod("POST")

    def postRC = post.getResponseCode()
    restResponse = post.getInputStream().getText()
    println("[INFO] azurePurge returned code $postRC")
    if (postRC.equals(200)) {
        return restResponse
    }
}