puppet-lint-wmf_styleguide-check 1.0.0

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