spaceforge 0.0.3__tar.gz → 0.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {spaceforge-0.0.3 → spaceforge-0.0.4}/PKG-INFO +11 -1
- {spaceforge-0.0.3 → spaceforge-0.0.4}/README.md +9 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/plugins/sops/plugin.yaml +53 -4
- {spaceforge-0.0.3 → spaceforge-0.0.4}/plugins/wiz/plugin.py +1 -1
- {spaceforge-0.0.3 → spaceforge-0.0.4}/plugins/wiz/plugin.yaml +55 -5
- {spaceforge-0.0.3 → spaceforge-0.0.4}/pyproject.toml +1 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/setup.py +1 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/_version_scm.py +3 -3
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/cls.py +8 -8
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/generator.py +53 -40
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/plugin.py +2 -1
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/schema.json +15 -24
- spaceforge-0.0.4/spaceforge/templates/binary_install.sh.j2 +23 -0
- spaceforge-0.0.4/spaceforge/templates/ensure_spaceforge_and_run.sh.j2 +22 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/test_generator.py +159 -58
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/test_generator_binaries.py +40 -13
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge.egg-info/PKG-INFO +11 -1
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge.egg-info/SOURCES.txt +3 -1
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge.egg-info/requires.txt +1 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/test.sh +1 -1
- {spaceforge-0.0.3 → spaceforge-0.0.4}/.github/workflows/ci.yml +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/.github/workflows/release.yml +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/.gitignore +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/LICENSE +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/MANIFEST.in +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/go.mod +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/plugins/infracost/plugin.py +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/plugins/infracost/plugin.yaml +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/plugins/sops/plugin.py +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/plugins/sops/requirements.txt +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/setup.cfg +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/README.md +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/__init__.py +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/__main__.py +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/_version.py +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/conftest.py +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/runner.py +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/test_cls.py +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/test_generator_core.py +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/test_generator_hooks.py +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/test_generator_parameters.py +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/test_plugin.py +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/test_plugin_file_operations.py +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/test_plugin_hooks.py +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/test_plugin_inheritance.py +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/test_runner.py +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/test_runner_cli.py +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/test_runner_core.py +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge/test_runner_execution.py +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge.egg-info/dependency_links.txt +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge.egg-info/entry_points.txt +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge.egg-info/not-zip-safe +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/spaceforge.egg-info/top_level.txt +0 -0
- {spaceforge-0.0.3 → spaceforge-0.0.4}/templates.go +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spaceforge
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: A Python framework for building Spacelift plugins
|
|
5
5
|
Home-page: https://github.com/spacelift-io/plugins
|
|
6
6
|
Author: Spacelift
|
|
@@ -28,6 +28,7 @@ License-File: LICENSE
|
|
|
28
28
|
Requires-Dist: PyYAML>=6.0
|
|
29
29
|
Requires-Dist: click>=8.0.0
|
|
30
30
|
Requires-Dist: pydantic>=2.11.7
|
|
31
|
+
Requires-Dist: Jinja2>=3.1.0
|
|
31
32
|
Provides-Extra: dev
|
|
32
33
|
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
33
34
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
@@ -584,6 +585,15 @@ export SPACEFORGE_PARAM_SEVERITY_THRESHOLD="high"
|
|
|
584
585
|
spaceforge runner after_plan
|
|
585
586
|
```
|
|
586
587
|
|
|
588
|
+
## Speeding up plugin execution
|
|
589
|
+
|
|
590
|
+
There are a few things you can do to speed up plugin execution.
|
|
591
|
+
|
|
592
|
+
1. Ensure your runner has `spaceforge` preinstalled. This will avoid the overhead of installing it during the run. (15-30 seconds)
|
|
593
|
+
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.
|
|
594
|
+
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.
|
|
595
|
+
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.
|
|
596
|
+
|
|
587
597
|
## Next Steps
|
|
588
598
|
|
|
589
599
|
1. **Install spaceforge:** `pip install spaceforge`
|
|
@@ -540,6 +540,15 @@ export SPACEFORGE_PARAM_SEVERITY_THRESHOLD="high"
|
|
|
540
540
|
spaceforge runner after_plan
|
|
541
541
|
```
|
|
542
542
|
|
|
543
|
+
## Speeding up plugin execution
|
|
544
|
+
|
|
545
|
+
There are a few things you can do to speed up plugin execution.
|
|
546
|
+
|
|
547
|
+
1. Ensure your runner has `spaceforge` preinstalled. This will avoid the overhead of installing it during the run. (15-30 seconds)
|
|
548
|
+
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.
|
|
549
|
+
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.
|
|
550
|
+
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.
|
|
551
|
+
|
|
543
552
|
## Next Steps
|
|
544
553
|
|
|
545
554
|
1. **Install spaceforge:** `pip install spaceforge`
|
|
@@ -131,10 +131,59 @@ contexts:
|
|
|
131
131
|
except Exception as e:
|
|
132
132
|
self.logger.error(f"An unexpected error occurred: {e}")
|
|
133
133
|
sensitive: false
|
|
134
|
+
- path: /mnt/workspace/plugins/sops/binary_install_sops.sh
|
|
135
|
+
content: |-
|
|
136
|
+
#!/bin/sh
|
|
137
|
+
|
|
138
|
+
set -e
|
|
139
|
+
|
|
140
|
+
if command -v sops; then
|
|
141
|
+
echo "sops is already installed."
|
|
142
|
+
return
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
mkdir -p /mnt/workspace/plugins/plugin_binaries
|
|
146
|
+
|
|
147
|
+
echo "Installing sops..."
|
|
148
|
+
mkdir -p /mnt/workspace/plugins/plugin_binaries
|
|
149
|
+
cd /mnt/workspace/plugins/plugin_binaries
|
|
150
|
+
|
|
151
|
+
if [ "$(arch)" = "x86_64" ]; then
|
|
152
|
+
curl https://github.com/getsops/sops/releases/download/v3.9.1/sops-v3.9.1.linux.amd64 -o /mnt/workspace/plugins/plugin_binaries/sops -L
|
|
153
|
+
else
|
|
154
|
+
curl https://github.com/getsops/sops/releases/download/v3.9.1/sops-v3.9.1.linux.arm64 -o /mnt/workspace/plugins/plugin_binaries/sops -L
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
chmod +x /mnt/workspace/plugins/plugin_binaries/sops
|
|
158
|
+
cd /mnt/workspace/source/$TF_VAR_spacelift_project_root
|
|
159
|
+
sensitive: false
|
|
160
|
+
- path: /mnt/workspace/plugins/sops/before_init.sh
|
|
161
|
+
content: |-
|
|
162
|
+
#!/bin/sh
|
|
163
|
+
|
|
164
|
+
set -e
|
|
165
|
+
|
|
166
|
+
cd /mnt/workspace/plugins/sops
|
|
167
|
+
|
|
168
|
+
if [ ! -d "./venv" ]; then
|
|
169
|
+
python -m venv ./venv
|
|
170
|
+
fi
|
|
171
|
+
. venv/bin/activate
|
|
172
|
+
|
|
173
|
+
if ! command -v spaceforge; then
|
|
174
|
+
pip install spaceforge
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
if [ -f requirements.txt ] && [ ! -f .spaceforge_installed_requirements ]; then
|
|
178
|
+
pip install -r requirements.txt
|
|
179
|
+
touch .spaceforge_installed_requirements
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
cd /mnt/workspace/source/$TF_VAR_spacelift_project_root
|
|
183
|
+
spaceforge runner --plugin-file /mnt/workspace/plugins/sops/plugin.py before_init
|
|
184
|
+
sensitive: false
|
|
134
185
|
hooks:
|
|
135
186
|
before_init:
|
|
136
187
|
- mkdir -p /mnt/workspace/plugins/sops
|
|
137
|
-
-
|
|
138
|
-
-
|
|
139
|
-
- mkdir -p /mnt/workspace/plugins/plugin_binaries && cd /mnt/workspace/plugins/plugin_binaries && ([[ "$(echo "$(arch)")" == "x86_64" ]] && curl https://github.com/getsops/sops/releases/download/v3.9.1/sops-v3.9.1.linux.amd64 -o /mnt/workspace/plugins/plugin_binaries/sops -L && chmod +x /mnt/workspace/plugins/plugin_binaries/sops || curl https://github.com/getsops/sops/releases/download/v3.9.1/sops-v3.9.1.linux.arm64 -o /mnt/workspace/plugins/plugin_binaries/sops -L && chmod +x /mnt/workspace/plugins/plugin_binaries/sops) && cd /mnt/workspace/source/$TF_VAR_spacelift_project_root
|
|
140
|
-
- cd /mnt/workspace/source/$TF_VAR_spacelift_project_root && python -m spaceforge runner --plugin-file /mnt/workspace/plugins/sops/plugin.py before_init
|
|
188
|
+
- chmod +x /mnt/workspace/plugins/sops/binary_install_sops.sh && /mnt/workspace/plugins/sops/binary_install_sops.sh
|
|
189
|
+
- chmod +x /mnt/workspace/plugins/sops/before_init.sh && /mnt/workspace/plugins/sops/before_init.sh
|
|
@@ -135,7 +135,7 @@ contexts:
|
|
|
135
135
|
}
|
|
136
136
|
""",
|
|
137
137
|
labels={
|
|
138
|
-
"
|
|
138
|
+
"wiz-plugin"
|
|
139
139
|
}
|
|
140
140
|
)
|
|
141
141
|
]
|
|
@@ -227,13 +227,63 @@ contexts:
|
|
|
227
227
|
markdown += f"<a href=\"{stdout_json['reportUrl']}\" rel=\"noopener noreferrer\">View Report</a>\n"
|
|
228
228
|
self.send_markdown(markdown)
|
|
229
229
|
sensitive: false
|
|
230
|
+
- path: /mnt/workspace/plugins/wiz/binary_install_wizcli.sh
|
|
231
|
+
content: |-
|
|
232
|
+
#!/bin/sh
|
|
233
|
+
|
|
234
|
+
set -e
|
|
235
|
+
|
|
236
|
+
if command -v wizcli; then
|
|
237
|
+
echo "wizcli is already installed."
|
|
238
|
+
return
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
mkdir -p /mnt/workspace/plugins/plugin_binaries
|
|
242
|
+
|
|
243
|
+
echo "Installing wizcli..."
|
|
244
|
+
mkdir -p /mnt/workspace/plugins/plugin_binaries
|
|
245
|
+
cd /mnt/workspace/plugins/plugin_binaries
|
|
246
|
+
|
|
247
|
+
if [ "$(arch)" = "x86_64" ]; then
|
|
248
|
+
curl https://downloads.wiz.io/wizcli/0.94.0/wizcli-linux-amd64 -o /mnt/workspace/plugins/plugin_binaries/wizcli -L
|
|
249
|
+
else
|
|
250
|
+
curl https://downloads.wiz.io/wizcli/0.94.0/wizcli-linux-arm64 -o /mnt/workspace/plugins/plugin_binaries/wizcli -L
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
chmod +x /mnt/workspace/plugins/plugin_binaries/wizcli
|
|
254
|
+
cd /mnt/workspace/source/$TF_VAR_spacelift_project_root
|
|
255
|
+
sensitive: false
|
|
256
|
+
- path: /mnt/workspace/plugins/wiz/before_plan.sh
|
|
257
|
+
content: |-
|
|
258
|
+
#!/bin/sh
|
|
259
|
+
|
|
260
|
+
set -e
|
|
261
|
+
|
|
262
|
+
cd /mnt/workspace/plugins/wiz
|
|
263
|
+
|
|
264
|
+
if [ ! -d "./venv" ]; then
|
|
265
|
+
python -m venv ./venv
|
|
266
|
+
fi
|
|
267
|
+
. venv/bin/activate
|
|
268
|
+
|
|
269
|
+
if command -v spaceforge; then
|
|
270
|
+
pip install spaceforge
|
|
271
|
+
fi
|
|
272
|
+
|
|
273
|
+
if [ -f requirements.txt ] && [ ! -f .spaceforge_installed_requirements ]; then
|
|
274
|
+
pip install -r requirements.txt
|
|
275
|
+
touch .spaceforge_installed_requirements
|
|
276
|
+
fi
|
|
277
|
+
|
|
278
|
+
cd /mnt/workspace/source/$TF_VAR_spacelift_project_root
|
|
279
|
+
spaceforge runner --plugin-file /mnt/workspace/plugins/wiz/plugin.py before_plan
|
|
280
|
+
sensitive: false
|
|
230
281
|
hooks:
|
|
231
282
|
before_init:
|
|
232
283
|
- mkdir -p /mnt/workspace/plugins/wiz
|
|
233
|
-
-
|
|
284
|
+
- chmod +x /mnt/workspace/plugins/wiz/binary_install_wizcli.sh && /mnt/workspace/plugins/wiz/binary_install_wizcli.sh
|
|
234
285
|
before_plan:
|
|
235
|
-
-
|
|
236
|
-
- cd /mnt/workspace/source/$TF_VAR_spacelift_project_root && python -m spaceforge runner --plugin-file /mnt/workspace/plugins/wiz/plugin.py before_plan
|
|
286
|
+
- chmod +x /mnt/workspace/plugins/wiz/before_plan.sh && /mnt/workspace/plugins/wiz/before_plan.sh
|
|
237
287
|
policies:
|
|
238
288
|
- name_prefix: wiz_policy
|
|
239
289
|
type: notification
|
|
@@ -245,4 +295,4 @@ policies:
|
|
|
245
295
|
input.run_updated.run.marked_unsafe == true
|
|
246
296
|
}
|
|
247
297
|
labels:
|
|
248
|
-
|
|
298
|
+
- wiz-plugin
|
|
@@ -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 = '0.0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 0,
|
|
31
|
+
__version__ = version = '0.0.4'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 0, 4)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'g46df965db'
|
|
@@ -118,7 +118,7 @@ class Context:
|
|
|
118
118
|
Attributes:
|
|
119
119
|
name_prefix (str): The name of the context, will be appended with a unique ID.
|
|
120
120
|
description (str): A description of the context.
|
|
121
|
-
labels (
|
|
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
124
|
"""
|
|
@@ -128,7 +128,7 @@ class Context:
|
|
|
128
128
|
env: Optional[List[Variable]] = optional_field
|
|
129
129
|
mounted_files: Optional[List[MountedFile]] = optional_field
|
|
130
130
|
hooks: Optional[Dict[HookType, List[str]]] = optional_field
|
|
131
|
-
labels: Optional[
|
|
131
|
+
labels: Optional[List[str]] = optional_field
|
|
132
132
|
|
|
133
133
|
|
|
134
134
|
@pydantic_dataclass
|
|
@@ -139,14 +139,14 @@ class Webhook:
|
|
|
139
139
|
Attributes:
|
|
140
140
|
name_prefix (str): The name of the webhook, will be appended with a unique ID.
|
|
141
141
|
endpoint (str): The URL endpoint for the webhook.
|
|
142
|
-
labels (Optional[
|
|
143
|
-
|
|
142
|
+
labels (Optional[List[str]]): Labels associated with the webhook.
|
|
143
|
+
secret (str): the ID of the parameter where the webhook secret is retrieved from
|
|
144
144
|
"""
|
|
145
145
|
|
|
146
146
|
name_prefix: str
|
|
147
147
|
endpoint: str
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
secretFromParameter: str
|
|
149
|
+
labels: Optional[List[str]] = optional_field
|
|
150
150
|
|
|
151
151
|
|
|
152
152
|
@pydantic_dataclass
|
|
@@ -158,13 +158,13 @@ class Policy:
|
|
|
158
158
|
name_prefix (str): The name of the policy, will be appended with a unique ID.
|
|
159
159
|
type (str): The type of the policy (e.g., "terraform", "kubernetes").
|
|
160
160
|
body (str): The body of the policy, typically a configuration or script.
|
|
161
|
-
labels (Optional[
|
|
161
|
+
labels (Optional[List[str]]): Labels associated with the policy.
|
|
162
162
|
"""
|
|
163
163
|
|
|
164
164
|
name_prefix: str
|
|
165
165
|
type: str
|
|
166
166
|
body: str
|
|
167
|
-
labels: Optional[
|
|
167
|
+
labels: Optional[List[str]] = optional_field
|
|
168
168
|
|
|
169
169
|
|
|
170
170
|
@pydantic_dataclass
|
|
@@ -7,6 +7,7 @@ import os
|
|
|
7
7
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union
|
|
8
8
|
|
|
9
9
|
import yaml
|
|
10
|
+
from jinja2 import Environment, PackageLoader, select_autoescape
|
|
10
11
|
|
|
11
12
|
if TYPE_CHECKING:
|
|
12
13
|
from .plugin import SpaceforgePlugin
|
|
@@ -47,6 +48,9 @@ class PluginGenerator:
|
|
|
47
48
|
self.plugin_instance: Optional[SpaceforgePlugin] = None
|
|
48
49
|
self.plugin_working_directory: Optional[str] = None
|
|
49
50
|
self.config: Optional[Dict[str, Any]] = None
|
|
51
|
+
self.jinja = Environment(
|
|
52
|
+
loader=PackageLoader("spaceforge"), autoescape=select_autoescape()
|
|
53
|
+
)
|
|
50
54
|
|
|
51
55
|
def load_plugin(self) -> None:
|
|
52
56
|
"""Load the plugin class from the specified path."""
|
|
@@ -84,7 +88,7 @@ class PluginGenerator:
|
|
|
84
88
|
self.config = {
|
|
85
89
|
"setup_virtual_env": (
|
|
86
90
|
f"cd {self.plugin_working_directory} && python -m venv ./venv && "
|
|
87
|
-
+ "source venv/bin/activate && pip install spaceforge"
|
|
91
|
+
+ "source venv/bin/activate && (command -v spaceforge &> /dev/null || pip install spaceforge)"
|
|
88
92
|
),
|
|
89
93
|
"plugin_mounted_path": f"{self.plugin_working_directory}/{os.path.basename(self.plugin_path)}",
|
|
90
94
|
}
|
|
@@ -137,14 +141,27 @@ class PluginGenerator:
|
|
|
137
141
|
|
|
138
142
|
return hook_methods
|
|
139
143
|
|
|
140
|
-
def
|
|
141
|
-
self,
|
|
144
|
+
def _add_to_mounted_files(
|
|
145
|
+
self,
|
|
146
|
+
hooks: Dict[str, List[str]],
|
|
147
|
+
mounted_files: List[MountedFile],
|
|
148
|
+
phase: str,
|
|
149
|
+
filepath: str,
|
|
150
|
+
filecontent: str,
|
|
142
151
|
) -> None:
|
|
152
|
+
file = f"{self.plugin_working_directory}/{filepath}"
|
|
153
|
+
hooks[phase].append(f"chmod +x {file} && {file}")
|
|
154
|
+
mounted_files.append(
|
|
155
|
+
MountedFile(
|
|
156
|
+
path=f"{self.plugin_working_directory}/{filepath}",
|
|
157
|
+
content=filecontent,
|
|
158
|
+
sensitive=False,
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def _update_with_requirements(self, mounted_files: List[MountedFile]) -> None:
|
|
143
163
|
"""Update the plugin hooks if there is a requirements.txt"""
|
|
144
164
|
if os.path.exists("requirements.txt") and self.config is not None:
|
|
145
|
-
if self.config["setup_virtual_env"] not in hooks["before_init"]:
|
|
146
|
-
hooks["before_init"].append(self.config["setup_virtual_env"])
|
|
147
|
-
hooks["before_init"].append(f"pip install -r requirements.txt")
|
|
148
165
|
# read the requirements.txt file
|
|
149
166
|
with open("requirements.txt", "r") as f:
|
|
150
167
|
mounted_files.append(
|
|
@@ -167,7 +184,9 @@ class PluginGenerator:
|
|
|
167
184
|
)
|
|
168
185
|
)
|
|
169
186
|
|
|
170
|
-
def _add_spaceforge_hooks(
|
|
187
|
+
def _add_spaceforge_hooks(
|
|
188
|
+
self, hooks: Dict[str, List[str]], mounted_files: List[MountedFile]
|
|
189
|
+
) -> None:
|
|
171
190
|
# Add the spaceforge hook to actually run the plugin
|
|
172
191
|
if self.config is None:
|
|
173
192
|
raise ValueError("Plugin config not set. Call load_plugin() first.")
|
|
@@ -178,12 +197,14 @@ class PluginGenerator:
|
|
|
178
197
|
if hook not in hooks:
|
|
179
198
|
hooks[hook] = []
|
|
180
199
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
200
|
+
directory = os.path.dirname(self.config["plugin_mounted_path"])
|
|
201
|
+
template = self.jinja.get_template("ensure_spaceforge_and_run.sh.j2")
|
|
202
|
+
render = template.render(
|
|
203
|
+
plugin_path=directory,
|
|
204
|
+
plugin_file=self.config["plugin_mounted_path"],
|
|
205
|
+
phase=hook,
|
|
186
206
|
)
|
|
207
|
+
self._add_to_mounted_files(hooks, mounted_files, hook, f"{hook}.sh", render)
|
|
187
208
|
|
|
188
209
|
def _map_variables_to_parameters(self, contexts: List[Context]) -> None:
|
|
189
210
|
for context in contexts:
|
|
@@ -221,10 +242,10 @@ class PluginGenerator:
|
|
|
221
242
|
}
|
|
222
243
|
mounted_files: List[MountedFile] = []
|
|
223
244
|
|
|
224
|
-
self._update_with_requirements(
|
|
245
|
+
self._update_with_requirements(mounted_files)
|
|
225
246
|
self._update_with_python_file(mounted_files)
|
|
226
|
-
self._generate_binary_install_command(hooks)
|
|
227
|
-
self._add_spaceforge_hooks(hooks)
|
|
247
|
+
self._generate_binary_install_command(hooks, mounted_files)
|
|
248
|
+
self._add_spaceforge_hooks(hooks, mounted_files)
|
|
228
249
|
|
|
229
250
|
# Get the contexts and append the hooks and mounted files to it.
|
|
230
251
|
if self.plugin_class is None:
|
|
@@ -256,46 +277,38 @@ class PluginGenerator:
|
|
|
256
277
|
|
|
257
278
|
return contexts
|
|
258
279
|
|
|
259
|
-
def _generate_binary_install_command(
|
|
280
|
+
def _generate_binary_install_command(
|
|
281
|
+
self, hooks: Dict[str, List[str]], mounted_files: List[MountedFile]
|
|
282
|
+
) -> None:
|
|
260
283
|
binaries = self.get_plugin_binaries()
|
|
261
284
|
if binaries is None:
|
|
262
285
|
return None
|
|
263
286
|
|
|
264
|
-
binary_cmd = ""
|
|
265
|
-
if len(binaries) > 0:
|
|
266
|
-
binary_cmd = f"mkdir -p {static_binary_directory} && cd {static_binary_directory} && "
|
|
267
287
|
for i, binary in enumerate(binaries):
|
|
268
288
|
amd64_url = binary.download_urls.get("amd64", None)
|
|
269
289
|
arm64_url = binary.download_urls.get("arm64", None)
|
|
290
|
+
binary_path = f"{static_binary_directory}/{binary.name}"
|
|
270
291
|
if amd64_url is None and arm64_url is None:
|
|
271
292
|
raise ValueError(
|
|
272
293
|
f"Binary {binary.name} must have at least one download URL defined (amd64 or arm64)"
|
|
273
294
|
)
|
|
274
295
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
f"curl {arm64_url} -o {binary_path} -L && chmod +x {binary_path}"
|
|
283
|
-
if arm64_url is not None
|
|
284
|
-
else "echo 'arm64 binary not available' && exit 1"
|
|
296
|
+
template = self.jinja.get_template("binary_install.sh.j2")
|
|
297
|
+
render = template.render(
|
|
298
|
+
binary=binary,
|
|
299
|
+
amd64_url=amd64_url,
|
|
300
|
+
arm64_url=arm64_url,
|
|
301
|
+
binary_path=binary_path,
|
|
302
|
+
static_binary_directory=static_binary_directory,
|
|
285
303
|
)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
304
|
+
self._add_to_mounted_files(
|
|
305
|
+
hooks,
|
|
306
|
+
mounted_files,
|
|
307
|
+
"before_init",
|
|
308
|
+
f"binary_install_{binary.name}.sh",
|
|
309
|
+
render,
|
|
291
310
|
)
|
|
292
311
|
|
|
293
|
-
if binary_cmd != "":
|
|
294
|
-
binary_cmd += "cd /mnt/workspace/source/$TF_VAR_spacelift_project_root"
|
|
295
|
-
|
|
296
|
-
hooks["before_init"].append(binary_cmd)
|
|
297
|
-
return None
|
|
298
|
-
|
|
299
312
|
def get_plugin_binaries(self) -> Optional[List[Binary]]:
|
|
300
313
|
"""Get binary definitions from the plugin class."""
|
|
301
314
|
return getattr(self.plugin_class, "__binaries__", None)
|
|
@@ -263,12 +263,13 @@ class SpaceforgePlugin(ABC):
|
|
|
263
263
|
self._spacelift_markdown_endpoint,
|
|
264
264
|
json.dumps(body).encode("utf-8"),
|
|
265
265
|
headers,
|
|
266
|
+
method="POST",
|
|
266
267
|
)
|
|
267
268
|
|
|
268
269
|
with urllib.request.urlopen(req) as response:
|
|
269
270
|
if response.status != 200:
|
|
270
271
|
self.logger.error(
|
|
271
|
-
f"Error getting signed URL for markdown upload: {response
|
|
272
|
+
f"Error getting signed URL for markdown upload: {response}"
|
|
272
273
|
)
|
|
273
274
|
return
|
|
274
275
|
|
|
@@ -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 (
|
|
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.",
|
|
5
5
|
"properties": {
|
|
6
6
|
"name_prefix": {
|
|
7
7
|
"title": "Name Prefix",
|
|
@@ -74,10 +74,10 @@
|
|
|
74
74
|
"labels": {
|
|
75
75
|
"anyOf": [
|
|
76
76
|
{
|
|
77
|
-
"
|
|
77
|
+
"items": {
|
|
78
78
|
"type": "string"
|
|
79
79
|
},
|
|
80
|
-
"type": "
|
|
80
|
+
"type": "array"
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
"type": "null"
|
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
"type": "object"
|
|
164
164
|
},
|
|
165
165
|
"Policy": {
|
|
166
|
-
"description": "A class to represent a policy configuration.\n\nAttributes:\n name_prefix (str): The name of the policy, will be appended with a unique ID.\n type (str): The type of the policy (e.g., \"terraform\", \"kubernetes\").\n body (str): The body of the policy, typically a configuration or script.\n labels (Optional[
|
|
166
|
+
"description": "A class to represent a policy configuration.\n\nAttributes:\n name_prefix (str): The name of the policy, will be appended with a unique ID.\n type (str): The type of the policy (e.g., \"terraform\", \"kubernetes\").\n body (str): The body of the policy, typically a configuration or script.\n labels (Optional[List[str]]): Labels associated with the policy.",
|
|
167
167
|
"properties": {
|
|
168
168
|
"name_prefix": {
|
|
169
169
|
"title": "Name Prefix",
|
|
@@ -180,10 +180,10 @@
|
|
|
180
180
|
"labels": {
|
|
181
181
|
"anyOf": [
|
|
182
182
|
{
|
|
183
|
-
"
|
|
183
|
+
"items": {
|
|
184
184
|
"type": "string"
|
|
185
185
|
},
|
|
186
|
-
"type": "
|
|
186
|
+
"type": "array"
|
|
187
187
|
},
|
|
188
188
|
{
|
|
189
189
|
"type": "null"
|
|
@@ -242,7 +242,7 @@
|
|
|
242
242
|
"type": "object"
|
|
243
243
|
},
|
|
244
244
|
"Webhook": {
|
|
245
|
-
"description": "A class to represent a webhook configuration.\n\nAttributes:\n name_prefix (str): The name of the webhook, will be appended with a unique ID.\n endpoint (str): The URL endpoint for the webhook.\n labels (Optional[
|
|
245
|
+
"description": "A class to represent a webhook configuration.\n\nAttributes:\n name_prefix (str): The name of the webhook, will be appended with a unique ID.\n endpoint (str): The URL endpoint for the webhook.\n labels (Optional[List[str]]): Labels associated with the webhook.\n secret (str): the ID of the parameter where the webhook secret is retrieved from",
|
|
246
246
|
"properties": {
|
|
247
247
|
"name_prefix": {
|
|
248
248
|
"title": "Name Prefix",
|
|
@@ -252,25 +252,15 @@
|
|
|
252
252
|
"title": "Endpoint",
|
|
253
253
|
"type": "string"
|
|
254
254
|
},
|
|
255
|
-
"
|
|
256
|
-
"
|
|
257
|
-
|
|
258
|
-
"additionalProperties": {
|
|
259
|
-
"type": "string"
|
|
260
|
-
},
|
|
261
|
-
"type": "object"
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
"type": "null"
|
|
265
|
-
}
|
|
266
|
-
],
|
|
267
|
-
"title": "Labels"
|
|
255
|
+
"secretFromParameter": {
|
|
256
|
+
"title": "Secretfromparameter",
|
|
257
|
+
"type": "string"
|
|
268
258
|
},
|
|
269
|
-
"
|
|
259
|
+
"labels": {
|
|
270
260
|
"anyOf": [
|
|
271
261
|
{
|
|
272
262
|
"items": {
|
|
273
|
-
"
|
|
263
|
+
"type": "string"
|
|
274
264
|
},
|
|
275
265
|
"type": "array"
|
|
276
266
|
},
|
|
@@ -278,12 +268,13 @@
|
|
|
278
268
|
"type": "null"
|
|
279
269
|
}
|
|
280
270
|
],
|
|
281
|
-
"title": "
|
|
271
|
+
"title": "Labels"
|
|
282
272
|
}
|
|
283
273
|
},
|
|
284
274
|
"required": [
|
|
285
275
|
"name_prefix",
|
|
286
|
-
"endpoint"
|
|
276
|
+
"endpoint",
|
|
277
|
+
"secretFromParameter"
|
|
287
278
|
],
|
|
288
279
|
"title": "Webhook",
|
|
289
280
|
"type": "object"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
if command -v {{binary.name}}; then
|
|
6
|
+
echo "{{binary.name}} is already installed."
|
|
7
|
+
return
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
mkdir -p {{static_binary_directory}}
|
|
11
|
+
|
|
12
|
+
echo "Installing {{binary.name}}..."
|
|
13
|
+
mkdir -p {{static_binary_directory}}
|
|
14
|
+
cd {{static_binary_directory}}
|
|
15
|
+
|
|
16
|
+
if [ "$(arch)" = "x86_64" ]; then
|
|
17
|
+
curl {{amd64_url}} -o {{binary_path}} -L
|
|
18
|
+
else
|
|
19
|
+
curl {{arm64_url}} -o {{binary_path}} -L
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
chmod +x {{binary_path}}
|
|
23
|
+
cd /mnt/workspace/source/$TF_VAR_spacelift_project_root
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
cd {{plugin_path}}
|
|
6
|
+
|
|
7
|
+
if [ ! -d "./venv" ]; then
|
|
8
|
+
python -m venv ./venv
|
|
9
|
+
fi
|
|
10
|
+
. venv/bin/activate
|
|
11
|
+
|
|
12
|
+
if ! command -v spaceforge; then
|
|
13
|
+
pip install spaceforge
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
if [ -f requirements.txt ] && [ ! -f .spaceforge_installed_requirements ]; then
|
|
17
|
+
pip install -r requirements.txt
|
|
18
|
+
touch .spaceforge_installed_requirements
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
cd /mnt/workspace/source/$TF_VAR_spacelift_project_root
|
|
22
|
+
spaceforge runner --plugin-file {{plugin_file}} {{phase}}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import tempfile
|
|
3
|
-
from typing import Dict, List
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
4
|
from unittest.mock import Mock, mock_open, patch
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
@@ -55,7 +55,7 @@ class PluginExample(SpaceforgePlugin):
|
|
|
55
55
|
Context(
|
|
56
56
|
name_prefix="test_context",
|
|
57
57
|
description="Test context",
|
|
58
|
-
labels=
|
|
58
|
+
labels=["env:test"],
|
|
59
59
|
env=[Variable(key="TEST_VAR", value="test_value")],
|
|
60
60
|
)
|
|
61
61
|
]
|
|
@@ -64,7 +64,8 @@ class PluginExample(SpaceforgePlugin):
|
|
|
64
64
|
Webhook(
|
|
65
65
|
name_prefix="test_webhook",
|
|
66
66
|
endpoint="https://webhook.example.com",
|
|
67
|
-
|
|
67
|
+
secretFromParameter="api_key",
|
|
68
|
+
labels=["type:notification"],
|
|
68
69
|
)
|
|
69
70
|
]
|
|
70
71
|
|
|
@@ -73,7 +74,7 @@ class PluginExample(SpaceforgePlugin):
|
|
|
73
74
|
name_prefix="test_policy",
|
|
74
75
|
type="notification",
|
|
75
76
|
body="package test",
|
|
76
|
-
labels=
|
|
77
|
+
labels=["type:security"],
|
|
77
78
|
)
|
|
78
79
|
]
|
|
79
80
|
|
|
@@ -325,14 +326,25 @@ class NotAPlugin:
|
|
|
325
326
|
}
|
|
326
327
|
|
|
327
328
|
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
328
|
-
|
|
329
|
-
|
|
329
|
+
mounted_files: List = []
|
|
330
|
+
generator._generate_binary_install_command(hooks, mounted_files)
|
|
330
331
|
|
|
331
|
-
|
|
332
|
-
assert "
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
332
|
+
# Should have added a script execution to hooks
|
|
333
|
+
assert len(hooks["before_init"]) == 1
|
|
334
|
+
command = hooks["before_init"][0]
|
|
335
|
+
|
|
336
|
+
# Should have added a mounted file with script content
|
|
337
|
+
assert len(mounted_files) == 1
|
|
338
|
+
script_content = mounted_files[0].content
|
|
339
|
+
|
|
340
|
+
# Check that hook command runs the script
|
|
341
|
+
assert "chmod +x" in command
|
|
342
|
+
assert "binary_install_test-cli.sh" in command
|
|
343
|
+
|
|
344
|
+
# Check script content contains binary installation logic
|
|
345
|
+
assert "test-cli" in script_content
|
|
346
|
+
assert "https://example.com/test-cli-amd64" in script_content
|
|
347
|
+
assert "https://example.com/test-cli-arm64" in script_content
|
|
336
348
|
|
|
337
349
|
def test_generate_binary_install_command_no_binaries(self) -> None:
|
|
338
350
|
"""Test binary command generation when no binaries."""
|
|
@@ -349,9 +361,11 @@ class NotAPlugin:
|
|
|
349
361
|
}
|
|
350
362
|
|
|
351
363
|
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
352
|
-
|
|
353
|
-
|
|
364
|
+
mounted_files: List = []
|
|
365
|
+
generator._generate_binary_install_command(hooks, mounted_files)
|
|
366
|
+
# No binaries should mean no new commands or files added
|
|
354
367
|
assert len(hooks["before_init"]) == 0
|
|
368
|
+
assert len(mounted_files) == 0
|
|
355
369
|
|
|
356
370
|
def test_generate_binary_install_command_missing_urls(self) -> None:
|
|
357
371
|
"""Test binary command generation with missing URLs."""
|
|
@@ -368,8 +382,9 @@ class NotAPlugin:
|
|
|
368
382
|
}
|
|
369
383
|
|
|
370
384
|
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
385
|
+
mounted_files: List = []
|
|
371
386
|
with pytest.raises(ValueError, match="must have at least one download URL"):
|
|
372
|
-
generator._generate_binary_install_command(hooks)
|
|
387
|
+
generator._generate_binary_install_command(hooks, mounted_files)
|
|
373
388
|
|
|
374
389
|
def test_get_plugin_policies(self) -> None:
|
|
375
390
|
"""Test policy extraction."""
|
|
@@ -397,37 +412,50 @@ class NotAPlugin:
|
|
|
397
412
|
assert webhooks[0].endpoint == "https://webhook.example.com"
|
|
398
413
|
|
|
399
414
|
@patch("os.path.exists")
|
|
400
|
-
|
|
401
|
-
def test_get_plugin_contexts_with_requirements(
|
|
402
|
-
self, mock_file: Mock, mock_exists: Mock
|
|
403
|
-
) -> None:
|
|
415
|
+
def test_get_plugin_contexts_with_requirements(self, mock_exists: Mock) -> None:
|
|
404
416
|
"""Test context generation with requirements.txt."""
|
|
405
417
|
mock_exists.side_effect = (
|
|
406
418
|
lambda path: path == "requirements.txt" or "plugin.py" in path
|
|
407
419
|
)
|
|
408
420
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
421
|
+
# Mock specific file contents with a custom open function
|
|
422
|
+
original_open = open
|
|
423
|
+
|
|
424
|
+
def mock_open_func(filename: str, *args: Any, **kwargs: Any) -> Any:
|
|
425
|
+
if filename == "requirements.txt":
|
|
426
|
+
from io import StringIO
|
|
427
|
+
|
|
428
|
+
return StringIO("requirements content")
|
|
429
|
+
elif "plugin.py" in filename:
|
|
430
|
+
from io import StringIO
|
|
416
431
|
|
|
417
|
-
|
|
432
|
+
return StringIO("plugin content")
|
|
433
|
+
else:
|
|
434
|
+
return original_open(filename, *args, **kwargs)
|
|
435
|
+
|
|
436
|
+
with patch("builtins.open", side_effect=mock_open_func):
|
|
437
|
+
generator = PluginGenerator(self.test_plugin_path)
|
|
438
|
+
generator.plugin_class = PluginExample
|
|
439
|
+
generator.plugin_working_directory = "/mnt/workspace/plugins/test_plugin"
|
|
440
|
+
generator.config = {
|
|
441
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/test_plugin && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
442
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/test_plugin/plugin.py",
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
contexts = generator.get_plugin_contexts()
|
|
418
446
|
|
|
419
447
|
assert len(contexts) == 1
|
|
420
448
|
context = contexts[0]
|
|
421
449
|
|
|
422
|
-
# Should have before_init hooks
|
|
450
|
+
# Should have before_init hooks with mkdir command
|
|
423
451
|
assert context.hooks is not None
|
|
424
452
|
assert "before_init" in context.hooks
|
|
425
|
-
|
|
453
|
+
mkdir_command = None
|
|
426
454
|
for cmd in context.hooks["before_init"]:
|
|
427
|
-
if "
|
|
428
|
-
|
|
455
|
+
if "mkdir -p" in cmd:
|
|
456
|
+
mkdir_command = cmd
|
|
429
457
|
break
|
|
430
|
-
assert
|
|
458
|
+
assert mkdir_command is not None
|
|
431
459
|
|
|
432
460
|
# Should have requirements.txt as mounted file
|
|
433
461
|
assert context.mounted_files is not None
|
|
@@ -439,26 +467,44 @@ class NotAPlugin:
|
|
|
439
467
|
assert req_file is not None
|
|
440
468
|
assert req_file.content == "requirements content"
|
|
441
469
|
|
|
470
|
+
# Should have plugin.py as mounted file
|
|
471
|
+
plugin_file = None
|
|
472
|
+
for mf in context.mounted_files:
|
|
473
|
+
if "plugin.py" in mf.path:
|
|
474
|
+
plugin_file = mf
|
|
475
|
+
break
|
|
476
|
+
assert plugin_file is not None
|
|
477
|
+
assert plugin_file.content == "plugin content"
|
|
478
|
+
|
|
442
479
|
@patch("os.path.exists")
|
|
443
|
-
|
|
444
|
-
def test_get_plugin_contexts_basic(
|
|
445
|
-
self, mock_file: Mock, mock_exists: Mock
|
|
446
|
-
) -> None:
|
|
480
|
+
def test_get_plugin_contexts_basic(self, mock_exists: Mock) -> None:
|
|
447
481
|
"""Test basic context generation."""
|
|
448
482
|
mock_exists.side_effect = lambda path: "plugin.py" in path
|
|
449
483
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
"
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
484
|
+
# Mock specific file contents with a custom open function
|
|
485
|
+
original_open = open
|
|
486
|
+
|
|
487
|
+
def mock_open_func(filename: str, *args: Any, **kwargs: Any) -> Any:
|
|
488
|
+
if "plugin.py" in filename:
|
|
489
|
+
from io import StringIO
|
|
490
|
+
|
|
491
|
+
return StringIO("plugin content")
|
|
492
|
+
else:
|
|
493
|
+
return original_open(filename, *args, **kwargs)
|
|
494
|
+
|
|
495
|
+
with patch("builtins.open", side_effect=mock_open_func):
|
|
496
|
+
generator = PluginGenerator(self.test_plugin_path)
|
|
497
|
+
generator.plugin_class = PluginExample
|
|
498
|
+
generator.plugin_working_directory = "/mnt/workspace/plugins/test_plugin"
|
|
499
|
+
generator.config = {
|
|
500
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/test_plugin && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
501
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/test_plugin/plugin.py",
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
with patch.object(
|
|
505
|
+
generator, "get_available_hooks", return_value=["after_plan"]
|
|
506
|
+
):
|
|
507
|
+
contexts = generator.get_plugin_contexts()
|
|
462
508
|
|
|
463
509
|
assert len(contexts) == 1
|
|
464
510
|
context = contexts[0]
|
|
@@ -472,12 +518,24 @@ class NotAPlugin:
|
|
|
472
518
|
break
|
|
473
519
|
assert plugin_file is not None
|
|
474
520
|
|
|
475
|
-
# Should have
|
|
521
|
+
# Should have hooks for after_plan (the hook we mocked as available)
|
|
476
522
|
assert context.hooks is not None
|
|
477
523
|
assert "after_plan" in context.hooks
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
524
|
+
|
|
525
|
+
# Should have a script execution command
|
|
526
|
+
hook_command = context.hooks["after_plan"][0]
|
|
527
|
+
assert "chmod +x" in hook_command
|
|
528
|
+
assert "after_plan.sh" in hook_command
|
|
529
|
+
|
|
530
|
+
# Should have a mounted script file for after_plan
|
|
531
|
+
after_plan_script = None
|
|
532
|
+
for mf in context.mounted_files:
|
|
533
|
+
if "after_plan.sh" in mf.path:
|
|
534
|
+
after_plan_script = mf
|
|
535
|
+
break
|
|
536
|
+
assert after_plan_script is not None
|
|
537
|
+
assert "spaceforge runner" in after_plan_script.content
|
|
538
|
+
assert "after_plan" in after_plan_script.content
|
|
481
539
|
|
|
482
540
|
def test_generate_manifest(self) -> None:
|
|
483
541
|
"""Test complete manifest generation."""
|
|
@@ -646,11 +704,23 @@ class TestPluginGeneratorEdgeCases:
|
|
|
646
704
|
}
|
|
647
705
|
|
|
648
706
|
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
649
|
-
|
|
650
|
-
|
|
707
|
+
mounted_files: List = []
|
|
708
|
+
generator._generate_binary_install_command(hooks, mounted_files)
|
|
709
|
+
|
|
710
|
+
# Should have added a script execution to hooks
|
|
711
|
+
assert len(hooks["before_init"]) == 1
|
|
712
|
+
command = hooks["before_init"][0]
|
|
651
713
|
|
|
652
|
-
|
|
653
|
-
assert
|
|
714
|
+
# Should have added a mounted file with script content
|
|
715
|
+
assert len(mounted_files) == 1
|
|
716
|
+
script_content = mounted_files[0].content
|
|
717
|
+
|
|
718
|
+
# Check that hook command runs the script
|
|
719
|
+
assert "chmod +x" in command
|
|
720
|
+
assert "binary_install_single-arch.sh" in command
|
|
721
|
+
|
|
722
|
+
# Check script content contains the binary URL
|
|
723
|
+
assert "https://example.com/binary-amd64" in script_content
|
|
654
724
|
|
|
655
725
|
def test_context_merging_with_existing(self) -> None:
|
|
656
726
|
"""Test that generated hooks are merged with existing context hooks."""
|
|
@@ -683,10 +753,21 @@ class TestPluginGeneratorEdgeCases:
|
|
|
683
753
|
"plugin_mounted_path": "/mnt/workspace/plugins/existing_hooks/plugin.py",
|
|
684
754
|
}
|
|
685
755
|
|
|
756
|
+
# Mock specific file contents with a custom open function
|
|
757
|
+
original_open = open
|
|
758
|
+
|
|
759
|
+
def mock_open_func(filename: str, *args: Any, **kwargs: Any) -> Any:
|
|
760
|
+
if filename == "/fake/path":
|
|
761
|
+
from io import StringIO
|
|
762
|
+
|
|
763
|
+
return StringIO("fake content")
|
|
764
|
+
else:
|
|
765
|
+
return original_open(filename, *args, **kwargs)
|
|
766
|
+
|
|
686
767
|
with patch("os.path.exists") as mock_exists:
|
|
687
768
|
# Only plugin file exists, not requirements.txt
|
|
688
769
|
mock_exists.side_effect = lambda path: path == "/fake/path"
|
|
689
|
-
with patch("builtins.open",
|
|
770
|
+
with patch("builtins.open", side_effect=mock_open_func):
|
|
690
771
|
contexts = generator.get_plugin_contexts()
|
|
691
772
|
|
|
692
773
|
context = contexts[0]
|
|
@@ -702,11 +783,20 @@ class TestPluginGeneratorEdgeCases:
|
|
|
702
783
|
assert len(existing_files) == 1
|
|
703
784
|
|
|
704
785
|
# Plugin file should be mounted (path contains the working directory)
|
|
705
|
-
|
|
786
|
+
# The new implementation creates both plugin.py and hook scripts
|
|
787
|
+
working_dir_files = [
|
|
706
788
|
mf
|
|
707
789
|
for mf in context.mounted_files
|
|
708
790
|
if "/mnt/workspace/plugins/existing_hooks" in mf.path
|
|
709
791
|
]
|
|
792
|
+
assert (
|
|
793
|
+
len(working_dir_files) >= 1
|
|
794
|
+
) # At least plugin.py, possibly hook scripts too
|
|
795
|
+
|
|
796
|
+
# Specifically check for plugin.py
|
|
797
|
+
plugin_files = [
|
|
798
|
+
mf for mf in context.mounted_files if mf.path.endswith("plugin.py")
|
|
799
|
+
]
|
|
710
800
|
assert len(plugin_files) == 1
|
|
711
801
|
|
|
712
802
|
# Should have existing env vars
|
|
@@ -767,10 +857,21 @@ class TestPluginGeneratorEdgeCases:
|
|
|
767
857
|
"plugin_mounted_path": "/mnt/workspace/plugins/parametersandvariables/plugin.py",
|
|
768
858
|
}
|
|
769
859
|
|
|
860
|
+
# Mock specific file contents with a custom open function
|
|
861
|
+
original_open = open
|
|
862
|
+
|
|
863
|
+
def mock_open_func(filename: str, *args: Any, **kwargs: Any) -> Any:
|
|
864
|
+
if filename == "/fake/path":
|
|
865
|
+
from io import StringIO
|
|
866
|
+
|
|
867
|
+
return StringIO("fake content")
|
|
868
|
+
else:
|
|
869
|
+
return original_open(filename, *args, **kwargs)
|
|
870
|
+
|
|
770
871
|
with patch("os.path.exists") as mock_exists:
|
|
771
872
|
# Only plugin file exists, not requirements.txt
|
|
772
873
|
mock_exists.side_effect = lambda path: path == "/fake/path"
|
|
773
|
-
with patch("builtins.open",
|
|
874
|
+
with patch("builtins.open", side_effect=mock_open_func):
|
|
774
875
|
contexts = generator.get_plugin_contexts()
|
|
775
876
|
parameters = generator.get_plugin_parameters()
|
|
776
877
|
|
|
@@ -80,17 +80,28 @@ class TestPluginGeneratorBinaries:
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
83
|
+
mounted_files: List = []
|
|
83
84
|
|
|
84
85
|
# Act
|
|
85
|
-
generator._generate_binary_install_command(hooks)
|
|
86
|
-
command = hooks["before_init"][-1]
|
|
86
|
+
generator._generate_binary_install_command(hooks, mounted_files)
|
|
87
87
|
|
|
88
88
|
# Assert
|
|
89
|
-
|
|
90
|
-
assert "
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
# Should have added a script execution to hooks
|
|
90
|
+
assert len(hooks["before_init"]) == 1
|
|
91
|
+
command = hooks["before_init"][0]
|
|
92
|
+
|
|
93
|
+
# Should have added a mounted file with script content
|
|
94
|
+
assert len(mounted_files) == 1
|
|
95
|
+
script_content = mounted_files[0].content
|
|
96
|
+
|
|
97
|
+
# Check that hook command runs the script
|
|
98
|
+
assert "chmod +x" in command
|
|
99
|
+
assert "binary_install_multi-cli.sh" in command
|
|
100
|
+
|
|
101
|
+
# Check script content contains binary installation logic
|
|
102
|
+
assert "multi-cli" in script_content
|
|
103
|
+
assert "https://example.com/multi-cli-amd64" in script_content
|
|
104
|
+
assert "https://example.com/multi-cli-arm64" in script_content
|
|
94
105
|
|
|
95
106
|
def test_should_not_add_commands_when_no_binaries(self) -> None:
|
|
96
107
|
"""Should not add installation commands when plugin has no binaries."""
|
|
@@ -108,12 +119,15 @@ class TestPluginGeneratorBinaries:
|
|
|
108
119
|
}
|
|
109
120
|
|
|
110
121
|
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
122
|
+
mounted_files: List = []
|
|
111
123
|
|
|
112
124
|
# Act
|
|
113
|
-
generator._generate_binary_install_command(hooks)
|
|
125
|
+
generator._generate_binary_install_command(hooks, mounted_files)
|
|
114
126
|
|
|
115
127
|
# Assert
|
|
128
|
+
# No binaries should mean no new commands or files added
|
|
116
129
|
assert len(hooks["before_init"]) == 0
|
|
130
|
+
assert len(mounted_files) == 0
|
|
117
131
|
|
|
118
132
|
def test_should_raise_error_when_binary_has_no_download_urls(self) -> None:
|
|
119
133
|
"""Should raise ValueError when binary has empty download URLs."""
|
|
@@ -131,10 +145,11 @@ class TestPluginGeneratorBinaries:
|
|
|
131
145
|
}
|
|
132
146
|
|
|
133
147
|
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
148
|
+
mounted_files: List = []
|
|
134
149
|
|
|
135
150
|
# Act & Assert
|
|
136
151
|
with pytest.raises(ValueError, match="must have at least one download URL"):
|
|
137
|
-
generator._generate_binary_install_command(hooks)
|
|
152
|
+
generator._generate_binary_install_command(hooks, mounted_files)
|
|
138
153
|
|
|
139
154
|
def test_should_handle_single_architecture_binary(self) -> None:
|
|
140
155
|
"""Should generate appropriate commands for single architecture binary."""
|
|
@@ -157,11 +172,23 @@ class TestPluginGeneratorBinaries:
|
|
|
157
172
|
}
|
|
158
173
|
|
|
159
174
|
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
175
|
+
mounted_files: List = []
|
|
160
176
|
|
|
161
177
|
# Act
|
|
162
|
-
generator._generate_binary_install_command(hooks)
|
|
163
|
-
command = hooks["before_init"][-1]
|
|
178
|
+
generator._generate_binary_install_command(hooks, mounted_files)
|
|
164
179
|
|
|
165
180
|
# Assert
|
|
166
|
-
|
|
167
|
-
assert "
|
|
181
|
+
# Should have added a script execution to hooks
|
|
182
|
+
assert len(hooks["before_init"]) == 1
|
|
183
|
+
command = hooks["before_init"][0]
|
|
184
|
+
|
|
185
|
+
# Should have added a mounted file with script content
|
|
186
|
+
assert len(mounted_files) == 1
|
|
187
|
+
script_content = mounted_files[0].content
|
|
188
|
+
|
|
189
|
+
# Check that hook command runs the script
|
|
190
|
+
assert "chmod +x" in command
|
|
191
|
+
assert "binary_install_single-arch.sh" in command
|
|
192
|
+
|
|
193
|
+
# Check script content contains the binary URL
|
|
194
|
+
assert "https://example.com/binary-amd64" in script_content
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spaceforge
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: A Python framework for building Spacelift plugins
|
|
5
5
|
Home-page: https://github.com/spacelift-io/plugins
|
|
6
6
|
Author: Spacelift
|
|
@@ -28,6 +28,7 @@ License-File: LICENSE
|
|
|
28
28
|
Requires-Dist: PyYAML>=6.0
|
|
29
29
|
Requires-Dist: click>=8.0.0
|
|
30
30
|
Requires-Dist: pydantic>=2.11.7
|
|
31
|
+
Requires-Dist: Jinja2>=3.1.0
|
|
31
32
|
Provides-Extra: dev
|
|
32
33
|
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
33
34
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
@@ -584,6 +585,15 @@ export SPACEFORGE_PARAM_SEVERITY_THRESHOLD="high"
|
|
|
584
585
|
spaceforge runner after_plan
|
|
585
586
|
```
|
|
586
587
|
|
|
588
|
+
## Speeding up plugin execution
|
|
589
|
+
|
|
590
|
+
There are a few things you can do to speed up plugin execution.
|
|
591
|
+
|
|
592
|
+
1. Ensure your runner has `spaceforge` preinstalled. This will avoid the overhead of installing it during the run. (15-30 seconds)
|
|
593
|
+
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.
|
|
594
|
+
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.
|
|
595
|
+
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.
|
|
596
|
+
|
|
587
597
|
## Next Steps
|
|
588
598
|
|
|
589
599
|
1. **Install spaceforge:** `pip install spaceforge`
|
|
@@ -47,4 +47,6 @@ spaceforge.egg-info/dependency_links.txt
|
|
|
47
47
|
spaceforge.egg-info/entry_points.txt
|
|
48
48
|
spaceforge.egg-info/not-zip-safe
|
|
49
49
|
spaceforge.egg-info/requires.txt
|
|
50
|
-
spaceforge.egg-info/top_level.txt
|
|
50
|
+
spaceforge.egg-info/top_level.txt
|
|
51
|
+
spaceforge/templates/binary_install.sh.j2
|
|
52
|
+
spaceforge/templates/ensure_spaceforge_and_run.sh.j2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|