jenkins2 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
4
  require 'logger'
3
5
 
@@ -5,11 +7,12 @@ module Jenkins2
5
7
  class Log
6
8
  extend SingleForwardable
7
9
 
8
- def self.init( log: $stdout, verbose: 0 )
10
+ def self.init(log: $stderr, verbose: 0)
11
+ log ||= $stderr
9
12
  @logger = Logger.new log
10
- @logger.level = Logger::ERROR - verbose
11
- @logger.formatter = proc do |severity, datetime, progname, msg|
12
- if log == $stdout
13
+ @logger.level = Logger::ERROR - verbose.to_i
14
+ @logger.formatter = proc do |severity, datetime, _progname, msg|
15
+ if [$stdout, $stderr].include?(log)
13
16
  "#{msg}\n"
14
17
  else
15
18
  "[#{datetime.strftime '%FT%T%:z'}] #{severity} #{msg}\n"
@@ -20,10 +23,8 @@ module Jenkins2
20
23
 
21
24
  def_delegators :logger, :debug, :info, :warn, :error, :fatal, :unknown, :level
22
25
 
23
- private
24
-
25
26
  def self.logger
26
- @logger ||= self.init
27
+ @logger ||= init
27
28
  end
28
29
  end
29
30
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+
5
+ require_relative 'util'
6
+
7
+ module Jenkins2
8
+ class ResourceProxy < ::BasicObject
9
+ attr_reader :connection, :path
10
+
11
+ def initialize(connection, path, params={}, &block)
12
+ @path = path
13
+ @id = nil
14
+ @connection, @params = connection, params
15
+ subject if block
16
+ end
17
+
18
+ def method_missing(message, *args, &block)
19
+ if respond_to_missing? message
20
+ ::Jenkins2::Log.debug message
21
+ subject.send(message, *args, &block)
22
+ else
23
+ super
24
+ end
25
+ end
26
+
27
+ def respond_to_missing?(method_name, include_private=false)
28
+ subject.respond_to? method_name, include_private
29
+ end
30
+
31
+ def raw
32
+ @raw ||= connection.get_json(build_path, @params)
33
+ end
34
+
35
+ def subject
36
+ @subject ||= ::JSON.parse(raw.body, object_class: ::OpenStruct)
37
+ end
38
+
39
+ private
40
+
41
+ def build_path(*sections)
42
+ escaped_sections = [@id, sections].flatten.compact.collect{|i| ::ERB::Util.url_encode i }
43
+ ::File.join(@path, *escaped_sections)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+
5
+ require_relative 'log'
6
+ require_relative 'errors'
7
+
8
+ module Jenkins2
9
+ module Util
10
+ extend self
11
+ # Waits for a block to return +truthful+ value. Useful, for example, when you set a node
12
+ # temporarily offline, and then wait for it to become idle.
13
+ # +max_wait_minutes+:: Maximum wait time in minutes.
14
+ # +&block+:: Run this block until it returs true, max_wait_minutes pass or block throws some
15
+ # kind of exception.
16
+ #
17
+ # Returns the result of a block, if it eventually succeeded or nil in case of timeout.
18
+ #
19
+ # Note that this is both a method of module Util, so you can <tt>include Jenkins::Util</tt>
20
+ # into your classes so they have a #wait method, as well as a module method, so you can call it
21
+ # directly as ::wait().
22
+ def wait(max_wait_minutes: 60)
23
+ [3, 5, 7, 15, 30, [60] * (max_wait_minutes - 1)].flatten.each do |sec|
24
+ begin
25
+ result = yield
26
+ return result if result
27
+ Log.warn{ "Received result is not truthy: #{result}." }
28
+ Log.warn{ "Retry request in #{sec} seconds." }
29
+ sleep sec
30
+ rescue Jenkins2::NotFoundError, Jenkins2::ServiceUnavailableError => e
31
+ Log.warn{ "Received error: #{e}." }
32
+ Log.warn{ "Retry request in #{sec} seconds." }
33
+ sleep sec
34
+ end
35
+ end
36
+ Log.error{ "Tired of waiting (#{max_wait_minutes} minutes). Give up." }
37
+ nil
38
+ end
39
+
40
+ # Tries a block several times, if raised exception is <tt>Net::HTTPFatalError</tt>,
41
+ # <tt>Errno::ECONNREFUSED</tt> or <tt>Net::ReadTimeout</tt>.
42
+ # +retries+:: Number of retries.
43
+ # +retry_delay+:: Seconds to sleep, before attempting next retry.
44
+ # +&block+:: Code to run inside <tt>retry</tt> loop.
45
+ #
46
+ # Returns the result of a block, if it eventually succeeded or throws the exception, thown by
47
+ # the block on last try.
48
+ #
49
+ # Note that this is both a method of module Util, so you can <tt>include Jenkins::Util</tt>
50
+ # into your classes so they have a #try method, as well as a module method, so you can call it
51
+ # directly as ::try().
52
+ def try(retries: 3, retry_delay: 5)
53
+ yield
54
+ rescue Errno::ECONNREFUSED, Net::HTTPFatalError, Net::ReadTimeout => e
55
+ i ||= 0
56
+ unless (i += 1) == retries
57
+ Log.warn{ "Received error: #{e}." }
58
+ Log.warn{ "Retry request in #{retry_delay} seconds. Retry number #{i}." }
59
+ sleep retry_delay
60
+ retry
61
+ end
62
+ Log.error{ "Received error: #{e}." }
63
+ Log.error{ "Reached maximum number of retries (#{retries}). Give up." }
64
+ raise e
65
+ end
66
+ end
67
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Jenkins2
2
- VERSION = '0.1.0'
4
+ VERSION = '1.0.0'
3
5
  end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jenkins2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Juri Timošin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-10-11 00:00:00.000000000 Z
