spaceforge 0.1.0.dev0__py3-none-any.whl → 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +34 -0
  5. spaceforge/cls.py +24 -14
  6. spaceforge/conftest.py +89 -0
  7. spaceforge/generator.py +129 -56
  8. spaceforge/plugin.py +199 -22
  9. spaceforge/runner.py +3 -15
  10. spaceforge/schema.json +45 -22
  11. spaceforge/templates/binary_install.sh.j2 +24 -0
  12. spaceforge/templates/ensure_spaceforge_and_run.sh.j2 +24 -0
  13. spaceforge/{generator_test.py → test_generator.py} +265 -53
  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} +5 -68
  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-1.0.1.dist-info/METADATA +606 -0
  27. spaceforge-1.0.1.dist-info/RECORD +33 -0
  28. spaceforge/plugin_test.py +0 -621
  29. spaceforge-0.1.0.dev0.dist-info/METADATA +0 -163
  30. spaceforge-0.1.0.dev0.dist-info/RECORD +0 -19
  31. /spaceforge/{cls_test.py → test_cls.py} +0 -0
  32. {spaceforge-0.1.0.dev0.dist-info → spaceforge-1.0.1.dist-info}/WHEEL +0 -0
  33. {spaceforge-0.1.0.dev0.dist-info → spaceforge-1.0.1.dist-info}/entry_points.txt +0 -0
  34. {spaceforge-0.1.0.dev0.dist-info → spaceforge-1.0.1.dist-info}/licenses/LICENSE +0 -0
  35. {spaceforge-0.1.0.dev0.dist-info → spaceforge-1.0.1.dist-info}/top_level.txt +0 -0
spaceforge/__init__.py CHANGED
@@ -4,10 +4,18 @@ Spaceforge - Spacelift Plugin Framework
4
4
  A Python framework for building Spacelift plugins with hook-based functionality.
5
5
  """
6
6
 
7
- from ._version import get_version
8
- from .cls import Binary, Context, MountedFile, Parameter, Policy, Variable, Webhook
9
- from .plugin import SpaceforgePlugin
10
- from .runner import PluginRunner
7
+ from spaceforge._version import get_version
8
+ from spaceforge.cls import (
9
+ Binary,
10
+ Context,
11
+ MountedFile,
12
+ Parameter,
13
+ Policy,
14
+ Variable,
15
+ Webhook,
16
+ )
17
+ from spaceforge.plugin import SpaceforgePlugin
18
+ from spaceforge.runner import PluginRunner
11
19
 
12
20
  __version__ = get_version()
13
21
  __all__ = [
spaceforge/__main__.py CHANGED
@@ -4,9 +4,9 @@ Main entry point for spaceforge module.
4
4
 
5
5
  import click
6
6
 
7
- from ._version import get_version
8
- from .generator import generate_command
9
- from .runner import runner_command
7
+ from spaceforge._version import get_version
8
+ from spaceforge.generator import generate_command
9
+ from spaceforge.runner import runner_command
10
10
 
11
11
 
12
12
  @click.group()
spaceforge/_version.py CHANGED
@@ -3,7 +3,6 @@ Dynamic version detection from git tags.
3
3
  """
4
4
 
5
5
  import subprocess
6
- import sys
7
6
  from typing import Optional
8
7
 
9
8
 
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '1.0.1'
32
+ __version_tuple__ = version_tuple = (1, 0, 1)
33
+
34
+ __commit_id__ = commit_id = None
spaceforge/cls.py CHANGED
@@ -1,3 +1,4 @@
1
+ import uuid
1
2
  from typing import Dict, List, Literal, Optional
2
3
 
3
4
  from pydantic import Field
@@ -19,8 +20,7 @@ class Binary:
19
20
 
20
21
  Attributes:
21
22
  name (str): The name of the binary file.
22
- path (str): The path to the binary file.
23
- sensitive (bool): Whether the binary file is sensitive.
23
+ download_urls (Dict[BinaryType, str]): A dictionary mapping binary types to their download URLs.
24
24
  """
25
25
 
26
26
  name: str
@@ -38,6 +38,7 @@ class Parameter:
38
38
  sensitive (bool): Whether the parameter contains sensitive information.
39
39
  required (bool): Whether the parameter is required.
40
40
  default (Optional[str]): The default value of the parameter, if any. (required if sensitive is False)
41
+ id (str): Unique identifier for the parameter.
41
42
  """
