Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c324fb4
Validate pattern directory structure
shatakshiiii Jun 18, 2025
af9ed4c
Merge branch 'main' into validate_pattern_dir
shatakshiiii Jun 18, 2025
71e2f99
Add creator as a dep
shatakshiiii Jun 18, 2025
833a9ac
Updates for schema validation
shatakshiiii Jun 19, 2025
6943f03
Revert "Updates for schema validation"
shatakshiiii Jun 24, 2025
05a51cd
Merge branch 'main' into validate_pattern_dir
shatakshiiii Jun 24, 2025
0cbe1fd
Add pattern rule
shatakshiiii Jun 24, 2025
2dde0cd
pre-commit fixes
shatakshiiii Jun 24, 2025
8800130
update rules number
shatakshiiii Jun 24, 2025
1e84250
Updates for pattern rule
shatakshiiii Jun 26, 2025
23a3792
Merge branch 'main' into validate_pattern_dir
shatakshiiii Jun 26, 2025
66719e3
Remove duplicate pattern entry from config file
shatakshiiii Jun 26, 2025
6fddb40
Changes for making pattern as file kind
shatakshiiii Jun 26, 2025
f720399
Fixes
ssbarnea Jun 26, 2025
d1979c6
Fixes in the pattern rule
shatakshiiii Jun 27, 2025
b1a73bc
Merge branch 'main' into validate_pattern_dir
shatakshiiii Jun 27, 2025
231c523
Improve the get_playbook_file function
shatakshiiii Jun 27, 2025
0941a8c
Validation for pattern name
shatakshiiii Jun 27, 2025
f86d646
clean up
shatakshiiii Jun 27, 2025
5ef4ee5
Use metadata as tag
shatakshiiii Jun 27, 2025
2f829ba
Update docs
shatakshiiii Jun 27, 2025
4164a3d
Improve pattern rule descriptions for better clarity
shatakshiiii Jul 1, 2025
0856d3d
Merge branch 'main' into validate_pattern_dir
shatakshiiii Jul 1, 2025
c6eb7ae
Add usage details in pattern.md
shatakshiiii Jul 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .config/constraints.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# This file was autogenerated by uv via the following command:
# tox run -e deps
ansible-compat==25.5.0 # via ansible-lint (pyproject.toml)
ansible-creator==25.6.0 # via ansible-lint (pyproject.toml)
astroid==3.3.10 # via pylint
asttokens==3.0.0 # via stack-data
attrs==25.3.0 # via jsonschema, referencing
Expand Down Expand Up @@ -47,7 +48,7 @@ ipdb==0.13.13 # via ansible-lint (pyproject.toml)
ipython==8.36.0 # via ipdb, ansible-lint (pyproject.toml)
isort==6.0.1 # via pylint
jedi==0.19.2 # via ipython
jinja2==3.1.6 # via ansible-core, mkdocs, mkdocs-macros-plugin, mkdocs-material, mkdocstrings
jinja2==3.1.6 # via ansible-core, ansible-creator, mkdocs, mkdocs-macros-plugin, mkdocs-material, mkdocstrings
jmespath==1.0.1 # via ansible-lint (pyproject.toml)
jsmin==3.0.1 # via mkdocs-minify-plugin
jsonschema==4.24.0 # via ansible-compat, ansible-lint (pyproject.toml)
Expand Down Expand Up @@ -102,7 +103,7 @@ pytest-plus==0.8.1 # via ansible-lint (pyproject.toml)
pytest-sugar==1.0.0 # via ansible-lint (pyproject.toml)
pytest-xdist==3.7.0 # via ansible-lint (pyproject.toml)
python-dateutil==2.9.0.post0 # via ghp-import, mkdocs-macros-plugin
pyyaml==6.0.2 # via ansible-compat, ansible-core, mkdocs, mkdocs-get-deps, mkdocs-macros-plugin, pymdown-extensions, pyyaml-env-tag, yamllint, ansible-lint (pyproject.toml)
pyyaml==6.0.2 # via ansible-compat, ansible-core, ansible-creator, mkdocs, mkdocs-get-deps, mkdocs-macros-plugin, pymdown-extensions, pyyaml-env-tag, yamllint, ansible-lint (pyproject.toml)
pyyaml-env-tag==1.1 # via mkdocs
referencing==0.36.2 # via jsonschema, jsonschema-specifications, types-jsonschema, ansible-lint (pyproject.toml)
requests==2.32.4 # via linkchecker, mkdocs-htmlproofer-plugin, mkdocs-material
Expand Down
1 change: 1 addition & 0 deletions .config/requirements-test.in
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
ansible-creator >=24.6.0
black # IDE support
coverage-enable-subprocess # see https://github.com/nedbat/coveragepy/issues/1341#issuecomment-1228942657
coverage[toml] >= 6.4.4
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"schema_version": "1.0",
"name": "weather_forecasting",
"title": "Weather Forecasting",
"description": "This pattern is designed to help get the weather forecast for a given airport code. It creates a project, EE, and job templates in automation controller to get the weather forecast.",
"short_description": "This pattern is designed to help get the weather forecast for a given airport code.",
"tags": ["weather", "forecasting"],
"aap_resources": {
"controller_project": {
"name": "Weather Forecasting",
"description": "Project for the Weather Forecasting pattern"
},
"controller_execution_environment": {
"name": "Weather Forecasting",
"description": "EE for the Weather Forecasting pattern",
"image_name": "weather-demo-ee",
"pull": "missing"
},
"controller_labels": ["weather", "forecasting"],
"controller_job_templates": [
{
"name": "Get Weather Forecast",
"description": "This job template gets the weather at the location of a provided airport code.",
"execution_environment": "Weather Forecasting",
"playbook": "site.yml",
"primary": true,
"labels": ["weather", "forecasting"],
"survey": {
"name": "Weather Forecasting",
"description": "Survey to configure the weather forecasting pattern",
"spec": [
{
"type": "text",
"question_name": "Location",
"question_description": "Enter the airport code for which you want to get the weather forecast",
"variable": "location",
"required": true
}
]
}
}
]
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"schema_version": "1.0",
"name": "weather_forecasting",
"title": "Weather Forecasting",
"description": "This pattern is designed to help get the weather forecast for a given airport code. It creates a project, EE, and job templates in automation controller to get the weather forecast.",
"short_description": "This pattern is designed to help get the weather forecast for a given airport code.",
"tags": ["weather", "forecasting"],
"aap_resources": {
"controller_project": {
"name": "Weather Forecasting",
"description": "Project for the Weather Forecasting pattern"
},
"controller_execution_environment": {
"name": "Weather Forecasting",
"description": "EE for the Weather Forecasting pattern",
"image_name": "weather-demo-ee",
"pull": "missing"
},
"controller_labels": ["weather", "forecasting"],
"controller_job_templates": [
{
"name": "Get Weather Forecast",
"description": "This job template gets the weather at the location of a provided airport code.",
"execution_environment": "Weather Forecasting",
"playbook": "site.yml",
"primary": true,
"labels": ["weather", "forecasting"],
"survey": {
"name": "Weather Forecasting",
"description": "Survey to configure the weather forecasting pattern",
"spec": [
{
"type": "text",
"question_name": "Location",
"question_description": "Enter the airport code for which you want to get the weather forecast",
"variable": "location",
"required": true
}
]
}
}
]
}
}
1 change: 1 addition & 0 deletions src/ansiblelint/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
{"yaml": "**/molecule/*/{base,molecule}.{yaml,yml}"}, # molecule config
{"requirements": "**/requirements.{yaml,yml}"}, # v2 and v1
{"playbook": "**/molecule/*/*.{yaml,yml}"}, # molecule playbooks
{"pattern": "**/patterns/*/meta/pattern.json"},
{"yaml": "**/{.ansible-lint,.yamllint}"},
{"changelog": "**/changelogs/changelog.{yaml,yml}"},
{"yaml": "**/*.{yaml,yml}"},
Expand Down
1 change: 1 addition & 0 deletions src/ansiblelint/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def main():
"sanity-ignore-file", # tests/sanity/ignore file
"plugin",
"galaxy", # galaxy.yml
"pattern", # pattern.json file
"", # unknown file type
]

