jenkins2 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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