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.
Files changed (35) hide show
  1. spaceforge/__init__.py +12 -4
  2. spaceforge/__main__.py +3 -3
  3. spaceforge/_version.py +0 -1
  4. spaceforge/_version_scm.py +2 -2
  5. spaceforge/cls.py +16 -12
  6. spaceforge/conftest.py +89 -0
  7. spaceforge/generator.py +119 -54
  8. spaceforge/plugin.py +106 -12
  9. spaceforge/runner.py +0 -12
  10. spaceforge/schema.json +38 -29
  11. spaceforge/templates/binary_install.sh.j2 +23 -0
  12. spaceforge/templates/ensure_spaceforge_and_run.sh.j2 +22 -0
  13. spaceforge/{generator_test.py → test_generator.py} +263 -51
  14. spaceforge/test_generator_binaries.py +194 -0
  15. spaceforge/test_generator_core.py +180 -0
  16. spaceforge/test_generator_hooks.py +90 -0
  17. spaceforge/test_generator_parameters.py +59 -0
  18. spaceforge/test_plugin.py +357 -0
  19. spaceforge/test_plugin_file_operations.py +118 -0
  20. spaceforge/test_plugin_hooks.py +100 -0
  21. spaceforge/test_plugin_inheritance.py +102 -0
  22. spaceforge/{runner_test.py → test_runner.py} +2 -65
  23. spaceforge/test_runner_cli.py +69 -0
  24. spaceforge/test_runner_core.py +124 -0
  25. spaceforge/test_runner_execution.py +169 -0
  26. spaceforge-0.0.4.dist-info/METADATA +605 -0
  27. spaceforge-0.0.4.dist-info/RECORD +33 -0
  28. spaceforge/plugin_test.py +0 -621
  29. spaceforge-0.0.2.dist-info/METADATA +0 -163
  30. spaceforge-0.0.2.dist-info/RECORD +0 -20
  31. /spaceforge/{cls_test.py → test_cls.py} +0 -0
  32. {spaceforge-0.0.2.dist-info → spaceforge-0.0.4.dist-info}/WHEEL +0 -0
  33. {spaceforge-0.0.2.dist-info → spaceforge-0.0.4.dist-info}/entry_points.txt +0 -0
  34. {spaceforge-0.0.2.dist-info → spaceforge-0.0.4.dist-info}/licenses/LICENSE +0 -0
  35. {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, Union
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", False)
37
- self._spacelift_domain = os.environ.get(
38
- "TF_VAR_spacelift_graphql_endpoint", False
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 != False and self._spacelift_domain != False
41
- self._workspace_root = os.environ.get("WORKSPACE_ROOT", os.getcwd())
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 = os.environ.get("TF_VAR_spacelift_run_id", "local")
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 "SPACELIFT_DOMAIN".'
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
- f"{self._spacelift_domain}/graphql",
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
- # TODO
230
- print(markdown)
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 (dict): 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.",
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
- "additionalProperties": {
77
+ "items": {
78
78
  "type": "string"
79
79
  },
80
- "type": "object"
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[dict[str, str]]): Labels associated with the 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[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
- "additionalProperties": {
183
+ "items": {
180
184
  "type": "string"
181
185
  },
182
- "type": "object"
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[dict]): Labels associated with the webhook.\n secrets (Optional[list[Variable]]): List of secrets associated with the 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[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
- "labels": {
252
- "anyOf": [
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
- "secrets": {
259
+ "labels": {
266
260
  "anyOf": [
267
261
  {
268
262
  "items": {
269
- "$ref": "#/$defs/Variable"
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": "Secrets"
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 name_prefix (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 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.",
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
- "name_prefix": {
291
- "title": "Name Prefix",
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
- "name_prefix",
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}}