Expand Down
3 changes: 3 additions & 0 deletions src/ansiblelint/rules/pattern.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# pattern

This rule aims to validate Ansible pattern directory structure.
121 changes: 121 additions & 0 deletions src/ansiblelint/rules/pattern.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""Implementation of PatternRule."""

from __future__ import annotations

import sys
from typing import TYPE_CHECKING, Any

from ansiblelint.rules import AnsibleLintRule

if TYPE_CHECKING:
from ansiblelint.errors import MatchError
from ansiblelint.file_utils import Lintable


class PatternRule(AnsibleLintRule):
"""Rule for checking pattern directory."""

id = "pattern"
description = "Confirm that pattern has valid directory structure."
severity = "MEDIUM"
tags = ["metadata"]
version_changed = "25.7.0"

def matchplay(self, file: Lintable, data: dict[str, Any]) -> list[MatchError]:
"""Return matches found for a specific play (entry in playbook)."""
if file.kind != "pattern":
return []

results = []

pattern_dir = file.path.parent.parent.resolve()
meta_dir = pattern_dir / "meta"

# Check if meta directory exists
if not meta_dir.is_dir():
results.append(
self.create_matcherror(
message=(
f"Pattern directory '{pattern_dir}' contains pattern.json but is missing the required 'meta' directory."
),
tag=self.id,
filename=file,
),
)
return results

# Define required files relative to the pattern dir
required_paths = [
pattern_dir / "README.md",
pattern_dir / "playbooks" / "site.yml",
]
missing = [
str(p.relative_to(pattern_dir)) for p in required_paths if not p.exists()
]

# Check execution_environments directory if it exists
ee_dir = pattern_dir / "execution_environments"
if ee_dir.exists():
expected_file = ee_dir / "execution_environment.yml"
# Must contain only execution_environment.yml
files = list(ee_dir.iterdir())
if not expected_file.exists():
missing.append("execution_environments/execution_environment.yml")
if len(files) != 1 or files[0].name != "execution_environment.yml":
results.append(
self.create_matcherror(
message=(
f"'execution_environments' directory in '{pattern_dir}' must contain only 'execution_environment.yml' file."
),
tag=self.id,
filename=file,
),
)

if missing:
results.append(
self.create_matcherror(
message=(
f"Pattern directory '{pattern_dir}' is missing required files: {', '.join(missing)}"
),
tag=self.id,
filename=file,
),
)

return results


if "pytest" in sys.modules:
import pytest

from ansiblelint.rules import RulesCollection # pylint: disable=ungrouped-imports
from ansiblelint.runner import Runner

@pytest.mark.parametrize(
("file", "expected"),
(
pytest.param(
"examples/collections/extensions/patterns/valid_pattern/meta/pattern.json",
["pattern"],
id="valid-pattern",
),
pytest.param(
"examples/collections/extensions/patterns/invalid_pattern/pattern.json",
["pattern"],
id="invalid-pattern",
),
),
)
def test_pattern(
default_rules_collection: RulesCollection,
file: str,
expected: list[str],
) -> None:
"""Validate that rule works as intended."""
results = Runner(file, rules=default_rules_collection).run()

assert len(results) == len(expected)
for index, result in enumerate(results):
assert result.rule.id == PatternRule.id, result
assert result.tag == expected[index]
53 changes: 53 additions & 0 deletions test/test_pattern.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Tests for the pattern feature."""

import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

LINT_BIN = Path(sys.executable).parent / "ansible-lint"


def test_creator_scaffolded_pattern() -> None:
"""Validate creator scaffolded pattern.

Scaffold a pattern using ansible-creator. Run ansible-lint
on the scaffolded pattern and validate lint results.

Args:
monkeypatch: Monkeypatch fixture.
"""
# Create a tmp dir and copy an existing collection into it
collection_src = Path("examples/collections/broken_no_runtime")
with tempfile.TemporaryDirectory() as tmpdir:
collection_dest = Path(tmpdir) / collection_src.name
shutil.copytree(collection_src, collection_dest)

# Scaffold a pattern using ansible-creator
result = subprocess.run(
[
"ansible-creator",
"add",
"resource",
"pattern",
"sample_pattern",
collection_dest,
],
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, f"Pattern scaffolding failed: {result.stderr}"

# Run ansible-lint on the scaffolded pattern
pattern_path = collection_dest

lint_result = subprocess.run(
[str(LINT_BIN), pattern_path],
capture_output=True,
text=True,
env={"NO_COLOR": "1"},
check=False,
)
assert lint_result.returncode == 0, f"ansible-lint failed: {lint_result.stderr}"
2 changes: 1 addition & 1 deletion test/test_rules_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,5 +173,5 @@ def test_rules_id_format(config_options: Options) -> None:
f"Rule {rule.id} must have at least one of: .help, .description, .__doc__"
)
assert "yaml" in keys, "yaml rule is missing"
assert len(rules) == 51 # update this number when adding new rules!
assert len(rules) == 52 # update this number when adding new rules!
assert len(keys) == len(rules), "Duplicate rule ids?"
Loading