42
43
 
43
44
  name: str
@@ -45,6 +46,7 @@ class Parameter:
45
46
  sensitive: bool = False
46
47
  required: bool = False
47
48
  default: Optional[str] = None
49
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()))
48
50
 
49
51
  def __post_init__(self) -> None:
50
52
  if not self.required and self.default is None:
@@ -116,7 +118,7 @@ class Context:
116
118
  Attributes:
117
119
  name_prefix (str): The name of the context, will be appended with a unique ID.
118
120
  description (str): A description of the context.
119
- labels (dict): Labels associated with the context.
121
+ labels (Optional[List[str]]): Labels associated with the context.
120
122
  env (list): List of variables associated with the context.
121
123
  hooks (dict): Hooks associated with the context.
122
124
  """
@@ -126,7 +128,7 @@ class Context:
126
128
  env: Optional[List[Variable]] = optional_field
127
129
  mounted_files: Optional[List[MountedFile]] = optional_field
128
130
  hooks: Optional[Dict[HookType, List[str]]] = optional_field
129
- labels: Optional[Dict[str, str]] = optional_field
131
+ labels: Optional[List[str]] = optional_field
130
132
 
131
133
 
132
134
  @pydantic_dataclass
@@ -137,14 +139,17 @@ class Webhook:
137
139
  Attributes:
138
140
  name_prefix (str): The name of the webhook, will be appended with a unique ID.
139
141
  endpoint (str): The URL endpoint for the webhook.
140
- labels (Optional[dict]): Labels associated with the webhook.
141
- secrets (Optional[list[Variable]]): List of secrets associated with the webhook.
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
142
144
  """
143
145
 
144
146
  name_prefix: str
145
147
  endpoint: str
146
- labels: Optional[Dict[str, str]] = optional_field
147
- secrets: Optional[List[Variable]] = optional_field
148
+ secretFromParameter: Optional[str] = optional_field
149
+ labels: Optional[List[str]] = optional_field
150
+
151
+
152
+ PolicyTypes = Literal["PUSH", "PLAN", "TRIGGER", "APPROVAL", "NOTIFICATION"]
148
153
 
149
154
 
150
155
  @pydantic_dataclass
@@ -156,13 +161,13 @@ class Policy:
156
161
  name_prefix (str): The name of the policy, will be appended with a unique ID.
157
162
  type (str): The type of the policy (e.g., "terraform", "kubernetes").
158
163
  body (str): The body of the policy, typically a configuration or script.
159
- labels (Optional[dict[str, str]]): Labels associated with the policy.
164
+ labels (Optional[List[str]]): Labels associated with the policy.
160
165
  """
161
166
 
162
167
  name_prefix: str
163
- type: str
168
+ type: PolicyTypes
164
169
  body: str
165
- labels: Optional[Dict[str, str]] = optional_field
170
+ labels: Optional[List[str]] = optional_field
166
171
 
167
172
 
168
173
  @pydantic_dataclass
@@ -171,19 +176,21 @@ class PluginManifest:
171
176
  A class to represent the manifest of a Spacelift plugin.
172
177
 
173
178
  Attributes:
174
- name_prefix (str): The name of the plugin, will be appended with a unique ID.
179
+ name (str): The name of the plugin, will be appended with a unique ID.
175
180
  description (str): A description of the plugin.
176
181
  author (str): The author of the plugin.
182
+ labels (list[str]): List of labels for the plugin.
177
183
  parameters (list[Parameter]): List of parameters for the plugin.
178
184
  contexts (list[Context]): List of contexts for the plugin.
179
185
  webhooks (list[Webhook]): List of webhooks for the plugin.
180
186
  policies (list[Policy]): List of policies for the plugin.
181
187
  """
182
188
 
183
- name_prefix: str
189
+ name: str
184
190
  version: str
185
191
  description: str
