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.
- {spaceforge-0.0.5 → spaceforge-0.0.6}/PKG-INFO +2 -1
- spaceforge-0.0.6/plugins/enviroment_manager/plugin.py +248 -0
- spaceforge-0.0.6/plugins/enviroment_manager/plugin.yaml +416 -0
- spaceforge-0.0.6/plugins/enviroment_manager/requirements.txt +1 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/plugins/sops/plugin.yaml +2 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/plugins/wiz/plugin.py +24 -4
- {spaceforge-0.0.5 → spaceforge-0.0.6}/plugins/wiz/plugin.yaml +50 -8
- {spaceforge-0.0.5 → spaceforge-0.0.6}/pyproject.toml +1 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/setup.py +1 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/_version_scm.py +3 -3
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/cls.py +4 -1
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/generator.py +14 -7
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/plugin.py +8 -8
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/schema.json +7 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/templates/ensure_spaceforge_and_run.sh.j2 +3 -1
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_generator.py +2 -2
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_plugin.py +7 -7
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge.egg-info/PKG-INFO +2 -1
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge.egg-info/SOURCES.txt +3 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge.egg-info/requires.txt +1 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/.github/workflows/ci.yml +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/.github/workflows/release.yml +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/.gitignore +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/LICENSE +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/MANIFEST.in +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/README.md +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/go.mod +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/plugins/infracost/plugin.py +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/plugins/infracost/plugin.yaml +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/plugins/sops/plugin.py +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/plugins/sops/requirements.txt +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/setup.cfg +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/README.md +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/__init__.py +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/__main__.py +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/_version.py +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/conftest.py +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/runner.py +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/templates/binary_install.sh.j2 +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_cls.py +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_generator_binaries.py +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_generator_core.py +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_generator_hooks.py +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_generator_parameters.py +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_plugin_file_operations.py +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_plugin_hooks.py +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_plugin_inheritance.py +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_runner.py +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_runner_cli.py +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_runner_core.py +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge/test_runner_execution.py +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge.egg-info/dependency_links.txt +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge.egg-info/entry_points.txt +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge.egg-info/not-zip-safe +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/spaceforge.egg-info/top_level.txt +0 -0
- {spaceforge-0.0.5 → spaceforge-0.0.6}/templates.go +0 -0
- {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.
|
|
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="
|
|
82
|
+
type="PLAN",
|
|
83
83
|
body="""
|
|
84
84
|
package spacelift
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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="
|
|
128
|
+
type="PLAN",
|
|
129
129
|
body="""
|
|
130
130
|
package spacelift
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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:
|
|
313
|
+
type: PLAN
|
|
292
314
|
body: |-
|
|
293
315
|
package spacelift
|
|
294
316
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
|
@@ -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.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 0,
|
|
31
|
+
__version__ = version = '0.0.6'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 0, 6)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
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:
|
|
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,
|
|
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.
|
|
274
|
-
contexts[0].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
|
-
) ->
|
|
287
|
+
) -> bool:
|
|
283
288
|
binaries = self.get_plugin_binaries()
|
|
284
289
|
if binaries is None:
|
|
285
|
-
return
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
52
|
-
if self.
|
|
53
|
-
self.
|
|
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.
|
|
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.
|
|
203
|
+
self.spacelift_domain, # type: ignore[arg-type]
|
|
204
204
|
json.dumps(data).encode("utf-8"),
|
|
205
205
|
headers,
|
|
206
206
|
)
|
|
@@ -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="
|
|
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 == "
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|