diff --git a/README.md b/README.md index 2a4124cd5..475c64698 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/contrib/examples/job-configuration/build.pp b/contrib/examples/job-configuration/build.pp new file mode 100644 index 000000000..2713aa832 --- /dev/null +++ b/contrib/examples/job-configuration/build.pp @@ -0,0 +1,20 @@ +class jenkins::job::build( + $config = undef, + $jobname = $title, + $enabled = 1, + $ensure = 'present', +) { + + if $config == undef { + $real_content = template('jenkins/job/build.xml.erb') + } else { + $real_content = $config + } + + jenkins::job { 'build': + config => $real_content, + jobname => $jobname, + enabled => $enabled, + ensure => $ensure, + } +} diff --git a/contrib/examples/job-configuration/templates/build.xml.erb b/contrib/examples/job-configuration/templates/build.xml.erb new file mode 100644 index 000000000..26eab7b3d --- /dev/null +++ b/contrib/examples/job-configuration/templates/build.xml.erb @@ -0,0 +1,17 @@ + + + + + false + + + true + false + false + false + + false + + + + diff --git a/manifests/cli.pp b/manifests/cli.pp index 34f22f63c..6afd9ebfe 100644 --- a/manifests/cli.pp +++ b/manifests/cli.pp @@ -18,6 +18,41 @@ path => ['/bin', '/usr/bin'], cwd => '/tmp', creates => $jar, - require => Package['jenkins'], + require => Service['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 => 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 => File[$jar], } } diff --git a/manifests/init.pp b/manifests/init.pp index d0aabbc0f..235bea593 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -83,6 +83,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, @@ -131,6 +132,7 @@ include jenkins::firewall } } + if $cli { include jenkins::cli } @@ -138,10 +140,19 @@ Anchor['jenkins::begin'] -> Class['jenkins::package'] -> Class['jenkins::config'] -> - Class['jenkins::plugins']~> + Class['jenkins::plugins'] ~> Class['jenkins::service'] -> + Class['jenkins::jobs'] -> Anchor['jenkins::end'] + if $cli { + Anchor['jenkins::begin'] -> + Class['jenkins::service'] -> + Class['jenkins::cli'] -> + Class['jenkins::jobs'] -> + Anchor['jenkins::end'] + } + if $install_java { Anchor['jenkins::begin'] -> Class['java'] -> 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..be22c7585 --- /dev/null +++ b/manifests/job/absent.pp @@ -0,0 +1,39 @@ +# 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, +){ + include jenkins::cli + + if $jenkins::service_ensure == 'stopped' or $jenkins::service_ensure == false { + fail('Management of Jenkins jobs requires \$jenkins::service_ensure to be set to \'running\'') + } + + $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}", + require => Exec['jenkins-cli'], + } + +} diff --git a/manifests/job/present.pp b/manifests/job/present.pp new file mode 100644 index 000000000..a0a2fc4ef --- /dev/null +++ b/manifests/job/present.pp @@ -0,0 +1,100 @@ +# 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, +){ + include jenkins::cli + + if $jenkins::service_ensure == 'stopped' or $jenkins::service_ensure == false { + fail('Management of Jenkins jobs requires \$jenkins::service_ensure to be set to \'running\'') + } + + $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, + require => Exec['jenkins-cli'], + } + + # 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 => File[$tmp_config_path], + } + + # 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 => File[$tmp_config_path], + notify => Exec['reload-jenkins'], + } + + # 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 => [ + Exec["jenkins create-job ${jobname}"], + Exec["jenkins update-job ${jobname}"], + ], + } + } else { + exec { "jenkins disable-job ${jobname}": + command => "${jenkins_cli} disable-job ${jobname}", + onlyif => "cat ${config_path} | grep 'false'", + require => [ + Exec["jenkins create-job ${jobname}"], + Exec["jenkins update-job ${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/classes/jenkins_jobs_spec.rb b/spec/classes/jenkins_jobs_spec.rb new file mode 100644 index 000000000..58f87db52 --- /dev/null +++ b/spec/classes/jenkins_jobs_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe 'jenkins', :type => :module do + let(:facts) { { :osfamily => 'RedHat', :operatingsystem => 'RedHat' } } + + context 'jobs' do + context 'default' do + it { should contain_class('jenkins::jobs') } + end + + context 'with one job' do + let(:params) { { :job_hash => { 'build' => { 'config' => '' } } } } + it { should contain_jenkins__job('build').with_config('') } + end + + context 'with cli disabled' do + let(:params) { { :service_ensure => 'stopped', + :cli => false, + :job_hash => { 'build' => { 'config' => '' } } } } + it { expect { should compile }.to raise_error } + end + + 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..c9afbcef4 100644 --- a/tests/RedHatEnterpriseServer.pp +++ b/tests/RedHatEnterpriseServer.pp @@ -5,4 +5,25 @@ 'ansicolor' : version => '0.3.1'; } + + jenkins::job { + 'build' : + config => ' + + + + false + + + true + false + false + false + + false + + + +'; + } } diff --git a/tests/Ubuntu.pp b/tests/Ubuntu.pp index 82da0777e..c9afbcef4 100644 --- a/tests/Ubuntu.pp +++ b/tests/Ubuntu.pp @@ -5,4 +5,25 @@ 'ansicolor' : version => '0.3.1'; } + + jenkins::job { + 'build' : + config => ' + + + + false + + + true + false + false + false + + false + + + +'; + } }