186
192
  author: str
193
+ labels: Optional[List[str]] = optional_field
187
194
  parameters: Optional[List[Parameter]] = optional_field
188
195
  contexts: Optional[List[Context]] = optional_field
189
196
  webhooks: Optional[List[Webhook]] = optional_field
@@ -195,4 +202,7 @@ if __name__ == "__main__":
195
202
 
196
203
  from pydantic import TypeAdapter
197
204
 
198
- print(json.dumps(TypeAdapter(PluginManifest).json_schema(), indent=2))
205
+ schema = TypeAdapter(PluginManifest).json_schema()
206
+ schema["$schema"] = "http://json-schema.org/draft-07/schema#"
207
+
208
+ print(json.dumps(schema, indent=2))
spaceforge/conftest.py ADDED
@@ -0,0 +1,89 @@
1
+ """Shared test fixtures for spaceforge tests."""
2
+
3
+ import os
4
+ import tempfile
5
+ from typing import Dict, Generator, List
6
+ from unittest.mock import Mock
7
+
8
+ import pytest
9
+
10
+ from spaceforge.plugin import SpaceforgePlugin
11
+
12
+
13
+ @pytest.fixture
14
+ def temp_dir() -> Generator[str, None, None]:
15
+ """Provide a temporary directory for test files."""
16
+ temp_dir = tempfile.mkdtemp()
17
+ yield temp_dir
18
+ import shutil
19
+
20
+ shutil.rmtree(temp_dir, ignore_errors=True)
21
+
22
+
23
+ @pytest.fixture
24
+ def test_plugin_content() -> str:
25
+ """Basic test plugin content."""
26
+ return """
27
+ from spaceforge import SpaceforgePlugin, Parameter
28
+
29
+ class TestPlugin(SpaceforgePlugin):
30
+ __plugin_name__ = "test"
31
+ __version__ = "1.0.0"
32
+ __author__ = "Test Author"
33
+
34
+ __parameters__ = [
35
+ Parameter(
36
+ name="test_param",
37
+ description="Test parameter",
38
+ required=False,
39
+ default="default_value"
40
+ )
41
+ ]
42
+
43
+ def after_plan(self) -> None:
44
+ pass
45
+ """
46
+
47
+
48
+ @pytest.fixture
49
+ def test_plugin_file(temp_dir: str, test_plugin_content: str) -> str:
50
+ """Create a test plugin file."""
51
+ plugin_path = os.path.join(temp_dir, "plugin.py")
52
+ with open(plugin_path, "w") as f:
53
+ f.write(test_plugin_content)
54
+ return plugin_path
55
+
56
+
57
+ @pytest.fixture
58
+ def mock_env() -> Dict[str, str]:
59
+ """Common test environment variables."""
60
+ return {
61
+ "SPACELIFT_API_TOKEN": "test_token",
62
+ "TF_VAR_spacelift_graphql_endpoint": "https://test.spacelift.io",
63
+ }
64
+
65
+
66
+ @pytest.fixture
67
+ def mock_api_response() -> Mock:
68
+ """Mock API response for testing."""
69
+ mock_response = Mock()
70
+ mock_response.read.return_value = b'{"data": {"test": "result"}}'
71
+ return mock_response
72
+
73
+
74
+ class ExampleTestPlugin(SpaceforgePlugin):
75
+ """Reusable test plugin class."""
76
+
77
+ __plugin_name__ = "example"
78
+ __version__ = "1.0.0"
79
+ __author__ = "Test"
80
+
81
+ def __init__(self) -> None:
82
+ super().__init__()
83
+ self.hook_calls: List[str] = []
84
+
85
+ def after_plan(self) -> None:
86
+ self.hook_calls.append("after_plan")
87
+
88
+ def before_apply(self) -> None:
89
+ self.hook_calls.append("before_apply")
spaceforge/generator.py CHANGED
@@ -4,9 +4,11 @@ YAML generator for Spacelift plugins.
4
4
 
5
5
  import importlib.util
6
6
  import os
7
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type
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
11
+ from mergedeep import Strategy, merge # type: ignore
10
12
 
