spaceforge 1.1.5__py3-none-any.whl → 1.1.6__py3-none-any.whl
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/_version_scm.py +2 -2
- spaceforge/generator.py +6 -2
- spaceforge/templates/ensure_spaceforge_and_run.sh.j2 +2 -2
- spaceforge/test_generator.py +2 -2
- spaceforge-1.1.6.dist-info/METADATA +56 -0
- {spaceforge-1.1.5.dist-info → spaceforge-1.1.6.dist-info}/RECORD +10 -10
- {spaceforge-1.1.5.dist-info → spaceforge-1.1.6.dist-info}/licenses/LICENSE +1 -1
- spaceforge-1.1.5.dist-info/METADATA +0 -736
- {spaceforge-1.1.5.dist-info → spaceforge-1.1.6.dist-info}/WHEEL +0 -0
- {spaceforge-1.1.5.dist-info → spaceforge-1.1.6.dist-info}/entry_points.txt +0 -0
- {spaceforge-1.1.5.dist-info → spaceforge-1.1.6.dist-info}/top_level.txt +0 -0
spaceforge/_version_scm.py
CHANGED
|
@@ -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.6'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 1, 6)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
spaceforge/generator.py
CHANGED
|
@@ -198,9 +198,13 @@ class PluginGenerator:
|
|
|
198
198
|
|
|
199
199
|
def _update_with_requirements(self, mounted_files: List[MountedFile]) -> None:
|
|
200
200
|
"""Update the plugin hooks if there is a requirements.txt"""
|
|
201
|
-
|
|
201
|
+
# Look for requirements.txt in the same directory as the plugin file
|
|
202
|
+
plugin_dir = os.path.dirname(self.plugin_path)
|
|
203
|
+
requirements_path = os.path.join(plugin_dir, "requirements.txt")
|
|
204
|
+
|
|
205
|
+
if os.path.exists(requirements_path) and self.config is not None:
|
|
202
206
|
# read the requirements.txt file
|
|
203
|
-
with open(
|
|
207
|
+
with open(requirements_path, "r") as f:
|
|
204
208
|
mounted_files.append(
|
|
205
209
|
MountedFile(
|
|
206
210
|
path=f"{self.plugin_working_directory}/requirements.txt",
|
|
@@ -5,7 +5,7 @@ set -e
|
|
|
5
5
|
cd {{plugin_path}}
|
|
6
6
|
|
|
7
7
|
if [ ! -d "./venv" ]; then
|
|
8
|
-
python -m venv ./venv
|
|
8
|
+
python -m venv --system-site-packages ./venv
|
|
9
9
|
fi
|
|
10
10
|
. venv/bin/activate
|
|
11
11
|
|
|
@@ -21,4 +21,4 @@ fi
|
|
|
21
21
|
export PATH="/mnt/workspace/plugins/plugin_binaries:$PATH"
|
|
22
22
|
{% endif %}
|
|
23
23
|
cd /mnt/workspace/source/$TF_VAR_spacelift_project_root
|
|
24
|
-
spaceforge run --plugin-file {{plugin_file}} {{phase}}
|
|
24
|
+
python -m spaceforge run --plugin-file {{plugin_file}} {{phase}}
|
spaceforge/test_generator.py
CHANGED
|
@@ -414,14 +414,14 @@ class NotAPlugin:
|
|
|
414
414
|
def test_get_plugin_contexts_with_requirements(self, mock_exists: Mock) -> None:
|
|
415
415
|
"""Test context generation with requirements.txt."""
|
|
416
416
|
mock_exists.side_effect = (
|
|
417
|
-
lambda path: path
|
|
417
|
+
lambda path: path.endswith("requirements.txt") or "plugin.py" in path
|
|
418
418
|
)
|
|
419
419
|
|
|
420
420
|
# Mock specific file contents with a custom open function
|
|
421
421
|
original_open = open
|
|
422
422
|
|
|
423
423
|
def mock_open_func(filename: str, *args: Any, **kwargs: Any) -> Any:
|
|
424
|
-
if filename
|
|
424
|
+
if filename.endswith("requirements.txt"):
|
|
425
425
|
from io import StringIO
|
|
426
426
|
|
|
427
427
|
return StringIO("requirements content")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spaceforge
|
|
3
|
+
Version: 1.1.6
|
|
4
|
+
Summary: A Python framework for building Spacelift plugins
|
|
5
|
+
Author-email: Spacelift <support@spacelift.io>
|
|
6
|
+
Maintainer-email: Spacelift <support@spacelift.io>
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/spacelift-io/plugins
|
|
9
|
+
Project-URL: Documentation, https://github.com/spacelift-io/plugins#readme
|
|
10
|
+
Project-URL: Repository, https://github.com/spacelift-io/plugins
|
|
11
|
+
Project-URL: Bug Reports, https://github.com/spacelift-io/plugins/issues
|
|
12
|
+
Keywords: spacelift,plugin,framework,infrastructure,devops,spaceforge
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: System :: Systems Administration
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: PyYAML>=6.0
|
|
27
|
+
Requires-Dist: click>=8.0.0
|
|
28
|
+
Requires-Dist: pydantic>=2.11.7
|
|
29
|
+
Requires-Dist: Jinja2>=3.1.0
|
|
30
|
+
Requires-Dist: mergedeep>=1.3.4
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
34
|
+
Requires-Dist: black; extra == "dev"
|
|
35
|
+
Requires-Dist: isort; extra == "dev"
|
|
36
|
+
Requires-Dist: mypy; extra == "dev"
|
|
37
|
+
Requires-Dist: types-PyYAML; extra == "dev"
|
|
38
|
+
Requires-Dist: setuptools-scm[toml]>=6.2; extra == "dev"
|
|
39
|
+
Requires-Dist: autoflake; extra == "dev"
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# Spaceforge - Build Spacelift Plugins in Python
|
|
43
|
+
|
|
44
|
+
Spaceforge is a Python framework for building powerful Spacelift plugins using a declarative, hook-based approach. Define your plugin logic in Python, and Spaceforge automatically generates the plugin manifest for Spacelift.
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
For installation and usage instructions, see [our documentation](https://docs.spacelift.io/integrations/plugins).
|
|
49
|
+
|
|
50
|
+
## Contributing
|
|
51
|
+
|
|
52
|
+
To contribute to Spaceforge or create plugins, see our [CONTRIBUTING.md](./CONTRIBUTING.md) file.
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
Spaceforge and Spacelift plugins are licensed under the [MIT license](./LICENSE).
|
|
@@ -2,15 +2,15 @@ spaceforge/README.md,sha256=gDyCQN0EW4xp8Skvs6J0qHpO9CjVTnRfSnhduAIFQs4,7717
|
|
|
2
2
|
spaceforge/__init__.py,sha256=TU-vvm15dK1ucixNW0V42eTT72x3_hmKSyxP4MC1Occ,589
|
|
3
3
|
spaceforge/__main__.py,sha256=UKbeCuEFgoNlEOX44-kYUAQuUdySdkAKsvbjXEyLeWI,965
|
|
4
4
|
spaceforge/_version.py,sha256=70eCBV_uVCm0U7qrtYRUZM3GJnJRtvZQ_aPFLd8H7CI,1009
|
|
5
|
-
spaceforge/_version_scm.py,sha256=
|
|
5
|
+
spaceforge/_version_scm.py,sha256=Ss7ZrKeNFejVcP8xqi-vmh6yUDDLhmA6FdF5e4QxSZA,704
|
|
6
6
|
spaceforge/cls.py,sha256=oYW5t5_xs9ZM6Ne_b4trxCPxLHQrqbqgUeibeM8O4PU,6329
|
|
7
7
|
spaceforge/conftest.py,sha256=U-xCavCsgRAQXqflIIOMeq9pcGbeqRviUNkEXgZol8g,2141
|
|
8
|
-
spaceforge/generator.py,sha256=
|
|
8
|
+
spaceforge/generator.py,sha256=inhw38FGKsiktwssiSFgGsR1d7FGxWDv_e8WeEydenw,18634
|
|
9
9
|
spaceforge/plugin.py,sha256=Ytm2B7nnJNH231V4gUFY1pBmwVNTpIg3YviUL_Bnf24,14963
|
|
10
10
|
spaceforge/runner.py,sha256=NNddgK_OsnQWx2YFcyTpAOWfFLBDUphufBVfXYEzfzM,3166
|
|
11
11
|
spaceforge/schema.json,sha256=89IROLVlCj8txGMiLt4Bbuo_2muSxKoZCyaXQ2vuA9c,10869
|
|
12
12
|
spaceforge/test_cls.py,sha256=nXAgbnFnGdFxrtA7vNXiePjNUASuoYW-lEuQGx9WMGs,468
|
|
13
|
-
spaceforge/test_generator.py,sha256=
|
|
13
|
+
spaceforge/test_generator.py,sha256=sEDV7ShT2BpLoW4zC8ZT-P-V4_dP8kOI0WuiZ6uVIgc,32625
|
|
14
14
|
spaceforge/test_generator_binaries.py,sha256=X_7pPLGE45eQt-Kv9_ku__LsyLgOvViHc_BvVpSCMp0,7263
|
|
15
15
|
spaceforge/test_generator_core.py,sha256=gOqRx0rnME-srGMHun4KidXMN-iaqfKKTyoQ0Tw6b9Q,6253
|
|
16
16
|
spaceforge/test_generator_hooks.py,sha256=2lJs8dYlFb7QehWcYF0O4qg38s5UudEpzJyBi1XiS3k,2542
|
|
@@ -24,10 +24,10 @@ spaceforge/test_runner_cli.py,sha256=qkklvRHETCmgKaVxPfUR_IBy7Vne5hKupAOK9igKqLA
|
|
|
24
24
|
spaceforge/test_runner_core.py,sha256=eNR9YOwJwv7LsMtNQ4WXXMPIW6RE_A7hUp4bCpzz1Rk,3941
|
|
25
25
|
spaceforge/test_runner_execution.py,sha256=GJhoECdhIY2M3MWcmTrIYfkJd2P5n86zixO3FY38_CQ,5344
|
|
26
26
|
spaceforge/templates/binary_install.sh.j2,sha256=I3setUsp6fCUJLkynvfZT4DCqVsoEQ42rhSmK6MJVAY,1517
|
|
27
|
-
spaceforge/templates/ensure_spaceforge_and_run.sh.j2,sha256=
|
|
28
|
-
spaceforge-1.1.
|
|
29
|
-
spaceforge-1.1.
|
|
30
|
-
spaceforge-1.1.
|
|
31
|
-
spaceforge-1.1.
|
|
32
|
-
spaceforge-1.1.
|
|
33
|
-
spaceforge-1.1.
|
|
27
|
+
spaceforge/templates/ensure_spaceforge_and_run.sh.j2,sha256=OKrpZI-M1VU5rCdu7XMbBto7R-HV9lAImc3x3112vhY,580
|
|
28
|
+
spaceforge-1.1.6.dist-info/licenses/LICENSE,sha256=qtl16T_VToz1-IpjGKSLCHsy_zmCFg2H5SkvJi85C4c,1065
|
|
29
|
+
spaceforge-1.1.6.dist-info/METADATA,sha256=nw4so47bK38vLSQZU_4Kl7lJT9Mjl6rOuc91Eb4LD7s,2307
|
|
30
|
+
spaceforge-1.1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
31
|
+
spaceforge-1.1.6.dist-info/entry_points.txt,sha256=qawuuKBSNTGg-njnQnhxxFldFvXYAPej6bF_f3iyQ48,56
|
|
32
|
+
spaceforge-1.1.6.dist-info/top_level.txt,sha256=eVw-Lw4Th0oHM8Gx1Y8YetyNgbNbMBU00yWs-kwGeSs,11
|
|
33
|
+
spaceforge-1.1.6.dist-info/RECORD,,
|
|
@@ -1,736 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: spaceforge
|
|
3
|
-
Version: 1.1.5
|
|
4
|
-
Summary: A Python framework for building Spacelift plugins
|
|
5
|
-
Author-email: Spacelift <support@spacelift.io>
|
|
6
|
-
Maintainer-email: Spacelift <support@spacelift.io>
|
|
7
|
-
License: MIT
|
|
8
|
-
Project-URL: Homepage, https://github.com/spacelift-io/plugins
|
|
9
|
-
Project-URL: Documentation, https://github.com/spacelift-io/plugins#readme
|
|
10
|
-
Project-URL: Repository, https://github.com/spacelift-io/plugins
|
|
11
|
-
Project-URL: Bug Reports, https://github.com/spacelift-io/plugins/issues
|
|
12
|
-
Keywords: spacelift,plugin,framework,infrastructure,devops,spaceforge
|
|
13
|
-
Classifier: Development Status :: 3 - Alpha
|
|
14
|
-
Classifier: Intended Audience :: Developers
|
|
15
|
-
Classifier: Operating System :: OS Independent
|
|
16
|
-
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
-
Classifier: Topic :: System :: Systems Administration
|
|
23
|
-
Requires-Python: >=3.9
|
|
24
|
-
Description-Content-Type: text/markdown
|
|
25
|
-
License-File: LICENSE
|
|
26
|
-
Requires-Dist: PyYAML>=6.0
|
|
27
|
-
Requires-Dist: click>=8.0.0
|
|
28
|
-
Requires-Dist: pydantic>=2.11.7
|
|
29
|
-
Requires-Dist: Jinja2>=3.1.0
|
|
30
|
-
Requires-Dist: mergedeep>=1.3.4
|
|
31
|
-
Provides-Extra: dev
|
|
32
|
-
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
33
|
-
Requires-Dist: pytest-cov; extra == "dev"
|
|
34
|
-
Requires-Dist: black; extra == "dev"
|
|
35
|
-
Requires-Dist: isort; extra == "dev"
|
|
36
|
-
Requires-Dist: mypy; extra == "dev"
|
|
37
|
-
Requires-Dist: types-PyYAML; extra == "dev"
|
|
38
|
-
Requires-Dist: setuptools-scm[toml]>=6.2; extra == "dev"
|
|
39
|
-
Requires-Dist: autoflake; extra == "dev"
|
|
40
|
-
Dynamic: license-file
|
|
41
|
-
|
|
42
|
-
# Spaceforge - Build Spacelift Plugins in Python
|
|
43
|
-
|
|
44
|
-
Spaceforge is a Python framework that makes it easy to build powerful Spacelift plugins using a declarative, hook-based approach. Define your plugin logic in Python, and spaceforge automatically generates the plugin manifest for Spacelift.
|
|
45
|
-
|
|
46
|
-
## Installation
|
|
47
|
-
|
|
48
|
-
Install spaceforge from PyPI:
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
pip install spaceforge
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Quick Start
|
|
55
|
-
|
|
56
|
-
### 1. Create Your Plugin
|
|
57
|
-
|
|
58
|
-
Create a Python file (e.g., `plugin.py`) and inherit from `SpaceforgePlugin`:
|
|
59
|
-
|
|
60
|
-
```python
|
|
61
|
-
from spaceforge import SpaceforgePlugin, Parameter, Variable, Context
|
|
62
|
-
import os
|
|
63
|
-
|
|
64
|
-
class MyPlugin(SpaceforgePlugin):
|
|
65
|
-
# Plugin metadata
|
|
66
|
-
__plugin_name__ = "my-plugin"
|
|
67
|
-
__version__ = "1.0.0"
|
|
68
|
-
__author__ = "Your Name"
|
|
69
|
-
__labels__ = ["security", "monitoring"] # Optional labels for categorization
|
|
70
|
-
|
|
71
|
-
# Define plugin parameters
|
|
72
|
-
__parameters__ = [
|
|
73
|
-
Parameter(
|
|
74
|
-
name="API Key",
|
|
75
|
-
id="api_key", # Optional ID for parameter reference
|
|
76
|
-
description="API key for external service",
|
|
77
|
-
required=True,
|
|
78
|
-
sensitive=True
|
|
79
|
-
),
|
|
80
|
-
Parameter(
|
|
81
|
-
name="Environment",
|
|
82
|
-
id="environment",
|
|
83
|
-
description="Target environment",
|
|
84
|
-
required=False,
|
|
85
|
-
default="production"
|
|
86
|
-
)
|
|
87
|
-
]
|
|
88
|
-
|
|
89
|
-
# Define Spacelift contexts
|
|
90
|
-
__contexts__ = [
|
|
91
|
-
Context(
|
|
92
|
-
name_prefix="my-plugin",
|
|
93
|
-
description="Main plugin context",
|
|
94
|
-
env=[
|
|
95
|
-
Variable(
|
|
96
|
-
key="API_KEY",
|
|
97
|
-
value_from_parameter="api_key", # Matches parameter id or name
|
|
98
|
-
sensitive=True
|
|
99
|
-
),
|
|
100
|
-
Variable(
|
|
101
|
-
key="ENVIRONMENT",
|
|
102
|
-
value_from_parameter="environment" # Matches parameter id or name
|
|
103
|
-
)
|
|
104
|
-
]
|
|
105
|
-
)
|
|
106
|
-
]
|
|
107
|
-
|
|
108
|
-
def after_plan(self):
|
|
109
|
-
"""Run security checks after Terraform plan"""
|
|
110
|
-
# Run external commands
|
|
111
|
-
return_code, stdout, stderr = self.run_cli("my-security-tool", "--scan", "./", '--api', os.environ["API_KEY"])
|
|
112
|
-
|
|
113
|
-
if return_code != 0:
|
|
114
|
-
self.logger.error("Security scan failed!")
|
|
115
|
-
exit(1)
|
|
116
|
-
|
|
117
|
-
self.logger.info("Security scan passed!")
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### 2. Generate Plugin Manifest
|
|
121
|
-
|
|
122
|
-
Generate the Spacelift plugin YAML manifest:
|
|
123
|
-
|
|
124
|
-
```bash
|
|
125
|
-
spaceforge generate plugin.py
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
This creates `plugin.yaml` that you can upload to Spacelift.
|
|
129
|
-
|
|
130
|
-
### 3. Test Your Plugin
|
|
131
|
-
|
|
132
|
-
Test individual hooks locally:
|
|
133
|
-
|
|
134
|
-
```bash
|
|
135
|
-
# Set parameter values
|
|
136
|
-
export API_KEY="your-api-key"
|
|
137
|
-
export ENVIRONMENT="staging"
|
|
138
|
-
|
|
139
|
-
# Test the after_plan hook
|
|
140
|
-
spaceforge run after_plan
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
## Available Hooks
|
|
144
|
-
|
|
145
|
-
Override these methods in your plugin to add custom logic:
|
|
146
|
-
|
|
147
|
-
- `before_init()` - Before Terraform init
|
|
148
|
-
- `after_init()` - After Terraform init
|
|
149
|
-
- `before_plan()` - Before Terraform plan
|
|
150
|
-
- `after_plan()` - After Terraform plan
|
|
151
|
-
- `before_apply()` - Before Terraform apply
|
|
152
|
-
- `after_apply()` - After Terraform apply
|
|
153
|
-
- `before_perform()` - Before the run performs
|
|
154
|
-
- `after_perform()` - After the run performs
|
|
155
|
-
- `before_destroy()` - Before Terraform destroy
|
|
156
|
-
- `after_destroy()` - After Terraform destroy
|
|
157
|
-
- `after_run()` - After the run completes
|
|
158
|
-
|
|
159
|
-
## Plugin Components
|
|
160
|
-
|
|
161
|
-
### Labels
|
|
162
|
-
|
|
163
|
-
Add optional labels to categorize your plugin:
|
|
164
|
-
|
|
165
|
-
```python
|
|
166
|
-
class MyPlugin(SpaceforgePlugin):
|
|
167
|
-
__labels__ = ["security", "monitoring", "compliance"]
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
### Parameters
|
|
171
|
-
|
|
172
|
-
Define user-configurable parameters:
|
|
173
|
-
|
|
174
|
-
```python
|
|
175
|
-
__parameters__ = [
|
|
176
|
-
Parameter(
|
|
177
|
-
name="Database URL",
|
|
178
|
-
id="database_url", # Optional: used for parameter reference
|
|
179
|
-
description="Database connection URL",
|
|
180
|
-
required=True,
|
|
181
|
-
sensitive=True
|
|
182
|
-
),
|
|
183
|
-
Parameter(
|
|
184
|
-
name="Timeout",
|
|
185
|
-
id="timeout",
|
|
186
|
-
description="Timeout in seconds",
|
|
187
|
-
required=False,
|
|
188
|
-
default="30" # Default values should be strings
|
|
189
|
-
)
|
|
190
|
-
]
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
**Parameter Notes:**
|
|
194
|
-
- Parameter `name` is displayed in the Spacelift UI
|
|
195
|
-
- Parameter `id` (optional) is used for programmatic reference
|
|
196
|
-
- `value_from_parameter` can reference either the `id` (if present) or the `name`
|
|
197
|
-
- Parameters are made available as environment variables through Variable definitions
|
|
198
|
-
- Default values must be strings
|
|
199
|
-
- Required parameters cannot have default values
|
|
200
|
-
|
|
201
|
-
### Contexts
|
|
202
|
-
|
|
203
|
-
Define Spacelift contexts with environment variables and custom hooks:
|
|
204
|
-
|
|
205
|
-
```python
|
|
206
|
-
__contexts__ = [
|
|
207
|
-
Context(
|
|
208
|
-
name_prefix="production",
|
|
209
|
-
description="Production environment context",
|
|
210
|
-
labels=["env:prod"],
|
|
211
|
-
env=[
|
|
212
|
-
Variable(
|
|
213
|
-
key="DATABASE_URL",
|
|
214
|
-
value_from_parameter="database_url", # Matches parameter id
|
|
215
|
-
sensitive=True
|
|
216
|
-
),
|
|
217
|
-
Variable(
|
|
218
|
-
key="API_ENDPOINT",
|
|
219
|
-
value="https://api.prod.example.com"
|
|
220
|
-
)
|
|
221
|
-
],
|
|
222
|
-
hooks={
|
|
223
|
-
"before_apply": [
|
|
224
|
-
"echo 'Starting production deployment'",
|
|
225
|
-
"kubectl get pods"
|
|
226
|
-
]
|
|
227
|
-
}
|
|
228
|
-
)
|
|
229
|
-
]
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
### Binaries
|
|
233
|
-
|
|
234
|
-
Automatically download and install external tools:
|
|
235
|
-
|
|
236
|
-
```python
|
|
237
|
-
__binaries__ = [
|
|
238
|
-
Binary(
|
|
239
|
-
name="kubectl",
|
|
240
|
-
download_urls={
|
|
241
|
-
"amd64": "https://dl.k8s.io/release/v1.28.0/bin/linux/amd64/kubectl",
|
|
242
|
-
"arm64": "https://dl.k8s.io/release/v1.28.0/bin/linux/arm64/kubectl"
|
|
243
|
-
}
|
|
244
|
-
)
|
|
245
|
-
]
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
**Context Priority System:**
|
|
249
|
-
|
|
250
|
-
Control the execution order of contexts using the `priority` field:
|
|
251
|
-
|
|
252
|
-
```python
|
|
253
|
-
__contexts__ = [
|
|
254
|
-
Context(
|
|
255
|
-
name_prefix="setup",
|
|
256
|
-
description="Setup context (runs first)",
|
|
257
|
-
priority=0, # Lower numbers run first
|
|
258
|
-
hooks={
|
|
259
|
-
"before_init": ["echo 'Setting up environment'"]
|
|
260
|
-
}
|
|
261
|
-
),
|
|
262
|
-
Context(
|
|
263
|
-
name_prefix="main",
|
|
264
|
-
description="Main context (runs second)",
|
|
265
|
-
priority=1, # Higher numbers run after lower ones
|
|
266
|
-
hooks={
|
|
267
|
-
"before_init": ["echo 'Main execution'"]
|
|
268
|
-
}
|
|
269
|
-
)
|
|
270
|
-
]
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
**Priority Notes:**
|
|
274
|
-
- Default priority is `0`
|
|
275
|
-
- Lower numbers execute first (0, then 1, then 2, etc.)
|
|
276
|
-
- Useful for ensuring setup contexts run before main execution contexts
|
|
277
|
-
|
|
278
|
-
**Binary PATH Management:**
|
|
279
|
-
- When using Python hook methods (e.g., `def before_apply()`), binaries are automatically available in PATH
|
|
280
|
-
- When using raw context hooks, you must manually export the PATH:
|
|
281
|
-
|
|
282
|
-
```python
|
|
283
|
-
__contexts__ = [
|
|
284
|
-
Context(
|
|
285
|
-
name_prefix="kubectl-setup",
|
|
286
|
-
description="Setup kubectl binary for raw hooks",
|
|
287
|
-
hooks={
|
|
288
|
-
"before_init": [
|
|
289
|
-
'export PATH="/mnt/workspace/plugins/plugin_binaries:$PATH"',
|
|
290
|
-
"kubectl version"
|
|
291
|
-
]
|
|
292
|
-
}
|
|
293
|
-
)
|
|
294
|
-
]
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
### Mounted Files
|
|
298
|
-
|
|
299
|
-
Mount file content directly into contexts:
|
|
300
|
-
|
|
301
|
-
```python
|
|
302
|
-
from spaceforge import MountedFile
|
|
303
|
-
|
|
304
|
-
__contexts__ = [
|
|
305
|
-
Context(
|
|
306
|
-
name_prefix="config",
|
|
307
|
-
description="Context with mounted configuration files",
|
|
308
|
-
mounted_files=[
|
|
309
|
-
MountedFile(
|
|
310
|
-
path="tmp/config.json",
|
|
311
|
-
content='{"environment": "production", "debug": false}',
|
|
312
|
-
sensitive=False
|
|
313
|
-
),
|
|
314
|
-
MountedFile(
|
|
315
|
-
path="tmp/secret-config.yaml",
|
|
316
|
-
content="api_key: secret-value\nendpoint: https://api.example.com",
|
|
317
|
-
sensitive=True # Marks content as sensitive
|
|
318
|
-
)
|
|
319
|
-
]
|
|
320
|
-
)
|
|
321
|
-
]
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
**MountedFile Notes:**
|
|
325
|
-
- Files are created at the specified path when the context is applied
|
|
326
|
-
- Content is written exactly as provided
|
|
327
|
-
- Use `sensitive=True` for files containing secrets or sensitive data
|
|
328
|
-
- path is from `/mnt/workspace/`. An example would be `tmp/config.json` which would be mounted at `/mnt/workspace/tmp/config.json`
|
|
329
|
-
|
|
330
|
-
### Policies
|
|
331
|
-
|
|
332
|
-
Define OPA policies for your plugin:
|
|
333
|
-
|
|
334
|
-
```python
|
|
335
|
-
__policies__ = [
|
|
336
|
-
Policy(
|
|
337
|
-
name_prefix="security-check",
|
|
338
|
-
type="NOTIFICATION",
|
|
339
|
-
body="""
|
|
340
|
-
package spacelift
|
|
341
|
-
|
|
342
|
-
webhook[{"endpoint_id": "security-alerts"}] {
|
|
343
|
-
input.run_updated.run.marked_unsafe == true
|
|
344
|
-
}
|
|
345
|
-
""",
|
|
346
|
-
labels=["security"]
|
|
347
|
-
)
|
|
348
|
-
]
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
### Webhooks
|
|
352
|
-
|
|
353
|
-
Define webhooks to trigger external actions:
|
|
354
|
-
|
|
355
|
-
```python
|
|
356
|
-
__webhooks__ = [
|
|
357
|
-
Webhook(
|
|
358
|
-
name_prefix="security-alerts",
|
|
359
|
-
endpoint="https://alerts.example.com/webhook",
|
|
360
|
-
secretFromParameter="webhook_secret", # Parameter id/name for webhook secret
|
|
361
|
-
labels=["security"]
|
|
362
|
-
)
|
|
363
|
-
]
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
## Plugin Features
|
|
367
|
-
|
|
368
|
-
### Logging
|
|
369
|
-
|
|
370
|
-
Built-in structured logging with run context:
|
|
371
|
-
|
|
372
|
-
```python
|
|
373
|
-
def after_plan(self):
|
|
374
|
-
self.logger.info("Starting security scan")
|
|
375
|
-
self.logger.debug("Debug info (only shown when SPACELIFT_DEBUG=true)")
|
|
376
|
-
self.logger.warning("Warning message")
|
|
377
|
-
self.logger.error("Error occurred")
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
### CLI Execution
|
|
381
|
-
|
|
382
|
-
Run external commands with automatic logging:
|
|
383
|
-
|
|
384
|
-
```python
|
|
385
|
-
def before_apply(self):
|
|
386
|
-
# Run command with automatic output capture
|
|
387
|
-
return_code, stdout, stderr = self.run_cli("terraform", "validate")
|
|
388
|
-
|
|
389
|
-
if return_code != 0:
|
|
390
|
-
self.logger.error("Terraform validation failed")
|
|
391
|
-
exit(1)
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
### Spacelift API Integration
|
|
395
|
-
|
|
396
|
-
Query the Spacelift GraphQL API (requires `SPACELIFT_API_TOKEN` and `TF_VAR_spacelift_graphql_endpoint`):
|
|
397
|
-
|
|
398
|
-
```python
|
|
399
|
-
def after_plan(self):
|
|
400
|
-
result = self.query_api("""
|
|
401
|
-
query {
|
|
402
|
-
stack(id: "my-stack-id") {
|
|
403
|
-
name
|
|
404
|
-
state
|
|
405
|
-
latestRun {
|
|
406
|
-
id
|
|
407
|
-
state
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
""")
|
|
412
|
-
|
|
413
|
-
self.logger.info(f"Stack state: {result['stack']['state']}")
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
### User Token Authentication
|
|
417
|
-
|
|
418
|
-
Use user API tokens instead of service tokens for Spacelift API access. This is useful because the token on the run may not have sufficient permissions for certain operations.
|
|
419
|
-
|
|
420
|
-
```python
|
|
421
|
-
def before_plan(self):
|
|
422
|
-
# Use user API token for authentication
|
|
423
|
-
user_id = os.environ.get('SPACELIFT_USER_ID')
|
|
424
|
-
user_secret = os.environ.get('SPACELIFT_USER_SECRET')
|
|
425
|
-
|
|
426
|
-
if user_id and user_secret:
|
|
427
|
-
self.use_user_token(user_id, user_secret)
|
|
428
|
-
|
|
429
|
-
# Now you can use the API with user permissions
|
|
430
|
-
result = self.query_api("""
|
|
431
|
-
query {
|
|
432
|
-
viewer {
|
|
433
|
-
id
|
|
434
|
-
login
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
""")
|
|
438
|
-
|
|
439
|
-
self.logger.info(f"Authenticated as: {result['viewer']['login']}")
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
**User Token Notes:**
|
|
443
|
-
- Allows plugins to act on behalf of a specific user
|
|
444
|
-
- Useful for operations requiring user-specific permissions
|
|
445
|
-
- User tokens may have different access levels than service tokens
|
|
446
|
-
- Call `use_user_token()` before making API requests
|
|
447
|
-
|
|
448
|
-
### Access Plan and State
|
|
449
|
-
|
|
450
|
-
Access Terraform plan and state data:
|
|
451
|
-
|
|
452
|
-
```python
|
|
453
|
-
def after_plan(self):
|
|
454
|
-
# Get the current plan
|
|
455
|
-
plan = self.get_plan_json()
|
|
456
|
-
|
|
457
|
-
# Get the state before changes
|
|
458
|
-
state = self.get_state_before_json()
|
|
459
|
-
|
|
460
|
-
# Analyze planned changes
|
|
461
|
-
resource_count = len(plan.get('planned_values', {}).get('root_module', {}).get('resources', []))
|
|
462
|
-
self.logger.info(f"Planning to manage {resource_count} resources")
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
### Send Rich Output
|
|
466
|
-
|
|
467
|
-
Send formatted markdown to the Spacelift UI:
|
|
468
|
-
|
|
469
|
-
```python
|
|
470
|
-
def after_plan(self):
|
|
471
|
-
markdown = """
|
|
472
|
-
# Security Scan Results
|
|
473
|
-
|
|
474
|
-
✅ **Passed:** 45 checks
|
|
475
|
-
⚠️ **Warnings:** 3 issues
|
|
476
|
-
❌ **Failed:** 0 critical issues
|
|
477
|
-
|
|
478
|
-
[View detailed report](https://security.example.com/reports/123)
|
|
479
|
-
"""
|
|
480
|
-
|
|
481
|
-
self.send_markdown(markdown)
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
### Add to Policy Input
|
|
485
|
-
|
|
486
|
-
Add custom data to the OPA policy input:
|
|
487
|
-
|
|
488
|
-
The following example will create input available via `input.third_party_metadata.custom.my_custom_data` in your OPA policies:
|
|
489
|
-
```python
|
|
490
|
-
def after_plan(self):
|
|
491
|
-
self.add_to_policy_input("my_custom_data", {
|
|
492
|
-
"scan_results": {
|
|
493
|
-
"passed": True,
|
|
494
|
-
"issues": []
|
|
495
|
-
}
|
|
496
|
-
})
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
## CLI Commands
|
|
500
|
-
|
|
501
|
-
### Generate Plugin Manifest
|
|
502
|
-
|
|
503
|
-
```bash
|
|
504
|
-
# Generate from plugin.py (default filename)
|
|
505
|
-
spaceforge generate
|
|
506
|
-
|
|
507
|
-
# Generate from specific file
|
|
508
|
-
spaceforge generate my_plugin.py
|
|
509
|
-
|
|
510
|
-
# Specify output file
|
|
511
|
-
spaceforge generate my_plugin.py -o custom-output.yaml
|
|
512
|
-
|
|
513
|
-
# Get help
|
|
514
|
-
spaceforge generate --help
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
### Test Plugin Hooks
|
|
518
|
-
|
|
519
|
-
```bash
|
|
520
|
-
# Set parameters for local testing (parameters are normally provided by Spacelift)
|
|
521
|
-
export API_KEY="test-key"
|
|
522
|
-
export TIMEOUT="60"
|
|
523
|
-
|
|
524
|
-
# Test specific hook
|
|
525
|
-
spaceforge run after_plan
|
|
526
|
-
|
|
527
|
-
# Test with specific plugin file
|
|
528
|
-
spaceforge run --plugin-file my_plugin.py before_apply
|
|
529
|
-
|
|
530
|
-
# Get help
|
|
531
|
-
spaceforge run --help
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
## Plugin Development Tips
|
|
535
|
-
|
|
536
|
-
### 1. Handle Dependencies
|
|
537
|
-
|
|
538
|
-
If your plugin needs Python packages, create a `requirements.txt` file. Spaceforge automatically adds a `before_init` hook to install them:
|
|
539
|
-
|
|
540
|
-
```txt
|
|
541
|
-
requests>=2.28.0
|
|
542
|
-
pydantic>=1.10.0
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
### 2. Environment Variables
|
|
546
|
-
|
|
547
|
-
Access Spacelift environment variables in your hooks:
|
|
548
|
-
|
|
549
|
-
```python
|
|
550
|
-
def after_plan(self):
|
|
551
|
-
run_id = os.environ.get('TF_VAR_spacelift_run_id')
|
|
552
|
-
stack_id = os.environ.get('TF_VAR_spacelift_stack_id')
|
|
553
|
-
self.logger.info(f"Processing run {run_id} for stack {stack_id}")
|
|
554
|
-
```
|
|
555
|
-
|
|
556
|
-
### 3. Error Handling
|
|
557
|
-
|
|
558
|
-
Always handle errors gracefully:
|
|
559
|
-
|
|
560
|
-
```python
|
|
561
|
-
def after_plan(self):
|
|
562
|
-
try:
|
|
563
|
-
# Your plugin logic here
|
|
564
|
-
result = self.run_external_service()
|
|
565
|
-
|
|
566
|
-
except Exception as e:
|
|
567
|
-
self.logger.error(f"Plugin failed: {str(e)}")
|
|
568
|
-
# Exit with non-zero code to fail the run
|
|
569
|
-
exit(1)
|
|
570
|
-
```
|
|
571
|
-
|
|
572
|
-
### 4. Testing and Debugging
|
|
573
|
-
|
|
574
|
-
- Set `SPACELIFT_DEBUG=true` to enable debug logging
|
|
575
|
-
- Use the `run` command to test hooks during development
|
|
576
|
-
- Test with different parameter combinations
|
|
577
|
-
- Validate your generated YAML before uploading to Spacelift
|
|
578
|
-
|
|
579
|
-
## Example: Security Scanning Plugin
|
|
580
|
-
|
|
581
|
-
Here's a complete example of a security scanning plugin:
|
|
582
|
-
|
|
583
|
-
```python
|
|
584
|
-
import os
|
|
585
|
-
import json
|
|
586
|
-
from spaceforge import SpaceforgePlugin, Parameter, Variable, Context, Binary, Policy, MountedFile
|
|
587
|
-
|
|
588
|
-
class SecurityScannerPlugin(SpaceforgePlugin):
|
|
589
|
-
__plugin_name__ = "security-scanner"
|
|
590
|
-
__version__ = "1.0.0"
|
|
591
|
-
__author__ = "Security Team"
|
|
592
|
-
|
|
593
|
-
__binaries__ = [
|
|
594
|
-
Binary(
|
|
595
|
-
name="security-cli",
|
|
596
|
-
download_urls={
|
|
597
|
-
"amd64": "https://releases.example.com/security-cli-linux-amd64",
|
|
598
|
-
"arm64": "https://releases.example.com/security-cli-linux-arm64"
|
|
599
|
-
}
|
|
600
|
-
)
|
|
601
|
-
]
|
|
602
|
-
|
|
603
|
-
__parameters__ = [
|
|
604
|
-
Parameter(
|
|
605
|
-
name="API Token",
|
|
606
|
-
id="api_token",
|
|
607
|
-
description="Security service API token",
|
|
608
|
-
required=True,
|
|
609
|
-
sensitive=True
|
|
610
|
-
),
|
|
611
|
-
Parameter(
|
|
612
|
-
name="Severity Threshold",
|
|
613
|
-
id="severity_threshold",
|
|
614
|
-
description="Minimum severity level to report",
|
|
615
|
-
required=False,
|
|
616
|
-
default="medium"
|
|
617
|
-
)
|
|
618
|
-
]
|
|
619
|
-
|
|
620
|
-
__contexts__ = [
|
|
621
|
-
Context(
|
|
622
|
-
name_prefix="security-scanner",
|
|
623
|
-
description="Security scanning context",
|
|
624
|
-
env=[
|
|
625
|
-
Variable(
|
|
626
|
-
key="SECURITY_API_TOKEN",
|
|
627
|
-
value_from_parameter="api_token",
|
|
628
|
-
sensitive=True
|
|
629
|
-
),
|
|
630
|
-
Variable(
|
|
631
|
-
key="SEVERITY_THRESHOLD",
|
|
632
|
-
value_from_parameter="severity_threshold"
|
|
633
|
-
)
|
|
634
|
-
]
|
|
635
|
-
)
|
|
636
|
-
]
|
|
637
|
-
|
|
638
|
-
def after_plan(self):
|
|
639
|
-
"""Run security scan after Terraform plan"""
|
|
640
|
-
self.logger.info("Starting security scan of Terraform plan")
|
|
641
|
-
|
|
642
|
-
# Authenticate with security service
|
|
643
|
-
return_code, stdout, stderr = self.run_cli(
|
|
644
|
-
"security-cli", "auth",
|
|
645
|
-
"--token", os.environ["SECURITY_API_TOKEN"]
|
|
646
|
-
)
|
|
647
|
-
|
|
648
|
-
if return_code != 0:
|
|
649
|
-
self.logger.error("Failed to authenticate with security service")
|
|
650
|
-
exit(1)
|
|
651
|
-
|
|
652
|
-
# Scan the Terraform plan
|
|
653
|
-
return_code, stdout, stderr = self.run_cli(
|
|
654
|
-
"security-cli", "scan", "terraform",
|
|
655
|
-
"--plan-file", "spacelift.plan.json",
|
|
656
|
-
"--format", "json",
|
|
657
|
-
"--severity", os.environ.get("SEVERITY_THRESHOLD", "medium"),
|
|
658
|
-
print_output=False
|
|
659
|
-
)
|
|
660
|
-
|
|
661
|
-
if return_code != 0:
|
|
662
|
-
self.logger.error("Security scan failed")
|
|
663
|
-
for line in stderr:
|
|
664
|
-
self.logger.error(line)
|
|
665
|
-
exit(1)
|
|
666
|
-
|
|
667
|
-
# Parse scan results
|
|
668
|
-
try:
|
|
669
|
-
results = json.loads('\n'.join(stdout))
|
|
670
|
-
|
|
671
|
-
# Generate markdown report
|
|
672
|
-
markdown = self._generate_report(results)
|
|
673
|
-
self.send_markdown(markdown)
|
|
674
|
-
|
|
675
|
-
# Fail run if critical issues found
|
|
676
|
-
if results.get('critical_count', 0) > 0:
|
|
677
|
-
self.logger.error(f"Found {results['critical_count']} critical security issues")
|
|
678
|
-
exit(1)
|
|
679
|
-
|
|
680
|
-
self.logger.info("Security scan completed successfully")
|
|
681
|
-
|
|
682
|
-
except json.JSONDecodeError:
|
|
683
|
-
self.logger.error("Failed to parse scan results")
|
|
684
|
-
exit(1)
|
|
685
|
-
|
|
686
|
-
def _generate_report(self, results):
|
|
687
|
-
"""Generate markdown report from scan results"""
|
|
688
|
-
report = "# Security Scan Results\n\n"
|
|
689
|
-
|
|
690
|
-
if results.get('total_issues', 0) == 0:
|
|
691
|
-
report += "✅ **No security issues found!**\n"
|
|
692
|
-
else:
|
|
693
|
-
report += f"Found {results['total_issues']} security issues:\n\n"
|
|
694
|
-
|
|
695
|
-
for severity in ['critical', 'high', 'medium', 'low']:
|
|
696
|
-
count = results.get(f'{severity}_count', 0)
|
|
697
|
-
if count > 0:
|
|
698
|
-
emoji = {'critical': '🔴', 'high': '🟠', 'medium': '🟡', 'low': '🟢'}[severity]
|
|
699
|
-
report += f"- {emoji} **{severity.upper()}:** {count}\n"
|
|
700
|
-
|
|
701
|
-
if results.get('report_url'):
|
|
702
|
-
report += f"\n[View detailed report]({results['report_url']})\n"
|
|
703
|
-
|
|
704
|
-
return report
|
|
705
|
-
```
|
|
706
|
-
|
|
707
|
-
Generate and test this plugin:
|
|
708
|
-
|
|
709
|
-
```bash
|
|
710
|
-
# Generate the manifest
|
|
711
|
-
spaceforge generate security_scanner.py
|
|
712
|
-
|
|
713
|
-
# Test locally
|
|
714
|
-
export API_TOKEN="your-token"
|
|
715
|
-
export SEVERITY_THRESHOLD="high"
|
|
716
|
-
spaceforge run after_plan
|
|
717
|
-
```
|
|
718
|
-
|
|
719
|
-
## Speeding up plugin execution
|
|
720
|
-
|
|
721
|
-
There are a few things you can do to speed up plugin execution.
|
|
722
|
-
|
|
723
|
-
1. Ensure your runner has `spaceforge` preinstalled. This will avoid the overhead of installing it during the run. (15-30 seconds)
|
|
724
|
-
2. If youre using binaries, we will only install the binary if its not found. You can gain a few seconds by ensuring its already on the runner.
|
|
725
|
-
3. If your plugin has a lot of dependencies, consider using a prebuilt runner image with your plugin and its dependencies installed. This avoids the overhead of installing them during each run.
|
|
726
|
-
4. Ensure your runner has enough core resources (CPU, memory) to handle the plugin execution efficiently. If your plugin is resource-intensive, consider using a more powerful runner.
|
|
727
|
-
|
|
728
|
-
## Next Steps
|
|
729
|
-
|
|
730
|
-
1. **Install spaceforge:** `pip install spaceforge`
|
|
731
|
-
2. **Create your plugin:** Start with the quick start example
|
|
732
|
-
3. **Test locally:** Use the `run` command to test your hooks
|
|
733
|
-
4. **Generate manifest:** Use the `generate` command to create plugin.yaml
|
|
734
|
-
5. **Upload to Spacelift:** Add your plugin manifest to your Spacelift account
|
|
735
|
-
|
|
736
|
-
For more advanced examples, see the [plugins](plugins/) directory in this repository.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|