From 04d18a18702552b7974fc0fc77a4f2685c84cab1 Mon Sep 17 00:00:00 2001 From: troyready Date: Fri, 15 Dec 2017 16:52:28 -0800 Subject: [PATCH] windows master support Signed-off-by: troyready --- .kitchen.yml | 46 ++++++++-- README.md | 5 +- attributes/default.rb | 5 +- attributes/master.rb | 88 ++++++++++++++----- libraries/_helper.rb | 7 +- libraries/plugin.rb | 2 +- metadata.rb | 3 +- recipes/_master_msi.rb | 76 ++++++++++++++++ recipes/_master_package.rb | 5 ++ recipes/_master_war.rb | 5 ++ .../jenkins_credentials/recipes/create.rb | 5 +- .../jenkins_server_wrapper/recipes/default.rb | 2 +- .../serverspec/support/jenkins_credentials.rb | 4 +- .../helpers/serverspec/support/jenkins_job.rb | 4 +- .../serverspec/support/jenkins_plugin.rb | 12 ++- .../serverspec/support/jenkins_slave.rb | 25 +++++- .../serverspec/support/jenkins_user.rb | 9 +- 17 files changed, 258 insertions(+), 45 deletions(-) create mode 100644 recipes/_master_msi.rb diff --git a/.kitchen.yml b/.kitchen.yml index 236c698a15..9da46b654b 100644 --- a/.kitchen.yml +++ b/.kitchen.yml @@ -14,24 +14,50 @@ provisioner: name: chef_zero data_path: test/fixtures/keys data_bags_path: test/fixtures/data_bags - attributes: - jenkins: - master: - host: localhost - install_method: war - mirror: https://updates.jenkins.io platforms: - name: amazonlinux driver_config: box: mvbcoding/awslinux + attributes: + jenkins: + master: + install_method: war - name: centos-6 + attributes: + jenkins: + master: + install_method: war - name: centos-7 + attributes: + jenkins: + master: + install_method: war - name: debian-7 + attributes: + jenkins: + master: + install_method: war - name: debian-8 + attributes: + jenkins: + master: + install_method: war - name: debian-9 + attributes: + jenkins: + master: + install_method: war - name: ubuntu-14.04 + attributes: + jenkins: + master: + install_method: war - name: ubuntu-16.04 + attributes: + jenkins: + master: + install_method: war - name: windows-2012r2 driver: box: chef/windows-server-2012r2-standard # private box @@ -48,8 +74,6 @@ suites: excludes: - ubuntu-14.04 - debian-7 - - windows-2012r2 - - windows-2016 - name: smoke_package_current run_list: jenkins_server_wrapper::default @@ -69,6 +93,9 @@ suites: master: install_method: war source: https://updates.jenkins.io/stable/latest/jenkins.war + excludes: + - windows-2012r2 + - windows-2016 - name: smoke_war_latest run_list: jenkins_server_wrapper::default attributes: @@ -76,6 +103,9 @@ suites: master: install_method: war source: https://updates.jenkins.io/latest/jenkins.war + excludes: + - windows-2012r2 + - windows-2016 # # Authentication suites diff --git a/README.md b/README.md index 8e0c04a7cc..6806999e90 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Installs and configures Jenkins CI master & node slaves. Resource providers to s - Debian 7+ (Package installs require 9+ due to dependencies) - Ubuntu 14.04+ (Package installs require 16.04+ due to dependencies) - RHEL/CentOS/Scientific/Oracle 6+ +- Windows 2008R2+ ### Chef @@ -18,6 +19,7 @@ Installs and configures Jenkins CI master & node slaves. Resource providers to s ### Cookbooks +- ark - compat_resource - runit @@ -37,10 +39,11 @@ Documentation and examples are provided inline using YARD. The tests and fixture ### master -The master recipe will create the required directory structure and install jenkins. There are two installation methods, controlled by the `node['jenkins']['master']['install_method']` attribute: +The master recipe will create the required directory structure and install jenkins. There are three installation methods, controlled by the `node['jenkins']['master']['install_method']` attribute: - `package` - Install Jenkins from the official jenkins-ci.org packages - `war` - Download the latest version of the WAR file and configure it with Runit +- `msi` - Install Jenkins on Windows from the official jenkins-ci.org packages ## Resource/Provider diff --git a/attributes/default.rb b/attributes/default.rb index 8439d7254a..f441861c42 100644 --- a/attributes/default.rb +++ b/attributes/default.rb @@ -49,6 +49,9 @@ elsif ENV['JAVA_HOME'] File.join(ENV['JAVA_HOME'], 'bin', 'java') else - 'java' + case node['os'] + when 'windows' then 'C:\Program Files (x86)\Jenkins\jre\bin\java.exe' + else 'java' + end end end diff --git a/attributes/master.rb b/attributes/master.rb index 92771c653a..19a52929eb 100644 --- a/attributes/master.rb +++ b/attributes/master.rb @@ -33,16 +33,28 @@ # node.normal['jenkins']['master']['install_method'] = 'war' # master['install_method'] = case node['platform_family'] - when 'debian', 'rhel', 'amazon' then 'package' + when 'debian', 'rhel', 'amazon' + 'package' + when 'windows' + 'msi' else 'war' end + # + # Installation options to pass to MSI installer. + # + # node.normal['jenkins']['master']['msi_install_options'] = "JENKINSDIR=\"#{node['jenkins']['master']['home']}\"" + # + master['msi_install_options'] = nil + # # The version of the Jenkins master to install. This can be a specific # package version (from the yum or apt repo), or the version of the war # file to download from the Jenkins mirror. # - master['version'] = nil + master['version'] = case node['os'] + when 'windows' then '2.89.2' + end # # The "channel" to use, default is stable @@ -64,39 +76,62 @@ master['mirror'] = 'https://updates.jenkins.io' # - # The full URL to the Jenkins WAR file on the remote mirror. This attribute is - # only used in the "war" installation method. This is a compiled attribute - # from the +mirror+ and +version+ attributes, but you can override this - # attribute and specify the full URL path to a remote file for the Jenkins - # war file. If you choose to override this file manually, it is highly - # recommended that you also set the +checksum+ attribute. + # The full URL to the Jenkins WAR/ZIP file on the remote mirror. This + # attribute is only used in the "war" & "msi" installation methods. This is a + # compiled attribute from the +mirror+ and +version+ attributes, but you can + # override this attribute and specify the full URL path to a remote file for + # the Jenkins war/zip file. If you choose to override this file manually, it + # is highly recommended that you also set the +checksum+ attribute. # # node.normal['jenkins']['master']['source'] = 'http://fs01.example.com/jenkins.war' # # Warning: Setting this attribute will negate/ignore any values for +mirror+ - # and +version+. - # - master['source'] = "#{node['jenkins']['master']['mirror']}/"\ - "#{node['jenkins']['master']['version'] || node['jenkins']['master']['channel']}/"\ - 'latest/jenkins.war' + # and (for the "war" installation method) +version+. + # + master['source'] = + case node['os'] + when 'windows' + "http://mirrors.jenkins-ci.org/windows-#{node['jenkins']['master']['channel']}/"\ + "jenkins-#{node['jenkins']['master']['version']}.zip" + else + "#{node['jenkins']['master']['mirror']}/"\ + "#{node['jenkins']['master']['version'] || node['jenkins']['master']['channel']}/"\ + 'latest/jenkins.war' + end # - # The checksum of the war file. This is use to verify that the remote war file - # has not been tampered with (such as a MITM attack). If you leave this # + # The checksum of the war or zip file. This is use to verify that the remote + # file has not been tampered with (such as a MITM attack). If you leave this # attribute set to +nil+, no validation will be performed. If this attribute # is set to the wrong SHA-256 checksum, the Chef Client run will fail. # # node.normal['jenkins']['master']['checksum'] = 'abcd1234...' # - master['checksum'] = nil + master['checksum'] = case node['os'] + when 'windows' then 'b0c65a14d554d2b5b588c3ee8ab69af68334aea1bcfeebefb40c84fa7b6d5526' + end # - # The list of options to pass to the Java JVM script when using the package - # installer. For example: + # When installing Jenkins via a msi on Windows, this attribute can be used + # to specify the msi's SHA-256 checksum. + # + # node.normal['jenkins']['master']['msi_checksum'] = 'abcd1234...' + # + master['msi_checksum'] = nil + + # + # The list of options to pass to the Java JVM script when using the + # package/msi installer. For example: # # node.normal['jenkins']['master']['jvm_options'] = '-Xmx256m' # - master['jvm_options'] = '-Djenkins.install.runSetupWizard=false' + master['jvm_options'] = + case node['os'] + when 'windows' + '-Xrs -Xmx256m -Djenkins.install.runSetupWizard=false -Dhudson.lifecycle=hudson.lifecycle.WindowsServiceLifecycle -jar "%BASE%\jenkins.war" --httpPort=8080 --webroot="%BASE%\war"' + else + '-Djenkins.install.runSetupWizard=false' + end # # The list of Jenkins arguments to pass to the initialize script. This varies @@ -125,13 +160,19 @@ # # node.normal['jenkins']['master']['user'] = 'root' # - master['user'] = 'jenkins' + master['user'] = case node['os'] + when 'windows' then 'SYSTEM' + else 'jenkins' + end # # The group under which Jenkins is running. Jenkins doesn't actually use or # honor this attribute - it is used for file permission purposes. # - master['group'] = 'jenkins' + master['group'] = case node['os'] + when 'windows' then 'Administrators' + else 'jenkins' + end # # Jenkins user/group should be created as `system` accounts for `war` install. @@ -180,7 +221,10 @@ # configuration and build artifacts. You should ensure this directory resides # on a volume with adequate disk space. # - master['home'] = '/var/lib/jenkins' + master['home'] = case node['os'] + when 'windows' then 'C:\Program Files (x86)\Jenkins' + else '/var/lib/jenkins' + end # # The directory where Jenkins should write its logfile(s). **This attribute diff --git a/libraries/_helper.rb b/libraries/_helper.rb index 0eb40f1bc7..cc61169947 100644 --- a/libraries/_helper.rb +++ b/libraries/_helper.rb @@ -220,7 +220,12 @@ def convert_blank_values_to_nil(hash) # the escaped value # def escape(value) - Shellwords.escape(value) + case node['os'] + when 'windows' + "\"#{value}\"" + else + Shellwords.escape(value) + end end # diff --git a/libraries/plugin.rb b/libraries/plugin.rb index 481ceced5a..4fe84d4d30 100644 --- a/libraries/plugin.rb +++ b/libraries/plugin.rb @@ -317,7 +317,7 @@ def install_plugin_from_url(source_url, plugin_name, plugin_version = nil, opts # Jenkins that prevents Jenkins from following 302 redirects, so we # use Chef to download the plugin and then use Jenkins to install it. # It's a bit backwards, but so is Jenkins. - executor.execute!('install-plugin', escape('file://' + plugin.path), '-name', escape(plugin_name), opts[:cli_opts]) + executor.execute!('install-plugin', escape("#{node['os'] == 'windows' ? '' : 'file://'}#{plugin.path}"), '-name', escape(plugin_name), opts[:cli_opts]) end # diff --git a/metadata.rb b/metadata.rb index 99306c95c3..0a6ea75489 100644 --- a/metadata.rb +++ b/metadata.rb @@ -8,10 +8,11 @@ recipe 'jenkins::master', 'Installs a Jenkins master' -%w(ubuntu debian redhat centos scientific oracle amazon).each do |os| +%w(ubuntu debian redhat centos scientific oracle amazon windows).each do |os| supports os end +depends 'ark', '>= 2.2.0' depends 'runit', '>= 1.7' depends 'compat_resource', '>= 12.16.3' depends 'dpkg_autostart' diff --git a/recipes/_master_msi.rb b/recipes/_master_msi.rb new file mode 100644 index 0000000000..85589c6974 --- /dev/null +++ b/recipes/_master_msi.rb @@ -0,0 +1,76 @@ +# +# Cookbook Name:: jenkins +# Recipe:: _master_msi +# +# Author: Troy Ready +# +# Copyright:: 2017, Sturdy Networks +# Copyright:: 2014-2017, Chef Software, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +if ::File.extname(node['jenkins']['master']['source']) == '.zip' + include_recipe 'ark::default' + + cached_jenkins_msi = ::File.join(Chef::Config[:file_cache_path], + "jenkins-#{node['jenkins']['master']['version']}.msi") + jenkins_msi_source = cached_jenkins_msi + + unless ::File.exist? cached_jenkins_msi + ark "jenkins-#{node['jenkins']['master']['version']}" do + url node['jenkins']['master']['source'] + checksum node['jenkins']['master']['checksum'] if node['jenkins']['master']['checksum'] + creates 'jenkins.msi' + path Chef::Config[:file_cache_path] + action :cherry_pick + end + ruby_block 'rename_generic_jenkins_msi_file' do + block do + require 'fileutils' + ::FileUtils.mv(::File.join(Chef::Config[:file_cache_path], 'jenkins.msi'), cached_jenkins_msi) + end + end + end +else + jenkins_msi_source = node['jenkins']['master']['source'] +end + +windows_package "Jenkins #{node['jenkins']['master']['version']}" do + source jenkins_msi_source + checksum node['jenkins']['master']['msi_checksum'] if node['jenkins']['master']['msi_checksum'] + options node['jenkins']['master']['msi_install_options'] if node['jenkins']['master']['msi_install_options'] +end + +service 'jenkins' do + action [:enable, :start] +end + +jenkins_service_file = ::File.join(node['jenkins']['master']['home'], 'jenkins.xml') +ruby_block 'update_jenkins_jvm_options' do + block do + # This XML update would be nice to do with rexml, but the quotes in the + # options (e.g. -jar "%BASE%\jenkins.war") get escaped (") in an + # undesirable way + fe = Chef::Util::FileEdit.new(jenkins_service_file) + fe.search_file_replace(%r{^\s\s.*$}, + " #{node['jenkins']['master']['jvm_options']}") + fe.write_file + end + not_if do + require 'rexml/document' + jenkinsdoc = ::REXML::Document.new ::File.read(jenkins_service_file) + jenkinsdoc.elements['service'].elements['arguments'].text == node['jenkins']['master']['jvm_options'] + end + notifies :restart, 'service[jenkins]', :immediately +end diff --git a/recipes/_master_package.rb b/recipes/_master_package.rb index 4e610ba9b6..67e162586b 100644 --- a/recipes/_master_package.rb +++ b/recipes/_master_package.rb @@ -21,6 +21,11 @@ # limitations under the License. # +if Chef::Platform.windows? + Chef::Application.fatal! 'Jenkins "package" installation method not '\ + 'supported on Windows (use "msi" instead)' +end + case node['platform_family'] when 'debian' package 'apt-transport-https' diff --git a/recipes/_master_war.rb b/recipes/_master_war.rb index 2c7ac1320c..e865b4bb8b 100644 --- a/recipes/_master_war.rb +++ b/recipes/_master_war.rb @@ -24,6 +24,11 @@ # limitations under the License. # +if Chef::Platform.windows? + Chef::Application.fatal! 'Jenkins "war" installation method not supported '\ + 'on Windows (use "msi" instead)' +end + # Create the Jenkins user user node['jenkins']['master']['user'] do home node['jenkins']['master']['home'] diff --git a/test/fixtures/cookbooks/jenkins_credentials/recipes/create.rb b/test/fixtures/cookbooks/jenkins_credentials/recipes/create.rb index 498690a79b..a88df686b8 100644 --- a/test/fixtures/cookbooks/jenkins_credentials/recipes/create.rb +++ b/test/fixtures/cookbooks/jenkins_credentials/recipes/create.rb @@ -2,7 +2,10 @@ include_recipe 'jenkins_server_wrapper::default' -fixture_data_base_path = '/tmp/kitchen/data' +fixture_data_base_path = case Chef::Platform.windows? + when true then 'c:/Users/Administrator/AppData/Local/Temp/kitchen/data' + else '/tmp/kitchen/data' + end # Test basic password credentials creation jenkins_password_credentials 'schisamo' do diff --git a/test/fixtures/cookbooks/jenkins_server_wrapper/recipes/default.rb b/test/fixtures/cookbooks/jenkins_server_wrapper/recipes/default.rb index 005c2c088b..2f63ccc8cc 100644 --- a/test/fixtures/cookbooks/jenkins_server_wrapper/recipes/default.rb +++ b/test/fixtures/cookbooks/jenkins_server_wrapper/recipes/default.rb @@ -1,6 +1,6 @@ apt_update 'update' if platform_family?('debian') -include_recipe 'java::default' +include_recipe 'java::default' unless node['os'] == 'windows' include_recipe 'jenkins::master' # Install some plugins needed, but not installed on jenkins2 by default diff --git a/test/integration/helpers/serverspec/support/jenkins_credentials.rb b/test/integration/helpers/serverspec/support/jenkins_credentials.rb index 2a1bb9184d..6b23ae9d3c 100644 --- a/test/integration/helpers/serverspec/support/jenkins_credentials.rb +++ b/test/integration/helpers/serverspec/support/jenkins_credentials.rb @@ -74,7 +74,9 @@ def has_passphrase?(_passphrase) def xml return @xml if @xml - contents = ::File.read('/var/lib/jenkins/credentials.xml') + contents = ::File.read( + RUBY_PLATFORM.include?('mingw') ? 'C:\Program Files (x86)\Jenkins\credentials.xml' : '/var/lib/jenkins/credentials.xml' + ) doc = REXML::Document.new(contents) @xml = REXML::XPath.first(doc, "//*[username/text() = '#{username}']/") rescue Errno::ENOENT diff --git a/test/integration/helpers/serverspec/support/jenkins_job.rb b/test/integration/helpers/serverspec/support/jenkins_job.rb index 14a870af8e..3bc9df7e43 100644 --- a/test/integration/helpers/serverspec/support/jenkins_job.rb +++ b/test/integration/helpers/serverspec/support/jenkins_job.rb @@ -40,7 +40,9 @@ def has_plugin_like?(rx) def xml return @xml if @xml - contents = ::File.read("/var/lib/jenkins/jobs/#{name}/config.xml") + contents = ::File.read( + RUBY_PLATFORM.include?('mingw') ? "C:\\Program Files (x86)\\Jenkins\\jobs\\#{name}\\config.xml" : "/var/lib/jenkins/jobs/#{name}/config.xml" + ) @xml = REXML::Document.new(contents) rescue Errno::ENOENT @xml = nil diff --git a/test/integration/helpers/serverspec/support/jenkins_plugin.rb b/test/integration/helpers/serverspec/support/jenkins_plugin.rb index 0ecf20e7fe..012bf761e3 100644 --- a/test/integration/helpers/serverspec/support/jenkins_plugin.rb +++ b/test/integration/helpers/serverspec/support/jenkins_plugin.rb @@ -26,11 +26,19 @@ def has_version?(version) private def disabled_plugin - "/var/lib/jenkins/plugins/#{name}.jpi.disabled" + case RUBY_PLATFORM.include?('mingw') + when true then "C:\\Program Files (x86)\\Jenkins\\plugins\\#{name}.jpi.disabled" + else "/var/lib/jenkins/plugins/#{name}.jpi.disabled" + end end def config - manifest = "/var/lib/jenkins/plugins/#{name}/META-INF/MANIFEST.MF" + manifest = case RUBY_PLATFORM.include?('mingw') + when true + "C:\\Program Files (x86)\\Jenkins\\plugins\\#{name}\\META-INF\\MANIFEST.MF" + else + "/var/lib/jenkins/plugins/#{name}/META-INF/MANIFEST.MF" + end @config ||= Hash[*::File.readlines(manifest).map do |line| next if line.strip.empty? diff --git a/test/integration/helpers/serverspec/support/jenkins_slave.rb b/test/integration/helpers/serverspec/support/jenkins_slave.rb index 0c8d34775f..f1dcb68d51 100644 --- a/test/integration/helpers/serverspec/support/jenkins_slave.rb +++ b/test/integration/helpers/serverspec/support/jenkins_slave.rb @@ -127,14 +127,26 @@ def xml # If authn is enabled fall back to reading main config from disk elsif response.is_a?(Net::HTTPForbidden) # attempt to read from dedicated slave xml file first - config_path = "/var/lib/jenkins/nodes/#{name}/config.xml" + config_path = case RUBY_PLATFORM.include?('mingw') + when true + "C:\\Program Files (x86)\\Jenkins\\nodes\\#{name}\\config.xml" + else + "/var/lib/jenkins/nodes/#{name}/config.xml" + end if ::File.exist?(config_path) contents = ::File.read(config_path) REXML::Document.new(contents) # Fall back to reading from the main config xml else - contents = ::File.read('/var/lib/jenkins/config.xml') + contents = ::File.read( + case RUBY_PLATFORM.include?('mingw') + when true + 'C:\Program Files (x86)\Jenkins\config.xml' + else + '/var/lib/jenkins/config.xml' + end + ) config_xml = REXML::Document.new(contents) REXML::Document.new(config_xml.elements["//slave[name='#{name}']"].to_s) end @@ -157,7 +169,14 @@ def json end def credentials_xml_for_id(credentials_id) - contents = ::File.read('/var/lib/jenkins/credentials.xml') + contents = ::File.read( + case RUBY_PLATFORM.include?('mingw') + when true + 'C:\Program Files (x86)\Jenkins\credentials.xml' + else + '/var/lib/jenkins/credentials.xml' + end + ) doc = REXML::Document.new(contents) REXML::XPath.first(doc, "//*[id/text() = '#{credentials_id}']/") rescue Errno::ENOENT diff --git a/test/integration/helpers/serverspec/support/jenkins_user.rb b/test/integration/helpers/serverspec/support/jenkins_user.rb index 74ded92943..d69f86961e 100644 --- a/test/integration/helpers/serverspec/support/jenkins_user.rb +++ b/test/integration/helpers/serverspec/support/jenkins_user.rb @@ -40,7 +40,14 @@ def password_hash def xml return @xml if @xml - contents = ::File.read("/var/lib/jenkins/users/#{id}/config.xml") + contents = ::File.read( + case RUBY_PLATFORM.include?('mingw') + when true + "C:\\Program Files (x86)\\Jenkins\\users\\#{id}\\config.xml" + else + "/var/lib/jenkins/users/#{id}/config.xml" + end + ) @xml = REXML::Document.new(contents) rescue Errno::ENOENT @xml = nil