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.
- checksums.yaml +7 -0
- data/LICENSE +674 -0
- data/README.md +41 -0
- data/lib/puppet-lint/plugins/check_wmf_styleguide.rb +436 -0
- data/spec/puppet-lint/plugins/check_wmf_styleguide_check_spec.rb +180 -0
- data/spec/spec_helper.rb +3 -0
- metadata +157 -0
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
|