spaceforge 0.0.4__py3-none-any.whl → 0.0.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.4'
32
- __version_tuple__ = version_tuple = (0, 0, 4)
31
+ __version__ = version = '0.0.6'
32
+ __version_tuple__ = version_tuple = (0, 0, 6)
33
33
 
34
34
  __commit_id__ = commit_id = None
spaceforge/cls.py CHANGED
@@ -149,6 +149,9 @@ class Webhook:
149
149
  labels: Optional[List[str]] = optional_field
150
150
 
151
151
 
152
+ PolicyTypes = Literal["PUSH", "PLAN", "TRIGGER", "APPROVAL", "NOTIFICATION"]
153
+
154
+
152
155
  @pydantic_dataclass
153
156
  class Policy:
154
157
  """
@@ -162,7 +165,7 @@ class Policy:
162
165
  """
163
166
 
164
167
  name_prefix: str
165
- type: str
168
+ type: PolicyTypes
166
169
  body: str
167
170
  labels: Optional[List[str]] = optional_field
168
171
 
spaceforge/generator.py CHANGED
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union
8
8
 
9
9
  import yaml
10
10
  from jinja2 import Environment, PackageLoader, select_autoescape
11
+ from mergedeep import Strategy, merge # type: ignore
11
12
 
12
13
  if TYPE_CHECKING:
13
14
  from .plugin import SpaceforgePlugin
@@ -185,7 +186,10 @@ class PluginGenerator:
185
186
  )
186
187
 
187
188
  def _add_spaceforge_hooks(
188
- self, hooks: Dict[str, List[str]], mounted_files: List[MountedFile]
189
+ self,
190
+ hooks: Dict[str, List[str]],
191
+ mounted_files: List[MountedFile],
192
+ has_binaries: bool,
189
193
  ) -> None:
190
194
  # Add the spaceforge hook to actually run the plugin
191
195
  if self.config is None:
@@ -203,6 +207,7 @@ class PluginGenerator:
203
207
  plugin_path=directory,
204
208
  plugin_file=self.config["plugin_mounted_path"],
205
209
  phase=hook,
210
+ has_binaries=has_binaries,
206
211
  )
207
212
  self._add_to_mounted_files(hooks, mounted_files, hook, f"{hook}.sh", render)
208
213
 
@@ -244,8 +249,8 @@ class PluginGenerator:
244
249
 
245
250
  self._update_with_requirements(mounted_files)
246
251
  self._update_with_python_file(mounted_files)
247
- self._generate_binary_install_command(hooks, mounted_files)
248
- self._add_spaceforge_hooks(hooks, mounted_files)
252
+ has_binaries = self._generate_binary_install_command(hooks, mounted_files)
253
+ self._add_spaceforge_hooks(hooks, mounted_files, has_binaries)
249
254
 
250
255
  # Get the contexts and append the hooks and mounted files to it.
251
256
  if self.plugin_class is None:
@@ -270,8 +275,8 @@ class PluginGenerator:
270
275
  contexts[0].env = []
271
276
 
272
277
  # Add the hooks and mounted files to the first context
273
- contexts[0].hooks.update(hooks)
274
- contexts[0].mounted_files.extend(mounted_files)
278
+ merge(contexts[0].hooks, hooks, strategy=Strategy.TYPESAFE_ADDITIVE)
279
+ contexts[0].mounted_files += mounted_files
275
280
 
276
281
  self._map_variables_to_parameters(contexts)
277
282
 
@@ -279,10 +284,10 @@ class PluginGenerator:
279
284
 
280
285
  def _generate_binary_install_command(
281
286
  self, hooks: Dict[str, List[str]], mounted_files: List[MountedFile]
282
- ) -> None:
287
+ ) -> bool:
283
288
  binaries = self.get_plugin_binaries()
284
289
  if binaries is None:
285
- return None
290
+ return False
286
291
 
287
292
  for i, binary in enumerate(binaries):
288
293
  amd64_url = binary.download_urls.get("amd64", None)
@@ -309,6 +314,8 @@ class PluginGenerator:
309
314
  render,
310
315
  )
311
316
 
317
+ return True
318
+
312
319
  def get_plugin_binaries(self) -> Optional[List[Binary]]:
313
320
  """Get binary definitions from the plugin class."""
314
321
  return getattr(self.plugin_class, "__binaries__", None)
spaceforge/plugin.py CHANGED
@@ -36,21 +36,21 @@ class SpaceforgePlugin(ABC):
36
36
  self.logger = self._setup_logger()
37
37
 
