rudder 0.1.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.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +16 -0
  5. data/.travis.yml +5 -0
  6. data/Gemfile +8 -0
  7. data/Gemfile.lock +53 -0
  8. data/LICENSE +21 -0
  9. data/README.md +101 -0
  10. data/Rakefile +24 -0
  11. data/bin/console +15 -0
  12. data/bin/setup +8 -0
  13. data/docker-compose.yml +26 -0
  14. data/docs/Rudder.html +382 -0
  15. data/docs/Rudder/DSL.html +456 -0
  16. data/docs/Rudder/DSL/Component.html +645 -0
  17. data/docs/Rudder/DSL/Group.html +733 -0
  18. data/docs/Rudder/DSL/Job.html +487 -0
  19. data/docs/Rudder/DSL/Pipeline.html +1869 -0
  20. data/docs/Rudder/DSL/Resource.html +584 -0
  21. data/docs/Rudder/DSL/ResourceType.html +223 -0
  22. data/docs/Rudder/DSL/Util.html +324 -0
  23. data/docs/_index.html +211 -0
  24. data/docs/class_list.html +51 -0
  25. data/docs/css/common.css +1 -0
  26. data/docs/css/full_list.css +58 -0
  27. data/docs/css/style.css +496 -0
  28. data/docs/file.README.html +191 -0
  29. data/docs/file_list.html +56 -0
  30. data/docs/frames.html +17 -0
  31. data/docs/index.html +191 -0
  32. data/docs/js/app.js +303 -0
  33. data/docs/js/full_list.js +216 -0
  34. data/docs/js/jquery.js +4 -0
  35. data/docs/method_list.html +347 -0
  36. data/docs/top-level-namespace.html +110 -0
  37. data/examples/README.md +6 -0
  38. data/examples/groups/README.md +12 -0
  39. data/examples/groups/groups_pipeline.rb +34 -0
  40. data/examples/groups/jobs/bash_stuff/date.rb +16 -0
  41. data/examples/groups/jobs/bash_stuff/hello.rb +17 -0
  42. data/examples/groups/jobs/git_stuff/log.rb +22 -0
  43. data/examples/groups/jobs/git_stuff/ls.rb +21 -0
  44. data/examples/groups/resources/rudder_git.rb +6 -0
  45. data/examples/groups/resources/timer.rb +5 -0
  46. data/examples/hello_world_pipeline.rb +44 -0
  47. data/examples/images/groups/groups_all.png +0 -0
  48. data/examples/images/groups/groups_bash_stuff.png +0 -0
  49. data/examples/images/groups/groups_git_stuff.png +0 -0
  50. data/examples/images/hello_world.png +0 -0
  51. data/examples/images/includes/includes.png +0 -0
  52. data/examples/images/shared/borrows.png +0 -0
  53. data/examples/images/shared/common.png +0 -0
  54. data/examples/images/shared/wrapper.png +0 -0
  55. data/examples/includes/README.md +7 -0
  56. data/examples/includes/includes_pipeline.rb +6 -0
  57. data/examples/includes/jobs/log.rb +15 -0
  58. data/examples/includes/resources/rudder_git.rb +5 -0
  59. data/examples/shared/README.md +21 -0
  60. data/examples/shared/borrows_pipeline.rb +31 -0
  61. data/examples/shared/common_pipeline.rb +34 -0
  62. data/examples/shared/wrapper_pipeline.rb +51 -0
  63. data/exe/rudder +52 -0
  64. data/lib/rudder.rb +35 -0
  65. data/lib/rudder/dsl.rb +54 -0
  66. data/lib/rudder/dsl/component.rb +79 -0
  67. data/lib/rudder/dsl/group.rb +110 -0
  68. data/lib/rudder/dsl/job.rb +65 -0
  69. data/lib/rudder/dsl/pipeline.rb +374 -0
  70. data/lib/rudder/dsl/resource.rb +82 -0
  71. data/lib/rudder/dsl/resource_type.rb +45 -0
  72. data/lib/rudder/dsl/util.rb +48 -0
  73. data/lib/rudder/version.rb +5 -0
  74. data/lib/tasks/docker.rb +11 -0
  75. data/rudder.gemspec +29 -0
  76. metadata +189 -0
