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