puppet-lint-wmf_styleguide-check 1.0.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.
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # puppet-lint-wmf_styleguide check
2
+ Puppet-lint plugin to check for violations of the WMF puppet coding style guide.
3
+
4
+ There are quite a few prescriptions for how to write puppet manifests in the
5
+ puppet coding page at https://wikitech.wikimedia.org/wiki/Puppet_coding.
6
+
7
+ While most of the coding style requirements are already covered by puppet-lint,
8
+ quite a few of them are not, specifically our own flavour of the role/profile
9
+ pattern.
10
+
11
+ This plugin checks those specific violations, so we have specific checks for
12
+ classes, roles, profiles and defined types. Let's see which in order.
13
+
14
+ For classes in modules, we check that:
15
+ * no hiera() call is made
16
+ * no class inclusion or declaration happens across modules
17
+ * no system::role call is made
18
+
19
+ For roles, we check that:
20
+ * no hiera() call is made
21
+ * no class is included that is not a profile
22
+ * no class is explicitly declared [TODO]
23
+ * one and only one system::role call is made
24
+
25
+ For profiles, our checks are:
26
+ * Every parameter has an explicit hiera() call
27
+ * No hiera() call is made outside of parameters
28
+ * No classes are included that are not globals or profiles
29
+ * No system::role declaration
30
+
31
+ For defined types, we check that:
32
+ * no hiera() call is made
33
+ * no class from other modules is either included or declared (except for defined
34
+ types in the profile module, which can declare classes from other modules).
35
+
36
+ While some of the rules are not enforced right now (so we don't check for
37
+ defines from other modules), that can be refined in the future.
38
+
39
+ This plugin will output a ton of errors when ran on the operations/puppet
40
+ repository as it stands now, and that's good as it gives us a good measure of
41
+ where we are in the transition, and will help enforce the style guide afterwards.
@@ -0,0 +1,436 @@
1
+ # Class to manage puppet resources.
2
+ class PuppetResource
3
+ attr_accessor :profile_module, :role_module
4
+
5
+ def initialize(resource_hash)
6
+ # Input should be a resource coming from
7
+ # the resource index
8
+ @resource = resource_hash
9
+ @params = parse_params
10
+ end
11
+
12
+ def type
13
+ if class?
14
+ 'class'
15
+ else
16
+ 'defined type'
17
+ end
18
+ end
19
+
20
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
21
+ def parse_params
22
+ # Parse parameters and return a hash containing
23
+ # the parameter name as a key and a hash of tokens as value:
24
+ # :param => the parameter token
25
+ # :value => All the code tokens that represent the value of the parameter
26
+ res = {}
27
+ current_param = nil
28
+ in_value = false
29
+ return res unless @resource[:param_tokens]
30
+ @resource[:param_tokens].each do |token|
31
+ case token.type
32
+ when :VARIABLE
33
+ current_param = token.value
34
+ res[current_param] ||= { param: token, value: [] }
35
+ when :COMMA
36
+ current_param = nil
37
+ in_value = false
38
+ when :EQUALS
39
+ in_value = true
40
+ when *PuppetLint::Lexer::FORMATTING_TOKENS
41
+ # Skip non-code tokens
42
+ next
43
+ else
44
+ res[current_param][:value] << token if in_value && token
45
+ end
46
+ end
47
+ res
48
+ end
49
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
50
+
51
+ def params
52
+ @params || parse_params
53
+ end
54
+
55
+ def profile_module
56
+ @profile_module || 'profile'
57
+ end
58
+
59
+ def role_module
60
+ @role_module || 'role'
61
+ end
62
+
63
+ def class?
64
+ @resource[:type] == :CLASS
65
+ end
66
+
67
+ def name
68
+ @resource[:name_token].value.gsub(/^::/, '')
69
+ end
70
+
71
+ def path
72
+ @resource[:path]
73
+ end
74
+
75
+ def filename
76
+ puts @resource
77
+ @resource[:filename]
78
+ end
79
+
80
+ def module_name
81
+ name.split('::')[0]
82
+ end
83
+
84
+ def profile?
85
+ class? && (module_name == profile_module)
86
+ end
87
+
88
+ def role?
89
+ class? && (module_name == role_module)
90
+ end
91
+
92
+ def hiera_calls
93
+ @resource[:tokens].select(&:hiera?)
94
+ end
95
+
96
+ def included_classes
97
+ @resource[:tokens].map(&:included_class).compact
98
+ end
99
+
100
+ def declared_classes
101
+ @resource[:tokens].map(&:declared_class).compact
102
+ end
103
+
104
+ def declared_resources
105
+ @resource[:tokens].select(&:declared_type?)
106
+ end
107
+
108
+ def resource?(name)
109
+ @resource[:tokens].select { |t| t.declared_type? && t.value.gsub(/^::/, '') == name }
110
+ end
111
+ end
112
+
113
+ class PuppetLint
114
+ class Lexer
115
+ # Add some utility functions to the PuppetLint::Lexer::Token class
116
+ class Token
117
+ # Extend the basic token with utility functions
118
+ def function?
119
+ @type == :NAME && @next_code_token.type == :LPAREN
120
+ end
121
+
122
+ def hiera?
123
+ function? && @value == 'hiera'
124
+ end
125
+
126
+ def class_include?
127
+ @type == :NAME && ['include', 'require'].include?(@value) && @next_code_token.type != :FARROW
128
+ end
129
+
130
+ def included_class
131
+ return unless class_include?
132
+ return @next_code_token.next_code_token if @next_code_token.type == :LPAREN
133
+ @next_code_token
134
+ end
135
+
136
+ def declared_class
137
+ return unless @type == :CLASS
138
+ # In a class declaration, the first token is the class declaration itself.
139
+ return if @next_code_token.type != :LBRACE
140
+ @next_code_token.next_code_token
141
+ end
142
+
143
+ def declared_type?
144
+ @type == :NAME && @next_code_token.type == :LBRACE && @prev_code_token.type != :CLASS
145
+ end
146
+
147
+ def node_def?
148
+ [:SSTRING, :STRING, :NAME, :REGEX].include?(@type)
149
+ end
150
+
151
+ def role_keyword?
152
+ @type == :NAME && @value = 'role' && @next_code_token.type == :LPAREN
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ # Checks and functions
159
+ def check_profile(klass)
160
+ # All parameters of profiles should have a default value that is a hiera lookup
161
+ params_without_hiera_defaults klass
162
+ # All hiera lookups should be in parameters
163
+ hiera_not_in_params klass
164
+ # Only a few selected classes should be included in a profile
165
+ profile_illegal_include klass
166
+ # System::role only goes in roles
167
+ check_no_system_role klass
168
+ end
169
+
170
+ def check_role(klass)
171
+ # Hiera lookups within a role are forbidden
172
+ hiera klass
173
+ # A role should only include profiles
174
+ include_not_profile klass
175
+ # A call, and only one, to system::role will be done
176
+ check_system_role klass
177
+ # No defines should be present in a role
178
+ check_no_defines klass
179
+ end
180
+
181
+ def check_class(klass)
182
+ # No hiera lookups allowed in a class.
183
+ hiera klass
184
+ # Cannot include or declare classes from other modules
185
+ class_illegal_include klass
186
+ illegal_class_declaration klass
187
+ # System::role only goes in roles
188
+ check_no_system_role klass
189
+ end
190
+
191
+ def check_define(define)
192
+ # No hiera calls are admitted in defines. ever.
193
+ hiera define
194
+ # No class can be included in defines, like in classes
195
+ class_illegal_include define
196
+ # Non-profile defines should respect the rules for classes
197
+ illegal_class_declaration define unless define.module_name == 'profile'
198
+ end
199
+
200
+ def hiera(klass)
201
+ hiera_errors(klass.hiera_calls, klass)
202
+ end
203
+
204
+ def params_without_hiera_defaults(klass)
205
+ # Finds parameters that have no hiera-defined default value.
206
+ klass.params.each do |name, data|
207
+ next unless data[:value].select(&:hiera?).empty?
208
+ token = data[:param]
209
+ msg = {
210
+ message: "wmf-style: Parameter '#{name}' of class '#{klass.name}' has no call to hiera",
211
+ line: token.line,
212
+ column: token.column
213
+ }
214
+ notify :error, msg
215
+ end
216
+ end
217
+
218
+ def hiera_not_in_params(klass)
219
+ tokens = klass.hiera_calls.reject do |token|
220
+ maybe_param = token.prev_code_token.prev_code_token
221
+ klass.params.keys.include?(maybe_param.value)
222
+ end
223
+ hiera_errors(tokens, klass)
224
+ end
225
+
226
+ def hiera_errors(tokens, klass)
227
+ tokens.each do |token|
228
+ value = token.next_code_token.next_code_token.value
229
+ msg = {
230
+ message: "wmf-style: Found hiera call in #{klass.type} '#{klass.name}' for '#{value}'",
231
+ line: token.line,
232
+ column: token.column
233
+ }
234
+ notify :error, msg
235
+ end
236
+ end
237
+
238
+ def profile_illegal_include(klass)
239
+ modules_include_ok = ['profile', 'passwords']
240
+ classes_include_ok = ['lvs::configuration', 'network::constants']
241
+ klass.included_classes.each do |token|
242
+ class_name = token.value.gsub(/^::/, '')
243
+ next if classes_include_ok.include? class_name
244
+ module_name = class_name.split('::')[0]
245
+ next if modules_include_ok.include? module_name
246
+ msg = {
247
+ message: "wmf-style: profile '#{klass.name}' includes non-profile class #{class_name}",
248
+ line: token.line,
249
+ column: token.column
250
+ }
251
+ notify :error, msg
252
+ end
253
+ end
254
+
255
+ def class_illegal_include(klass)
256
+ modules_include_ok = [klass.module_name]
257
+ klass.included_classes.each do |token|
258
+ class_name = token.value.gsub(/^::/, '')
259
+ module_name = class_name.split('::')[0]
260
+ next if modules_include_ok.include? module_name
261
+ msg = {
262
+ message: "wmf-style: #{klass.type} '#{klass.name}' includes #{class_name} from another module",
263
+ line: token.line,
264
+ column: token.column
265
+ }
266
+ notify :error, msg
267
+ end
268
+ end
269
+
270
+ def include_not_profile(klass)
271
+ modules_include_ok = ['role', 'profile', 'standard']
272
+ klass.included_classes.each do |token|
273
+ class_name = token.value.gsub(/^::/, '')
274
+ module_name = class_name.split('::')[0]
275
+ next if modules_include_ok.include? module_name
276
+ msg = {
277
+ message: "wmf-style: role '#{klass.name}' includes #{class_name} which is neither a role nor a profile",
278
+ line: token.line,
279
+ column: token.column
280
+ }
281
+ notify :error, msg
282
+ end
283
+ end
284
+
285
+ def illegal_class_declaration(klass)
286
+ # Classes and defines should NEVER declare
287
+ # classes from other modules.
288
+ # If a class has multiple such occurrences, it should be a profile
289
+ klass.declared_classes.each do |token|
290
+ class_name = token.value.gsub(/^::/, '')
291
+ module_name = class_name.split('::')[0]
292
+ next if klass.module_name == module_name
293
+ msg = {
294
+ message: "wmf-style: #{klass.type} '#{klass.name}' declares class #{class_name} from another module",
295
+ line: token.line,
296
+ column: token.column
297
+ }
298
+ notify :error, msg
299
+ end
300
+ end
301
+
302
+ def check_no_system_role(klass)
303
+ # The system::role define should only be used in roles
304
+ klass.resource?('system::role').each do |token|
305
+ msg = {
306
+ message: "wmf-style: #{klass.type} '#{klass.name}' declares system::role, which should only be used in roles",
307
+ line: token.line,
308
+ column: token.column
309
+ }
310
+ notify :error, msg
311
+ end
312
+ end
313
+
314
+ def check_system_role(klass)
315
+ return if klass.resource?('system::role').length == 1
316
+ msg = {
317
+ message: "wmf-style: role '#{klass.name}' should declare system::role once",
318
+ line: 1,
319
+ column: 1
320
+ }
321
+ notify :error, msg
322
+ end
323
+
324
+ def check_no_defines(klass)
325
+ return if klass.declared_resources == klass.resource?('system::role')
326
+ msg = {
327
+ message: "wmf-style: role '#{klass.name}' should not include defines",
328
+ line: 1,
329
+ column: 1
330
+ }
331
+ notify :error, msg
332
+ end
333
+
334
+ # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize, Metrics/CyclomaticComplexity
335
+ def check_node(node)
336
+ title = node[:title_tokens].map(&:value).join(', ')
337
+ node[:tokens].each do |token|
338
+ msg = nil
339
+ if token.hiera?
340
+ msg = {
341
+ message: "wmf-style: Found hiera call in node '#{title}'",
342
+ line: token.line,
343
+ column: token.column
344
+ }
345
+
346
+ elsif token.class_include?
347
+ msg = {
348
+ message: "wmf-style: node '#{title}' includes class #{token.included_class.value}",
349
+ line: token.line,
350
+ column: token.column
351
+ }
352
+ elsif token.declared_class
353
+ msg = {
354
+ message: "wmf-style: node '#{title}' declares class #{token.declared_class.value}",
355
+ line: token.line,
356
+ column: token.column
357
+ }
358
+ elsif token.declared_type?
359
+ msg = {
360
+ message: "wmf-style: node '#{title}' declares #{token.value}",
361
+ line: token.line,
362
+ column: token.column
363
+ }
364
+ elsif token.role_keyword? && token.next_code_token.next_code_token.next_code_token.type != :RPAREN
365
+ msg = {
366
+ message: "wmf-style: node '#{title}' includes multiple roles",
367
+ line: token.line,
368
+ column: token.column
369
+ }
370
+ end
371
+ notify :error, msg if msg
372
+ end
373
+ end
374
+
375
+ PuppetLint.new_check(:wmf_styleguide) do
376
+ def node_indexes
377
+ # Override the faulty "node_indexes" method from puppet-lint
378
+ result = []
379
+ in_node_def = false
380
+ braces_level = nil
381
+ start = 0
382
+ title_tokens = []
383
+ tokens.each_with_index do |token, i|
384
+ if token.type == :NODE
385
+ braces_level = 0
386
+ start = i
387
+ in_node_def = true
388
+ next
389
+ end
390
+ # If we're not within a node definition, skip this token
391
+ next unless in_node_def
392
+ case token.type
393
+ when :LBRACE
394
+ title_tokens = tokens[start + 1..(i - 1)].select(&:node_def?) if braces_level.zero?
395
+ braces_level += 1
396
+ when :RBRACE
397
+ braces_level -= 1
398
+ if braces_level.zero?
399
+ result << {
400
+ start: start,
401
+ end: i,
402
+ tokens: tokens[start..i],
403
+ title_tokens: title_tokens
404
+ }
405
+ in_node_def = false
406
+ end
407
+ end
408
+ end
409
+ result
410
+ end
411
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PercievedComplexity
412
+
413
+ def check_classes
414
+ class_indexes.each do |cl|
415
+ klass = PuppetResource.new(cl)
416
+ if klass.profile?
417
+ check_profile klass
418
+ elsif klass.role?
419
+ check_role klass
420
+ else
421
+ check_class klass
422
+ end
423
+ end
424
+ end
425
+
426
+ def check
427
+ check_classes
428
+ defined_type_indexes.each do |df|
429
+ define = PuppetResource.new(df)
430
+ check_define define
431
+ end
432
+ node_indexes.each do |node|
433
+ check_node node
434
+ end
435
+ end
436
+ end