11
13
  if TYPE_CHECKING:
12
14
  from .plugin import SpaceforgePlugin
@@ -46,6 +48,10 @@ class PluginGenerator:
46
48
  self.plugin_class: Optional[Type[SpaceforgePlugin]] = None
47
49
  self.plugin_instance: Optional[SpaceforgePlugin] = None
48
50
  self.plugin_working_directory: Optional[str] = None
51
+ self.config: Optional[Dict[str, Any]] = None
52
+ self.jinja = Environment(
53
+ loader=PackageLoader("spaceforge"), autoescape=select_autoescape()
54
+ )
49
55
 
50
56
  def load_plugin(self) -> None:
51
57
  """Load the plugin class from the specified path."""
@@ -78,10 +84,18 @@ class PluginGenerator:
78
84
  self.plugin_class = plugin_class
79
85
  self.plugin_instance = plugin_class()
80
86
  self.plugin_working_directory = (
81
- "/mnt/workspace/plugins/" + plugin_class.__plugin_name__.lower()
87
+ "/mnt/workspace/plugins/"
88
+ + plugin_class.__plugin_name__.lower().replace(" ", "_")
82
89
  )
90
+ self.config = {
91
+ "setup_virtual_env": (
92
+ f"cd {self.plugin_working_directory} && python -m venv ./venv && "
93
+ + "source venv/bin/activate && (command -v spaceforge &> /dev/null || pip install spaceforge)"
94
+ ),
95
+ "plugin_mounted_path": f"{self.plugin_working_directory}/{os.path.basename(self.plugin_path)}",
96
+ }
83
97
 
84
- def get_plugin_metadata(self) -> Dict[str, str]:
98
+ def get_plugin_metadata(self) -> Dict[str, Union[str, List[str]]]:
85
99
  """Extract metadata from the plugin class."""
86
100
  if self.plugin_class is None:
87
101
  raise ValueError("Plugin class not loaded. Call load_plugin() first.")
@@ -97,6 +111,7 @@ class PluginGenerator:
97
111
  self.plugin_class.__name__.lower().replace("plugin", ""),
98
112
  ),
99
113
  "version": getattr(self.plugin_class, "__version__", "1.0.0"),
114
+ "labels": getattr(self.plugin_class, "__labels__", []),
100
115
  "description": doc
101
116
  or f"A Spacelift plugin built with {self.plugin_class.__name__}",
102
117
  "author": getattr(self.plugin_class, "__author__", "Unknown"),
@@ -128,22 +143,27 @@ class PluginGenerator:
128
143
 
129
144
  return hook_methods
130
145
 
131
- def get_plugin_contexts(self) -> List[Context]:
132
- """Get context definitions from the plugin class."""
133
-
134
- change_to_working_directory = f"cd {self.plugin_working_directory}"
135
- hooks = {
136
- "before_init": [
137
- f"mkdir -p {self.plugin_working_directory}",
138
- ],
139
- }
140
- mounted_files = []
141
-
142
- # Add a virtual environment before_init hook if requirements.txt exists
143
- if os.path.exists("requirements.txt"):
144
- hooks["before_init"].append(
145
- f"{change_to_working_directory} && python -m venv ./venv && source venv/bin/activate && pip install -r requirements.txt"
146
+ def _add_to_mounted_files(
147
+ self,
148
+ hooks: Dict[str, List[str]],
149
+ mounted_files: List[MountedFile],
150
+ phase: str,
151
+ filepath: str,
152
+ filecontent: str,
153
+ ) -> None:
154
+ file = f"{self.plugin_working_directory}/{filepath}"
155
+ hooks[phase].append(f"chmod +x {file} && {file}")
156
+ mounted_files.append(
157
+ MountedFile(
158
+ path=f"{self.plugin_working_directory}/{filepath}",
159
+ content=filecontent,
160
+ sensitive=False,
146
161
  )
162
+ )
163
+
164
+ def _update_with_requirements(self, mounted_files: List[MountedFile]) -> None:
165
+ """Update the plugin hooks if there is a requirements.txt"""
166
+ if os.path.exists("requirements.txt") and self.config is not None:
147
167
  # read the requirements.txt file
148
168
  with open("requirements.txt", "r") as f:
149
169
  mounted_files.append(
@@ -154,33 +174,84 @@ class PluginGenerator:
154
174
  )
155
175
  )
