Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi cloud and template automation script #8

Open
willis7 opened this issue Jul 6, 2018 · 14 comments
Open

Multi cloud and template automation script #8

willis7 opened this issue Jul 6, 2018 · 14 comments
Labels

Comments

@willis7
Copy link

willis7 commented Jul 6, 2018

Hey, I recently watched your talk about using the script console. Great!

I have a use case where I need to add 4 clouds, and multiple templates. The clouds will be linked to some or all of the templates. From your scripts I can see you doing a 1-2-1 (template-2-cloud) mapping, but I envisage this isn't scaleable for my scenario.

Have you tried this? Can you steer me in the right direction?

@willis7 willis7 closed this as completed Jul 17, 2018
@samrocketman
Copy link
Owner

Hi @willis7 I meant to reply but forgot. In general, I package a binary package of a Jenkins server with plugins and publish it somewhere (GitHub releases, artifactory, Nexus, whatever you use). Then using a cloud provisioning tool I would bake images for the cloud (e.g. packer). Then I would provision the infrastructure using those images (terraform or cloudformation)

@samrocketman
Copy link
Owner

I’ll elaborate more later

@willis7
Copy link
Author

willis7 commented Jul 18, 2018

It's really great to hear you're doing infra as code, but I was referring to the Docker plugin script 😜

@samrocketman
Copy link
Owner

Okay that was not clear to me. Ca

@samrocketman samrocketman reopened this Jul 18, 2018
@samrocketman
Copy link
Owner

samrocketman commented Jul 18, 2018

Okay that was not clear to me. Can you elaborate which docker plugin script? There are two. I’ve used these scripts to configure 40 docker clouds in one go so it should be scalable for your needs. The yet-another-docker-plugin script is the one I typically use because the docker plugin was broken using the Docker v1.0 API last I used it.

@willis7
Copy link
Author

willis7 commented Jul 18, 2018

This was my attempt at scripting the provisioning of Clouds and Templates in the Docker Plugin. I have since been advised that sharing the same template object over many clouds could be dangerous. I like the use of Groovies config slurper as it gives an appearance of inheritance - and I could seperate that config from the parsing logic. It's all in the same file here.

It would be great to get your thoughts.

import com.nirima.jenkins.plugins.docker.DockerCloud
import com.nirima.jenkins.plugins.docker.DockerTemplate
import com.nirima.jenkins.plugins.docker.DockerTemplateBase
import com.nirima.jenkins.plugins.docker.launcher.AttachedDockerComputerLauncher
import io.jenkins.docker.connector.DockerComputerAttachConnector
import com.cloudbees.plugins.credentials.Credentials
import com.cloudbees.plugins.credentials.CredentialsScope
import com.cloudbees.plugins.credentials.domains.Domain
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl
import com.cloudbees.plugins.credentials.SystemCredentialsProvider

/*
   Automatically configure the docker cloud stack in Jenkins.
   Docker plugin v1.1.4

   Usage:
    For adding a new cloud or Template *only* edit rawTemplateConf and/or 
    rawCloudConf
 */


// 
// TEMPLATES
def rawTemplateConf = '''
params {
    // Common
    pullCredentialsId = 'af-docker-repo__ons-docker-registry'
    dnsString =         ''
    network =           ''
    dockerCommand =     ''
    volumesString =     ''
    volumesFromString = ''
    environmentsString =''
    hostname =          ''
    memoryLimit =       null
    memorySwap =        null
    cpuShares =         null
    bindPorts =         ''
    bindAllPorts =      false
    privileged =        false
    tty =               true
    macAddress =        ''
    extraHostsString =  ''
    instanceCapStr =    '4'
    remoteFs =          ''
}

templates {
    'jenkins-slave-base' {}
    'scala_2-11-8' {}
    'node_6-11-5' {}
    'node_9.9.0' {}
    'maven_3.2.5' {}
    'maven_3.5.4' {}
    'python_2.7.15' {}
    'python_3.3.0' {}
    'r_3.5.0-1' {}
    'sbt_0-13-13' {}
}

environments {
    'jenkins-slave-base' {
        params {
            labelString =    'jenkins.slave'
            image =          'onsdigital/jenkins-slave-base'
        }
    }
    'scala_2-11-8' {
        params {
            labelString =    'scala_2-11-8'
            image =          'onsdigital/jenkins-slave-scala:2.11.8'
        }
    }
    'node_6-11-5' {
        params {
            labelString =    'node_6-11-5'
            image =          'onsdigital/jenkins-slave-node:v6.11.5'
        }
    }
    'node_9.9.0' {
        params {
            labelString =    'node_9.9.0'
            image =          'onsdigital/jenkins-slave-node:v9.9.0'
        }
    }
    'maven_3.2.5' {
        params {
            labelString =    'maven_3.2.5'
            image =          'onsdigital/jenkins-slave-maven:3.2.5'
        }
    }
    'maven_3.5.4' {
        params {
            labelString =    'maven_3.5.4'
            image =          'onsdigital/jenkins-slave-maven:3.5.4'
        }
    }
    'python_2.7.15' {
        params {
            labelString =    'python_2.7.15'
            image =          'onsdigital/jenkins-slave-python:2.7.15'
        }
    }
    'python_3.3.0' {
        params {
            labelString =    'python_3.3.0'
            image =          'onsdigital/jenkins-slave-python:3.3.0'
        }
    }
    'r_3.5.0-1' {
        params {
            labelString =    'r_3.5.0-1'
            image =          'onsdigital/jenkins-slave-r:3.5.0-1'
        }
    }
    'sbt_0-13-13' {
        params {
            labelString =    'sbt_0-13-13'
            image =          'onsdigital/jenkins-slave-sbt:0.13.13'
        }
    }
}
'''

