From c54187a2aa73df7eda8b1545c80c98632c9016d3 Mon Sep 17 00:00:00 2001 From: Jeremy Christian Date: Thu, 21 Aug 2014 17:10:51 -0400 Subject: [PATCH] Allow build jobs to be configured and managed by puppet. Includes #163 and resolves #120. --- README.md | 27 +++++++ manifests/cli.pp | 55 ++++++++++++++- manifests/init.pp | 5 +- manifests/job.pp | 38 ++++++++++ manifests/job/absent.pp | 34 +++++++++ manifests/job/present.pp | 106 ++++++++++++++++++++++++++++ manifests/jobs.pp | 11 +++ spec/classes/jenkins_cli_spec.rb | 7 +- spec/classes/jenkins_config_spec.rb | 2 +- spec/defines/jenkins_job_spec.rb | 96 +++++++++++++++++++++++++ tests/RedHatEnterpriseServer.pp | 5 ++ tests/Ubuntu.pp | 5 ++ 12 files changed, 386 insertions(+), 5 deletions(-) create mode 100644 manifests/job.pp create mode 100644 manifests/job/absent.pp create mode 100644 manifests/job/present.pp create mode 100644 manifests/jobs.pp create mode 100644 spec/defines/jenkins_job_spec.rb diff --git a/README.md b/README.md index 62df873c5..abbf79a92 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,33 @@ puppet module install rtyler/jenkins ``` Then the service should be running at [http://hostname.example.com:8080/](http://hostname.example.com:8080/). +### Managing Jenkins jobs + + +Build jobs can be managed using the `jenkins::job` define + +#### Creating or updating a build job +```puppet + jenkins::job { 'test-build-job': + config => template("${templates}/test-build-job.xml.erb"), + } +``` + +#### Disabling a build job +```puppet + jenkins::job { 'test-build-job': + enabled => 0, + config => template("${templates}/test-build-job.xml.erb"), + } +``` + +#### Removing an existing build job +```puppet + jenkins::job { 'test-build-job': + ensure => 'absent', + } +``` + ### Installing Jenkins plugins diff --git a/manifests/cli.pp b/manifests/cli.pp index ca445717c..48028d867 100644 --- a/manifests/cli.pp +++ b/manifests/cli.pp @@ -8,8 +8,18 @@ fail("Use of private class ${name} by ${caller_module_name}") } - $jar = '/usr/lib/jenkins/jenkins-cli.jar' - $extract_jar = 'unzip /usr/lib/jenkins/jenkins.war WEB-INF/jenkins-cli.jar' + case $::osfamily { + 'Debian': { + $war = '/usr/share/jenkins/jenkins.war' + $jar = '/usr/share/jenkins/jenkins-cli.jar' + } + default: { + $war = '/usr/lib/jenkins/jenkins.war' + $jar = '/usr/lib/jenkins/jenkins-cli.jar' + } + } + + $extract_jar = "unzip ${war} WEB-INF/jenkins-cli.jar" $move_jar = "mv WEB-INF/jenkins-cli.jar ${jar}" $remove_dir = 'rm -rf WEB-INF' @@ -21,4 +31,45 @@ require => Package['jenkins'], } + file { $jar: + ensure => file, + require => Exec['jenkins-cli'], + } + + # Get the value of JENKINS_PORT from config_hash or default + $hash = $::jenkins::config_hash + if is_hash($hash) and has_key($hash, 'JENKINS_PORT') and + has_key($hash['JENKINS_PORT'], 'value') { + $port = $hash['JENKINS_PORT']['value'] + } else { + $port = '8080' + } + + # The jenkins cli command with required parameter(s) + $cmd = "java -jar ${jar} -s http://localhost:${port}" + + # Reload all Jenkins config from disk (only when notified) + exec { 'reload-jenkins': + command => "${cmd} reload-configuration", + tries => 10, + try_sleep => 2, + refreshonly => true, + require => [ + Service['jenkins'], + File[$jar], + ], + } + + # Do a safe restart of Jenkins (only when notified) + exec { 'safe-restart-jenkins': + command => "${cmd} safe-restart && /bin/sleep 10", + tries => 10, + try_sleep => 2, + refreshonly => true, + require => [ + Service['jenkins'], + File[$jar], + ], + } + } diff --git a/manifests/init.pp b/manifests/init.pp index 22056bb6a..c145e4304 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -76,6 +76,7 @@ $service_ensure = $jenkins::params::service_ensure, $config_hash = {}, $plugin_hash = {}, + $job_hash = {}, $configure_firewall = undef, $install_java = $jenkins::params::install_java, $proxy_host = undef, @@ -103,12 +104,14 @@ class {'jenkins::repo':} } - class {'jenkins::package': } + class { 'jenkins::package': } class { 'jenkins::config': } class { 'jenkins::plugins': } + class { 'jenkins::jobs': } + if $proxy_host and $proxy_port { class { 'jenkins::proxy': require => Package['jenkins'], diff --git a/manifests/job.pp b/manifests/job.pp new file mode 100644 index 000000000..6568b82c3 --- /dev/null +++ b/manifests/job.pp @@ -0,0 +1,38 @@ +# Define: jenkins::job +# +# This class create a new jenkins job given a name and config xml +# +# Parameters: +# +# config +# the content of the jenkins job config file (required) +# +# jobname = $title +# the name of the jenkins job +# +# enabled = true +# whether to enable the job +# +# ensure = 'present' +# choose 'absent' to ensure the job is removed +# +define jenkins::job( + $config, + $jobname = $title, + $enabled = 1, + $ensure = 'present', +){ + + if ($ensure == 'absent') { + jenkins::job::absent { $title: + jobname => $jobname, + } + } else { + jenkins::job::present { $title: + config => $config, + jobname => $jobname, + enabled => $enabled, + } + } + +} diff --git a/manifests/job/absent.pp b/manifests/job/absent.pp new file mode 100644 index 000000000..b76b1a355 --- /dev/null +++ b/manifests/job/absent.pp @@ -0,0 +1,34 @@ +# Define: jenkins::job::absent +# +# Removes a jenkins build job +# +# Parameters: +# +# config +# the content of the jenkins job config file (required) +# +# jobname = $title +# the name of the jenkins job +# +define jenkins::job::absent( + $jobname = $title, +){ + require jenkins::cli + + $tmp_config_path = "/tmp/${jobname}-config.xml" + $job_dir = "/var/lib/jenkins/jobs/${jobname}" + $config_path = "${job_dir}/config.xml" + + # Temp file to use as stdin for Jenkins CLI executable + file { $tmp_config_path: + ensure => absent, + } + + # Delete the job + exec { "jenkins delete-job ${jobname}": + command => "${jenkins::cli::cmd} delete-job ${jobname}", + logoutput => false, + onlyif => "test -f ${config_path}", + } + +} diff --git a/manifests/job/present.pp b/manifests/job/present.pp new file mode 100644 index 000000000..39e7d2920 --- /dev/null +++ b/manifests/job/present.pp @@ -0,0 +1,106 @@ +# Define: jenkins::job::present +# +# Creates or updates a jenkins build job +# +# Parameters: +# +# config +# the content of the jenkins job config file (required) +# +# jobname = $title +# the name of the jenkins job +# +# enabled = 1 +# if the job should be enabled +# +define jenkins::job::present( + $config, + $jobname = $title, + $enabled = 1, +){ + require jenkins::cli + + $jenkins_cli = $jenkins::cli::cmd + $tmp_config_path = "/tmp/${jobname}-config.xml" + $job_dir = "/var/lib/jenkins/jobs/${jobname}" + $config_path = "${job_dir}/config.xml" + + Exec { + logoutput => false, + path => '/bin:/usr/bin:/sbin:/usr/sbin', + tries => 5, + try_sleep => 5, + } + + # + # When a Jenkins job is imported via the cli, Jenkins will + # re-format the xml file based on its own internal rules. + # In order to make job management idempotent, we need to + # apply that formatting before the import, so we can do a diff + # on any pre-existing job to determine if an update is needed. + # + # Jenkins likes to change single quotes to double quotes + $a = regsubst($config, "version='1.0' encoding='UTF-8'", + 'version="1.0" encoding="UTF-8"') + # Change empty tags into self-closing tags + $b = regsubst($a, '<([a-z]+)><\/\1>', '<\1/>', 'IG') + # Change " to " since Jenkins is wierd like that + $c = regsubst($b, '"', '"', 'MG') + + # Temp file to use as stdin for Jenkins CLI executable + file { $tmp_config_path: + content => $c, + } + #file { 'normalized_content': + # content => normalize_jenkins_job($config), + #} + + anchor { "before-create-${jobname}": + require => [ + Service['jenkins'], + File[$tmp_config_path] + ], + } + + # Use Jenkins CLI to create the job + $cat_config = "cat ${tmp_config_path}" + $create_job = "${jenkins_cli} create-job ${jobname}" + exec { "jenkins create-job ${jobname}": + command => "${cat_config} | ${create_job}", + creates => [$config_path, "${job_dir}/builds"], + require => Anchor["before-create-${jobname}"], + } + + # Use Jenkins CLI to update the job if it already exists + $update_job = "${jenkins_cli} update-job ${jobname}" + exec { "jenkins update-job ${jobname}": + command => "${cat_config} | ${update_job}", + onlyif => "test -e ${config_path}", + unless => "diff -b -q ${config_path} ${tmp_config_path}", + require => Anchor["before-create-${jobname}"], + notify => Exec['reload-jenkins'], + } + + anchor { "after-create-${jobname}": + require => [ + Exec["jenkins create-job ${jobname}"], + Exec["jenkins update-job ${jobname}"], + ], + } + + # Enable or disable the job (if necessary) + if ($enabled == 1) { + exec { "jenkins enable-job ${jobname}": + command => "${jenkins_cli} enable-job ${jobname}", + onlyif => "cat ${config_path} | grep 'true'", + require => Anchor["after-create-${jobname}"], + } + } else { + exec { "jenkins disable-job ${jobname}": + command => "${jenkins_cli} disable-job ${jobname}", + onlyif => "cat ${config_path} | grep 'false'", + require => Anchor["after-create-${jobname}"], + } + } + +} diff --git a/manifests/jobs.pp b/manifests/jobs.pp new file mode 100644 index 000000000..3a6aa0d9b --- /dev/null +++ b/manifests/jobs.pp @@ -0,0 +1,11 @@ +# Class: jenkins::jobs +# +class jenkins::jobs { + + if $caller_module_name != $module_name { + fail("Use of private class ${name} by ${caller_module_name}") + } + + create_resources('jenkins::job',$::jenkins::job_hash) + +} diff --git a/spec/classes/jenkins_cli_spec.rb b/spec/classes/jenkins_cli_spec.rb index 60f9caf5d..7d40f5093 100644 --- a/spec/classes/jenkins_cli_spec.rb +++ b/spec/classes/jenkins_cli_spec.rb @@ -9,9 +9,14 @@ end context '$cli => true' do - let(:params) { { :cli => true } } + let(:params) {{ :cli => true, + :config_hash => { 'JENKINS_PORT' => { 'value' => '9000' } } + }} it { should create_class('jenkins::cli') } it { should contain_exec('jenkins-cli') } + it { should contain_exec('reload-jenkins').with_command(/http:\/\/localhost:9000/) } + it { should contain_exec('safe-restart-jenkins') } + it { should contain_jenkins__sysconfig('JENKINS_PORT').with_value('9000') } end end diff --git a/spec/classes/jenkins_config_spec.rb b/spec/classes/jenkins_config_spec.rb index e29e10a61..3b8465041 100644 --- a/spec/classes/jenkins_config_spec.rb +++ b/spec/classes/jenkins_config_spec.rb @@ -10,7 +10,7 @@ context 'create config' do let(:params) { { :config_hash => { 'AJP_PORT' => { 'value' => '1234' } } }} - it { should contain_jenkins__sysconfig('AJP_PORT') } + it { should contain_jenkins__sysconfig('AJP_PORT').with_value('1234') } end end diff --git a/spec/defines/jenkins_job_spec.rb b/spec/defines/jenkins_job_spec.rb new file mode 100644 index 000000000..f8d8bd6c9 --- /dev/null +++ b/spec/defines/jenkins_job_spec.rb @@ -0,0 +1,96 @@ +require 'spec_helper' + +describe 'jenkins::job' do + let(:title) { 'myjob' } + + describe 'with defaults' do + let(:params) {{ :config => '' }} + it { should contain_exec('jenkins create-job myjob') } + it { should contain_exec('jenkins update-job myjob') } + it { should contain_exec('jenkins enable-job myjob') } + it { should_not contain_exec('jenkins disable-job myjob') } + it { should_not contain_exec('jenkins delete-job myjob') } + end + + describe 'with job enabled' do + let(:params) {{ :enabled => 1 , :config => '' }} + it { should contain_exec('jenkins create-job myjob') } + it { should contain_exec('jenkins update-job myjob') } + it { should contain_exec('jenkins enable-job myjob') } + it { should_not contain_exec('jenkins disable-job myjob') } + it { should_not contain_exec('jenkins delete-job myjob') } + end + + describe 'with job disabled' do + let(:params) {{ :enabled => 0 , :config => '' }} + it { should contain_exec('jenkins create-job myjob') } + it { should contain_exec('jenkins update-job myjob') } + it { should_not contain_exec('jenkins enable-job myjob') } + it { should contain_exec('jenkins disable-job myjob') } + it { should_not contain_exec('jenkins delete-job myjob') } + end + + describe 'with job present' do + let(:params) {{ :ensure => 'present', :config => '' }} + it { should contain_exec('jenkins create-job myjob') } + it { should contain_exec('jenkins update-job myjob') } + it { should contain_exec('jenkins enable-job myjob') } + it { should_not contain_exec('jenkins disable-job myjob') } + it { should_not contain_exec('jenkins delete-job myjob') } + end + + describe 'with job absent' do + let(:params) {{ :ensure => 'absent', :config => '' }} + it { should_not contain_exec('jenkins create-job myjob') } + it { should_not contain_exec('jenkins update-job myjob') } + it { should_not contain_exec('jenkins enable-job myjob') } + it { should_not contain_exec('jenkins disable-job myjob') } + it { should contain_exec('jenkins delete-job myjob') } + end + + describe 'with unformatted config' do + unformatted_config = < + + ... + + "..." + +eos + formatted_config = < + + ... + + "..." + +eos + + let(:params) {{ :ensure => 'present', + :config => unformatted_config }} + it { should contain_file('/tmp/myjob-config.xml')\ + .with_content(formatted_config) } + end + + describe 'with config with single quotes' do + quotes = "" + let(:params) {{ :ensure => 'present', :config => quotes }} + it { should contain_file('/tmp/myjob-config.xml')\ + .with_content(/version="1\.0" encoding="UTF-8"/) } + end + + describe 'with config with empty tags' do + empty_tags = '' + let(:params) {{ :ensure => 'present', :config => empty_tags }} + it { should contain_file('/tmp/myjob-config.xml')\ + .with_content('') } + end + + describe 'with config with "' do + quotes = "the dog said "woof"" + let(:params) {{ :ensure => 'present', :config => quotes }} + it { should contain_file('/tmp/myjob-config.xml')\ + .with_content('the dog said "woof"') } + end + +end diff --git a/tests/RedHatEnterpriseServer.pp b/tests/RedHatEnterpriseServer.pp index 82da0777e..1b38fbab3 100644 --- a/tests/RedHatEnterpriseServer.pp +++ b/tests/RedHatEnterpriseServer.pp @@ -5,4 +5,9 @@ 'ansicolor' : version => '0.3.1'; } + + jenkins::job { + 'build' : + content => ''; + } } diff --git a/tests/Ubuntu.pp b/tests/Ubuntu.pp index 82da0777e..1b38fbab3 100644 --- a/tests/Ubuntu.pp +++ b/tests/Ubuntu.pp @@ -5,4 +5,9 @@ 'ansicolor' : version => '0.3.1'; } + + jenkins::job { + 'build' : + content => ''; + } }