diff --git a/CHANGELOG.md b/CHANGELOG.md index 268a37a3f2..e8a213cf1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,18 @@ This file is used to list changes made in each version of the jenkins cookbook. ## Unreleased +- Require Chef 15.3 for custom resources unified_mode +- Require Chef 16 for resource partials +- Move credentials_file to a custom resource +- Add the credentials partial for reuse for all credentials resources + ## 9.5.4 - *2022-12-08* -Standardise files with files in sous-chefs/repo-management +- Standardise files with files in sous-chefs/repo-management ## 9.5.3 - *2022-12-02* -Standardise files with files in sous-chefs/repo-management +- Standardise files with files in sous-chefs/repo-management ## 9.5.2 - *2022-03-28* @@ -56,8 +61,8 @@ Standardise files with files in sous-chefs/repo-management ### Breaking Changes / Deprecations - `jenkins_jnlp_slave`: - - Renamed `runit_groups` property to `service_groups` - - New service created -- old Runit service will need manual cleanup + - Renamed `runit_groups` property to `service_groups` + - New service created -- old Runit service will need manual cleanup - `jenkins::_master_war`: - New service created -- old Runit service will need manual cleanup @@ -518,22 +523,22 @@ Standardise files with files in sous-chefs/repo-management - Remove old TODO file - Refactor attributes into semantic groupings and namespaces - - `jenkins.cli` has been removed - - `jenkins.java_home` has been changed to `jenkins.java` and accepts the full path to the java binary, not the JAVA_HOME - - `jenkins.iptables_allow` has been removed - - `jenkins.mirror` -> `jenkins.master.mirror` - - `jenkins.executor` created - - `jenkins.executor.timeout` created - - `jenkins.executor.private_key` created - - `jenkins.executor.proxy` created - - `jenkins.master` created and only refers to the Jenkins master installation - - `jenkins.master.source` created to refer to the full URL of the war download - - `jenkins.master.jvm_options` created - - `jenkins.master.jenkins_args` added - - `jenkins.master.url` -> `jenkins.master.endpoint` - - `jenkins.master.log_directory` created - - `jenkins.node` attributes have all been removed - - `jenkins.server` attributes have all been removed + - `jenkins.cli` has been removed + - `jenkins.java_home` has been changed to `jenkins.java` and accepts the full path to the java binary, not the JAVA_HOME + - `jenkins.iptables_allow` has been removed + - `jenkins.mirror` -> `jenkins.master.mirror` + - `jenkins.executor` created + - `jenkins.executor.timeout` created + - `jenkins.executor.private_key` created + - `jenkins.executor.proxy` created + - `jenkins.master` created and only refers to the Jenkins master installation + - `jenkins.master.source` created to refer to the full URL of the war download + - `jenkins.master.jvm_options` created + - `jenkins.master.jenkins_args` added + - `jenkins.master.url` -> `jenkins.master.endpoint` + - `jenkins.master.log_directory` created + - `jenkins.node` attributes have all been removed + - `jenkins.server` attributes have all been removed - Removed Chef MiniTest handler diff --git a/libraries/_executor.rb b/libraries/_executor.rb index 0020e77d87..a6f242bf23 100644 --- a/libraries/_executor.rb +++ b/libraries/_executor.rb @@ -1,24 +1,3 @@ -# -# Cookbook:: jenkins -# Library:: executor -# -# Author:: Seth Vargo -# -# Copyright:: 2013-2019, 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. -# - require 'mixlib/shellout' require 'shellwords' require 'tempfile' diff --git a/libraries/_slave.rb b/libraries/_slave.rb new file mode 100644 index 0000000000..7872c1d9b1 --- /dev/null +++ b/libraries/_slave.rb @@ -0,0 +1,117 @@ +# attr_writer :exists +# attr_writer :connected +# attr_writer :online + +module Jenkins + module Cookbook + module Slave + # + # Determine if the slave exists on the master. This value is set by + # the provider when the current resource is loaded. + # + # @return [Boolean] + # + def exists? + !@exists.nil? && @exists + end + + # + # Determine if the slave is connected to the master. This value is + # set by the provider when the current resource is loaded. + # + # @return [Boolean] + # + def connected? + !@connected.nil? && @connected + end + + # + # Determine if the slave is online. This value is set by the + # provider when the current resource is loaded. + # + # @return [Boolean] + # + def online? + !@online.nil? && @online + end + + def merge_preserved_labels! + new_resource.labels |= current_resource.labels.select { |i| i[/^prsrv_/] } + end + + def do_create + # Preserve some labels... + merge_preserved_labels! + if current_resource.exists? && correct_config? + Chef::Log.info("#{new_resource} exists - skipping") + else + converge_by("Create #{new_resource}") do + executor.groovy! <<-EOH.gsub(/^ {12}/, '') + import hudson.model.* + import hudson.slaves.* + import jenkins.model.* + import jenkins.slaves.* + + props = [] + availability = #{convert_to_groovy(new_resource.availability)} + usage_mode = #{convert_to_groovy(new_resource.usage_mode)} + env_map = #{convert_to_groovy(new_resource.environment)} + labels = #{convert_to_groovy(new_resource.labels.sort.join(' '))} + + // Compute the usage mode + if (usage_mode == 'normal') { + mode = Node.Mode.NORMAL + } else { + mode = Node.Mode.EXCLUSIVE + } + + // Compute the retention strategy + if (availability == 'demand') { + retention_strategy = + new RetentionStrategy.Demand( + #{new_resource.in_demand_delay}, + #{new_resource.idle_delay} + ) + } else if (availability == 'always') { + retention_strategy = new RetentionStrategy.Always() + } else { + retention_strategy = RetentionStrategy.NOOP + } + + // Create an entry in the prop list for all env vars + if (env_map != null) { + env_vars = new hudson.EnvVars(env_map) + entries = env_vars.collect { + k,v -> new EnvironmentVariablesNodeProperty.Entry(k,v) + } + props << new EnvironmentVariablesNodeProperty(entries) + } + + // Launcher + #{launcher_groovy} + + // Build the slave object + slave = new DumbSlave( + #{convert_to_groovy(new_resource.name)}, + #{convert_to_groovy(new_resource.description)}, + #{convert_to_groovy(new_resource.remote_fs)}, + #{convert_to_groovy(new_resource.executors.to_s)}, + mode, + labels, + launcher, + retention_strategy, + props + ) + + // Create or update the slave in the Jenkins instance + nodes = new ArrayList(Jenkins.instance.getNodes()) + ix = nodes.indexOf(slave) + (ix >= 0) ? nodes.set(ix, slave) : nodes.add(slave) + Jenkins.instance.setNodes(nodes) + EOH + end + end + end + end + end +end diff --git a/libraries/command.rb b/libraries/command.rb deleted file mode 100644 index aa49b3732f..0000000000 --- a/libraries/command.rb +++ /dev/null @@ -1,59 +0,0 @@ -# -# Cookbook:: jenkins -# Resource:: command -# -# Author:: Seth Vargo -# -# Copyright:: 2013-2019, 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. -# - -require_relative '_helper' - -class Chef - class Resource::JenkinsCommand < Resource::LWRPBase - resource_name :jenkins_command # Still needed for Chef 15 and below - provides :jenkins_command - - # Chef attributes - identity_attr :command - - # Actions - actions :execute - default_action :execute - - # Attributes - attribute :command, - kind_of: String, - name_attribute: true - end -end - -class Chef - class Provider::JenkinsCommand < Provider::LWRPBase - include Jenkins::Helper - - provides :jenkins_command - - def load_current_resource - @current_resource ||= Resource::JenkinsCommand.new(new_resource.command) - end - - action :execute do - converge_by("Execute #{new_resource}") do - executor.execute!(new_resource.command) - end - end - end -end diff --git a/libraries/credentials.rb b/libraries/credentials.rb deleted file mode 100644 index cac49b5ae7..0000000000 --- a/libraries/credentials.rb +++ /dev/null @@ -1,246 +0,0 @@ -# -# Cookbook:: jenkins -# Resource:: credentials -# -# Author:: Seth Chisamore -# -# Copyright:: 2013-2019, 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. -# - -require 'json' -require 'openssl' -require 'securerandom' - -require_relative '_helper' - -class Chef - class Resource::JenkinsCredentials < Resource::LWRPBase - # Default all crendentials resources to sensitive so secret data - # is not printed out in the Chef logs. - def initialize(name, run_context = nil) - super - @sensitive = true - end - - # Actions - actions :create, :delete - default_action :create - - # Attributes - attribute :id, - kind_of: String, - required: true - - attr_writer :exists - - # - # Determine if the credentials exists on the master. This value is - # set by the provider when the current resource is loaded. - # - # @return [Boolean] - # - def exists? - !@exists.nil? && @exists - end - end -end - -class Chef - class Provider::JenkinsCredentials < Provider::LWRPBase - include Jenkins::Helper - - def load_current_resource - @current_resource ||= Resource::JenkinsCredentials.new(new_resource.name) - - if current_credentials - @current_resource.exists = true - @current_resource.id(current_credentials[:id]) - @current_resource.description(current_credentials[:description]) - end - - @current_resource - end - - # - # Create the given credentials. - # - action :create do - if current_resource.exists? && correct_config? - Chef::Log.info("#{new_resource} exists - skipping") - else - converge_by("Create #{new_resource}") do - executor.groovy! <<-EOH.gsub(/^ {12}/, '') - import jenkins.model.* - import com.cloudbees.plugins.credentials.* - import com.cloudbees.plugins.credentials.domains.* - import hudson.plugins.sshslaves.*; - - global_domain = Domain.global() - credentials_store = - Jenkins.instance.getExtensionList( - 'com.cloudbees.plugins.credentials.SystemCredentialsProvider' - )[0].getStore() - - #{credentials_groovy} - - #{fetch_existing_credentials_groovy('existing_credentials')} - - if(existing_credentials != null) { - credentials_store.updateCredentials( - global_domain, - existing_credentials, - credentials - ) - } else { - credentials_store.addCredentials(global_domain, credentials) - } - EOH - end - end - end - - # - # Delete the given credentials. - # - action :delete do - if current_resource.exists? - converge_by("Delete #{new_resource}") do - executor.groovy! <<-EOH.gsub(/^ {12}/, '') - import jenkins.model.* - import com.cloudbees.plugins.credentials.*; - - global_domain = com.cloudbees.plugins.credentials.domains.Domain.global() - credentials_store = - Jenkins.instance.getExtensionList( - 'com.cloudbees.plugins.credentials.SystemCredentialsProvider' - )[0].getStore() - - #{fetch_existing_credentials_groovy('existing_credentials')} - - if(existing_credentials != null) { - credentials_store.removeCredentials( - global_domain, - existing_credentials - ) - } - EOH - end - else - Chef::Log.debug("#{new_resource} does not exist - skipping") - end - end - - private - - # - # Returns a Groovy snippet that creates an instance of the - # credential's implementation. The credentials instance should be - # set to a Groovy variable named `credentials`. - # - # @abstract - # @return [String] - # - def credentials_groovy - raise NotImplementedError, 'You must implement #credentials_groovy.' - end - - # - # Returns a Groovy snippet that fetches credentials from the - # credentials store. The snippet relies on the existence of both - # 'credentials_store' and 'credentials' variables, representing the - # Jenkins credentials store and the credentials to be fetched, respectively - # @abstract - # @return [String] - # - def fetch_existing_credentials_groovy(_groovy_variable_name) - raise NotImplementedError, 'You must implement #fetch_existing_credentials_groovy.' - end - - # - # Returns a Groovy snippet with an array of the resource attributes. The snippet - # relies on the existence of a variable credentials that represents the resource - # @abstract - # @return [String] - # - def resource_attributes_groovy(_groovy_variable_name) - raise NotImplementedError, 'You must implement #resource_attributes_groovy.' - end - - # - # Helper method for determining if the given JSON is in sync with the - # current configuration on the Jenkins instance. - # - # @return [Boolean] - # - def correct_config? - raise NotImplementedError, 'You must implement #correct_config?.' - end - - # - # Maps a credentials's resource attribute name to the equivalent - # property in the Groovy representation. This mapping is useful in - # Ruby/Groovy serialization/deserialization. - # - # @return [Hash] - # - # @example - # {password: 'credentials.password.plainText'} - # - def attribute_to_property_map - {} - end - - # - # Loads the current credential into a Hash. - # - def current_credentials - return @current_credentials if @current_credentials - - Chef::Log.debug "Load #{new_resource} credentials information" - - credentials_attributes = [] - attribute_to_property_map.each_pair do |resource_attribute, groovy_property| - credentials_attributes << - "current_credentials['#{resource_attribute}'] = #{groovy_property}" - end - - json = executor.groovy! <<-EOH.gsub(/^ {8}/, '') - import com.cloudbees.plugins.credentials.impl.*; - import com.cloudbees.jenkins.plugins.sshcredentials.impl.*; - - #{fetch_existing_credentials_groovy('credentials')} - - if(credentials == null) { - return null - } - - #{resource_attributes_groovy('current_credentials')} - - #{credentials_attributes.join("\n")} - - builder = new groovy.json.JsonBuilder(current_credentials) - println(builder) - EOH - - return if json.nil? || json.empty? - - @current_credentials = JSON.parse(json, symbolize_names: true) - - # Values that were serialized as nil/null are deserialized as an - # empty string! :( Let's ensure we convert back to nil. - @current_credentials = convert_blank_values_to_nil(@current_credentials) - end - end -end diff --git a/libraries/credentials_file.rb b/libraries/credentials_file.rb deleted file mode 100644 index 4edbe40638..0000000000 --- a/libraries/credentials_file.rb +++ /dev/null @@ -1,122 +0,0 @@ -# -# Cookbook:: jenkins -# HWRP:: credentials_file -# -# Author:: Olivier Abdesselam -# -# Copyright:: 2018-2019, Teads -# -# 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. -# - -# This is required to appease Travis-CI -# https://travis-ci.org/chef-cookbooks/jenkins/builds/197337230 -require_relative 'credentials' - -class Chef - class Resource::JenkinsFileCredentials < Resource::JenkinsCredentials - include Jenkins::Helper - - resource_name :jenkins_file_credentials # Still needed for Chef 15 and below - provides :jenkins_file_credentials - - attribute :description, - kind_of: String, - default: lazy { |new_resource| "Credentials for #{new_resource.filename} - created by Chef" } - - # Attributes - attribute :filename, - kind_of: String, - name_attribute: true - attribute :data, - kind_of: String, - required: true - end -end - -class Chef - class Provider::JenkinsFileCredentials < Provider::JenkinsCredentials - include Jenkins::Helper - provides :jenkins_file_credentials - - def load_current_resource - @current_resource ||= Resource::JenkinsFileCredentials.new(new_resource.name) - - super - - @current_resource.filename(current_credentials[:filename]) if current_credentials - - @current_resource - end - - private - - # - # @see Chef::Resource::JenkinsCredentials#save_credentials_groovy - # - def fetch_existing_credentials_groovy(groovy_variable_name) - <<-EOH.gsub(/^ {8}/, '') - #{credentials_for_id_groovy(new_resource.id, groovy_variable_name)} - EOH - end - - # - # @see Chef::Resource::JenkinsCredentials#resource_attributes_groovy - # - def resource_attributes_groovy(groovy_variable_name) - <<-EOH.gsub(/^ {8}/, '') - #{groovy_variable_name} = [ - id:credentials.id, - description:credentials.description, - filename:credentials.filename, - ] - EOH - end - - # - # @see Chef::Resource::JenkinsCredentials#correct_config? - # - def correct_config? - wanted_credentials = { - description: new_resource.description, - filename: new_resource.filename, - } - - attribute_to_property_map.each_key do |key| - wanted_credentials[key] = new_resource.send(key) - end - - # Don't compare the ID as it is generated - current_credentials.dup.tap { |c| c.delete(:id) } == convert_blank_values_to_nil(wanted_credentials) - end - - # - # @see Chef::Resource::JenkinsCredentials#credentials_groovy - # @see https://github.com/jenkinsci/ssh-credentials-plugin/blob/master/src/main/java/com/cloudbees/jenkins/plugins/sshcredentials/impl/BasicSSHUserPrivateKey.java - # - def credentials_groovy - <<-EOH.gsub(/^ {8}/, '') - import com.cloudbees.plugins.credentials.* - import org.jenkinsci.plugins.plaincredentials.impl.* - - credentials = new FileCredentialsImpl( - CredentialsScope.GLOBAL, - #{convert_to_groovy(new_resource.id)}, - #{convert_to_groovy(new_resource.description)}, - #{convert_to_groovy(new_resource.filename)}, - SecretBytes.fromBytes(#{convert_to_groovy(new_resource.data)}.getBytes()) - ) - EOH - end - end -end diff --git a/libraries/credentials_password.rb b/libraries/credentials_password.rb deleted file mode 100644 index b40b19d51d..0000000000 --- a/libraries/credentials_password.rb +++ /dev/null @@ -1,82 +0,0 @@ -# -# Cookbook:: jenkins -# Resource:: credentials_password -# -# Author:: Seth Chisamore -# -# Copyright:: 2013-2019, 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. -# - -require_relative 'credentials' -require_relative 'credentials_user' - -class Chef - class Resource::JenkinsPasswordCredentials < Resource::JenkinsUserCredentials - resource_name :jenkins_password_credentials # Still needed for Chef 15 and below - provides :jenkins_password_credentials - - # Attributes - attribute :username, - kind_of: String, - name_attribute: true - attribute :password, - kind_of: String, - required: true - end -end - -class Chef - class Provider::JenkinsPasswordCredentials < Provider::JenkinsUserCredentials - provides :jenkins_password_credentials - - def load_current_resource - @current_resource ||= Resource::JenkinsPasswordCredentials.new(new_resource.name) - - super - - @current_resource.password(current_credentials[:password]) if current_credentials - - @current_credentials - end - - private - - # - # @see Chef::Resource::JenkinsCredentials#credentials_groovy - # @see https://github.com/jenkinsci/credentials-plugin/blob/master/src/main/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsImpl.java - # - def credentials_groovy - <<-EOH.gsub(/^ {8}/, '') - import com.cloudbees.plugins.credentials.* - import com.cloudbees.plugins.credentials.impl.* - - credentials = new UsernamePasswordCredentialsImpl( - CredentialsScope.GLOBAL, - #{convert_to_groovy(new_resource.id)}, - #{convert_to_groovy(new_resource.description)}, - #{convert_to_groovy(new_resource.username)}, - #{convert_to_groovy(new_resource.password)} - ) - EOH - end - - # - # @see Chef::Resource::JenkinsCredentials#attribute_to_property_map - # - def attribute_to_property_map - { password: 'credentials.password.plainText' } - end - end -end diff --git a/libraries/credentials_private_key.rb b/libraries/credentials_private_key.rb deleted file mode 100644 index 6040cd201a..0000000000 --- a/libraries/credentials_private_key.rb +++ /dev/null @@ -1,138 +0,0 @@ -# -# Cookbook:: jenkins -# Resource:: credentials_password -# -# Author:: Seth Chisamore -# -# Copyright:: 2013-2019, 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. -# -require_relative 'credentials' -require_relative 'credentials_user' - -# -# Determine whether a key is an ECDSA key. As original functionality -# assumed that exclusively RSA keys were used, not breaking this assumption -# despite ECDSA keys being a possibility alleviates some issues with -# backwards-compatibility. -# -# @param [String] key -# @return [TrueClass, FalseClass] -def ecdsa_key?(key) - key.include?('BEGIN EC PRIVATE KEY') -end - -class Chef - class Resource::JenkinsPrivateKeyCredentials < Resource::JenkinsUserCredentials - include Jenkins::Helper - - resource_name :jenkins_private_key_credentials # Still needed for Chef 15 and below - provides :jenkins_private_key_credentials - - # Attributes - attribute :username, - kind_of: String, - name_attribute: true - attribute :private_key, - kind_of: [String, OpenSSL::PKey::RSA, OpenSSL::PKey::EC], - required: true - attribute :passphrase, - kind_of: String - - # - # Private key of the credentials . This should be the actual key - # contents (as opposed to the path to a private key file) in OpenSSH - # format. - # - # @param [String] arg - # @return [String] - # - def pem_private_key - if private_key.is_a?(OpenSSL::PKey::RSA) || private_key.is_a?(OpenSSL::PKey::EC) - private_key.to_pem - elsif ecdsa_key?(private_key) - OpenSSL::PKey::EC.new(private_key).to_pem - else - OpenSSL::PKey::RSA.new(private_key).to_pem - end - end - end -end - -class Chef - class Provider::JenkinsPrivateKeyCredentials < Provider::JenkinsUserCredentials - provides :jenkins_private_key_credentials - - def load_current_resource - @current_resource ||= Resource::JenkinsPrivateKeyCredentials.new(new_resource.name) - - super - - @current_resource.private_key(current_credentials[:private_key]) if current_credentials - - @current_resource - end - - private - - # - # @see Chef::Resource::JenkinsCredentials#credentials_groovy - # @see https://github.com/jenkinsci/ssh-credentials-plugin/blob/master/src/main/java/com/cloudbees/jenkins/plugins/sshcredentials/impl/BasicSSHUserPrivateKey.java - # - def credentials_groovy - <<-EOH.gsub(/^ {8}/, '') - import com.cloudbees.plugins.credentials.* - import com.cloudbees.jenkins.plugins.sshcredentials.impl.* - - private_key = """#{new_resource.pem_private_key} - """ - - credentials = new BasicSSHUserPrivateKey( - CredentialsScope.GLOBAL, - #{convert_to_groovy(new_resource.id)}, - #{convert_to_groovy(new_resource.username)}, - new BasicSSHUserPrivateKey.DirectEntryPrivateKeySource(private_key), - #{convert_to_groovy(new_resource.passphrase)}, - #{convert_to_groovy(new_resource.description)} - ) - EOH - end - - # - # @see Chef::Resource::JenkinsCredentials#attribute_to_property_map - # - def attribute_to_property_map - { - private_key: 'credentials.privateKey', - passphrase: 'credentials.passphrase && credentials.passphrase.plainText', - } - end - - # - # @see Chef::Resource::JenkinsCredentials#current_credentials - # - def current_credentials - super - - # Normalize the private key - if @current_credentials && @current_credentials[:private_key] - cc = @current_credentials[:private_key] - cc = @current_credentials[:private_key].to_pem unless cc.is_a?(String) - @current_credentials[:private_key] = ecdsa_key?(cc) ? OpenSSL::PKey::EC.new(cc) : OpenSSL::PKey::RSA.new(cc) - end - - @current_credentials - end - end -end diff --git a/libraries/credentials_secret_text.rb b/libraries/credentials_secret_text.rb deleted file mode 100644 index fba0b2a728..0000000000 --- a/libraries/credentials_secret_text.rb +++ /dev/null @@ -1,123 +0,0 @@ -# -# Cookbook:: jenkins -# Resource:: credentials_secret_text -# -# Author:: Miguel Ferreira -# -# Copyright:: 2015-2016, Schuberg Philis -# -# 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. -# - -require_relative 'credentials' - -class Chef - class Resource::JenkinsSecretTextCredentials < Resource::JenkinsCredentials - resource_name :jenkins_secret_text_credentials # Still needed for Chef 15 and below - provides :jenkins_secret_text_credentials - - # Chef attributes - identity_attr :description - - # Attributes - attribute :description, - kind_of: String, - name_attribute: true - attribute :secret, - kind_of: String, - required: true - end -end - -class Chef - class Provider::JenkinsSecretTextCredentials < Provider::JenkinsCredentials - provides :jenkins_secret_text_credentials - - def load_current_resource - @current_resource ||= Resource::JenkinsSecretTextCredentials.new(new_resource.name) - - super - - @current_resource.secret(current_credentials[:secret]) if current_credentials - - @current_credentials - end - - private - - # - # @see Chef::Resource::JenkinsCredentials#credentials_groovy - # @see https://github.com/jenkinsci/plain-credentials-plugin/blob/master/src/main/java/org/jenkinsci/plugins/plaincredentials/impl/StringCredentialsImpl.java - # - def credentials_groovy - <<-EOH.gsub(/^ {8}/, '') - import hudson.util.Secret; - import com.cloudbees.plugins.credentials.CredentialsScope; - import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl; - - credentials = new StringCredentialsImpl( - CredentialsScope.GLOBAL, - #{convert_to_groovy(new_resource.id)}, - #{convert_to_groovy(new_resource.description)}, - new Secret(#{convert_to_groovy(new_resource.secret)}), - ) - EOH - end - - # - # @see Chef::Resource::JenkinsCredentials#fetch_credentials_groovy - # - def fetch_existing_credentials_groovy(groovy_variable_name) - <<-EOH.gsub(/^ {8}/, '') - #{credentials_for_secret_groovy(new_resource.secret, new_resource.description, groovy_variable_name)} - EOH - end - - # - # @see Chef::Resource::JenkinsCredentials#resource_attributes_groovy - # - def resource_attributes_groovy(groovy_variable_name) - <<-EOH.gsub(/^ {8}/, '') - #{groovy_variable_name} = [ - id:credentials.id, - description:credentials.description, - secret:credentials.secret - ] - EOH - end - - # - # @see Chef::Resource::JenkinsCredentials#attribute_to_property_map - # - def attribute_to_property_map - { secret: 'credentials.secret.plainText' } - end - - # - # @see Chef::Resource::JenkinsCredentials#correct_config? - # - def correct_config? - wanted_credentials = { - description: new_resource.description, - secret: new_resource.secret, - } - - attribute_to_property_map.each_key do |key| - wanted_credentials[key] = new_resource.send(key) - end - - # Don't compare the ID as it is generated - current_credentials.dup.tap { |c| c.delete(:id) } == convert_blank_values_to_nil(wanted_credentials) - end - end -end diff --git a/libraries/credentials_user.rb b/libraries/credentials_user.rb deleted file mode 100644 index f748f8ad4b..0000000000 --- a/libraries/credentials_user.rb +++ /dev/null @@ -1,89 +0,0 @@ -# -# Cookbook:: jenkins -# Resource:: credentials_user -# -# Author:: Miguel Ferreira -# -# Copyright:: 2015-2016, Schuberg Philis -# -# 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. -# - -# This is required to appease Travis-CI -# https://travis-ci.org/chef-cookbooks/jenkins/builds/197337230 -require_relative 'credentials' - -class Chef - class Resource::JenkinsUserCredentials < Resource::JenkinsCredentials - attribute :description, - kind_of: String, - default: lazy { |new_resource| "Credentials for #{new_resource.username} - created by Chef" } - end -end - -class Chef - class Provider::JenkinsUserCredentials < Provider::JenkinsCredentials - include Jenkins::Helper - - def load_current_resource - @current_resource ||= Resource::JenkinsUserCredentials.new(new_resource.name) - - super - - @current_resource.username(current_credentials[:username]) if current_credentials - - @current_resource - end - - private - - # - # @see Chef::Resource::JenkinsCredentials#save_credentials_groovy - # - def fetch_existing_credentials_groovy(groovy_variable_name) - <<-EOH.gsub(/^ {8}/, '') - #{credentials_for_id_groovy(new_resource.id, groovy_variable_name)} - EOH - end - - # - # @see Chef::Resource::JenkinsCredentials#resource_attributes_groovy - # - def resource_attributes_groovy(groovy_variable_name) - <<-EOH.gsub(/^ {8}/, '') - #{groovy_variable_name} = [ - id:credentials.id, - description:credentials.description, - username:credentials.username - ] - EOH - end - - # - # @see Chef::Resource::JenkinsCredentials#correct_config? - # - def correct_config? - wanted_credentials = { - description: new_resource.description, - username: new_resource.username, - } - - attribute_to_property_map.each_key do |key| - wanted_credentials[key] = new_resource.send(key) - end - - # Don't compare the ID as it is generated - current_credentials.dup.tap { |c| c.delete(:id) } == convert_blank_values_to_nil(wanted_credentials) - end - end -end diff --git a/libraries/job.rb b/libraries/job.rb deleted file mode 100644 index 1037208620..0000000000 --- a/libraries/job.rb +++ /dev/null @@ -1,343 +0,0 @@ -# -# Cookbook:: jenkins -# Resource:: job -# -# Author:: Seth Vargo -# -# Copyright:: 2013-2019, 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. -# - -require 'rexml/document' - -require_relative '_helper' - -class Chef - class Resource::JenkinsJob < Resource::LWRPBase - resource_name :jenkins_job # Still needed for Chef 15 and below - provides :jenkins_job - - # Chef attributes - identity_attr :name - - # Actions - actions :build, :create, :delete, :disable, :enable - default_action :create - - # Attributes - attribute :config, - kind_of: String - - # Execute specific attributes - attribute :parameters, - kind_of: Hash, - default: {} - attribute :stream_job_output, - kind_of: [TrueClass, FalseClass], - default: true - attribute :wait_for_completion, - kind_of: [TrueClass, FalseClass], - default: true - - attr_writer :enabled, :exists - - # - # Determine if the job exists on the master. This value is set by the - # provider when the current resource is loaded. - # - # @return [Boolean] - # - def exists? - !@exists.nil? && @exists - end - - # - # Determine if the job is enabled on the master. This value is set by the - # provider when the current resource is loaded. - # - # @return [Boolean] - # - def enabled? - !@enabled.nil? && @enabled - end - end -end - -class Chef - class Provider::JenkinsJob < Provider::LWRPBase - include Jenkins::Helper - - provides :jenkins_job - - # After some careful discussions internally, it was decided that - # raising an exception when the job does not exist is the best - # developer experience. - class JobDoesNotExist < StandardError - def initialize(job, action) - super <<-EOH -The Jenkins job `#{job}' does not exist. In order to :#{action} `#{job}', that -job must first exist on the Jenkins master! - EOH - end - end - - def load_current_resource - @current_resource ||= Resource::JenkinsJob.new(new_resource.name) - @current_resource.name(new_resource.name) - @current_resource.config(new_resource.config) - - if current_job - @current_resource.exists = true - @current_resource.enabled = current_job[:enabled] - else - @current_resource.exists = false - @current_resource.enabled = false - end - - @current_resource - end - - # - # Executes a Jenkins job. - # - # @raise [JobDoesNotExist] - # if the job does not exist - # - action :build do - raise JobDoesNotExist.new(new_resource.name, :build) unless current_resource.exists? - - if current_resource.enabled? - converge_by("Build #{new_resource}") do - command_args = [ - 'build', - escape(new_resource.name), - ] - - if new_resource.wait_for_completion - command_args << '-s' # Wait until the completion/abortion of the command. - end - - new_resource.parameters.each_pair do |key, value| - command_args << case value - when TrueClass, FalseClass - "-p #{key}=#{value}" - else - if value.include?(' ') - "-p #{key}='#{value}'" - else - "-p #{key}=#{value}" - end - end - end - - if new_resource.stream_job_output && new_resource.wait_for_completion && stdout_stream - command_args << '-v' # Prints out the console output of the build. - - stdout_stream.print <<-EOH - - -================================================================================ -= BEGIN '#{new_resource.name}' Jenkins job output -================================================================================ - - EOH - - executor.execute!(*command_args, live_stream: stdout_stream) - - stdout_stream.print <<-EOH - -================================================================================ -= END '#{new_resource.name}' Jenkins job output -================================================================================ - EOH - else - executor.execute!(*command_args) - end - end - else - Chef::Log.info("#{new_resource} disabled - skipping") - end - end - - # - # Idempotently create a new Jenkins job with the current resource's name - # and configuration file. If the job already exists, no action will be - # taken. If the job does not exist, one will be created from the given - # `config` XML file using the Jenkins CLI. - # - # This method also ensures the given configuration file matches the one - # rendered on the Jenkins master. If the configuration file does not match, - # a new one is rendered. - # - # Requirements: - # - `config` parameter - # - action :create do - validate_config! - - if current_resource.exists? - Chef::Log.info("#{new_resource} exists - skipping") - else - converge_by("Create #{new_resource}") do - executor.execute!('create-job', escape(new_resource.name), '<', escape(new_resource.config)) - end - end - - if correct_config? - Chef::Log.info("#{new_resource} config up to date - skipping") - else - converge_by("Update #{new_resource} config") do - executor.execute!('update-job', escape(new_resource.name), '<', escape(new_resource.config)) - end - end - end - - # - # Idempotently delete a Jenkins job with the current resource's name. If - # the job does not exist, no action will be taken. If the job does exist, - # it will be deleted using the Jenkins CLI. - # - action :delete do - if current_resource.exists? - converge_by("Delete #{new_resource}") do - executor.execute!('delete-job', escape(new_resource.name)) - end - else - Chef::Log.info("#{new_resource} does not exist - skipping") - end - end - - # - # Disable an existing Jenkins job. - # - # @raise [JobDoesNotExist] - # if the job does not exist - # - action :disable do - raise JobDoesNotExist.new(new_resource.name, :disable) unless current_resource.exists? - - if current_resource.enabled? - converge_by("Disable #{new_resource}") do - executor.execute!('disable-job', escape(new_resource.name)) - end - else - Chef::Log.info("#{new_resource} disabled - skipping") - end - end - - # - # Enable an existing Jenkins job. - # - # @raise [JobDoesNotExist] - # if the job does not exist - # - action :enable do - raise JobDoesNotExist.new(new_resource.name, :enable) unless current_resource.exists? - - if current_resource.enabled? - Chef::Log.info("#{new_resource} enabled - skipping") - else - converge_by("Enable #{new_resource}") do - executor.execute!('enable-job', escape(new_resource.name)) - end - end - end - - private - - # - # The job in the current, in XML format. - # - # @return [nil, Hash] - # nil if the job does not exist, or a hash of important information if - # it does - # - def current_job - return @current_job if @current_job - - Chef::Log.debug "Load #{new_resource} job information" - - response = executor.execute('get-job', escape(new_resource.name)) - return if response.nil? || response =~ /No such job/ - - Chef::Log.debug "Parse #{new_resource} as XML" - xml = REXML::Document.new(response) - disabled = xml.elements['//disabled'] - - @current_job = { - enabled: disabled.nil? ? true : disabled.text == 'false', - xml: xml, - raw: response, - } - @current_job - end - - # - # Helper method for determining if the given JSON is in sync with the - # current configuration on the Jenkins master. - # - # We have to create REXML objects and then remove any whitespace because - # XML is evil and sometimes sucks at the simplest things, like comparing - # itself. - # - # @return [Boolean] - # - def correct_config? - current = StringIO.new - wanted = StringIO.new - - current_job[:xml].write(current, 2) - REXML::Document.new(::File.read(new_resource.config)).write(wanted, 2) - - current.string == wanted.string - end - - # - # Validate that a configuration file was given as a parameter to the - # resource. This method also validates the given config file path exists - # on the target node. Finally, this method reads the contents of the file - # and verifies it is valid XML. - # - def validate_config! - Chef::Log.debug "Validate #{new_resource} configuration" - - raise("#{new_resource} must specify a configuration file!") if new_resource.config.nil? - - raise("#{new_resource} config `#{new_resource.config}` does not exist!") unless ::File.exist?(new_resource.config) - - begin - REXML::Document.new(::File.read(new_resource.config)) - rescue REXML::ParseException - raise("#{new_resource} config `#{new_resource.config}` is not valid XML!") - end - end - - # Inspired by chef/chef/#4040 - def formatter? - if run_context.events.respond_to?(:subscribers) - run_context.events.subscribers.any? { |s| s.respond_to?(:is_formatter?) && s.is_formatter? } - else - false - end - end - - def stdout_stream - @stdout_stream ||= if formatter? - Chef::EventDispatch::EventsOutputStream.new(run_context.events, name: new_resource.name.to_sym) - elsif STDOUT.tty? && !Chef::Config[:daemon] - STDOUT - end - end - end -end diff --git a/libraries/plugin.rb b/libraries/plugin.rb deleted file mode 100644 index a87afc02f7..0000000000 --- a/libraries/plugin.rb +++ /dev/null @@ -1,390 +0,0 @@ -# -# Cookbook:: jenkins -# Resource:: plugin -# -# Author:: Seth Vargo -# Author:: Seth Chisamore -# -# Copyright:: 2013-2019, 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. -# - -require 'digest' - -require_relative '_helper' - -class Chef - class Resource::JenkinsPlugin < Resource::LWRPBase - resource_name :jenkins_plugin # Still needed for Chef 15 and below - provides :jenkins_plugin - - # Chef attributes - identity_attr :name - - # Actions - actions :install, :uninstall, :enable, :disable - default_action :install - - # Attributes - attribute :version, - kind_of: [String, Symbol], - default: :latest - attribute :source, - kind_of: String - # TODO: Remove in next major version release - attribute :install_deps, - kind_of: [TrueClass, FalseClass] - attribute :options, - kind_of: String - - attr_writer :installed - - # - # Determine if the plugin is installed on the master. This value is set by - # the provider when the current resource is loaded. - # - # @return [Boolean] - # - def installed? - !@installed.nil? && @installed - end - end -end - -class Chef - class Provider::JenkinsPlugin < Provider::LWRPBase - provides :jenkins_plugin - - include Jenkins::Helper - - provides :jenkins_plugin - - class PluginNotInstalled < StandardError - def initialize(plugin, action) - super <<-EOH -The Jenkins plugin `#{plugin}' is not installed. In order to #{action} -`#{plugin}', that plugin must first be installed on the Jenkins master! - EOH - end - end - - def load_current_resource - @current_resource ||= Resource::JenkinsPlugin.new(new_resource.name) - @current_resource.source(new_resource.source) - @current_resource.version(new_resource.version) - - current_plugin = plugin_installation_manifest(new_resource.name) - - if current_plugin - @current_resource.installed = true - @current_resource.version(current_plugin['plugin_version']) - else - @current_resource.installed = false - end - - @current_resource - end - - action :install do - # TODO: remove in next major version release - # Check for dependency property and give deprecation if used - if new_resource.install_deps - Chef::Log.warn('The install_deps property on the plugin provider is deprecated and not used. See Readme on how to install plugins with or without dependencies.') - end - - # This block stores the actual command to execute, since its the same - # for upgrades and installs. - install_block = proc do - # Install a plugin from a given hpi (or jpi) if a link was provided. - # In that case jenkins does not handle plugin dependencies automatically. - # Otherwise the plugin is installed through the jenkins update-center - # (default behaviour). In that case plugin dependencies are handled by jenkins. - # if installing latest version - install_plugin( - new_resource.source, - new_resource.name, - new_resource.version, - cli_opts: new_resource.options - ) - end - - downgrade_block = proc do - # remove the existing, newer version - uninstall_plugin(new_resource.name) - - # proceed with a normal install - install_block.call - end - - if current_resource.installed? - if plugin_version(current_resource.version) == desired_version - Chef::Log.info("#{new_resource} version #{current_resource.version} already installed - skipping") - else - current_version = plugin_version(current_resource.version) - unless current_version.to_s.include? 'SNAPSHOT' - if plugin_upgrade?(current_version, desired_version) # rubocop: disable Metrics/BlockNesting - converge_by("Upgrade #{new_resource} from #{current_resource.version} to #{desired_version}", &install_block) - else - converge_by("Downgrade #{new_resource} from #{current_resource.version} to #{desired_version}", &downgrade_block) - end - end - end - else - converge_by("Install #{new_resource}", &install_block) - end - end - - # - # Disable the given plugin. - # - # Disabling a plugin is a softer way to retire a plugin. Jenkins will - # continue to recognize that the plugin is installed, but it will not - # start the plugin, and no extensions contributed from this plugin will be - # visible. - # - # The fragments contributed from a disabled plugin to configuration files - # would follow the same fate as in the case of uninstalled plugins. - # - # Plugins that are disabled can be re-enabled from the UI (or by removing - # *.jpi.disabled file from the disk.) - # - action :disable do - raise PluginNotInstalled.new(new_resource.name, :disable) unless current_resource.installed? - - disabled = "#{plugin_file(new_resource.name)}.disabled" - - if ::File.exist?(disabled) - Chef::Log.debug("#{new_resource} already disabled - skipping") - else - converge_by("Disable #{new_resource}") do - Resource::File.new(disabled, run_context).run_action(:create) - end - end - end - - # - # Enable the given plugin. - # - # Enabling a plugin brings back a formerly disabled plugin. Jenkins will - # being recognizing this plugin again on the next restart. - # - # Plugins may be disabled by re-adding the +.jpi.disabled+ plugin. - # - action :enable do - raise PluginNotInstalled.new(new_resource.name, :enable) unless current_resource.installed? - - disabled = "#{plugin_file(new_resource.name)}.disabled" - - if ::File.exist?(disabled) - converge_by("Enable #{new_resource}") do - Resource::File.new(disabled, run_context).run_action(:delete) - end - else - Chef::Log.debug("#{new_resource} already enabled - skipping") - end - end - - # - # Uninstall the given plugin. - # - # Uninstalling a plugin removes the plugin binary (*.jpi) from the disk. - # The plugin continues to function normally until you restart Jenkins, but - # once you restart, Jenkins will behave as if you didn't have the plugin - # to being with. They will not appear anywhere in the UI, all the - # extensions they contributed will disappear. - # - # WARNING: Uninstalling a plugin, however, does not remove the configuration - # that the plugin might have created. If there are existing - # jobs/slaves/views/builds/etc that used some extensions from the plugin, - # during the boot Jenkins will report that there are some fragments in - # those configurations that it didn't understand, and pretend as if it - # didn't see such a fragment. - # - action :uninstall do - if current_resource.installed? - converge_by("Uninstall #{new_resource}") do - uninstall_plugin(new_resource.name) - end - else - Chef::Log.debug("#{new_resource} not installed - skipping") - end - end - - private - - def desired_version(name = nil, version = nil) - name = new_resource.name if name.nil? - version = new_resource.version if version.nil? - - if version.to_sym == :latest - remote_plugin_data = plugin_universe[name] - - return :latest unless remote_plugin_data - - plugin_version(remote_plugin_data['version']) - else - plugin_version(version) - end - end - - # - # Installs a plugin along with all of it's dependencies if version is :latest and source property is not specified. - # - # @param [String] full url of the *.hpi/*.jpi to install - # @param [String] name of the plugin to be installed - # @param [String] version of the plugin to be installed - # @param [Hash] opts the options install plugin with - # @option opts [Boolean] :cli_opts additional flags to pass the jenkins cli command - # - def install_plugin(source_url, plugin_name, plugin_version, opts = {}) - test = (source_url || plugin_version != :latest) ? true : false - if test - url = if source_url - source_url - else - remote_plugin_data = plugin_universe[plugin_name] - # Compute some versions; Parse them as `Gem::Version` instances for easy comparisons. - latest_version = plugin_version(remote_plugin_data['version']) - # Replace the latest version with the desired version in the URL - remote_plugin_data['url'].gsub!(latest_version.to_s, desired_version(plugin_name, plugin_version).to_s) - end - end - ensure_update_center_present! - executor.execute!('install-plugin', escape(test ? url : plugin_name), opts[:cli_opts]) - end - - # - # Uninstalling a plugin removes the plugin binary (*.jpi) from the disk. - # - # @param [String] name of the plugin to be uninstall - # - def uninstall_plugin(plugin_name) - file = Resource::File.new(plugin_file(plugin_name), run_context) - file.backup(false) - file.run_action(:delete) - directory = Resource::Directory.new(plugin_data_directory(plugin_name), run_context) - directory.recursive(true) - directory.run_action(:delete) - end - - # - # The path to the plugins directory on the Jenkins node. - # - # @return [String] - # - def plugins_directory - ::File.join(node['jenkins']['master']['home'], 'plugins') - end - - # - # The path to the actual plugin file on disk (+.jpi+) - # - # @param [String] name of the plugin to be installed - # @return [String] - # - def plugin_file(plugin_name) - hpi = ::File.join(plugins_directory, "#{plugin_name}.hpi") - jpi = ::File.join(plugins_directory, "#{plugin_name}.jpi") - - ::File.exist?(hpi) ? hpi : jpi - end - - # - # The path to where the plugin stores its data on disk. - # - def plugin_data_directory(plugin_name) - ::File.join(plugins_directory, plugin_name) - end - - # - # Parsed hash of all known Jenkins plugins - # - # @return [Hash] - # - def plugin_universe - @plugin_universe ||= begin - ensure_update_center_present! - JSON.parse(IO.read(extracted_update_center_json).force_encoding('UTF-8'))['plugins'] - end - end - - # - # Return the installation manifest for +plugin_name+. If the plugin is not - # installed +nil+ is returned. - # - # @param [String] name of the plugin to be installed - # @return [Hash] - # - def plugin_installation_manifest(plugin_name) - manifest = ::File.join(plugins_directory, plugin_name, 'META-INF', 'MANIFEST.MF') - Chef::Log.debug "Load #{plugin_name} plugin information from #{manifest}" - - return unless ::File.exist?(manifest) - - plugin_manifest = {} - - ::File.open(manifest, 'r', encoding: 'utf-8') do |file| - file.each_line do |line| - next if line.strip.empty? - - # - # Example Data: - # Plugin-Version: 1.4 - # - config, value = line.split(/:\s/, 2) - config = config.tr('-', '_').downcase - value = value.strip if value # remove trailing \r\n - - plugin_manifest[config] = value - end - end - - plugin_manifest - end - - # - # Return whether plugin should be upgraded to desired version - # (i.e. that current < desired). - # https://github.com/chef-cookbooks/jenkins/issues/380 - # If only one of the two versions is a Gem::Version, we - # fallback to String comparison. - # - # @param [Gem::Version, String] current_version - # @param [Gem::Version, String] desired_version - # @return [Boolean] - # - def plugin_upgrade?(current_version, desired_version) - current_version < desired_version - rescue ArgumentError - current_version.to_s < desired_version.to_s - end - - # - # Return the plugin version for +version+. - # https://github.com/chef-cookbooks/jenkins/issues/292 - # Prefer to use Gem::Version as that will be more accurate than - # comparing strings, but sadly Jenkins plugins may not always - # follow "normal" version patterns - # - # @param [String] version - # @return [String] - # - def plugin_version(version) - gem_version = Gem::Version.new(version) - gem_version.prerelease? ? version : gem_version - rescue ArgumentError - version - end - end -end diff --git a/libraries/proxy.rb b/libraries/proxy.rb deleted file mode 100644 index b836fac624..0000000000 --- a/libraries/proxy.rb +++ /dev/null @@ -1,184 +0,0 @@ -# -# Cookbook:: jenkins -# Resource:: proxy -# -# Author:: Stephan Linz -# -# Copyright:: 2014-2019, Li-Pro.Net -# -# 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. -# - -require 'json' - -require_relative '_helper' -require_relative '_params_validate' - -class Chef - class Resource::JenkinsProxy < Resource::LWRPBase - resource_name :jenkins_proxy # Still needed for Chef 15 and below - provides :jenkins_proxy - - # Chef attributes - identity_attr :proxy - - # Actions - actions :config, :remove - default_action :config - - # Attributes - attribute :proxy, - kind_of: String, - name_attribute: true - attribute :noproxy, - kind_of: Array, - default: [] - attribute :username, - kind_of: String - - attribute :password, - kind_of: String - - attr_writer :configured - - # - # Determine if the proxy is configured on the master. This value is set by - # the provider when the current resource is loaded. - # - # @return [Boolean] - # - def configured? - !@configured.nil? && @configured - end - end -end - -class Chef - class Provider::JenkinsProxy < Provider::LWRPBase - include Jenkins::Helper - - provides :jenkins_proxy - - def load_current_resource - @current_resource ||= Resource::JenkinsProxy.new(new_resource.proxy) - - if current_proxy - @current_resource.configured = true - @current_resource.proxy(current_proxy[:proxy]) - @current_resource.noproxy(current_proxy[:username]) - @current_resource.noproxy(current_proxy[:password]) - @current_resource.noproxy(current_proxy[:noproxy]) - end - - @current_resource - end - - action(:config) do - if current_resource.configured? && - current_resource.proxy == new_resource.proxy && - current_resource.username == new_resource.username && - current_resource.password == new_resource.password && - current_resource.noproxy == new_resource.noproxy - Chef::Log.info("#{new_resource} already configured - skipping") - else - name, port = new_resource.proxy.split(':') - if name && port && port.to_i > 0 - converge_by("Configure #{new_resource}") do - executor.groovy! <<-EOH.gsub(/^ {14}/, '') - name = #{convert_to_groovy(name)} - port = #{convert_to_groovy(port.to_i)} - username = #{convert_to_groovy(username)} - password = #{convert_to_groovy(password)} - noproxy = '#{new_resource.noproxy.join('\n')}' - - import hudson.ProxyConfiguration - def pc = new ProxyConfiguration(name, port, username, password, noproxy) - pc.save() - - import jenkins.model.Jenkins - def instance = Jenkins.getInstance() - instance.proxy = pc.load() - EOH - end - else - Chef::Log.debug("#{new_resource} incorrect format - skipping") - end - end - end - - action(:remove) do - if current_resource.configured? - converge_by("Remove #{new_resource}") do - executor.groovy! <<-EOH.gsub(/^ {12}/, '') - import jenkins.model.Jenkins - def instance = Jenkins.getInstance() - - def pc = instance.proxy - if (pc == null) { - return null - } - - pc.getXmlFile().delete() - instance.proxy = pc.load() - EOH - end - else - Chef::Log.debug("#{new_resource} does not exist - skipping") - end - end - - private - - # - # Loads the local proxy into a hash - # - def current_proxy - return @current_proxy if @current_proxy - - Chef::Log.debug "Load #{new_resource} proxy information" - - json = executor.groovy <<-EOH.gsub(/^ {8}/, '') - import java.util.Collections - import java.util.List - - import jenkins.model.Jenkins - def instance = Jenkins.getInstance() - - def pc = instance.proxy - if (pc == null) { - return null - } - - def no_proxy = pc.noProxyHost - if (no_proxy != null) { - no_proxy = no_proxy.tokenize('[ \\t\\n,|]+') - } else { - no_proxy = Collections.emptyList() - } - - def builder = new groovy.json.JsonBuilder() - builder { - proxy pc.name + ':' + pc.port.toString() - noproxy no_proxy - } - - println(builder) - EOH - - return if json.nil? || json.empty? - - @current_proxy = JSON.parse(json, symbolize_names: true) - @current_proxy - end - end -end diff --git a/libraries/script.rb b/libraries/script.rb deleted file mode 100644 index 4dd59b96e7..0000000000 --- a/libraries/script.rb +++ /dev/null @@ -1,71 +0,0 @@ -# -# Cookbook:: jenkins -# Resource:: script -# -# Author:: Seth Vargo -# -# Copyright:: 2014-2019, 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. -# - -require_relative 'command' - -class Chef - class Resource::JenkinsScript < Resource::JenkinsCommand - resource_name :jenkins_script # Still needed for Chef 15 and below - provides :jenkins_script - - # Chef attributes - identity_attr :name - - attribute :groovy_path, - kind_of: String, - default: nil - attribute :name, - kind_of: String, - name_attribute: true, - required: false - - # Actions - actions :execute - default_action :execute - end -end - -class Chef - class Provider::JenkinsScript < Provider::JenkinsCommand - provides :jenkins_script - - def load_current_resource - if new_resource.groovy_path - @current_resource ||= Resource::JenkinsScript.new(new_resource.name) - @current_resource.name(new_resource.name) - @current_resource.groovy_path(new_resource.groovy_path) - else - @current_resource ||= Resource::JenkinsScript.new(new_resource.command) - end - super - end - - action :execute do - converge_by("Execute script #{new_resource}") do - if new_resource.groovy_path - executor.groovy_from_file!(new_resource.groovy_path) - else - executor.groovy!(new_resource.command) - end - end - end - end -end diff --git a/libraries/slave.rb b/libraries/slave.rb deleted file mode 100644 index 58238b815a..0000000000 --- a/libraries/slave.rb +++ /dev/null @@ -1,413 +0,0 @@ -# -# Cookbook:: jenkins -# Resource:: slave -# -# Author:: Seth Chisamore -# -# Copyright:: 2013-2019, 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. -# - -require 'json' - -require_relative '_helper' - -class Chef - class Resource::JenkinsSlave < Resource::LWRPBase - resource_name :jenkins_slave # Still needed for Chef 15 and below - provides :jenkins_slave - - # Chef attributes - identity_attr :slave_name - - # Actions - actions :create, :delete, :connect, :disconnect, :online, :offline - default_action :create - - # Attributes - attribute :slave_name, - kind_of: String, - name_attribute: true - attribute :description, - kind_of: String, - default: lazy { |new_resource| "Jenkins slave #{new_resource.slave_name}" } - attribute :remote_fs, - kind_of: String, - default: '/home/jenkins' - attribute :executors, - kind_of: Integer, - default: 1 - attribute :usage_mode, - kind_of: String, - equal_to: %w(exclusive normal), - default: 'normal' - attribute :labels, - kind_of: Array, - default: [] - attribute :availability, - kind_of: String, - equal_to: %w(always demand) - attribute :in_demand_delay, - kind_of: Integer, - default: 0 - attribute :idle_delay, - kind_of: Integer, - default: 1 - attribute :environment, - kind_of: Hash - attribute :offline_reason, - kind_of: String - attribute :user, - kind_of: String, - regex: Config[:user_valid_regex], - default: 'jenkins' - attribute :jvm_options, - kind_of: String - attribute :java_path, - kind_of: String - - attr_writer :exists - attr_writer :connected - attr_writer :online - - # - # Determine if the slave exists on the master. This value is set by - # the provider when the current resource is loaded. - # - # @return [Boolean] - # - def exists? - !@exists.nil? && @exists - end - - # - # Determine if the slave is connected to the master. This value is - # set by the provider when the current resource is loaded. - # - # @return [Boolean] - # - def connected? - !@connected.nil? && @connected - end - - # - # Determine if the slave is online. This value is set by the - # provider when the current resource is loaded. - # - # @return [Boolean] - # - def online? - !@online.nil? && @online - end - end -end - -class Chef - class Provider::JenkinsSlave < Provider::LWRPBase - provides :jenkins_slave - - include Jenkins::Helper - - provides :jenkins_slave - - def load_current_resource - @current_resource ||= Resource::JenkinsSlave.new(new_resource.name) - - if current_slave - @current_resource.exists = true - @current_resource.connected = current_slave[:connected] - @current_resource.online = current_slave[:online] - - @current_resource.slave_name(new_resource.slave_name) - @current_resource.description(current_slave[:description]) - @current_resource.remote_fs(current_slave[:remote_fs]) - @current_resource.executors(current_slave[:executors]) - @current_resource.labels(current_slave[:labels]) - end - - @current_resource - end - - action :create do - do_create - end - - def merge_preserved_labels! - new_resource.labels |= current_resource.labels.select { |i| i[/^prsrv_/] } - end - - def do_create - # Preserve some labels... - merge_preserved_labels! - if current_resource.exists? && correct_config? - Chef::Log.info("#{new_resource} exists - skipping") - else - converge_by("Create #{new_resource}") do - executor.groovy! <<-EOH.gsub(/^ {12}/, '') - import hudson.model.* - import hudson.slaves.* - import jenkins.model.* - import jenkins.slaves.* - - props = [] - availability = #{convert_to_groovy(new_resource.availability)} - usage_mode = #{convert_to_groovy(new_resource.usage_mode)} - env_map = #{convert_to_groovy(new_resource.environment)} - labels = #{convert_to_groovy(new_resource.labels.sort.join(' '))} - - // Compute the usage mode - if (usage_mode == 'normal') { - mode = Node.Mode.NORMAL - } else { - mode = Node.Mode.EXCLUSIVE - } - - // Compute the retention strategy - if (availability == 'demand') { - retention_strategy = - new RetentionStrategy.Demand( - #{new_resource.in_demand_delay}, - #{new_resource.idle_delay} - ) - } else if (availability == 'always') { - retention_strategy = new RetentionStrategy.Always() - } else { - retention_strategy = RetentionStrategy.NOOP - } - - // Create an entry in the prop list for all env vars - if (env_map != null) { - env_vars = new hudson.EnvVars(env_map) - entries = env_vars.collect { - k,v -> new EnvironmentVariablesNodeProperty.Entry(k,v) - } - props << new EnvironmentVariablesNodeProperty(entries) - } - - // Launcher - #{launcher_groovy} - - // Build the slave object - slave = new DumbSlave( - #{convert_to_groovy(new_resource.name)}, - #{convert_to_groovy(new_resource.description)}, - #{convert_to_groovy(new_resource.remote_fs)}, - #{convert_to_groovy(new_resource.executors.to_s)}, - mode, - labels, - launcher, - retention_strategy, - props - ) - - // Create or update the slave in the Jenkins instance - nodes = new ArrayList(Jenkins.instance.getNodes()) - ix = nodes.indexOf(slave) - (ix >= 0) ? nodes.set(ix, slave) : nodes.add(slave) - Jenkins.instance.setNodes(nodes) - EOH - end - end - end - - action :delete do - do_delete - end - - def do_delete - if current_resource.exists? - converge_by("Delete #{new_resource}") do - executor.execute!('delete-node', escape(new_resource.slave_name)) - end - else - Chef::Log.debug("#{new_resource} does not exist - skipping") - end - end - - action :connect do - if current_resource.exists? && current_resource.connected? - Chef::Log.debug("#{new_resource} already connected - skipping") - else - converge_by("Connect #{new_resource}") do - executor.execute!('connect-node', escape(new_resource.slave_name)) - end - end - end - - action :disconnect do - if current_resource.connected? - converge_by("Disconnect #{new_resource}") do - executor.execute!('disconnect-node', escape(new_resource.slave_name)) - end - else - Chef::Log.debug("#{new_resource} already disconnected - skipping") - end - end - - action :online do - if current_resource.exists? && current_resource.online? - Chef::Log.debug("#{new_resource} already online - skipping") - else - converge_by("Online #{new_resource}") do - executor.execute!('online-node', escape(new_resource.slave_name)) - end - end - end - - action :offline do - if current_resource.online? - converge_by("Offline #{new_resource}") do - command_pieces = [escape(new_resource.slave_name)] - command_pieces << "-m '#{escape(new_resource.offline_reason)}'" if new_resource.offline_reason - executor.execute!('offline-node', command_pieces) - end - else - Chef::Log.debug("#{new_resource} already offline - skipping") - end - end - - private - - # - # Returns a Groovy snippet that creates an instance of the slave's - # launcher implementation. The launcher instance should be set to - # a Groovy variable named `launcher`. - # - # @return [String] - # - def launcher_groovy - 'launcher = new hudson.slaves.JNLPLauncher()' - end - - # - # Maps a slave's resource attribute name to the equivalent property - # in the Groovy representation. This mapping is useful in - # Ruby/Groovy serialization/deserialization. - # - # @return [Hash] - # - # @example - # {host: 'host', - # port: 'port', - # credential_username: 'username', - # jvm_options: 'jvmOptions'} - # - def attribute_to_property_map - {} - end - - # - # Loads the current slave into a Hash. - # - def current_slave - return @current_slave if @current_slave - - Chef::Log.debug "Load #{new_resource} slave information" - - launcher_attributes = [] - attribute_to_property_map.each_pair do |resource_attribute, groovy_property| - launcher_attributes << "current_slave['#{resource_attribute}'] = #{groovy_property}" - end - - json = executor.groovy! <<-EOH.gsub(/^ {8}/, '') - import hudson.model.* - import hudson.slaves.* - import jenkins.model.* - import jenkins.slaves.* - - slave = Jenkins.instance.getNode('#{new_resource.slave_name}') as Slave - - if(slave == null) { - return null - } - - def slave_environment = null - slave_env_vars = slave.nodeProperties.get(EnvironmentVariablesNodeProperty.class)?.envVars - if (slave_env_vars) - slave_environment = new java.util.HashMap(slave_env_vars) - - current_slave = [ - name:slave.name, - description:slave.nodeDescription, - remote_fs:slave.remoteFS, - executors:slave.numExecutors.toInteger(), - usage_mode:slave.mode.toString().toLowerCase(), - labels:slave.labelString.split().sort(), - environment:slave_environment, - connected:(slave.computer.connectTime > 0), - online:slave.computer.online - ] - - // Determine retention strategy - if (slave.retentionStrategy instanceof RetentionStrategy.Always) { - current_slave['availability'] = 'always' - } else if (slave.retentionStrategy instanceof RetentionStrategy.Demand) { - current_slave['availability'] = 'demand' - retention = slave.retentionStrategy as RetentionStrategy.Demand - current_slave['in_demand_delay'] = retention.inDemandDelay - current_slave['idle_delay'] = retention.idleDelay - } else { - current_slave['availability'] = null - } - - #{launcher_attributes.join("\n")} - - builder = new groovy.json.JsonBuilder(current_slave) - println(builder) - EOH - - return if json.nil? || json.empty? - - @current_slave = JSON.parse(json, symbolize_names: true) - - # Values that were serialized as nil/null are deserialized as an - # empty string! :( Let's ensure we convert back to nil. - @current_slave = convert_blank_values_to_nil(@current_slave) - end - - # - # Helper method for determining if the given JSON is in sync with the - # current configuration on the Jenkins master. - # - # @return [Boolean] - # - def correct_config? - wanted_slave = { - name: new_resource.slave_name, - description: new_resource.description, - remote_fs: new_resource.remote_fs, - executors: new_resource.executors, - usage_mode: new_resource.usage_mode, - labels: new_resource.labels.sort, - availability: new_resource.availability, - environment: new_resource.environment, - } - - if new_resource.availability.to_s == 'demand' - wanted_slave[:in_demand_delay] = new_resource.in_demand_delay - wanted_slave[:idle_delay] = new_resource.idle_delay - end - - attribute_to_property_map.each_key do |key| - wanted_slave[key] = new_resource.send(key) - end - - # Don't include connected/online values in the comparison - current_slave.dup.tap do |c| - c.delete(:connected) - c.delete(:online) - end == wanted_slave - end - end -end diff --git a/libraries/slave_jnlp.rb b/libraries/slave_jnlp.rb deleted file mode 100644 index 48cbec3596..0000000000 --- a/libraries/slave_jnlp.rb +++ /dev/null @@ -1,225 +0,0 @@ -# -# Cookbook:: jenkins -# Resource:: jnlp_slave -# -# Author:: Seth Chisamore -# -# Copyright:: 2013-2019, 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. -# - -require_relative 'slave' - -class Chef - class Resource::JenkinsJnlpSlave < Resource::JenkinsSlave - resource_name :jenkins_jnlp_slave # Still needed for Chef 15 and below - provides :jenkins_jnlp_slave - - # Actions - actions :create, :delete, :connect, :disconnect, :online, :offline - default_action :create - - # Properties - property :group, String, - default: 'jenkins', - regex: Config[:group_valid_regex] - - property :service_name, String, - default: 'jenkins-slave' - - property :service_groups, Array, - default: lazy { [group] } - - deprecated_property_alias 'runit_groups', 'service_groups', - '`runit_groups` was renamed to `service_groups` with the move to systemd services' - end -end - -class Chef - class Provider::JenkinsJnlpSlave < Provider::JenkinsSlave - provides :jenkins_jnlp_slave - - def load_current_resource - @current_resource ||= Resource::JenkinsJnlpSlave.new(new_resource.name) - - super - end - - action :create do - do_create - - declare_resource(:directory, ::File.expand_path(new_resource.remote_fs, '..')) do - recursive(true) - action :create - end - - unless platform?('windows') - declare_resource(:group, new_resource.group) do - system(node['jenkins']['master']['use_system_accounts']) - end - - declare_resource(:user, new_resource.user) do - gid(new_resource.group) - comment('Jenkins slave user - Created by Chef') - home(new_resource.remote_fs) - system(node['jenkins']['master']['use_system_accounts']) - action :create - end - end - - declare_resource(:directory, new_resource.remote_fs) do - owner(new_resource.user) - group(new_resource.group) - recursive(true) - action :create - end - - declare_resource(:remote_file, slave_jar).tap do |r| - # We need to use .tap() to access methods in the provider's scope. - r.source slave_jar_url - r.backup(false) - r.mode('0755') - r.atomic_update(false) - r.notifies :restart, "systemd_unit[#{new_resource.service_name}.service]" unless platform?('windows') - end - - # The Windows's specific child class manages it's own service - return if platform?('windows') - - # disable runit services before starting new service - # TODO: remove in future version - - %W( - /etc/init.d/#{new_resource.service_name} - /etc/service/#{new_resource.service_name} - ).each do |f| - file f do - action :delete - notifies :stop, "service[#{new_resource.service_name}]", :before - end - end - - # runit_service = if platform_family?('debian') - # 'runit' - # else - # 'runsvdir-start' - # end - # service runit_service do - # action [:stop, :disable] - # end - - exec_string = "#{java} #{new_resource.jvm_options}" - exec_string << " -jar #{slave_jar}" if slave_jar - exec_string << " -secret #{jnlp_secret}" if jnlp_secret - exec_string << " -jnlpUrl #{jnlp_url}" - - systemd_unit "#{new_resource.service_name}.service" do - content <<~EOU - # - # Generated by Chef for #{node['fqdn']} - # Changes will be overwritten! - # - - [Unit] - Description=Jenkins JNLP Slave (#{new_resource.service_name}) - After=network.target - - [Service] - Type=simple - User=#{new_resource.user} - Group=#{new_resource.group} - SupplementaryGroups=#{(new_resource.service_groups - [new_resource.group]).join(' ')} - Environment="HOME=#{new_resource.remote_fs}" - Environment="JENKINS_HOME=#{new_resource.remote_fs}" - WorkingDirectory=#{new_resource.remote_fs} - ExecStart=#{exec_string} - - [Install] - WantedBy=multi-user.target - EOU - action :create - end - - service new_resource.service_name do - action [:enable, :start] - end - end - - action :delete do - # Stop and remove the service - service "#{new_resource.service_name}" do - action [:disable, :stop] - end - - do_delete - end - - private - - # - # @see Chef::Resource::JenkinsSlave#launcher_groovy - # @see http://javadoc.jenkins-ci.org/hudson/slaves/JNLPLauncher.html - # - def launcher_groovy - 'launcher = new hudson.slaves.JNLPLauncher()' - end - - # - # The path (url) of the slave's unique JNLP file on the Jenkins - # master. - # - # @return [String] - # - def jnlp_url - @jnlp_url ||= uri_join(endpoint, 'computer', new_resource.slave_name, 'slave-agent.jnlp') - end - - # - # Generates the slaves unique JNLP secret using the Groovy API. - # - # @return [String] - # - def jnlp_secret - return @jnlp_secret if @jnlp_secret - json = executor.groovy! <<~EOH - output = [ - secret:jenkins.slaves.JnlpSlaveAgentProtocol.SLAVE_SECRET.mac('#{new_resource.slave_name}') - ] - - builder = new groovy.json.JsonBuilder(output) - println(builder) - EOH - output = JSON.parse(json, symbolize_names: true) - @jnlp_secret = output[:secret] - end - - # - # The url of the +slave.jar+ on the Jenkins master. - # - # @return [String] - # - def slave_jar_url - @slave_jar_url ||= uri_join(endpoint, 'jnlpJars', 'slave.jar') - end - - # - # The path to the +slave.jar+ on disk (which may or may not exist). - # - # @return [String] - # - def slave_jar - ::File.join(new_resource.remote_fs, 'slave.jar') - end - end -end diff --git a/libraries/slave_ssh.rb b/libraries/slave_ssh.rb deleted file mode 100644 index b827e6ca97..0000000000 --- a/libraries/slave_ssh.rb +++ /dev/null @@ -1,159 +0,0 @@ -# -# Cookbook:: jenkins -# Resource:: ssh_slave -# -# Author:: Seth Chisamore -# -# Copyright:: 2013-2019, 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. -# - -require_relative 'slave' -require_relative 'credentials' - -class Chef - class Resource::JenkinsSshSlave < Resource::JenkinsSlave - resource_name :jenkins_ssh_slave # Still needed for Chef 15 and below - provides :jenkins_ssh_slave - - # Actions - actions :create, :delete, :connect, :disconnect, :online, :offline - default_action :create - - # Attributes - attribute :host, - kind_of: String - attribute :port, - kind_of: Integer, - default: 22 - attribute :credentials, - kind_of: [String, Resource::JenkinsCredentials] - attribute :command_prefix, - kind_of: String - attribute :command_suffix, - kind_of: String - attribute :launch_timeout, - kind_of: Integer - attribute :ssh_retries, - kind_of: Integer - attribute :ssh_wait_retries, - kind_of: Integer - - # - # The credentials to SSH into the slave with. Credentials can be any - # of the following: - # - # * username which maps to a valid Jenkins credentials instance. - # * UUID of a Jenkins credentials instance. - # * A `Chef::Resource::JenkinsCredentials` instance. - # - # @return [String] - # - def parsed_credentials - if credentials.is_a?(Resource::JenkinsCredentials) - credentials.send(:id) - else - credentials.to_s - end - end - end -end - -class Chef - class Provider::JenkinsSshSlave < Provider::JenkinsSlave - provides :jenkins_ssh_slave - - def load_current_resource - @current_resource ||= Resource::JenkinsSshSlave.new(new_resource.name) - - super - - if current_slave - @current_resource.host(current_slave[:host]) - @current_resource.port(current_slave[:port]) - @current_resource.credentials(current_slave[:credentials]) - @current_resource.jvm_options(current_slave[:jvm_options]) - @current_resource.java_path(current_slave[:java_path]) - @current_resource.launch_timeout(current_slave[:launch_timeout]) - @current_resource.ssh_retries(current_slave[:ssh_retries]) - @current_resource.ssh_wait_retries(current_slave[:ssh_wait_retries]) - end - - @current_resource - end - - private - - # - # @see Chef::Resource::JenkinsSlave#launcher_groovy - # @see https://github.com/jenkinsci/ssh-credentials-plugin/blob/master/src/main/java/com/cloudbees/jenkins/plugins/sshcredentials/impl/BasicSSHUserPrivateKey.java - # @see https://github.com/jenkinsci/ssh-slaves-plugin/blob/master/src/main/java/hudson/plugins/sshslaves/SSHLauncher.java - # - def launcher_groovy - <<-EOH.gsub(/^ {8}/, '') - import hudson.plugins.sshslaves.verifiers.* - #{credential_lookup_groovy('credentials')} - launcher = - new hudson.plugins.sshslaves.SSHLauncher( - #{convert_to_groovy(new_resource.host)}, - #{convert_to_groovy(new_resource.port)}, - #{convert_to_groovy(new_resource.credentials)}, - #{convert_to_groovy(new_resource.jvm_options)}, - #{convert_to_groovy(new_resource.java_path)}, - #{convert_to_groovy(new_resource.command_prefix)}, - #{convert_to_groovy(new_resource.command_suffix)}, - #{convert_to_groovy(new_resource.launch_timeout)}, - #{convert_to_groovy(new_resource.ssh_retries)}, - #{convert_to_groovy(new_resource.ssh_wait_retries)}, - new ManuallyTrustedKeyVerificationStrategy(false) - ) - EOH - end - - # - # @see Chef::Resource::JenkinsSlave#attribute_to_property_map - # - def attribute_to_property_map - map = { - host: 'slave.launcher.host', - port: 'slave.launcher.port', - jvm_options: 'slave.launcher.jvmOptions', - java_path: 'slave.launcher.javaPath', - command_prefix: 'slave.launcher.prefixStartSlaveCmd', - command_suffix: 'slave.launcher.suffixStartSlaveCmd', - launch_timeout: 'slave.launcher.launchTimeoutSeconds', - ssh_retries: 'slave.launcher.maxNumRetries', - ssh_wait_retries: 'slave.launcher.retryWaitTime', - } - - map[:credentials] = 'slave.launcher.credentialsId' - - map - end - - # - # A Groovy snippet that will set the requested local Groovy variable - # to an instance of the credentials represented by - # `new_resource.parsed_credentials`. - # - # @param [String] groovy_variable_name - # @return [String] - # - def credential_lookup_groovy(groovy_variable_name = 'credentials_id') - <<-EOH.gsub(/^ {8}/, '') - #{credentials_for_id_groovy(new_resource.parsed_credentials, groovy_variable_name)} - EOH - end - end -end diff --git a/libraries/user.rb b/libraries/user.rb deleted file mode 100644 index eb2cc3f252..0000000000 --- a/libraries/user.rb +++ /dev/null @@ -1,179 +0,0 @@ -# -# Cookbook:: jenkins -# Resource:: user -# -# Author:: Seth Vargo -# -# Copyright:: 2013-2019, 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. -# - -require 'json' - -require_relative '_helper' - -class Chef - class Resource::JenkinsUser < Resource::LWRPBase - resource_name :jenkins_user # Still needed for Chef 15 and below - provides :jenkins_user - - # Chef attributes - identity_attr :id - - # Actions - actions :create, :delete - default_action :create - - # Attributes - attribute :id, - kind_of: String, - name_attribute: true - attribute :full_name, - kind_of: String - attribute :email, - kind_of: String - attribute :public_keys, - kind_of: Array, - default: [] - attribute :password, - kind_of: String - - attr_writer :exists - - # - # Determine if the user exists on the master. This value is set by - # the provider when the current resource is loaded. - # - # @return [Boolean] - # - def exists? - !@exists.nil? && @exists - end - end -end - -class Chef - class Provider::JenkinsUser < Provider::LWRPBase - provides :jenkins_user - - include Jenkins::Helper - - provides :jenkins_user - - def load_current_resource - @current_resource ||= Resource::JenkinsUser.new(new_resource.id) - - if current_user - @current_resource.exists = true - @current_resource.full_name(current_user[:full_name]) - @current_resource.email(current_user[:email]) - @current_resource.public_keys(current_user[:public_keys]) - end - - @current_resource - end - - action :create do - if current_resource.exists? && - (new_resource.full_name.nil? || current_resource.full_name == new_resource.full_name) && - (new_resource.email.nil? || current_resource.email == new_resource.email) && - current_resource.public_keys == new_resource.public_keys - Chef::Log.info("#{new_resource} exists - skipping") - else - converge_by("Create #{new_resource}") do - executor.groovy! <<-EOH.gsub(/^ {12}/, '') - user = hudson.model.User.get('#{new_resource.id}') - user.setFullName('#{new_resource.full_name}') - - if (jenkins.model.Jenkins.instance.pluginManager.getPlugin('mailer')) { - propertyClass = this.class.classLoader.loadClass('hudson.tasks.Mailer$UserProperty') - email = propertyClass.newInstance('#{new_resource.email}') - user.addProperty(email) - } - - password = hudson.security.HudsonPrivateSecurityRealm.Details.fromPlainPassword('#{new_resource.password}') - user.addProperty(password) - - keys = new org.jenkinsci.main.modules.cli.auth.ssh.UserPropertyImpl('#{new_resource.public_keys.join('\n')}') - user.addProperty(keys) - - user.save() - EOH - end - end - end - - action :delete do - if current_resource.exists? - converge_by("Delete #{new_resource}") do - executor.groovy! <<-EOH.gsub(/^ {12}/, '') - user = hudson.model.User.get('#{new_resource.id}', false) - user.delete() - EOH - end - else - Chef::Log.debug("#{new_resource} does not exist - skipping") - end - end - - private - - # - # Loads the local user into a hash - # - def current_user - return @current_user if @current_user - - Chef::Log.debug "Load #{new_resource} user information" - - json = executor.groovy <<-EOH.gsub(/^ {8}/, '') - user = hudson.model.User.get('#{new_resource.id}', false) - - if(user == null) { - return null - } - - id = user.getId() - name = user.getFullName() - - email = null - emailProperty = user.getProperty(hudson.tasks.Mailer.UserProperty) - if(emailProperty != null) { - email = emailProperty.getAddress() - } - - keys = null - keysProperty = user.getProperty(org.jenkinsci.main.modules.cli.auth.ssh.UserPropertyImpl) - if(keysProperty != null) { - keys = keysProperty.authorizedKeys.split('\\n') - "" // Remove empty strings - } - - builder = new groovy.json.JsonBuilder() - builder { - id id - full_name name - email email - public_keys keys - } - - println(builder) - EOH - - return if json.nil? || json.empty? - - @current_user = JSON.parse(json, symbolize_names: true) - @current_user - end - end -end diff --git a/libraries/view.rb b/libraries/view.rb deleted file mode 100644 index f2a65eb4da..0000000000 --- a/libraries/view.rb +++ /dev/null @@ -1,201 +0,0 @@ -# -# Cookbook:: jenkins -# Resource:: view -# -# Author:: Dan Fruehauf -# -# 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. -# - -require_relative '_helper' -require_relative '_params_validate' - -class Chef - class Resource::JenkinsView < Resource::LWRPBase - resource_name :jenkins_view # Still needed for Chef 15 and below - provides :jenkins_view - - # Chef attributes - identity_attr :name - - # Actions - actions :create, :delete - default_action :create - - # Attributes - attribute :jobs, - kind_of: Array, - default: [] - attribute :code, - kind_of: String, - default: '', - required: false - - attr_writer :exists - - # - # Determine if the view exists on the master. This value is set by the - # provider when the current resource is loaded. - # - # @return [Boolean] - # - def exists? - !@exists.nil? && @exists - end - end -end - -class Chef - class Provider::JenkinsView < Provider::LWRPBase - class ViewDoesNotExist < StandardError - def initialize(view, action) - super <<-EOH -The Jenkins view `#{view}' does not exist. In order to #{action} `#{view}', that -view must first exist on the Jenkins master! - EOH - end - end - - include Jenkins::Helper - - provides :jenkins_view - - def load_current_resource - @current_resource ||= Resource::JenkinsView.new(new_resource.name) - @current_resource.name(new_resource.name) - @current_resource.jobs(new_resource.jobs) - - @current_resource.exists = if current_view - true - else - false - end - - @current_resource - end - - # - # Idempotently create a new Jenkins view with the current resource's name - # and given configuration. If the view already exists, update it. If the - # view does not exist, one will be created from the given # `config` XML - # file using the Jenkins CLI. - # - # If `code` is passed then the view is not necessarily created idempotently - # as we cannot guarantee what the user has in mind - # - action(:create) do - current_view_jobs = current_view[:jobs] - current_view_jobs ||= [] - - if current_resource.exists? && - current_view_jobs == new_resource.jobs && - new_resource.code == '' - Chef::Log.debug("#{new_resource} exists - skipping") - else - jobs_to_remove = current_view_jobs - new_resource.jobs - jobs_to_add = new_resource.jobs - current_view_jobs - - create_view = - <<-GROOVY - import hudson.model.* - import jenkins.model.* - def view_name = '#{new_resource.name}' - def jenkins = Jenkins.instance - - def create_view = { name -> - return new ListView(name) - } - - def configure_view = { view -> - #{jobs_to_remove}.each { view.remove(jenkins.getItem(it)) } - #{jobs_to_add}.each { view.add(jenkins.getItem(it)) } - } - - #{new_resource.code} - - def view = jenkins.getView(view_name) - if (!view) { - view = create_view(view_name) - jenkins.addView(view) - } - configure_view(view) - - jenkins.save() - GROOVY - - converge_by("Create #{new_resource}") do - executor.groovy!(create_view) - end - end - end - - # - # Idempotently delete a Jenkins view with the current resource's name. If - # the view does not exist, no action will be taken. If the view does exist, - # it will be deleted using the Jenkins CLI. - # - action(:delete) do - if current_resource.exists? - converge_by("Delete #{new_resource}") do - executor.execute!('delete-view', escape(new_resource.name)) - end - else - Chef::Log.debug("#{new_resource} does not exist - skipping") - end - end - - private - - # - # The view in a hash format - # - # @return [Hash] - # Empty hash if the job does not exist, or a hash of important information - # if it does - # - def current_view - return @current_view if @current_view - - Chef::Log.debug "Load #{new_resource} view information" - - get_view_as_json = - <<-GROOVY - import hudson.model.* - import jenkins.model.* - view_name = '#{new_resource.name}' - jenkins = Jenkins.instance - - def view_variables = new groovy.json.JsonBuilder() - view_variables {} - - // Output view as JSON, easily parse-able by ruby - view = jenkins.getView(view_name) - if (view) { - view_variables { - name view_name - jobs view.getItems().collect { it.name } - } - } - - println view_variables.toString() - GROOVY - - response = executor.groovy!(get_view_as_json) - return if response.nil? - - Chef::Log.debug "Parse #{new_resource} as JSON" - @current_view = JSON.parse(response, object_class: Mash) - @current_view - end - end -end diff --git a/metadata.rb b/metadata.rb index 5ef5c754a7..6d94f410be 100644 --- a/metadata.rb +++ b/metadata.rb @@ -6,7 +6,7 @@ version '9.5.4' source_url 'https://github.com/sous-chefs/jenkins' issues_url 'https://github.com/sous-chefs/jenkins/issues' -chef_version '>= 13.0' +chef_version '>= 16' supports 'amazon' supports 'centos' diff --git a/resources/command.rb b/resources/command.rb new file mode 100644 index 0000000000..4a1a1b2778 --- /dev/null +++ b/resources/command.rb @@ -0,0 +1,17 @@ +unified_mode true + +property :command, + String, + name_property: true + +include Jenkins::Helper + +def load_current_resource + @current_resource ||= Resource::JenkinsCommand.new(new_resource.command) +end + +action :execute do + converge_by("Execute #{new_resource}") do + executor.execute!(new_resource.command) + end +end diff --git a/resources/credentials.rb b/resources/credentials.rb new file mode 100644 index 0000000000..c5bb5bf48b --- /dev/null +++ b/resources/credentials.rb @@ -0,0 +1,184 @@ +unified_mode true + +require 'json' +require 'openssl' +require 'securerandom' + +include Jenkins::Helper +use 'partials/_credentials' + +def load_current_resource + @current_resource ||= Resource::JenkinsCredentials.new(new_resource.name) + + if current_credentials + @current_resource.exists = true + @current_resource.id(current_credentials[:id]) + @current_resource.description(current_credentials[:description]) + end + + @current_resource +end + +action :create do + if current_resource.exists? && correct_config? + Chef::Log.info("#{new_resource} exists - skipping") + else + converge_by("Create #{new_resource}") do + executor.groovy! <<-EOH.gsub(/^ {12}/, '') + import jenkins.model.* + import com.cloudbees.plugins.credentials.* + import com.cloudbees.plugins.credentials.domains.* + import hudson.plugins.sshslaves.*; + + global_domain = Domain.global() + credentials_store = + Jenkins.instance.getExtensionList( + 'com.cloudbees.plugins.credentials.SystemCredentialsProvider' + )[0].getStore() + + #{credentials_groovy} + + #{fetch_existing_credentials_groovy('existing_credentials')} + + if(existing_credentials != null) { + credentials_store.updateCredentials( + global_domain, + existing_credentials, + credentials + ) + } else { + credentials_store.addCredentials(global_domain, credentials) + } + EOH + end + end +end + +action :delete do + if current_resource.exists? + converge_by("Delete #{new_resource}") do + executor.groovy! <<-EOH.gsub(/^ {12}/, '') + import jenkins.model.* + import com.cloudbees.plugins.credentials.*; + + global_domain = com.cloudbees.plugins.credentials.domains.Domain.global() + credentials_store = + Jenkins.instance.getExtensionList( + 'com.cloudbees.plugins.credentials.SystemCredentialsProvider' + )[0].getStore() + + #{fetch_existing_credentials_groovy('existing_credentials')} + + if(existing_credentials != null) { + credentials_store.removeCredentials( + global_domain, + existing_credentials + ) + } + EOH + end + else + Chef::Log.debug("#{new_resource} does not exist - skipping") + end +end + +action_class do + # + # Returns a Groovy snippet that creates an instance of the + # credential's implementation. The credentials instance should be + # set to a Groovy variable named `credentials`. + # + # @abstract + # @return [String] + # + def credentials_groovy + raise NotImplementedError, 'You must implement #credentials_groovy.' + end + + # + # Returns a Groovy snippet that fetches credentials from the + # credentials store. The snippet relies on the existence of both + # 'credentials_store' and 'credentials' variables, representing the + # Jenkins credentials store and the credentials to be fetched, respectively + # @abstract + # @return [String] + # + def fetch_existing_credentials_groovy(_groovy_variable_name) + raise NotImplementedError, 'You must implement #fetch_existing_credentials_groovy.' + end + + # + # Returns a Groovy snippet with an array of the resource attributes. The snippet + # relies on the existence of a variable credentials that represents the resource + # @abstract + # @return [String] + # + def resource_attributes_groovy(_groovy_variable_name) + raise NotImplementedError, 'You must implement #resource_attributes_groovy.' + end + + # + # Helper method for determining if the given JSON is in sync with the + # current configuration on the Jenkins instance. + # + # @return [Boolean] + # + def correct_config? + raise NotImplementedError, 'You must implement #correct_config?.' + end + + # + # Maps a credentials's resource attribute name to the equivalent + # property in the Groovy representation. This mapping is useful in + # Ruby/Groovy serialization/deserialization. + # + # @return [Hash] + # + # @example + # {password: 'credentials.password.plainText'} + # + def attribute_to_property_map + {} + end + + # + # Loads the current credential into a Hash. + # + def current_credentials + return @current_credentials if @current_credentials + + Chef::Log.debug "Load #{new_resource} credentials information" + + credentials_attributes = [] + attribute_to_property_map.each_pair do |resource_attribute, groovy_property| + credentials_attributes << + "current_credentials['#{resource_attribute}'] = #{groovy_property}" + end + + json = executor.groovy! <<-EOH.gsub(/^ {8}/, '') + import com.cloudbees.plugins.credentials.impl.*; + import com.cloudbees.jenkins.plugins.sshcredentials.impl.*; + + #{fetch_existing_credentials_groovy('credentials')} + + if(credentials == null) { + return null + } + + #{resource_attributes_groovy('current_credentials')} + + #{credentials_attributes.join("\n")} + + builder = new groovy.json.JsonBuilder(current_credentials) + println(builder) + EOH + + return if json.nil? || json.empty? + + @current_credentials = JSON.parse(json, symbolize_names: true) + + # Values that were serialized as nil/null are deserialized as an + # empty string! :( Let's ensure we convert back to nil. + @current_credentials = convert_blank_values_to_nil(@current_credentials) + end +end diff --git a/resources/credentials_file.rb b/resources/credentials_file.rb new file mode 100644 index 0000000000..ca6b6bd190 --- /dev/null +++ b/resources/credentials_file.rb @@ -0,0 +1,87 @@ +unified_mode true + +include Jenkins::Helper +provides :jenkins_file_credentials + +property :description, + String, + default: lazy { |new_resource| "Credentials for #{new_resource.filename} - created by Chef" } + +property :filename, + String, + name_property: true +property :data, + String, + required: true + +use 'partials/_credentials' + +def load_current_resource + @current_resource ||= Resource::JenkinsFileCredentials.new(new_resource.name) + + super + + @current_resource.filename(current_credentials[:filename]) if current_credentials + + @current_resource +end + +action_class do + # + # @see Chef::Resource::JenkinsCredentials#save_credentials_groovy + # + def fetch_existing_credentials_groovy(groovy_variable_name) + <<-EOH.gsub(/^ {8}/, '') + #{credentials_for_id_groovy(new_resource.id, groovy_variable_name)} + EOH + end + + # + # @see Chef::Resource::JenkinsCredentials#resource_attributes_groovy + # + def resource_attributes_groovy(groovy_variable_name) + <<-EOH.gsub(/^ {8}/, '') + #{groovy_variable_name} = [ + id:credentials.id, + description:credentials.description, + filename:credentials.filename, + ] + EOH + end + + # + # @see Chef::Resource::JenkinsCredentials#correct_config? + # + def correct_config? + wanted_credentials = { + description: new_resource.description, + filename: new_resource.filename, + } + + attribute_to_property_map.each_key do |key| + wanted_credentials[key] = new_resource.send(key) + end + + # Don't compare the ID as it is generated + current_credentials.dup.tap { |c| c.delete(:id) } == convert_blank_values_to_nil(wanted_credentials) + end + + # + # @see Chef::Resource::JenkinsCredentials#credentials_groovy + # @see https://github.com/jenkinsci/ssh-credentials-plugin/blob/master/src/main/java/com/cloudbees/jenkins/plugins/sshcredentials/impl/BasicSSHUserPrivateKey.java + # + def credentials_groovy + <<-EOH.gsub(/^ {8}/, '') + import com.cloudbees.plugins.credentials.* + import org.jenkinsci.plugins.plaincredentials.impl.* + + credentials = new FileCredentialsImpl( + CredentialsScope.GLOBAL, + #{convert_to_groovy(new_resource.id)}, + #{convert_to_groovy(new_resource.description)}, + #{convert_to_groovy(new_resource.filename)}, + SecretBytes.fromBytes(#{convert_to_groovy(new_resource.data)}.getBytes()) + ) + EOH + end +end diff --git a/resources/credentials_password.rb b/resources/credentials_password.rb new file mode 100644 index 0000000000..075f4d2ae2 --- /dev/null +++ b/resources/credentials_password.rb @@ -0,0 +1,30 @@ +# require_relative 'credentials' +# require_relative 'credentials_user' + +property :username, + String, + name_property: true +property :password, + String, + required: true + +unified_mode true +use 'partials/_credentials' + +def load_current_resource + @current_resource ||= Resource::JenkinsPasswordCredentials.new(new_resource.name) + + super + + @current_resource.password(current_credentials[:password]) if current_credentials + @current_credentials +end + +action_class do + # + # @see Chef::Resource::JenkinsCredentials#attribute_to_property_map + # + def attribute_to_property_map + { password: 'credentials.password.plainText' } + end +end diff --git a/resources/credentials_private_key.rb b/resources/credentials_private_key.rb new file mode 100644 index 0000000000..8f14147f97 --- /dev/null +++ b/resources/credentials_private_key.rb @@ -0,0 +1,110 @@ +unified_mode true + +# require_relative 'credentials' +# require_relative 'credentials_user' + +include Jenkins::Helper + +resource_name :jenkins_private_key_credentials # Still needed for Chef 15 and below +provides :jenkins_private_key_credentials + +# Attributes +property :username, + String, + name_property: true +property :private_key, + [String, OpenSSL::PKey::RSA, OpenSSL::PKey::EC], + required: true +property :passphrase, + String + +def load_current_resource + @current_resource ||= Resource::JenkinsPrivateKeyCredentials.new(new_resource.name) + + super + + @current_resource.private_key(current_credentials[:private_key]) if current_credentials + + @current_resource +end + +action_class do + # + # @see Chef::Resource::JenkinsCredentials#credentials_groovy + # @see https://github.com/jenkinsci/ssh-credentials-plugin/blob/master/src/main/java/com/cloudbees/jenkins/plugins/sshcredentials/impl/BasicSSHUserPrivateKey.java + # + def credentials_groovy + <<-EOH.gsub(/^ {8}/, '') + import com.cloudbees.plugins.credentials.* + import com.cloudbees.jenkins.plugins.sshcredentials.impl.* + + private_key = """#{new_resource.pem_private_key} + """ + + credentials = new BasicSSHUserPrivateKey( + CredentialsScope.GLOBAL, + #{convert_to_groovy(new_resource.id)}, + #{convert_to_groovy(new_resource.username)}, + new BasicSSHUserPrivateKey.DirectEntryPrivateKeySource(private_key), + #{convert_to_groovy(new_resource.passphrase)}, + #{convert_to_groovy(new_resource.description)} + ) + EOH + end + + # + # @see Chef::Resource::JenkinsCredentials#attribute_to_property_map + # + def attribute_to_property_map + { + private_key: 'credentials.privateKey', + passphrase: 'credentials.passphrase && credentials.passphrase.plainText', + } + end + + # + # @see Chef::Resource::JenkinsCredentials#current_credentials + # + def current_credentials + super + + # Normalize the private key + if @current_credentials && @current_credentials[:private_key] + cc = @current_credentials[:private_key] + cc = @current_credentials[:private_key].to_pem unless cc.is_a?(String) + @current_credentials[:private_key] = ecdsa_key?(cc) ? OpenSSL::PKey::EC.new(cc) : OpenSSL::PKey::RSA.new(cc) + end + + @current_credentials + end + + # + # Determine whether a key is an ECDSA key. As original functionality + # assumed that exclusively RSA keys were used, not breaking this assumption + # despite ECDSA keys being a possibility alleviates some issues with + # backwards-compatibility. + # + # @param [String] key + # @return [TrueClass, FalseClass] + def ecdsa_key?(key) + key.include?('BEGIN EC PRIVATE KEY') + end + + # + # Private key of the credentials . This should be the actual key + # contents (as opposed to the path to a private key file) in OpenSSH + # format. + # + # @param [String] arg + # @return [String] + # + def pem_private_key + if private_key.is_a?(OpenSSL::PKey::RSA) || private_key.is_a?(OpenSSL::PKey::EC) + private_key.to_pem + elsif ecdsa_key?(private_key) + OpenSSL::PKey::EC.new(private_key).to_pem + else + OpenSSL::PKey::RSA.new(private_key).to_pem + end + end +end diff --git a/resources/credentials_secret_text.rb b/resources/credentials_secret_text.rb new file mode 100644 index 0000000000..edbd4d5927 --- /dev/null +++ b/resources/credentials_secret_text.rb @@ -0,0 +1,86 @@ +unified_mode true +# require_relative 'credentials' + +property :description, + String, + name_property: true +property :secret, + String, + required: true + +def load_current_resource + @current_resource ||= Resource::JenkinsSecretTextCredentials.new(new_resource.name) + + super + + @current_resource.secret(current_credentials[:secret]) if current_credentials + + @current_credentials +end + +action_class do + # + # @see Chef::Resource::JenkinsCredentials#credentials_groovy + # @see https://github.com/jenkinsci/plain-credentials-plugin/blob/master/src/main/java/org/jenkinsci/plugins/plaincredentials/impl/StringCredentialsImpl.java + # + def credentials_groovy + <<-EOH.gsub(/^ {8}/, '') + import hudson.util.Secret; + import com.cloudbees.plugins.credentials.CredentialsScope; + import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl; + + credentials = new StringCredentialsImpl( + CredentialsScope.GLOBAL, + #{convert_to_groovy(new_resource.id)}, + #{convert_to_groovy(new_resource.description)}, + new Secret(#{convert_to_groovy(new_resource.secret)}), + ) + EOH + end + + # + # @see Chef::Resource::JenkinsCredentials#fetch_credentials_groovy + # + def fetch_existing_credentials_groovy(groovy_variable_name) + <<-EOH.gsub(/^ {8}/, '') + #{credentials_for_secret_groovy(new_resource.secret, new_resource.description, groovy_variable_name)} + EOH + end + + # + # @see Chef::Resource::JenkinsCredentials#resource_attributes_groovy + # + def resource_attributes_groovy(groovy_variable_name) + <<-EOH.gsub(/^ {8}/, '') + #{groovy_variable_name} = [ + id:credentials.id, + description:credentials.description, + secret:credentials.secret + ] + EOH + end + + # + # @see Chef::Resource::JenkinsCredentials#attribute_to_property_map + # + def attribute_to_property_map + { secret: 'credentials.secret.plainText' } + end + + # + # @see Chef::Resource::JenkinsCredentials#correct_config? + # + def correct_config? + wanted_credentials = { + description: new_resource.description, + secret: new_resource.secret, + } + + attribute_to_property_map.each_key do |key| + wanted_credentials[key] = new_resource.send(key) + end + + # Don't compare the ID as it is generated + current_credentials.dup.tap { |c| c.delete(:id) } == convert_blank_values_to_nil(wanted_credentials) + end +end diff --git a/resources/credentials_user.rb b/resources/credentials_user.rb new file mode 100644 index 0000000000..0ea6c281cc --- /dev/null +++ b/resources/credentials_user.rb @@ -0,0 +1,55 @@ +unified_mode true + +include Jenkins::Helper +use 'partials/_credentials' + +property :description, + String, + default: lazy { |new_resource| "Credentials for #{new_resource.username} - created by Chef" } + +def load_current_resource + @current_resource ||= Resource::JenkinsUserCredentials.new(new_resource.name) + + super + + @current_resource.username(current_credentials[:username]) if current_credentials + @current_resource +end + +action_class do + def fetch_existing_credentials_groovy(groovy_variable_name) + <<-EOH.gsub(/^ {8}/, '') + #{credentials_for_id_groovy(new_resource.id, groovy_variable_name)} + EOH + end + + # + # @see Chef::Resource::JenkinsCredentials#resource_attributes_groovy + # + def resource_attributes_groovy(groovy_variable_name) + <<-EOH.gsub(/^ {8}/, '') + #{groovy_variable_name} = [ + id:credentials.id, + description:credentials.description, + username:credentials.username + ] + EOH + end + + # + # @see Chef::Resource::JenkinsCredentials#correct_config? + # + def correct_config? + wanted_credentials = { + description: new_resource.description, + username: new_resource.username, + } + + attribute_to_property_map.each_key do |key| + wanted_credentials[key] = new_resource.send(key) + end + + # Don't compare the ID as it is generated + current_credentials.dup.tap { |c| c.delete(:id) } == convert_blank_values_to_nil(wanted_credentials) + end +end diff --git a/resources/job.rb b/resources/job.rb new file mode 100644 index 0000000000..cff1694131 --- /dev/null +++ b/resources/job.rb @@ -0,0 +1,293 @@ +require 'rexml/document' +unified_mode true +# require_relative '_helper' + +property :config, + String + +# Execute specific attributes +property :parameters, + Hash, + default: {} +property :stream_job_output, + [true, false], + default: true +property :wait_for_completion, + [true, false], + default: true + +attr_writer :enabled, :exists + +# +# Determine if the job exists on the master. This value is set by the +# provider when the current resource is loaded. +# +# @return [Boolean] +# +def exists? + !@exists.nil? && @exists +end + +# +# Determine if the job is enabled on the master. This value is set by the +# provider when the current resource is loaded. +# +# @return [Boolean] +# +def enabled? + !@enabled.nil? && @enabled +end + +# After some careful discussions internally, it was decided that +# raising an exception when the job does not exist is the best +# developer experience. +class JobDoesNotExist < StandardError + def initialize(job, action) + super <<-EOH +The Jenkins job `#{job}' does not exist. In order to :#{action} `#{job}', that +job must first exist on the Jenkins master! + EOH + end +end + +def load_current_resource + @current_resource ||= Resource::JenkinsJob.new(new_resource.name) + @current_resource.name(new_resource.name) + @current_resource.config(new_resource.config) + + if current_job + @current_resource.exists = true + @current_resource.enabled = current_job[:enabled] + else + @current_resource.exists = false + @current_resource.enabled = false + end + + @current_resource +end + +action :build do + raise JobDoesNotExist.new(new_resource.name, :build) unless current_resource.exists? + + if current_resource.enabled? + converge_by("Build #{new_resource}") do + command_args = [ + 'build', + escape(new_resource.name), + ] + + if new_resource.wait_for_completion + command_args << '-s' # Wait until the completion/abortion of the command. + end + + new_resource.parameters.each_pair do |key, value| + command_args << case value + when TrueClass, FalseClass + "-p #{key}=#{value}" + else + if value.include?(' ') + "-p #{key}='#{value}'" + else + "-p #{key}=#{value}" + end + end + end + + if new_resource.stream_job_output && new_resource.wait_for_completion && stdout_stream + command_args << '-v' # Prints out the console output of the build. + + stdout_stream.print <<-EOH + + +================================================================================ += BEGIN '#{new_resource.name}' Jenkins job output +================================================================================ + + EOH + + executor.execute!(*command_args, live_stream: stdout_stream) + + stdout_stream.print <<-EOH + +================================================================================ += END '#{new_resource.name}' Jenkins job output +================================================================================ + EOH + else + executor.execute!(*command_args) + end + end + else + Chef::Log.info("#{new_resource} disabled - skipping") + end +end + +# +# Create a new Jenkins job with the current resource's name +# and configuration file. If the job already exists, no action will be +# taken. If the job does not exist, one will be created from the given +# `config` XML file using the Jenkins CLI. +# +# This method also ensures the given configuration file matches the one +# rendered on the Jenkins master. If the configuration file does not match, +# a new one is rendered. +# +# Requirements: +# - `config` parameter +# +action :create do + validate_config! + + if current_resource.exists? + Chef::Log.info("#{new_resource} exists - skipping") + else + converge_by("Create #{new_resource}") do + executor.execute!('create-job', escape(new_resource.name), '<', escape(new_resource.config)) + end + end + + if correct_config? + Chef::Log.info("#{new_resource} config up to date - skipping") + else + converge_by("Update #{new_resource} config") do + executor.execute!('update-job', escape(new_resource.name), '<', escape(new_resource.config)) + end + end +end + +# +# Delete a Jenkins job with the current resource's name. If +# the job does not exist, no action will be taken. If the job does exist, +# it will be deleted using the Jenkins CLI. +# +action :delete do + if current_resource.exists? + converge_by("Delete #{new_resource}") do + executor.execute!('delete-job', escape(new_resource.name)) + end + else + Chef::Log.info("#{new_resource} does not exist - skipping") + end +end + +# +# Disable an existing Jenkins job. +# +# @raise [JobDoesNotExist] +# if the job does not exist +# +action :disable do + raise JobDoesNotExist.new(new_resource.name, :disable) unless current_resource.exists? + + if current_resource.enabled? + converge_by("Disable #{new_resource}") do + executor.execute!('disable-job', escape(new_resource.name)) + end + else + Chef::Log.info("#{new_resource} disabled - skipping") + end +end + +# +# Enable an existing Jenkins job. +# +# @raise [JobDoesNotExist] +# if the job does not exist +# +action :enable do + raise JobDoesNotExist.new(new_resource.name, :enable) unless current_resource.exists? + + if current_resource.enabled? + Chef::Log.info("#{new_resource} enabled - skipping") + else + converge_by("Enable #{new_resource}") do + executor.execute!('enable-job', escape(new_resource.name)) + end + end +end + +action_class do + # + # The job in the current, in XML format. + # + # @return [nil, Hash] + # nil if the job does not exist, or a hash of important information if + # it does + # + def current_job + return @current_job if @current_job + + Chef::Log.debug "Load #{new_resource} job information" + + response = executor.execute('get-job', escape(new_resource.name)) + return if response.nil? || response =~ /No such job/ + + Chef::Log.debug "Parse #{new_resource} as XML" + xml = REXML::Document.new(response) + disabled = xml.elements['//disabled'] + + @current_job = { + enabled: disabled.nil? ? true : disabled.text == 'false', + xml: xml, + raw: response, + } + @current_job + end + + # + # Helper method for determining if the given JSON is in sync with the + # current configuration on the Jenkins master. + # + # We have to create REXML objects and then remove any whitespace because + # XML is evil and sometimes sucks at the simplest things, like comparing + # itself. + # + # @return [Boolean] + # + def correct_config? + current = StringIO.new + wanted = StringIO.new + + current_job[:xml].write(current, 2) + REXML::Document.new(::File.read(new_resource.config)).write(wanted, 2) + + current.string == wanted.string + end + + # + # Validate that a configuration file was given as a parameter to the + # resource. This method also validates the given config file path exists + # on the target node. Finally, this method reads the contents of the file + # and verifies it is valid XML. + # + def validate_config! + Chef::Log.debug "Validate #{new_resource} configuration" + + raise("#{new_resource} must specify a configuration file!") if new_resource.config.nil? + + raise("#{new_resource} config `#{new_resource.config}` does not exist!") unless ::File.exist?(new_resource.config) + + begin + REXML::Document.new(::File.read(new_resource.config)) + rescue REXML::ParseException + raise("#{new_resource} config `#{new_resource.config}` is not valid XML!") + end + end + + # Inspired by chef/chef/#4040 + def formatter? + if run_context.events.respond_to?(:subscribers) + run_context.events.subscribers.any? { |s| s.respond_to?(:is_formatter?) && s.is_formatter? } + else + false + end + end + + def stdout_stream + @stdout_stream ||= if formatter? + Chef::EventDispatch::EventsOutputStream.new(run_context.events, name: new_resource.name.to_sym) + elsif STDOUT.tty? && !Chef::Config[:daemon] + STDOUT + end + end +end diff --git a/resources/partials/_command.rb b/resources/partials/_command.rb new file mode 100644 index 0000000000..4a1a1b2778 --- /dev/null +++ b/resources/partials/_command.rb @@ -0,0 +1,17 @@ +unified_mode true + +property :command, + String, + name_property: true + +include Jenkins::Helper + +def load_current_resource + @current_resource ||= Resource::JenkinsCommand.new(new_resource.command) +end + +action :execute do + converge_by("Execute #{new_resource}") do + executor.execute!(new_resource.command) + end +end diff --git a/resources/partials/_credentials.rb b/resources/partials/_credentials.rb new file mode 100644 index 0000000000..41227b3e6d --- /dev/null +++ b/resources/partials/_credentials.rb @@ -0,0 +1,183 @@ +require 'json' +require 'openssl' +require 'securerandom' + +property :id, + String, + required: true + +def load_current_resource + @current_resource ||= Resource::JenkinsCredentials.new(new_resource.name) + + if current_credentials + @current_resource.exists = true + @current_resource.id(current_credentials[:id]) + @current_resource.description(current_credentials[:description]) + end + + @current_resource +end + +action :create do + if current_resource.exists? && correct_config? + Chef::Log.info("#{new_resource} exists - skipping") + else + converge_by("Create #{new_resource}") do + executor.groovy! <<-EOH.gsub(/^ {12}/, '') + import jenkins.model.* + import com.cloudbees.plugins.credentials.* + import com.cloudbees.plugins.credentials.domains.* + import hudson.plugins.sshslaves.*; + + global_domain = Domain.global() + credentials_store = + Jenkins.instance.getExtensionList( + 'com.cloudbees.plugins.credentials.SystemCredentialsProvider' + )[0].getStore() + + #{credentials_groovy} + + #{fetch_existing_credentials_groovy('existing_credentials')} + + if(existing_credentials != null) { + credentials_store.updateCredentials( + global_domain, + existing_credentials, + credentials + ) + } else { + credentials_store.addCredentials(global_domain, credentials) + } + EOH + end + end +end + +action :delete do + if current_resource.exists? + converge_by("Delete #{new_resource}") do + executor.groovy! <<-EOH.gsub(/^ {12}/, '') + import jenkins.model.* + import com.cloudbees.plugins.credentials.*; + + global_domain = com.cloudbees.plugins.credentials.domains.Domain.global() + credentials_store = + Jenkins.instance.getExtensionList( + 'com.cloudbees.plugins.credentials.SystemCredentialsProvider' + )[0].getStore() + + #{fetch_existing_credentials_groovy('existing_credentials')} + + if(existing_credentials != null) { + credentials_store.removeCredentials( + global_domain, + existing_credentials + ) + } + EOH + end + else + Chef::Log.debug("#{new_resource} does not exist - skipping") + end +end + +action_class do + # + # Returns a Groovy snippet that creates an instance of the + # credential's implementation. The credentials instance should be + # set to a Groovy variable named `credentials`. + # + # @abstract + # @return [String] + # + def credentials_groovy + raise NotImplementedError, 'You must implement #credentials_groovy.' + end + + # + # Returns a Groovy snippet that fetches credentials from the + # credentials store. The snippet relies on the existence of both + # 'credentials_store' and 'credentials' variables, representing the + # Jenkins credentials store and the credentials to be fetched, respectively + # @abstract + # @return [String] + # + def fetch_existing_credentials_groovy(_groovy_variable_name) + raise NotImplementedError, 'You must implement #fetch_existing_credentials_groovy.' + end + + # + # Returns a Groovy snippet with an array of the resource attributes. The snippet + # relies on the existence of a variable credentials that represents the resource + # @abstract + # @return [String] + # + def resource_attributes_groovy(_groovy_variable_name) + raise NotImplementedError, 'You must implement #resource_attributes_groovy.' + end + + # + # Helper method for determining if the given JSON is in sync with the + # current configuration on the Jenkins instance. + # + # @return [Boolean] + # + def correct_config? + raise NotImplementedError, 'You must implement #correct_config?.' + end + + # + # Maps a credentials's resource attribute name to the equivalent + # property in the Groovy representation. This mapping is useful in + # Ruby/Groovy serialization/deserialization. + # + # @return [Hash] + # + # @example + # {password: 'credentials.password.plainText'} + # + def attribute_to_property_map + {} + end + + # + # Loads the current credential into a Hash. + # + def current_credentials + return @current_credentials if @current_credentials + + Chef::Log.debug "Load #{new_resource} credentials information" + + credentials_attributes = [] + attribute_to_property_map.each_pair do |resource_attribute, groovy_property| + credentials_attributes << + "current_credentials['#{resource_attribute}'] = #{groovy_property}" + end + + json = executor.groovy! <<-EOH.gsub(/^ {8}/, '') + import com.cloudbees.plugins.credentials.impl.*; + import com.cloudbees.jenkins.plugins.sshcredentials.impl.*; + + #{fetch_existing_credentials_groovy('credentials')} + + if(credentials == null) { + return null + } + + #{resource_attributes_groovy('current_credentials')} + + #{credentials_attributes.join("\n")} + + builder = new groovy.json.JsonBuilder(current_credentials) + println(builder) + EOH + + return if json.nil? || json.empty? + + @current_credentials = JSON.parse(json, symbolize_names: true) + + # Values that were serialized as nil/null are deserialized as an + # empty string! :( Let's ensure we convert back to nil. + @current_credentials = convert_blank_values_to_nil(@current_credentials) + end +end diff --git a/resources/partials/_slave.rb b/resources/partials/_slave.rb new file mode 100644 index 0000000000..85d7f83e89 --- /dev/null +++ b/resources/partials/_slave.rb @@ -0,0 +1,369 @@ +require 'json' +provides :jenkins_slave +unified_mode true + +property :slave.name, + String, + name_property: true +property :description, + String, + default: lazy { |new_resource| "Jenkins slave #{new_resource.slave.name}" } +property :remote_fs, + String, + default: '/home/jenkins' +property :executors, + Integer, + default: 1 +property :usage_mode, + String, + equal_to: %w(exclusive normal), + default: 'normal' +property :labels, + Array, + default: [] +property :availability, + String, + equal_to: %w(always demand) +property :in_demand_delay, + Integer, + default: 0 +property :idle_delay, + Integer, + default: 1 +property :environment, + Hash +property :offline_reason, + String +property :user, + String, + regex: Config[:user_valid_regex], + default: 'jenkins' +property :jvm_options, + String +property :java_path, + String + +# attr_writer :exists +# attr_writer :connected +# attr_writer :online + +# +# Determine if the slave exists on the master. This value is set by +# the provider when the current resource is loaded. +# +# @return [Boolean] +# +def exists? + !@exists.nil? && @exists +end + +# +# Determine if the slave is connected to the master. This value is +# set by the provider when the current resource is loaded. +# +# @return [Boolean] +# +def connected? + !@connected.nil? && @connected +end + +# +# Determine if the slave is online. This value is set by the +# provider when the current resource is loaded. +# +# @return [Boolean] +# +def online? + !@online.nil? && @online +end + +include Jenkins::Helper + +def load_current_resource + @current_resource ||= Resource::JenkinsSlave.new(new_resource.name) + + if current_slave + @current_resource.exists = true + @current_resource.connected = current_slave[:connected] + @current_resource.online = current_slave[:online] + + @current_resource.slave.name(new_resource.slave.name) + @current_resource.description(current_slave[:description]) + @current_resource.remote_fs(current_slave[:remote_fs]) + @current_resource.executors(current_slave[:executors]) + @current_resource.labels(current_slave[:labels]) + end + + @current_resource +end + +action :create do + do_create +end + +def merge_preserved_labels! + new_resource.labels |= current_resource.labels.select { |i| i[/^prsrv_/] } +end + +def do_create + # Preserve some labels... + merge_preserved_labels! + if current_resource.exists? && correct_config? + Chef::Log.info("#{new_resource} exists - skipping") + else + converge_by("Create #{new_resource}") do + executor.groovy! <<-EOH.gsub(/^ {12}/, '') + import hudson.model.* + import hudson.slaves.* + import jenkins.model.* + import jenkins.slaves.* + + props = [] + availability = #{convert_to_groovy(new_resource.availability)} + usage_mode = #{convert_to_groovy(new_resource.usage_mode)} + env_map = #{convert_to_groovy(new_resource.environment)} + labels = #{convert_to_groovy(new_resource.labels.sort.join(' '))} + + // Compute the usage mode + if (usage_mode == 'normal') { + mode = Node.Mode.NORMAL + } else { + mode = Node.Mode.EXCLUSIVE + } + + // Compute the retention strategy + if (availability == 'demand') { + retention_strategy = + new RetentionStrategy.Demand( + #{new_resource.in_demand_delay}, + #{new_resource.idle_delay} + ) + } else if (availability == 'always') { + retention_strategy = new RetentionStrategy.Always() + } else { + retention_strategy = RetentionStrategy.NOOP + } + + // Create an entry in the prop list for all env vars + if (env_map != null) { + env_vars = new hudson.EnvVars(env_map) + entries = env_vars.collect { + k,v -> new EnvironmentVariablesNodeProperty.Entry(k,v) + } + props << new EnvironmentVariablesNodeProperty(entries) + } + + // Launcher + #{launcher_groovy} + + // Build the slave object + slave = new DumbSlave( + #{convert_to_groovy(new_resource.name)}, + #{convert_to_groovy(new_resource.description)}, + #{convert_to_groovy(new_resource.remote_fs)}, + #{convert_to_groovy(new_resource.executors.to_s)}, + mode, + labels, + launcher, + retention_strategy, + props + ) + + // Create or update the slave in the Jenkins instance + nodes = new ArrayList(Jenkins.instance.getNodes()) + ix = nodes.indexOf(slave) + (ix >= 0) ? nodes.set(ix, slave) : nodes.add(slave) + Jenkins.instance.setNodes(nodes) + EOH + end + end +end + +action :delete do + do_delete +end + +def do_delete + if current_resource.exists? + converge_by("Delete #{new_resource}") do + executor.execute!('delete-node', escape(new_resource.slave.name)) + end + else + Chef::Log.debug("#{new_resource} does not exist - skipping") + end +end + +action :connect do + if current_resource.exists? && current_resource.connected? + Chef::Log.debug("#{new_resource} already connected - skipping") + else + converge_by("Connect #{new_resource}") do + executor.execute!('connect-node', escape(new_resource.slave.name)) + end + end +end + +action :disconnect do + if current_resource.connected? + converge_by("Disconnect #{new_resource}") do + executor.execute!('disconnect-node', escape(new_resource.slave.name)) + end + else + Chef::Log.debug("#{new_resource} already disconnected - skipping") + end +end + +action :online do + if current_resource.exists? && current_resource.online? + Chef::Log.debug("#{new_resource} already online - skipping") + else + converge_by("Online #{new_resource}") do + executor.execute!('online-node', escape(new_resource.slave.name)) + end + end +end + +action :offline do + if current_resource.online? + converge_by("Offline #{new_resource}") do + command_pieces = [escape(new_resource.slave.name)] + command_pieces << "-m '#{escape(new_resource.offline_reason)}'" if new_resource.offline_reason + executor.execute!('offline-node', command_pieces) + end + else + Chef::Log.debug("#{new_resource} already offline - skipping") + end +end + +private + +# +# Returns a Groovy snippet that creates an instance of the slave's +# launcher implementation. The launcher instance should be set to +# a Groovy variable named `launcher`. +# +# @return [String] +# +def launcher_groovy + 'launcher = new hudson.slaves.JNLPLauncher()' +end + +# +# Maps a slave's resource property name to the equivalent property +# in the Groovy representation. This mapping is useful in +# Ruby/Groovy serialization/deserialization. +# +# @return [Hash] +# +# @example +# {host: 'host', +# port: 'port', +# credential_username: 'username', +# jvm_options: 'jvmOptions'} +# +def property_to_property_map + {} +end + +# +# Loads the current slave into a Hash. +# +def current_slave + return @current_slave if @current_slave + + Chef::Log.debug "Load #{new_resource} slave information" + + launcher_propertys = [] + property_to_property_map.each_pair do |resource_property, groovy_property| + launcher_propertys << "current_slave['#{resource_property}'] = #{groovy_property}" + end + + json = executor.groovy! <<-EOH.gsub(/^ {8}/, '') + import hudson.model.* + import hudson.slaves.* + import jenkins.model.* + import jenkins.slaves.* + + slave = Jenkins.instance.getNode('#{new_resource.slave.name}') as Slave + + if(slave == null) { + return null + } + + def slave_environment = null + slave_env_vars = slave.nodeProperties.get(EnvironmentVariablesNodeProperty.class)?.envVars + if (slave_env_vars) + slave_environment = new java.util.HashMap(slave_env_vars) + + current_slave = [ + name:slave.name, + description:slave.nodeDescription, + remote_fs:slave.remoteFS, + executors:slave.numExecutors.toInteger(), + usage_mode:slave.mode.toString().toLowerCase(), + labels:slave.labelString.split().sort(), + environment:slave_environment, + connected:(slave.computer.connectTime > 0), + online:slave.computer.online + ] + + // Determine retention strategy + if (slave.retentionStrategy instanceof RetentionStrategy.Always) { + current_slave['availability'] = 'always' + } else if (slave.retentionStrategy instanceof RetentionStrategy.Demand) { + current_slave['availability'] = 'demand' + retention = slave.retentionStrategy as RetentionStrategy.Demand + current_slave['in_demand_delay'] = retention.inDemandDelay + current_slave['idle_delay'] = retention.idleDelay + } else { + current_slave['availability'] = null + } + + #{launcher_propertys.join("\n")} + + builder = new groovy.json.JsonBuilder(current_slave) + println(builder) + EOH + + return if json.nil? || json.empty? + + @current_slave = JSON.parse(json, symbolize_names: true) + + # Values that were serialized as nil/null are deserialized as an + # empty string! :( Let's ensure we convert back to nil. + @current_slave = convert_blank_values_to_nil(@current_slave) +end + +# +# Helper method for determining if the given JSON is in sync with the +# current configuration on the Jenkins master. +# +# @return [Boolean] +# +def correct_config? + wanted_slave = { + name: new_resource.slave.name, + description: new_resource.description, + remote_fs: new_resource.remote_fs, + executors: new_resource.executors, + usage_mode: new_resource.usage_mode, + labels: new_resource.labels.sort, + availability: new_resource.availability, + environment: new_resource.environment, + } + + if new_resource.availability.to_s == 'demand' + wanted_slave[:in_demand_delay] = new_resource.in_demand_delay + wanted_slave[:idle_delay] = new_resource.idle_delay + end + + property_to_property_map.each_key do |key| + wanted_slave[key] = new_resource.send(key) + end + + # Don't include connected/online values in the comparison + current_slave.dup.tap do |c| + c.delete(:connected) + c.delete(:online) + end == wanted_slave +end diff --git a/resources/plugin.rb b/resources/plugin.rb new file mode 100644 index 0000000000..a610cdf3c0 --- /dev/null +++ b/resources/plugin.rb @@ -0,0 +1,347 @@ +require 'digest' + +provides :jenkins_plugin +unified_mode true + +property :version, + [String, Symbol], + default: :latest +property :source, + String +# TODO: Remove in next major version release +property :install_deps, + [true, false] +property :options, + String + +attr_writer :installed + +# +# Determine if the plugin is installed on the master. This value is set by +# the provider when the current resource is loaded. +# +# @return [Boolean] +# +def installed? + !@installed.nil? && @installed +end + + +include Jenkins::Helper + +class PluginNotInstalled < StandardError + def initialize(plugin, action) + super <<-EOH +The Jenkins plugin `#{plugin}' is not installed. In order to #{action} +`#{plugin}', that plugin must first be installed on the Jenkins master! + EOH + end +end + +def load_current_resource + @current_resource ||= Resource::JenkinsPlugin.new(new_resource.name) + @current_resource.source(new_resource.source) + @current_resource.version(new_resource.version) + + current_plugin = plugin_installation_manifest(new_resource.name) + + if current_plugin + @current_resource.installed = true + @current_resource.version(current_plugin['plugin_version']) + else + @current_resource.installed = false + end + + @current_resource +end + +action :install do + # TODO: remove in next major version release + # Check for dependency property and give deprecation if used + if new_resource.install_deps + Chef::Log.warn('The install_deps property on the plugin provider is deprecated and not used. See Readme on how to install plugins with or without dependencies.') + end + + # This block stores the actual command to execute, since its the same + # for upgrades and installs. + install_block = proc do + # Install a plugin from a given hpi (or jpi) if a link was provided. + # In that case jenkins does not handle plugin dependencies automatically. + # Otherwise the plugin is installed through the jenkins update-center + # (default behaviour). In that case plugin dependencies are handled by jenkins. + # if installing latest version + install_plugin( + new_resource.source, + new_resource.name, + new_resource.version, + cli_opts: new_resource.options + ) + end + + downgrade_block = proc do + # remove the existing, newer version + uninstall_plugin(new_resource.name) + + # proceed with a normal install + install_block.call + end + + if current_resource.installed? + if plugin_version(current_resource.version) == desired_version + Chef::Log.info("#{new_resource} version #{current_resource.version} already installed - skipping") + else + current_version = plugin_version(current_resource.version) + unless current_version.to_s.include? 'SNAPSHOT' + if plugin_upgrade?(current_version, desired_version) # rubocop: disable Metrics/BlockNesting + converge_by("Upgrade #{new_resource} from #{current_resource.version} to #{desired_version}", &install_block) + else + converge_by("Downgrade #{new_resource} from #{current_resource.version} to #{desired_version}", &downgrade_block) + end + end + end + else + converge_by("Install #{new_resource}", &install_block) + end +end + +# +# Disable the given plugin. +# +# Disabling a plugin is a softer way to retire a plugin. Jenkins will +# continue to recognize that the plugin is installed, but it will not +# start the plugin, and no extensions contributed from this plugin will be +# visible. +# +# The fragments contributed from a disabled plugin to configuration files +# would follow the same fate as in the case of uninstalled plugins. +# +# Plugins that are disabled can be re-enabled from the UI (or by removing +# *.jpi.disabled file from the disk.) +# +action :disable do + raise PluginNotInstalled.new(new_resource.name, :disable) unless current_resource.installed? + + disabled = "#{plugin_file(new_resource.name)}.disabled" + + if ::File.exist?(disabled) + Chef::Log.debug("#{new_resource} already disabled - skipping") + else + converge_by("Disable #{new_resource}") do + Resource::File.new(disabled, run_context).run_action(:create) + end + end +end + +# +# Enable the given plugin. +# +# Enabling a plugin brings back a formerly disabled plugin. Jenkins will +# being recognizing this plugin again on the next restart. +# +# Plugins may be disabled by re-adding the +.jpi.disabled+ plugin. +# +action :enable do + raise PluginNotInstalled.new(new_resource.name, :enable) unless current_resource.installed? + + disabled = "#{plugin_file(new_resource.name)}.disabled" + + if ::File.exist?(disabled) + converge_by("Enable #{new_resource}") do + Resource::File.new(disabled, run_context).run_action(:delete) + end + else + Chef::Log.debug("#{new_resource} already enabled - skipping") + end +end + +# +# Uninstall the given plugin. +# +# Uninstalling a plugin removes the plugin binary (*.jpi) from the disk. +# The plugin continues to function normally until you restart Jenkins, but +# once you restart, Jenkins will behave as if you didn't have the plugin +# to being with. They will not appear anywhere in the UI, all the +# extensions they contributed will disappear. +# +# WARNING: Uninstalling a plugin, however, does not remove the configuration +# that the plugin might have created. If there are existing +# jobs/slaves/views/builds/etc that used some extensions from the plugin, +# during the boot Jenkins will report that there are some fragments in +# those configurations that it didn't understand, and pretend as if it +# didn't see such a fragment. +# +action :uninstall do + if current_resource.installed? + converge_by("Uninstall #{new_resource}") do + uninstall_plugin(new_resource.name) + end + else + Chef::Log.debug("#{new_resource} not installed - skipping") + end +end + +private + +def desired_version(name = nil, version = nil) + name = new_resource.name if name.nil? + version = new_resource.version if version.nil? + + if version.to_sym == :latest + remote_plugin_data = plugin_universe[name] + + return :latest unless remote_plugin_data + + plugin_version(remote_plugin_data['version']) + else + plugin_version(version) + end +end + +# +# Installs a plugin along with all of it's dependencies if version is :latest and source property is not specified. +# +# @param [String] full url of the *.hpi/*.jpi to install +# @param [String] name of the plugin to be installed +# @param [String] version of the plugin to be installed +# @param [Hash] opts the options install plugin with +# @option opts [Boolean] :cli_opts additional flags to pass the jenkins cli command +# +def install_plugin(source_url, plugin_name, plugin_version, opts = {}) + test = (source_url || plugin_version != :latest) ? true : false + if test + url = if source_url + source_url + else + remote_plugin_data = plugin_universe[plugin_name] + # Compute some versions; Parse them as `Gem::Version` instances for easy comparisons. + latest_version = plugin_version(remote_plugin_data['version']) + # Replace the latest version with the desired version in the URL + remote_plugin_data['url'].gsub!(latest_version.to_s, desired_version(plugin_name, plugin_version).to_s) + end + end + ensure_update_center_present! + executor.execute!('install-plugin', escape(test ? url : plugin_name), opts[:cli_opts]) +end + +# +# Uninstalling a plugin removes the plugin binary (*.jpi) from the disk. +# +# @param [String] name of the plugin to be uninstall +# +def uninstall_plugin(plugin_name) + file = Resource::File.new(plugin_file(plugin_name), run_context) + file.backup(false) + file.run_action(:delete) + directory = Resource::Directory.new(plugin_data_directory(plugin_name), run_context) + directory.recursive(true) + directory.run_action(:delete) +end + +# +# The path to the plugins directory on the Jenkins node. +# +# @return [String] +# +def plugins_directory + ::File.join(node['jenkins']['master']['home'], 'plugins') +end + +# +# The path to the actual plugin file on disk (+.jpi+) +# +# @param [String] name of the plugin to be installed +# @return [String] +# +def plugin_file(plugin_name) + hpi = ::File.join(plugins_directory, "#{plugin_name}.hpi") + jpi = ::File.join(plugins_directory, "#{plugin_name}.jpi") + + ::File.exist?(hpi) ? hpi : jpi +end + +# +# The path to where the plugin stores its data on disk. +# +def plugin_data_directory(plugin_name) + ::File.join(plugins_directory, plugin_name) +end + +# +# Parsed hash of all known Jenkins plugins +# +# @return [Hash] +# +def plugin_universe + @plugin_universe ||= begin + ensure_update_center_present! + JSON.parse(IO.read(extracted_update_center_json).force_encoding('UTF-8'))['plugins'] + end +end + +# +# Return the installation manifest for +plugin_name+. If the plugin is not +# installed +nil+ is returned. +# +# @param [String] name of the plugin to be installed +# @return [Hash] +# +def plugin_installation_manifest(plugin_name) + manifest = ::File.join(plugins_directory, plugin_name, 'META-INF', 'MANIFEST.MF') + Chef::Log.debug "Load #{plugin_name} plugin information from #{manifest}" + + return unless ::File.exist?(manifest) + + plugin_manifest = {} + + ::File.open(manifest, 'r', encoding: 'utf-8') do |file| + file.each_line do |line| + next if line.strip.empty? + + # + # Example Data: + # Plugin-Version: 1.4 + # + config, value = line.split(/:\s/, 2) + config = config.tr('-', '_').downcase + value = value.strip if value # remove trailing \r\n + + plugin_manifest[config] = value + end + end + + plugin_manifest +end + +# +# Return whether plugin should be upgraded to desired version +# (i.e. that current < desired). +# https://github.com/chef-cookbooks/jenkins/issues/380 +# If only one of the two versions is a Gem::Version, we +# fallback to String comparison. +# +# @param [Gem::Version, String] current_version +# @param [Gem::Version, String] desired_version +# @return [Boolean] +# +def plugin_upgrade?(current_version, desired_version) + current_version < desired_version +rescue ArgumentError + current_version.to_s < desired_version.to_s +end + +# +# Return the plugin version for +version+. +# https://github.com/chef-cookbooks/jenkins/issues/292 +# Prefer to use Gem::Version as that will be more accurate than +# comparing strings, but sadly Jenkins plugins may not always +# follow "normal" version patterns +# +# @param [String] version +# @return [String] +# +def plugin_version(version) + gem_version = Gem::Version.new(version) + gem_version.prerelease? ? version : gem_version +rescue ArgumentError + version +end diff --git a/resources/proxy.rb b/resources/proxy.rb new file mode 100644 index 0000000000..2237b2e73c --- /dev/null +++ b/resources/proxy.rb @@ -0,0 +1,145 @@ +require 'json' + +# require_relative '_helper' +# require_relative '_params_validate' + +property :proxy, + kind_of: String, + name_property: true +property :noproxy, + kind_of: Array, + default: [] +property :username, + kind_of: String + +property :password, + kind_of: String + +attr_writer :configured + +# +# Determine if the proxy is configured on the master. This value is set by +# the provider when the current resource is loaded. +# +# @return [Boolean] +# +def configured? + !@configured.nil? && @configured +end + +include Jenkins::Helper + +provides :jenkins_proxy +unified_mode true + +def load_current_resource + @current_resource ||= Resource::JenkinsProxy.new(new_resource.proxy) + + if current_proxy + @current_resource.configured = true + @current_resource.proxy(current_proxy[:proxy]) + @current_resource.noproxy(current_proxy[:username]) + @current_resource.noproxy(current_proxy[:password]) + @current_resource.noproxy(current_proxy[:noproxy]) + end + + @current_resource +end + +action :config do + if current_resource.configured? && + current_resource.proxy == new_resource.proxy && + current_resource.username == new_resource.username && + current_resource.password == new_resource.password && + current_resource.noproxy == new_resource.noproxy + Chef::Log.info("#{new_resource} already configured - skipping") + else + name, port = new_resource.proxy.split(':') + if name && port && port.to_i > 0 + converge_by("Configure #{new_resource}") do + executor.groovy! <<-EOH.gsub(/^ {14}/, '') + name = #{convert_to_groovy(name)} + port = #{convert_to_groovy(port.to_i)} + username = #{convert_to_groovy(username)} + password = #{convert_to_groovy(password)} + noproxy = '#{new_resource.noproxy.join('\n')}' + + import hudson.ProxyConfiguration + def pc = new ProxyConfiguration(name, port, username, password, noproxy) + pc.save() + + import jenkins.model.Jenkins + def instance = Jenkins.getInstance() + instance.proxy = pc.load() + EOH + end + else + Chef::Log.debug("#{new_resource} incorrect format - skipping") + end + end +end + +action :remove do + if current_resource.configured? + converge_by("Remove #{new_resource}") do + executor.groovy! <<-EOH.gsub(/^ {12}/, '') + import jenkins.model.Jenkins + def instance = Jenkins.getInstance() + + def pc = instance.proxy + if (pc == null) { + return null + } + + pc.getXmlFile().delete() + instance.proxy = pc.load() + EOH + end + else + Chef::Log.debug("#{new_resource} does not exist - skipping") + end +end + +action_class do + # + # Loads the local proxy into a hash + # + def current_proxy + return @current_proxy if @current_proxy + + Chef::Log.debug "Load #{new_resource} proxy information" + + json = executor.groovy <<-EOH.gsub(/^ {8}/, '') + import java.util.Collections + import java.util.List + + import jenkins.model.Jenkins + def instance = Jenkins.getInstance() + + def pc = instance.proxy + if (pc == null) { + return null + } + + def no_proxy = pc.noProxyHost + if (no_proxy != null) { + no_proxy = no_proxy.tokenize('[ \\t\\n,|]+') + } else { + no_proxy = Collections.emptyList() + } + + def builder = new groovy.json.JsonBuilder() + builder { + proxy pc.name + ':' + pc.port.toString() + noproxy no_proxy + } + + println(builder) + EOH + + return if json.nil? || json.empty? + + @current_proxy = JSON.parse(json, symbolize_names: true) + @current_proxy + end +end diff --git a/resources/script.rb b/resources/script.rb new file mode 100644 index 0000000000..55a4960537 --- /dev/null +++ b/resources/script.rb @@ -0,0 +1,32 @@ +# require_relative 'command' +use 'partials/_command' +unified_mode true + +property :groovy_path, + String + +property :name, + String, + name_property: true, + required: false + +def load_current_resource + if new_resource.groovy_path + @current_resource ||= Resource::JenkinsScript.new(new_resource.name) + @current_resource.name(new_resource.name) + @current_resource.groovy_path(new_resource.groovy_path) + else + @current_resource ||= Resource::JenkinsScript.new(new_resource.command) + end + super +end + +action :execute do + converge_by("Execute script #{new_resource}") do + if new_resource.groovy_path + executor.groovy_from_file!(new_resource.groovy_path) + else + executor.groovy!(new_resource.command) + end + end +end diff --git a/resources/slave.rb b/resources/slave.rb new file mode 100644 index 0000000000..d0a9992393 --- /dev/null +++ b/resources/slave.rb @@ -0,0 +1,258 @@ +require 'json' + +unified_mode true + +property :slave.name, + String, + name_property: true +property :description, + String, + default: lazy { |new_resource| "Jenkins slave #{new_resource.slave.name}" } +property :remote_fs, + String, + default: '/home/jenkins' +property :executors, + Integer, + default: 1 +property :usage_mode, + String, + equal_to: %w(exclusive normal), + default: 'normal' +property :labels, + Array, + default: [] +property :availability, + String, + equal_to: %w(always demand) +property :in_demand_delay, + Integer, + default: 0 +property :idle_delay, + Integer, + default: 1 +property :environment, + Hash +property :offline_reason, + String +property :user, + String, + regex: Config[:user_valid_regex], + default: 'jenkins' +property :jvm_options, + String +property :java_path, + String + +include Jenkins::Cookbook::Slave +include Jenkins::Helper + +def load_current_resource + @current_resource ||= Resource::JenkinsSlave.new(new_resource.name) + + if current_slave + @current_resource.exists = true + @current_resource.connected = current_slave[:connected] + @current_resource.online = current_slave[:online] + + @current_resource.slave.name(new_resource.slave.name) + @current_resource.description(current_slave[:description]) + @current_resource.remote_fs(current_slave[:remote_fs]) + @current_resource.executors(current_slave[:executors]) + @current_resource.labels(current_slave[:labels]) + end + + @current_resource +end + +action :create do + do_create +end + +action :delete do + do_delete +end + +def do_delete + if current_resource.exists? + converge_by("Delete #{new_resource}") do + executor.execute!('delete-node', escape(new_resource.slave.name)) + end + else + Chef::Log.debug("#{new_resource} does not exist - skipping") + end +end + +action :connect do + if current_resource.exists? && current_resource.connected? + Chef::Log.debug("#{new_resource} already connected - skipping") + else + converge_by("Connect #{new_resource}") do + executor.execute!('connect-node', escape(new_resource.slave.name)) + end + end +end + +action :disconnect do + if current_resource.connected? + converge_by("Disconnect #{new_resource}") do + executor.execute!('disconnect-node', escape(new_resource.slave.name)) + end + else + Chef::Log.debug("#{new_resource} already disconnected - skipping") + end +end + +action :online do + if current_resource.exists? && current_resource.online? + Chef::Log.debug("#{new_resource} already online - skipping") + else + converge_by("Online #{new_resource}") do + executor.execute!('online-node', escape(new_resource.slave.name)) + end + end +end + +action :offline do + if current_resource.online? + converge_by("Offline #{new_resource}") do + command_pieces = [escape(new_resource.slave.name)] + command_pieces << "-m '#{escape(new_resource.offline_reason)}'" if new_resource.offline_reason + executor.execute!('offline-node', command_pieces) + end + else + Chef::Log.debug("#{new_resource} already offline - skipping") + end +end + +private + +# +# Returns a Groovy snippet that creates an instance of the slave's +# launcher implementation. The launcher instance should be set to +# a Groovy variable named `launcher`. +# +# @return [String] +# +def launcher_groovy + 'launcher = new hudson.slaves.JNLPLauncher()' +end + +# +# Maps a slave's resource property name to the equivalent property +# in the Groovy representation. This mapping is useful in +# Ruby/Groovy serialization/deserialization. +# +# @return [Hash] +# +# @example +# {host: 'host', +# port: 'port', +# credential_username: 'username', +# jvm_options: 'jvmOptions'} +# +def property_to_property_map + {} +end + +# +# Loads the current slave into a Hash. +# +def current_slave + return @current_slave if @current_slave + + Chef::Log.debug "Load #{new_resource} slave information" + + launcher_propertys = [] + property_to_property_map.each_pair do |resource_property, groovy_property| + launcher_propertys << "current_slave['#{resource_property}'] = #{groovy_property}" + end + + json = executor.groovy! <<-EOH.gsub(/^ {8}/, '') + import hudson.model.* + import hudson.slaves.* + import jenkins.model.* + import jenkins.slaves.* + + slave = Jenkins.instance.getNode('#{new_resource.slave.name}') as Slave + + if(slave == null) { + return null + } + + def slave_environment = null + slave_env_vars = slave.nodeProperties.get(EnvironmentVariablesNodeProperty.class)?.envVars + if (slave_env_vars) + slave_environment = new java.util.HashMap(slave_env_vars) + + current_slave = [ + name:slave.name, + description:slave.nodeDescription, + remote_fs:slave.remoteFS, + executors:slave.numExecutors.toInteger(), + usage_mode:slave.mode.toString().toLowerCase(), + labels:slave.labelString.split().sort(), + environment:slave_environment, + connected:(slave.computer.connectTime > 0), + online:slave.computer.online + ] + + // Determine retention strategy + if (slave.retentionStrategy instanceof RetentionStrategy.Always) { + current_slave['availability'] = 'always' + } else if (slave.retentionStrategy instanceof RetentionStrategy.Demand) { + current_slave['availability'] = 'demand' + retention = slave.retentionStrategy as RetentionStrategy.Demand + current_slave['in_demand_delay'] = retention.inDemandDelay + current_slave['idle_delay'] = retention.idleDelay + } else { + current_slave['availability'] = null + } + + #{launcher_propertys.join("\n")} + + builder = new groovy.json.JsonBuilder(current_slave) + println(builder) + EOH + + return if json.nil? || json.empty? + + @current_slave = JSON.parse(json, symbolize_names: true) + + # Values that were serialized as nil/null are deserialized as an + # empty string! :( Let's ensure we convert back to nil. + @current_slave = convert_blank_values_to_nil(@current_slave) +end + +# +# Helper method for determining if the given JSON is in sync with the +# current configuration on the Jenkins master. +# +# @return [Boolean] +# +def correct_config? + wanted_slave = { + name: new_resource.slave.name, + description: new_resource.description, + remote_fs: new_resource.remote_fs, + executors: new_resource.executors, + usage_mode: new_resource.usage_mode, + labels: new_resource.labels.sort, + availability: new_resource.availability, + environment: new_resource.environment, + } + + if new_resource.availability.to_s == 'demand' + wanted_slave[:in_demand_delay] = new_resource.in_demand_delay + wanted_slave[:idle_delay] = new_resource.idle_delay + end + + property_to_property_map.each_key do |key| + wanted_slave[key] = new_resource.send(key) + end + + # Don't include connected/online values in the comparison + current_slave.dup.tap do |c| + c.delete(:connected) + c.delete(:online) + end == wanted_slave +end diff --git a/resources/slave_jnlp.rb b/resources/slave_jnlp.rb new file mode 100644 index 0000000000..962076bd9b --- /dev/null +++ b/resources/slave_jnlp.rb @@ -0,0 +1,177 @@ +unified_mode true +use 'partials/slave' + +property :group, String, + default: 'jenkins', + regex: Config[:group_valid_regex] + +property :service_name, String, + default: 'jenkins-slave' + +property :service_groups, Array, + default: lazy { [group] } + +deprecated_property_alias 'runit_groups', 'service_groups', + '`runit_groups` was renamed to `service_groups` with the move to systemd services' + +def load_current_resource + @current_resource ||= Resource::JenkinsJnlpSlave.new(new_resource.name) + super +end + +action :create do + do_create + + declare_resource(:directory, ::File.expand_path(new_resource.remote_fs, '..')) do + recursive(true) + action :create + end + + unless platform?('windows') + declare_resource(:group, new_resource.group) do + system(node['jenkins']['master']['use_system_accounts']) + end + + declare_resource(:user, new_resource.user) do + gid(new_resource.group) + comment('Jenkins slave user - Created by Chef') + home(new_resource.remote_fs) + system(node['jenkins']['master']['use_system_accounts']) + action :create + end + end + + declare_resource(:directory, new_resource.remote_fs) do + owner(new_resource.user) + group(new_resource.group) + recursive(true) + action :create + end + + declare_resource(:remote_file, slave_jar).tap do |r| + # We need to use .tap() to access methods in the provider's scope. + r.source slave_jar_url + r.backup(false) + r.mode('0755') + r.atomic_update(false) + r.notifies :restart, "systemd_unit[#{new_resource.service_name}.service]" unless platform?('windows') + end + + # The Windows's specific child class manages it's own service + return if platform?('windows') + + # disable runit services before starting new service + # TODO: remove in future version + + %W( + /etc/init.d/#{new_resource.service_name} + /etc/service/#{new_resource.service_name} + ).each do |f| + file f do + action :delete + notifies :stop, "service[#{new_resource.service_name}]", :before + end + end + + exec_string = "#{java} #{new_resource.jvm_options}" + exec_string << " -jar #{slave_jar}" if slave_jar + exec_string << " -secret #{jnlp_secret}" if jnlp_secret + exec_string << " -jnlpUrl #{jnlp_url}" + + systemd_unit "#{new_resource.service_name}.service" do + content <<~EOU + # + # Generated by Chef for #{node['fqdn']} + # Changes will be overwritten! + # + + [Unit] + Description=Jenkins JNLP Slave (#{new_resource.service_name}) + After=network.target + + [Service] + Type=simple + User=#{new_resource.user} + Group=#{new_resource.group} + SupplementaryGroups=#{(new_resource.service_groups - [new_resource.group]).join(' ')} + Environment="HOME=#{new_resource.remote_fs}" + Environment="JENKINS_HOME=#{new_resource.remote_fs}" + WorkingDirectory=#{new_resource.remote_fs} + ExecStart=#{exec_string} + + [Install] + WantedBy=multi-user.target + EOU + action :create + end + + service new_resource.service_name do + action [:enable, :start] + end +end + +action :delete do + # Stop and remove the service + service "#{new_resource.service_name}" do + action [:disable, :stop] + end + + do_delete +end + +action_class do + # + # @see Chef::Resource::JenkinsSlave#launcher_groovy + # @see http://javadoc.jenkins-ci.org/hudson/slaves/JNLPLauncher.html + # + def launcher_groovy + 'launcher = new hudson.slaves.JNLPLauncher()' + end + + # + # The path (url) of the slave's unique JNLP file on the Jenkins + # master. + # + # @return [String] + # + def jnlp_url + @jnlp_url ||= uri_join(endpoint, 'computer', new_resource.slave_name, 'slave-agent.jnlp') + end + + # + # Generates the slaves unique JNLP secret using the Groovy API. + # + # @return [String] + # + def jnlp_secret + return @jnlp_secret if @jnlp_secret + json = executor.groovy! <<~EOH + output = [ + secret:jenkins.slaves.JnlpSlaveAgentProtocol.SLAVE_SECRET.mac('#{new_resource.slave_name}') + ] + + builder = new groovy.json.JsonBuilder(output) + println(builder) + EOH + output = JSON.parse(json, symbolize_names: true) + @jnlp_secret = output[:secret] + end + + # + # The url of the +slave.jar+ on the Jenkins master. + # + # @return [String] + # + def slave_jar_url + @slave_jar_url ||= uri_join(endpoint, 'jnlpJars', 'slave.jar') + end + + # + # The path to the +slave.jar+ on disk (which may or may not exist). + # + # @return [String] + # + def slave_jar + ::File.join(new_resource.remote_fs, 'slave.jar') + end +end diff --git a/resources/slave_ssh.rb b/resources/slave_ssh.rb new file mode 100644 index 0000000000..0eaed9bb9f --- /dev/null +++ b/resources/slave_ssh.rb @@ -0,0 +1,122 @@ +unified_mode true +use 'partials/slave' +use 'partials/credentials' + +property :host, + String +property :port, + Integer, + default: 22 +property :credentials, + String + # [String, Resource::JenkinsCredentials] +property :command_prefix, + String +property :command_suffix, + String +property :launch_timeout, + Integer +property :ssh_retries, + Integer +property :ssh_wait_retries, + Integer + +# +# The credentials to SSH into the slave with. Credentials can be any +# of the following: +# +# * username which maps to a valid Jenkins credentials instance. +# * UUID of a Jenkins credentials instance. +# * A `Chef::Resource::JenkinsCredentials` instance. +# +# @return [String] +# +def parsed_credentials + if credentials.is_a?(Resource::JenkinsCredentials) + credentials.send(:id) + else + credentials.to_s + end +end + +def load_current_resource + @current_resource ||= Resource::JenkinsSshSlave.new(new_resource.name) + + super + + if current_slave + @current_resource.host(current_slave[:host]) + @current_resource.port(current_slave[:port]) + @current_resource.credentials(current_slave[:credentials]) + @current_resource.jvm_options(current_slave[:jvm_options]) + @current_resource.java_path(current_slave[:java_path]) + @current_resource.launch_timeout(current_slave[:launch_timeout]) + @current_resource.ssh_retries(current_slave[:ssh_retries]) + @current_resource.ssh_wait_retries(current_slave[:ssh_wait_retries]) + end + + @current_resource +end + +action_class do + # + # @see Chef::Resource::JenkinsSlave#launcher_groovy + # @see https://github.com/jenkinsci/ssh-credentials-plugin/blob/master/src/main/java/com/cloudbees/jenkins/plugins/sshcredentials/impl/BasicSSHUserPrivateKey.java + # @see https://github.com/jenkinsci/ssh-slaves-plugin/blob/master/src/main/java/hudson/plugins/sshslaves/SSHLauncher.java + # + def launcher_groovy + <<-EOH.gsub(/^ {8}/, '') + import hudson.plugins.sshslaves.verifiers.* + #{credential_lookup_groovy('credentials')} + launcher = + new hudson.plugins.sshslaves.SSHLauncher( + #{convert_to_groovy(new_resource.host)}, + #{convert_to_groovy(new_resource.port)}, + #{convert_to_groovy(new_resource.credentials)}, + #{convert_to_groovy(new_resource.jvm_options)}, + #{convert_to_groovy(new_resource.java_path)}, + #{convert_to_groovy(new_resource.command_prefix)}, + #{convert_to_groovy(new_resource.command_suffix)}, + #{convert_to_groovy(new_resource.launch_timeout)}, + #{convert_to_groovy(new_resource.ssh_retries)}, + #{convert_to_groovy(new_resource.ssh_wait_retries)}, + new ManuallyTrustedKeyVerificationStrategy(false) + ) + EOH + end + + # + # @see Chef::Resource::JenkinsSlave#attribute_to_property_map + # + def attribute_to_property_map + map = { + host: 'slave.launcher.host', + port: 'slave.launcher.port', + jvm_options: 'slave.launcher.jvmOptions', + java_path: 'slave.launcher.javaPath', + command_prefix: 'slave.launcher.prefixStartSlaveCmd', + command_suffix: 'slave.launcher.suffixStartSlaveCmd', + launch_timeout: 'slave.launcher.launchTimeoutSeconds', + ssh_retries: 'slave.launcher.maxNumRetries', + ssh_wait_retries: 'slave.launcher.retryWaitTime', + } + + map[:credentials] = 'slave.launcher.credentialsId' + + map + end + + # + # A Groovy snippet that will set the requested local Groovy variable + # to an instance of the credentials represented by + # `new_resource.parsed_credentials`. + # + # @param [String] groovy_variable_name + # @return [String] + # + def credential_lookup_groovy(groovy_variable_name = 'credentials_id') + <<-EOH.gsub(/^ {8}/, '') + #{credentials_for_id_groovy(new_resource.parsed_credentials, groovy_variable_name)} + EOH + end +end diff --git a/libraries/slave_windows.rb b/resources/slave_windows.rb similarity index 82% rename from libraries/slave_windows.rb rename to resources/slave_windows.rb index 75266a9b71..4628fa5856 100644 --- a/libraries/slave_windows.rb +++ b/resources/slave_windows.rb @@ -1,64 +1,33 @@ -# -# Cookbook:: jenkins -# Resource:: windows_slave -# -# Author:: Seth Chisamore -# -# Copyright:: 2013-2019, 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. -# - require_relative 'slave' require_relative 'slave_jnlp' +use 'partials/_jnlp_slave' -class Chef - class Resource::JenkinsWindowsSlave < Resource::JenkinsJnlpSlave - resource_name :jenkins_windows_slave # Still needed for Chef 15 and below - provides :jenkins_windows_slave - - # Actions - actions :create, :delete, :connect, :disconnect, :online, :offline - default_action :create - - # Attributes - attribute :password, - kind_of: String - attribute :user, - kind_of: String, - default: 'LocalSystem' - attribute :remote_fs, - kind_of: String, - default: 'C:\jenkins' - attribute :winsw_url, - kind_of: String, - default: 'http://repo.jenkins-ci.org/releases/com/sun/winsw/winsw/1.17/winsw-1.17-bin.exe' - attribute :winsw_checksum, - kind_of: String, - default: '5859b114d96800a2b98ef9d19eaa573a786a422dad324547ef25be181389df01' - attribute :path, - kind_of: String - attribute :pre_run_cmds, - kind_of: Array, - default: [] - attribute :jnlp_options, - kind_of: String - end -end +property :password, + String +property :user, + String, + default: 'LocalSystem' +property :remote_fs, + String, + default: 'C:\jenkins' +property :winsw_url, + String, + default: 'http://repo.jenkins-ci.org/releases/com/sun/winsw/winsw/1.17/winsw-1.17-bin.exe' +property :winsw_checksum, + String, + default: '5859b114d96800a2b98ef9d19eaa573a786a422dad324547ef25be181389df01' +property :path, + String +property :pre_run_cmds, + Array, + default: [] +property :jnlp_options, + String class Chef class Provider::JenkinsWindowsSlave < Provider::JenkinsJnlpSlave provides :jenkins_windows_slave, platform: %w(windows) + unified_mode true def load_current_resource @current_resource ||= Resource::JenkinsWindowsSlave.new(new_resource.name) diff --git a/resources/user.rb b/resources/user.rb new file mode 100644 index 0000000000..c81e6f5c02 --- /dev/null +++ b/resources/user.rb @@ -0,0 +1,134 @@ +unified_mode true +require 'json' + +property :id, + String, + name_property: true +property :full_name, + String +property :email, + String +property :public_keys, + Array, + default: [] +property :password, + String + +attr_writer :exists + +# +# Determine if the user exists on the master. This value is set by +# the provider when the current resource is loaded. +# +# @return [Boolean] +# +def exists? + !@exists.nil? && @exists +end + +include Jenkins::Helper + +def load_current_resource + @current_resource ||= Resource::JenkinsUser.new(new_resource.id) + + if current_user + @current_resource.exists = true + @current_resource.full_name(current_user[:full_name]) + @current_resource.email(current_user[:email]) + @current_resource.public_keys(current_user[:public_keys]) + end + + @current_resource +end + +action :create do + if current_resource.exists? && + (new_resource.full_name.nil? || current_resource.full_name == new_resource.full_name) && + (new_resource.email.nil? || current_resource.email == new_resource.email) && + current_resource.public_keys == new_resource.public_keys + Chef::Log.info("#{new_resource} exists - skipping") + else + converge_by("Create #{new_resource}") do + executor.groovy! <<-EOH.gsub(/^ {12}/, '') + user = hudson.model.User.get('#{new_resource.id}') + user.setFullName('#{new_resource.full_name}') + + if (jenkins.model.Jenkins.instance.pluginManager.getPlugin('mailer')) { + propertyClass = this.class.classLoader.loadClass('hudson.tasks.Mailer$UserProperty') + email = propertyClass.newInstance('#{new_resource.email}') + user.addProperty(email) + } + + password = hudson.security.HudsonPrivateSecurityRealm.Details.fromPlainPassword('#{new_resource.password}') + user.addProperty(password) + + keys = new org.jenkinsci.main.modules.cli.auth.ssh.UserPropertyImpl('#{new_resource.public_keys.join('\n')}') + user.addProperty(keys) + + user.save() + EOH + end + end +end + +action :delete do + if current_resource.exists? + converge_by("Delete #{new_resource}") do + executor.groovy! <<-EOH.gsub(/^ {12}/, '') + user = hudson.model.User.get('#{new_resource.id}', false) + user.delete() + EOH + end + else + Chef::Log.debug("#{new_resource} does not exist - skipping") + end +end + +action_class do + # + # Loads the local user into a hash + # + def current_user + return @current_user if @current_user + + Chef::Log.debug "Load #{new_resource} user information" + + json = executor.groovy <<-EOH.gsub(/^ {8}/, '') + user = hudson.model.User.get('#{new_resource.id}', false) + + if(user == null) { + return null + } + + id = user.getId() + name = user.getFullName() + + email = null + emailProperty = user.getProperty(hudson.tasks.Mailer.UserProperty) + if(emailProperty != null) { + email = emailProperty.getAddress() + } + + keys = null + keysProperty = user.getProperty(org.jenkinsci.main.modules.cli.auth.ssh.UserPropertyImpl) + if(keysProperty != null) { + keys = keysProperty.authorizedKeys.split('\\n') - "" // Remove empty strings + } + + builder = new groovy.json.JsonBuilder() + builder { + id id + full_name name + email email + public_keys keys + } + + println(builder) + EOH + + return if json.nil? || json.empty? + + @current_user = JSON.parse(json, symbolize_names: true) + @current_user + end +end diff --git a/resources/view.rb b/resources/view.rb new file mode 100644 index 0000000000..d7f087b1b5 --- /dev/null +++ b/resources/view.rb @@ -0,0 +1,155 @@ +unified_mode true + +property :jobs, + Array, + default: [] +property :code, + String, + default: '' + +attr_writer :exists + +# +# Determine if the view exists on the master. This value is set by the +# provider when the current resource is loaded. +# +# @return [Boolean] +# +def exists? + !@exists.nil? && @exists +end + +# class ViewDoesNotExist < StandardError +# def initialize(view, action) +# super <<-EOH +# The Jenkins view `#{view}' does not exist. In order to #{action} `#{view}', that +# view must first exist on the Jenkins master! +# EOH +# end +# end + +include Jenkins::Helper + +def load_current_resource + @current_resource ||= Resource::JenkinsView.new(new_resource.name) + @current_resource.name(new_resource.name) + @current_resource.jobs(new_resource.jobs) + + @current_resource.exists = current_view ? true : false + + @current_resource +end + +# +# Create a new Jenkins view with the current resource's name +# and given configuration. If the view already exists, update it. If the +# view does not exist, one will be created from the given # `config` XML +# file using the Jenkins CLI. +# +# If `code` is passed then the view is not necessarily created idempotently +# as we cannot guarantee what the user has in mind +# +action(:create) do + current_view_jobs = current_view[:jobs] + current_view_jobs ||= [] + + if current_resource.exists? && + current_view_jobs == new_resource.jobs && + new_resource.code == '' + Chef::Log.debug("#{new_resource} exists - skipping") + else + jobs_to_remove = current_view_jobs - new_resource.jobs + jobs_to_add = new_resource.jobs - current_view_jobs + + create_view = + <<-GROOVY + import hudson.model.* + import jenkins.model.* + def view_name = '#{new_resource.name}' + def jenkins = Jenkins.instance + + def create_view = { name -> + return new ListView(name) + } + + def configure_view = { view -> + #{jobs_to_remove}.each { view.remove(jenkins.getItem(it)) } + #{jobs_to_add}.each { view.add(jenkins.getItem(it)) } + } + + #{new_resource.code} + + def view = jenkins.getView(view_name) + if (!view) { + view = create_view(view_name) + jenkins.addView(view) + } + configure_view(view) + + jenkins.save() + GROOVY + + converge_by("Create #{new_resource}") do + executor.groovy!(create_view) + end + end +end + +# +# Delete a Jenkins view with the current resource's name. If +# the view does not exist, no action will be taken. If the view does exist, +# it will be deleted using the Jenkins CLI. +# +action(:delete) do + if current_resource.exists? + converge_by("Delete #{new_resource}") do + executor.execute!('delete-view', escape(new_resource.name)) + end + else + Chef::Log.debug("#{new_resource} does not exist - skipping") + end +end + +private + +# +# The view in a hash format +# +# @return [Hash] +# Empty hash if the job does not exist, or a hash of important information +# if it does +# +def current_view + return @current_view if @current_view + + Chef::Log.debug "Load #{new_resource} view information" + + get_view_as_json = + <<-GROOVY + import hudson.model.* + import jenkins.model.* + view_name = '#{new_resource.name}' + jenkins = Jenkins.instance + + def view_variables = new groovy.json.JsonBuilder() + view_variables {} + + // Output view as JSON, easily parse-able by ruby + view = jenkins.getView(view_name) + if (view) { + view_variables { + name view_name + jobs view.getItems().collect { it.name } + } + } + + println view_variables.toString() + GROOVY + + response = executor.groovy!(get_view_as_json) + return if response.nil? + + Chef::Log.debug "Parse #{new_resource} as JSON" + @current_view = JSON.parse(response, object_class: Mash) + @current_view +end diff --git a/spec/libraries/executor_spec.rb b/spec/libraries/executor_spec.rb deleted file mode 100644 index a683788e80..0000000000 --- a/spec/libraries/executor_spec.rb +++ /dev/null @@ -1,187 +0,0 @@ -require 'spec_helper' - -describe Jenkins::Executor do - describe '.initialize' do - it 'uses default options' do - options = described_class.new.options - expect(options[:cli]).to eq('/usr/share/jenkins/cli/java/cli.jar') - expect(options[:java]).to eq('java') - end - - it 'overrides with options from the initializer' do - options = described_class.new(cli: 'foo', java: 'bar').options - expect(options[:cli]).to eq('foo') - expect(options[:java]).to eq('bar') - end - end - - describe '#execute!' do - let(:shellout) { double(run_command: nil, error!: nil, stdout: '') } - before { allow(Mixlib::ShellOut).to receive(:new).and_return(shellout) } - before do - allow(File).to receive(:file?).with('/etc/cli_cred_file').and_return(true) - end - - # it 'wraps the java and jar paths in quotes' do - # command = %("java" -jar "/usr/share/jenkins/cli/java/cli.jar" foo) - # expect(Mixlib::ShellOut).to receive(:new).with(command, timeout: 60) - # subject.execute!('foo') - # end - - # context 'when no options are given' do - # it 'builds the correct command' do - # command = %("java" -jar "/usr/share/jenkins/cli/java/cli.jar" foo) - # expect(Mixlib::ShellOut).to receive(:new).with(command, timeout: 60) - # subject.execute!('foo') - # end - # end - - context 'when an :endpoint option is given' do - # it 'builds the correct command' do - # subject.options[:endpoint] = 'http://jenkins.ci' - # command = %("java" -jar "/usr/share/jenkins/cli/java/cli.jar" -s http://jenkins.ci foo) - # expect(Mixlib::ShellOut).to receive(:new).with(command, timeout: 60) - # subject.execute!('foo') - # end - - # it 'escapes the endpoint' do - # subject.options[:endpoint] = 'http://jenkins.ci?foo=this is a text' - # command = %("java" -jar "/usr/share/jenkins/cli/java/cli.jar" -s http://jenkins.ci?foo=this%20is%20a%20text foo) - # expect(Mixlib::ShellOut).to receive(:new).with(command, timeout: 60) - # subject.execute!('foo') - # end - end - - context 'when a :cli_username option is given' do - context 'when a :cli_password option is given' do - # it 'adds -auth option' do - # subject.options[:cli_username] = 'user' - # subject.options[:cli_password] = 'password' - # command = %("java" -jar "/usr/share/jenkins/cli/java/cli.jar" -auth user:password foo) - # expect(Mixlib::ShellOut).to receive(:new).with(command, timeout: 60) - # subject.execute!('foo') - # end - end - end - - context 'when a :cli_credential_file option is given' do - # i - end - - context 'when a :key option is given' do - # it 'builds the correct command' do - # subject.options[:key] = '/key/path.pem' - # command = %("java" -jar "/usr/share/jenkins/cli/java/cli.jar" -i "/key/path.pem" foo) - # expect(Mixlib::ShellOut).to receive(:new).with(command, timeout: 60) - # subject.execute!('foo') - # end - - # it 'wraps key path in quotes' do - # subject.options[:key] = '/key/path/to /pem with/spaces.pem' - # command = %("java" -jar "/usr/share/jenkins/cli/java/cli.jar" -i "/key/path/to /pem with/spaces.pem" foo) - # expect(Mixlib::ShellOut).to receive(:new).with(command, timeout: 60) - # subject.execute!('foo') - # end - - context 'the private key is unknown to the Jenkins instance' do - before do - # This is really ugly but there is no easy way to stub a method to - # raise an exception a set number of times. - @times = 0 - allow(shellout).to receive(:error!) do - @times += 1 - raise Mixlib::ShellOut::ShellCommandFailed unless @times > 2 - end - allow(shellout).to receive(:exitstatus).and_return(255, 1, 0) - allow(shellout).to receive(:stderr).and_return( - 'Authentication failed. No private key accepted.', - 'Exception in thread "main" java.io.EOFException', - '' - ) - end - - # it 'retrys the command without a private key' do - # subject.options[:key] = '/key/path.pem' - # command = %("java" -jar "/usr/share/jenkins/cli/java/cli.jar" -i "/key/path.pem" foo) - # expect(Mixlib::ShellOut).to receive(:new).with(command, timeout: 60) - # command_no_key = %("java" -jar "/usr/share/jenkins/cli/java/cli.jar" foo) - # expect(Mixlib::ShellOut).to receive(:new).with(command_no_key, timeout: 60) - # subject.execute!('foo') - # end - end - end - - context 'when a :proxy option is given' do - # it 'builds the correct command' do - # subject.options[:proxy] = 'http://proxy.jenkins.ci' - # command = %("java" -jar "/usr/share/jenkins/cli/java/cli.jar" -p http://proxy.jenkins.ci foo) - # expect(Mixlib::ShellOut).to receive(:new).with(command, timeout: 60) - # subject.execute!('foo') - # end - - # it 'escapes the proxy' do - # subject.options[:proxy] = 'http://proxy.jenkins.ci?foo=this is a text' - # command = %("java" -jar "/usr/share/jenkins/cli/java/cli.jar" -p http://proxy.jenkins.ci?foo=this%20is%20a%20text foo) - # expect(Mixlib::ShellOut).to receive(:new).with(command, timeout: 60) - # subject.execute!('foo') - # end - end - - # context 'when :jvm_options option is given' do - # it 'builds the correct command' do - # subject.options[:jvm_options] = '-Djava.arg1=foo -Djava.arg2=bar' - # command = %("java" -Djava.arg1=foo -Djava.arg2=bar -jar "/usr/share/jenkins/cli/java/cli.jar" foo) - # expect(Mixlib::ShellOut).to receive(:new).with(command, timeout: 60) - # subject.execute!('foo') - # end - # end - - # context 'when execute! with options' do - # let(:stdin) { "hello\nworld" } - # it 'pass to shellout' do - # command = '"java" -jar "/usr/share/jenkins/cli/java/cli.jar" foo' - # expect(Mixlib::ShellOut).to receive(:new).with(command, timeout: 60, input: stdin) - # subject.execute!('foo', input: stdin) - # end - # end - - context 'when the command fails' do - it 'raises an error' do - allow(shellout).to receive(:error!).and_raise(RuntimeError) - expect { subject.execute!('bad') }.to raise_error(RuntimeError) - end - end - end - - describe '#execute' do - before { allow(subject).to receive(:execute!) } - - it 'calls #execute!' do - expect(subject).to receive(:execute).with('foo', 'bar') - subject.execute('foo', 'bar') - end - - context 'when the command fails' do - it 'does not raise an error' do - allow(subject).to receive(:execute!).and_raise(Mixlib::ShellOut::ShellCommandFailed) - expect { subject.execute('foo') }.to_not raise_error - end - end - end - - describe '#groovy!' do - before { allow(subject).to receive(:execute!) } - - # i - end - - describe '#groovy' do - before { allow(subject).to receive(:execute) } - - it 'calls execute' do - expect(subject).to receive(:execute) - .with('groovy =', input: 'script') - subject.groovy('script') - end - end -end diff --git a/spec/libraries/view_spec.rb b/spec/libraries/view_spec.rb deleted file mode 100644 index f55f8c0d08..0000000000 --- a/spec/libraries/view_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'spec_helper' - -RSpec.describe Chef::Provider::JenkinsView do - describe 'provides :jenkins_view' do - before do - allow(described_class).to receive(:new).and_wrap_original do |m, *args| - view_double = double('view').tap do |d| - allow(d).to receive(:[]).with(:jobs).and_return([]) - end - m.call(*args).tap do |v| - allow(v).to receive(:current_view).and_return(view_double) - allow(v).to receive(:executor) - .and_return(double('executor').as_null_object) - end - end - end - - step_into :jenkins_view - - recipe do - jenkins_view 'ham' do - jobs %w(pig giraffe) - end - end - - it 'should not raise Chef::Exceptions::ProviderNotFound' do - expect { chef_run }.not_to raise_error - end - end -end diff --git a/spec/recipes/java_spec.rb b/spec/recipes/java_spec.rb deleted file mode 100644 index 0f00ad6d73..0000000000 --- a/spec/recipes/java_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'spec_helper' - -describe 'jenkins::java' do - context 'on Debian' do - cached(:chef_run) do - ChefSpec::SoloRunner.new(platform: 'debian').converge(described_recipe) - end - - it 'installs openjdk-8-jdk' do - expect(chef_run).to install_package('openjdk-8-jdk') - end - end - - context 'on Ubuntu 16.04' do - cached(:chef_run) do - ChefSpec::SoloRunner.new(platform: 'ubuntu').converge(described_recipe) - end - - it 'installs openjdk-8-jdk' do - expect(chef_run).to install_package('openjdk-8-jdk') - end - end - - context 'on CentOS' do - cached(:chef_run) do - ChefSpec::SoloRunner.new(platform: 'centos').converge(described_recipe) - end - - it 'installs java-1.8.0-openjdk' do - expect(chef_run).to install_package('java-1.8.0-openjdk') - end - end - - context 'on Amazon Linux' do - cached(:chef_run) do - ChefSpec::SoloRunner.new(platform: 'amazon').converge(described_recipe) - end - - it 'installs java-1.8.0-openjdk' do - expect(chef_run).to install_package('java-1.8.0-openjdk') - end - end - - context 'on an unsupported platform' do - cached(:chef_run) do - ChefSpec::SoloRunner.new(platform: 'mac_os_x').converge(described_recipe) - end - - it 'raises an exception' do - expect { chef_run } - .to raise_error(RuntimeError, "`mac_os_x' is not supported!") - end - end -end