spaceforge 1.1.5__py3-none-any.whl → 1.1.7__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.
@@ -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.5'
32
- __version_tuple__ = version_tuple = (1, 1, 5)
31
+ __version__ = version = '1.1.7'
32
+ __version_tuple__ = version_tuple = (1, 1, 7)
33
33
 
34
34
  __commit_id__ = commit_id = None
spaceforge/generator.py CHANGED
@@ -4,6 +4,7 @@ YAML generator for Spacelift plugins.
4
4
 
5
5
  import importlib.util
6
6
  import os
7
+ import textwrap
7
8
  from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union
8
9
 
9
10
  import yaml
@@ -137,7 +138,7 @@ class PluginGenerator:
137
138
 
138
139
  doc = getattr(self.plugin_class, "__doc__", "")
139
140
  if doc:
140
- doc = doc.strip()
141
+ doc = textwrap.dedent(doc).strip()
141
142
 
142
143
  metadata = {
143
144
  "name_prefix": getattr(
@@ -198,9 +199,13 @@ class PluginGenerator:
198
199
 
199
200
  def _update_with_requirements(self, mounted_files: List[MountedFile]) -> None:
200
201
  """Update the plugin hooks if there is a requirements.txt"""
201
- if os.path.exists("requirements.txt") and self.config is not None:
202
+ # Look for requirements.txt in the same directory as the plugin file
203
+ plugin_dir = os.path.dirname(self.plugin_path)
204
+ requirements_path = os.path.join(plugin_dir, "requirements.txt")
205
+
206
+ if os.path.exists(requirements_path) and self.config is not None:
202
207
  # read the requirements.txt file
203
- with open("requirements.txt", "r") as f:
208
+ with open(requirements_path, "r") as f:
204
209
  mounted_files.append(
205
210
  MountedFile(
206
211
  path=f"{self.plugin_working_directory}/requirements.txt",
@@ -412,7 +417,8 @@ class PluginGenerator:
412
417
  def represent_str(self, data: str) -> Any:
413
418
  """Override string representation for multiline strings."""
414
419
  if data.count("\n") > 0:
415
- data = data.strip()
420
+ # Use dedent to remove common leading whitespace from all lines
421
+ data = textwrap.dedent(data).strip()
416
422
  data = "\n".join([line.rstrip() for line in data.splitlines()])
417
423
  return self.represent_scalar(
418
424
  "tag:yaml.org,2002:str", data, style="|"
@@ -14,7 +14,7 @@ mkdir -p {{static_binary_directory}}
14
14
  ARCH="$(arch)"
15
15
  if [ "$ARCH" = "x86_64" ]; then
16
16
  URL="{{amd64_url}}"
17
- elif [ "$ARCH" = "arm64" ]; then
17
+ elif [ "$ARCH" = "arm64" ] || [ "$ARCH" = "aarch64" ]; then
18
18
  URL="{{arm64_url}}"
19
19
  else
20
20
  echo "Error: Unsupported architecture '$ARCH'"
@@ -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}}
@@ -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 == "requirements.txt" or "plugin.py" in 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 == "requirements.txt":
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.7
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=SBXhEAFQbfcjLb_eXnqg0cn0I8-y4wDGA3DkPBmBv2s,704
5
+ spaceforge/_version_scm.py,sha256=XScxAJn_hLwoilMyvvJG_0tPX3X-Jhde5mefhI8pVSM,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=hCxtbOKmrGd7HCCz7HMaiK566kIre5MNxcJEx2TVURM,18430
8
+ spaceforge/generator.py,sha256=qOC9YeP9QmwFZQoe0BbvJb8RHtv3BVupYxvu2ceQMkE,18768
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=k2jPEqNEJ9jYiY3xGf59Bo3VxTK6s_jEe3hZ0D8i79U,32611
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
@@ -23,11 +23,11 @@ spaceforge/test_runner.py,sha256=PWFiGsBSJ6QXL5OyGA_OoDIILtRaX1NDQnech15Ady0,177
23
23
  spaceforge/test_runner_cli.py,sha256=qkklvRHETCmgKaVxPfUR_IBy7Vne5hKupAOK9igKqLA,2290
24
24
  spaceforge/test_runner_core.py,sha256=eNR9YOwJwv7LsMtNQ4WXXMPIW6RE_A7hUp4bCpzz1Rk,3941
25
25
  spaceforge/test_runner_execution.py,sha256=GJhoECdhIY2M3MWcmTrIYfkJd2P5n86zixO3FY38_CQ,5344
26
- spaceforge/templates/binary_install.sh.j2,sha256=I3setUsp6fCUJLkynvfZT4DCqVsoEQ42rhSmK6MJVAY,1517
27
- spaceforge/templates/ensure_spaceforge_and_run.sh.j2,sha256=8vq9cvFtNLjgcEr8OPblZ6bQRsOsklOnDOzygnw3We4,547
28
- spaceforge-1.1.5.dist-info/licenses/LICENSE,sha256=wyljRrfnWY2ggQKkSCg3Nw2hxwPMmupopaKs9Kpgys8,1065
29
- spaceforge-1.1.5.dist-info/METADATA,sha256=6a5k8eotU_WQipZXRVuIxytYHcf9cxuGo5AM34hnnQU,20968
30
- spaceforge-1.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
- spaceforge-1.1.5.dist-info/entry_points.txt,sha256=qawuuKBSNTGg-njnQnhxxFldFvXYAPej6bF_f3iyQ48,56
32
- spaceforge-1.1.5.dist-info/top_level.txt,sha256=eVw-Lw4Th0oHM8Gx1Y8YetyNgbNbMBU00yWs-kwGeSs,11
33
- spaceforge-1.1.5.dist-info/RECORD,,
26
+ spaceforge/templates/binary_install.sh.j2,sha256=xyq-ol6z8M4RJ107GAs5cdvudVV45JmhwExHovfCHMI,1544
27
+ spaceforge/templates/ensure_spaceforge_and_run.sh.j2,sha256=OKrpZI-M1VU5rCdu7XMbBto7R-HV9lAImc3x3112vhY,580
28
+ spaceforge-1.1.7.dist-info/licenses/LICENSE,sha256=qtl16T_VToz1-IpjGKSLCHsy_zmCFg2H5SkvJi85C4c,1065
29
+ spaceforge-1.1.7.dist-info/METADATA,sha256=sNVO6Alkg9uDBZXmd0rpr8kZy6P6bnAK81gJ0GzvP-o,2307
30
+ spaceforge-1.1.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
+ spaceforge-1.1.7.dist-info/entry_points.txt,sha256=qawuuKBSNTGg-njnQnhxxFldFvXYAPej6bF_f3iyQ48,56
32
+ spaceforge-1.1.7.dist-info/top_level.txt,sha256=eVw-Lw4Th0oHM8Gx1Y8YetyNgbNbMBU00yWs-kwGeSs,11
33
+ spaceforge-1.1.7.dist-info/RECORD,,
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Spacelift
3
+ Copyright (c) 2025 Spacelift
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -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.