jenkins_pipeline_builder 0.1.2 → 0.1.3

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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-gemset +1 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +4 -0
  5. data/Rakefile +68 -0
  6. data/bin/generate +28 -0
  7. data/config/login.yml +24 -0
  8. data/jenkins_pipeline_builder.gemspec +42 -0
  9. data/lib/jenksin_pipeline_builder.rb +39 -0
  10. data/lib/jenksin_pipeline_builder/builders.rb +72 -0
  11. data/lib/jenksin_pipeline_builder/cli/base.rb +69 -0
  12. data/lib/jenksin_pipeline_builder/cli/helper.rb +68 -0
  13. data/lib/jenksin_pipeline_builder/cli/pipeline.rb +40 -0
  14. data/lib/jenksin_pipeline_builder/cli/view.rb +40 -0
  15. data/lib/jenksin_pipeline_builder/compiler.rb +81 -0
  16. data/lib/jenksin_pipeline_builder/generator.rb +346 -0
  17. data/lib/jenksin_pipeline_builder/job_builder.rb +82 -0
  18. data/lib/jenksin_pipeline_builder/module_registry.rb +82 -0
  19. data/lib/jenksin_pipeline_builder/publishers.rb +113 -0
  20. data/lib/jenksin_pipeline_builder/triggers.rb +38 -0
  21. data/lib/jenksin_pipeline_builder/utils.rb +46 -0
  22. data/lib/jenksin_pipeline_builder/version.rb +25 -0
  23. data/lib/jenksin_pipeline_builder/view.rb +259 -0
  24. data/lib/jenksin_pipeline_builder/wrappers.rb +91 -0
  25. data/lib/jenksin_pipeline_builder/xml_helper.rb +40 -0
  26. data/spec/func_tests/spec_helper.rb +18 -0
  27. data/spec/func_tests/view_spec.rb +93 -0
  28. data/spec/unit_tests/compiler_spec.rb +19 -0
  29. data/spec/unit_tests/fixtures/files/Job-Build-Flow.xml +57 -0
  30. data/spec/unit_tests/fixtures/files/Job-Build-Flow.yaml +22 -0
  31. data/spec/unit_tests/fixtures/files/Job-Build-Maven.xml +90 -0
  32. data/spec/unit_tests/fixtures/files/Job-Build-Maven.yaml +26 -0
  33. data/spec/unit_tests/fixtures/files/Job-DSL.yaml +14 -0
  34. data/spec/unit_tests/fixtures/files/Job-DSL1.xml +27 -0
  35. data/spec/unit_tests/fixtures/files/Job-DSL2.xml +25 -0
  36. data/spec/unit_tests/fixtures/files/Job-Gem-Build.xml +142 -0
  37. data/spec/unit_tests/fixtures/files/Job-Gem-Build.yaml +41 -0
  38. data/spec/unit_tests/fixtures/files/Job-Generate-From-Template.xml +32 -0
  39. data/spec/unit_tests/fixtures/files/Job-Generate-From-Template.yaml +8 -0
  40. data/spec/unit_tests/fixtures/files/Job-Multi-Project.xml +117 -0
  41. data/spec/unit_tests/fixtures/files/Job-Multi-Project.yaml +36 -0
  42. data/spec/unit_tests/fixtures/files/project.yaml +15 -0
  43. data/spec/unit_tests/generator_spec.rb +67 -0
  44. data/spec/unit_tests/module_registry_spec.rb +19 -0
  45. data/spec/unit_tests/resolve_dependencies_spec.rb +230 -0
  46. data/spec/unit_tests/spec_helper.rb +29 -0
  47. metadata +75 -6