156
176
 
157
- # Ensure the plugin file itself is mounted
158
- plugin_mounted_path = (
159
- f"{self.plugin_working_directory}/{os.path.basename(self.plugin_path)}"
160
- )
161
- if os.path.exists(self.plugin_path):
177
+ def _update_with_python_file(self, mounted_files: List[MountedFile]) -> None:
178
+ """Ensure the plugin file itself is mounted."""
179
+ if os.path.exists(self.plugin_path) and self.config is not None:
162
180
  with open(self.plugin_path, "r") as f:
163
181
  mounted_files.append(
164
182
  MountedFile(
165
- path=plugin_mounted_path,
183
+ path=self.config["plugin_mounted_path"],
166
184
  content=f.read(),
167
185
  sensitive=False,
168
186
  )
169
187
  )
170
188
 
171
- binary_cmd = self.generate_binary_install_command()
172
- if binary_cmd != "":
173
- hooks["before_init"].append(binary_cmd)
174
-
189
+ def _add_spaceforge_hooks(
190
+ self,
191
+ hooks: Dict[str, List[str]],
192
+ mounted_files: List[MountedFile],
193
+ has_binaries: bool,
194
+ ) -> None:
175
195
  # Add the spaceforge hook to actually run the plugin
196
+ if self.config is None:
197
+ raise ValueError("Plugin config not set. Call load_plugin() first.")
198
+
176
199
  available_hooks = self.get_available_hooks()
177
200
  for hook in available_hooks:
178
201
  # Ensure the hook exists in the first context
179
202
  if hook not in hooks:
180
203
  hooks[hook] = []
