Skip to content

feat: add --serverless-rules to sam validate --lint #7950

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
55 changes: 55 additions & 0 deletions docs/extra-lint-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# SAM CLI Extra Lint Rules Usage Guide

The AWS SAM CLI's `validate` command uses [cfn-lint](https://github.com/aws-cloudformation/cfn-lint) for template validation.
SAM CLI now supports additional lint rules through the `--extra-lint-rules` option.

## Usage

```bash
sam validate --lint --extra-lint-rules="cfn_lint_serverless.rules"
```

## Considerations when Installing SAM CLI with the Installer

When SAM CLI is installed using the installer, it uses its own Python environment. In this case, additional rule modules must be installed in that environment. There are two approaches:

1. **Install packages in the installer's Python environment**: Install the required packages in the installer's Python environment.
2. **Specify the full path to the module**: Specify the full path to the package installed in the user's environment.

## Usage Examples

### Using Serverless Rules (cfn-lint-serverless)

```bash
# First, install the package
pip install cfn-lint-serverless

# Run SAM template validation
sam validate --lint --extra-lint-rules="cfn_lint_serverless.rules"
```

### Using Multiple Rule Modules

#### Method 1: Specify Multiple Modules Separated by Commas

You can specify multiple rule modules separated by commas in a single option:

```bash
sam validate --lint --extra-lint-rules="module1.rules,module2.rules,module3.rules"
```

Each module is automatically separated and passed to cfn-lint.

#### Method 2: Use the Option Multiple Times

You can also specify multiple rule modules by using the `--extra-lint-rules` option multiple times:

```bash
sam validate --lint --extra-lint-rules="module1.rules" --extra-lint-rules="module2.rules"
```

## Notes

* The previously used `--serverless-rules` option is deprecated.
* It is recommended to use the new `--extra-lint-rules` option.
* If you installed SAM CLI using the installer and additional rules are not working, check if the package is installed in the installer's Python environment.
116 changes: 111 additions & 5 deletions samcli/commands/validate/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,43 @@ class SamTemplate:
"Create a cfnlintrc config file to specify additional parameters. "
"For more information, see: https://github.com/aws-cloudformation/cfn-lint",
)
@click.option(
"--serverless-rules",
is_flag=True,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any particular region to enable this flag by default?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vicheey No, there's no particular region requirement. You can enable this flag by default in all regions.

help="[DEPRECATED] Enable Serverless Rules for linting validation. "
"Requires the cfn-lint-serverless package to be installed. "
"Use --extra-lint-rules=\"cfn_lint_serverless.rules\" instead. "
"For more information, see: https://github.com/awslabs/serverless-rules",
)
Comment on lines +63 to +67
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this was never released in any official version of SAM CLI, so it doesn't make sense to add it as "DEPRECATED", since it never really existed.

