jenkins_api_client 1.0.0.alpha.2 → 1.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -20,12 +20,18 @@
20
20
  # THE SOFTWARE.
21
21
  #
22
22
 
23
+ require 'jenkins_api_client/urihelper'
24
+
23
25
  module JenkinsApi
24
26
  class Client
25
27
  # This class communicates with the Jenkins "/job" API to obtain details
26
28
  # about jobs, creating, deleting, building, and various other operations.
27
29
  #
28
30
  class Job
31
+ include JenkinsApi::UriHelper
32
+
33
+ # Version that jenkins started to include queued build info in build response
34
+ JENKINS_QUEUE_ID_SUPPORT_VERSION = '1.519'
29
35
 
30
36
  # Initialize the Job object and store the reference to Client object
31
37
  #
@@ -74,7 +80,7 @@ module JenkinsApi
74
80
  #
75
81
  def create(job_name, xml)
76
82
  @logger.info "Creating job '#{job_name}'"
77
- @client.post_config("/createItem?name=#{job_name}", xml)
83
+ @client.post_config("/createItem?name=#{form_encode job_name}", xml)
78
84
  end
79
85
 
80
86
  # Update a job with the name specified and the xml given
@@ -400,7 +406,7 @@ module JenkinsApi
400
406
  #
401
407
  def rename(old_job, new_job)
402
408
  @logger.info "Renaming job '#{old_job}' to '#{new_job}'"
403
- @client.api_post_request("/job/#{old_job}/doRename?newName=#{new_job}")
409
+ @client.api_post_request("/job/#{path_encode old_job}/doRename?newName=#{form_encode new_job}")
404
410
  end
405
411
 
406
412
  # Delete a job given the name
@@ -411,7 +417,7 @@ module JenkinsApi
411
417
  #
412
418
  def delete(job_name)
413
419
  @logger.info "Deleting job '#{job_name}'"
414
- @client.api_post_request("/job/#{job_name}/doDelete")
420
+ @client.api_post_request("/job/#{path_encode job_name}/doDelete")
415
421
  end
416
422
 
417
423
  # Deletes all jobs from Jenkins
@@ -432,7 +438,7 @@ module JenkinsApi
432
438
  #
433
439
  def wipe_out_workspace(job_name)
434
440
  @logger.info "Wiping out the workspace of job '#{job_name}'"
435
- @client.api_post_request("/job/#{job_name}/doWipeOutWorkspace")
441
+ @client.api_post_request("/job/#{path_encode job_name}/doWipeOutWorkspace")
436
442
  end
437
443
 
438
444
  # Stops a running build of a job
@@ -449,10 +455,10 @@ module JenkinsApi
449
455
  @logger.info "Stopping job '#{job_name}' Build ##{build_number}"
450
456
  # Check and see if the build is running
451
457
  is_building = @client.api_get_request(
452
- "/job/#{job_name}/#{build_number}"
458
+ "/job/#{path_encode job_name}/#{build_number}"
453
459
  )["building"]
454
460
  if is_building
455
- @client.api_post_request("/job/#{job_name}/#{build_number}/stop")
461
+ @client.api_post_request("/job/#{path_encode job_name}/#{build_number}/stop")
456
462
  end
457
463
  end
458
464
  alias_method :stop, :stop_build
@@ -515,7 +521,7 @@ module JenkinsApi
515
521
  else
516
522
  raise "Mode should either be 'text' or 'html'. You gave: #{mode}"
517
523
  end
518
- get_msg = "/job/#{job_name}/#{build_num}/logText/progressive#{mode}?"
524
+ get_msg = "/job/#{path_encode job_name}/#{build_num}/logText/progressive#{mode}?"
519
525
  get_msg << "start=#{start}"
520
526
  raw_response = true
521
527
  api_response = @client.api_get_request(get_msg, nil, nil, raw_response)
@@ -610,7 +616,7 @@ module JenkinsApi
610
616
  #
611
617
  def list_details(job_name)
612
618
  @logger.info "Obtaining the details of '#{job_name}'"
613
- @client.api_get_request("/job/#{job_name}")
619
+ @client.api_get_request("/job/#{path_encode job_name}")
614
620
  end
615
621
 
616
622
  # List upstream projects of a specific job
@@ -620,7 +626,7 @@ module JenkinsApi
620
626
  #
621
627
  def get_upstream_projects(job_name)
622
628
  @logger.info "Obtaining the upstream projects of '#{job_name}'"
623
- response_json = @client.api_get_request("/job/#{job_name}")
629
+ response_json = @client.api_get_request("/job/#{path_encode job_name}")
624
630
  response_json["upstreamProjects"]