11
+ date: 2018-02-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rake
14
+ name: ci_reporter_minitest
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '11.3'
19
+ version: '1.0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '11.3'
26
+ version: '1.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: minitest
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -39,33 +39,33 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '5.5'
41
41
  - !ruby/object:Gem::Dependency
42
- name: ci_reporter_minitest
42
+ name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '1.0'
47
+ version: '11.3'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '1.0'
54
+ version: '11.3'
55
55
  - !ruby/object:Gem::Dependency
56
- name: mocha
56
+ name: rubocop
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '1.1'
61
+ version: '0.52'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '1.1'
68
+ version: '0.52'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: simplecov
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -80,34 +80,6 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0.10'
83
- - !ruby/object:Gem::Dependency
84
- name: test-kitchen
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: '1.17'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "~>"
95
- - !ruby/object:Gem::Version
96
- version: '1.17'
97
- - !ruby/object:Gem::Dependency
98
- name: kitchen-lxd
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - "~>"
102
- - !ruby/object:Gem::Version
103
- version: '0.2'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - "~>"
109
- - !ruby/object:Gem::Version
110
- version: '0.2'
111
83
  description: Command line interface and API client for Jenkins 2. Allows manipulating
112
84
  nodes, jobs, plugins, credentials. See README.md for details.
113
85
  email: draco.ater@gmail.com
@@ -121,17 +93,29 @@ files:
121
93
  - README.md
122
94
  - bin/jenkins2
123
95
  - lib/jenkins2.rb
124
- - lib/jenkins2/client.rb
125
- - lib/jenkins2/client/credential_commands.rb
126
- - lib/jenkins2/client/node_commands.rb
127
- - lib/jenkins2/client/plugin_commands.rb
128
- - lib/jenkins2/cmdparse.rb
129
- - lib/jenkins2/command_line.rb
96
+ - lib/jenkins2/api.rb
97
+ - lib/jenkins2/api/computer.rb
98
+ - lib/jenkins2/api/credentials.rb
99
+ - lib/jenkins2/api/job.rb
100
+ - lib/jenkins2/api/plugins.rb
101
+ - lib/jenkins2/api/root.rb
102
+ - lib/jenkins2/api/rud.rb
103
+ - lib/jenkins2/api/user.rb
104
+ - lib/jenkins2/api/view.rb
105
+ - lib/jenkins2/cli.rb
106
+ - lib/jenkins2/cli/credentials.rb
107
+ - lib/jenkins2/cli/job.rb
108
+ - lib/jenkins2/cli/nodes.rb
109
+ - lib/jenkins2/cli/plugins.rb
110
+ - lib/jenkins2/cli/root.rb
111
+ - lib/jenkins2/cli/user.rb
112
+ - lib/jenkins2/cli/view.rb
113
+ - lib/jenkins2/connection.rb
114
+ - lib/jenkins2/errors.rb
130
115
  - lib/jenkins2/log.rb