data/lib/rudder.rb ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ require_relative 'rudder/dsl.rb'
6
+ require_relative 'rudder/version.rb'
7
+
8
+ ##
9
+ # Methods to compile Rudder definitions
10
+ # to Concourse Pipeline definitions
11
+ #
12
+ module Rudder
13
+ ##
14
+ # Compiles a {Rudder::DSL::Pipeline} definition from +path+
15
+ # to a {Hash}
16
+ #
17
+ # @param path [String] the path to the +Rudder+ definition
18
+ # @return [Hash] Concourse YAML friendly hash
19
+ #
20
+ def self.compile(path)
21
+ Rudder::DSL.eval_from_file(path).to_h
22
+ end
23
+
24
+ ##
25
+ # Dumps a {Rudder::DSL::Pipeline} or Pipeline {Hash}
26
+ # to the provided file handle +output+
27
+ #
28
+ # @param pipeline {Rudder::DSL::Pipeline} definition. Assumed to be evaluated.
29
+ # @param output [File] handle to dump YAML to
30
+ # @return [nil]
31
+ #
32
+ def self.dump(pipeline, output)
33
+ output.puts(YAML.dump(pipeline.to_h))
34
+ end
35
+ end
data/lib/rudder/dsl.rb ADDED
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'dsl/pipeline.rb'
4
+
5
+ module Rudder
6
+ ##
7
+ # DSL for configuring and manipulating Concourse Pipelines
8
+ #
9
+ # The building blocks of the DSL are:
10
+ # - {Rudder::DSL::Pipeline}, the top level definition containg all other definitions.
11
+ # See {https://concourse-ci.org/pipelines.html Concourse Pipeline}
12
+ # - {Rudder::DSL::Resource}, representing inputs and outputs of jobs.
13
+ # See {https://concourse-ci.org/resources.html Concourse Resource}
14
+ # - {Rudder::DSL::Job}, units of work.
15
+ # See {https://concourse-ci.org/jobs.html Concourse Job}
16
+ # - {Rudder::DSL::ResourceType}, defines how a {Rudder::DSL::Resource} operates.
17
+ # See {https://concourse-ci.org/resource-types.html Concourse Resource Type}
18
+ # - {Rudder::DSL::Group}, logically groups together Concourse Jobs in the UI.
19
+ # See {https://concourse-ci.org/pipeline-groups.html Concourse Grouping Jobs}
20
+ #
21
+ module DSL
22
+ ##
23
+ # Entry to the DSL. Creates a new pipeline
24
+ # instance to evaluate user defined pipelines
25
+ #
26
+ # @return [Rudder::DSL::Pipeline] new, and unevaluated
27
+ #
28
+ def self.pipeline(*args, **kwargs)
29
+ Rudder::DSL::Pipeline.new(*args, **kwargs)
30
+ end
31
+
32
+ ##
33
+ # Load a pipeline from a definition file
34
+ # at the +path+
35
+ #
36
+ # @param path [String] to the {Rudder::DSL::Pipeline} definition
37
+ # @return [Rudder::DSL::Pipeline] from +path+, unevaluated
38
+ #
39
+ def self.from_file(path)
40
+ Rudder::DSL::Pipeline.new path
41
+ end
42
+
43
+ ##
44
+ # Load and evaluate a pipeline from a definition file
45
+ # at the +path+
46
+ #
47
+ # @param path [String] to the {Rudder::DSL::Pipeline} definition
48
+ # @return [Rudder::DSL::Pipeline] from +path+, fully evaluated
49
+ #
50
+ def self.eval_from_file(path)
51
+ Rudder::DSL::Pipeline.new(path).eval
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'util'
4
+
5
+ module Rudder
6
+ module DSL
7
+ ##
8
+ # Base class for other pipeline sub components to extend
9
+ #
10
+ # Not intended for public usage and subject to change.
11
+ #
12
+ class Component
13
+ include Rudder::DSL::Util
14
+
15
+ ##
16
+ # Required method for all subclasses to implement.
17
+ #
18
+ # @raise [RuntimeError] if not implemented
19
+ def _inner_hash
20
+ raise 'Implement this in a subclass'
21
+ end
22
+
23
+ def to_h
24
+ _deep_to_h(_inner_hash)
25
+ end
26
+
27
+ ##
28
+ # Populates the inner hash with missing method names and
29
+ # their arguments
30
+ #
31
+ # @param method [Symbol] top level key of this {Rudder::DSL::Component}
32
+ # Corresponds to the highest level key in a concourse
33
+ # component.
34
+ #
35
+ # @param *args entire arg collection is assigned to the value of the key
36
+ # +method+. Note: if only 1 argument is provided it is
37
+ # unwrapped from the +args+ {Array}.
38
+ # @param **kwargs treated as the last value of +args+ if provided
39
+ # @return the value related to +method+ if no arguments or keyword
40
+ # arguments are provided. Otherwise, +nil+.
41
+ #
42
+ # rubocop:disable Style/MethodMissingSuper
43
+ def method_missing(method, *args, **kwargs)
44
+ # Accessing inner hash as attribute
45
+ return _inner_hash[method] if args.empty? && kwargs.empty?
46
+
47
+ # Ruby treats dictionaries passed as the last argument as keyword dicts
48
+ # (specifically when they use symbols as keys). Just smashing
49
+ # these into args so we don't miss anything
50
+ args << kwargs unless kwargs.empty?
51
+ raise "Argument list missing from [#{method}]" if args.empty?
52
+
53
+ # If a single arg is given then assume this field is scalar,
54
+ # otherwise assume its a list that needs all args
55
+ formatted_args = args.size == 1 ? args[0] : args
56
+ _inner_hash[method] = formatted_args
57
+ end
58
+ # rubocop:enable Style/MethodMissingSuper
59
+
60
+ ##
61
+ # Components respond to everything by default
62
+ #
63
+ # @return true
64
+ #
65
+ def respond_to?(_name, _include_all = true)
66
+ true
67
+ end
68
+
69
+ ##
70
+ # Components respond to missing by default
71
+ #
72
+ # @return true
73
+ #
74
+ def respond_to_missing?(*_)
75
+ true
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module Rudder
6
+ module DSL
7
+ ##
8
+ # Concourse group. Logically groups together Concourse
9
+ # jobs in the UI.
10
+ #
11
+ # == DSL Usage:
12
+ #
13
+ # {Rudder::DSL::Group}'s are the simplest element of any
14
+ # Concourse Pipeline, defined by only a name and a non-empty
15
+ # list of jobs.
16
+ #
17
+ #
18
+ # @example
19
+ # # Name's are typically set during initialization
20
+ # group :my_awesome_group do # => Name is set to :my_awesome_group
21
+ #
22
+ # # but the name may be changed post construction as well
23
+ # group :not_the_best_name do # => Name initialized to :not_the_best_name
24
+ # name :the_best_name
25
+ # end # => but is set to :the_best_name after the block is executed
26
+ #
27
+ # @example
28
+ # # Job's are always set post construction. They can be added
29
+ # # individually:
30
+ # group :my_awesome_group do
31
+ # job :some_prereq
32
+ # job :my_awesome_work
33
+ # end # => group.jobs = [:some_prereq, :my_awesome_work]
34
+ #
35
+ # # and they can be added in collections
36
+ # group :my_awesome_group do
37
+ # jobs :a_job, :and_another, :and_one_more
38
+ # end # => group.jobs = [:a_job, :and_another, :and_one_more]
39
+ #
40
+ #
41
+ class Group
42
+ ##
43
+ # All {Rudder::DSL::Group}'s require
44
+ #
45
+ # - Name of the group
46
+ # - A list of jobs in the group
47
+ #
48
+ # Jobs are added after initilization.
49
+ #
50
+ # @param [String, Symbol] the non-+nil+ name of this group
51
+ # @raise [ArgumentError] if +name+ is +nil
52
+ #
53
+ def initialize(name)
54
+ raise super.ArgumentError 'Name cannot be nil' if name.nil?
55
+
56
+ @name = name
57
+ @jobs = Set.new
58
+ end
59
+
60
+ ##
61
+ # Replace's this {Rudder::DSL::Group}'s name unless
62
+ # +name+ is nil.
63
+ #
64
+ # @param name [String, Symbol] the new name to use. Ignored if +nil+.
65
+ # @return [String, Symbol] the latest component name.
66
+ #
67
+ def name(name = nil)
68
+ @name = name unless name.nil?
69
+ @name
70
+ end
71
+
72
+ ##
73
+ # Add a single job to the jobs list
74
+ #
75
+ # @param job_name [String, Symbol] to add to the jobs list
76
+ # @return [Set<String, Symbol>] the latest list of jobs
77
+ #
78
+ def job(job_name)
79
+ jobs job_name
80
+ end
81
+
82
+ ##
83
+ # Adds all the jobs to the jobs list
84
+ #
85
+ # @param *args [*String, *Symbol] collection of jobs to add to this
86
+ # {Rudder::DSL::Group}
87
+ # @return [Set<String, Symbol>] the latest list of jobs
88
+ #
89
+ def jobs(*args)
90
+ args.each { |arg| @jobs << arg }
91
+ @jobs
92
+ end
93
+
94
+ ##
95
+ # @return [Hash] YAML friendly +Hash+ representation of this resource
96
+ #
97
+ # @raise [RuntimeError] if +name+ is +nil+ or +jobs+ is empty
98
+ #
99
+ def to_h
100
+ raise 'Groups require a name' if @name.nil?
101
+ raise 'Groups require at least 1 job' if @jobs.empty?
102
+
103
+ {
104
+ 'name' => @name.to_s,
105
+ 'jobs' => @jobs.to_a.map(&:to_s)
106
+ }
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'component'
4
+
5
+ module Rudder
6
+ module DSL
7
+ ##
8
+ # Concourse job
9
+ #
10
+ # Defines a plan of work that may share state
11
+ # in an explicit manner.
12
+ #
13
+ # == DSL Usage:
14
+ #
15
+ # {Rudder::DSL::Job} are defined by a +name+ and a +plan+ of work.
16
+ #
17
+ # @example
18
+ # # Name's are set during initializtion, and may not be nil
19
+ # job :awesome_job # => job.name = :awesome_job
20
+ #
21
+ # job nil # => Raises ArgumentError
22
+ #
23
+ # @example
24
+ # # The plan is set after construction
25
+ # job :awesome_job do
26
+ # plan << { get: :some_resource }
27
+ # plan << { get: :another_resource }
28
+ # end # => plan.source = [{get: :some_resource}, {get: :another_resource}]
29
+ #
30
+ class Job < Rudder::DSL::Component
31
+ ##
32
+ # All Jobs require:
33
+ #
34
+ # - A name
35
+ # - A plan of work
36
+ #
37
+ # Plans are defined after initialization
38
+ #
39
+ # @param name [String, Symbol] name of this Concourse job. Must not be +nil+
40
+ # @raise [ArgumentError] when +name+ is nil
41
+ #
42
+ def initialize(name)
43
+ raise super.ArgumentError 'Name cannot be nil' if name.nil?
44
+
45
+ @job = { name: name, plan: [] }
46
+ end
47
+
48
+ def _inner_hash
49
+ @job
50
+ end
51
+
52
+ ##
53
+ # @return [Hash] YAML friendly +Hash+ representation of this resource
54
+ #
55
+ # @raise [RuntimeError] if +name+ is +nil+ or +plan+ is empty
56
+ #
57
+ def to_h
58
+ raise 'Name must be set for Concourse Jobs' if @job[:name].nil?
59
+ raise 'Plan must be set for Concourse Jobs' if @job[:plan].empty?
60
+
61
+ super.to_h
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,374 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'group'
4
+ require_relative 'job'
5
+ require_relative 'resource'
6
+ require_relative 'resource_type'
7
+ require_relative 'util'
8
+
9
+ module Rudder
10
+ module DSL
11
+ ##
12
+ # Concourse Pipeline. Main entry of the DSL. Evaluates
13
+ # user defined pipelines.
14
+ #
15
+ # == DSL Usage:
16
+ #
17
+ # {Rudder::DSL::Pipeline}'s are composed of various components:
18
+ #
19
+ # - {Rudder::DSL::Resource}: basic inputs and output of jobs.
20
+ # - {Rudder::DSL::Job}: basic computation unit of a pipeline
21
+ # - {Rudder::DSL::ResourceType}: custom resource definitions
22
+ # - {Rudder::DSL::Group}: logical grouping of jobs in the UI.
23
+ # Either every job is in a Group or no job is (hard Concourse
24
+ # requirement)
25
+ #
26
+ # === Adding Components
27
+ #
28
+ # Components are added to the Pipeline by component type, followed
29
+ # by name, optional arguments, then typically a block.
30
+ #
31
+ # @example Adding Components to Pipelines
32
+ # #
33
+ # # my_pipeline_definition.rb
34
+ # #
35
+ # resource :my_git_repo, :git do
36
+ # source[:uri] = 'https://github.com/my/repo.git'
37
+ # source[:branch] = :master
38
+ # end
39
+ #
40
+ # resource :daily, :time do
41
+ # source[:interval] = '24h'
42
+ #
43
+ # job :build_project do
44
+ # plan << [in_parallel: [{ get: :my_git_repo }, { get: :daily, trigger: true}]]
45
+ # build = { task: 'build my project', config: {
46
+ # platform: :linux,
47
+ # image_resource: { type: 'docker-image', source: { repository: 'busybox' } },
48
+ # run: { path: 'my_git_repo/build.sh' }
49
+ # }}
50
+ # plan << build
51
+ # end
52
+ #
53
+ #
54
+ # === Loading Other Pipelines
55
+ #
56
+ # {Rudder::DSL::Pipeline}'s can load other pipeline definitions
57
+ # using {Rudder::DSL::Pipeline#load}. This is a useful mechanism
58
+ # for abstracting out common subsections of pipelines, then
59
+ # merging them into larger pipelines.
60
+ #
61
+ # @example Loading / Importing Pipelines
62
+ # #
63
+ # # load_neighbor.rb
64
+ # #
65
+ # neighbor = load 'neighbor_pipeline.rb'
66
+ #
67
+ # # merge all the neighboring resources and jobs into this pipeline
68
+ # resources.merge! neighbor.resources
69
+ # jobs.merge! neighbor.jobs
70
+ #
71
+ # resource_type :slack_notification, 'docker-image' do
72
+ # source[:repository] = 'some/slack-docker-repo'
73
+ # end
74
+ #
75
+ # resource :our_slack_channel, :slack_notification do
76
+ # source[:url] = '((slack-team-webhook))'
77
+ # end
78
+ #
79
+ # # Add a slack notification task to the end
80
+ # # of every job
81
+ # jobs.values.each do |job|
82
+ # job.plan << {
83
+ # put: :our_slack_channel,
84
+ # params: { text: "Job #{job.name} complete!" }
85
+ # }
86
+ # end
87
+ #
88
+ # === Loading Individual Components
89
+ # Individual pipeline components can also be defined on a per-file
90
+ # basis and then loaded into a {Rudder::DSL::Pipeline} using
91
+ # {Rudder::DSL::Pipeline#load_component}. This is useful for factoring
92
+ # out common resources for multiple pipeline's to use.
93
+ #
94
+ # @example Loading / Importing Individual Components
95
+ # #
96
+ # # operations_scripts_resource.rb
97
+ # #
98
+ # type :git
99
+ # source[:uri] = 'https://github.com/<our org>/operations_scripts.git'
100
+ # source[:branch] = 'master'
101
+ #
102
+ #
103
+ # #
104
+ # # some_operations_pipeline.rb
105
+ # #
106
+ #
107
+ # # load the resource into the pipeline. Automatically includes
108
+ # # the resource into the resources list with the name :scripts
109
+ # load_component 'operations_scripts_resource.rb', :resource, :scripts
110
+ #
111
+ # job :audit do |pipeline|
112
+ # plan << {
113
+ # task: 'run the audit script', config: {
114
+ # platform: :linux,
115
+ # image_resource: {
116
+ # type: 'docker-image',
117
+ # source: { repository: 'alpine/git' }
118
+ # },
119
+ # run: {
120
+ # path: pipeline.scripts.sub_path('audit.rb')
121
+ # }
122
+ # }
123
+ # }
124
+ # end
125
+ class Pipeline
126
+ include Rudder::DSL::Util
127
+ # {Hash} of names to {Rudder::DSL::Resource}
128
+ # @return [Hash<(String, Symbol), Rudder::DSL::Resource>]
129
+ attr_accessor :resources
130
+ # {Hash} of names to {Rudder::DSL::Job}
131
+ # @return [Hash<(String, Symbol), Rudder::DSL::Job>]
132
+ attr_accessor :jobs
133
+ # {Hash} of names to {Rudder::DSL::ResourceType}
134
+ # @return [Hash<(String, Symbol), Rudder::DSL::ResourceType>]
135
+ attr_accessor :resource_types
136
+ # {Hash} of names to {Rudder::DSL::Group}
137
+ # @return [Hash<(String, Symbol), Rudder::DSL::Group>]
138
+ attr_accessor :groups
139
+
140
+ ##
141
+ # All pipelines require:
142
+ # - Jobs
143
+ # - Resources
144
+ #
145
+ # Concourse Pipelines may optionally provide:
146
+ # - Resource Types
147
+ # - Groups
148
+ #
149
+ # +Rudder+ Pipelines may optionally include a +file_path+. This
150
+ # is required when loading resources from neighboring files.
151
+ #
152
+ # All pipeline requirements are only needed at the Pipeline
153
+ # render time (after evaluation), and need not be specified
154
+ # for initialization.
155
+ #
156
+ # @param file_path [String] path to this {Rudder::DSL::Pipeline} definition.
157
+ # @param resources [Hash<(String, Symbol), Rudder::DSL::Resource]
158
+ # map of Resource names to their definitions.
159
+ # @param jobs [Hash<(String, Symbol), Rudder::DSL::Job]
160
+ # map of Job names to their definitions.
161
+ # @param groups [Hash<(String, Symbol), Rudder::DSL::Group]
162
+ # map of Group names to their definitions.
163
+ # @param resources_types [Hash<(String, Symbol), Rudder::DSL::ResourceType]
164
+ # map of Resource Type names to their definitions.
165
+ #
166
+ def initialize(file_path = nil, resources: {}, jobs: {},
167
+ groups: {}, resource_types: {})
168
+ @resources = resources
169
+ @jobs = jobs
170
+ @groups = groups
171
+ @resource_types = resource_types
172
+ # rubocop:disable Layout/AlignHash, Layout/SpaceBeforeComma
173
+ @known_classes = {
174
+ resource: { clazz: Resource , pipeline_group: @resources },
175
+ job: { clazz: Job , pipeline_group: @jobs },
176
+ group: { clazz: Group , pipeline_group: @groups },
177
+ resource_type: { clazz: ResourceType, pipeline_group: @resource_types }
178
+ }
179
+ # rubocop:enable Layout/AlignHash, Layout/SpaceBeforeComma
180
+ @pipelines = {}
181
+ @file_path = file_path
182
+ end
183
+
184
+ ##
185
+ # Renders all of this pipeline's components to their +Hash+
186
+ # representations.
187
+ #
188
+ # @return [Hash] YAML friendly +Hash+ representation of this +Pipeline+
189
+ # if either +groups+ or +resource_types+ is empty they will
190
+ # not be included in the rendering at all.
191
+ #
192
+ def to_h
193
+ h = {
194
+ 'resources' => _convert_h_val(@resources.values),
195
+ 'jobs' => _convert_h_val(@jobs.values)
196
+ }
197
+ h['groups'] = _convert_h_val(@groups.values) unless @groups.empty?
198
+ h['resource_types'] = _convert_h_val(@resource_types.values) unless @resource_types.empty?
199
+ h
200
+ end
201
+
202
+ ##
203
+ # Populates this {Rudder::DSL::Pipeline} with components
204
+ # and optionally fetches defined components.
205
+ #
206
+ # Fetching
207
+ # --------------------------------------------------------------------
208
+ # When +method+ is called with no arguments it is treated
209
+ # as a {Rudder::DSL::Pipeline} getter method. +method+ is translated
210
+ # to the name of a {Rudder::DSL::Component} and the +Component+
211
+ # is returned if defined, otherwise nil is returned.
212
+ # --------------------------------------------------------------------
213
+ #
214
+ # Setting
215
+ # --------------------------------------------------------------------
216
+ # When +method+ is passed _any_ arguments (positional, placement, or block)
217
+ # then +method+ is treated as a setter.
218
+ #
219
+ # When setting, +method+ must be the name of a known {Rudder::DSL::Component}.
220
+ # The first argument is a required _name_ for the component. _All_ arguments
221
+ # and keyword arguments are then delegated to the {Rudder::DSL::Component}'s
222
+ # specific initializer.
223
+ #
224
+ # Finally, when a block is provided it is evaluated within the context
225
+ # of the newly constructed {Rudder::DSL::Component} with full priveleges
226
+ # to operate on it.
227
+ # --------------------------------------------------------------------
228
+ #
229
+ # @return [Rudder::DSL::Component, nil] when +method+ is called with no
230
+ # arguments, returns the {Rudder::DSL::Component} with name
231
+ # +method+, if any exists. Otherwise returns +nil+.
232
+ # @raise [RuntimeError] when attempting to define an unknown
233
+ # {Rudder::DSL::Component}
234
+ # @internal_todo
235
+ # TODO: Clean this up so these can be reenabled
236
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
237
+ def method_missing(method, *args, &component_block)
238
+ local_component = _get_local_component(method)
239
+ if !@known_classes.include?(method) && !local_component
240
+ return super.send(method, args, component_block)
241
+ end
242
+
243
+ # Look up a previously defined component from the pipeline
244
+ return local_component if local_component && args.empty? && !block_given?
245
+
246
+ component_group = @known_classes[method][:pipeline_group]
247
+ name = args[0]
248
+ raise "Overlapping component name: #{method}" if component_group.include? name
249
+
250
+ component = @known_classes[method][:clazz].new(*args)
251
+
252
+ component.instance_exec self, &component_block if block_given?
253
+ component_group[name] = component
254
+ end
255
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
256
+
257
+ ##
258
+ # {Rudder::DSL::Pipeline}'s respond to missing
259
+ #
260
+ # @return true
261
+ def respond_to_missing?(*_)
262
+ true
263
+ end
264
+
265
+ def respond_to?(name, _include_all = true)
266
+ @known_classes.key? name
267
+ end
268
+
269
+ ##
270
+ # Evaluates the given file path.
271
+ # If file_path nil, defaults to the one provided at construction time
272
+ # If both are nil, raises an exception
273
+ #
274
+ # @param file_path [String, nil] path to {Rudder::DSL::Pipeline} definition
275
+ # to evaluate. Uses the current +file_path+ if +nil+
276
+ # @raise [RuntimeError] if +file_path+ and {Rudder::DSL::Pipeline#file_path}
277
+ # are both +nil+
278
+ # @return [Rudder::DSL::Pipeline] the evaluated pipeline
279
+ def eval(file_path = nil)
280
+ @file_path = file_path || @file_path
281
+ if @file_path.nil?
282
+ raise 'File path must be provided at Pipeline initialization or eval call'
283
+ end
284
+
285
+ File.open(@file_path) do |f|
286
+ instance_eval f.read, @file_path
287
+ end
288
+ self
289
+ end
290
+
291
+ ##
292
+ # Given a path relative to this pipeline, loads another
293
+ # pipeline and returns it
294
+ #
295
+ # Note that this includes _nothing_ from the relative pipeline in this
296
+ # one, instead just returning the pipeline to be manipulated
297
+ #
298
+ # May also optionally provides hashes for
299
+ # - resources
300
+ # - resource_types
301
+ # - jobs
302
+ # - groups
303
+ #
304
+ # @param other_pipeline_path [String] relative path to {Rudder::DSL::Pipeline}
305
+ # definition to load and evaluate.
306
+ # @param resources [Hash<(String, Symbol), Rudder::DSL::Resource]
307
+ # resources to initialize the other {Rudder::DSL::Pipeline} with
308
+ # @param resources_types [Hash<(String, Symbol), Rudder::DSL::ResourceType]
309
+ # resources_types to initialize the other {Rudder::DSL::Pipeline} with
310
+ # @param jobs [Hash<(String, Symbol), Rudder::DSL::Job]
311
+ # jobs to initialize the other {Rudder::DSL::Pipeline} with
312
+ # @param groups [Hash<(String, Symbol), Rudder::DSL::Group]
313
+ # groups to initialize the other {Rudder::DSL::Pipeline} with
314
+ def load(other_pipeline_path, resources: {}, resource_types: {},
315
+ jobs: {}, groups: {})
316
+ if @pipelines.key? other_pipeline_path
317
+ @pipelines[other_pipeline_path]
318
+ else
319
+ dir = File.dirname(@file_path)
320
+ full_path = File.join(dir, other_pipeline_path)
321
+ pipeline = Rudder::DSL::Pipeline.new(
322
+ full_path, resources: resources, resource_types: resource_types,
323
+ jobs: jobs, groups: groups
324
+ ).eval
325
+ @pipelines[other_pipeline_path] = pipeline
326
+ pipeline
327
+ end
328
+ end
329
+
330
+ ##
331
+ # Given a path to a component, its class, and
332
+ # any args required to construct it, creates
333
+ # a new component
334
+ #
335
+ # Note that this automatically includes the component into this pipeline
336
+ #
337
+ # @param component_path [String] path, relative to this pipeline, containing
338
+ # a {Rudder::DSL::Component} to load
339
+ # @param class_sym [Symbol] symbol of a {Rudder::DSL::Component}. May be one of:
340
+ # (+:job+, +:resource+, +:resource_type+, +:group+)
341
+ # @param name [String, Symbol] name to use for the loaded
342
+ # {Rudder::DSL::Component}. Must not be +nil+.
343
+ # @param *args any additional arguments to pass to the {Rudder::DSL::Component}
344
+ # constructor.
345
+ # @raise RuntimeError if +name+ is +nil+ or an uknown +class_sym+ is provided.
346
+ #
347
+ def load_component(component_path, class_sym, name, *args)
348
+ raise "Unable to load #{class_sym}" unless @known_classes.keys.include? class_sym
349
+ raise 'Name must not be nil' if name.nil?
350
+
351
+ full_path = File.join(File.dirname(@file_path), component_path)
352
+ component = @known_classes[class_sym][:clazz].new(name, *args)
353
+ component.instance_eval File.read(full_path), full_path
354
+ @known_classes[class_sym][:pipeline_group][name] = component
355
+ component
356
+ end
357
+
358
+ # Yikes! Seems like a bad idea - if someone uses the same name twice (say, 1 resource
359
+ # and 1 job), then this will return one pretty much at random.
360
+ # TODO: Make this not bad
361
+ # Oh well..
362
+ # TODO: This may be returning a non-nil/non-falsey type that causes some issues
363
+ def _get_local_component(component)
364
+ component = component.to_sym
365
+ locals = @known_classes.values.map do |m|
366
+ m[:pipeline_group][component]
367
+ end.compact
368
+ # TODO: Add a logger here..
369
+ puts "Found multiple bindings for: #{p}. Getting first found" unless locals.size <= 1
370
+ locals[0]
371
+ end
372
+ end
373
+ end
374
+ end