625
631
  end
626
632
 
@@ -631,7 +637,7 @@ module JenkinsApi
631
637
  #
632
638
  def get_downstream_projects(job_name)
633
639
  @logger.info "Obtaining the down stream projects of '#{job_name}'"
634
- response_json = @client.api_get_request("/job/#{job_name}")
640
+ response_json = @client.api_get_request("/job/#{path_encode job_name}")
635
641
  response_json["downstreamProjects"]
636
642
  end
637
643
 
@@ -641,7 +647,7 @@ module JenkinsApi
641
647
  #
642
648
  def get_builds(job_name)
643
649
  @logger.info "Obtaining the build details of '#{job_name}'"
644
- response_json = @client.api_get_request("/job/#{job_name}")
650
+ response_json = @client.api_get_request("/job/#{path_encode job_name}")
645
651
  response_json["builds"]
646
652
  end
647
653
 
@@ -683,7 +689,7 @@ module JenkinsApi
683
689
  #
684
690
  def get_current_build_status(job_name)
685
691
  @logger.info "Obtaining the current build status of '#{job_name}'"
686
- response_json = @client.api_get_request("/job/#{job_name}")
692
+ response_json = @client.api_get_request("/job/#{path_encode job_name}")
687
693
  color_to_status(response_json["color"])
688
694
  end
689
695
  alias_method :status, :get_current_build_status
@@ -697,61 +703,248 @@ module JenkinsApi
697
703
  #
698
704
  def get_current_build_number(job_name)
699
705
  @logger.info "Obtaining the current build number of '#{job_name}'"
700
- @client.api_get_request("/job/#{job_name}")['nextBuildNumber'].to_i - 1
706
+ @client.api_get_request("/job/#{path_encode job_name}")['nextBuildNumber'].to_i - 1
701
707
  end
702
708
  alias_method :build_number, :get_current_build_number
703
709
 
704
- # Build a job given the name of the job
705
- # You can optionally pass in a list of params for Jenkins to use for
706
- # parameterized builds
710
+ # Build a Jenkins job, optionally waiting for build to start and
711
+ # returning the build number.
712
+ # Adds support for new/old Jenkins servers where build_queue id may
713
+ # not be available. Also adds support for periodic callbacks, and
714
+ # optional cancellation of queued_job if not started within allowable
715
+ # time window (if build_queue option available)
716
+ #
717
+ # Notes:
718
+ # 'opts' may be a 'true' or 'false' value to maintain
719
+ # compatibility with old method signature, where true indicates
720
+ # 'return_build_number'. In this case, true is translated to:
721
+ # { 'build_start_timeout' => @client_timeout }
722
+ # which simulates earlier behavior.
723
+ #
724
+ # progress_proc
725
+ # Optional proc that is called periodically while waiting for
726
+ # build to start.
727
+ # Initial call (with poll_count == 0) indicates build has been
728
+ # requested, and that polling is starting.
729
+ # Final call will indicate one of build_started or cancelled.
730
+ # params:
731
+ # max_wait [Integer] Same as opts['build_start_timeout']
732
+ # current_wait [Integer]
733
+ # poll_count [Integer] How many times has queue been polled
734
+ #
735
+ # completion_proc
736
+ # Optional proc that is called <just before> the 'build' method
737
+ # exits.
738
+ # params:
739
+ # build_number [Integer] Present if build started or nil
740
+ # build_cancelled [Boolean] True if build timed out and was
741
+ # successfully removed from build-queue
707
742
  #
708
743
  # @param [String] job_name the name of the job