@click.option(
"--extra-lint-rules",
help="Specify additional lint rules to be used with cfn-lint. "
"Format: module.path (e.g. 'cfn_lint_serverless.rules')",
default=None
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need to pass multiple=True to allow the parameter to be passed multiple times (https://click.palletsprojects.com/en/stable/options/#multiple-options and it will be stored as a list).

You mentioned in the documentation that that is actually supported, did you test passing it multiple times? (I imagine the last one will just replace any other one if we don't add the multiple=True)

@save_params_option
@pass_context
@track_command
@check_newer_version
@print_cmdline_args
@unsupported_command_cdk(alternative_command="cdk doctor")
@command_exception_handler
def cli(ctx, template_file, config_file, config_env, lint, save_params):
def cli(ctx, template_file, config_file, config_env, lint, save_params, serverless_rules, extra_lint_rules):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, remove all references to --serverless-rules, since it doesn't really exist and it never existed officially. (Not only in this section but further down below too)

# All logic must be implemented in the ``do_cli`` method. This helps with easy unit testing

do_cli(ctx, template_file, lint) # pragma: no cover
# Show warning and convert to extra_lint_rules if serverless_rules is used
if serverless_rules and not extra_lint_rules:
click.secho(
"Warning: --serverless-rules is deprecated. Please use --extra-lint-rules=\"cfn_lint_serverless.rules\" instead.",
fg="yellow"
)
# Convert old option to new option
extra_lint_rules = "cfn_lint_serverless.rules"

do_cli(ctx, template_file, lint, serverless_rules, extra_lint_rules) # pragma: no cover

def do_cli(ctx, template, lint):

def do_cli(ctx, template, lint, serverless_rules, extra_lint_rules=None):
"""
Implementation of the ``cli`` method, just separated out for unit testing purposes
"""
Expand All @@ -84,7 +107,7 @@ def do_cli(ctx, template, lint):
sam_template = _read_sam_file(template)

if lint:
_lint(ctx, sam_template.serialized, template)
_lint(ctx, sam_template.serialized, template, serverless_rules, extra_lint_rules)
else:
iam_client = boto3.client("iam")
validator = SamTemplateValidator(
Expand Down Expand Up @@ -136,7 +159,7 @@ def _read_sam_file(template) -> SamTemplate:
return SamTemplate(serialized=template_string, deserialized=sam_template)


def _lint(ctx: Context, template: str, template_path: str) -> None:
def _lint(ctx: Context, template: str, template_path: str, serverless_rules: bool = False, extra_lint_rules: str = None) -> None:
"""
Parses provided SAM template and maps errors from CloudFormation template back to SAM template.

Expand All @@ -153,10 +176,16 @@ def _lint(ctx: Context, template: str, template_path: str) -> None:
Contents of sam template as a string
template_path
Path to the sam template
serverless_rules
Flag to enable Serverless Rules for linting
"""

from cfnlint.api import ManualArgs, lint
from cfnlint.runner import InvalidRegionException
from samcli.lib.telemetry.event import EventTracker

# Add debug information
print(f"Debug info: serverless_rules option value = {serverless_rules}")

cfn_lint_logger = logging.getLogger(CNT_LINT_LOGGER_NAME)
cfn_lint_logger.propagate = False
Expand All @@ -170,20 +199,97 @@ def _lint(ctx: Context, template: str, template_path: str) -> None:
cfn_lint_logger.propagate = True
cfn_lint_logger.setLevel(logging.DEBUG)

print(f"Debug info: initial linter_config = {linter_config}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use print for debugging strings, use the logging module instead.

import logging

LOG = logging.getLogger(__name__)

...

LOG.debug("Debug info: initial linter_config %s", linter_config)

Basic usage on this other file:

import logging
import os
LOG = logging.getLogger(__name__)

LOG.debug("Resolving code path. Cwd=%s, CodeUri=%s", cwd, codeuri)

Also, prefer using a message with the string format only, and the values as parameters of debug (like "Debug info: initial linter_config %s", linter_config) instead of formatting in the string directly (f"Debug info: initial linter_config = {linter_config}"). When f" {parameter}" is used for logging, the values are formatted even when logging is off, just adding extra processing that's not needed. (full details about how to use it in here: https://docs.python.org/3/library/logging.html#logging.Logger.debug)


# Initialize variable to handle both options together
rules_to_append = []

# Support for previous serverless_rules option (deprecated)
if serverless_rules:
Comment on lines +207 to +208
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to emphasize what I mentioned before, don't add any of the serverless_rules code.

print("Debug info: serverless_rules option is activated.")
# Track usage of Serverless Rules
EventTracker.track_event("UsedFeature", "ServerlessRules")

# Check if cfn-lint-serverless is installed
import importlib.util
serverless_spec = importlib.util.find_spec("cfn_lint_serverless")
print(f"Debug info: cfn_lint_serverless package installed = {serverless_spec is not None}")

if serverless_spec is None:
print("Debug info: cfn_lint_serverless package is not installed.")
click.secho(
"Serverless Rules package (cfn-lint-serverless) is not installed. "
"Please install it using: pip install cfn-lint-serverless",
fg="red",
)
raise UserException(
"Serverless Rules package (cfn-lint-serverless) is not installed. "
"Please install it using: pip install cfn-lint-serverless"
)

try:
# Try to import the package
import cfn_lint_serverless
print("Debug info: cfn_lint_serverless package import successful")

# Add Serverless Rules to the rule list
rules_to_append.append("cfn_lint_serverless.rules")
click.secho("Serverless Rules enabled for linting", fg="green")
except ImportError as e:
print(f"Debug info: cfn_lint_serverless import error = {e}")
click.secho(
"Serverless Rules package (cfn-lint-serverless) is not installed. "
"Please install it using: pip install cfn-lint-serverless",
fg="red",
)
raise UserException(
"Serverless Rules package (cfn-lint-serverless) is not installed. "
"Please install it using: pip install cfn-lint-serverless"
)

# Support for the new extra_lint_rules option
if extra_lint_rules:
print(f"Debug info: extra_lint_rules option is activated. Value: {extra_lint_rules}")
# Track usage of Extra Lint Rules
EventTracker.track_event("UsedFeature", "ExtraLintRules")

# Parse comma-separated rule modules
modules = [module.strip() for module in extra_lint_rules.split(',') if module.strip()]
print(f"Debug info: parsed rule modules list = {modules}")

# Add each module to the rule list
rules_to_append.extend(modules)
click.secho(f"Extra lint rules enabled: {extra_lint_rules}", fg="green")

# Add rules to linter_config if any exist
if rules_to_append:
print(f"Debug info: rules to append = {rules_to_append}")
linter_config["append_rules"] = rules_to_append
print(f"Debug info: updated linter_config = {linter_config}")

config = ManualArgs(**linter_config)
print(f"Debug info: config creation completed")

try:
print(f"Debug info: starting lint function call")
matches = lint(template, config=config)
print(f"Debug info: lint function call completed, matches = {matches}")
except InvalidRegionException as ex:
print(f"Debug info: InvalidRegionException occurred = {ex}")
raise UserException(
f"AWS Region was not found. Please configure your region through the --region option.\n{ex}",
wrapped_from=ex.__class__.__name__,
) from ex
except Exception as e:
print(f"Debug info: exception occurred = {e}")
raise

if not matches:
print(f"Debug info: template validation successful")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need all these debugging messages though. Some make sense, but this part is already printing something else already.

click.secho("{} is a valid SAM Template".format(template_path), fg="green")
return

print(f"Debug info: template validation failed, matches = {matches}")
click.secho(matches)

raise LinterRuleMatchedException("Linting failed. At least one linting rule was matched to the provided template.")
12 changes: 11 additions & 1 deletion schema/samcli.json
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@
"properties": {
"parameters": {
"title": "Parameters for the validate command",
"description": "Available parameters for the validate command:\n* template_file:\nAWS SAM template file.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* lint:\nRun linting validation on template through cfn-lint. Create a cfnlintrc config file to specify additional parameters. For more information, see: https://github.com/aws-cloudformation/cfn-lint\n* save_params:\nSave the parameters provided via the command line to the configuration file.",
"description": "Available parameters for the validate command:\n* template_file:\nAWS SAM template file.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* lint:\nRun linting validation on template through cfn-lint. Create a cfnlintrc config file to specify additional parameters. For more information, see: https://github.com/aws-cloudformation/cfn-lint\n* serverless_rules:\n[DEPRECATED] Enable Serverless Rules for linting validation. Use --extra-lint-rules=\"cfn_lint_serverless.rules\" instead. Requires the cfn-lint-serverless package to be installed. For more information, see: https://github.com/awslabs/serverless-rules\n* extra_lint_rules:\nSpecify additional lint rules to be used with cfn-lint. Format: module.path (e.g. 'cfn_lint_serverless.rules')\n* save_params:\nSave the parameters provided via the command line to the configuration file.",
"type": "object",
"properties": {
"template_file": {
Expand Down Expand Up @@ -230,6 +230,16 @@
"type": "boolean",
"description": "Run linting validation on template through cfn-lint. Create a cfnlintrc config file to specify additional parameters. For more information, see: https://github.com/aws-cloudformation/cfn-lint"
},
"serverless_rules": {
"title": "serverless_rules",
"type": "boolean",
"description": "[DEPRECATED] Enable Serverless Rules for linting validation. Use --extra-lint-rules=\"cfn_lint_serverless.rules\" instead. Requires the cfn-lint-serverless package to be installed. For more information, see: https://github.com/awslabs/serverless-rules"
},
"extra_lint_rules": {
"title": "extra_lint_rules",
"type": "string",
"description": "Specify additional lint rules to be used with cfn-lint. Format: module.path (e.g. 'cfn_lint_serverless.rules')"
},
"save_params": {
"title": "save_params",
"type": "boolean",
Expand Down
Loading
Loading