spaceforge 1.1.4__tar.gz → 1.1.5__tar.gz
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.
- {spaceforge-1.1.4 → spaceforge-1.1.5}/PKG-INFO +1 -6
- spaceforge-1.1.5/spaceforge/_version.py +39 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/_version_scm.py +3 -3
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge.egg-info/PKG-INFO +1 -6
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge.egg-info/SOURCES.txt +0 -4
- spaceforge-1.1.4/plugins/tflint/plugin.py +0 -195
- spaceforge-1.1.4/plugins/tflint/plugin.yaml +0 -363
- spaceforge-1.1.4/setup.py +0 -66
- spaceforge-1.1.4/spaceforge/_version.py +0 -80
- spaceforge-1.1.4/spaceforge.egg-info/not-zip-safe +0 -1
- {spaceforge-1.1.4 → spaceforge-1.1.5}/.github/workflows/ci.yml +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/.github/workflows/release.yml +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/.gitignore +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/LICENSE +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/MANIFEST.in +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/README.md +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/go.mod +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/plugins/enviroment_manager/plugin.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/plugins/enviroment_manager/plugin.yaml +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/plugins/enviroment_manager/requirements.txt +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/plugins/envsubst/plugin.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/plugins/envsubst/plugin.yaml +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/plugins/infracost/plugin.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/plugins/infracost/plugin.yaml +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/plugins/sops/plugin.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/plugins/sops/plugin.yaml +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/plugins/sops/requirements.txt +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/plugins/wiz/plugin.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/plugins/wiz/plugin.yaml +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/pyproject.toml +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/setup.cfg +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/README.md +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/__init__.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/__main__.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/cls.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/conftest.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/generator.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/plugin.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/runner.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/schema.json +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/templates/binary_install.sh.j2 +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/templates/ensure_spaceforge_and_run.sh.j2 +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/test_cls.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/test_generator.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/test_generator_binaries.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/test_generator_core.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/test_generator_hooks.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/test_generator_parameters.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/test_plugin.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/test_plugin_file_operations.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/test_plugin_hooks.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/test_plugin_inheritance.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/test_runner.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/test_runner_cli.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/test_runner_core.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge/test_runner_execution.py +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge.egg-info/dependency_links.txt +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge.egg-info/entry_points.txt +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge.egg-info/requires.txt +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/spaceforge.egg-info/top_level.txt +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/templates.go +0 -0
- {spaceforge-1.1.4 → spaceforge-1.1.5}/test.sh +0 -0
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spaceforge
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.5
|
|
4
4
|
Summary: A Python framework for building Spacelift plugins
|
|
5
|
-
Home-page: https://github.com/spacelift-io/plugins
|
|
6
|
-
Author: Spacelift
|
|
7
5
|
Author-email: Spacelift <support@spacelift.io>
|
|
8
6
|
Maintainer-email: Spacelift <support@spacelift.io>
|
|
9
7
|
License: MIT
|
|
@@ -39,10 +37,7 @@ Requires-Dist: mypy; extra == "dev"
|
|
|
39
37
|
Requires-Dist: types-PyYAML; extra == "dev"
|
|
40
38
|
Requires-Dist: setuptools-scm[toml]>=6.2; extra == "dev"
|
|
41
39
|
Requires-Dist: autoflake; extra == "dev"
|
|
42
|
-
Dynamic: author
|
|
43
|
-
Dynamic: home-page
|
|
44
40
|
Dynamic: license-file
|
|
45
|
-
Dynamic: requires-python
|
|
46
41
|
|
|
47
42
|
# Spaceforge - Build Spacelift Plugins in Python
|
|
48
43
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Version detection using setuptools_scm.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_version() -> str:
|
|
7
|
+
"""
|
|
8
|
+
Get the package version.
|
|
9
|
+
|
|
10
|
+
Uses setuptools_scm generated version file, which is created during build.
|
|
11
|
+
Falls back to a development version if not available.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
Version string
|
|
15
|
+
"""
|
|
16
|
+
# Try setuptools-scm generated version file (created during build)
|
|
17
|
+
try:
|
|
18
|
+
from ._version_scm import version # type: ignore[import-not-found]
|
|
19
|
+
|
|
20
|
+
return str(version)
|
|
21
|
+
except ImportError:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
# Try setuptools-scm directly (works in development)
|
|
25
|
+
try:
|
|
26
|
+
from setuptools_scm import (
|
|
27
|
+
get_version as scm_get_version, # type: ignore[import-untyped]
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
result = scm_get_version(root="..", relative_to=__file__)
|
|
31
|
+
return str(result)
|
|
32
|
+
except ImportError:
|
|
33
|
+
pass
|
|
34
|
+
except Exception:
|
|
35
|
+
# setuptools_scm might fail in various ways, ignore
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
# Fall back to default version for development
|
|
39
|
+
return "0.1.0-dev"
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '1.1.
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 1,
|
|
31
|
+
__version__ = version = '1.1.5'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 1, 5)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'gf45d03256'
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spaceforge
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.5
|
|
4
4
|
Summary: A Python framework for building Spacelift plugins
|
|
5
|
-
Home-page: https://github.com/spacelift-io/plugins
|
|
6
|
-
Author: Spacelift
|
|
7
5
|
Author-email: Spacelift <support@spacelift.io>
|
|
8
6
|
Maintainer-email: Spacelift <support@spacelift.io>
|
|
9
7
|
License: MIT
|
|
@@ -39,10 +37,7 @@ Requires-Dist: mypy; extra == "dev"
|
|
|
39
37
|
Requires-Dist: types-PyYAML; extra == "dev"
|
|
40
38
|
Requires-Dist: setuptools-scm[toml]>=6.2; extra == "dev"
|
|
41
39
|
Requires-Dist: autoflake; extra == "dev"
|
|
42
|
-
Dynamic: author
|
|
43
|
-
Dynamic: home-page
|
|
44
40
|
Dynamic: license-file
|
|
45
|
-
Dynamic: requires-python
|
|
46
41
|
|
|
47
42
|
# Spaceforge - Build Spacelift Plugins in Python
|
|
48
43
|
|
|
@@ -4,7 +4,6 @@ MANIFEST.in
|
|
|
4
4
|
README.md
|
|
5
5
|
go.mod
|
|
6
6
|
pyproject.toml
|
|
7
|
-
setup.py
|
|
8
7
|
templates.go
|
|
9
8
|
test.sh
|
|
10
9
|
.github/workflows/ci.yml
|
|
@@ -19,8 +18,6 @@ plugins/infracost/plugin.yaml
|
|
|
19
18
|
plugins/sops/plugin.py
|
|
20
19
|
plugins/sops/plugin.yaml
|
|
21
20
|
plugins/sops/requirements.txt
|
|
22
|
-
plugins/tflint/plugin.py
|
|
23
|
-
plugins/tflint/plugin.yaml
|
|
24
21
|
plugins/wiz/plugin.py
|
|
25
22
|
plugins/wiz/plugin.yaml
|
|
26
23
|
spaceforge/README.md
|
|
@@ -52,7 +49,6 @@ spaceforge.egg-info/PKG-INFO
|
|
|
52
49
|
spaceforge.egg-info/SOURCES.txt
|
|
53
50
|
spaceforge.egg-info/dependency_links.txt
|
|
54
51
|
spaceforge.egg-info/entry_points.txt
|
|
55
|
-
spaceforge.egg-info/not-zip-safe
|
|
56
52
|
spaceforge.egg-info/requires.txt
|
|
57
53
|
spaceforge.egg-info/top_level.txt
|
|
58
54
|
spaceforge/templates/binary_install.sh.j2
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import os
|
|
3
|
-
|
|
4
|
-
from spaceforge import Binary, Context, Parameter, Policy, SpaceforgePlugin, Variable
|
|
5
|
-
|
|
6
|
-
DEFAULT_TFLINT_CONFIG_FILE = ""
|
|
7
|
-
DEFAULT_TFLINT_RECURSIVE = "true"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class TFLintPlugin(SpaceforgePlugin):
|
|
11
|
-
"""
|
|
12
|
-
# Plugin TFLint
|
|
13
|
-
|
|
14
|
-
The TFLint plugin analyzes your Terraform/OpenTofu files and generates a report with findings categorized by severity.
|
|
15
|
-
|
|
16
|
-
You can also access the data from a plan policy via the `input.third_party_metadata.custom.tflint` object.
|
|
17
|
-
An example Plan policy is included with the plugin.
|
|
18
|
-
|
|
19
|
-
## Usage
|
|
20
|
-
|
|
21
|
-
1. Install the plugin
|
|
22
|
-
2. Add the `autoattach` label to any stack that uses Terraform/OpenTofu.
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
__author__ = "Spacelift"
|
|
26
|
-
__labels__ = ["qa", "security"]
|
|
27
|
-
__plugin_name__ = "tflint"
|
|
28
|
-
__version__ = "0.1.0"
|
|
29
|
-
|
|
30
|
-
__binaries__ = [
|
|
31
|
-
Binary(
|
|
32
|
-
name="tflint",
|
|
33
|
-
download_urls={
|
|
34
|
-
"amd64": "https://github.com/terraform-linters/tflint/releases/download/v0.59.1/tflint_linux_amd64.zip",
|
|
35
|
-
"arm64": "https://github.com/terraform-linters/tflint/releases/download/v0.59.1/tflint_linux_arm64.zip",
|
|
36
|
-
},
|
|
37
|
-
),
|
|
38
|
-
]
|
|
39
|
-
|
|
40
|
-
__parameters__ = [
|
|
41
|
-
Parameter(
|
|
42
|
-
name="Configuration file",
|
|
43
|
-
id="tflint_config_file",
|
|
44
|
-
description="Configuration file name",
|
|
45
|
-
default=DEFAULT_TFLINT_CONFIG_FILE,
|
|
46
|
-
),
|
|
47
|
-
Parameter(
|
|
48
|
-
name="Recursive",
|
|
49
|
-
id="tflint_recursive",
|
|
50
|
-
description="Run command in each directory recursively. Allowed values: true, false",
|
|
51
|
-
default=DEFAULT_TFLINT_RECURSIVE,
|
|
52
|
-
),
|
|
53
|
-
]
|
|
54
|
-
|
|
55
|
-
__contexts__ = [
|
|
56
|
-
Context(
|
|
57
|
-
name_prefix="tflint",
|
|
58
|
-
description="TFLint Plugin",
|
|
59
|
-
env=[
|
|
60
|
-
Variable(
|
|
61
|
-
key="TFLINT_CONFIG_FILE",
|
|
62
|
-
value_from_parameter="tflint_config_file",
|
|
63
|
-
),
|
|
64
|
-
Variable(
|
|
65
|
-
key="TFLINT_RECURSIVE",
|
|
66
|
-
value_from_parameter="tflint_recursive",
|
|
67
|
-
),
|
|
68
|
-
],
|
|
69
|
-
)
|
|
70
|
-
]
|
|
71
|
-
|
|
72
|
-
__policies__ = [
|
|
73
|
-
Policy(
|
|
74
|
-
name_prefix="tflint",
|
|
75
|
-
type="PLAN",
|
|
76
|
-
labels=["tflint"],
|
|
77
|
-
body="""
|
|
78
|
-
package spacelift
|
|
79
|
-
|
|
80
|
-
import rego.v1
|
|
81
|
-
|
|
82
|
-
max_errors := 0
|
|
83
|
-
max_warnings := 0
|
|
84
|
-
max_notices := 3
|
|
85
|
-
|
|
86
|
-
issues := input.third_party_metadata.custom.tflint.issues
|
|
87
|
-
|
|
88
|
-
deny contains sprintf("Too many errors (%d)", [cnt]) if {
|
|
89
|
-
cnt := count([issue | issue := issues[_]; issue.rule.severity == "error"])
|
|
90
|
-
cnt > max_errors
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
deny contains sprintf("Too many warnings (%d)", [cnt]) if {
|
|
94
|
-
cnt := count([issue | issue := issues[_]; issue.rule.severity == "warning"])
|
|
95
|
-
cnt > max_warnings
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
deny contains sprintf("Too many notices (%d)", [cnt]) if {
|
|
99
|
-
cnt := count([issue | issue := issues[_]; issue.rule.severity == "notice"])
|
|
100
|
-
cnt > max_notices
|
|
101
|
-
}
|
|
102
|
-
""",
|
|
103
|
-
)
|
|
104
|
-
]
|
|
105
|
-
|
|
106
|
-
def before_plan(self):
|
|
107
|
-
try:
|
|
108
|
-
options = ["--format=json"]
|
|
109
|
-
|
|
110
|
-
# Configuration file
|
|
111
|
-
config_file = (
|
|
112
|
-
os.environ.get("TFLINT_CONFIG_FILE") or DEFAULT_TFLINT_CONFIG_FILE
|
|
113
|
-
)
|
|
114
|
-
if config_file:
|
|
115
|
-
options.append(f"--config={config_file}")
|
|
116
|
-
|
|
117
|
-
# Recursive
|
|
118
|
-
recursive = os.environ.get("TFLINT_RECURSIVE") or DEFAULT_TFLINT_RECURSIVE
|
|
119
|
-
if recursive == "true":
|
|
120
|
-
options.append("--recursive")
|
|
121
|
-
|
|
122
|
-
return_code, stdout, stderr = self.run_cli("tflint", "--init")
|
|
123
|
-
if return_code != 0:
|
|
124
|
-
self.logger.error(f"tflint --init failed with code {return_code}")
|
|
125
|
-
if stderr:
|
|
126
|
-
# Display stderr manually because output display is disabled
|
|
127
|
-
self.logger.error("\n".join(stderr))
|
|
128
|
-
exit(1)
|
|
129
|
-
|
|
130
|
-
return_code, stdout, stderr = self.run_cli(
|
|
131
|
-
"tflint", *options, print_output=False
|
|
132
|
-
)
|
|
133
|
-
stdout_json = json.loads("\n".join(stdout))
|
|
134
|
-
|
|
135
|
-
# KLUDGE: The `expect_code` argument does not support multiple values so we need to manually handle this.
|
|
136
|
-
# `0` means success and `2` means issues found which is informational, not a failure.
|
|
137
|
-
if return_code not in [0, 2]:
|
|
138
|
-
for error in stdout_json["errors"]:
|
|
139
|
-
self.logger.error(error["message"])
|
|
140
|
-
exit(1)
|
|
141
|
-
|
|
142
|
-
self.add_to_policy_input("tflint", stdout_json)
|
|
143
|
-
|
|
144
|
-
if len(stdout_json["issues"]) == 0:
|
|
145
|
-
self.logger.info("No issues found")
|
|
146
|
-
return
|
|
147
|
-
|
|
148
|
-
findings = {
|
|
149
|
-
"error": {},
|
|
150
|
-
"warning": {},
|
|
151
|
-
"notice": {},
|
|
152
|
-
}
|
|
153
|
-
for match in stdout_json["issues"]:
|
|
154
|
-
severity = match["rule"]["severity"]
|
|
155
|
-
rule_name = match["rule"]["name"]
|
|
156
|
-
|
|
157
|
-
if rule_name not in findings[severity]:
|
|
158
|
-
findings[severity][rule_name] = []
|
|
159
|
-
|
|
160
|
-
findings[severity][rule_name].append(match)
|
|
161
|
-
|
|
162
|
-
markdown = "# TFLint Findings\n\n"
|
|
163
|
-
for severity, rules in findings.items():
|
|
164
|
-
# Skip severity level if no issues were found
|
|
165
|
-
if len(rules) == 0:
|
|
166
|
-
continue
|
|
167
|
-
|
|
168
|
-
emoji = None
|
|
169
|
-
if severity == "notice":
|
|
170
|
-
emoji = "🟡"
|
|
171
|
-
elif severity == "warning":
|
|
172
|
-
emoji = "🟠"
|
|
173
|
-
elif severity == "error":
|
|
174
|
-
emoji = "🔴"
|
|
175
|
-
if emoji is not None:
|
|
176
|
-
markdown += f"## {emoji} {severity.title()} Findings\n\n"
|
|
177
|
-
else:
|
|
178
|
-
markdown += f"## {severity.title()} Findings\n\n"
|
|
179
|
-
|
|
180
|
-
for rule_name, issues in rules.items():
|
|
181
|
-
markdown += f"### {rule_name}\n\n"
|
|
182
|
-
for issue in issues:
|
|
183
|
-
markdown += f"- {issue['message']} _({issue['range']['filename']}:{issue['range']['start']['line']})_\n"
|
|
184
|
-
markdown += "\n"
|
|
185
|
-
|
|
186
|
-
result = self.send_markdown(markdown)
|
|
187
|
-
if result:
|
|
188
|
-
self.logger.info(
|
|
189
|
-
"Issues found. Check the Plugins Output tab for details."
|
|
190
|
-
)
|
|
191
|
-
else:
|
|
192
|
-
self.logger.error("Failed to upload plugin outputs")
|
|
193
|
-
except Exception as e:
|
|
194
|
-
self.logger.error(f"Plugin failed: {e}")
|
|
195
|
-
exit(1)
|
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
name: tflint
|
|
2
|
-
version: 0.1.0
|
|
3
|
-
description: |-
|
|
4
|
-
# Plugin TFLint
|
|
5
|
-
|
|
6
|
-
The TFLint plugin analyzes your Terraform/OpenTofu files and generates a report with findings categorized by severity.
|
|
7
|
-
|
|
8
|
-
You can also access the data from a plan policy via the `input.third_party_metadata.custom.tflint` object.
|
|
9
|
-
An example Plan policy is included with the plugin.
|
|
10
|
-
|
|
11
|
-
## Usage
|
|
12
|
-
|
|
13
|
-
1. Install the plugin
|
|
14
|
-
2. Add the `autoattach` label to any stack that uses Terraform/OpenTofu.
|
|
15
|
-
author: Spacelift
|
|
16
|
-
labels:
|
|
17
|
-
- qa
|
|
18
|
-
- security
|
|
19
|
-
parameters:
|
|
20
|
-
- name: Configuration file
|
|
21
|
-
description: Configuration file name
|
|
22
|
-
sensitive: false
|
|
23
|
-
required: false
|
|
24
|
-
default: ''
|
|
25
|
-
id: tflint_config_file
|
|
26
|
-
- name: Recursive
|
|
27
|
-
description: 'Run command in each directory recursively. Allowed values: true, false'
|
|
28
|
-
sensitive: false
|
|
29
|
-
required: false
|
|
30
|
-
default: 'true'
|
|
31
|
-
id: tflint_recursive
|
|
32
|
-
contexts:
|
|
33
|
-
- name_prefix: tflint
|
|
34
|
-
description: TFLint Plugin
|
|
35
|
-
env:
|
|
36
|
-
- key: TFLINT_CONFIG_FILE
|
|
37
|
-
value_from_parameter: tflint_config_file
|
|
38
|
-
sensitive: false
|
|
39
|
-
- key: TFLINT_RECURSIVE
|
|
40
|
-
value_from_parameter: tflint_recursive
|
|
41
|
-
sensitive: false
|
|
42
|
-
mounted_files:
|
|
43
|
-
- path: /mnt/workspace/plugins/tflint/plugin.py
|
|
44
|
-
content: |-
|
|
45
|
-
import json
|
|
46
|
-
import os
|
|
47
|
-
|
|
48
|
-
from spaceforge import Binary, Context, Parameter, Policy, SpaceforgePlugin, Variable
|
|
49
|
-
|
|
50
|
-
DEFAULT_TFLINT_CONFIG_FILE = ""
|
|
51
|
-
DEFAULT_TFLINT_RECURSIVE = "true"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
class TFLintPlugin(SpaceforgePlugin):
|
|
55
|
-
"""
|
|
56
|
-
# Plugin TFLint
|
|
57
|
-
|
|
58
|
-
The TFLint plugin analyzes your Terraform/OpenTofu files and generates a report with findings categorized by severity.
|
|
59
|
-
|
|
60
|
-
You can also access the data from a plan policy via the `input.third_party_metadata.custom.tflint` object.
|
|
61
|
-
An example Plan policy is included with the plugin.
|
|
62
|
-
|
|
63
|
-
## Usage
|
|
64
|
-
|
|
65
|
-
1. Install the plugin
|
|
66
|
-
2. Add the `autoattach` label to any stack that uses Terraform/OpenTofu.
|
|
67
|
-
"""
|
|
68
|
-
|
|
69
|
-
__author__ = "Spacelift"
|
|
70
|
-
__labels__ = ["qa", "security"]
|
|
71
|
-
__plugin_name__ = "tflint"
|
|
72
|
-
__version__ = "0.1.0"
|
|
73
|
-
|
|
74
|
-
__binaries__ = [
|
|
75
|
-
Binary(
|
|
76
|
-
name="tflint",
|
|
77
|
-
download_urls={
|
|
78
|
-
"amd64": "https://github.com/terraform-linters/tflint/releases/download/v0.59.1/tflint_linux_amd64.zip",
|
|
79
|
-
"arm64": "https://github.com/terraform-linters/tflint/releases/download/v0.59.1/tflint_linux_arm64.zip",
|
|
80
|
-
},
|
|
81
|
-
),
|
|
82
|
-
]
|
|
83
|
-
|
|
84
|
-
__parameters__ = [
|
|
85
|
-
Parameter(
|
|
86
|
-
name="Configuration file",
|
|
87
|
-
id="tflint_config_file",
|
|
88
|
-
description="Configuration file name",
|
|
89
|
-
default=DEFAULT_TFLINT_CONFIG_FILE,
|
|
90
|
-
),
|
|
91
|
-
Parameter(
|
|
92
|
-
name="Recursive",
|
|
93
|
-
id="tflint_recursive",
|
|
94
|
-
description="Run command in each directory recursively. Allowed values: true, false",
|
|
95
|
-
default=DEFAULT_TFLINT_RECURSIVE,
|
|
96
|
-
),
|
|
97
|
-
]
|
|
98
|
-
|
|
99
|
-
__contexts__ = [
|
|
100
|
-
Context(
|
|
101
|
-
name_prefix="tflint",
|
|
102
|
-
description="TFLint Plugin",
|
|
103
|
-
env=[
|
|
104
|
-
Variable(
|
|
105
|
-
key="TFLINT_CONFIG_FILE",
|
|
106
|
-
value_from_parameter="tflint_config_file",
|
|
107
|
-
),
|
|
108
|
-
Variable(
|
|
109
|
-
key="TFLINT_RECURSIVE",
|
|
110
|
-
value_from_parameter="tflint_recursive",
|
|
111
|
-
),
|
|
112
|
-
],
|
|
113
|
-
)
|
|
114
|
-
]
|
|
115
|
-
|
|
116
|
-
__policies__ = [
|
|
117
|
-
Policy(
|
|
118
|
-
name_prefix="tflint",
|
|
119
|
-
type="PLAN",
|
|
120
|
-
labels=["tflint"],
|
|
121
|
-
body="""
|
|
122
|
-
package spacelift
|
|
123
|
-
|
|
124
|
-
import rego.v1
|
|
125
|
-
|
|
126
|
-
max_errors := 0
|
|
127
|
-
max_warnings := 0
|
|
128
|
-
max_notices := 3
|
|
129
|
-
|
|
130
|
-
issues := input.third_party_metadata.custom.tflint.issues
|
|
131
|
-
|
|
132
|
-
deny contains sprintf("Too many errors (%d)", [cnt]) if {
|
|
133
|
-
cnt := count([issue | issue := issues[_]; issue.rule.severity == "error"])
|
|
134
|
-
cnt > max_errors
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
deny contains sprintf("Too many warnings (%d)", [cnt]) if {
|
|
138
|
-
cnt := count([issue | issue := issues[_]; issue.rule.severity == "warning"])
|
|
139
|
-
cnt > max_warnings
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
deny contains sprintf("Too many notices (%d)", [cnt]) if {
|
|
143
|
-
cnt := count([issue | issue := issues[_]; issue.rule.severity == "notice"])
|
|
144
|
-
cnt > max_notices
|
|
145
|
-
}
|
|
146
|
-
""",
|
|
147
|
-
)
|
|
148
|
-
]
|
|
149
|
-
|
|
150
|
-
def before_plan(self):
|
|
151
|
-
try:
|
|
152
|
-
options = ["--format=json"]
|
|
153
|
-
|
|
154
|
-
# Configuration file
|
|
155
|
-
config_file = (
|
|
156
|
-
os.environ.get("TFLINT_CONFIG_FILE") or DEFAULT_TFLINT_CONFIG_FILE
|
|
157
|
-
)
|
|
158
|
-
if config_file:
|
|
159
|
-
options.append(f"--config={config_file}")
|
|
160
|
-
|
|
161
|
-
# Recursive
|
|
162
|
-
recursive = os.environ.get("TFLINT_RECURSIVE") or DEFAULT_TFLINT_RECURSIVE
|
|
163
|
-
if recursive == "true":
|
|
164
|
-
options.append("--recursive")
|
|
165
|
-
|
|
166
|
-
return_code, stdout, stderr = self.run_cli("tflint", "--init")
|
|
167
|
-
if return_code != 0:
|
|
168
|
-
self.logger.error(f"tflint --init failed with code {return_code}")
|
|
169
|
-
if stderr:
|
|
170
|
-
# Display stderr manually because output display is disabled
|
|
171
|
-
self.logger.error("\n".join(stderr))
|
|
172
|
-
exit(1)
|
|
173
|
-
|
|
174
|
-
return_code, stdout, stderr = self.run_cli(
|
|
175
|
-
"tflint", *options, print_output=False
|
|
176
|
-
)
|
|
177
|
-
stdout_json = json.loads("\n".join(stdout))
|
|
178
|
-
|
|
179
|
-
# KLUDGE: The `expect_code` argument does not support multiple values so we need to manually handle this.
|
|
180
|
-
# `0` means success and `2` means issues found which is informational, not a failure.
|
|
181
|
-
if return_code not in [0, 2]:
|
|
182
|
-
for error in stdout_json["errors"]:
|
|
183
|
-
self.logger.error(error["message"])
|
|
184
|
-
exit(1)
|
|
185
|
-
|
|
186
|
-
self.add_to_policy_input("tflint", stdout_json)
|
|
187
|
-
|
|
188
|
-
if len(stdout_json["issues"]) == 0:
|
|
189
|
-
self.logger.info("No issues found")
|
|
190
|
-
return
|
|
191
|
-
|
|
192
|
-
findings = {
|
|
193
|
-
"error": {},
|
|
194
|
-
"warning": {},
|
|
195
|
-
"notice": {},
|
|
196
|
-
}
|
|
197
|
-
for match in stdout_json["issues"]:
|
|
198
|
-
severity = match["rule"]["severity"]
|
|
199
|
-
rule_name = match["rule"]["name"]
|
|
200
|
-
|
|
201
|
-
if rule_name not in findings[severity]:
|
|
202
|
-
findings[severity][rule_name] = []
|
|
203
|
-
|
|
204
|
-
findings[severity][rule_name].append(match)
|
|
205
|
-
|
|
206
|
-
markdown = "# TFLint Findings\n\n"
|
|
207
|
-
for severity, rules in findings.items():
|
|
208
|
-
# Skip severity level if no issues were found
|
|
209
|
-
if len(rules) == 0:
|
|
210
|
-
continue
|
|
211
|
-
|
|
212
|
-
emoji = None
|
|
213
|
-
if severity == "notice":
|
|
214
|
-
emoji = "🟡"
|
|
215
|
-
elif severity == "warning":
|
|
216
|
-
emoji = "🟠"
|
|
217
|
-
elif severity == "error":
|
|
218
|
-
emoji = "🔴"
|
|
219
|
-
if emoji is not None:
|
|
220
|
-
markdown += f"## {emoji} {severity.title()} Findings\n\n"
|
|
221
|
-
else:
|
|
222
|
-
markdown += f"## {severity.title()} Findings\n\n"
|
|
223
|
-
|
|
224
|
-
for rule_name, issues in rules.items():
|
|
225
|
-
markdown += f"### {rule_name}\n\n"
|
|
226
|
-
for issue in issues:
|
|
227
|
-
markdown += f"- {issue['message']} _({issue['range']['filename']}:{issue['range']['start']['line']})_\n"
|
|
228
|
-
markdown += "\n"
|
|
229
|
-
|
|
230
|
-
result = self.send_markdown(markdown)
|
|
231
|
-
if result:
|
|
232
|
-
self.logger.info(
|
|
233
|
-
"Issues found. Check the Plugins Output tab for details."
|
|
234
|
-
)
|
|
235
|
-
else:
|
|
236
|
-
self.logger.error("Failed to upload plugin outputs")
|
|
237
|
-
except Exception as e:
|
|
238
|
-
self.logger.error(f"Plugin failed: {e}")
|
|
239
|
-
exit(1)
|
|
240
|
-
sensitive: false
|
|
241
|
-
- path: /mnt/workspace/plugins/tflint/binary_install_tflint.sh
|
|
242
|
-
content: |-
|
|
243
|
-
#!/bin/sh
|
|
244
|
-
|
|
245
|
-
set -e
|
|
246
|
-
|
|
247
|
-
export PATH="/mnt/workspace/plugins/plugin_binaries:$PATH"
|
|
248
|
-
if command -v tflint; then
|
|
249
|
-
echo "tflint is already installed."
|
|
250
|
-
return
|
|
251
|
-
fi
|
|
252
|
-
|
|
253
|
-
echo "Installing tflint..."
|
|
254
|
-
mkdir -p /mnt/workspace/plugins/plugin_binaries
|
|
255
|
-
|
|
256
|
-
ARCH="$(arch)"
|
|
257
|
-
if [ "$ARCH" = "x86_64" ]; then
|
|
258
|
-
URL="https://github.com/terraform-linters/tflint/releases/download/v0.59.1/tflint_linux_amd64.zip"
|
|
259
|
-
elif [ "$ARCH" = "arm64" ]; then
|
|
260
|
-
URL="https://github.com/terraform-linters/tflint/releases/download/v0.59.1/tflint_linux_arm64.zip"
|
|
261
|
-
else
|
|
262
|
-
echo "Error: Unsupported architecture '$ARCH'"
|
|
263
|
-
exit 1
|
|
264
|
-
fi
|
|
265
|
-
|
|
266
|
-
case "$URL" in
|
|
267
|
-
*.tar.gz|*.tar.bz2|*.zip)
|
|
268
|
-
TMP_DIR=$(mktemp -d)
|
|
269
|
-
trap 'rm -rf "$TMP_DIR"' EXIT
|
|
270
|
-
|
|
271
|
-
ARCHIVE="$TMP_DIR/archive"
|
|
272
|
-
curl "$URL" -o "$ARCHIVE" -fL
|
|
273
|
-
|
|
274
|
-
case "$URL" in
|
|
275
|
-
*.tar.gz)
|
|
276
|
-
tar -xzf "$ARCHIVE" -C "$TMP_DIR" || { echo "Error: Failed to extract archive"; exit 1; }
|
|
277
|
-
;;
|
|
278
|
-
*.tar.bz2)
|
|
279
|
-
tar -xjf "$ARCHIVE" -C "$TMP_DIR" || { echo "Error: Failed to extract archive"; exit 1; }
|
|
280
|
-
;;
|
|
281
|
-
*.zip)
|
|
282
|
-
unzip -o "$ARCHIVE" -d "$TMP_DIR" || { echo "Error: Failed to extract archive"; exit 1; }
|
|
283
|
-
;;
|
|
284
|
-
esac
|
|
285
|
-
|
|
286
|
-
FOUND_BINARY=$(find "$TMP_DIR" -type f -name "tflint" | head -n 1)
|
|
287
|
-
if [ -z "$FOUND_BINARY" ]; then
|
|
288
|
-
echo "Error: Could not find binary 'tflint' in extracted archive"
|
|
289
|
-
exit 1
|
|
290
|
-
fi
|
|
291
|
-
|
|
292
|
-
mv "$FOUND_BINARY" "/mnt/workspace/plugins/plugin_binaries/tflint"
|
|
293
|
-
;;
|
|
294
|
-
*)
|
|
295
|
-
curl "$URL" -o "/mnt/workspace/plugins/plugin_binaries/tflint" -fL
|
|
296
|
-
;;
|
|
297
|
-
esac
|
|
298
|
-
|
|
299
|
-
chmod +x "/mnt/workspace/plugins/plugin_binaries/tflint"
|
|
300
|
-
sensitive: false
|
|
301
|
-
- path: /mnt/workspace/plugins/tflint/before_plan.sh
|
|
302
|
-
content: |-
|
|
303
|
-
#!/bin/sh
|
|
304
|
-
|
|
305
|
-
set -e
|
|
306
|
-
|
|
307
|
-
cd /mnt/workspace/plugins/tflint
|
|
308
|
-
|
|
309
|
-
if [ ! -d "./venv" ]; then
|
|
310
|
-
python -m venv ./venv
|
|
311
|
-
fi
|
|
312
|
-
. venv/bin/activate
|
|
313
|
-
|
|
314
|
-
if ! command -v spaceforge; then
|
|
315
|
-
pip install spaceforge
|
|
316
|
-
fi
|
|
317
|
-
|
|
318
|
-
if [ -f requirements.txt ] && [ ! -f .spaceforge_installed_requirements ]; then
|
|
319
|
-
pip install -r requirements.txt
|
|
320
|
-
touch .spaceforge_installed_requirements
|
|
321
|
-
fi
|
|
322
|
-
|
|
323
|
-
export PATH="/mnt/workspace/plugins/plugin_binaries:$PATH"
|
|
324
|
-
|
|
325
|
-
cd /mnt/workspace/source/$TF_VAR_spacelift_project_root
|
|
326
|
-
spaceforge run --plugin-file /mnt/workspace/plugins/tflint/plugin.py before_plan
|
|
327
|
-
sensitive: false
|
|
328
|
-
hooks:
|
|
329
|
-
before_init:
|
|
330
|
-
- mkdir -p /mnt/workspace/plugins/tflint
|
|
331
|
-
- chmod +x /mnt/workspace/plugins/tflint/binary_install_tflint.sh && /mnt/workspace/plugins/tflint/binary_install_tflint.sh
|
|
332
|
-
before_plan:
|
|
333
|
-
- chmod +x /mnt/workspace/plugins/tflint/before_plan.sh && /mnt/workspace/plugins/tflint/before_plan.sh
|
|
334
|
-
policies:
|
|
335
|
-
- name_prefix: tflint
|
|
336
|
-
type: PLAN
|
|
337
|
-
body: |-
|
|
338
|
-
package spacelift
|
|
339
|
-
|
|
340
|
-
import rego.v1
|
|
341
|
-
|
|
342
|
-
max_errors := 0
|
|
343
|
-
max_warnings := 0
|
|
344
|
-
max_notices := 3
|
|
345
|
-
|
|
346
|
-
issues := input.third_party_metadata.custom.tflint.issues
|
|
347
|
-
|
|
348
|
-
deny contains sprintf("Too many errors (%d)", [cnt]) if {
|
|
349
|
-
cnt := count([issue | issue := issues[_]; issue.rule.severity == "error"])
|
|
350
|
-
cnt > max_errors
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
deny contains sprintf("Too many warnings (%d)", [cnt]) if {
|
|
354
|
-
cnt := count([issue | issue := issues[_]; issue.rule.severity == "warning"])
|
|
355
|
-
cnt > max_warnings
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
deny contains sprintf("Too many notices (%d)", [cnt]) if {
|
|
359
|
-
cnt := count([issue | issue := issues[_]; issue.rule.severity == "notice"])
|
|
360
|
-
cnt > max_notices
|
|
361
|
-
}
|
|
362
|
-
labels:
|
|
363
|
-
- tflint
|
spaceforge-1.1.4/setup.py
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Setup script for spaceforge - Spacelift Plugin Framework
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from setuptools import setup, find_packages
|
|
6
|
-
|
|
7
|
-
with open("README.md", "r", encoding="utf-8") as fh:
|
|
8
|
-
long_description = fh.read()
|
|
9
|
-
|
|
10
|
-
setup(
|
|
11
|
-
name="spaceforge",
|
|
12
|
-
author="Spacelift",
|
|
13
|
-
author_email="support@spacelift.io",
|
|
14
|
-
description="A Python framework for building Spacelift plugins",
|
|
15
|
-
long_description=long_description,
|
|
16
|
-
long_description_content_type="text/markdown",
|
|
17
|
-
url="https://github.com/spacelift-io/plugins",
|
|
18
|
-
project_urls={
|
|
19
|
-
"Bug Reports": "https://github.com/spacelift-io/plugins/issues",
|
|
20
|
-
"Source": "https://github.com/spacelift-io/plugins",
|
|
21
|
-
},
|
|
22
|
-
packages=find_packages(),
|
|
23
|
-
package_data={
|
|
24
|
-
"spaceforge": ["schema.json"],
|
|
25
|
-
},
|
|
26
|
-
include_package_data=True,
|
|
27
|
-
classifiers=[
|
|
28
|
-
"Development Status :: 3 - Alpha",
|
|
29
|
-
"Intended Audience :: Developers",
|
|
30
|
-
"License :: OSI Approved :: MIT License",
|
|
31
|
-
"Operating System :: OS Independent",
|
|
32
|
-
"Programming Language :: Python :: 3",
|
|
33
|
-
"Programming Language :: Python :: 3.8",
|
|
34
|
-
"Programming Language :: Python :: 3.9",
|
|
35
|
-
"Programming Language :: Python :: 3.10",
|
|
36
|
-
"Programming Language :: Python :: 3.11",
|
|
37
|
-
"Programming Language :: Python :: 3.12",
|
|
38
|
-
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
39
|
-
"Topic :: System :: Systems Administration",
|
|
40
|
-
],
|
|
41
|
-
python_requires=">=3.9",
|
|
42
|
-
install_requires=[
|
|
43
|
-
"PyYAML>=6.0",
|
|
44
|
-
"click>=8.0.0",
|
|
45
|
-
"pydantic>=2.11.7",
|
|
46
|
-
"Jinja2>=3.1.0",
|
|
47
|
-
"mergedeep>=1.3.4",
|
|
48
|
-
],
|
|
49
|
-
extras_require={
|
|
50
|
-
"dev": [
|
|
51
|
-
"pytest>=6.0",
|
|
52
|
-
"pytest-cov",
|
|
53
|
-
"black",
|
|
54
|
-
"isort",
|
|
55
|
-
"mypy",
|
|
56
|
-
"autoflake"
|
|
57
|
-
],
|
|
58
|
-
},
|
|
59
|
-
entry_points={
|
|
60
|
-
"console_scripts": [
|
|
61
|
-
"spaceforge=spaceforge.__main__:main",
|
|
62
|
-
],
|
|
63
|
-
},
|
|
64
|
-
keywords="spacelift plugin framework infrastructure devops spaceforge",
|
|
65
|
-
zip_safe=False,
|
|
66
|
-
)
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Dynamic version detection from git tags.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import subprocess
|
|
6
|
-
from typing import Optional
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def get_git_version() -> Optional[str]:
|
|
10
|
-
"""
|
|
11
|
-
Get version from git tags.
|
|
12
|
-
|
|
13
|
-
Returns:
|
|
14
|
-
Version string (without 'v' prefix) or None if not available
|
|
15
|
-
"""
|
|
16
|
-
try:
|
|
17
|
-
# Try to get the current tag
|
|
18
|
-
result = subprocess.run(
|
|
19
|
-
["git", "describe", "--tags", "--exact-match"],
|
|
20
|
-
capture_output=True,
|
|
21
|
-
text=True,
|
|
22
|
-
check=True,
|
|
23
|
-
)
|
|
24
|
-
tag = result.stdout.strip()
|
|
25
|
-
# Remove 'v' prefix if present
|
|
26
|
-
return tag[1:] if tag.startswith("v") else tag
|
|
27
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
28
|
-
# Fall back to describe with commit info
|
|
29
|
-
try:
|
|
30
|
-
result = subprocess.run(
|
|
31
|
-
["git", "describe", "--tags", "--always"],
|
|
32
|
-
capture_output=True,
|
|
33
|
-
text=True,
|
|
34
|
-
check=True,
|
|
35
|
-
)
|
|
36
|
-
tag = result.stdout.strip()
|
|
37
|
-
# Remove 'v' prefix if present
|
|
38
|
-
return tag[1:] if tag.startswith("v") else tag
|
|
39
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
40
|
-
return None
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def get_version() -> str:
|
|
44
|
-
"""
|
|
45
|
-
Get the package version.
|
|
46
|
-
|
|
47
|
-
Tries git tags first, then setuptools-scm, falls back to default version.
|
|
48
|
-
|
|
49
|
-
Returns:
|
|
50
|
-
Version string
|
|
51
|
-
"""
|
|
52
|
-
# Try git version first
|
|
53
|
-
git_version = get_git_version()
|
|
54
|
-
if git_version:
|
|
55
|
-
return git_version
|
|
56
|
-
|
|
57
|
-
# Try setuptools-scm generated version file
|
|
58
|
-
try:
|
|
59
|
-
from ._version_scm import version # type: ignore[import-not-found]
|
|
60
|
-
|
|
61
|
-
return str(version)
|
|
62
|
-
except ImportError:
|
|
63
|
-
pass
|
|
64
|
-
|
|
65
|
-
# Try setuptools-scm directly
|
|
66
|
-
try:
|
|
67
|
-
from setuptools_scm import (
|
|
68
|
-
get_version as scm_get_version, # type: ignore[import-untyped]
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
result = scm_get_version(root="..", relative_to=__file__)
|
|
72
|
-
return str(result)
|
|
73
|
-
except ImportError:
|
|
74
|
-
pass
|
|
75
|
-
except Exception:
|
|
76
|
-
# setuptools_scm might fail in various ways, ignore
|
|
77
|
-
pass
|
|
78
|
-
|
|
79
|
-
# Fall back to default version for development
|
|
80
|
-
return "0.1.0-dev"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|