// 
// CLOUDS
def rawCloudConf = '''
params{
    containerCapStr =     '10'
    connectTimeout =      5
    credentialsId =      ''
    dockerHostname =      ''
    readTimeout =         15
    version =             ''
}

clouds {
    download {}
    build {}
    test {}
    deploy{}
}

environments {
    download{
        params{
            name =                'download'
            serverUrl =           'tcp://10.50.34.32:4243'
            templates =           ['jenkins-slave-base']
        }
    }
    build{
        params{
            name =                'build'
            serverUrl =           'tcp://10.50.34.39:4243'
            templates =           ['jenkins-slave-base',
                                    'scala_2-11-8',
                                    'node_6-11-5',
                                    'node_9.9.0',
                                    'maven_3.2.5',
                                    'maven_3.5.4',
                                    'python_2.7.15',
                                    'python_3.3.0',
                                    'r_3.5.0-1',
                                    'sbt_0-13-13']
        }
    }
    test{
        params{
            name =                'test'
            serverUrl =           'tcp://10.50.34.54:4243'
            templates =           ['jenkins-slave-base',
                                    'scala_2-11-8',
                                    'node_6-11-5',
                                    'node_9.9.0',
                                    'maven_3.2.5',
                                    'maven_3.5.4',
                                    'python_2.7.15',
                                    'python_3.3.0',
                                    'r_3.5.0-1',
                                    'sbt_0-13-13']
        }  
    }
    deploy{
        params{
            name =                'deploy'
            serverUrl =           'tcp://10.50.34.69:4243'
            templates =           ['jenkins-slave-base']            
        }
    }
}
'''

//configure credentials
SystemCredentialsProvider system_creds = SystemCredentialsProvider.getInstance()
Boolean foundId=false
system_creds.getCredentials().each{
    if('af-docker-repo__ons-docker-registry'.equals(it.getId())) {
        foundId=true
    }
}

//                +--------------------+
//                |                    |
//                | DockerTemplateBase |
//                |                    |
//                +--------------------+
//                          |
//                          |
//                  +-------v--------+
//                  |                |
//                  | DockerTemplate |
//                  |                |
//                  +----------------+
//                          |
//                          |
//                   +------v------+
//                   |             |
//                   | DockerCloud |
//                   |             |
//                   +-------------+
// 
// Docker Plugin API Class Diagram

// Constructor
def newDockerTemplateBase(Map templateParams) {
    // https://github.com/jenkinsci/docker-plugin/blob/docker-plugin-1.1.2/src/main/java/com/nirima/jenkins/plugins/docker/DockerTemplateBase.java
    new DockerTemplateBase(
        templateParams.image,
        templateParams.pullCredentialsId,
        templateParams.dnsString,
        templateParams.network,
        templateParams.dockerCommand,
        templateParams.volumesString,
        templateParams.volumesFromString,
        templateParams.environmentsString,
        templateParams.hostname,
        templateParams.memoryLimit,
        templateParams.memorySwap,
        templateParams.cpuShares,
        templateParams.bindPorts,
        templateParams.bindAllPorts,
        templateParams.privileged,
        templateParams.tty,
        templateParams.macAddress,
        templateParams.extraHostsString
    )
}

// Constructor
def newDockerTemplate(DockerTemplateBase dockerTemplateBase, Map templateParams) {
    // https://github.com/jenkinsci/docker-plugin/blob/docker-plugin-1.1.2/src/main/java/com/nirima/jenkins/plugins/docker/DockerTemplate.java
    new DockerTemplate(
        dockerTemplateBase,
        new DockerComputerAttachConnector(),
        templateParams.labelString,
        templateParams.remoteFs,
        templateParams.instanceCapStr
    )
}