@@ -0,0 +1,40 @@
1
+ #
2
+ # Copyright (c) 2014 Igor Moochnick
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+
23
+ module JenkinsPipelineBuilder
24
+ module CLI
25
+ # This class provides various command line operations related to jobs.
26
+ class View < Thor
27
+ include Thor::Actions
28
+
29
+ desc 'dump', 'Dump view'
30
+ def dump(job_name)
31
+ Helper.setup(parent_options).dump(job_name)
32
+ end
33
+
34
+ desc 'bootstrap Path', 'Generates view from folder or a file'
35
+ def create(path)
36
+ Helper.setup(parent_options).view.generate(path)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,81 @@
1
+ #
2
+ # Copyright (c) 2014 Igor Moochnick
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+
23
+ module JenkinsPipelineBuilder
24
+ class Compiler
25
+ def self.resolve_value(value, settings)
26
+ value_s = value.to_s.clone
27
+ vars = value_s.scan(/{{([^}]+)}}/).flatten
28
+ vars.select! do |var|
29
+ var_val = settings[var.to_sym]
30
+ value_s.gsub!("{{#{var.to_s}}}", var_val) unless var_val.nil?
31
+ var_val.nil?
32
+ end
33
+ return nil if vars.count != 0
34
+ return value_s
35
+ end
36
+
37
+ def self.get_settings_bag(item_bag, settings_bag = {})
38
+ item = item_bag[:value]
39
+ bag = {}
40
+ return unless item.kind_of?(Hash)
41
+ item.keys.each do |k|
42
+ val = item[k]
43
+ if val.kind_of? String
44
+ new_value = resolve_value(val, settings_bag)
45
+ return nil if new_value.nil?
46
+ bag[k] = new_value
47
+ end
48
+ end
49
+ my_settings_bag = settings_bag.clone
50
+ return my_settings_bag.merge(bag)
51
+ end
52
+
53
+ def self.compile(item, settings = {})
54
+ case item
55
+ when String
56
+ new_value = resolve_value(item, settings)
57
+ puts "Failed to resolve #{item}" if new_value.nil?
58
+ return new_value
59
+ when Hash
60
+ result = {}
61
+ item.each do |key, value|
62
+ new_value = compile(value, settings)
63
+ puts "Failed to resolve #{value}" if new_value.nil?
64
+ return nil if new_value.nil?
65
+ result[key] = new_value
66
+ end
67
+ return result
68
+ when Array
69
+ result = []
70
+ item.each do |value|
71
+ new_value = compile(value, settings)
72
+ puts "Failed to resolve #{value}" if new_value.nil?
73
+ return nil if new_value.nil?
74
+ result << new_value
75
+ end
76
+ return result
77
+ end
78
+ return item
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,346 @@
1
+ #
2
+ # Copyright (c) 2014 Igor Moochnick
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+
23
+ require 'yaml'
24
+ require 'pp'
25
+
26
+ module JenkinsPipelineBuilder
27
+ class Generator
28
+ # Initialize a Client object with Jenkins Api Client
29
+ #
30
+ # @param args [Hash] Arguments to connect to Jenkins server
31
+ #
32
+ # @option args [String] :something some option description
33
+ #
34
+ # @return [JenkinsPipelineBuilder::Generator] a client generator
35
+ #
36
+ # @raise [ArgumentError] when required options are not provided.
37
+ #
38
+ def initialize(args, client)
39
+ @client = client
40
+ @logger = @client.logger
41
+ @job_templates = {}
42
+ @job_collection = {}
43
+
44
+ @module_registry = ModuleRegistry.new ({
45
+ job: {
46
+ description: JobBuilder.method(:change_description),
47
+ scm_params: JobBuilder.method(:apply_scm_params),
48
+ hipchat: JobBuilder.method(:hipchat_notifier),
49
+ parameters: JobBuilder.method(:build_parameters),
50
+ builders: {
51
+ registry: {
52
+ job_builder: Builders.method(:build_multijob_builder),
53
+ inject_vars_file: Builders.method(:build_environment_vars_injector),
54
+ shell_command: Builders.method(:build_shell_command),
55
+ maven3: Builders.method(:build_maven3)
56
+ },
57
+ method:
58
+ lambda { |registry, params, n_xml| @module_registry.run_registry_on_path('//builders', registry, params, n_xml) }
59
+ },
60
+ publishers: {
61
+ registry: {
62
+ git: Publishers.method(:push_to_git),
63
+ hipchat: Publishers.method(:push_to_hipchat),
64
+ description_setter: Publishers.method(:description_setter),
65
+ downstream: Publishers.method(:push_to_projects),
66
+ junit_result: Publishers.method(:publish_junit),
67
+ coverage_result: Publishers.method(:publish_rcov)
68
+ },
69
+ method:
70
+ lambda { |registry, params, n_xml| @module_registry.run_registry_on_path('//publishers', registry, params, n_xml) }
71
+ },
72
+ wrappers: {
73
+ registry: {
74
+ timestamp: Wrappers.method(:console_timestamp),
75
+ ansicolor: Wrappers.method(:ansicolor),
76
+ artifactory: Wrappers.method(:publish_to_artifactory),
77
+ rvm: Wrappers.method(:run_with_rvm),
78
+ inject_env_var: Wrappers.method(:inject_env_vars),
79
+ inject_passwords: Wrappers.method(:inject_passwords)
80
+ },
81
+ method:
82
+ lambda { |registry, params, n_xml| @module_registry.run_registry_on_path('//buildWrappers', registry, params, n_xml) }
83
+ },
84
+ triggers: {
85
+ registry: {
86
+ git_push: Triggers.method(:enable_git_push),
87
+ scm_polling: Triggers.method(:enable_scm_polling)
88
+ },
89
+ method:
90
+ lambda { |registry, params, n_xml| @module_registry.run_registry_on_path('//triggers', registry, params, n_xml) }
91
+ }
92
+ }
93
+ })
94
+ end
95
+
96
+ attr_accessor :client
97
+ attr_accessor :debug
98
+ # TODO: WTF?
99
+ attr_accessor :no_files
100
+ attr_accessor :job_collection
101
+
102
+ # Creates an instance to the View class by passing a reference to self
103
+ #
104
+ # @return [JenkinsApi::Client::System] An object to System subclass
105
+ #
106
+ def view
107
+ JenkinsPipelineBuilder::View.new(self)
108
+ end
109
+
110
+ def load_collection_from_path(path, recursively = false)
111
+ if File.directory?(path)
112
+ @logger.info "Generating from folder #{path}"
113
+ Dir.glob(File.join(path, '/*.yaml')).each do |file|
114
+ if File.directory?(file)
115
+ if recursively
116
+ load_collection_from_path(File.join(path, file), recursively)
117
+ else
118
+ next
119
+ end
120
+ end
121
+ @logger.info "Loading file #{file}"
122
+ yaml = YAML.load_file(file)
123
+ load_job_collection(yaml)
124
+ end
125
+ else
126
+ @logger.info "Loading file #{path}"
127
+ yaml = YAML.load_file(path)
128
+ load_job_collection(yaml)
129
+ end
130
+ end
131
+
132
+ def load_job_collection(yaml)
133
+ yaml.each do |section|
134
+ Utils.symbolize_keys_deep!(section)
135
+ key = section.keys.first
136
+ value = section[key]
137
+ name = value[:name]
138
+ raise "Duplicate item with name '#{name}' was detected." if @job_collection.has_key?(name)
139
+ @job_collection[name.to_s] = { name: name.to_s, type: key, value: value }
140
+ end
141
+ end
142
+
143
+ def get_item(name)
144
+ @job_collection[name.to_s]
145
+ end
146
+
147
+ def resolve_project(project)
148
+ defaults = get_item('global')
149
+ settings = defaults.nil? ? {} : defaults[:value] || {}
150
+
151
+ project[:settings] = Compiler.get_settings_bag(project, settings) unless project[:settings]
152
+ project_body = project[:value]
153
+
154
+ # Process jobs
155
+ jobs = project_body[:jobs] || []
156
+ jobs.map! do |job|
157
+ job.kind_of?(String) ? { job.to_sym => {} } : job
158
+ end
159
+ @logger.info project
160
+ jobs.each do |job|
161
+ job_id = job.keys.first
162
+ settings = project[:settings].clone.merge(job[job_id])
163
+ job[:result] = resolve_job_by_name(job_id, settings)
164
+ end
165
+
166
+ # Process views
167
+ views = project_body[:views] || []
168
+ views.map! do |view|
169
+ view.kind_of?(String) ? { view.to_sym => {} } : view
170
+ end
171
+ views.each do |view|
172
+ view_id = view.keys.first
173
+ settings = project[:settings].clone.merge(view[view_id])
174
+ # TODO: rename resolve_job_by_name properly
175
+ view[:result] = resolve_job_by_name(view_id, settings)
176
+ end
177
+
178
+ project
179
+ end
180
+
181
+ def resolve_job_by_name(name, settings = {})
182
+ job = get_item(name)
183
+ raise "Failed to locate job by name '#{name}'" if job.nil?
184
+ job_value = job[:value]
185
+ compiled_job = Compiler.compile(job_value, settings)
186
+ return compiled_job
187
+ end
188
+
189
+ def projects
190
+ result = []
191
+ @job_collection.values.each do |item|
192
+ result << item if item[:type] == :project
193
+ end
194
+ return result
195
+ end
196
+
197
+ def bootstrap(path)
198
+ @logger.info "Bootstrapping pipeline from path #{path}"
199
+ load_collection_from_path(path)
200
+
201
+ projects.each do |project|
202
+ compiled_project = resolve_project(project)
203
+ pp compiled_project
204
+
205
+ if compiled_project[:value][:jobs]
206
+ compiled_project[:value][:jobs].each do |i|
207
+ job = i[:result]
208
+ xml = compile_job_to_xml(job)
209
+ create_or_update(job, xml)
210
+ end
211
+ end
212
+
213
+ if compiled_project[:value][:views]
214
+ compiled_project[:value][:views].each do |v|
215
+ _view = v[:result]
216
+ view.create(_view)
217
+ end
218
+ end
219
+ end
220
+
221
+ end
222
+
223
+ def dump(job_name)
224
+ @logger.info "Debug #{@debug}"
225
+ @logger.info "Dumping #{job_name} into #{job_name}.xml"
226
+ xml = @client.job.get_config(job_name)
227
+ File.open(job_name + '.xml', 'w') { |f| f.write xml }
228
+ end
229
+
230
+ def create_or_update(job, xml)
231
+ job_name = job[:name]
232
+ if @debug
233
+ @logger.info "Will create job #{job}"
234
+ @logger.info "#{xml}"
235
+ File.open(job_name + '.xml', 'w') { |f| f.write xml }
236
+ return
237
+ end
238
+
239
+ if @client.job.exists?(job_name)
240
+ @client.job.update(job_name, xml)
241
+ else
242
+ @client.job.create(job_name, xml)
243
+ end
244
+ end
245
+
246
+ def compile_job_to_xml(job)
247
+ raise 'Job name is not specified' unless job[:name]
248
+
249
+ @logger.info "Creating Yaml Job #{job}"
250
+ job[:job_type] = 'free_style' unless job[:job_type]
251
+ case job[:job_type]
252
+ when 'job_dsl'
253
+ xml = compile_freestyle_job_to_xml(job)
254
+ update_job_dsl(job, xml)
255
+ when 'multi_project'
256
+ xml = compile_freestyle_job_to_xml(job)
257
+ adjust_multi_project xml
258
+ when 'build_flow'
259
+ xml = compile_freestyle_job_to_xml(job)
260
+ add_job_dsl(job, xml)
261
+ when 'free_style'
262
+ compile_freestyle_job_to_xml job
263
+ else
264
+ @logger.info 'Unknown job type'
265
+ ''
266
+ end
267
+
268
+ end
269
+
270
+ def adjust_multi_project(xml)
271
+ n_xml = Nokogiri::XML(xml)
272
+ root = n_xml.root()
273
+ root.name = 'com.tikal.jenkins.plugins.multijob.MultiJobProject'
274
+ n_xml.to_xml
275
+ end
276
+
277
+ def compile_freestyle_job_to_xml(params)
278
+ if params.has_key?(:template)
279
+ template_name = params[:template]
280
+ raise "Job template '#{template_name}' can't be resolved." unless @job_templates.has_key?(template_name)
281
+ params.delete(:template)
282
+ template = @job_templates[template_name]
283
+ puts "Template found: #{template}"
284
+ params = template.deep_merge(params)
285
+ puts "Template merged: #{template}"
286
+ end
287
+
288
+ xml = @client.job.build_freestyle_config(params)
289
+ n_xml = Nokogiri::XML(xml)
290
+
291
+ @module_registry.traverse_registry_path('job', params, n_xml)
292
+
293
+ n_xml.to_xml
294
+ end
295
+
296
+ def add_job_dsl(job, xml)
297
+ n_xml = Nokogiri::XML(xml)
298
+ n_xml.root.name = 'com.cloudbees.plugins.flow.BuildFlow'
299
+ Nokogiri::XML::Builder.with(n_xml.root) do |xml|
300
+ xml.dsl job[:build_flow]
301
+ end
302
+ n_xml.to_xml
303
+ end
304
+
305
+ # TODO: make sure this is tested
306
+ def update_job_dsl(job, xml)
307
+ n_xml = Nokogiri::XML(xml)
308
+ n_builders = n_xml.xpath('//builders').first
309
+ Nokogiri::XML::Builder.with(n_builders) do |xml|
310
+ build_job_dsl(job, xml)
311
+ end
312
+ n_xml.to_xml
313
+ end
314
+
315
+ def generate_job_dsl_body(params)
316
+ @logger.info "Generating pipeline"
317
+
318
+ xml = @client.job.build_freestyle_config(params)
319
+
320
+ n_xml = Nokogiri::XML(xml)
321
+ if n_xml.xpath('//javaposse.jobdsl.plugin.ExecuteDslScripts').empty?
322
+ p_xml = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |b_xml|
323
+ build_job_dsl(params, b_xml)
324
+ end
325
+
326
+ n_xml.xpath('//builders').first.add_child("\r\n" + p_xml.doc.root.to_xml(:indent => 4) + "\r\n")
327
+ xml = n_xml.to_xml
328
+ end
329
+ xml
330
+ end
331
+
332
+ def build_job_dsl(job, xml)
333
+ xml.send('javaposse.jobdsl.plugin.ExecuteDslScripts') {
334
+ if job.has_key?(:job_dsl)
335
+ xml.scriptText job[:job_dsl]
336
+ xml.usingScriptText true
337
+ else
338
+ xml.targets job[:job_dsl_targets]
339
+ xml.usingScriptText false
340
+ end
341
+ xml.ignoreExisting false
342
+ xml.removedJobAction 'IGNORE'
343
+ }
344
+ end
345
+ end
346
+ end