181
- hooks[hook].append(
182
- f"{change_to_working_directory} && python -m spaceforge runner --plugin-file {plugin_mounted_path} {hook}"
204
+
205
+ directory = os.path.dirname(self.config["plugin_mounted_path"])
206
+ template = self.jinja.get_template("ensure_spaceforge_and_run.sh.j2")
207
+ render = template.render(
208
+ plugin_path=directory,
209
+ plugin_file=self.config["plugin_mounted_path"],
210
+ phase=hook,
211
+ has_binaries=has_binaries,
183
212
  )
213
+ self._add_to_mounted_files(hooks, mounted_files, hook, f"{hook}.sh", render)
214
+
215
+ def _map_variables_to_parameters(self, contexts: List[Context]) -> None:
216
+ for context in contexts:
217
+ # Get the variables from the plugin and change the value_from_parameter to the ID of the parameter
218
+ # based on its name.
219
+ if context.env is None:
220
+ continue
221
+
222
+ for variable in context.env:
223
+ if variable.value_from_parameter:
224
+ parameter_name = variable.value_from_parameter
225
+ parameters = self.get_plugin_parameters()
226
+ if parameters:
227
+ parameter = next(
228
+ (
229
+ p
230
+ for p in parameters
231
+ if p.name == parameter_name or p.id == parameter_name
232
+ ),
233
+ None,
234
+ )
235
+ if parameter:
236
+ variable.value_from_parameter = parameter.id
237
+ else:
238
+ raise ValueError(
239
+ f"Parameter {parameter_name} not found for variable {variable.key}"
240
+ )
241
+
242
+ def get_plugin_contexts(self) -> List[Context]:
243
+ """Get context definitions from the plugin class."""
244
+
245
+ f"cd {self.plugin_working_directory}"
246
+ hooks: Dict[str, List[str]] = {
247
+ "before_init": [f"mkdir -p {self.plugin_working_directory}"]
248
+ }
249
+ mounted_files: List[MountedFile] = []
250
+
251
+ self._update_with_requirements(mounted_files)
252
+ self._update_with_python_file(mounted_files)
253
+ has_binaries = self._generate_binary_install_command(hooks, mounted_files)
254
+ self._add_spaceforge_hooks(hooks, mounted_files, has_binaries)
184
255
 
185
256
  # Get the contexts and append the hooks and mounted files to it.
186
257
  if self.plugin_class is None:
@@ -205,45 +276,46 @@ class PluginGenerator:
205
276
  contexts[0].env = []
206
277
 
207
278
  # Add the hooks and mounted files to the first context
208
- contexts[0].hooks.update(hooks)
209
- contexts[0].mounted_files.extend(mounted_files)
279
+ merge(contexts[0].hooks, hooks, strategy=Strategy.TYPESAFE_ADDITIVE)
280
+ contexts[0].mounted_files += mounted_files
281
+
282
+ self._map_variables_to_parameters(contexts)
210
283
 
211
284
  return contexts
212
285
 
213
- def generate_binary_install_command(self) -> str:
286
+ def _generate_binary_install_command(
287
+ self, hooks: Dict[str, List[str]], mounted_files: List[MountedFile]
288
+ ) -> bool:
214
289
  binaries = self.get_plugin_binaries()
215
290
  if binaries is None:
216
- return ""
291
+ return False
217
292
 
218
- binary_cmd = ""
219
- if len(binaries) > 0:
220
- binary_cmd = f"mkdir -p {static_binary_directory} && cd {static_binary_directory} && "
221
293
  for i, binary in enumerate(binaries):
222
294
  amd64_url = binary.download_urls.get("amd64", None)
223
295
  arm64_url = binary.download_urls.get("arm64", None)
296
+ binary_path = f"{static_binary_directory}/{binary.name}"
224
297
  if amd64_url is None and arm64_url is None:
225
298
  raise ValueError(
226
299
  f"Binary {binary.name} must have at least one download URL defined (amd64 or arm64)"
227
300
  )
228
301
 
229
- binary_path = f"{static_binary_directory}/{binary.name}"
230
- amd64_download_command = (
231
- f"curl {amd64_url} -o {binary_path} -L && chmod +x {binary_path}"
232
- if amd64_url is not None
233
- else "echo 'amd64 binary not available' && exit 1"
302
+ template = self.jinja.get_template("binary_install.sh.j2")
303
+ render = template.render(
304
+ binary=binary,
305
+ amd64_url=amd64_url,
306
+ arm64_url=arm64_url,
307
+ binary_path=binary_path,
308
+ static_binary_directory=static_binary_directory,
234
309
  )
235
- arm64_download_command = (
236
- f"curl {arm64_url} -o {binary_path} -L && chmod +x {binary_path}"
237
- if arm64_url is not None
238
- else "echo 'arm64 binary not available' && exit 1"
310
+ self._add_to_mounted_files(
311
+ hooks,
312
+ mounted_files,
313
+ "before_init",
314
+ f"binary_install_{binary.name}.sh",
315
+ render,
239
316
  )
240
317
 
241
- binary_cmd += '([[ "$(echo "$(arch)")" == "x86_64" ]] && {} || {})'.format(
242
- amd64_download_command, arm64_download_command
243
- )
244
- if i < len(binaries) - 1:
245
- binary_cmd += " && "
246
- return binary_cmd
318
+ return True
247
319
 
248
320
  def get_plugin_binaries(self) -> Optional[List[Binary]]:
249
321
  """Get binary definitions from the plugin class."""
@@ -265,10 +337,11 @@ class PluginGenerator:
265
337
  metadata = self.get_plugin_metadata()
266
338
 
267
339
  return PluginManifest(
268
- name_prefix=metadata.get("name_prefix", "unknown"),
269
- version=metadata.get("version", "1.0.0"),
270
- description=metadata.get("description", ""),
271
- author=metadata.get("author", "Unknown"),
340
+ name=str(metadata.get("name_prefix", "unknown")),
341
+ version=str(metadata.get("version", "1.0.0")),
342
+ description=str(metadata.get("description", "")),
343
+ author=str(metadata.get("author", "Unknown")),
344
+ labels=list(metadata.get("labels", [])),
272
345
  parameters=self.get_plugin_parameters(),
273
346
  contexts=self.get_plugin_contexts(),
274
347
  webhooks=self.get_plugin_webhooks(),