spaceforge 0.0.2__py3-none-any.whl → 0.0.4__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/__init__.py +12 -4
- spaceforge/__main__.py +3 -3
- spaceforge/_version.py +0 -1
- spaceforge/_version_scm.py +2 -2
- spaceforge/cls.py +16 -12
- spaceforge/conftest.py +89 -0
- spaceforge/generator.py +119 -54
- spaceforge/plugin.py +106 -12
- spaceforge/runner.py +0 -12
- spaceforge/schema.json +38 -29
- spaceforge/templates/binary_install.sh.j2 +23 -0
- spaceforge/templates/ensure_spaceforge_and_run.sh.j2 +22 -0
- spaceforge/{generator_test.py → test_generator.py} +263 -51
- spaceforge/test_generator_binaries.py +194 -0
- spaceforge/test_generator_core.py +180 -0
- spaceforge/test_generator_hooks.py +90 -0
- spaceforge/test_generator_parameters.py +59 -0
- spaceforge/test_plugin.py +357 -0
- spaceforge/test_plugin_file_operations.py +118 -0
- spaceforge/test_plugin_hooks.py +100 -0
- spaceforge/test_plugin_inheritance.py +102 -0
- spaceforge/{runner_test.py → test_runner.py} +2 -65
- spaceforge/test_runner_cli.py +69 -0
- spaceforge/test_runner_core.py +124 -0
- spaceforge/test_runner_execution.py +169 -0
- spaceforge-0.0.4.dist-info/METADATA +605 -0
- spaceforge-0.0.4.dist-info/RECORD +33 -0
- spaceforge/plugin_test.py +0 -621
- spaceforge-0.0.2.dist-info/METADATA +0 -163
- spaceforge-0.0.2.dist-info/RECORD +0 -20
- /spaceforge/{cls_test.py → test_cls.py} +0 -0
- {spaceforge-0.0.2.dist-info → spaceforge-0.0.4.dist-info}/WHEEL +0 -0
- {spaceforge-0.0.2.dist-info → spaceforge-0.0.4.dist-info}/entry_points.txt +0 -0
- {spaceforge-0.0.2.dist-info → spaceforge-0.0.4.dist-info}/licenses/LICENSE +0 -0
- {spaceforge-0.0.2.dist-info → spaceforge-0.0.4.dist-info}/top_level.txt +0 -0
spaceforge/plugin.py
CHANGED
|
@@ -9,7 +9,7 @@ import os
|
|
|
9
9
|
import subprocess
|
|
10
10
|
import urllib.request
|
|
11
11
|
from abc import ABC
|
|
12
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
12
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class SpaceforgePlugin(ABC):
|
|
@@ -31,14 +31,17 @@ class SpaceforgePlugin(ABC):
|
|
|
31
31
|
__author__ = "Spacelift Team"
|
|
32
32
|
|
|
33
33
|
def __init__(self) -> None:
|
|
34
|
+
self._run_id = os.environ.get("TF_VAR_spacelift_run_id", "local")
|
|
35
|
+
self._is_local = self._run_id == "local"
|
|
34
36
|
self.logger = self._setup_logger()
|
|
35
37
|
|
|
36
|
-
self._api_token = os.environ.get("SPACELIFT_API_TOKEN"
|
|
37
|
-
self._spacelift_domain =
|
|
38
|
-
"TF_VAR_spacelift_graphql_endpoint"
|
|
38
|
+
self._api_token = os.environ.get("SPACELIFT_API_TOKEN") or False
|
|
39
|
+
self._spacelift_domain = (
|
|
40
|
+
os.environ.get("TF_VAR_spacelift_graphql_endpoint") or False
|
|
39
41
|
)
|
|
40
|
-
self._api_enabled = self._api_token
|
|
41
|
-
self._workspace_root = os.
|
|
42
|
+
self._api_enabled = bool(self._api_token and self._spacelift_domain)
|
|
43
|
+
self._workspace_root = os.getcwd()
|
|
44
|
+
self._spacelift_markdown_endpoint = None
|
|
42
45
|
|
|
43
46
|
# This should be the last thing we do in the constructor
|
|
44
47
|
# because we set api_enabled to false if the domain is set up incorrectly.
|
|
@@ -54,6 +57,11 @@ class SpaceforgePlugin(ABC):
|
|
|
54
57
|
)
|
|
55
58
|
self._api_enabled = False
|
|
56
59
|
|
|
60
|
+
if self._api_enabled:
|
|
61
|
+
self._spacelift_markdown_endpoint = self._spacelift_domain.replace(
|
|
62
|
+
"/graphql", "/worker/plugin_logs_url"
|
|
63
|
+
)
|
|
64
|
+
|
|
57
65
|
def _setup_logger(self) -> logging.Logger:
|
|
58
66
|
"""Set up logging for the plugin."""
|
|
59
67
|
|
|
@@ -62,7 +70,7 @@ class SpaceforgePlugin(ABC):
|
|
|
62
70
|
warn_color = "\033[33m"
|
|
63
71
|
error_color = "\033[31m"
|
|
64
72
|
end_color = "\033[0m"
|
|
65
|
-
run_id =
|
|
73
|
+
run_id = self._run_id
|
|
66
74
|
plugin_name = self.__plugin_name__
|
|
67
75
|
|
|
68
76
|
class ColorFormatter(logging.Formatter):
|
|
@@ -93,7 +101,7 @@ class SpaceforgePlugin(ABC):
|
|
|
93
101
|
handler.setFormatter(ColorFormatter())
|
|
94
102
|
|
|
95
103
|
# Always check for debug mode spacelift variable
|
|
96
|
-
if os.environ.get("SPACELIFT_DEBUG"):
|
|
104
|
+
if os.environ.get("SPACELIFT_DEBUG") or self._is_local:
|
|
97
105
|
logger.setLevel(logging.DEBUG)
|
|
98
106
|
else:
|
|
99
107
|
logger.setLevel(logging.INFO)
|
|
@@ -175,7 +183,7 @@ class SpaceforgePlugin(ABC):
|
|
|
175
183
|
) -> Dict[str, Any]:
|
|
176
184
|
if not self._api_enabled:
|
|
177
185
|
self.logger.error(
|
|
178
|
-
'API is not enabled, please export "SPACELIFT_API_TOKEN" and "
|
|
186
|
+
'API is not enabled, please export "SPACELIFT_API_TOKEN" and "TF_VAR_spacelift_graphql_endpoint".'
|
|
179
187
|
)
|
|
180
188
|
exit(1)
|
|
181
189
|
|
|
@@ -192,7 +200,7 @@ class SpaceforgePlugin(ABC):
|
|
|
192
200
|
data["variables"] = variables
|
|
193
201
|
|
|
194
202
|
req = urllib.request.Request(
|
|
195
|
-
|
|
203
|
+
self._spacelift_domain, # type: ignore[arg-type]
|
|
196
204
|
json.dumps(data).encode("utf-8"),
|
|
197
205
|
headers,
|
|
198
206
|
)
|
|
@@ -226,8 +234,94 @@ class SpaceforgePlugin(ABC):
|
|
|
226
234
|
return data
|
|
227
235
|
|
|
228
236
|
def send_markdown(self, markdown: str) -> None:
|
|
229
|
-
|
|
230
|
-
|
|
237
|
+
"""
|
|
238
|
+
Send a markdown message to the Spacelift run.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
markdown: The markdown content to send
|
|
242
|
+
"""
|
|
243
|
+
if self._is_local:
|
|
244
|
+
self.logger.info(
|
|
245
|
+
"Spacelift run is local. Not uploading markdown. Below is a preview of what would be sent"
|
|
246
|
+
)
|
|
247
|
+
self.logger.info(markdown)
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
if self._spacelift_markdown_endpoint is None:
|
|
251
|
+
self.logger.error(
|
|
252
|
+
'API is not enabled, please export "SPACELIFT_API_TOKEN" and "TF_VAR_spacelift_graphql_endpoint".'
|
|
253
|
+
)
|
|
254
|
+
exit(1)
|
|
255
|
+
|
|
256
|
+
headers = {"Authorization": f"Bearer {self._api_token}"}
|
|
257
|
+
body = {
|
|
258
|
+
"plugin_name": self.__plugin_name__,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# First we get the signed url for uploading
|
|
262
|
+
req = urllib.request.Request(
|
|
263
|
+
self._spacelift_markdown_endpoint,
|
|
264
|
+
json.dumps(body).encode("utf-8"),
|
|
265
|
+
headers,
|
|
266
|
+
method="POST",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
with urllib.request.urlopen(req) as response:
|
|
270
|
+
if response.status != 200:
|
|
271
|
+
self.logger.error(
|
|
272
|
+
f"Error getting signed URL for markdown upload: {response}"
|
|
273
|
+
)
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
raw_response = response.read().decode("utf-8")
|
|
277
|
+
self.logger.debug(raw_response)
|
|
278
|
+
resp: Dict[str, Any] = json.loads(raw_response)
|
|
279
|
+
if "url" not in resp or "headers" not in resp:
|
|
280
|
+
self.logger.error(
|
|
281
|
+
"Markdown signed url response does not contain 'url' or 'headers' key."
|
|
282
|
+
)
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
signed_url = resp["url"]
|
|
286
|
+
headers = resp["headers"]
|
|
287
|
+
headers["Content-Type"] = "text/markdown"
|
|
288
|
+
headers["Content-Length"] = str(len(markdown))
|
|
289
|
+
|
|
290
|
+
# Now we upload the markdown content to the signed URL
|
|
291
|
+
req = urllib.request.Request(
|
|
292
|
+
signed_url,
|
|
293
|
+
data=markdown.encode("utf-8"),
|
|
294
|
+
headers=headers,
|
|
295
|
+
method="PUT",
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
with urllib.request.urlopen(req) as put_response:
|
|
299
|
+
if put_response.status != 200:
|
|
300
|
+
self.logger.error(
|
|
301
|
+
f"Error uploading markdown content: {put_response.status}"
|
|
302
|
+
)
|
|
303
|
+
return
|
|
304
|
+
self.logger.debug("Markdown content uploaded successfully.")
|
|
305
|
+
|
|
306
|
+
def add_to_policy_input(self, input_name: str, data: Dict[str, Any]) -> None:
|
|
307
|
+
"""
|
|
308
|
+
Add data to the policy input for the current Spacelift run.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
input_name: The name of the input to add (will be available as input.third_party_metadata.custom.{input_name} to the policy).
|
|
312
|
+
data: Dictionary containing data to add to the policy input
|
|
313
|
+
"""
|
|
314
|
+
if self._is_local:
|
|
315
|
+
self.logger.info(
|
|
316
|
+
"Spacelift run is local. Not writing custom policy input. Below is a preview of what would be written"
|
|
317
|
+
)
|
|
318
|
+
self.logger.info(json.dumps(data, indent=2))
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
with open(
|
|
322
|
+
f"{self._workspace_root}/{input_name}.custom.spacelift.json", "w"
|
|
323
|
+
) as f:
|
|
324
|
+
f.write(json.dumps(data))
|
|
231
325
|
|
|
232
326
|
# Hook methods - override these in your plugin
|
|
233
327
|
def before_init(self) -> None:
|
spaceforge/runner.py
CHANGED
|
@@ -4,7 +4,6 @@ Plugin runner for executing hook methods.
|
|
|
4
4
|
|
|
5
5
|
import importlib.util
|
|
6
6
|
import os
|
|
7
|
-
import sys
|
|
8
7
|
from typing import Optional
|
|
9
8
|
|
|
10
9
|
|
|
@@ -102,14 +101,3 @@ def runner_command(hook_name: str, plugin_file: str) -> None:
|
|
|
102
101
|
"""
|
|
103
102
|
runner = PluginRunner(plugin_file)
|
|
104
103
|
runner.run_hook(hook_name)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def main() -> None:
|
|
108
|
-
"""Legacy main entry point for backward compatibility."""
|
|
109
|
-
if len(sys.argv) != 2:
|
|
110
|
-
print("Usage: python -m spaceforge.runner <hook_name>")
|
|
111
|
-
sys.exit(1)
|
|
112
|
-
|
|
113
|
-
hook_name = sys.argv[1]
|
|
114
|
-
runner = PluginRunner()
|
|
115
|
-
runner.run_hook(hook_name)
|
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 (
|
|
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"
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
"type": "object"
|
|
119
119
|
},
|
|
120
120
|
"Parameter": {
|
|
121
|
-
"description": "A class to represent a parameter with a name and value.\n\nAttributes:\n name (str): The name of the parameter.\n description (str): A description of the parameter.\n sensitive (bool): Whether the parameter contains sensitive information.\n required (bool): Whether the parameter is required.\n default (Optional[str]): The default value of the parameter, if any. (required if sensitive is False)",
|
|
121
|
+
"description": "A class to represent a parameter with a name and value.\n\nAttributes:\n name (str): The name of the parameter.\n description (str): A description of the parameter.\n sensitive (bool): Whether the parameter contains sensitive information.\n required (bool): Whether the parameter is required.\n default (Optional[str]): The default value of the parameter, if any. (required if sensitive is False)\n id (str): Unique identifier for the parameter.",
|
|
122
122
|
"properties": {
|
|
123
123
|
"name": {
|
|
124
124
|
"title": "Name",
|
|
@@ -149,6 +149,10 @@
|
|
|
149
149
|
],
|
|
150
150
|
"default": null,
|
|
151
151
|
"title": "Default"
|
|
152
|
+
},
|
|
153
|
+
"id": {
|
|
154
|
+
"title": "Id",
|
|
155
|
+
"type": "string"
|
|
152
156
|
}
|
|
153
157
|
},
|
|
154
158
|
"required": [
|
|
@@ -159,7 +163,7 @@
|
|
|
159
163
|
"type": "object"
|
|
160
164
|
},
|
|
161
165
|
"Policy": {
|
|
162
|
-
"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.",
|
|
163
167
|
"properties": {
|
|
164
168
|
"name_prefix": {
|
|
165
169
|
"title": "Name Prefix",
|
|
@@ -176,10 +180,10 @@
|
|
|
176
180
|
"labels": {
|
|
177
181
|
"anyOf": [
|
|
178
182
|
{
|
|
179
|
-
"
|
|
183
|
+
"items": {
|
|
180
184
|
"type": "string"
|
|
181
185
|
},
|
|
182
|
-
"type": "
|
|
186
|
+
"type": "array"
|
|
183
187
|
},
|
|
184
188
|
{
|
|
185
189
|
"type": "null"
|
|
@@ -238,7 +242,7 @@
|
|
|
238
242
|
"type": "object"
|
|
239
243
|
},
|
|
240
244
|
"Webhook": {
|
|
241
|
-
"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",
|
|
242
246
|
"properties": {
|
|
243
247
|
"name_prefix": {
|
|
244
248
|
"title": "Name Prefix",
|
|
@@ -248,25 +252,15 @@
|
|
|
248
252
|
"title": "Endpoint",
|
|
249
253
|
"type": "string"
|
|
250
254
|
},
|
|
251
|
-
"
|
|
252
|
-
"
|
|
253
|
-
|
|
254
|
-
"additionalProperties": {
|
|
255
|
-
"type": "string"
|
|
256
|
-
},
|
|
257
|
-
"type": "object"
|
|
258
|
-
},
|
|
259
|
-
{
|
|
260
|
-
"type": "null"
|
|
261
|
-
}
|
|
262
|
-
],
|
|
263
|
-
"title": "Labels"
|
|
255
|
+
"secretFromParameter": {
|
|
256
|
+
"title": "Secretfromparameter",
|
|
257
|
+
"type": "string"
|
|
264
258
|
},
|
|
265
|
-
"
|
|
259
|
+
"labels": {
|
|
266
260
|
"anyOf": [
|
|
267
261
|
{
|
|
268
262
|
"items": {
|
|
269
|
-
"
|
|
263
|
+
"type": "string"
|
|
270
264
|
},
|
|
271
265
|
"type": "array"
|
|
272
266
|
},
|
|
@@ -274,21 +268,22 @@
|
|
|
274
268
|
"type": "null"
|
|
275
269
|
}
|
|
276
270
|
],
|
|
277
|
-
"title": "
|
|
271
|
+
"title": "Labels"
|
|
278
272
|
}
|
|
279
273
|
},
|
|
280
274
|
"required": [
|
|
281
275
|
"name_prefix",
|
|
282
|
-
"endpoint"
|
|
276
|
+
"endpoint",
|
|
277
|
+
"secretFromParameter"
|
|
283
278
|
],
|
|
284
279
|
"title": "Webhook",
|
|
285
280
|
"type": "object"
|
|
286
281
|
}
|
|
287
282
|
},
|
|
288
|
-
"description": "A class to represent the manifest of a Spacelift plugin.\n\nAttributes:\n
|
|
283
|
+
"description": "A class to represent the manifest of a Spacelift plugin.\n\nAttributes:\n name (str): The name of the plugin, will be appended with a unique ID.\n description (str): A description of the plugin.\n author (str): The author of the plugin.\n labels (list[str]): List of labels for the plugin.\n parameters (list[Parameter]): List of parameters for the plugin.\n contexts (list[Context]): List of contexts for the plugin.\n webhooks (list[Webhook]): List of webhooks for the plugin.\n policies (list[Policy]): List of policies for the plugin.",
|
|
289
284
|
"properties": {
|
|
290
|
-
"
|
|
291
|
-
"title": "Name
|
|
285
|
+
"name": {
|
|
286
|
+
"title": "Name",
|
|
292
287
|
"type": "string"
|
|
293
288
|
},
|
|
294
289
|
"version": {
|
|
@@ -303,6 +298,20 @@
|
|
|
303
298
|
"title": "Author",
|
|
304
299
|
"type": "string"
|
|
305
300
|
},
|
|
301
|
+
"labels": {
|
|
302
|
+
"anyOf": [
|
|
303
|
+
{
|
|
304
|
+
"items": {
|
|
305
|
+
"type": "string"
|
|
306
|
+
},
|
|
307
|
+
"type": "array"
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"type": "null"
|
|
311
|
+
}
|
|
312
|
+
],
|
|
313
|
+
"title": "Labels"
|
|
314
|
+
},
|
|
306
315
|
"parameters": {
|
|
307
316
|
"anyOf": [
|
|
308
317
|
{
|
|
@@ -361,7 +370,7 @@
|
|
|
361
370
|
}
|
|
362
371
|
},
|
|
363
372
|
"required": [
|
|
364
|
-
"
|
|
373
|
+
"name",
|
|
365
374
|
"version",
|
|
366
375
|
"description",
|
|
367
376
|
"author"
|
|
@@ -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}}
|