// Constructor
def newDockerCloud(def cloudParams, def templates) {
    // https://github.com/jenkinsci/docker-plugin/blob/docker-plugin-1.1.2/src/main/java/com/nirima/jenkins/plugins/docker/DockerCloud.java
    new DockerCloud(
        cloudParams.name,
        templates,
        cloudParams.serverUrl,
        cloudParams.containerCapStr,
        cloudParams.connectTimeout,
        cloudParams.readTimeout,
        cloudParams.credentialsId,
        cloudParams.version,
        cloudParams.dockerHostname
    )
}

// buildTemplates returns a Map whose key matches the image name
// and the value is a DockerTemplate object. The function first creates the DockerTemplateBase
// object and uses that to construct the DockerTemplate. See class diagram above.
def buildTemplates(def templateConfig) {
    def config = new ConfigSlurper().parse(templateConfig)
    def templates = [:]

    config.templates.each { name, unused ->
        namedTemplateConf = new ConfigSlurper(name).parse(templateConfig)
        dockerTemplateBase = newDockerTemplateBase(namedTemplateConf.params)
        dockerTemplate = newDockerTemplate(dockerTemplateBase, namedTemplateConf.params)
        templates.put(name, dockerTemplate)
    }
    return templates
}

// buildClouds returns a List of DockerCloud objects with wiring to 
// the templates which should linked to this cloud.
def buildClouds(def cloudConfig, Map templates) {
    def config = new ConfigSlurper().parse(cloudConfig)
    def clouds = []
    
    config.clouds.each { name, unused ->
        namedCloudConf = new ConfigSlurper(name).parse(cloudConfig)
        def attachTmpl = []

        namedCloudConf.params.templates.each {
            attachTmpl << templates.get(it)
        }
        clouds << newDockerCloud(namedCloudConf.params, attachTmpl)
    }
    return clouds
}

// buildAll creates all the API objects from the raw config
// and returns a list of clouds
def buildAll(def templateConfig, def cloudConfig) {
    templates = buildTemplates(templateConfig)
    clouds = buildClouds(cloudConfig, templates)
    return clouds
}

Jenkins jenkins = Jenkins.getInstance()

// remove existing clouds
jenkins.clouds.removeAll(DockerCloud.class)

// install new clouds
jenkins.clouds.addAll(buildAll(rawTemplateConf, rawCloudConf))

// save current Jenkins state to disk
jenkins.save()

println "If there are no error messages above ^^^  then I guess I ran successfully...."

@samrocketman
Copy link
Owner

samrocketman commented Jul 18, 2018

I wouldn’t recommend sharing the same template base across many clouds. In general, that’s not how configuring via the web ui would instantiate objects. Jenkins would use a new instance per cloud. Anything other than that may have unintended side effects or bugs since the runtime doesn’t normally encounter that from web ui setup.

Have you seen my docker-plugin script which includes an example JSON config?
https://github.com/samrocketman/jenkins-bootstrap-shared/blob/master/scripts/configure-docker-cloud.groovy#L40-L93

It includes sane defaults when settings are omitted from the JSON map.

@willis7
Copy link
Author

willis7 commented Jul 18, 2018

I had seen it, but I couldnt see how I could avoid scaling hell (extra lines of code for almost identical templates) which led to my solution. With hindsight, I would have just used your script, as the dangers in my script make it too risky to use.

@samrocketman
Copy link
Owner

In my script you can templates the JSON object. The JSON object is read for values and not directly creating anything. So you can templatize as many settings as you want. The important part is that there are new instances of the docker classes.

@samrocketman
Copy link
Owner

samrocketman commented Jul 18, 2018

Also note, that my config includes every setting but does not have to. Take for example this line

obj.optBoolean('bind_all_declared_ports', false),

If you don’t specify the setting at all in the JSONObject, then it will default to false. Learn more about net.sf.json.JSONObject

http://json-lib.sourceforge.net/apidocs/net/sf/json/JSONObject.html#optBoolean(java.lang.String,%20boolean)

So you can customize as many defaults as you want in that script to limit your need to templatize. And then rely on JSONObject templates when you do need it. net.sf.json.JSONObject is a class built into Jenkins core.

@willis7
Copy link
Author

willis7 commented Jul 19, 2018

I have the same behavior in my script. Anything in common can be overwritten per template. That works really well.

I had tried your script as I mentioned before but was blocked by version compatibility - I was using a newer version of Docker Plugin and the API had changed.

My question was more; can I have the same template conf (albeit not the same instance of DockerTemplate) per cloud without duplication, and I believe the answer is no.

@samrocketman
Copy link
Owner

samrocketman commented Jul 22, 2018

I don’t see why you couldn’t have the same template conf. As long as you’re not sharing docker class instances it should be fine.

@samrocketman
Copy link
Owner

I might try to update that script this week. I don’t normally use that plugin because for a time it was abandoned and didn’t work for the latest docker API. Times have changed I guess.

@willis7
Copy link
Author

willis7 commented Jul 23, 2018

I would really find that useful and would happily reuse.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants