24
24
import sys
25
25
from typing import TYPE_CHECKING , Any
26
26
27
+ from ansiblelint .errors import MatchError
27
28
from ansiblelint .rules import AnsibleLintRule
28
29
29
30
if TYPE_CHECKING :
@@ -34,168 +35,60 @@ class CommandHasChangesCheckRule(AnsibleLintRule):
34
35
"""Commands should not change things if nothing needs doing."""
35
36
36
37
id = "no-changed-when"
37
- description = """
38
- Tasks should tell Ansible when to return ``changed``, unless the task only reads
39
- information. To do this, set ``changed_when``, use the ``creates`` or
40
- ``removes`` argument, or use ``when`` to run the task only if another check has
41
- a particular result.
42
-
43
- For example, this task registers the ``shell`` output and uses the return code
44
- to define when the task has changed.
45
-
46
- ```yaml
47
- - name: Handle shell output with return code
48
- ansible.builtin.shell: cat {{ my_file|quote }}
49
- register: my_output
50
- changed_when: my_output.rc != 0
51
- ```
52
-
53
- The following example will trigger the rule since the task does not
54
- handle the output of the ``command``.
55
-
56
- ```yaml
57
- - name: Does not handle any output or return codes
58
- ansible.builtin.command: cat {{ my_file|quote }}
59
- ```
60
- """
61
38
severity = "HIGH"
62
39
tags = ["command-shell" , "idempotency" ]
63
40
version_added = "historic"
64
41
65
- _commands = ["command" , "shell" , "raw" ]
42
+ _commands = [
43
+ "ansible.builtin.command" ,
44
+ "ansible.builtin.shell" ,
45
+ "ansible.builtin.raw" ,
46
+ "ansible.legacy.command" ,
47
+ "ansible.legacy.shell" ,
48
+ "ansible.legacy.raw" ,
49
+ "command" ,
50
+ "shell" ,
51
+ "raw" ,
52
+ ]
66
53
67
54
def matchtask (
68
55
self , task : dict [str , Any ], file : Lintable | None = None
69
- ) -> bool | str :
56
+ ) -> list [MatchError ]:
57
+ result = []
70
58
# tasks in a block are "meta" type
71
59
if task ["__ansible_action_type__" ] in ["task" , "meta" ]:
72
- if task ["action" ]["__ansible_module__" ] in self ._commands :
73
- return (
74
- "changed_when" not in task
75
- and "when" not in task
76
- and "creates" not in task ["action" ]
77
- and "removes" not in task ["action" ]
78
- )
79
- return False
60
+ if task ["action" ]["__ansible_module__" ] in self ._commands and (
61
+ "changed_when" not in task
62
+ and "creates" not in task ["action" ]
63
+ and "removes" not in task ["action" ]
64
+ ):
65
+ result .append (self .create_matcherror (filename = file ))
66
+ return result
80
67
81
68
82
69
if "pytest" in sys .modules :
83
70
import pytest
84
71
85
- NO_CHANGE_COMMAND_RC = """
86
- - hosts: all
87
- tasks:
88
- - name: Handle command output with return code
89
- ansible.builtin.command: cat {{ my_file|quote }}
90
- register: my_output
91
- changed_when: my_output.rc != 0
92
- """
93
-
94
- NO_CHANGE_SHELL_RC = """
95
- - hosts: all
96
- tasks:
97
- - name: Handle shell output with return code
98
- ansible.builtin.shell: cat {{ my_file|quote }}
99
- register: my_output
100
- changed_when: my_output.rc != 0
101
- """
102
-
103
- NO_CHANGE_SHELL_FALSE = """
104
- - hosts: all
105
- tasks:
106
- - name: Handle shell output with false changed_when
107
- ansible.builtin.shell: cat {{ my_file|quote }}
108
- register: my_output
109
- changed_when: false
110
- """
111
-
112
- NO_CHANGE_ARGS = """
113
- - hosts: all
114
- tasks:
115
- - name: Command with argument
116
- command: createfile.sh
117
- args:
118
- creates: /tmp/????unknown_files????
119
- """
120
-
121
- NO_CHANGE_REGISTER_FAIL = """
122
- - hosts: all
123
- tasks:
124
- - name: Register command output, but cat still does not change anything
125
- ansible.builtin.command: cat {{ my_file|quote }}
126
- register: my_output
127
- """
128
-
129
- # also test to ensure it catches tasks in nested blocks.
130
- NO_CHANGE_COMMAND_FAIL = """
131
- - hosts: all
132
- tasks:
133
- - block:
134
- - block:
135
- - name: Basic command task, should fail
136
- ansible.builtin.command: cat my_file
137
- """
138
-
139
- NO_CHANGE_SHELL_FAIL = """
140
- - hosts: all
141
- tasks:
142
- - name: Basic shell task, should fail
143
- shell: cat my_file
144
- """
145
-
146
- @pytest .mark .parametrize (
147
- "rule_runner" , (CommandHasChangesCheckRule ,), indirect = ["rule_runner" ]
148
- )
149
- def test_no_change_command_rc (rule_runner : Any ) -> None :
150
- """This should pass since ``*_when`` is used."""
151
- results = rule_runner .run_playbook (NO_CHANGE_COMMAND_RC )
152
- assert len (results ) == 0
153
-
154
- @pytest .mark .parametrize (
155
- "rule_runner" , (CommandHasChangesCheckRule ,), indirect = ["rule_runner" ]
156
- )
157
- def test_no_change_shell_rc (rule_runner : Any ) -> None :
158
- """This should pass since ``*_when`` is used."""
159
- results = rule_runner .run_playbook (NO_CHANGE_SHELL_RC )
160
- assert len (results ) == 0
161
-
162
- @pytest .mark .parametrize (
163
- "rule_runner" , (CommandHasChangesCheckRule ,), indirect = ["rule_runner" ]
164
- )
165
- def test_no_change_shell_false (rule_runner : Any ) -> None :
166
- """This should pass since ``*_when`` is used."""
167
- results = rule_runner .run_playbook (NO_CHANGE_SHELL_FALSE )
168
- assert len (results ) == 0
169
-
170
- @pytest .mark .parametrize (
171
- "rule_runner" , (CommandHasChangesCheckRule ,), indirect = ["rule_runner" ]
172
- )
173
- def test_no_change_args (rule_runner : Any ) -> None :
174
- """This test should not pass since the command doesn't do anything."""
175
- results = rule_runner .run_playbook (NO_CHANGE_ARGS )
176
- assert len (results ) == 0
177
-
178
- @pytest .mark .parametrize (
179
- "rule_runner" , (CommandHasChangesCheckRule ,), indirect = ["rule_runner" ]
180
- )
181
- def test_no_change_register_fail (rule_runner : Any ) -> None :
182
- """This test should not pass since cat still doesn't do anything."""
183
- results = rule_runner .run_playbook (NO_CHANGE_REGISTER_FAIL )
184
- assert len (results ) == 1
185
-
186
- @pytest .mark .parametrize (
187
- "rule_runner" , (CommandHasChangesCheckRule ,), indirect = ["rule_runner" ]
188
- )
189
- def test_no_change_command_fail (rule_runner : Any ) -> None :
190
- """This test should fail because command isn't handled."""
191
- # this also ensures that this catches tasks in nested blocks
192
- results = rule_runner .run_playbook (NO_CHANGE_COMMAND_FAIL )
193
- assert len (results ) == 1
72
+ from ansiblelint .rules import RulesCollection # pylint: disable=ungrouped-imports
73
+ from ansiblelint .runner import Runner # pylint: disable=ungrouped-imports
194
74
195
75
@pytest .mark .parametrize (
196
- "rule_runner" , (CommandHasChangesCheckRule ,), indirect = ["rule_runner" ]
76
+ ("file" , "expected" ),
77
+ (
78
+ pytest .param (
79
+ "examples/playbooks/rule-no-changed-when-pass.yml" , 0 , id = "pass"
80
+ ),
81
+ pytest .param (
82
+ "examples/playbooks/rule-no-changed-when-fail.yml" , 3 , id = "fail"
83
+ ),
84
+ ),
197
85
)
198
- def test_no_change_shell_fail (rule_runner : Any ) -> None :
199
- """This test should fail because shell isn't handled.."""
200
- results = rule_runner .run_playbook (NO_CHANGE_SHELL_FAIL )
201
- assert len (results ) == 1
86
+ def test_rule_no_changed_when (
87
+ default_rules_collection : RulesCollection , file : str , expected : int
88
+ ) -> None :
89
+ """Validate no-changed-when rule."""
90
+ results = Runner (file , rules = default_rules_collection ).run ()
91
+
92
+ for result in results :
93
+ assert result .rule .id == CommandHasChangesCheckRule .id , result
94
+ assert len (results ) == expected
0 commit comments