131
- - lib/jenkins2/try.rb
132
- - lib/jenkins2/uri.rb
116
+ - lib/jenkins2/resource_proxy.rb
117
+ - lib/jenkins2/util.rb
133
118
  - lib/jenkins2/version.rb
134
- - lib/jenkins2/wait.rb
135
119
  homepage: https://bitbucket.org/DracoAter/jenkins2
136
120
  licenses:
137
121
  - MIT
@@ -144,7 +128,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
144
128
  requirements:
145
129
  - - "~>"
146
130
  - !ruby/object:Gem::Version
147
- version: '2.0'
131
+ version: '2.3'
148
132
  required_rubygems_version: !ruby/object:Gem::Requirement
149
133
  requirements:
150
134
  - - ">="
@@ -1,127 +0,0 @@
1
- require 'net/http'
2
- require 'openssl'
3
- require 'json'
4
-
5
- require_relative 'log'
6
- require_relative 'uri'
7
- require_relative 'wait'
8
- require_relative 'client/credential_commands'
9
- require_relative 'client/node_commands'
10
- require_relative 'client/plugin_commands'
11
-
12
- module Jenkins2
13
- # The entrance point for your Jenkins remote management.
14
- class Client
15
- include CredentialCommands
16
- include PluginCommands
17
- include NodeCommands
18
- # Creates a "connection" to Jenkins.
19
- # Keyword parameters:
20
- # +server+:: Jenkins Server URL.
21
- # +user+:: Jenkins API user. Can be omitted, if no authentication required.
22
- # +key+:: Jenkins API key. Can be omitted, if no authentication required.
23
- def initialize( **args )
24
- @server = args[:server]
25
- @user = args[:user]
26
- @key = args[:key]
27
- @crumb = nil
28
- end
29
-
30
- # Returns Jenkins version
31
- def version
32
- api_request( :get, '/', :raw )['X-Jenkins']
33
- end
34
-
35
- # Stops executing new builds, so that the system can be eventually shut down safely.
36
- # Parameters are ignored
37
- def prepare_for_shutdown( **args )
38
- api_request( :post, '/quietDown' )
39
- end
40
-
41
- # Forcefully restart Jenkins NOW!
42
- # Parameters are ignored
43
- def restart!( **args )
44
- api_request( :post, '/restart' )
45
- end
46
-
47
- # Cancels the effect of +prepare-for-shutdown+ command.
48
- # Parameters are ignored
49
- def cancel_shutdown( **args )
50
- api_request( :post, '/cancelQuietDown' )
51
- end
52
-
53
- # Waits for all the nodes to become idle or until +max_wait_minutes+ pass. Is expected to be
54
- # called after +prepare_for_shutdown+, otherwise new builds will still be run.
55
- # +max_wait_minutes+:: Maximum wait time in minutes. Default 60.
56
- def wait_nodes_idle( max_wait_minutes: 60 )
57
- Wait.wait( max_wait_minutes: max_wait_minutes ) do
58
- api_request( :get, '/computer/api/json' )['busyExecutors'].zero?
59
- end
60
- end
61
-
62
- # Job Commands
63
-
64
- # Starts a build
65
- # +job_name+:: Name of the job to build
66
- # +build_params+:: Build parameters as hash, where keys are names of variables.
67
- def build( **args )
68
- job, params = args[:job], args[:params]
69
- if params.nil? or params.empty?
70
- api_request( :post, "/job/#{job}/build" )
71
- else
72
- api_request( :post, "/job/#{job}/buildWithParameters" ) do |req|
73
- req.form_data = params
74
- end
75
- end
76
- end
77
-
78
- private
79
- def api_request( method, path, reply_with=:json )
80
- req = case method
81
- when :get then Net::HTTP::Get
82
- when :post then Net::HTTP::Post
83
- end.new( URI File.join( @server, URI.escape( path ) ) )
84
- Log.debug { "Request: #{method} #{req.uri}" }
85
- req.basic_auth @user, @key
86
- yield req if block_given?
87
- req.content_type ||= 'application/x-www-form-urlencoded'
88
- Log.debug { "Request content_type: #{req.content_type}, body: #{req.body}" }
89
- begin
90
- req[@crumb["crumbRequestField"]] = @crumb["crumb"] if @crumb
91
- response = Net::HTTP.start( req.uri.hostname, req.uri.port, use_ssl: req.uri.scheme == 'https', verify_mode: OpenSSL::SSL::VERIFY_NONE ) do |http|
92
- http.request req
93
- end
94
- handle_response( response, reply_with )
95
- rescue Net::HTTPServerException => e
96
- if e.message == "403 \"No valid crumb was included in the request\""
97
- update_crumbs
98
- retry
99
- else
100
- raise
101
- end
102
- end
103
- end
104
-
105
- def update_crumbs
106
- @crumb = api_request( :get, '/crumbIssuer/api/json' )
107
- end
108
-
109
- def handle_response( response, reply_with )
110
- Log.debug { "Response: #{response.code}, #{response.body}" }
111
- case response
112
- when Net::HTTPSuccess
113
- case reply_with
114
- when :json then JSON.parse response.body
115
- when :body then response.body
116
- when :raw then response
117
- end
118
- when Net::HTTPRedirection
119
- response['location']
120
- when Net::HTTPClientError, Net::HTTPServerError
121
- response.value
122
- else
123
- response.value
124
- end
125
- end
126
- end
127
- end
@@ -1,134 +0,0 @@
1
- module Jenkins2
2
- class Client
3
- module CredentialCommands
4
- BOUNDARY = '----Jenkins2RubyMultipartClient' + rand(1000000).to_s
5
-
6
- # Creates credentials based on provided arguments. Calls either:
7
- # - +create_credential_username_password+
8
- # - +create_credential_ssh+
9
- # - +create_credential_secret_text+
10
- # - +create_credential_secret_file+
11
- # See those functions' documentation for the exact parameters required
12
- def create_credential( **args )
13
- return create_credential_username_password( args ) if args[:password]
14
- return create_credential_ssh( args ) if args[:private_key]
15
- return create_credential_secret_text( args ) if args[:secret]
16
- return create_credential_secret_file( args ) if args[:content]
17
- end
18
-
19
- # Creates username and password credential. Accepts hash with the following parameters.
20
- # +scope+:: Scope of the credential. GLOBAL or SYSTEM
21
- # +id+:: Id of the credential. Will be Generated by Jenkins, if not provided.
22
- # +description+:: Human readable text, what this credential is used for.
23
- # +username+:: Username.
24
- # +password+:: Password in plain text.
25
- def create_credential_username_password( **args )
26
- json_body = { "" => "0",
27
- credentials: args.merge(
28
- '$class' => 'com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl'
29
- )
30
- }.to_json
31
- api_request( :post, '/credentials/store/system/domain/_/createCredentials' ) do |req|
32
- req.body = "json=#{CGI::escape json_body}"
33
- req.content_type = 'application/x-www-form-urlencoded'
34
- end
35
- end
36
-
37
- # Creates ssh username with private key credential. Jenkins must have ssh-credentials plugin
38
- # installed, to use this functionality. Accepts hash with the following parameters.
39
- # +scope+:: Scope of the credential. GLOBAL or SYSTEM
40
- # +id+:: Id of the credential. Will be Generated by Jenkins, if not provided.
41
- # +description+:: Human readable text, what this credential is used for.
42
- # +username+:: Ssh username.
43
- # +private_key+:: Ssh private key, with new lines replaced by <tt>\n</tt> sequence.
44
- # +passphrase+:: Passphrase for the private key. Empty string, if not provided.
45
- def create_credential_ssh( **args )
46
- json_body = { "" => "1",
47
- credentials: {
48
- scope: args[:scope],
49
- username: args[:username],
50
- privateKeySource: {
51
- value: "0",
52
- privateKey: args[:private_key],
53
- 'stapler-class' => 'com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey$DirectEntryPrivateKeySource'
54
- },
55
- passphrase: args[:passphrase],
56
- id: args[:id],
57
- description: args[:description],
58
- '$class' => 'com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey'
59
- }
60
- }.to_json
61
- api_request( :post, '/credentials/store/system/domain/_/createCredentials' ) do |req|
62
- req.body = "json=#{CGI::escape json_body}"
63
- req.content_type = 'application/x-www-form-urlencoded'
64
- end
65
- end
66
- # Creates a secret text credential. Jenkins must have plain-credentials plugin
67
- # installed, to use this functionality. Accepts hash with the following parameters.
68
- # +scope+:: Scope of the credential. GLOBAL or SYSTEM
69
- # +id+:: Id of the credential. Will be Generated by Jenkins, if not provided.
70
- # +description+:: Human readable text, what this credential is used for.
71
- # +secret+:: Some secret text.
72
- def create_credential_secret_text( **args )
73
- json_body = { "" => "3",
74
- credentials: args.merge(
75
- '$class' => 'org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl'
76
- )
77
- }.to_json
78
- api_request( :post, '/credentials/store/system/domain/_/createCredentials' ) do |req|
79
- req.body = "json=#{CGI::escape json_body}"
80
- req.content_type = 'application/x-www-form-urlencoded'
81
- end
82
- end
83
-
84
- # Creates a secret file credential. Jenkins must have plain-credentials plugin
85
- # installed, to use this functionality. Accepts hash with the following parameters.
86
- # +scope+:: Scope of the credential. GLOBAL or SYSTEM
87
- # +id+:: Id of the credential. Will be Generated by Jenkins, if not provided.
88
- # +description+:: Human readable text, what this credential is used for.
89
- # +filename+:: Name of the file.
90
- # +content+:: File content.
91
- def create_credential_secret_file( **args )
92
- filename = args.delete :filename
93
- content = args.delete :content
94
- body = "--#{BOUNDARY}\r\n"
95
- body << "Content-Disposition: form-data; name=\"file0\"; filename=\"#{filename}\"\r\n"
96
- body << "Content-Type: application/octet-stream\r\n\r\n"
97
- body << content
98
- body << "\r\n"
99
- body << "--#{BOUNDARY}\r\n"
100
- body << "Content-Disposition: form-data; name=\"json\"\r\n\r\n"
101
- body << { "" => "2",
102
- credentials: args.merge(
103
- '$class' => 'org.jenkinsci.plugins.plaincredentials.impl.FileCredentialsImpl',
104
- 'file' => 'file0'
105
- )
106
- }.to_json
107
- body << "\r\n\r\n--#{BOUNDARY}--\r\n"
108
- api_request( :post, '/credentials/store/system/domain/_/createCredentials' ) do |req|
109
- req.add_field 'Content-Type', "multipart/form-data, boundary=#{BOUNDARY}"
110
- req.body = body
111
- end
112
- end
113
-
114
- # Deletes credential
115
- # +id+:: Credential's id
116
- def delete_credential( id )
117
- api_request( :post, "/credentials/store/system/domain/_/credential/#{id}/doDelete" ) do |req|
118
- req.content_type = 'application/x-www-form-urlencoded'
119
- end
120
- end
121
-
122
- # Returns credential as json. Raises Net::HTTPNotFound, if no such credential
123
- # +id+:: Credential's id
124
- def get_credential( id )
125
- api_request( :get, "/credentials/store/system/domain/_/credential/#{id}/api/json" )
126
- end
127
-
128
- # Lists all credentials
129
- def list_credentials( store: 'system', domain: '_' )
130
- api_request( :get, "/credentials/store/#{store}/domain/#{domain}/api/json?depth=1" )['credentials']
131
- end
132
- end
133
- end
134
- end