spaceforge 0.0.5__tar.gz → 0.0.6__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.
Files changed (57) hide show
  1. {spaceforge-0.0.5 → spaceforge-0.0.6}/PKG-INFO +2 -1
  2. spaceforge-0.0.6/plugins/enviroment_manager/plugin.py +248 -0
  3. spaceforge-0.0.6/plugins/enviroment_manager/plugin.yaml +416 -0
  4. spaceforge-0.0.6/plugins/enviroment_manager/requirements.txt +1 -0
  5. {spaceforge-0.0.5 → spaceforge-0.0.6}/plugins/sops/plugin.yaml +2 -0
  6. {spaceforge-0.0.5 → spaceforge-0.0.6}/plugins/wiz/plugin.py +24 -4
  7. {spaceforge-0.0.5 → spaceforge-0.0.6}/plugins/wiz/plugin.yaml +50 -8
  8. {spaceforge-0.0.5 → spaceforge-0.0.6}/pyproject.toml +1 -0
  9. {spaceforge-0.0.5 → spaceforge-0.0.6}/setup.py +1 -0
  10. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/_version_scm.py +3 -3
  11. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/cls.py +4 -1
  12. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/generator.py +14 -7
  13. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/plugin.py +8 -8
  14. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/schema.json +7 -0
  15. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/templates/ensure_spaceforge_and_run.sh.j2 +3 -1
  16. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_generator.py +2 -2
  17. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_plugin.py +7 -7
  18. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge.egg-info/PKG-INFO +2 -1
  19. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge.egg-info/SOURCES.txt +3 -0
  20. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge.egg-info/requires.txt +1 -0
  21. {spaceforge-0.0.5 → spaceforge-0.0.6}/.github/workflows/ci.yml +0 -0
  22. {spaceforge-0.0.5 → spaceforge-0.0.6}/.github/workflows/release.yml +0 -0
  23. {spaceforge-0.0.5 → spaceforge-0.0.6}/.gitignore +0 -0
  24. {spaceforge-0.0.5 → spaceforge-0.0.6}/LICENSE +0 -0
  25. {spaceforge-0.0.5 → spaceforge-0.0.6}/MANIFEST.in +0 -0
  26. {spaceforge-0.0.5 → spaceforge-0.0.6}/README.md +0 -0
  27. {spaceforge-0.0.5 → spaceforge-0.0.6}/go.mod +0 -0
  28. {spaceforge-0.0.5 → spaceforge-0.0.6}/plugins/infracost/plugin.py +0 -0
  29. {spaceforge-0.0.5 → spaceforge-0.0.6}/plugins/infracost/plugin.yaml +0 -0
  30. {spaceforge-0.0.5 → spaceforge-0.0.6}/plugins/sops/plugin.py +0 -0
  31. {spaceforge-0.0.5 → spaceforge-0.0.6}/plugins/sops/requirements.txt +0 -0
  32. {spaceforge-0.0.5 → spaceforge-0.0.6}/setup.cfg +0 -0
  33. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/README.md +0 -0
  34. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/__init__.py +0 -0
  35. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/__main__.py +0 -0
  36. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/_version.py +0 -0
  37. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/conftest.py +0 -0
  38. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/runner.py +0 -0
  39. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/templates/binary_install.sh.j2 +0 -0
  40. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_cls.py +0 -0
  41. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_generator_binaries.py +0 -0
  42. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_generator_core.py +0 -0
  43. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_generator_hooks.py +0 -0
  44. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_generator_parameters.py +0 -0
  45. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_plugin_file_operations.py +0 -0
  46. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_plugin_hooks.py +0 -0
  47. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_plugin_inheritance.py +0 -0
  48. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_runner.py +0 -0
  49. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_runner_cli.py +0 -0
  50. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_runner_core.py +0 -0
  51. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_runner_execution.py +0 -0
  52. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge.egg-info/dependency_links.txt +0 -0
  53. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge.egg-info/entry_points.txt +0 -0
  54. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge.egg-info/not-zip-safe +0 -0
  55. {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge.egg-info/top_level.txt +0 -0
  56. {spaceforge-0.0.5 → spaceforge-0.0.6}/templates.go +0 -0
  57. {spaceforge-0.0.5 → spaceforge-0.0.6}/test.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spaceforge
3
- Version: 0.0.5
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"
@@ -0,0 +1,248 @@
1
+ from spaceforge import SpaceforgePlugin, MountedFile, Context
2
+
3
+ import yaml
4
+ import os
5
+
6
+ class EnvironmentManagerPlugin(SpaceforgePlugin):
7
+ """
8
+ # Spacelift Environment Variable Manager
9
+ This plugin allows you to manage Spacelift environment variables using a centralized YAML configuration file for multiple stacks.
10
+
11
+ ## Features
12
+ - Centralized management of environment variables across multiple stacks.
13
+ - Supports sensitive variables.
14
+ - Preview of environment variable changes across stacks before applying changes here.
15
+
16
+ **Hint:** Use this in combination with the `sops` plugin to manage secrets in your environment variables.
17
+
18
+ ## Usage
19
+ Add this plugin to an **administrative** stack in your Spacelift account, the stack **must** have the `Spacelift` OpenTofu/Terraform provider configured.
20
+ example:
21
+ ```hcl
22
+ terraform {
23
+ required_providers {
24
+ spacelift = {
25
+ source = "spacelift-io/spacelift"
26
+ version = "~> 1.0"
27
+ }
28
+ }
29
+ }
30
+
31
+ provider "spacelift" {}
32
+ ```
33
+
34
+ 1. **YAML Configuration**: Environment variables are defined in `vars.yaml` using the following structure:
35
+ ```yaml
36
+ stack-id:
37
+ - name: VARIABLE_NAME
38
+ value: variable_value
39
+ sensitive: false
40
+ ```
41
+
42
+ 2. **Terraform Processing**: The main Terraform configuration:
43
+ - Reads and parses the YAML file using `yamldecode(file("vars.yaml"))`
44
+ - Flattens the structure into a list of variables with their associated stack IDs
45
+ - Creates `spacelift_environment_variable` resources for each variable
46
+
47
+ 3. **Stack Association**: Variables are automatically associated with their respective stacks based on the stack ids defined in the YAML file.
48
+
49
+ **note:** when using this plugin, if you open a PR to your variables file, the changes of the child stacks will be previewed and linked.
50
+
51
+ ## Example Configuration
52
+
53
+ ### vars.yaml
54
+ ```yaml
55
+ env-var-yaml-1:
56
+ - name: KUBECONFIG
57
+ value: /home/joey/.kube/config
58
+ sensitive: false
59
+
60
+ env-var-yaml-2:
61
+ - name: AWS_PROFILE
62
+ value: test
63
+ sensitive: false
64
+ - name: MY_AWESOME_SECRET
65
+ value: HelloWorld
66
+ sensitive: true
67
+ ```
68
+
69
+ The above configuration will create the following in a plan:
70
+
71
+ ```ansi
72
+ # spacelift_environment_variable.this["env-var-yaml-1_KUBECONFIG"] will be created
73
+ + resource "spacelift_environment_variable" "this" {
74
+ + checksum = (known after apply)
75
+ + id = (known after apply)
76
+ + name = "KUBECONFIG"
77
+ + stack_id = "env-var-yaml-1"
78
+ + value = (sensitive value)
79
+ + write_only = false
80
+ }
81
+
82
+ # spacelift_environment_variable.this["env-var-yaml-2_AWS_PROFILE"] will be created
83
+ + resource "spacelift_environment_variable" "this" {
84
+ + checksum = (known after apply)
85
+ + id = (known after apply)
86
+ + name = "AWS_PROFILE"
87
+ + stack_id = "env-var-yaml-2"
88
+ + value = (sensitive value)
89
+ + write_only = false
90
+ }
91
+
92
+ # spacelift_environment_variable.this["env-var-yaml-2_MY_AWESOME_SECRET"] will be created
93
+ + resource "spacelift_environment_variable" "this" {
94
+ + checksum = (known after apply)
95
+ + id = (known after apply)
96
+ + name = "MY_AWESOME_SECRET"
97
+ + stack_id = "env-var-yaml-2"
98
+ + value = (sensitive value)
99
+ + write_only = true
100
+ }
101
+ ```
102
+ """
103
+
104
+ # Plugin metadata
105
+ __plugin_name__ = "Environment Manager"
106
+ __labels__ = ["management", "infrastructure"]
107
+ __version__ = "1.0.0"
108
+ __author__ = "Spacelift Team"
109
+
110
+ __contexts__ = [
111
+ Context(
112
+ name_prefix="Environment Manager",
113
+ description="Environment Manager Plugin",
114
+ hooks = {
115
+ "before_init": [
116
+ "mv /mnt/workspace/__environment_manager.tf /mnt/workspace/source/$TF_VAR_spacelift_project_root/__environment_manager.tf",
117
+ ]
118
+ },
119
+ mounted_files=[
120
+ MountedFile(
121
+ path="__environment_manager.tf",
122
+ content="""
123
+ locals {
124
+ __env_vars = {
125
+ for obj in flatten([
126
+ for stack_id, values in yamldecode(file("${path.module}/vars.yaml")) : [
127
+ for v in values : {
128
+ stack_id = stack_id
129
+ name = v.name
130
+ value = v.value
131
+ write_only = v.write_only
132
+ }
133
+ ]
134
+ ]) : "${obj.stack_id}_${obj.name}" => obj
135
+ }
136
+ }
137
+
138
+ resource "spacelift_environment_variable" "__this" {
139
+ for_each = local.__env_vars
140
+
141
+ stack_id = each.value.stack_id
142
+ name = each.value.name
143
+ value = each.value.value
144
+ write_only = each.value.write_only
145
+ }
146
+ """
147
+ )
148
+ ]
149
+ )
150
+ ]
151
+
152
+ def load_yaml_file(self, file_path):
153
+ """Load YAML file and return parsed content"""
154
+ try:
155
+ with open(file_path, 'r') as file:
156
+ return yaml.safe_load(file)
157
+ except FileNotFoundError:
158
+ self.logger.error(f"Error: File {file_path} not found")
159
+ exit(1)
160
+ except yaml.YAMLError as e:
161
+ self.logger.error(f"Error parsing YAML: {e}")
162
+ exit(1)
163
+
164
+ def convert_to_runtime_config(self, yaml_data):
165
+ """Convert YAML data to Spacelift runtime config format"""
166
+ runtime_config = {}
167
+
168
+ for stack_id, env_vars in yaml_data.items():
169
+ if not isinstance(env_vars, list):
170
+ self.logger.error(f"Error: Environment variables for stack '{stack_id}' must be a list")
171
+ exit(1)
172
+
173
+ runtime_config[stack_id] = {
174
+ "environment": {}
175
+ }
176
+
177
+ for var in env_vars:
178
+ runtime_config[stack_id]["environment"][var["name"]] = var["value"]
179
+
180
+ return runtime_config
181
+
182
+ def trigger_stack_previews(self, runtime_config: dict):
183
+ """Trigger stack previews using Spacelift API"""
184
+
185
+ markdown = []
186
+
187
+ for stack_id, env in runtime_config.items():
188
+
189
+ # Get the current tracked sha from the stack
190
+ query = "{ stack(id: \"" + stack_id + "\") { trackedCommit { hash } } }"
191
+ response = self.query_api(query)
192
+ if "errors" in response:
193
+ self.logger.error("Error fetching stack tracked commit:", response["errors"])
194
+ continue
195
+
196
+ # Ensure we have a tracked commit
197
+ try:
198
+ tracked_commit = response["data"]["stack"]["trackedCommit"]["hash"]
199
+ except TypeError:
200
+ tracked_commit = None
201
+ if tracked_commit is None:
202
+ self.logger.error(f"Stack {stack_id} has no tracked commit. Skipping.")
203
+ continue
204
+
205
+ # Trigger the stack preview with the current tracked commit SHA
206
+ query = """
207
+ mutation TriggerStackPreview($stack: ID!, $commitSHA: String!, $runtimeConfig: String!) {
208
+ runTrigger(stack: $stack, commitSha: $commitSHA, runType: PROPOSED, runtimeConfig: { yaml: $runtimeConfig }) {
209
+ id
210
+ }
211
+ }
212
+ """
213
+
214
+ variables = {
215
+ "stack": stack_id,
216
+ "commitSHA": tracked_commit,
217
+ "runtimeConfig": yaml.dump(env)
218
+ }
219
+
220
+ response = self.query_api(query, variables)
221
+ if "errors" in response:
222
+ self.logger.error(f"Error triggering stack preview for {stack_id}:", response["errors"])
223
+ else:
224
+ url = f"https://{self.spacelift_domain}/stack/{stack_id}/run/{response['data']['runTrigger']['id']}"
225
+ markdown.append(f"- Triggered [stack preview]({url}) for {stack_id} with commit {tracked_commit}.")
226
+
227
+ if len(markdown) > 0:
228
+ mdown = "# Stack Previews Triggered\n\n" + "\n".join(markdown)
229
+ success = self.send_markdown(mdown)
230
+ if not success:
231
+ self.logger.error("Failed to send markdown message with stack previews.")
232
+ self.logger.info(mdown)
233
+
234
+
235
+ def before_init(self):
236
+ # ensure we are in a proposed run
237
+ if os.getenv("TF_VAR_spacelift_run_type") != "PROPOSED":
238
+ # This script should only be run in a proposed run context.
239
+ return
240
+
241
+ # Load YAML data
242
+ yaml_data = self.load_yaml_file("vars.yaml")
243
+
244
+ # Convert to runtime config
245
+ runtime_config = self.convert_to_runtime_config(yaml_data)
246
+
247
+ # Trigger stack previews
248
+ self.trigger_stack_previews(runtime_config)
@@ -0,0 +1,416 @@
1
+ name: Environment Manager
2
+ version: 1.0.0
3
+ description: |-
4
+ # Spacelift Environment Variable Manager
5
+ This plugin allows you to manage Spacelift environment variables using a centralized YAML configuration file for multiple stacks.
6
+
7
+ ## Features
8
+ - Centralized management of environment variables across multiple stacks.
9
+ - Supports sensitive variables.
10
+ - Preview of environment variable changes across stacks before applying changes here.
11
+
12
+ **Hint:** Use this in combination with the `sops` plugin to manage secrets in your environment variables.
13
+
14
+ ## Usage
15
+ Add this plugin to an **administrative** stack in your Spacelift account, the stack **must** have the `Spacelift` OpenTofu/Terraform provider configured.
16
+ example:
17
+ ```hcl
18
+ terraform {
19
+ required_providers {
20
+ spacelift = {
21
+ source = "spacelift-io/spacelift"
22
+ version = "~> 1.0"
23
+ }
24
+ }
25
+ }
26
+
27
+ provider "spacelift" {}
28
+ ```
29
+
30
+ 1. **YAML Configuration**: Environment variables are defined in `vars.yaml` using the following structure:
31
+ ```yaml
32
+ stack-id:
33
+ - name: VARIABLE_NAME
34
+ value: variable_value
35
+ sensitive: false
36
+ ```
37
+
38
+ 2. **Terraform Processing**: The main Terraform configuration:
39
+ - Reads and parses the YAML file using `yamldecode(file("vars.yaml"))`
40
+ - Flattens the structure into a list of variables with their associated stack IDs
41
+ - Creates `spacelift_environment_variable` resources for each variable
42
+
43
+ 3. **Stack Association**: Variables are automatically associated with their respective stacks based on the stack ids defined in the YAML file.
44
+
45
+ **note:** when using this plugin, if you open a PR to your variables file, the changes of the child stacks will be previewed and linked.
46
+
47
+ ## Example Configuration
48
+
49
+ ### vars.yaml
50
+ ```yaml
51
+ env-var-yaml-1:
52
+ - name: KUBECONFIG
53
+ value: /home/joey/.kube/config
54
+ sensitive: false
55
+
56
+ env-var-yaml-2:
57
+ - name: AWS_PROFILE
58
+ value: test
59
+ sensitive: false
60
+ - name: MY_AWESOME_SECRET
61
+ value: HelloWorld
62
+ sensitive: true
63
+ ```
64
+
65
+ The above configuration will create the following in a plan:
66
+
67
+ ```ansi
68
+ # spacelift_environment_variable.this["env-var-yaml-1_KUBECONFIG"] will be created
69
+ + resource "spacelift_environment_variable" "this" {
70
+ + checksum = (known after apply)
71
+ + id = (known after apply)
72
+ + name = "KUBECONFIG"
73
+ + stack_id = "env-var-yaml-1"
74
+ + value = (sensitive value)
75
+ + write_only = false
76
+ }
77
+
78
+ # spacelift_environment_variable.this["env-var-yaml-2_AWS_PROFILE"] will be created
79
+ + resource "spacelift_environment_variable" "this" {
80
+ + checksum = (known after apply)
81
+ + id = (known after apply)
82
+ + name = "AWS_PROFILE"
83
+ + stack_id = "env-var-yaml-2"
84
+ + value = (sensitive value)
85
+ + write_only = false
86
+ }
87
+
88
+ # spacelift_environment_variable.this["env-var-yaml-2_MY_AWESOME_SECRET"] will be created
89
+ + resource "spacelift_environment_variable" "this" {
90
+ + checksum = (known after apply)
91
+ + id = (known after apply)
92
+ + name = "MY_AWESOME_SECRET"
93
+ + stack_id = "env-var-yaml-2"
94
+ + value = (sensitive value)
95
+ + write_only = true
96
+ }
97
+ ```
98
+ author: Spacelift Team
99
+ labels:
100
+ - management
101
+ - infrastructure
102
+ contexts:
103
+ - name_prefix: Environment Manager
104
+ description: Environment Manager Plugin
105
+ env: []
106
+ mounted_files:
107
+ - path: __environment_manager.tf
108
+ content: |-
109
+ locals {
110
+ __env_vars = {
111
+ for obj in flatten([
112
+ for stack_id, values in yamldecode(file("${path.module}/vars.yaml")) : [
113
+ for v in values : {
114
+ stack_id = stack_id
115
+ name = v.name
116
+ value = v.value
117
+ write_only = v.write_only
118
+ }
119
+ ]
120
+ ]) : "${obj.stack_id}_${obj.name}" => obj
121
+ }
122
+ }
123
+
124
+ resource "spacelift_environment_variable" "__this" {
125
+ for_each = local.__env_vars
126
+
127
+ stack_id = each.value.stack_id
128
+ name = each.value.name
129
+ value = each.value.value
130
+ write_only = each.value.write_only
131
+ }
132
+ sensitive: false
133
+ - path: /mnt/workspace/plugins/environment manager/requirements.txt
134
+ content: pyyaml==6.0.2
135
+ sensitive: false
136
+ - path: /mnt/workspace/plugins/environment manager/plugin.py
137
+ content: |-
138
+ from spaceforge import SpaceforgePlugin, MountedFile, Context
139
+
140
+ import yaml
141
+ import os
142
+
143
+ class EnvironmentManagerPlugin(SpaceforgePlugin):
144
+ """
145
+ # Spacelift Environment Variable Manager
146
+ This plugin allows you to manage Spacelift environment variables using a centralized YAML configuration file for multiple stacks.
147
+
148
+ ## Features
149
+ - Centralized management of environment variables across multiple stacks.
150
+ - Supports sensitive variables.
151
+ - Preview of environment variable changes across stacks before applying changes here.
152
+
153
+ **Hint:** Use this in combination with the `sops` plugin to manage secrets in your environment variables.
154
+
155
+ ## Usage
156
+ Add this plugin to an **administrative** stack in your Spacelift account, the stack **must** have the `Spacelift` OpenTofu/Terraform provider configured.
157
+ example:
158
+ ```hcl
159
+ terraform {
160
+ required_providers {
161
+ spacelift = {
162
+ source = "spacelift-io/spacelift"
163
+ version = "~> 1.0"
164
+ }
165
+ }
166
+ }
167
+
168
+ provider "spacelift" {}
169
+ ```
170
+
171
+ 1. **YAML Configuration**: Environment variables are defined in `vars.yaml` using the following structure:
172
+ ```yaml
173
+ stack-id:
174
+ - name: VARIABLE_NAME
175
+ value: variable_value
176
+ sensitive: false
177
+ ```
178
+
179
+ 2. **Terraform Processing**: The main Terraform configuration:
180
+ - Reads and parses the YAML file using `yamldecode(file("vars.yaml"))`
181
+ - Flattens the structure into a list of variables with their associated stack IDs
182
+ - Creates `spacelift_environment_variable` resources for each variable
183
+
184
+ 3. **Stack Association**: Variables are automatically associated with their respective stacks based on the stack ids defined in the YAML file.
185
+
186
+ **note:** when using this plugin, if you open a PR to your variables file, the changes of the child stacks will be previewed and linked.
187
+
188
+ ## Example Configuration
189
+
190
+ ### vars.yaml
191
+ ```yaml
192
+ env-var-yaml-1:
193
+ - name: KUBECONFIG
194
+ value: /home/joey/.kube/config
195
+ sensitive: false
196
+
197
+ env-var-yaml-2:
198
+ - name: AWS_PROFILE
199
+ value: test
200
+ sensitive: false
201
+ - name: MY_AWESOME_SECRET
202
+ value: HelloWorld
203
+ sensitive: true
204
+ ```
205
+
206
+ The above configuration will create the following in a plan:
207
+
208
+ ```ansi
209
+ # spacelift_environment_variable.this["env-var-yaml-1_KUBECONFIG"] will be created
210
+ + resource "spacelift_environment_variable" "this" {
211
+ + checksum = (known after apply)
212
+ + id = (known after apply)
213
+ + name = "KUBECONFIG"
214
+ + stack_id = "env-var-yaml-1"
215
+ + value = (sensitive value)
216
+ + write_only = false
217
+ }
218
+
219
+ # spacelift_environment_variable.this["env-var-yaml-2_AWS_PROFILE"] will be created
220
+ + resource "spacelift_environment_variable" "this" {
221
+ + checksum = (known after apply)
222
+ + id = (known after apply)
223
+ + name = "AWS_PROFILE"
224
+ + stack_id = "env-var-yaml-2"
225
+ + value = (sensitive value)
226
+ + write_only = false
227
+ }
228
+
229
+ # spacelift_environment_variable.this["env-var-yaml-2_MY_AWESOME_SECRET"] will be created
230
+ + resource "spacelift_environment_variable" "this" {
231
+ + checksum = (known after apply)
232
+ + id = (known after apply)
233
+ + name = "MY_AWESOME_SECRET"
234
+ + stack_id = "env-var-yaml-2"
235
+ + value = (sensitive value)
236
+ + write_only = true
237
+ }
238
+ ```
239
+ """
240
+
241
+ # Plugin metadata
242
+ __plugin_name__ = "Environment Manager"
243
+ __labels__ = ["management", "infrastructure"]
244
+ __version__ = "1.0.0"
245
+ __author__ = "Spacelift Team"
246
+
247
+ __contexts__ = [
248
+ Context(
249
+ name_prefix="Environment Manager",
250
+ description="Environment Manager Plugin",
251
+ hooks = {
252
+ "before_init": [
253
+ "mv /mnt/workspace/__environment_manager.tf /mnt/workspace/source/$TF_VAR_spacelift_project_root/__environment_manager.tf",
254
+ ]
255
+ },
256
+ mounted_files=[
257
+ MountedFile(
258
+ path="__environment_manager.tf",
259
+ content="""
260
+ locals {
261
+ __env_vars = {
262
+ for obj in flatten([
263
+ for stack_id, values in yamldecode(file("${path.module}/vars.yaml")) : [
264
+ for v in values : {
265
+ stack_id = stack_id
266
+ name = v.name
267
+ value = v.value
268
+ write_only = v.write_only
269
+ }
270
+ ]
271
+ ]) : "${obj.stack_id}_${obj.name}" => obj
272
+ }
273
+ }
274
+
275
+ resource "spacelift_environment_variable" "__this" {
276
+ for_each = local.__env_vars
277
+
278
+ stack_id = each.value.stack_id
279
+ name = each.value.name
280
+ value = each.value.value
281
+ write_only = each.value.write_only
282
+ }
283
+ """
284
+ )
285
+ ]
286
+ )
287
+ ]
288
+
289
+ def load_yaml_file(self, file_path):
290
+ """Load YAML file and return parsed content"""
291
+ try:
292
+ with open(file_path, 'r') as file:
293
+ return yaml.safe_load(file)
294
+ except FileNotFoundError:
295
+ self.logger.error(f"Error: File {file_path} not found")
296
+ exit(1)
297
+ except yaml.YAMLError as e:
298
+ self.logger.error(f"Error parsing YAML: {e}")
299
+ exit(1)
300
+
301
+ def convert_to_runtime_config(self, yaml_data):
302
+ """Convert YAML data to Spacelift runtime config format"""
303
+ runtime_config = {}
304
+
305
+ for stack_id, env_vars in yaml_data.items():
306
+ if not isinstance(env_vars, list):
307
+ self.logger.error(f"Error: Environment variables for stack '{stack_id}' must be a list")
308
+ exit(1)
309
+
310
+ runtime_config[stack_id] = {
311
+ "environment": {}
312
+ }
313
+
314
+ for var in env_vars:
315
+ runtime_config[stack_id]["environment"][var["name"]] = var["value"]
316
+
317
+ return runtime_config
318
+
319
+ def trigger_stack_previews(self, runtime_config: dict):
320
+ """Trigger stack previews using Spacelift API"""
321
+
322
+ markdown = []
323
+
324
+ for stack_id, env in runtime_config.items():
325
+
326
+ # Get the current tracked sha from the stack
327
+ query = "{ stack(id: \"" + stack_id + "\") { trackedCommit { hash } } }"
328
+ response = self.query_api(query)
329
+ if "errors" in response:
330
+ self.logger.error("Error fetching stack tracked commit:", response["errors"])
331
+ continue
332
+
333
+ # Ensure we have a tracked commit
334
+ try:
335
+ tracked_commit = response["data"]["stack"]["trackedCommit"]["hash"]
336
+ except TypeError:
337
+ tracked_commit = None
338
+ if tracked_commit is None:
339
+ self.logger.error(f"Stack {stack_id} has no tracked commit. Skipping.")
340
+ continue
341
+
342
+ # Trigger the stack preview with the current tracked commit SHA
343
+ query = """
344
+ mutation TriggerStackPreview($stack: ID!, $commitSHA: String!, $runtimeConfig: String!) {
345
+ runTrigger(stack: $stack, commitSha: $commitSHA, runType: PROPOSED, runtimeConfig: { yaml: $runtimeConfig }) {
346
+ id
347
+ }
348
+ }
349
+ """
350
+
351
+ variables = {
352
+ "stack": stack_id,
353
+ "commitSHA": tracked_commit,
354
+ "runtimeConfig": yaml.dump(env)
355
+ }
356
+
357
+ response = self.query_api(query, variables)
358
+ if "errors" in response:
359
+ self.logger.error(f"Error triggering stack preview for {stack_id}:", response["errors"])
360
+ else:
361
+ url = f"https://{self.spacelift_domain}/stack/{stack_id}/run/{response['data']['runTrigger']['id']}"
362
+ markdown.append(f"- Triggered [stack preview]({url}) for {stack_id} with commit {tracked_commit}.")
363
+
364
+ if len(markdown) > 0:
365
+ mdown = "# Stack Previews Triggered\n\n" + "\n".join(markdown)
366
+ success = self.send_markdown(mdown)
367
+ if not success:
368
+ self.logger.error("Failed to send markdown message with stack previews.")
369
+ self.logger.info(mdown)
370
+
371
+
372
+ def before_init(self):
373
+ # ensure we are in a proposed run
374
+ if os.getenv("TF_VAR_spacelift_run_type") != "PROPOSED":
375
+ # This script should only be run in a proposed run context.
376
+ return
377
+
378
+ # Load YAML data
379
+ yaml_data = self.load_yaml_file("vars.yaml")
380
+
381
+ # Convert to runtime config
382
+ runtime_config = self.convert_to_runtime_config(yaml_data)
383
+
384
+ # Trigger stack previews
385
+ self.trigger_stack_previews(runtime_config)
386
+ sensitive: false
387
+ - path: /mnt/workspace/plugins/environment manager/before_init.sh
388
+ content: |-
389
+ #!/bin/sh
390
+
391
+ set -e
392
+
393
+ cd /mnt/workspace/plugins/environment manager
394
+
395
+ if [ ! -d "./venv" ]; then
396
+ python -m venv ./venv
397
+ fi
398
+ . venv/bin/activate
399
+
400
+ if ! command -v spaceforge; then
401
+ pip install spaceforge
402
+ fi
403
+
404
+ if [ -f requirements.txt ] && [ ! -f .spaceforge_installed_requirements ]; then
405
+ pip install -r requirements.txt
406
+ touch .spaceforge_installed_requirements
407
+ fi
408
+
409
+ cd /mnt/workspace/source/$TF_VAR_spacelift_project_root
410
+ spaceforge runner --plugin-file /mnt/workspace/plugins/environment manager/plugin.py before_init
411
+ sensitive: false
412
+ hooks:
413
+ before_init:
414
+ - mv /mnt/workspace/__environment_manager.tf /mnt/workspace/source/$TF_VAR_spacelift_project_root/__environment_manager.tf
415
+ - mkdir -p /mnt/workspace/plugins/environment manager
416
+ - chmod +x /mnt/workspace/plugins/environment manager/before_init.sh && /mnt/workspace/plugins/environment manager/before_init.sh
@@ -0,0 +1 @@
1
+ pyyaml==6.0.2
@@ -179,6 +179,8 @@ contexts:
179
179
  touch .spaceforge_installed_requirements
180
180
  fi
181
181
 
182
+ export PATH="/mnt/workspace/plugins/plugin_binaries:$PATH"
183
+
182
184
  cd /mnt/workspace/source/$TF_VAR_spacelift_project_root
183
185
  spaceforge runner --plugin-file /mnt/workspace/plugins/sops/plugin.py before_init
184
186
  sensitive: false
@@ -79,13 +79,33 @@ Samples of these policies are included with the plugin.
79
79
  __policies__ = [
80
80
  Policy(
81
81
  name_prefix="wiz_policy",
82
- type="notification",
82
+ type="PLAN",
83
83
  body="""
84
84
  package spacelift
85
85
 
86
- webhook[{"endpoint_id": "wiz-alert-endpoint"}] {
87
- input.run_updated.run.type == "TRACKED"
88
- input.run_updated.run.marked_unsafe == true
86
+ max_critical_vulnerabilities := 0
87
+ max_high_vulnerabilities := 0
88
+ max_medium_vulnerabilities := 3
89
+ max_low_vulnerabilities := 10
90
+
91
+ deny[sprintf("Too many critical vulnerabilities (%d)", [num])] {
92
+ num := input.third_party_metadata.custom.wiz.result.scanStatistics.criticalMatches
93
+ num > max_critical_vulnerabilities
94
+ }
95
+
96
+ deny[sprintf("Too many high vulnerabilities (%d)", [num])] {
97
+ num := input.third_party_metadata.custom.wiz.result.scanStatistics.highMatches
98
+ num > max_high_vulnerabilities
99
+ }
100
+
101
+ deny[sprintf("Too many medium vulnerabilities (%d)", [num])] {
102
+ num := input.third_party_metadata.custom.wiz.result.scanStatistics.mediumMatches
103
+ num > max_medium_vulnerabilities
104
+ }
105
+
106
+ deny[sprintf("Too many low vulnerabilities (%d)", [num])] {
107
+ num := input.third_party_metadata.custom.wiz.result.scanStatistics.lowMatches
108
+ num > max_low_vulnerabilities
89
109
  }
90
110
  """,
91
111
  labels=[
@@ -125,13 +125,33 @@ contexts:
125
125
  __policies__ = [
126
126
  Policy(
127
127
  name_prefix="wiz_policy",
128
- type="notification",
128
+ type="PLAN",
129
129
  body="""
130
130
  package spacelift
131
131
 
132
- webhook[{"endpoint_id": "wiz-alert-endpoint"}] {
133
- input.run_updated.run.type == "TRACKED"
134
- input.run_updated.run.marked_unsafe == true
132
+ max_critical_vulnerabilities := 0
133
+ max_high_vulnerabilities := 0
134
+ max_medium_vulnerabilities := 3
135
+ max_low_vulnerabilities := 10
136
+
137
+ deny[sprintf("Too many critical vulnerabilities (%d)", [num])] {
138
+ num := input.third_party_metadata.custom.wiz.result.scanStatistics.criticalMatches
139
+ num > max_critical_vulnerabilities
140
+ }
141
+
142
+ deny[sprintf("Too many high vulnerabilities (%d)", [num])] {
143
+ num := input.third_party_metadata.custom.wiz.result.scanStatistics.highMatches
144
+ num > max_high_vulnerabilities
145
+ }
146
+
147
+ deny[sprintf("Too many medium vulnerabilities (%d)", [num])] {
148
+ num := input.third_party_metadata.custom.wiz.result.scanStatistics.mediumMatches
149
+ num > max_medium_vulnerabilities
150
+ }
151
+
152
+ deny[sprintf("Too many low vulnerabilities (%d)", [num])] {
153
+ num := input.third_party_metadata.custom.wiz.result.scanStatistics.lowMatches
154
+ num > max_low_vulnerabilities
135
155
  }
136
156
  """,
137
157
  labels=[
@@ -277,6 +297,8 @@ contexts:
277
297
  touch .spaceforge_installed_requirements
278
298
  fi
279
299
 
300
+ export PATH="/mnt/workspace/plugins/plugin_binaries:$PATH"
301
+
280
302
  cd /mnt/workspace/source/$TF_VAR_spacelift_project_root
281
303
  spaceforge runner --plugin-file /mnt/workspace/plugins/wiz/plugin.py before_plan
282
304
  sensitive: false
@@ -288,13 +310,33 @@ contexts:
288
310
  - chmod +x /mnt/workspace/plugins/wiz/before_plan.sh && /mnt/workspace/plugins/wiz/before_plan.sh
289
311
  policies:
290
312
  - name_prefix: wiz_policy
291
- type: notification
313
+ type: PLAN
292
314
  body: |-
293
315
  package spacelift
294
316
 
295
- webhook[{"endpoint_id": "wiz-alert-endpoint"}] {
296
- input.run_updated.run.type == "TRACKED"
297
- input.run_updated.run.marked_unsafe == true
317
+ max_critical_vulnerabilities := 0
318
+ max_high_vulnerabilities := 0
319
+ max_medium_vulnerabilities := 3
320
+ max_low_vulnerabilities := 10
321
+
322
+ deny[sprintf("Too many critical vulnerabilities (%d)", [num])] {
323
+ num := input.third_party_metadata.custom.wiz.result.scanStatistics.criticalMatches
324
+ num > max_critical_vulnerabilities
325
+ }
326
+
327
+ deny[sprintf("Too many high vulnerabilities (%d)", [num])] {
328
+ num := input.third_party_metadata.custom.wiz.result.scanStatistics.highMatches
329
+ num > max_high_vulnerabilities
330
+ }
331
+
332
+ deny[sprintf("Too many medium vulnerabilities (%d)", [num])] {
333
+ num := input.third_party_metadata.custom.wiz.result.scanStatistics.mediumMatches
334
+ num > max_medium_vulnerabilities
335
+ }
336
+
337
+ deny[sprintf("Too many low vulnerabilities (%d)", [num])] {
338
+ num := input.third_party_metadata.custom.wiz.result.scanStatistics.lowMatches
339
+ num > max_low_vulnerabilities
298
340
  }
299
341
  labels:
300
342
  - wiz-plugin
@@ -33,6 +33,7 @@ dependencies = [
33
33
  "click>=8.0.0",
34
34
  "pydantic>=2.11.7",
35
35
  "Jinja2>=3.1.0",
36
+ "mergedeep>=1.3.4",
36
37
  ]
37
38
 
38
39
  [project.optional-dependencies]
@@ -44,6 +44,7 @@ setup(
44
44
  "click>=8.0.0",
45
45
  "pydantic>=2.11.7",
46
46
  "Jinja2>=3.1.0",
47
+ "mergedeep>=1.3.4",
47
48
  ],
48
49
  extras_require={
49
50
  "dev": [
@@ -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.5'
32
- __version_tuple__ = version_tuple = (0, 0, 5)
31
+ __version__ = version = '0.0.6'
32
+ __version_tuple__ = version_tuple = (0, 0, 6)
33
33
 
34
- __commit_id__ = commit_id = 'gd83c2c6ce'
34
+ __commit_id__ = commit_id = 'gf4078fecf'
@@ -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
 
@@ -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)
@@ -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
  )
@@ -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:
@@ -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.5
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"
@@ -9,6 +9,9 @@ templates.go
9
9
  test.sh
10
10
  .github/workflows/ci.yml
11
11
  .github/workflows/release.yml
12
+ plugins/enviroment_manager/plugin.py
13
+ plugins/enviroment_manager/plugin.yaml
14
+ plugins/enviroment_manager/requirements.txt
12
15
  plugins/infracost/plugin.py
13
16
  plugins/infracost/plugin.yaml
14
17
  plugins/sops/plugin.py
@@ -2,6 +2,7 @@ PyYAML>=6.0
2
2
  click>=8.0.0
3
3
  pydantic>=2.11.7
4
4
  Jinja2>=3.1.0
5
+ mergedeep>=1.3.4
5
6
 
6
7
  [dev]
7
8
  pytest>=6.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes