spaceforge 1.0.1__py3-none-any.whl → 1.1.1__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/cls.py +2 -0
- spaceforge/generator.py +64 -18
- spaceforge/plugin.py +0 -30
- spaceforge/schema.json +6 -1
- spaceforge/templates/binary_install.sh.j2 +0 -2
- spaceforge/test_plugin_hooks.py +0 -67
- {spaceforge-1.0.1.dist-info → spaceforge-1.1.1.dist-info}/METADATA +168 -33
- {spaceforge-1.0.1.dist-info → spaceforge-1.1.1.dist-info}/RECORD +13 -13
- {spaceforge-1.0.1.dist-info → spaceforge-1.1.1.dist-info}/WHEEL +0 -0
- {spaceforge-1.0.1.dist-info → spaceforge-1.1.1.dist-info}/entry_points.txt +0 -0
- {spaceforge-1.0.1.dist-info → spaceforge-1.1.1.dist-info}/licenses/LICENSE +0 -0
- {spaceforge-1.0.1.dist-info → spaceforge-1.1.1.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.
|
|
32
|
-
__version_tuple__ = version_tuple = (1,
|
|
31
|
+
__version__ = version = '1.1.1'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 1, 1)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
spaceforge/cls.py
CHANGED
|
@@ -121,6 +121,7 @@ class Context:
|
|
|
121
121
|
labels (Optional[List[str]]): Labels associated with the context.
|
|
122
122
|
env (list): List of variables associated with the context.
|
|
123
123
|
hooks (dict): Hooks associated with the context.
|
|
124
|
+
priority (optional[int]): The priority of the context, contexts with 0 priority run before contexts with 1 priority.
|
|
124
125
|
"""
|
|
125
126
|
|
|
126
127
|
name_prefix: str
|
|
@@ -129,6 +130,7 @@ class Context:
|
|
|
129
130
|
mounted_files: Optional[List[MountedFile]] = optional_field
|
|
130
131
|
hooks: Optional[Dict[HookType, List[str]]] = optional_field
|
|
131
132
|
labels: Optional[List[str]] = optional_field
|
|
133
|
+
priority: int = 0
|
|
132
134
|
|
|
133
135
|
|
|
134
136
|
@pydantic_dataclass
|
spaceforge/generator.py
CHANGED
|
@@ -30,6 +30,41 @@ from .plugin import SpaceforgePlugin
|
|
|
30
30
|
static_binary_directory = "/mnt/workspace/plugins/plugin_binaries"
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def _update_context_names_for_priority(contexts: List[Context]) -> List[Context]:
|
|
34
|
+
if len(contexts) <= 1:
|
|
35
|
+
return contexts
|
|
36
|
+
|
|
37
|
+
# Get unique priority values and sort them
|
|
38
|
+
priorities = sorted(set(ctx.priority for ctx in contexts))
|
|
39
|
+
|
|
40
|
+
# Create mapping from priority to letter prefix
|
|
41
|
+
priority_to_letter = {}
|
|
42
|
+
|
|
43
|
+
for i, priority in enumerate(priorities):
|
|
44
|
+
if priority == 0:
|
|
45
|
+
# Priority 0 gets 'Z' (lowest priority = furthest from A)
|
|
46
|
+
letter = "Z"
|
|
47
|
+
else:
|
|
48
|
+
# Higher priority numbers get letters closer to 'A'
|
|
49
|
+
# Reverse the mapping: higher priority index = closer to A
|
|
50
|
+
letter_index = max(0, 25 - i) # Start from Z and work backwards
|
|
51
|
+
letter = chr(ord("A") + letter_index)
|
|
52
|
+
|
|
53
|
+
priority_to_letter[priority] = letter * 5 # Repeat 5 times
|
|
54
|
+
|
|
55
|
+
# Update context names
|
|
56
|
+
for ctx in contexts:
|
|
57
|
+
# Remove existing prefix if it starts with repeated letters
|
|
58
|
+
name = ctx.name_prefix
|
|
59
|
+
if len(name) >= 5 and name[:5].isupper() and len(set(name[:5])) == 1:
|
|
60
|
+
name = name[5:]
|
|
61
|
+
|
|
62
|
+
prefix = priority_to_letter[ctx.priority]
|
|
63
|
+
ctx.name_prefix = prefix + "-" + name
|
|
64
|
+
|
|
65
|
+
return contexts
|
|
66
|
+
|
|
67
|
+
|
|
33
68
|
class PluginGenerator:
|
|
34
69
|
"""Generates plugin.yaml from a Python plugin class."""
|
|
35
70
|
|
|
@@ -257,29 +292,39 @@ class PluginGenerator:
|
|
|
257
292
|
if self.plugin_class is None:
|
|
258
293
|
raise ValueError("Plugin class not loaded. Call load_plugin() first.")
|
|
259
294
|
|
|
260
|
-
contexts = getattr(
|
|
261
|
-
self.plugin_class,
|
|
262
|
-
"__contexts__",
|
|
263
|
-
[
|
|
264
|
-
Context(
|
|
265
|
-
name_prefix=self.plugin_class.__plugin_name__.lower(),
|
|
266
|
-
description=f"Main context for {self.plugin_class.__plugin_name__}",
|
|
267
|
-
)
|
|
268
|
-
],
|
|
269
|
-
)
|
|
295
|
+
contexts = getattr(self.plugin_class, "__contexts__", [])
|
|
270
296
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
297
|
+
main_context: Optional[Context] = None
|
|
298
|
+
main_context_found = False
|
|
299
|
+
for context in contexts:
|
|
300
|
+
if context.priority == 0:
|
|
301
|
+
main_context = context
|
|
302
|
+
main_context_found = True
|
|
303
|
+
break
|
|
304
|
+
|
|
305
|
+
if main_context is None:
|
|
306
|
+
main_context = Context(
|
|
307
|
+
name_prefix=self.plugin_class.__plugin_name__.lower(),
|
|
308
|
+
description=f"Main context for {self.plugin_class.__plugin_name__}",
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
if main_context.hooks is None:
|
|
312
|
+
main_context.hooks = {}
|
|
313
|
+
if main_context.mounted_files is None:
|
|
314
|
+
main_context.mounted_files = []
|
|
315
|
+
if main_context.env is None:
|
|
316
|
+
main_context.env = []
|
|
277
317
|
|
|
278
318
|
# Add the hooks and mounted files to the first context
|
|
279
|
-
merge(
|
|
280
|
-
|
|
319
|
+
merge(main_context.hooks, hooks, strategy=Strategy.TYPESAFE_ADDITIVE)
|
|
320
|
+
main_context.mounted_files += mounted_files
|
|
321
|
+
|
|
322
|
+
# Ensure the main context is first
|
|
323
|
+
if not main_context_found:
|
|
324
|
+
contexts.insert(0, main_context)
|
|
281
325
|
|
|
282
326
|
self._map_variables_to_parameters(contexts)
|
|
327
|
+
contexts = _update_context_names_for_priority(contexts)
|
|
283
328
|
|
|
284
329
|
return contexts
|
|
285
330
|
|
|
@@ -380,6 +425,7 @@ class PluginGenerator:
|
|
|
380
425
|
field.name: getattr(data, field.name)
|
|
381
426
|
for field in fields(data)
|
|
382
427
|
if getattr(data, field.name) is not None
|
|
428
|
+
and field.name != "priority"
|
|
383
429
|
}
|
|
384
430
|
return self.represent_dict(filtered_dict)
|
|
385
431
|
|
spaceforge/plugin.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Base plugin class for Spaceforge framework.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
import inspect
|
|
6
5
|
import json
|
|
7
6
|
import logging
|
|
8
7
|
import os
|
|
@@ -162,35 +161,6 @@ class SpaceforgePlugin(ABC):
|
|
|
162
161
|
else:
|
|
163
162
|
self.logger.error(f"API call returned no data: {resp}")
|
|
164
163
|
|
|
165
|
-
def get_available_hooks(self) -> List[str]:
|
|
166
|
-
"""
|
|
167
|
-
Get list of hook methods available in this plugin.
|
|
168
|
-
|
|
169
|
-
Returns:
|
|
170
|
-
List of hook method names that are implemented
|
|
171
|
-
"""
|
|
172
|
-
hook_methods = []
|
|
173
|
-
for method_name in dir(self):
|
|
174
|
-
if not method_name.startswith("_") and method_name != "get_available_hooks":
|
|
175
|
-
method = getattr(self, method_name)
|
|
176
|
-
if callable(method) and not inspect.isbuiltin(method):
|
|
177
|
-
# Check if it's a hook method (not inherited from base class)
|
|
178
|
-
if method_name in [
|
|
179
|
-
"before_init",
|
|
180
|
-
"after_init",
|
|
181
|
-
"before_plan",
|
|
182
|
-
"after_plan",
|
|
183
|
-
"before_apply",
|
|
184
|
-
"after_apply",
|
|
185
|
-
"before_perform",
|
|
186
|
-
"after_perform",
|
|
187
|
-
"before_destroy",
|
|
188
|
-
"after_destroy",
|
|
189
|
-
"after_run",
|
|
190
|
-
]:
|
|
191
|
-
hook_methods.append(method_name)
|
|
192
|
-
return hook_methods
|
|
193
|
-
|
|
194
164
|
def run_cli(
|
|
195
165
|
self, *command: str, expect_code: int = 0, print_output: bool = True
|
|
196
166
|
) -> Tuple[int, List[str], List[str]]:
|
spaceforge/schema.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$defs": {
|
|
3
3
|
"Context": {
|
|
4
|
-
"description": "A class to represent a context for a plugin.\n\nAttributes:\n name_prefix (str): The name of the context, will be appended with a unique ID.\n description (str): A description of the context.\n labels (Optional[List[str]]): Labels associated with the context.\n env (list): List of variables associated with the context.\n hooks (dict): Hooks associated with the context.",
|
|
4
|
+
"description": "A class to represent a context for a plugin.\n\nAttributes:\n name_prefix (str): The name of the context, will be appended with a unique ID.\n description (str): A description of the context.\n labels (Optional[List[str]]): Labels associated with the context.\n env (list): List of variables associated with the context.\n hooks (dict): Hooks associated with the context.\n priority (optional[int]): The priority of the context, contexts with 0 priority run before contexts with 1 priority.",
|
|
5
5
|
"properties": {
|
|
6
6
|
"name_prefix": {
|
|
7
7
|
"title": "Name Prefix",
|
|
@@ -84,6 +84,11 @@
|
|
|
84
84
|
}
|
|
85
85
|
],
|
|
86
86
|
"title": "Labels"
|
|
87
|
+
},
|
|
88
|
+
"priority": {
|
|
89
|
+
"default": 0,
|
|
90
|
+
"title": "Priority",
|
|
91
|
+
"type": "integer"
|
|
87
92
|
}
|
|
88
93
|
},
|
|
89
94
|
"required": [
|
spaceforge/test_plugin_hooks.py
CHANGED
|
@@ -31,70 +31,3 @@ class TestSpaceforgePluginHooks:
|
|
|
31
31
|
assert callable(hook_method)
|
|
32
32
|
# Should be able to call without error (default implementation is pass)
|
|
33
33
|
hook_method()
|
|
34
|
-
|
|
35
|
-
def test_should_return_all_available_hooks_for_base_class(self) -> None:
|
|
36
|
-
"""Should detect and return all hook methods defined in base class."""
|
|
37
|
-
# Arrange
|
|
38
|
-
plugin = SpaceforgePlugin()
|
|
39
|
-
expected_hooks = [
|
|
40
|
-
"before_init",
|
|
41
|
-
"after_init",
|
|
42
|
-
"before_plan",
|
|
43
|
-
"after_plan",
|
|
44
|
-
"before_apply",
|
|
45
|
-
"after_apply",
|
|
46
|
-
"before_perform",
|
|
47
|
-
"after_perform",
|
|
48
|
-
"before_destroy",
|
|
49
|
-
"after_destroy",
|
|
50
|
-
"after_run",
|
|
51
|
-
]
|
|
52
|
-
|
|
53
|
-
# Act
|
|
54
|
-
hooks = plugin.get_available_hooks()
|
|
55
|
-
|
|
56
|
-
# Assert
|
|
57
|
-
for expected_hook in expected_hooks:
|
|
58
|
-
assert expected_hook in hooks
|
|
59
|
-
|
|
60
|
-
def test_should_detect_overridden_hooks_in_custom_plugin(self) -> None:
|
|
61
|
-
"""Should include all hooks including overridden ones in custom plugins."""
|
|
62
|
-
|
|
63
|
-
# Arrange
|
|
64
|
-
class TestPluginWithHooks(SpaceforgePlugin):
|
|
65
|
-
def after_plan(self) -> None:
|
|
66
|
-
pass
|
|
67
|
-
|
|
68
|
-
def before_apply(self) -> None:
|
|
69
|
-
pass
|
|
70
|
-
|
|
71
|
-
def custom_method(self) -> None: # Not a hook
|
|
72
|
-
pass
|
|
73
|
-
|
|
74
|
-
plugin = TestPluginWithHooks()
|
|
75
|
-
|
|
76
|
-
# Act
|
|
77
|
-
hooks = plugin.get_available_hooks()
|
|
78
|
-
|
|
79
|
-
# Assert
|
|
80
|
-
assert "after_plan" in hooks
|
|
81
|
-
assert "before_apply" in hooks
|
|
82
|
-
assert "custom_method" not in hooks # Not a recognized hook
|
|
83
|
-
|
|
84
|
-
# Should still have all the expected hooks from the base class
|
|
85
|
-
expected_hooks = [
|
|
86
|
-
"before_init",
|
|
87
|
-
"after_init",
|
|
88
|
-
"before_plan",
|
|
89
|
-
"after_plan",
|
|
90
|
-
"before_apply",
|
|
91
|
-
"after_apply",
|
|
92
|
-
"before_perform",
|
|
93
|
-
"after_perform",
|
|
94
|
-
"before_destroy",
|
|
95
|
-
"after_destroy",
|
|
96
|
-
"after_run",
|
|
97
|
-
]
|
|
98
|
-
|
|
99
|
-
for expected_hook in expected_hooks:
|
|
100
|
-
assert expected_hook in hooks
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spaceforge
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: A Python framework for building Spacelift plugins
|
|
5
5
|
Home-page: https://github.com/spacelift-io/plugins
|
|
6
6
|
Author: Spacelift
|
|
@@ -63,7 +63,7 @@ pip install spaceforge
|
|
|
63
63
|
Create a Python file (e.g., `my_plugin.py`) and inherit from `SpaceforgePlugin`:
|
|
64
64
|
|
|
65
65
|
```python
|
|
66
|
-
from spaceforge import SpaceforgePlugin, Parameter, Variable, Context
|
|
66
|
+
from spaceforge import SpaceforgePlugin, Parameter, Variable, Context, Binary, Policy, Webhook, MountedFile
|
|
67
67
|
import os
|
|
68
68
|
|
|
69
69
|
class MyPlugin(SpaceforgePlugin):
|
|
@@ -71,17 +71,20 @@ class MyPlugin(SpaceforgePlugin):
|
|
|
71
71
|
__plugin_name__ = "my-awesome-plugin"
|
|
72
72
|
__version__ = "1.0.0"
|
|
73
73
|
__author__ = "Your Name"
|
|
74
|
+
__labels__ = ["security", "monitoring"] # Optional labels for categorization
|
|
74
75
|
|
|
75
76
|
# Define plugin parameters
|
|
76
77
|
__parameters__ = [
|
|
77
78
|
Parameter(
|
|
78
|
-
name="
|
|
79
|
+
name="API Key",
|
|
80
|
+
id="api_key", # Optional ID for parameter reference
|
|
79
81
|
description="API key for external service",
|
|
80
82
|
required=True,
|
|
81
83
|
sensitive=True
|
|
82
84
|
),
|
|
83
85
|
Parameter(
|
|
84
|
-
name="
|
|
86
|
+
name="Environment",
|
|
87
|
+
id="environment",
|
|
85
88
|
description="Target environment",
|
|
86
89
|
required=False,
|
|
87
90
|
default="production"
|
|
@@ -96,12 +99,12 @@ class MyPlugin(SpaceforgePlugin):
|
|
|
96
99
|
env=[
|
|
97
100
|
Variable(
|
|
98
101
|
key="API_KEY",
|
|
99
|
-
value_from_parameter="api_key",
|
|
102
|
+
value_from_parameter="api_key", # Matches parameter id or name
|
|
100
103
|
sensitive=True
|
|
101
104
|
),
|
|
102
105
|
Variable(
|
|
103
106
|
key="ENVIRONMENT",
|
|
104
|
-
value_from_parameter="environment"
|
|
107
|
+
value_from_parameter="environment" # Matches parameter id or name
|
|
105
108
|
)
|
|
106
109
|
]
|
|
107
110
|
)
|
|
@@ -160,6 +163,15 @@ Override these methods in your plugin to add custom logic:
|
|
|
160
163
|
|
|
161
164
|
## Plugin Components
|
|
162
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
|
+
|
|
163
175
|
### Parameters
|
|
164
176
|
|
|
165
177
|
Define user-configurable parameters:
|
|
@@ -167,21 +179,30 @@ Define user-configurable parameters:
|
|
|
167
179
|
```python
|
|
168
180
|
__parameters__ = [
|
|
169
181
|
Parameter(
|
|
170
|
-
name="
|
|
182
|
+
name="Database URL",
|
|
183
|
+
id="database_url", # Optional: used for parameter reference
|
|
171
184
|
description="Database connection URL",
|
|
172
185
|
required=True,
|
|
173
|
-
sensitive=True
|
|
174
|
-
default="postgresql://localhost:5432/mydb"
|
|
186
|
+
sensitive=True
|
|
175
187
|
),
|
|
176
188
|
Parameter(
|
|
177
|
-
name="
|
|
189
|
+
name="Timeout",
|
|
190
|
+
id="timeout",
|
|
178
191
|
description="Timeout in seconds",
|
|
179
192
|
required=False,
|
|
180
|
-
default=30
|
|
193
|
+
default="30" # Default values should be strings
|
|
181
194
|
)
|
|
182
195
|
]
|
|
183
196
|
```
|
|
184
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
|
+
|
|
185
206
|
### Contexts
|
|
186
207
|
|
|
187
208
|
Define Spacelift contexts with environment variables and custom hooks:
|
|
@@ -191,11 +212,11 @@ __contexts__ = [
|
|
|
191
212
|
Context(
|
|
192
213
|
name_prefix="production",
|
|
193
214
|
description="Production environment context",
|
|
194
|
-
labels=
|
|
215
|
+
labels=["env:prod"],
|
|
195
216
|
env=[
|
|
196
217
|
Variable(
|
|
197
218
|
key="DATABASE_URL",
|
|
198
|
-
value_from_parameter="database_url",
|
|
219
|
+
value_from_parameter="database_url", # Matches parameter id
|
|
199
220
|
sensitive=True
|
|
200
221
|
),
|
|
201
222
|
Variable(
|
|
@@ -229,6 +250,88 @@ __binaries__ = [
|
|
|
229
250
|
]
|
|
230
251
|
```
|
|
231
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
|
+
|
|
232
335
|
### Policies
|
|
233
336
|
|
|
234
337
|
Define OPA policies for your plugin:
|
|
@@ -237,7 +340,7 @@ Define OPA policies for your plugin:
|
|
|
237
340
|
__policies__ = [
|
|
238
341
|
Policy(
|
|
239
342
|
name_prefix="security-check",
|
|
240
|
-
type="
|
|
343
|
+
type="NOTIFICATION",
|
|
241
344
|
body="""
|
|
242
345
|
package spacelift
|
|
243
346
|
|
|
@@ -245,7 +348,7 @@ webhook[{"endpoint_id": "security-alerts"}] {
|
|
|
245
348
|
input.run_updated.run.marked_unsafe == true
|
|
246
349
|
}
|
|
247
350
|
""",
|
|
248
|
-
labels=
|
|
351
|
+
labels=["security"]
|
|
249
352
|
)
|
|
250
353
|
]
|
|
251
354
|
```
|
|
@@ -258,11 +361,9 @@ Define webhooks to trigger external actions:
|
|
|
258
361
|
__webhooks__ = [
|
|
259
362
|
Webhook(
|
|
260
363
|
name_prefix="security-alerts",
|
|
261
|
-
description="Send security alerts to external service",
|
|
262
364
|
endpoint="https://alerts.example.com/webhook",
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
],
|
|
365
|
+
secretFromParameter="webhook_secret", # Parameter id/name for webhook secret
|
|
366
|
+
labels=["security"]
|
|
266
367
|
)
|
|
267
368
|
]
|
|
268
369
|
```
|
|
@@ -297,7 +398,7 @@ def before_apply(self):
|
|
|
297
398
|
|
|
298
399
|
### Spacelift API Integration
|
|
299
400
|
|
|
300
|
-
Query the Spacelift GraphQL API (requires `SPACELIFT_API_TOKEN` and `
|
|
401
|
+
Query the Spacelift GraphQL API (requires `SPACELIFT_API_TOKEN` and `TF_VAR_spacelift_graphql_endpoint`):
|
|
301
402
|
|
|
302
403
|
```python
|
|
303
404
|
def after_plan(self):
|
|
@@ -317,6 +418,38 @@ def after_plan(self):
|
|
|
317
418
|
self.logger.info(f"Stack state: {result['stack']['state']}")
|
|
318
419
|
```
|
|
319
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
|
+
|
|
320
453
|
### Access Plan and State
|
|
321
454
|
|
|
322
455
|
Access Terraform plan and state data:
|
|
@@ -353,14 +486,14 @@ def after_plan(self):
|
|
|
353
486
|
self.send_markdown(markdown)
|
|
354
487
|
```
|
|
355
488
|
|
|
356
|
-
###
|
|
489
|
+
### Add to Policy Input
|
|
357
490
|
|
|
358
|
-
|
|
491
|
+
Add custom data to the OPA policy input:
|
|
359
492
|
|
|
360
493
|
The following example will create input available via `input.third_party_metadata.custom.my_custom_data` in your OPA policies:
|
|
361
494
|
```python
|
|
362
495
|
def after_plan(self):
|
|
363
|
-
self.
|
|
496
|
+
self.add_to_policy_input("my_custom_data", {
|
|
364
497
|
"scan_results": {
|
|
365
498
|
"passed": True,
|
|
366
499
|
"issues": []
|
|
@@ -389,9 +522,9 @@ spaceforge generate --help
|
|
|
389
522
|
### Test Plugin Hooks
|
|
390
523
|
|
|
391
524
|
```bash
|
|
392
|
-
# Set parameters
|
|
393
|
-
export
|
|
394
|
-
export
|
|
525
|
+
# Set parameters for local testing (parameters are normally provided by Spacelift)
|
|
526
|
+
export API_KEY="test-key"
|
|
527
|
+
export TIMEOUT="60"
|
|
395
528
|
|
|
396
529
|
# Test specific hook
|
|
397
530
|
spaceforge runner after_plan
|
|
@@ -420,8 +553,8 @@ Access Spacelift environment variables in your hooks:
|
|
|
420
553
|
|
|
421
554
|
```python
|
|
422
555
|
def after_plan(self):
|
|
423
|
-
run_id = os.environ.get('
|
|
424
|
-
stack_id = os.environ.get('
|
|
556
|
+
run_id = os.environ.get('TF_VAR_spacelift_run_id')
|
|
557
|
+
stack_id = os.environ.get('TF_VAR_spacelift_stack_id')
|
|
425
558
|
self.logger.info(f"Processing run {run_id} for stack {stack_id}")
|
|
426
559
|
```
|
|
427
560
|
|
|
@@ -455,7 +588,7 @@ Here's a complete example of a security scanning plugin:
|
|
|
455
588
|
```python
|
|
456
589
|
import os
|
|
457
590
|
import json
|
|
458
|
-
from spaceforge import SpaceforgePlugin, Parameter, Variable, Context, Binary
|
|
591
|
+
from spaceforge import SpaceforgePlugin, Parameter, Variable, Context, Binary, Policy, MountedFile
|
|
459
592
|
|
|
460
593
|
class SecurityScannerPlugin(SpaceforgePlugin):
|
|
461
594
|
__plugin_name__ = "security-scanner"
|
|
@@ -474,13 +607,15 @@ class SecurityScannerPlugin(SpaceforgePlugin):
|
|
|
474
607
|
|
|
475
608
|
__parameters__ = [
|
|
476
609
|
Parameter(
|
|
477
|
-
name="
|
|
610
|
+
name="API Token",
|
|
611
|
+
id="api_token",
|
|
478
612
|
description="Security service API token",
|
|
479
613
|
required=True,
|
|
480
614
|
sensitive=True
|
|
481
615
|
),
|
|
482
616
|
Parameter(
|
|
483
|
-
name="
|
|
617
|
+
name="Severity Threshold",
|
|
618
|
+
id="severity_threshold",
|
|
484
619
|
description="Minimum severity level to report",
|
|
485
620
|
required=False,
|
|
486
621
|
default="medium"
|
|
@@ -581,8 +716,8 @@ Generate and test this plugin:
|
|
|
581
716
|
spaceforge generate security_scanner.py
|
|
582
717
|
|
|
583
718
|
# Test locally
|
|
584
|
-
export
|
|
585
|
-
export
|
|
719
|
+
export API_TOKEN="your-token"
|
|
720
|
+
export SEVERITY_THRESHOLD="high"
|
|
586
721
|
spaceforge runner after_plan
|
|
587
722
|
```
|
|
588
723
|
|
|
@@ -2,13 +2,13 @@ spaceforge/README.md,sha256=8o1Nuyasb4OxX3E7ZycyducOrR4J19bZcHrLvFeoFNg,7730
|
|
|
2
2
|
spaceforge/__init__.py,sha256=TU-vvm15dK1ucixNW0V42eTT72x3_hmKSyxP4MC1Occ,589
|
|
3
3
|
spaceforge/__main__.py,sha256=c3nAw4WBnHXIcfMlRV6Ja7r87pEhSeK-SAqiSYIasIY,643
|
|
4
4
|
spaceforge/_version.py,sha256=RP_LfUd4ODnrfwn9nam8wB6bR3lM4VwmoRxK08Tkiiw,2155
|
|
5
|
-
spaceforge/_version_scm.py,sha256=
|
|
6
|
-
spaceforge/cls.py,sha256=
|
|
5
|
+
spaceforge/_version_scm.py,sha256=O2MwkHziz63f5tgvyTDcaX64UwyuPAN_tWOgO5wh1WI,704
|
|
6
|
+
spaceforge/cls.py,sha256=oYW5t5_xs9ZM6Ne_b4trxCPxLHQrqbqgUeibeM8O4PU,6329
|
|
7
7
|
spaceforge/conftest.py,sha256=U-xCavCsgRAQXqflIIOMeq9pcGbeqRviUNkEXgZol8g,2141
|
|
8
|
-
spaceforge/generator.py,sha256=
|
|
9
|
-
spaceforge/plugin.py,sha256=
|
|
8
|
+
spaceforge/generator.py,sha256=hCxtbOKmrGd7HCCz7HMaiK566kIre5MNxcJEx2TVURM,18430
|
|
9
|
+
spaceforge/plugin.py,sha256=Ytm2B7nnJNH231V4gUFY1pBmwVNTpIg3YviUL_Bnf24,14963
|
|
10
10
|
spaceforge/runner.py,sha256=EUZ98gmOiJ766zOSk7YcTTrLCtHfst1xf3iE2Xu7Tao,3172
|
|
11
|
-
spaceforge/schema.json,sha256=
|
|
11
|
+
spaceforge/schema.json,sha256=89IROLVlCj8txGMiLt4Bbuo_2muSxKoZCyaXQ2vuA9c,10869
|
|
12
12
|
spaceforge/test_cls.py,sha256=nXAgbnFnGdFxrtA7vNXiePjNUASuoYW-lEuQGx9WMGs,468
|
|
13
13
|
spaceforge/test_generator.py,sha256=Nst3YVu_iZbFopH6ajjxCfqYrZvybteGbwMfZzjBFnI,32615
|
|
14
14
|
spaceforge/test_generator_binaries.py,sha256=X_7pPLGE45eQt-Kv9_ku__LsyLgOvViHc_BvVpSCMp0,7263
|
|
@@ -17,17 +17,17 @@ spaceforge/test_generator_hooks.py,sha256=2lJs8dYlFb7QehWcYF0O4qg38s5UudEpzJyBi1
|
|
|
17
17
|
spaceforge/test_generator_parameters.py,sha256=77az9rcocFny2AC4O2eTzjCW712fR1DBHzGrgBKeR4w,1878
|
|
18
18
|
spaceforge/test_plugin.py,sha256=rZ4Uv_0lIR0qb1GFHkiosGO3WHTWhO7epz8INDxV8Q0,13018
|
|
19
19
|
spaceforge/test_plugin_file_operations.py,sha256=B0qvIo5EcfKMiHLhBv-hAnpSonn83ojcmJHXasydojA,3782
|
|
20
|
-
spaceforge/test_plugin_hooks.py,sha256=
|
|
20
|
+
spaceforge/test_plugin_hooks.py,sha256=ugaVdzH1-heRJSJN0lu8zoqLcLPC3tg_PzUX98qu9Sw,1038
|
|
21
21
|
spaceforge/test_plugin_inheritance.py,sha256=WHfvU5s-2GtfcI9-1bHXH7bacr77ikq68V3Z3BBQKvQ,3617
|
|
22
22
|
spaceforge/test_runner.py,sha256=fDnUf6gEuf1CNMxz6zs3xXvERQsQU3z8qy9KdUc0Wo4,17739
|
|
23
23
|
spaceforge/test_runner_cli.py,sha256=Sf5X0O9Wc9EhGB5L8SzvlmO7QmgQZQoClSdNYefa-lQ,2299
|
|
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=
|
|
26
|
+
spaceforge/templates/binary_install.sh.j2,sha256=3vcKUSIpMxYWUuAfAWs3gP3tNr4ZnBIb2ELAqISViZo,498
|
|
27
27
|
spaceforge/templates/ensure_spaceforge_and_run.sh.j2,sha256=g5BldIEve0IkZ-mCzTXfB_rFvyWqUJqymRRaaMrpp0s,550
|
|
28
|
-
spaceforge-1.
|
|
29
|
-
spaceforge-1.
|
|
30
|
-
spaceforge-1.
|
|
31
|
-
spaceforge-1.
|
|
32
|
-
spaceforge-1.
|
|
33
|
-
spaceforge-1.
|
|
28
|
+
spaceforge-1.1.1.dist-info/licenses/LICENSE,sha256=wyljRrfnWY2ggQKkSCg3Nw2hxwPMmupopaKs9Kpgys8,1065
|
|
29
|
+
spaceforge-1.1.1.dist-info/METADATA,sha256=0Mc6aKdf5jOm3KwfG0mrn7-owmKGZTZ-H5DXAfRojnc,21164
|
|
30
|
+
spaceforge-1.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
31
|
+
spaceforge-1.1.1.dist-info/entry_points.txt,sha256=qawuuKBSNTGg-njnQnhxxFldFvXYAPej6bF_f3iyQ48,56
|
|
32
|
+
spaceforge-1.1.1.dist-info/top_level.txt,sha256=eVw-Lw4Th0oHM8Gx1Y8YetyNgbNbMBU00yWs-kwGeSs,11
|
|
33
|
+
spaceforge-1.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|