709
- # @param [Hash] params the parameters for parameterized builds
710
- # @param [Boolean] return_build_number whether to wait and obtain the build
711
- # number
712
- #
713
- # @return [String, Integer] the response code from the build POST request
714
- # if return_build_number is not requested and the build number if the
715
- # return_build_number is requested. nil will be returned if the build
716
- # number is requested and not available. This can happen if there is
717
- # already a job in the queue and concurrent build is disabled.
718
- #
719
- def build(job_name, params={}, return_build_number = false)
744
+ # @param [Hash] params the parameters for parameterized build
745
+ # @param [Hash] opts options for this method
746
+ # * +build_start_timeout+ [Integer] How long to wait for queued
747
+ # build to start before giving up. Default: 0/nil
748
+ # * +cancel_on_build_start_timeout+ [Boolean] Should an attempt be
749
+ # made to cancel the queued build if it hasn't started within
750
+ # 'build_start_timeout' seconds? This only works on newer versions
751
+ # of Jenkins where JobQueue is exposed in build post response.
752
+ # Default: false
753
+ # * +poll_interval+ [Integer] How often should we check with CI
754
+ # Server while waiting for start. Default: 2 (seconds)
755
+ # * +progress_proc+ [Proc] A proc that will receive progress notitications. Default: nil
756
+ # * +completion_proc+ [Proc] A proc that is called <just before>
757
+ # this method (build) exits. Default: nil
758
+ #
759
+ # @return [Integer] build number, or nil if not started (IF TIMEOUT SPECIFIED)
760
+ # @return [Integer] HTTP response code (per prev. behavior) (NO TIMEOUT SPECIFIED)
761
+ #
762
+ def build(job_name, params={}, opts = {})
763
+ if opts.nil? || opts.class.is_a?(FalseClass)
764
+ opts = {}
765
+ elsif opts.class.is_a?(TrueClass)
766
+ opts = { 'build_start_timeout' => @client_timeout }
767
+ end
768
+
769
+ opts['job_name'] = job_name
770
+
720
771
  msg = "Building job '#{job_name}'"
721
772
  msg << " with parameters: #{params.inspect}" unless params.empty?
722
773
  @logger.info msg
723
- build_endpoint = params.empty? ? "build" : "buildWithParameters"
724
- raw_response = return_build_number
725
- response =@client.api_post_request(
726
- "/job/#{job_name}/#{build_endpoint}",
727
- params,
728
- raw_response
729
- )
730
- # If return_build_number is enabled, obtain the queue ID from the location
774
+
775
+ # Best-guess build-id
776
+ # This is only used if we go the old-way below... but we can use this number to detect if multiple
777
+ # builds were queued
778
+ current_build_id = get_current_build_number(job_name)
779
+ expected_build_id = current_build_id > 0 ? current_build_id + 1 : 1
780
+
781
+ if (params.nil? or params.empty?)
782
+ response = @client.api_post_request("/job/#{path_encode job_name}/build",
783
+ {},
784
+ true)
785
+ else
786
+ response = @client.api_post_request("/job/#{path_encode job_name}/buildWithParameters",
787
+ params,
788
+ true)
789
+ end
790
+
791
+ if (opts['build_start_timeout'] || 0) > 0
792
+ if @client.compare_versions(@client.get_jenkins_version, JENKINS_QUEUE_ID_SUPPORT_VERSION) >= 0
793
+ return get_build_id_from_queue(response, expected_build_id, opts)
794
+ else
795
+ return get_build_id_the_old_way(expected_build_id, opts)
796
+ end
797
+ else
798
+ return response.code
799
+ end
800
+ end
801
+
802
+ def get_build_id_from_queue(response, expected_build_id, opts)
803
+ # If we get this far the API hasn't detected an error response (it would raise Exception)
804
+ # So no need to check response code
805
+ # Obtain the queue ID from the location
731
806
  # header and wait till the build is moved to one of the executors and a
732
807
  # build number is assigned