38
38
  self._api_token = os.environ.get("SPACELIFT_API_TOKEN") or False
39
- self._spacelift_domain = (
39
+ self.spacelift_domain = (
40
40
  os.environ.get("TF_VAR_spacelift_graphql_endpoint") or False
41
41
  )
42
- self._api_enabled = bool(self._api_token and self._spacelift_domain)
42
+ self._api_enabled = bool(self._api_token and self.spacelift_domain)
43
43
  self._workspace_root = os.getcwd()
44
44
  self._spacelift_markdown_endpoint = None
45
45
 
46
46
  # This should be the last thing we do in the constructor
47
47
  # because we set api_enabled to false if the domain is set up incorrectly.
48
- if self._spacelift_domain and isinstance(self._spacelift_domain, str):
48
+ if self.spacelift_domain and isinstance(self.spacelift_domain, str):
49
49
  # this must occur after we check if spacelift domain is false
50
50
  # because the domain could be set but not start with https://
51
- if self._spacelift_domain.startswith("https://"):
52
- if self._spacelift_domain.endswith("/"):
53
- self._spacelift_domain = self._spacelift_domain[:-1]
51
+ if self.spacelift_domain.startswith("https://"):
52
+ if self.spacelift_domain.endswith("/"):
53
+ self.spacelift_domain = self.spacelift_domain[:-1]
54
54
  else:
55
55
  self.logger.warning(
56
56
  "SPACELIFT_DOMAIN does not start with https://, api calls will fail."
@@ -58,7 +58,7 @@ class SpaceforgePlugin(ABC):
58
58
  self._api_enabled = False
59
59
 
60
60
  if self._api_enabled:
61
- self._spacelift_markdown_endpoint = self._spacelift_domain.replace(
61
+ self._spacelift_markdown_endpoint = self.spacelift_domain.replace(
62
62
  "/graphql", "/worker/plugin_logs_url"
63
63
  )
64
64
 
@@ -200,7 +200,7 @@ class SpaceforgePlugin(ABC):
200
200
  data["variables"] = variables
201
201
 
202
202
  req = urllib.request.Request(
203
- self._spacelift_domain, # type: ignore[arg-type]
203
+ self.spacelift_domain, # type: ignore[arg-type]
204
204
  json.dumps(data).encode("utf-8"),
205
205
  headers,
206
206
  )
@@ -233,7 +233,7 @@ class SpaceforgePlugin(ABC):
233
233
  data: Dict[str, Any] = json.load(f)
234
234
  return data
235
235
 
236
- def send_markdown(self, markdown: str) -> None:
236
+ def send_markdown(self, markdown: str) -> bool:
237
237
  """
238
238
  Send a markdown message to the Spacelift run.
239
239
 
@@ -245,13 +245,13 @@ class SpaceforgePlugin(ABC):
245
245
  "Spacelift run is local. Not uploading markdown. Below is a preview of what would be sent"
246
246
  )
247
247
  self.logger.info(markdown)
248
- return
248
+ return True
249
249
 
250
250
  if self._spacelift_markdown_endpoint is None:
251
251
  self.logger.error(
252
252
  'API is not enabled, please export "SPACELIFT_API_TOKEN" and "TF_VAR_spacelift_graphql_endpoint".'
253
253
  )
254
- exit(1)
254
+ return False
255
255
 
256
256
  headers = {"Authorization": f"Bearer {self._api_token}"}
257
257
  body = {
@@ -266,26 +266,30 @@ class SpaceforgePlugin(ABC):
266
266
  method="POST",
267
267
  )
268
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
269
+ try:
270
+ with urllib.request.urlopen(req) as response:
271
+ if response.status != 200:
272
+ self.logger.error(
273
+ f"Error getting signed URL for markdown upload: {response}"
274
+ )
275
+ return False
276
+
277
+ raw_response = response.read().decode("utf-8")
278
+ self.logger.debug(raw_response)
279
+ resp: Dict[str, Any] = json.loads(raw_response)
280
+ if "url" not in resp or "headers" not in resp:
281
+ self.logger.error(
282
+ "Markdown signed url response does not contain 'url' or 'headers' key."
283
+ )
284
+ return False
284
285
 
285
- signed_url = resp["url"]
286
- headers = resp["headers"]
287
- headers["Content-Type"] = "text/markdown"
288
- headers["Content-Length"] = str(len(markdown))
286
+ signed_url = resp["url"]
287
+ headers = resp["headers"]
288
+ headers["Content-Type"] = "text/markdown"
289
+ headers["Content-Length"] = str(len(markdown))
290
+ except urllib.request.HTTPError as e:
291
+ self.logger.error(f"HTTP error occurred: {e.code} - {e.reason}")
292
+ return False
289
293
 
290
294
  # Now we upload the markdown content to the signed URL
291
295
  req = urllib.request.Request(
@@ -300,8 +304,9 @@ class SpaceforgePlugin(ABC):
300
304
  self.logger.error(
301
305
  f"Error uploading markdown content: {put_response.status}"
302
306
  )
303
- return
307
+ return False
304
308
  self.logger.debug("Markdown content uploaded successfully.")
309
+ return True
305
310
 
306
311
  def add_to_policy_input(self, input_name: str, data: Dict[str, Any]) -> None:
307
312
  """
spaceforge/schema.json CHANGED
@@ -170,6 +170,13 @@
170
170
  "type": "string"
171
171
  },
172
172
  "type": {
173
+ "enum": [
174
+ "PUSH",
175
+ "PLAN",
176
+ "TRIGGER",
177
+ "APPROVAL",
178
+ "NOTIFICATION"
179
+ ],
173
180
  "title": "Type",
174
181
  "type": "string"
175
182
  },
@@ -17,6 +17,8 @@ if [ -f requirements.txt ] && [ ! -f .spaceforge_installed_requirements ]; then
17
17
  pip install -r requirements.txt
18
18
  touch .spaceforge_installed_requirements
19
19
  fi
20
-
20
+ {% if has_binaries %}
21
+ export PATH="/mnt/workspace/plugins/plugin_binaries:$PATH"
22
+ {% endif %}
21
23
  cd /mnt/workspace/source/$TF_VAR_spacelift_project_root
22
24
  spaceforge runner --plugin-file {{plugin_file}} {{phase}}
@@ -72,7 +72,7 @@ class PluginExample(SpaceforgePlugin):
72
72
  __policies__ = [
73
73
  Policy(
74
74
  name_prefix="test_policy",
75
- type="notification",
75
+ type="NOTIFICATION",
76
76
  body="package test",
77
77
  labels=["type:security"],
78
78
  )
@@ -396,7 +396,7 @@ class NotAPlugin:
396
396
  assert policies is not None
397
397
  assert len(policies) == 1
398
398
  assert policies[0].name_prefix == "test_policy"
399
- assert policies[0].type == "notification"
399
+ assert policies[0].type == "NOTIFICATION"
400
400
  assert policies[0].body == "package test"
401
401
 
402
402
  def test_get_plugin_webhooks(self) -> None:
spaceforge/test_plugin.py CHANGED
@@ -21,7 +21,7 @@ class TestSpaceforgePluginInitialization:
21
21
 
22
22
  # Assert
23
23
  assert plugin._api_token is False
24
- assert plugin._spacelift_domain is False
24
+ assert plugin.spacelift_domain is False
25
25
  assert plugin._api_enabled is False
26
26
  assert plugin._workspace_root == os.getcwd()
27
27
  assert isinstance(plugin.logger, logging.Logger)
@@ -36,7 +36,7 @@ class TestSpaceforgePluginInitialization:
36
36
 
37
37
  # Assert
38
38
  assert plugin._api_token == "test_token"
39
- assert plugin._spacelift_domain == "https://test.spacelift.io"
39
+ assert plugin.spacelift_domain == "https://test.spacelift.io"
40
40
  assert plugin._api_enabled is True
41
41
  assert plugin._workspace_root == os.getcwd()
42
42
 
@@ -53,7 +53,7 @@ class TestSpaceforgePluginInitialization:
53
53
  plugin = SpaceforgePlugin()
54
54
 
55
55
  # Assert
56
- assert plugin._spacelift_domain == "https://test.spacelift.io"
56
+ assert plugin.spacelift_domain == "https://test.spacelift.io"
57
57
  assert plugin._api_enabled is True
58
58
 
59
59
  def test_should_disable_api_when_domain_has_no_https_prefix(self) -> None:
@@ -69,7 +69,7 @@ class TestSpaceforgePluginInitialization:
69
69
  plugin = SpaceforgePlugin()
70
70
 
71
71
  # Assert
72
- assert plugin._spacelift_domain == "test.spacelift.io"
72
+ assert plugin.spacelift_domain == "test.spacelift.io"
73
73
  assert plugin._api_enabled is False
74
74
 
75
75
  def test_should_disable_api_when_only_token_provided(self) -> None:
@@ -271,7 +271,7 @@ class TestSpaceforgePluginAPI:
271
271
  plugin = SpaceforgePlugin()
272
272
  plugin._api_enabled = True
273
273
  plugin._api_token = "test_token"
274
- plugin._spacelift_domain = "https://test.spacelift.io"
274
+ plugin.spacelift_domain = "https://test.spacelift.io"
275
275
 
276
276
  expected_data = {"data": {"test": "result"}}
277
277
  mock_api_response.read.return_value = json.dumps(expected_data).encode("utf-8")
@@ -305,7 +305,7 @@ class TestSpaceforgePluginAPI:
305
305
  plugin = SpaceforgePlugin()
306
306
  plugin._api_enabled = True
307
307
  plugin._api_token = "test_token"
308
- plugin._spacelift_domain = "https://test.spacelift.io"
308
+ plugin.spacelift_domain = "https://test.spacelift.io"
309
309
 
310
310
  mock_response_data = {"data": {"test": "result"}}
311
311
  mock_response = Mock()
@@ -331,7 +331,7 @@ class TestSpaceforgePluginAPI:
331
331
  plugin = SpaceforgePlugin()
332
332
  plugin._api_enabled = True
333
333
  plugin._api_token = "test_token"
334
- plugin._spacelift_domain = "https://test.spacelift.io"
334
+ plugin.spacelift_domain = "https://test.spacelift.io"
335
335
 
336
336
  mock_response_data = {"errors": [{"message": "Test error"}]}
337
337
  mock_response = Mock()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spaceforge
3
- Version: 0.0.4
3
+ Version: 0.0.6
4
4
  Summary: A Python framework for building Spacelift plugins
5
5
  Home-page: https://github.com/spacelift-io/plugins
6
6
  Author: Spacelift
@@ -29,6 +29,7 @@ Requires-Dist: PyYAML>=6.0
29
29
  Requires-Dist: click>=8.0.0
30
30
  Requires-Dist: pydantic>=2.11.7
31
31
  Requires-Dist: Jinja2>=3.1.0
32
+ Requires-Dist: mergedeep>=1.3.4
32
33
  Provides-Extra: dev
33
34
  Requires-Dist: pytest>=6.0; extra == "dev"
34
35
  Requires-Dist: pytest-cov; extra == "dev"
@@ -2,20 +2,20 @@ 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=QlXZ5JTjE_pgpDaeHk0GTExkc75xUZFmd0hA7kGYCJ0,704
6
- spaceforge/cls.py,sha256=3Js2i0kfWBfa9g-VthuARc3WGwrbdpiQzbQYij59dMo,5981
5
+ spaceforge/_version_scm.py,sha256=7MyqQ3iPP2mJruPfRYGCNCq1z7_Nk7c-eyYecYITxsY,704
6
+ spaceforge/cls.py,sha256=lCGtlMv-rFFVnvfFaq36-GRqLiINswLD78amZigZo7I,6068
7
7
  spaceforge/conftest.py,sha256=U-xCavCsgRAQXqflIIOMeq9pcGbeqRviUNkEXgZol8g,2141
8
- spaceforge/generator.py,sha256=kwWEyVqKaOIgZI4nmu4q30ZefA7w10vj78PmROWW5V4,16484
9
- spaceforge/plugin.py,sha256=8qPDieG2i6pFBg02GYXSb06DU9xR51b20kU4X9Kv6Ic,13051
8
+ spaceforge/generator.py,sha256=-1lO_1bhaM85-aULpPE2TeG4SlaznETG3YfyucKALns,16709
9
+ spaceforge/plugin.py,sha256=PdQpBle5eU_N__2eArJgNe2BEnN3mL_SMeM6yjnBtGw,13322
10
10
  spaceforge/runner.py,sha256=aBQQLG8MFVrqCzM0X8HgERYbhLKbmlOHUPKjV7-xvpA,3163
11
- spaceforge/schema.json,sha256=syoxxTJuZ9Wn91vLRHeffyDi7xYHjwSZ14AheugLzhY,10336
11
+ spaceforge/schema.json,sha256=scQyV71IlbB5kipAhc880n-PG-FH8BeVk9FWFoA3Wtw,10483
12
12
  spaceforge/test_cls.py,sha256=nXAgbnFnGdFxrtA7vNXiePjNUASuoYW-lEuQGx9WMGs,468
13
- spaceforge/test_generator.py,sha256=liCccHjGQ7SmKc1RCrceS18bisPA9H0FSvMbgOIZ-Sg,32615
13
+ spaceforge/test_generator.py,sha256=Nst3YVu_iZbFopH6ajjxCfqYrZvybteGbwMfZzjBFnI,32615
14
14
  spaceforge/test_generator_binaries.py,sha256=X_7pPLGE45eQt-Kv9_ku__LsyLgOvViHc_BvVpSCMp0,7263
15
15
  spaceforge/test_generator_core.py,sha256=gOqRx0rnME-srGMHun4KidXMN-iaqfKKTyoQ0Tw6b9Q,6253
16
16
  spaceforge/test_generator_hooks.py,sha256=2lJs8dYlFb7QehWcYF0O4qg38s5UudEpzJyBi1XiS3k,2542
17
17
  spaceforge/test_generator_parameters.py,sha256=77az9rcocFny2AC4O2eTzjCW712fR1DBHzGrgBKeR4w,1878
18
- spaceforge/test_plugin.py,sha256=pFst9RUpTujuv_zdhWnB9LMeB_vN9qHLNEhnmo8WzkQ,13046
18
+ spaceforge/test_plugin.py,sha256=Q4ztmpDV8Xpr0HVvYXHyAm_VHv8TzLXdN5PHeMI9KnI,13039
19
19
  spaceforge/test_plugin_file_operations.py,sha256=B0qvIo5EcfKMiHLhBv-hAnpSonn83ojcmJHXasydojA,3782
20
20
  spaceforge/test_plugin_hooks.py,sha256=rNCZZyd_SDMkm1x3yl5mjQ5tBMGm3YNd1U6h_niWRQs,2962
21
21
  spaceforge/test_plugin_inheritance.py,sha256=WHfvU5s-2GtfcI9-1bHXH7bacr77ikq68V3Z3BBQKvQ,3617
@@ -24,10 +24,10 @@ spaceforge/test_runner_cli.py,sha256=Sf5X0O9Wc9EhGB5L8SzvlmO7QmgQZQoClSdNYefa-lQ
24
24
  spaceforge/test_runner_core.py,sha256=eNR9YOwJwv7LsMtNQ4WXXMPIW6RE_A7hUp4bCpzz1Rk,3941
25
25
  spaceforge/test_runner_execution.py,sha256=BRlOApCmlpjE9YYvqHc8creSRu2vyPubzOCVXv-on0w,5335
26
26
  spaceforge/templates/binary_install.sh.j2,sha256=UjP_kAdvkkATKds2nqk4ouqLhuIfCgM_x0WQIl1hbIo,477
27
- spaceforge/templates/ensure_spaceforge_and_run.sh.j2,sha256=qU5UvJhxh8toLrTSH3fUv7UPFaKo7Z1mQCV_bpToDB4,458
28
- spaceforge-0.0.4.dist-info/licenses/LICENSE,sha256=wyljRrfnWY2ggQKkSCg3Nw2hxwPMmupopaKs9Kpgys8,1065
29
- spaceforge-0.0.4.dist-info/METADATA,sha256=6o_AicFSDgu2TnQZIodI7barxJODWNgfMoRtd9DyLzA,16771
30
- spaceforge-0.0.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
- spaceforge-0.0.4.dist-info/entry_points.txt,sha256=qawuuKBSNTGg-njnQnhxxFldFvXYAPej6bF_f3iyQ48,56
32
- spaceforge-0.0.4.dist-info/top_level.txt,sha256=eVw-Lw4Th0oHM8Gx1Y8YetyNgbNbMBU00yWs-kwGeSs,11
33
- spaceforge-0.0.4.dist-info/RECORD,,
27
+ spaceforge/templates/ensure_spaceforge_and_run.sh.j2,sha256=g5BldIEve0IkZ-mCzTXfB_rFvyWqUJqymRRaaMrpp0s,550
28
+ spaceforge-0.0.6.dist-info/licenses/LICENSE,sha256=wyljRrfnWY2ggQKkSCg3Nw2hxwPMmupopaKs9Kpgys8,1065
29
+ spaceforge-0.0.6.dist-info/METADATA,sha256=OwvM8wjFNiRS5kDfYTClgo9H9ZAAMIMOlkCXmqnDMac,16803
30
+ spaceforge-0.0.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
+ spaceforge-0.0.6.dist-info/entry_points.txt,sha256=qawuuKBSNTGg-njnQnhxxFldFvXYAPej6bF_f3iyQ48,56
32
+ spaceforge-0.0.6.dist-info/top_level.txt,sha256=eVw-Lw4Th0oHM8Gx1Y8YetyNgbNbMBU00yWs-kwGeSs,11
33
+ spaceforge-0.0.6.dist-info/RECORD,,