octocatalog-diff 1.3.0 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.version +1 -1
- data/doc/CHANGELOG.md +9 -0
- data/doc/advanced-catalog-validation.md +3 -1
- data/doc/dev/api/v1/calls/catalog-diff.md +1 -5
- data/doc/dev/api/v1/calls/catalog.md +1 -5
- data/lib/octocatalog-diff/catalog-util/builddir.rb +1 -1
- data/lib/octocatalog-diff/catalog.rb +89 -69
- data/lib/octocatalog-diff/catalog/computed.rb +37 -45
- data/lib/octocatalog-diff/catalog/json.rb +18 -7
- data/lib/octocatalog-diff/catalog/noop.rb +5 -5
- data/lib/octocatalog-diff/catalog/puppetdb.rb +7 -20
- data/lib/octocatalog-diff/catalog/puppetmaster.rb +15 -22
- data/lib/octocatalog-diff/errors.rb +0 -1
- data/lib/octocatalog-diff/util/catalogs.rb +9 -6
- data/lib/octocatalog-diff/util/parallel.rb +1 -1
- data/lib/octocatalog-diff/util/scriptrunner.rb +1 -1
- metadata +18 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5f0b3862bc04c59a30e41e90cf689f508cb68dd6
|
4
|
+
data.tar.gz: 4e67157b3a0ba173d2d1099d1e0aec79027a8f3b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aa208efdde5e60a3bfdd1ced747632bc00d0fec7f916fed1765f9c8e7fe8de99160a7ef673e694fa043a90713af3df2c6eec620f9e792d2c0332864d04baf694
|
7
|
+
data.tar.gz: 228113ef4f77899dfe3185b839b9d67f41db4ae9db0901f9aa7aa3307d0c67d8983982118c8e2dff433f37f428cccc8c1ca755f83c41e9a8a9965a6027bd4952
|
data/.version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.4.0
|
data/doc/CHANGELOG.md
CHANGED
@@ -8,6 +8,15 @@
|
|
8
8
|
</tr>
|
9
9
|
</thead><tbody>
|
10
10
|
<tr valign=top>
|
11
|
+
<td>1.4.0</td>
|
12
|
+
<td>2017-08-03</td>
|
13
|
+
<td>
|
14
|
+
<li><a href="https://github.com/github/octocatalog-diff/pull/135">#135</a>: (Enhancement) Puppet 5 compatibility</li>
|
15
|
+
<li><a href="https://github.com/github/octocatalog-diff/pull/140">#140</a>: (Internal) Prefix tmpdirs with ocd-</li>
|
16
|
+
<li><a href="https://github.com/github/octocatalog-diff/pull/138">#138</a>: (Internal) Refactor catalog class with proper inheritance</li>
|
17
|
+
</td>
|
18
|
+
</tr>
|
19
|
+
<tr valign=top>
|
11
20
|
<td>1.3.0</td>
|
12
21
|
<td>2017-06-09</td>
|
13
22
|
<td>
|
@@ -4,12 +4,14 @@
|
|
4
4
|
|
5
5
|
Catalog validation features include:
|
6
6
|
|
7
|
-
- Validate references: Ensure resources targeted by `before`, `notify`, `require`, and/or `subscribe` exist in the catalog
|
7
|
+
- Validate references: Ensure resources targeted by `before`, `notify`, `require`, and/or `subscribe` exist in the catalog for Puppet 4 and below.
|
8
8
|
|
9
9
|
## Validate references
|
10
10
|
|
11
11
|
`octocatalog-diff` includes the ability to validate references by ensuring resources targeted by `before`, `notify`, `require`, and/or `subscribe` parameters also exist in the catalog.
|
12
12
|
|
13
|
+
Puppet 5 already has this checking built in, so the `--validate-references` option described in this section will be ignored if Puppet 5 is being used. The same exception (`OctocatalogDiff::Errors::CatalogError`) is raised for a missing reference, whether the problem was detected by octocatalog-diff or Puppet 5.
|
14
|
+
|
13
15
|
Consider the following Puppet code:
|
14
16
|
|
15
17
|
```
|
@@ -194,7 +194,7 @@ The following exceptions may occur during the compilation of a catalog within th
|
|
194
194
|
|
195
195
|
- `OctocatalogDiff::Errors::CatalogError`
|
196
196
|
|
197
|
-
Catalog failed to compile. Please note that whenever possible, a `OctocatalogDiff::API::V1::Catalog` object is still constructed for a failed catalog, with `#valid?` returning false.
|
197
|
+
Catalog failed to compile. Please note that whenever possible, a `OctocatalogDiff::API::V1::Catalog` object is still constructed for a failed catalog, with `#valid?` returning false. It's also possible that the catalog contained broken references -- see [Catalog validation](/doc/advanced-catalog-validation.md).
|
198
198
|
|
199
199
|
- `OctocatalogDiff::Errors::GitCheckoutError`
|
200
200
|
|
@@ -203,7 +203,3 @@ The following exceptions may occur during the compilation of a catalog within th
|
|
203
203
|
- `OctocatalogDiff::Errors::PuppetVersionError`
|
204
204
|
|
205
205
|
The version of Puppet could not be determined, generally because the Puppet binary was not found, or does not respond as expected to `puppet version`.
|
206
|
-
|
207
|
-
- `OctocatalogDiff::Errors::ReferenceValidationError`
|
208
|
-
|
209
|
-
See [Catalog validation](/doc/advanced-catalog-validation.md).
|
@@ -100,7 +100,7 @@ The following exceptions may occur during the compilation of a catalog:
|
|
100
100
|
|
101
101
|
- `OctocatalogDiff::Errors::CatalogError`
|
102
102
|
|
103
|
-
Catalog failed to compile. Please note that whenever possible, a `OctocatalogDiff::API::V1::Catalog` object is still constructed for a failed catalog, with `#valid?` returning false.
|
103
|
+
Catalog failed to compile. Please note that whenever possible, a `OctocatalogDiff::API::V1::Catalog` object is still constructed for a failed catalog, with `#valid?` returning false. It's also possible that the catalog contained broken references -- see [Catalog validation](/doc/advanced-catalog-validation.md).
|
104
104
|
|
105
105
|
- `OctocatalogDiff::Errors::GitCheckoutError`
|
106
106
|
|
@@ -109,7 +109,3 @@ The following exceptions may occur during the compilation of a catalog:
|
|
109
109
|
- `OctocatalogDiff::Errors::PuppetVersionError`
|
110
110
|
|
111
111
|
The version of Puppet could not be determined, generally because the Puppet binary was not found, or does not respond as expected to `puppet version`.
|
112
|
-
|
113
|
-
- `OctocatalogDiff::Errors::ReferenceValidationError`
|
114
|
-
|
115
|
-
See [Catalog validation](/doc/advanced-catalog-validation.md).
|
@@ -38,7 +38,7 @@ module OctocatalogDiff
|
|
38
38
|
# @param options [Hash] Options for class; see above description
|
39
39
|
def initialize(options = {}, logger = nil)
|
40
40
|
@options = options.dup
|
41
|
-
@tempdir = Dir.mktmpdir
|
41
|
+
@tempdir = Dir.mktmpdir('ocd-builddir-')
|
42
42
|
at_exit { FileUtils.rm_rf(@tempdir) if File.directory?(@tempdir) }
|
43
43
|
|
44
44
|
@factdir = nil
|
@@ -12,14 +12,42 @@ require_relative 'catalog-util/fileresources'
|
|
12
12
|
require_relative 'errors'
|
13
13
|
|
14
14
|
module OctocatalogDiff
|
15
|
-
#
|
15
|
+
# Basic methods for interacting with a catalog. Generation of the catalog is handled via one of the
|
16
16
|
# supported backends listed above as 'require_relative'. Usually, the 'computed' backend
|
17
17
|
# will build the catalog from the Puppet command.
|
18
18
|
class Catalog
|
19
|
-
|
20
|
-
attr_reader :built, :catalog, :catalog_json
|
19
|
+
attr_accessor :node
|
20
|
+
attr_reader :built, :catalog, :catalog_json, :options
|
21
21
|
|
22
22
|
# Constructor
|
23
|
+
def initialize(options = {})
|
24
|
+
unless options.is_a?(Hash)
|
25
|
+
raise ArgumentError, "#{self.class}.initialize requires hash argument, not #{options.class}"
|
26
|
+
end
|
27
|
+
@options = options
|
28
|
+
|
29
|
+
# Basic settings
|
30
|
+
@node = options[:node]
|
31
|
+
@error_message = nil
|
32
|
+
@catalog = nil
|
33
|
+
@catalog_json = nil
|
34
|
+
@retries = nil
|
35
|
+
|
36
|
+
# The compilation directory can be overridden, e.g. when testing
|
37
|
+
@override_compilation_dir = options[:compilation_dir]
|
38
|
+
|
39
|
+
# Keep track of whether references have been validated yet. Allow this to be fudged for when we do
|
40
|
+
# not desire reference validation to happen (e.g., for the "from" catalog that is otherwise valid).
|
41
|
+
@references_validated = options[:references_validated] || false
|
42
|
+
|
43
|
+
# Keep track of whether file resources have been converted.
|
44
|
+
@file_resources_converted = false
|
45
|
+
|
46
|
+
# Keep track of whether it's built yet
|
47
|
+
@built = false
|
48
|
+
end
|
49
|
+
|
50
|
+
# Guess the backend from the input and return the appropriate catalog object.
|
23
51
|
# @param :backend [Symbol] If set, this will force a backend
|
24
52
|
# @param :json [String] JSON catalog content (will avoid running Puppet to compile catalog)
|
25
53
|
# @param :puppetdb [Object] If set, pull the catalog from PuppetDB rather than building
|
@@ -30,18 +58,25 @@ module OctocatalogDiff
|
|
30
58
|
# @param :pass_env_vars [Array<String>] OPTIONAL: Additional environment vars to pass
|
31
59
|
# @param :convert_file_resources [Boolean] OPTIONAL: Convert file resource source to content
|
32
60
|
# @param :storeconfigs [Boolean] OPTIONAL: Pass the '-s' flag, for puppetdb (storeconfigs) integration
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
61
|
+
# @return [OctocatalogDiff::Catalog::<?>] Catalog object from guessed backend
|
62
|
+
def self.create(options = {})
|
63
|
+
# Hard-coded backend
|
64
|
+
if options[:backend]
|
65
|
+
return OctocatalogDiff::Catalog::JSON.new(options) if options[:backend] == :json
|
66
|
+
return OctocatalogDiff::Catalog::PuppetDB.new(options) if options[:backend] == :puppetdb
|
67
|
+
return OctocatalogDiff::Catalog::PuppetMaster.new(options) if options[:backend] == :puppetmaster
|
68
|
+
return OctocatalogDiff::Catalog::Computed.new(options) if options[:backend] == :computed
|
69
|
+
return OctocatalogDiff::Catalog::Noop.new(options) if options[:backend] == :noop
|
70
|
+
raise ArgumentError, "Unknown backend :#{options[:backend]}"
|
71
|
+
end
|
38
72
|
|
39
|
-
#
|
40
|
-
|
41
|
-
|
73
|
+
# Determine backend based on arguments
|
74
|
+
return OctocatalogDiff::Catalog::JSON.new(options) if options[:json]
|
75
|
+
return OctocatalogDiff::Catalog::PuppetDB.new(options) if options[:puppetdb]
|
76
|
+
return OctocatalogDiff::Catalog::PuppetMaster.new(options) if options[:puppet_master]
|
42
77
|
|
43
|
-
#
|
44
|
-
|
78
|
+
# Default is to build catalog ourselves
|
79
|
+
OctocatalogDiff::Catalog::Computed.new(options)
|
45
80
|
end
|
46
81
|
|
47
82
|
# Build catalog - this method needs to be called to build the catalog. It is separate due to
|
@@ -49,43 +84,42 @@ module OctocatalogDiff
|
|
49
84
|
# object so it cannot be part of any object that is passed around.
|
50
85
|
# @param logger [Logger] Logger object, initialized to a default throwaway value
|
51
86
|
def build(logger = Logger.new(StringIO.new))
|
52
|
-
#
|
87
|
+
# If already built, don't build again
|
53
88
|
return if @built
|
54
89
|
@built = true
|
55
90
|
|
56
|
-
# Call catalog's build method.
|
57
|
-
if @catalog_obj.respond_to?(:build)
|
58
|
-
logger.debug "Calling build for object #{@catalog_obj.class}"
|
59
|
-
@catalog_obj.build(logger)
|
60
|
-
end
|
61
|
-
|
62
|
-
# These methods must exist in all backends
|
63
|
-
@catalog = @catalog_obj.catalog
|
64
|
-
@catalog_json = @catalog_obj.catalog_json
|
65
|
-
@error_message = @catalog_obj.error_message
|
66
|
-
|
67
91
|
# The resource hash is computed the first time it's needed. For now initialize it as nil.
|
68
92
|
@resource_hash = nil
|
69
93
|
|
94
|
+
# Invoke the backend's build method, if there is one. There's a stub below in case there's not.
|
95
|
+
logger.debug "Calling build for object #{self.class}"
|
96
|
+
build_catalog(logger)
|
97
|
+
|
70
98
|
# Perform post-generation processing of the catalog
|
71
99
|
return unless valid?
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
100
|
+
|
101
|
+
validate_references
|
102
|
+
return unless valid?
|
103
|
+
|
104
|
+
convert_file_resources if @options[:compare_file_text]
|
105
|
+
|
106
|
+
true
|
107
|
+
end
|
108
|
+
|
109
|
+
# Stub method if the backend does not contain a build method.
|
110
|
+
def build_catalog(_logger)
|
77
111
|
end
|
78
112
|
|
79
113
|
# Compilation environment
|
80
114
|
# @return [String] Compilation environment (if set), else 'production' by default
|
81
115
|
def environment
|
82
|
-
@
|
116
|
+
@environment ||= 'production'
|
83
117
|
end
|
84
118
|
|
85
119
|
# For logging we may wish to know the backend being used
|
86
120
|
# @return [String] Class of backend used
|
87
121
|
def builder
|
88
|
-
|
122
|
+
self.class.to_s
|
89
123
|
end
|
90
124
|
|
91
125
|
# Set the catalog JSON
|
@@ -98,8 +132,7 @@ module OctocatalogDiff
|
|
98
132
|
# This retrieves the compilation directory from the catalog, or otherwise the passed-in directory.
|
99
133
|
# @return [String] Compilation directory
|
100
134
|
def compilation_dir
|
101
|
-
|
102
|
-
@catalog_obj.respond_to?(:compilation_dir) ? @catalog_obj.compilation_dir : @options[:basedir]
|
135
|
+
@override_compilation_dir || @options[:basedir]
|
103
136
|
end
|
104
137
|
|
105
138
|
# The compilation directory can be overridden, e.g. during testing.
|
@@ -108,16 +141,16 @@ module OctocatalogDiff
|
|
108
141
|
@override_compilation_dir = dir
|
109
142
|
end
|
110
143
|
|
111
|
-
#
|
112
|
-
#
|
113
|
-
def convert_file_resources
|
114
|
-
|
115
|
-
@catalog_obj.convert_file_resources
|
144
|
+
# Stub method for "convert_file_resources" -- returns false because if the underlying class does
|
145
|
+
# not implement this method, it's not supported.
|
146
|
+
def convert_file_resources(_dry_run = false)
|
147
|
+
false
|
116
148
|
end
|
117
149
|
|
118
150
|
# Retrieve the error message.
|
119
151
|
# @return [String] Error message (maximum 20,000 characters) - nil if no error.
|
120
152
|
def error_message
|
153
|
+
build
|
121
154
|
return nil if @error_message.nil? || !@error_message.is_a?(String)
|
122
155
|
@error_message[0, 20_000]
|
123
156
|
end
|
@@ -133,12 +166,11 @@ module OctocatalogDiff
|
|
133
166
|
@resource_hash = nil
|
134
167
|
end
|
135
168
|
|
136
|
-
#
|
137
|
-
# compiled by running Puppet (e.g., it was read in from JSON or puppetdb), then this returns the
|
138
|
-
# puppet version optionally passed in to the constructor. This can also be nil.
|
169
|
+
# Stub method to return the puppet version if the back end doesn't support this.
|
139
170
|
# @return [String] Puppet version
|
140
171
|
def puppet_version
|
141
|
-
|
172
|
+
build
|
173
|
+
@options[:puppet_version]
|
142
174
|
end
|
143
175
|
|
144
176
|
# This allows retrieving a resource by type and title. This is intended for use when a O(1) lookup is required.
|
@@ -147,6 +179,7 @@ module OctocatalogDiff
|
|
147
179
|
# @return [Hash] Resource item
|
148
180
|
def resource(opts = {})
|
149
181
|
raise ArgumentError, ':type and :title are required' unless opts[:type] && opts[:title]
|
182
|
+
build
|
150
183
|
build_resource_hash if @resource_hash.nil?
|
151
184
|
return nil unless @resource_hash[opts[:type]].is_a?(Hash)
|
152
185
|
@resource_hash[opts[:type]][opts[:title]]
|
@@ -155,6 +188,7 @@ module OctocatalogDiff
|
|
155
188
|
# This is a compatibility layer for the resources, which are in a different place in Puppet 3.x and Puppet 4.x
|
156
189
|
# @return [Array] Resource array
|
157
190
|
def resources
|
191
|
+
build
|
158
192
|
raise OctocatalogDiff::Errors::CatalogError, 'Catalog does not appear to have been built' if !valid? && error_message.nil?
|
159
193
|
raise OctocatalogDiff::Errors::CatalogError, error_message unless valid?
|
160
194
|
return @catalog['data']['resources'] if @catalog['data'].is_a?(Hash) && @catalog['data']['resources'].is_a?(Array)
|
@@ -165,16 +199,17 @@ module OctocatalogDiff
|
|
165
199
|
# :nocov:
|
166
200
|
end
|
167
201
|
|
168
|
-
#
|
202
|
+
# Stub method of the the number of retries necessary to compile the catalog. If the underlying catalog
|
169
203
|
# generation backend does not support retries, nil is returned.
|
170
204
|
# @return [Integer] Retry count
|
171
205
|
def retries
|
172
|
-
|
206
|
+
nil
|
173
207
|
end
|
174
208
|
|
175
209
|
# Determine if the catalog build was successful.
|
176
210
|
# @return [Boolean] Whether the catalog is valid
|
177
211
|
def valid?
|
212
|
+
build
|
178
213
|
!@catalog.nil?
|
179
214
|
end
|
180
215
|
|
@@ -182,11 +217,19 @@ module OctocatalogDiff
|
|
182
217
|
# Raise a OctocatalogDiff::Errors::ReferenceValidationError for any found to be missing.
|
183
218
|
# Uses @options[:validate_references] to influence which references are checked.
|
184
219
|
def validate_references
|
220
|
+
# If we've already done the validation, don't do it again
|
221
|
+
return if @references_validated
|
222
|
+
@references_validated = true
|
223
|
+
|
185
224
|
# Skip out early if no reference validation has been requested.
|
186
225
|
unless @options[:validate_references].is_a?(Array) && @options[:validate_references].any?
|
187
226
|
return
|
188
227
|
end
|
189
228
|
|
229
|
+
# Puppet 5 has reference validation built-in and enabled, so there won't even be a valid catalog if
|
230
|
+
# there were invalid references. It's pointless to perform validation of our own.
|
231
|
+
return if puppet_version && puppet_version >= '5.0.0'
|
232
|
+
|
190
233
|
# Iterate over all the resources and check each one that has one of the attributes being checked.
|
191
234
|
# Keep track of all references that are missing for ultimate inclusion in the error message.
|
192
235
|
missing = []
|
@@ -204,7 +247,7 @@ module OctocatalogDiff
|
|
204
247
|
# At this point there is at least one broken/missing reference. Format an error message and raise.
|
205
248
|
errors = format_missing_references(missing)
|
206
249
|
plural = errors =~ /;/ ? 's' : ''
|
207
|
-
|
250
|
+
self.error_message = "Catalog has broken reference#{plural}: #{errors}"
|
208
251
|
end
|
209
252
|
|
210
253
|
private
|
@@ -265,29 +308,6 @@ module OctocatalogDiff
|
|
265
308
|
end
|
266
309
|
end
|
267
310
|
|
268
|
-
# Private method: Choose backend based on passed-in options
|
269
|
-
# @param options [Hash] Options passed into constructor
|
270
|
-
# @return [Object] OctocatalogDiff::Catalog::<whatever> object
|
271
|
-
def backend(options)
|
272
|
-
# Hard-coded backend
|
273
|
-
if options[:backend]
|
274
|
-
return OctocatalogDiff::Catalog::JSON.new(options) if options[:backend] == :json
|
275
|
-
return OctocatalogDiff::Catalog::PuppetDB.new(options) if options[:backend] == :puppetdb
|
276
|
-
return OctocatalogDiff::Catalog::PuppetMaster.new(options) if options[:backend] == :puppetmaster
|
277
|
-
return OctocatalogDiff::Catalog::Computed.new(options) if options[:backend] == :computed
|
278
|
-
return OctocatalogDiff::Catalog::Noop.new(options) if options[:backend] == :noop
|
279
|
-
raise ArgumentError, "Unknown backend :#{options[:backend]}"
|
280
|
-
end
|
281
|
-
|
282
|
-
# Determine backend based on arguments
|
283
|
-
return OctocatalogDiff::Catalog::JSON.new(options) if options[:json]
|
284
|
-
return OctocatalogDiff::Catalog::PuppetDB.new(options) if options[:puppetdb]
|
285
|
-
return OctocatalogDiff::Catalog::PuppetMaster.new(options) if options[:puppet_master]
|
286
|
-
|
287
|
-
# Default is to build catalog ourselves
|
288
|
-
OctocatalogDiff::Catalog::Computed.new(options)
|
289
|
-
end
|
290
|
-
|
291
311
|
# Private method: Build the resource hash to be used used for O(1) lookups by type and title.
|
292
312
|
# This method is called the first time the resource hash is accessed.
|
293
313
|
def build_resource_hash
|
@@ -4,6 +4,7 @@ require 'fileutils'
|
|
4
4
|
require 'json'
|
5
5
|
require 'stringio'
|
6
6
|
|
7
|
+
require_relative '../catalog'
|
7
8
|
require_relative '../catalog-util/bootstrap'
|
8
9
|
require_relative '../catalog-util/builddir'
|
9
10
|
require_relative '../catalog-util/command'
|
@@ -15,9 +16,7 @@ module OctocatalogDiff
|
|
15
16
|
class Catalog
|
16
17
|
# Represents a Puppet catalog that is computed (via `puppet master --compile ...`)
|
17
18
|
# By instantiating this class, the catalog is computed.
|
18
|
-
class Computed
|
19
|
-
attr_reader :node, :error_message, :catalog, :catalog_json, :retries
|
20
|
-
|
19
|
+
class Computed < OctocatalogDiff::Catalog
|
21
20
|
# Constructor
|
22
21
|
# @param :node [String] REQUIRED: Node name
|
23
22
|
# @param :basedir [String] Directory in which to compile the catalog
|
@@ -28,14 +27,10 @@ module OctocatalogDiff
|
|
28
27
|
# @param :puppet_version [String] Puppet version (optional; if not supplied, it is calculated)
|
29
28
|
# @param :puppet_command [String] Full command to run Puppet (optional; if not supplied, it is calculated)
|
30
29
|
def initialize(options)
|
31
|
-
|
32
|
-
raise ArgumentError, 'Node name must be passed to OctocatalogDiff::Catalog::Computed' unless options[:node].is_a?(String)
|
30
|
+
super
|
33
31
|
|
34
|
-
|
35
|
-
|
36
|
-
@error_message = nil
|
37
|
-
@catalog = nil
|
38
|
-
@catalog_json = nil
|
32
|
+
raise ArgumentError, 'Node name must be passed to OctocatalogDiff::Catalog::Computed' unless options[:node].is_a?(String)
|
33
|
+
raise ArgumentError, 'Branch is undefined' unless options[:branch]
|
39
34
|
|
40
35
|
# Additional class variables
|
41
36
|
@pass_env_vars = options.fetch(:pass_env_vars, [])
|
@@ -44,31 +39,15 @@ module OctocatalogDiff
|
|
44
39
|
@puppet_binary = options[:puppet_binary]
|
45
40
|
@puppet_version = options[:puppet_version]
|
46
41
|
@puppet_command = options[:puppet_command]
|
47
|
-
@retries = nil
|
48
42
|
@builddir = nil
|
49
43
|
@facts_terminus = options.fetch(:facts_terminus, 'yaml')
|
50
|
-
|
51
|
-
# Pass through the input for other access
|
52
|
-
@opts = options
|
53
|
-
raise ArgumentError, 'Branch is undefined' unless @opts[:branch]
|
54
|
-
end
|
55
|
-
|
56
|
-
# Actually build the catalog (populate @error_message, @catalog, @catalog_json)
|
57
|
-
def build(logger = Logger.new(StringIO.new))
|
58
|
-
if @facts_terminus != 'facter'
|
59
|
-
facts_obj = OctocatalogDiff::CatalogUtil::Facts.new(@opts, logger)
|
60
|
-
logger.debug "Start retrieving facts for #{@node} from #{self.class}"
|
61
|
-
@opts[:facts] = facts_obj.facts
|
62
|
-
logger.debug "Success retrieving facts for #{@node} from #{self.class}"
|
63
|
-
end
|
64
|
-
build_catalog(logger)
|
65
44
|
end
|
66
45
|
|
67
46
|
# Get the Puppet version
|
68
47
|
# @return [String] Puppet version
|
69
48
|
def puppet_version
|
70
49
|
raise ArgumentError, '"puppet_binary" was not passed to OctocatalogDiff::Catalog::Computed' unless @puppet_binary
|
71
|
-
@puppet_version ||= OctocatalogDiff::Util::PuppetVersion.puppet_version(@puppet_binary, @
|
50
|
+
@puppet_version ||= OctocatalogDiff::Util::PuppetVersion.puppet_version(@puppet_binary, @options)
|
72
51
|
end
|
73
52
|
|
74
53
|
# Compilation directory
|
@@ -80,7 +59,14 @@ module OctocatalogDiff
|
|
80
59
|
|
81
60
|
# Environment used to compile catalog
|
82
61
|
def environment
|
83
|
-
@
|
62
|
+
@options.fetch(:environment, 'production')
|
63
|
+
end
|
64
|
+
|
65
|
+
# Convert file resources source => "puppet:///..." to content => "actual content of file".
|
66
|
+
def convert_file_resources(dry_run = false)
|
67
|
+
return @options.key?(:basedir) if dry_run
|
68
|
+
return false unless @options[:basedir]
|
69
|
+
OctocatalogDiff::CatalogUtil::FileResources.convert_file_resources(self, environment)
|
84
70
|
end
|
85
71
|
|
86
72
|
private
|
@@ -105,29 +91,29 @@ module OctocatalogDiff
|
|
105
91
|
return if @builddir
|
106
92
|
|
107
93
|
# Fill options for creating and populating the temporary directory
|
108
|
-
tmphash = @
|
94
|
+
tmphash = @options.dup
|
109
95
|
|
110
96
|
# Bootstrap directory if needed
|
111
|
-
if !@
|
112
|
-
raise Errno::ENOENT, "Invalid dir #{@
|
113
|
-
tmphash[:basedir] = @
|
114
|
-
elsif @
|
115
|
-
if @
|
116
|
-
tmphash[:basedir] =
|
97
|
+
if !@options[:bootstrapped_dir].nil?
|
98
|
+
raise Errno::ENOENT, "Invalid dir #{@options[:bootstrapped_dir]}" unless File.directory?(@options[:bootstrapped_dir])
|
99
|
+
tmphash[:basedir] = @options[:bootstrapped_dir]
|
100
|
+
elsif @options[:branch] == '.'
|
101
|
+
if @options[:bootstrap_current]
|
102
|
+
tmphash[:basedir] = Dir.mktmpdir('ocd-bootstrap-basedir-')
|
117
103
|
at_exit { cleanup_checkout_dir(tmphash[:basedir], logger) }
|
118
104
|
|
119
|
-
FileUtils.cp_r File.join(@
|
105
|
+
FileUtils.cp_r File.join(@options[:basedir], '.'), tmphash[:basedir]
|
120
106
|
|
121
|
-
o = @
|
107
|
+
o = @options.reject { |k, _v| k == :branch }.merge(path: tmphash[:basedir])
|
122
108
|
OctocatalogDiff::CatalogUtil::Bootstrap.bootstrap_directory(o, logger)
|
123
109
|
else
|
124
|
-
tmphash[:basedir] = @
|
110
|
+
tmphash[:basedir] = @options[:basedir]
|
125
111
|
end
|
126
112
|
else
|
127
|
-
checkout_dir = Dir.mktmpdir
|
113
|
+
checkout_dir = Dir.mktmpdir('ocd-bootstrap-checkout-')
|
128
114
|
at_exit { cleanup_checkout_dir(checkout_dir, logger) }
|
129
115
|
tmphash[:basedir] = checkout_dir
|
130
|
-
OctocatalogDiff::CatalogUtil::Bootstrap.bootstrap_directory(@
|
116
|
+
OctocatalogDiff::CatalogUtil::Bootstrap.bootstrap_directory(@options.merge(path: checkout_dir), logger)
|
131
117
|
end
|
132
118
|
|
133
119
|
# Create and populate the temporary directory
|
@@ -136,8 +122,14 @@ module OctocatalogDiff
|
|
136
122
|
|
137
123
|
# Private method: Build catalog by running Puppet
|
138
124
|
# @param logger [Logger] Logger object
|
139
|
-
def build_catalog(logger
|
140
|
-
|
125
|
+
def build_catalog(logger)
|
126
|
+
if @facts_terminus != 'facter'
|
127
|
+
facts_obj = OctocatalogDiff::CatalogUtil::Facts.new(@options, logger)
|
128
|
+
logger.debug "Start retrieving facts for #{@node} from #{self.class}"
|
129
|
+
@options[:facts] = facts_obj.facts
|
130
|
+
logger.debug "Success retrieving facts for #{@node} from #{self.class}"
|
131
|
+
end
|
132
|
+
|
141
133
|
bootstrap(logger)
|
142
134
|
result = run_puppet(logger)
|
143
135
|
@retries = result[:retries]
|
@@ -168,10 +160,10 @@ module OctocatalogDiff
|
|
168
160
|
@puppet_command_obj ||= begin
|
169
161
|
raise ArgumentError, '"puppet_binary" was not passed to OctocatalogDiff::Catalog::Computed' unless @puppet_binary
|
170
162
|
|
171
|
-
command_opts = @
|
163
|
+
command_opts = @options.merge(
|
172
164
|
node: @node,
|
173
165
|
compilation_dir: @builddir.tempdir,
|
174
|
-
parser: @
|
166
|
+
parser: @options.fetch(:parser, :default),
|
175
167
|
puppet_binary: @puppet_binary,
|
176
168
|
fact_file: @builddir.fact_file,
|
177
169
|
dir: @builddir.tempdir,
|
@@ -201,7 +193,7 @@ module OctocatalogDiff
|
|
201
193
|
# Set up the ScriptRunner
|
202
194
|
scriptrunner = OctocatalogDiff::Util::ScriptRunner.new(
|
203
195
|
default_script: 'puppet/puppet.sh',
|
204
|
-
override_script_path: @
|
196
|
+
override_script_path: @options[:override_script_path]
|
205
197
|
)
|
206
198
|
|
207
199
|
begin
|
@@ -1,21 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative '../catalog'
|
4
|
+
require_relative '../catalog-util/fileresources'
|
5
|
+
|
3
6
|
require 'json'
|
4
7
|
|
5
8
|
module OctocatalogDiff
|
6
9
|
class Catalog
|
7
10
|
# Represents a Puppet catalog that is read in directly from a JSON file.
|
8
|
-
class JSON
|
9
|
-
attr_accessor :node
|
10
|
-
attr_reader :error_message, :catalog, :catalog_json
|
11
|
-
|
11
|
+
class JSON < OctocatalogDiff::Catalog
|
12
12
|
# Constructor
|
13
13
|
# @param :json [String] REQUIRED: Content of catalog, will be parsed as JSON
|
14
14
|
# @param :node [String] Node name (if not supplied, will be determined from catalog)
|
15
15
|
def initialize(options)
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
super
|
17
|
+
|
18
|
+
unless options[:json].is_a?(String)
|
19
|
+
raise ArgumentError, "Must supply :json as string in options: #{options[:json].class}"
|
20
|
+
end
|
21
|
+
|
22
|
+
@catalog_json = options.fetch(:json)
|
19
23
|
begin
|
20
24
|
@catalog = ::JSON.parse(@catalog_json)
|
21
25
|
@error_message = nil
|
@@ -27,6 +31,13 @@ module OctocatalogDiff
|
|
27
31
|
@catalog_json = nil
|
28
32
|
end
|
29
33
|
end
|
34
|
+
|
35
|
+
# Convert file resources source => "puppet:///..." to content => "actual content of file".
|
36
|
+
def convert_file_resources(dry_run = false)
|
37
|
+
return @options.key?(:basedir) if dry_run
|
38
|
+
return false unless @options[:basedir]
|
39
|
+
OctocatalogDiff::CatalogUtil::FileResources.convert_file_resources(self, environment)
|
40
|
+
end
|
30
41
|
end
|
31
42
|
end
|
32
43
|
end
|
@@ -1,16 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative '../catalog'
|
4
|
+
|
3
5
|
require 'json'
|
4
6
|
|
5
7
|
module OctocatalogDiff
|
6
8
|
class Catalog
|
7
9
|
# Represents a null Puppet catalog.
|
8
|
-
class Noop
|
9
|
-
attr_accessor :node
|
10
|
-
attr_reader :error_message, :catalog, :catalog_json
|
11
|
-
|
12
|
-
# Constructor
|
10
|
+
class Noop < OctocatalogDiff::Catalog
|
13
11
|
def initialize(options)
|
12
|
+
super
|
13
|
+
|
14
14
|
@catalog_json = '{"resources":[]}'
|
15
15
|
@catalog = { 'resources' => [] }
|
16
16
|
@error_message = nil
|
@@ -3,43 +3,30 @@
|
|
3
3
|
require 'json'
|
4
4
|
require 'stringio'
|
5
5
|
|
6
|
+
require_relative '../catalog'
|
6
7
|
require_relative '../errors'
|
7
8
|
require_relative '../puppetdb'
|
8
9
|
|
9
10
|
module OctocatalogDiff
|
10
11
|
class Catalog
|
11
12
|
# Represents a Puppet catalog that is read from PuppetDB.
|
12
|
-
class PuppetDB
|
13
|
-
attr_accessor :node
|
14
|
-
attr_reader :error_message, :catalog, :catalog_json, :retries, :convert_file_resources
|
15
|
-
|
13
|
+
class PuppetDB < OctocatalogDiff::Catalog
|
16
14
|
# Constructor - See OctocatalogDiff::PuppetDB for additional parameters
|
17
15
|
# @param :node [String] Node name
|
18
16
|
# @param :retry [Integer] Number of retries, if fetch fails
|
19
17
|
def initialize(options)
|
20
|
-
|
21
|
-
raise ArgumentError, 'node must be a non-empty string' unless options[:node].is_a?(String) && options[:node] != ''
|
22
|
-
@node = options[:node]
|
23
|
-
@catalog = nil
|
24
|
-
@error_message = nil
|
25
|
-
@retries = nil
|
26
|
-
|
27
|
-
# Cannot convert file resources from this type of catalog
|
28
|
-
@convert_file_resources = false
|
18
|
+
super
|
29
19
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
def build(logger = Logger.new(StringIO.new))
|
35
|
-
fetch_catalog(logger)
|
20
|
+
unless @options[:node].is_a?(String) && @options[:node] != ''
|
21
|
+
raise ArgumentError, 'node must be a non-empty string'
|
22
|
+
end
|
36
23
|
end
|
37
24
|
|
38
25
|
private
|
39
26
|
|
40
27
|
# Private method: Get catalog from PuppetDB. Sets @catalog / @catalog_json or @error_message
|
41
28
|
# @param logger [Logger object] Logger object
|
42
|
-
def
|
29
|
+
def build_catalog(logger)
|
43
30
|
# Use OctocatalogDiff::PuppetDB to interact with puppetdb
|
44
31
|
puppetdb_obj = OctocatalogDiff::PuppetDB.new(@options)
|
45
32
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative '../catalog'
|
3
4
|
require_relative '../catalog-util/facts'
|
4
5
|
require_relative '../external/pson/pure'
|
5
6
|
require_relative '../util/httparty'
|
@@ -11,10 +12,7 @@ require 'stringio'
|
|
11
12
|
module OctocatalogDiff
|
12
13
|
class Catalog
|
13
14
|
# Represents a Puppet catalog that is obtained by contacting the Puppet Master.
|
14
|
-
class PuppetMaster
|
15
|
-
attr_accessor :node
|
16
|
-
attr_reader :error_message, :catalog, :catalog_json, :convert_file_resources, :options, :retries
|
17
|
-
|
15
|
+
class PuppetMaster < OctocatalogDiff::Catalog
|
18
16
|
# Defaults
|
19
17
|
DEFAULT_PUPPET_PORT_NUMBER = 8140
|
20
18
|
DEFAULT_PUPPET_SERVER_API = 3
|
@@ -34,32 +32,29 @@ module OctocatalogDiff
|
|
34
32
|
# @param :puppet_master_ssl_client_auth [Boolean] Override the client-auth that is guessed from parameters
|
35
33
|
# @param :timeout [Integer] Connection timeout for Puppet master (default=PUPPET_MASTER_TIMEOUT seconds)
|
36
34
|
def initialize(options)
|
37
|
-
|
38
|
-
|
39
|
-
unless options[:
|
35
|
+
super
|
36
|
+
|
37
|
+
unless @options[:node].is_a?(String) && @options[:node] != ''
|
38
|
+
raise ArgumentError, 'node must be a non-empty string'
|
39
|
+
end
|
40
|
+
|
41
|
+
unless @options[:branch].is_a?(String) && @options[:branch] != ''
|
40
42
|
raise ArgumentError, 'Environment must be a non-empty string'
|
41
43
|
end
|
42
|
-
|
44
|
+
|
45
|
+
unless @options[:puppet_master].is_a?(String) && @options[:puppet_master] != ''
|
43
46
|
raise ArgumentError, 'Puppet Master must be a non-empty string'
|
44
47
|
end
|
45
48
|
|
46
|
-
@node = options[:node]
|
47
|
-
@catalog = nil
|
48
|
-
@error_message = nil
|
49
|
-
@retries = nil
|
50
49
|
@timeout = options.fetch(:puppet_master_timeout, options.fetch(:timeout, PUPPET_MASTER_TIMEOUT))
|
51
50
|
@retry_failed_catalog = options.fetch(:retry_failed_catalog, 0)
|
52
|
-
|
53
|
-
# Cannot convert file resources from this type of catalog right now.
|
54
|
-
# FIXME: This is possible with additional API calls but is current unimplemented.
|
55
|
-
@convert_file_resources = false
|
56
|
-
|
57
|
-
options[:puppet_master] += ":#{DEFAULT_PUPPET_PORT_NUMBER}" unless options[:puppet_master] =~ /\:\d+$/
|
58
|
-
@options = options
|
51
|
+
@options[:puppet_master] += ":#{DEFAULT_PUPPET_PORT_NUMBER}" unless @options[:puppet_master] =~ /\:\d+$/
|
59
52
|
end
|
60
53
|
|
54
|
+
private
|
55
|
+
|
61
56
|
# Build method
|
62
|
-
def
|
57
|
+
def build_catalog(logger = Logger.new(StringIO.new))
|
63
58
|
facts_obj = OctocatalogDiff::CatalogUtil::Facts.new(@options, logger)
|
64
59
|
logger.debug "Start retrieving facts for #{@node} from #{self.class}"
|
65
60
|
@facts = facts_obj.facts
|
@@ -67,8 +62,6 @@ module OctocatalogDiff
|
|
67
62
|
fetch_catalog(logger)
|
68
63
|
end
|
69
64
|
|
70
|
-
private
|
71
|
-
|
72
65
|
# Returns a hash of parameters for each supported version of the Puppet Server Catalog API.
|
73
66
|
# @return [Hash] Hash of parameters
|
74
67
|
#
|
@@ -11,7 +11,6 @@ module OctocatalogDiff
|
|
11
11
|
class BootstrapError < RuntimeError; end
|
12
12
|
class CatalogError < RuntimeError; end
|
13
13
|
class PuppetVersionError < RuntimeError; end
|
14
|
-
class ReferenceValidationError < RuntimeError; end
|
15
14
|
class GitCheckoutError < RuntimeError; end
|
16
15
|
|
17
16
|
# Error classes for retrieving facts
|
@@ -63,15 +63,15 @@ module OctocatalogDiff
|
|
63
63
|
# build the catalog.
|
64
64
|
result = {}
|
65
65
|
catalog_tasks.each do |x|
|
66
|
-
result[x[0]] = OctocatalogDiff::Catalog.
|
66
|
+
result[x[0]] = OctocatalogDiff::Catalog.create(x[1].args)
|
67
67
|
@logger.debug "Initialized #{result[x[0]].builder} for #{x[0]}-catalog"
|
68
68
|
end
|
69
69
|
|
70
70
|
# Disable --compare-file-text if either (or both) of the chosen backends do not support it
|
71
71
|
if @options.fetch(:compare_file_text, false)
|
72
|
-
result.each do |_key,
|
73
|
-
next
|
74
|
-
@logger.debug "Disabling --compare-file-text; not supported by #{
|
72
|
+
result.each do |_key, builder_obj|
|
73
|
+
next if builder_obj.convert_file_resources(true)
|
74
|
+
@logger.debug "Disabling --compare-file-text; not supported by #{builder_obj.builder}"
|
75
75
|
@options[:compare_file_text] = false
|
76
76
|
catalog_tasks.map! do |x|
|
77
77
|
x[1].args[:compare_file_text] = false
|
@@ -147,6 +147,7 @@ module OctocatalogDiff
|
|
147
147
|
retry_failed_catalog: @options.fetch(:retry_failed_catalog, 0),
|
148
148
|
parser: @options["parser_#{key}".to_sym]
|
149
149
|
)
|
150
|
+
args[:basedir] ||= args[:bootstrapped_dir]
|
150
151
|
|
151
152
|
# If any options are in the form of 'to_SOMETHING' or 'from_SOMETHING', this sets the option to
|
152
153
|
# 'SOMETHING' for the catalog if it matches this key. For example, when compiling the 'to' catalog
|
@@ -157,6 +158,9 @@ module OctocatalogDiff
|
|
157
158
|
args.delete(opt_key)
|
158
159
|
end
|
159
160
|
|
161
|
+
# Skip reference validation in the from-catalog by saying we already performed it.
|
162
|
+
args[:references_validated] = (key == :from)
|
163
|
+
|
160
164
|
# The task is a OctocatalogDiff::Util::Parallel::Task object that contains the method to execute,
|
161
165
|
# validator method, text description, and arguments to provide when calling the method.
|
162
166
|
task = OctocatalogDiff::Util::Parallel::Task.new(
|
@@ -250,10 +254,9 @@ module OctocatalogDiff
|
|
250
254
|
# @param logger [Logger] Logger object (presently unused)
|
251
255
|
# @param args [Hash] Additional arguments set specifically for validator
|
252
256
|
# @return [Boolean] Return true if catalog is valid, false otherwise
|
253
|
-
def catalog_validator(catalog = nil, _logger = @logger,
|
257
|
+
def catalog_validator(catalog = nil, _logger = @logger, _args = {})
|
254
258
|
raise ArgumentError, "Expects a catalog, got #{catalog.class}" unless catalog.is_a?(OctocatalogDiff::Catalog)
|
255
259
|
raise OctocatalogDiff::Errors::CatalogError, "Catalog failed: #{catalog.error_message}" unless catalog.valid?
|
256
|
-
catalog.validate_references if args[:task] == :to # Raises exception for broken references
|
257
260
|
true
|
258
261
|
end
|
259
262
|
end
|
@@ -107,7 +107,7 @@ module OctocatalogDiff
|
|
107
107
|
# @return [Exception] First exception encountered by a child process; returns nil if no exceptions encountered.
|
108
108
|
def self.run_tasks_parallel(result, task_array, logger)
|
109
109
|
pidmap = {}
|
110
|
-
ipc_tempdir = Dir.mktmpdir
|
110
|
+
ipc_tempdir = Dir.mktmpdir('ocd-ipc-')
|
111
111
|
|
112
112
|
# Child process forking
|
113
113
|
task_array.each_with_index do |task, index|
|
@@ -91,7 +91,7 @@ module OctocatalogDiff
|
|
91
91
|
# @return [String] Path to tempfile containing script
|
92
92
|
def temp_script(script)
|
93
93
|
raise Errno::ENOENT, "Script '#{script}' not found" unless File.file?(script)
|
94
|
-
temp_dir = Dir.mktmpdir
|
94
|
+
temp_dir = Dir.mktmpdir('ocd-scriptrunner-')
|
95
95
|
at_exit do
|
96
96
|
begin
|
97
97
|
FileUtils.remove_entry_secure temp_dir
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: octocatalog-diff
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GitHub, Inc.
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2017-
|
12
|
+
date: 2017-08-03 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: diffy
|
@@ -137,32 +137,18 @@ dependencies:
|
|
137
137
|
- - '='
|
138
138
|
- !ruby/object:Gem::Version
|
139
139
|
version: 0.48.1
|
140
|
-
- !ruby/object:Gem::Dependency
|
141
|
-
name: puppetdb-terminus
|
142
|
-
requirement: !ruby/object:Gem::Requirement
|
143
|
-
requirements:
|
144
|
-
- - '='
|
145
|
-
- !ruby/object:Gem::Version
|
146
|
-
version: 3.2.4
|
147
|
-
type: :development
|
148
|
-
prerelease: false
|
149
|
-
version_requirements: !ruby/object:Gem::Requirement
|
150
|
-
requirements:
|
151
|
-
- - '='
|
152
|
-
- !ruby/object:Gem::Version
|
153
|
-
version: 3.2.4
|
154
140
|
- !ruby/object:Gem::Dependency
|
155
141
|
name: simplecov
|
156
142
|
requirement: !ruby/object:Gem::Requirement
|
157
143
|
requirements:
|
158
|
-
- - "
|
144
|
+
- - "~>"
|
159
145
|
- !ruby/object:Gem::Version
|
160
146
|
version: 0.14.1
|
161
147
|
type: :development
|
162
148
|
prerelease: false
|
163
149
|
version_requirements: !ruby/object:Gem::Requirement
|
164
150
|
requirements:
|
165
|
-
- - "
|
151
|
+
- - "~>"
|
166
152
|
- !ruby/object:Gem::Version
|
167
153
|
version: 0.14.1
|
168
154
|
- !ruby/object:Gem::Dependency
|
@@ -193,6 +179,20 @@ dependencies:
|
|
193
179
|
- - "~>"
|
194
180
|
- !ruby/object:Gem::Version
|
195
181
|
version: 4.10.0
|
182
|
+
- !ruby/object:Gem::Dependency
|
183
|
+
name: puppetdb-terminus
|
184
|
+
requirement: !ruby/object:Gem::Requirement
|
185
|
+
requirements:
|
186
|
+
- - '='
|
187
|
+
- !ruby/object:Gem::Version
|
188
|
+
version: 3.2.4
|
189
|
+
type: :development
|
190
|
+
prerelease: false
|
191
|
+
version_requirements: !ruby/object:Gem::Requirement
|
192
|
+
requirements:
|
193
|
+
- - '='
|
194
|
+
- !ruby/object:Gem::Version
|
195
|
+
version: 3.2.4
|
196
196
|
description: |
|
197
197
|
Octocatalog-Diff assists with Puppet development and testing by enabling the user to
|
198
198
|
compile 2 Puppet catalogs and compare them. It is possible to compare different
|