jenkins_pipeline_builder 0.1.2 → 0.1.3

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