733
- if return_build_number
734
- if response["location"]
735
- task_id_match = response["location"].match(/\/item\/(\d*)\//)
736
- task_id = task_id_match.nil? ? nil : task_id_match[1]
737
- unless task_id.nil?
738
- @logger.debug "Queue task ID for job '#{job_name}': #{task_id}"
739
- Timeout::timeout(@client.timeout) do
740
- while @client.queue.get_item_by_id(task_id)["executable"].nil?
741
- sleep 5
808
+ build_start_timeout = opts['build_start_timeout']
809
+ poll_interval = opts['poll_interval'] || 2
810
+ poll_interval = 1 if poll_interval < 1
811
+ progress_proc = opts['progress_proc']
812
+ completion_proc = opts['completion_proc']
813
+ job_name = opts['job_name']
814
+
815
+ if response["location"]
816
+ task_id_match = response["location"].match(/\/item\/(\d*)\//)
817
+ task_id = task_id_match.nil? ? nil : task_id_match[1]
818
+ unless task_id.nil?
819
+ @logger.info "Job queued for #{job_name}, will wait up to #{build_start_timeout} seconds for build to start..."
820
+
821
+ # Let progress proc know we've queued the build
822
+ progress_proc.call(build_start_timeout, 0, 0) if progress_proc
823
+
824
+ # Wait for the build to start
825
+ begin
826
+ start = Time.now.to_i
827
+ Timeout::timeout(build_start_timeout) do
828
+ started = false
829
+ attempts = 0
830
+
831
+ while !started
832
+ # Don't really care about the response... if we get thru here, then it must have worked.
833
+ # Jenkins will return 404's until the job starts
834
+ queue_item = @client.queue.get_item_by_id(task_id)
835
+
836
+ if queue_item['executable'].nil?
837
+ # Job not started yet
838
+ attempts += 1
839
+
840
+ progress_proc.call(build_start_timeout, (Time.now.to_i - start), attempts) if progress_proc
841
+ # Every 5 attempts (~10 seconds)
842
+ @logger.info "Still waiting..." if attempts % 5 == 0
843
+
844
+ sleep poll_interval
845
+ else
846
+ build_number = queue_item['executable']['number']
847
+ completion_proc.call(build_number, false) if completion_proc
848
+
849
+ return build_number
850
+ end
742
851
  end
743
852
  end
744
- @client.queue.get_item_by_id(task_id)["executable"]["number"]
745
- else
746
- nil
853
+ rescue Timeout::Error
854
+ # Well, we waited - and the job never started building
855
+ # Attempt to kill off queued job (if flag set)
856
+ if opts['cancel_on_build_start_timeout']
857
+ @logger.info "Job for '#{job_name}' did not start in a timely manner, attempting to cancel pending build..."
858
+
859
+ begin
860
+ @client.api_post_request("/queue/cancelItem?id=#{task_id}")
861
+ @logger.info "Job cancelled"
862
+ completion_proc.call(nil, true) if completion_proc
863
+ rescue JenkinsApi::Exceptions::ApiException => e
864
+ completion_proc.call(nil, false) if completion_proc
865
+ @logger.warn "Error while attempting to cancel pending job for '#{job_name}'. #{e.class} #{e}"
866
+ raise
867
+ end
868
+ else
869
+ @logger.info "Jenkins build for '#{job_name}' failed to start in a timely manner"
870
+ completion_proc.call(nil, false) if completion_proc
871
+ end
872
+
873
+ # Old version used to throw timeout error, so we should let that go thru now
874
+ raise
875
+ rescue JenkinsApi::Exceptions::ApiException => e
876
+ # Jenkins Api threw an error at us
877
+ completion_proc.call(nil, false) if completion_proc
878
+ @logger.warn "Problem while waiting for '#{job_name}' build to start. #{e.class} #{e}"
879
+ raise
747
880
  end
748
881
  else
749
- nil
882
+ @logger.warn "Jenkins did not return a queue_id for '#{job_name}' build (location: #{response['location']})"
883
+ return get_build_id_the_old_way(expected_build_id, opts)
750
884
  end
751
885
  else
752
- response
886
+ @logger.warn "Jenkins did not return a location header for '#{job_name}' build"
887
+ return get_build_id_the_old_way(expected_build_id, opts)
753
888
  end
754
889
  end
890
+ private :get_build_id_from_queue
891
+
892
+ def get_build_id_the_old_way(expected_build_id, opts)
893
+ # Try to wait until the build starts so we can mimic queue
894
+ # Wait for the build to start
895
+ build_start_timeout = opts['build_start_timeout']
896
+ poll_interval = opts['poll_interval'] || 2
897
+ poll_interval = 1 if poll_interval < 1
898
+ progress_proc = opts['progress_proc']
899
+ completion_proc = opts['completion_proc']
900
+ job_name = opts['job_name']
901
+
902
+ @logger.info "Build requested for '#{job_name}', will wait up to #{build_start_timeout} seconds for build to start..."
903
+
904
+ # Let progress proc know we've queued the build
905
+ progress_proc.call(build_start_timeout, 0, 0) if progress_proc
906
+
907
+ begin
908
+ start = Time.now.to_i
909
+ Timeout::timeout(build_start_timeout) do
910
+ attempts = 0
911
+
912
+ while true
913
+ attempts += 1
914
+
915
+ # Don't really care about the response... if we get thru here, then it must have worked.
916
+ # Jenkins will return 404's until the job starts
917
+ begin
918
+ get_build_details(job_name, expected_build_id)
919
+ completion_proc.call(expected_build_number, false) if completion_proc
920
+
921
+ return expected_build_id
922
+ rescue JenkinsApi::Exceptions::NotFound => e
923
+ progress_proc.call(build_start_timeout, (Time.now.to_i - start), attempts) if progress_proc
924
+
925
+ # Every 5 attempts (~10 seconds)
926
+ @logger.info "Still waiting..." if attempts % 5 == 0
927
+
928
+ sleep poll_interval
929
+ end
930
+ end
931
+ end
932
+ rescue Timeout::Error
933
+ # Well, we waited - and the job never started building
934
+ # Now we need to raise an exception so that the build can be officially failed
935
+ completion_proc.call(nil, false) if completion_proc
936
+ @logger.info "Jenkins '#{job_name}' build failed to start in a timely manner"
937
+
938
+ # Old version used to propagate timeout error
939
+ raise
940
+ rescue JenkinsApi::Exceptions::ApiException => e
941
+ completion_proc.call(nil, false) if completion_proc
942
+ # Jenkins Api threw an error at us
943
+ @logger.warn "Problem while waiting for '#{job_name}' build ##{expected_build_number} to start. #{e.class} #{e}"
944
+ raise
945
+ end
946
+ end
947
+ private :get_build_id_the_old_way
755
948
 
756
949
  # Programatically schedule SCM polling for the specified job
757
950
  #
@@ -779,7 +972,7 @@ module JenkinsApi
779
972
  #
780
973
  def disable(job_name)
781
974
  @logger.info "Disabling job '#{job_name}'"
782
- @client.api_post_request("/job/#{job_name}/disable")
975
+ @client.api_post_request("/job/#{path_encode job_name}/disable")
783
976
  end
784
977
 
785
978
  # Obtain the configuration stored in config.xml of a specific job
@@ -790,7 +983,7 @@ module JenkinsApi
790
983
  #
791
984
  def get_config(job_name)
792
985
  @logger.info "Obtaining the config.xml of '#{job_name}'"
793
- @client.get_config("/job/#{job_name}")
986
+ @client.get_config("/job/#{path_encode job_name}")
794
987
  end
795
988
 
796
989
  # Post the configuration of a job given the job name and the config.xml
@@ -802,7 +995,7 @@ module JenkinsApi
802
995
  #
803
996
  def post_config(job_name, xml)
804
997
  @logger.info "Posting the config.xml of '#{job_name}'"
805
- @client.post_config("/job/#{job_name}/config.xml", xml)
998
+ @client.post_config("/job/#{path_encode job_name}/config.xml", xml)
806
999
  end
807
1000
 
808
1001
  # Obtain the test results for a specific build of a job
@@ -814,7 +1007,7 @@ module JenkinsApi
814
1007
  build_num = get_current_build_number(job_name) if build_num == 0
815
1008
  @logger.info "Obtaining the test results of '#{job_name}'" +
816
1009
  " Build ##{build_num}"
817
- @client.api_get_request("/job/#{job_name}/#{build_num}/testReport")
1010
+ @client.api_get_request("/job/#{path_encode job_name}/#{build_num}/testReport")
818
1011
  rescue Exceptions::NotFound
819
1012
  # Not found is acceptable, as not all builds will have test results
820
1013
  # and this is what jenkins throws at us in that case
@@ -831,7 +1024,7 @@ module JenkinsApi
831
1024
  @logger.info "Obtaining the build details of '#{job_name}'" +
832
1025
  " Build ##{build_num}"
833
1026
 
834
- @client.api_get_request("/job/#{job_name}/#{build_num}/")
1027
+ @client.api_get_request("/job/#{path_encode job_name}/#{build_num}/")
835
1028
  end
836
1029
 
837
1030
  # Change the description of a specific job
@@ -1207,6 +1400,31 @@ module JenkinsApi
1207
1400
  filtered_job_names[0..parallel-1]
1208
1401
  end
1209
1402
 
1403
+ # Get a list of promoted builds for given job
1404
+ #
1405
+ # @param [String] job_name
1406
+ # @return [Hash] Hash map of promitions and the promoted builds. Promotions that didn't took place yet
1407
+ # return nil
1408
+ def get_promotions(job_name)
1409
+ result = {}
1410
+
1411
+ @logger.info "Obtaining the promotions of '#{job_name}'"
1412
+ response_json = @client.api_get_request("/job/#{job_name}/promotion")
1413
+
1414
+ response_json["processes"].each do |promotion|
1415
+ @logger.info "Getting promotion details of '#{promotion['name']}'"
1416
+
1417
+ if promotion['color'] == 'notbuilt'
1418
+ result[promotion['name']] = nil
1419
+ else
1420
+ promo_json = @client.api_get_request("/job/#{job_name}/promotion/latest/#{promotion['name']}")
1421
+ result[promotion['name']] = promo_json['target']['number']
1422
+ end
1423
+ end
1424
+
1425
+ result
1426
+ end
1427
+
1210
1428
  private
1211
1429
 
1212
1430
  # Obtains the threshold params used by jenkins in the XML file