@@ -57,19 +57,13 @@ class SamTemplate:
57
57
"Create a cfnlintrc config file to specify additional parameters. "
58
58
"For more information, see: https://github.com/aws-cloudformation/cfn-lint" ,
59
59
)
60
- @click .option (
61
- "--serverless-rules" ,
62
- is_flag = True ,
63
- help = "[DEPRECATED] Enable Serverless Rules for linting validation. "
64
- "Requires the cfn-lint-serverless package to be installed. "
65
- "Use --extra-lint-rules=\" cfn_lint_serverless.rules\" instead. "
66
- "For more information, see: https://github.com/awslabs/serverless-rules" ,
67
- )
68
60
@click .option (
69
61
"--extra-lint-rules" ,
70
62
help = "Specify additional lint rules to be used with cfn-lint. "
71
- "Format: module.path (e.g. 'cfn_lint_serverless.rules')" ,
72
- default = None
63
+ "Format: module.path (e.g. 'cfn_lint_serverless.rules'). "
64
+ "Multiple rule modules can be specified by separating with commas or using this option multiple times." ,
65
+ default = None ,
66
+ multiple = True
73
67
)
74
68
@save_params_option
75
69
@pass_context
@@ -78,22 +72,12 @@ class SamTemplate:
78
72
@print_cmdline_args
79
73
@unsupported_command_cdk (alternative_command = "cdk doctor" )
80
74
@command_exception_handler
81
- def cli (ctx , template_file , config_file , config_env , lint , save_params , serverless_rules , extra_lint_rules ):
75
+ def cli (ctx , template_file , config_file , config_env , lint , save_params , extra_lint_rules ):
82
76
# All logic must be implemented in the ``do_cli`` method. This helps with easy unit testing
83
-
84
- # Show warning and convert to extra_lint_rules if serverless_rules is used
85
- if serverless_rules and not extra_lint_rules :
86
- click .secho (
87
- "Warning: --serverless-rules is deprecated. Please use --extra-lint-rules=\" cfn_lint_serverless.rules\" instead." ,
88
- fg = "yellow"
89
- )
90
- # Convert old option to new option
91
- extra_lint_rules = "cfn_lint_serverless.rules"
92
-
93
- do_cli (ctx , template_file , lint , serverless_rules , extra_lint_rules ) # pragma: no cover
77
+ do_cli (ctx , template_file , lint , extra_lint_rules ) # pragma: no cover
94
78
95
79
96
- def do_cli (ctx , template , lint , serverless_rules , extra_lint_rules = None ):
80
+ def do_cli (ctx , template , lint , extra_lint_rules = None ):
97
81
"""
98
82
Implementation of the ``cli`` method, just separated out for unit testing purposes
99
83
"""
@@ -107,7 +91,7 @@ def do_cli(ctx, template, lint, serverless_rules, extra_lint_rules=None):
107
91
sam_template = _read_sam_file (template )
108
92
109
93
if lint :
110
- _lint (ctx , sam_template .serialized , template , serverless_rules , extra_lint_rules )
94
+ _lint (ctx , sam_template .serialized , template , extra_lint_rules )
111
95
else :
112
96
iam_client = boto3 .client ("iam" )
113
97
validator = SamTemplateValidator (
@@ -159,137 +143,190 @@ def _read_sam_file(template) -> SamTemplate:
159
143
return SamTemplate (serialized = template_string , deserialized = sam_template )
160
144
161
145
162
- def _lint (ctx : Context , template : str , template_path : str , serverless_rules : bool = False , extra_lint_rules : str = None ) -> None :
146
+ def _lint (ctx : Context , template : str , template_path : str , extra_lint_rules = None ):
163
147
"""
164
148
Parses provided SAM template and maps errors from CloudFormation template back to SAM template.
165
149
166
150
Cfn-lint loggers are added to the SAM cli logging hierarchy which at the root logger
167
- configures with INFO level logging and a different formatting. This exposes and duplicates
168
- some cfn-lint logs that are not typically shown to customers. Explicitly setting the level to
169
- WARNING and propagate to be False remediates these issues.
151
+ formatter and handlers are defined. This ensures that logging is output correctly when used from SAM cli
152
+ for CLI consumers.
170
153
171
154
Parameters
172
- -----------
155
+ ----------
173
156
ctx
174
- Click context object
157
+ Click Context
175
158
template
176
- Contents of sam template as a string
159
+ SAM template contents
177
160
template_path
178
161
Path to the sam template
179
- serverless_rules
180
- Flag to enable Serverless Rules for linting
162
+ extra_lint_rules
163
+ List of additional rule modules to apply
181
164
"""
165
+ import logging
166
+ import importlib .util
167
+ import cfnlint
182
168
183
- from cfnlint .api import ManualArgs , lint
169
+ from cfnlint .api import lint , ManualArgs
184
170
from cfnlint .runner import InvalidRegionException
171
+ # Import only what is necessary
172
+
173
+ # To track events, we need to enable telemetry
185
174
from samcli .lib .telemetry .event import EventTracker
186
175
187
- # Add debug information
188
- print ( f"Debug info: serverless_rules option value = { serverless_rules } " )
176
+ LOG = logging . getLogger ( __name__ )
177
+ LOG . debug ( "Starting template validation with linting " )
189
178
190
- cfn_lint_logger = logging .getLogger (CNT_LINT_LOGGER_NAME )
191
- cfn_lint_logger .propagate = False
179
+ # Set up cfnlint logger verbosity using context provided
180
+ cfnlint_logger = logging .getLogger (CNT_LINT_LOGGER_NAME )
181
+ cfnlint_logger .propagate = False
182
+
183
+ if ctx and ctx .debug :
184
+ cfnlint_logger .propagate = True
185
+ cfnlint_logger .setLevel (logging .DEBUG )
186
+ else :
187
+ cfnlint_logger .setLevel (logging .INFO )
192
188
189
+ # Track linting in telemetry
193
190
EventTracker .track_event ("UsedFeature" , "CFNLint" )
194
-
191
+
192
+ # Create linter configuration
195
193
linter_config = {}
196
194
if ctx .region :
197
195
linter_config ["regions" ] = [ctx .region ]
198
- if ctx .debug :
199
- cfn_lint_logger .propagate = True
200
- cfn_lint_logger .setLevel (logging .DEBUG )
201
-
202
- print (f"Debug info: initial linter_config = { linter_config } " )
203
-
204
- # Initialize variable to handle both options together
205
- rules_to_append = []
206
196
207
- # Support for previous serverless_rules option (deprecated)
208
- if serverless_rules :
209
- print ("Debug info: serverless_rules option is activated." )
210
- # Track usage of Serverless Rules
211
- EventTracker .track_event ("UsedFeature" , "ServerlessRules" )
212
-
213
- # Check if cfn-lint-serverless is installed
214
- import importlib .util
215
- serverless_spec = importlib .util .find_spec ("cfn_lint_serverless" )
216
- print (f"Debug info: cfn_lint_serverless package installed = { serverless_spec is not None } " )
217
-
218
- if serverless_spec is None :
219
- print ("Debug info: cfn_lint_serverless package is not installed." )
220
- click .secho (
221
- "Serverless Rules package (cfn-lint-serverless) is not installed. "
222
- "Please install it using: pip install cfn-lint-serverless" ,
223
- fg = "red" ,
224
- )
225
- raise UserException (
226
- "Serverless Rules package (cfn-lint-serverless) is not installed. "
227
- "Please install it using: pip install cfn-lint-serverless"
228
- )
229
-
230
- try :
231
- # Try to import the package
232
- import cfn_lint_serverless
233
- print ("Debug info: cfn_lint_serverless package import successful" )
234
-
235
- # Add Serverless Rules to the rule list
236
- rules_to_append .append ("cfn_lint_serverless.rules" )
237
- click .secho ("Serverless Rules enabled for linting" , fg = "green" )
238
- except ImportError as e :
239
- print (f"Debug info: cfn_lint_serverless import error = { e } " )
240
- click .secho (
241
- "Serverless Rules package (cfn-lint-serverless) is not installed. "
242
- "Please install it using: pip install cfn-lint-serverless" ,
243
- fg = "red" ,
244
- )
245
- raise UserException (
246
- "Serverless Rules package (cfn-lint-serverless) is not installed. "
247
- "Please install it using: pip install cfn-lint-serverless"
248
- )
249
-
250
- # Support for the new extra_lint_rules option
197
+ # Process extra lint rules if provided
198
+ rules_to_append = []
251
199
if extra_lint_rules :
252
- print (f"Debug info: extra_lint_rules option is activated. Value: { extra_lint_rules } " )
253
200
# Track usage of Extra Lint Rules
254
201
EventTracker .track_event ("UsedFeature" , "ExtraLintRules" )
255
202
256
- # Parse comma-separated rule modules
257
- modules = [module .strip () for module in extra_lint_rules .split (',' ) if module .strip ()]
258
- print (f"Debug info: parsed rule modules list = { modules } " )
203
+ # Process each rule option (multiple=True gives us a list)
204
+ for rule_option in extra_lint_rules :
205
+ # Handle comma-separated rule modules
206
+ for module in rule_option .split (',' ):
207
+ module = module .strip ()
208
+ if not module :
209
+ continue
210
+
211
+ LOG .debug ("Processing lint rule module: %s" , module )
212
+ if _is_module_available (module ):
213
+ rules_to_append .append (module )
214
+ LOG .debug ("Module %s is available and will be used" , module )
215
+ else :
216
+ module_name = module .split ('.' )[0 ].replace ('_' , '-' )
217
+ _handle_missing_module (module_name ,
218
+ f"The rule module '{ module } ' was specified but is not available." ,
219
+ ctx .debug )
259
220
260
- # Add each module to the rule list
261
- rules_to_append .extend (modules )
262
- click .secho (f"Extra lint rules enabled: { extra_lint_rules } " , fg = "green" )
263
-
264
- # Add rules to linter_config if any exist
265
- if rules_to_append :
266
- print (f"Debug info: rules to append = { rules_to_append } " )
267
- linter_config ["append_rules" ] = rules_to_append
268
- print (f"Debug info: updated linter_config = { linter_config } " )
269
-
270
- config = ManualArgs (** linter_config )
271
- print (f"Debug info: config creation completed" )
221
+ if rules_to_append :
222
+ module_names = ', ' .join (rules_to_append )
223
+ click .secho (f"Extra lint rules enabled: { module_names } " , fg = "green" )
224
+ linter_config ["append_rules" ] = rules_to_append
225
+ LOG .debug ("Linter configuration updated with rules: %s" , rules_to_append )
272
226
273
227
try :
274
- print (f"Debug info: starting lint function call" )
228
+ # Create linter configuration and execute linting
229
+ config = ManualArgs (** linter_config )
230
+ LOG .debug ("Executing linting with configuration" )
275
231
matches = lint (template , config = config )
276
- print (f"Debug info: lint function call completed, matches = { matches } " )
232
+
233
+ if not matches :
234
+ click .secho ("{} is a valid SAM Template" .format (template_path ), fg = "green" )
235
+ return
236
+
237
+ # Display validation failures
238
+ click .secho (matches )
239
+ raise LinterRuleMatchedException ("Linting failed. At least one linting rule was matched to the provided template." )
240
+
277
241
except InvalidRegionException as ex :
278
- print ( f"Debug info: InvalidRegionException occurred = { ex } " )
242
+ LOG . debug ( "Region validation failed: %s" , ex )
279
243
raise UserException (
280
244
f"AWS Region was not found. Please configure your region through the --region option.\n { ex } " ,
281
245
wrapped_from = ex .__class__ .__name__ ,
282
246
) from ex
283
247
except Exception as e :
284
- print ( f"Debug info: exception occurred = { e } " )
248
+ LOG . debug ( "Unexpected exception during linting: %s" , e )
285
249
raise
286
250
287
- if not matches :
288
- print (f"Debug info: template validation successful" )
289
- click .secho ("{} is a valid SAM Template" .format (template_path ), fg = "green" )
290
- return
291
251
292
- print (f"Debug info: template validation failed, matches = { matches } " )
293
- click .secho (matches )
252
+ def _is_module_available (module_path : str ) -> bool :
253
+ """
254
+ Check if a module is available for import.
255
+ Works with both standard pip installations and installer-based SAM CLI.
256
+
257
+ Parameters
258
+ ----------
259
+ module_path
260
+ Full module path (e.g. 'cfn_lint_serverless.rules')
261
+
262
+ Returns
263
+ -------
264
+ bool
265
+ True if module can be imported, False otherwise
266
+ """
267
+ LOG = logging .getLogger (__name__ )
268
+
269
+ # Try using importlib.util which is safer
270
+ try :
271
+ root_module = module_path .split ('.' )[0 ]
272
+ spec = importlib .util .find_spec (root_module )
273
+ if spec is None :
274
+ LOG .debug ("Module %s not found with importlib.util.find_spec" , root_module )
275
+ return False
276
+
277
+ # For deeper paths, try actually importing
278
+ try :
279
+ __import__ (module_path )
280
+ return True
281
+ except (ImportError , ModuleNotFoundError ) as e :
282
+ LOG .debug ("Could not import module %s: %s" , module_path , e )
283
+ return False
284
+ except Exception as e :
285
+ LOG .debug ("Unexpected error checking for module %s: %s" , module_path , e )
286
+ # Fallback to direct import attempt
287
+ try :
288
+ __import__ (module_path )
289
+ return True
290
+ except (ImportError , ModuleNotFoundError ):
291
+ return False
292
+
294
293
295
- raise LinterRuleMatchedException ("Linting failed. At least one linting rule was matched to the provided template." )
294
+ def _handle_missing_module (package_name : str , error_context : str , debug_mode : bool = False ):
295
+ """
296
+ Handle missing module by providing appropriate error message that works
297
+ in both pip and installer environments.
298
+
299
+ Parameters
300
+ ----------
301
+ package_name
302
+ Name of the package (for pip install instructions)
303
+ error_context
304
+ Contextual message describing what feature requires this package
305
+ debug_mode
306
+ Whether to include detailed instructions for different install methods
307
+
308
+ Raises
309
+ ------
310
+ UserException
311
+ With appropriate error message
312
+ """
313
+ LOG = logging .getLogger (__name__ )
314
+ LOG .debug ("Module %s is missing: %s" , package_name , error_context )
315
+
316
+ base_message = error_context
317
+ install_instruction = f"Please install it using: pip install { package_name } "
318
+
319
+ if debug_mode :
320
+ # In debug mode, provide more comprehensive instructions
321
+ message = (
322
+ f"{ base_message } \n \n "
323
+ f"The package '{ package_name } ' is not available. Installation options:\n "
324
+ f"1. If using pip-installed SAM CLI: { install_instruction } \n "
325
+ f"2. If using installer-based SAM CLI: You need to install the package in the same Python environment\n "
326
+ f" that SAM CLI uses. Check the SAM CLI installation documentation for details."
327
+ )
328
+ else :
329
+ message = f"{ base_message } \n \n { package_name } package is not installed. { install_instruction } "
330
+
331
+ click .secho (message , fg = "red" )
332
+ raise UserException (message )
0 commit comments