cloudfall 0.2.0__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.
- cloudfall/__init__.py +19 -0
- cloudfall/__main__.py +5 -0
- cloudfall/_bundled/schemas/v1/alert-rule.schema.json +63 -0
- cloudfall/_bundled/schemas/v1/application.schema.json +60 -0
- cloudfall/_bundled/schemas/v1/artifact.schema.json +71 -0
- cloudfall/_bundled/schemas/v1/backup-receipt.schema.json +58 -0
- cloudfall/_bundled/schemas/v1/common.schema.json +115 -0
- cloudfall/_bundled/schemas/v1/component.schema.json +234 -0
- cloudfall/_bundled/schemas/v1/deployment-receipt.schema.json +53 -0
- cloudfall/_bundled/schemas/v1/domain.schema.json +208 -0
- cloudfall/_bundled/schemas/v1/environment-receipt.schema.json +55 -0
- cloudfall/_bundled/schemas/v1/logging-stack.schema.json +473 -0
- cloudfall/_bundled/schemas/v1/observed-domain.schema.json +158 -0
- cloudfall/_bundled/schemas/v1/observed-server.schema.json +627 -0
- cloudfall/_bundled/schemas/v1/operator-policy.schema.json +97 -0
- cloudfall/_bundled/schemas/v1/operator-proposal.schema.json +253 -0
- cloudfall/_bundled/schemas/v1/release-receipt.schema.json +66 -0
- cloudfall/_bundled/schemas/v1/server-type.schema.json +289 -0
- cloudfall/_bundled/schemas/v1/server.schema.json +146 -0
- cloudfall/_bundled/schemas/v1/service.schema.json +222 -0
- cloudfall/_bundled/schemas/v1/ssh-public-key.schema.json +55 -0
- cloudfall/agent_tools.py +868 -0
- cloudfall/arguments.py +92 -0
- cloudfall/audit.py +905 -0
- cloudfall/authoring.py +341 -0
- cloudfall/cli.py +1748 -0
- cloudfall/commands.py +257 -0
- cloudfall/cutover.py +370 -0
- cloudfall/dashboard.py +407 -0
- cloudfall/dashboard_server.py +201 -0
- cloudfall/domain.py +1157 -0
- cloudfall/importer.py +906 -0
- cloudfall/inventory.py +2116 -0
- cloudfall/lifecycle.py +874 -0
- cloudfall/mcp_server.py +704 -0
- cloudfall/migrate.py +730 -0
- cloudfall/observation.py +143 -0
- cloudfall/operations.py +1228 -0
- cloudfall/operator.py +1099 -0
- cloudfall/project.py +841 -0
- cloudfall/py.typed +0 -0
- cloudfall/render_api.py +338 -0
- cloudfall/resources.py +67 -0
- cloudfall/secrets.py +262 -0
- cloudfall/service_evidence.py +659 -0
- cloudfall/validation.py +1340 -0
- cloudfall-0.2.0.dist-info/METADATA +408 -0
- cloudfall-0.2.0.dist-info/RECORD +165 -0
- cloudfall-0.2.0.dist-info/WHEEL +4 -0
- cloudfall-0.2.0.dist-info/entry_points.txt +4 -0
- cloudfall-0.2.0.dist-info/licenses/LICENSE +661 -0
- cloudfall_engine/__init__.py +5 -0
- cloudfall_engine/__main__.py +5 -0
- cloudfall_engine/_bundled/ansible/ansible.cfg +7 -0
- cloudfall_engine/_bundled/ansible/playbooks/backup.yml +86 -0
- cloudfall_engine/_bundled/ansible/playbooks/baseline.yml +35 -0
- cloudfall_engine/_bundled/ansible/playbooks/bootstrap.yml +24 -0
- cloudfall_engine/_bundled/ansible/playbooks/data.yml +28 -0
- cloudfall_engine/_bundled/ansible/playbooks/deploy.yml +35 -0
- cloudfall_engine/_bundled/ansible/playbooks/domains.yml +21 -0
- cloudfall_engine/_bundled/ansible/playbooks/health.yml +31 -0
- cloudfall_engine/_bundled/ansible/playbooks/inspect.yml +15 -0
- cloudfall_engine/_bundled/ansible/playbooks/logging.yml +35 -0
- cloudfall_engine/_bundled/ansible/playbooks/restart.yml +31 -0
- cloudfall_engine/_bundled/ansible/playbooks/rollback.yml +33 -0
- cloudfall_engine/_bundled/ansible/playbooks/services.yml +37 -0
- cloudfall_engine/_bundled/ansible/playbooks/time.yml +9 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_access/defaults/main.yml +2 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_access/handlers/main.yml +5 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_access/meta/argument_specs.yml +34 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_access/meta/main.yml +13 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_access/tasks/main.yml +59 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_bootstrap/defaults/main.yml +11 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_bootstrap/meta/argument_specs.yml +52 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_bootstrap/meta/main.yml +13 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_bootstrap/tasks/main.yml +99 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_data_migration/defaults/main.yml +15 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_data_migration/meta/argument_specs.yml +36 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_data_migration/meta/main.yml +13 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_data_migration/tasks/main.yml +201 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/defaults/main.yml +10 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/meta/argument_specs.yml +125 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/meta/main.yml +13 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/tasks/health.yml +3 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/tasks/health_gate.yml +37 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/tasks/main.yml +285 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/tasks/restart.yml +17 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/tasks/rollback.yml +52 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/templates/component.service.j2 +24 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_firewall/defaults/main.yml +2 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_firewall/handlers/main.yml +5 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_firewall/meta/argument_specs.yml +33 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_firewall/meta/main.yml +13 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_firewall/tasks/main.yml +51 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_firewall/templates/nftables.conf.j2 +31 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_inspect/defaults/main.yml +6 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_inspect/meta/argument_specs.yml +24 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_inspect/meta/main.yml +13 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_inspect/tasks/main.yml +643 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/defaults/main.yml +6 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/handlers/main.yml +29 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/meta/argument_specs.yml +227 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/meta/main.yml +11 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/tasks/main.yml +487 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/alertmanager-defaults.j2 +2 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/alertmanager.yaml.j2 +33 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/grafana.ini.j2 +20 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/loki-datasource.yml.j2 +11 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/loki-systemd-override.conf.j2 +3 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/loki.yaml.j2 +49 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/nginx-loki-gateway.conf.j2 +45 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/prometheus-datasource.yml.j2 +11 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/prometheus-defaults.j2 +2 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/prometheus-rules.yaml.j2 +20 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/prometheus.yaml.j2 +24 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_collector/defaults/main.yml +7 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_collector/handlers/main.yml +6 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_collector/meta/argument_specs.yml +122 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_collector/meta/main.yml +11 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_collector/tasks/main.yml +224 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_collector/templates/alloy-systemd-override.conf.j2 +3 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_collector/templates/config.alloy.j2 +127 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_nginx_site/defaults/main.yml +4 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_nginx_site/handlers/main.yml +5 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_nginx_site/meta/argument_specs.yml +69 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_nginx_site/meta/main.yml +13 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_nginx_site/tasks/main.yml +212 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_nginx_site/templates/domain-site.conf.j2 +54 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/defaults/main.yml +6 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/handlers/main.yml +9 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/meta/argument_specs.yml +91 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/meta/main.yml +13 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/tasks/main.yml +295 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/templates/backup.service.j2 +11 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/templates/backup.sh.j2 +17 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/templates/backup.timer.j2 +10 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/templates/cloudfall.conf.j2 +3 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/templates/restore-check.service.j2 +11 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/templates/restore-check.sh.j2 +27 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/templates/restore-check.timer.j2 +10 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/defaults/main.yml +2 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/handlers/main.yml +6 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/meta/argument_specs.yml +68 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/meta/main.yml +13 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/tasks/main.yml +148 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/templates/backup.service.j2 +11 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/templates/backup.sh.j2 +14 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/templates/backup.timer.j2 +10 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/templates/cloudfall.conf.j2 +6 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/templates/restore-check.service.j2 +11 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/templates/restore-check.sh.j2 +15 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/templates/restore-check.timer.j2 +10 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_time/defaults/main.yml +2 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_time/meta/argument_specs.yml +10 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_time/meta/main.yml +13 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_time/tasks/main.yml +119 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_unattended_upgrades/defaults/main.yml +3 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_unattended_upgrades/meta/argument_specs.yml +8 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_unattended_upgrades/meta/main.yml +13 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_unattended_upgrades/tasks/main.yml +37 -0
- cloudfall_engine/ansible_inventory.py +253 -0
- cloudfall_engine/artifact.py +258 -0
- cloudfall_engine/cli.py +273 -0
- cloudfall_engine/playbook.py +175 -0
- cloudfall_engine/py.typed +0 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Render deterministic static Ansible inventory from typed Cloudfall inventory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from cloudfall.inventory import firewall_rules_for_server
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Iterable
|
|
11
|
+
|
|
12
|
+
from cloudfall.domain import ResourceId
|
|
13
|
+
from cloudfall.inventory import (
|
|
14
|
+
AlertRuleInventory,
|
|
15
|
+
ApplicationInventory,
|
|
16
|
+
ComponentInventory,
|
|
17
|
+
LoggingStackInventory,
|
|
18
|
+
PlatformInventory,
|
|
19
|
+
ServerInventory,
|
|
20
|
+
ServerTypeInventory,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def render_ansible_inventory(inventory: PlatformInventory) -> dict[str, object]:
|
|
25
|
+
"""Render static JSON accepted by Ansible's YAML inventory plugin."""
|
|
26
|
+
environment_groups: dict[str, object] = {}
|
|
27
|
+
environments = sorted(
|
|
28
|
+
{server.environment for server in inventory.servers},
|
|
29
|
+
key=lambda environment: environment.value,
|
|
30
|
+
)
|
|
31
|
+
for environment in environments:
|
|
32
|
+
environment_groups[_group_name("environment", environment)] = {
|
|
33
|
+
"hosts": _empty_host_entries(
|
|
34
|
+
server.resource_id.value
|
|
35
|
+
for server in inventory.servers
|
|
36
|
+
if server.environment == environment
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
application_groups: dict[str, object] = {}
|
|
41
|
+
for application in inventory.applications:
|
|
42
|
+
application_components = frozenset(application.component_ids)
|
|
43
|
+
application_hosts = sorted(
|
|
44
|
+
{
|
|
45
|
+
server_id.value
|
|
46
|
+
for component in inventory.components
|
|
47
|
+
if component.resource_id in application_components
|
|
48
|
+
for server_id in component.server_ids
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
application_groups[_group_name("application", application.resource_id)] = {
|
|
52
|
+
"hosts": _empty_host_entries(application_hosts)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
component_groups = {
|
|
56
|
+
_group_name("component", component.resource_id): {
|
|
57
|
+
"hosts": _empty_host_entries(
|
|
58
|
+
server.value for server in component.server_ids
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
for component in inventory.components
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
logging_backend_hosts = _empty_host_entries(
|
|
65
|
+
stack.backend.server_id.value for stack in inventory.logging_stacks
|
|
66
|
+
)
|
|
67
|
+
logging_collector_hosts = _empty_host_entries(
|
|
68
|
+
server_id.value
|
|
69
|
+
for stack in inventory.logging_stacks
|
|
70
|
+
for server_id in stack.collectors.server_ids
|
|
71
|
+
)
|
|
72
|
+
domain_origin_hosts = _empty_host_entries(
|
|
73
|
+
domain.origin.server_id.value for domain in inventory.domains
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
"all": {
|
|
78
|
+
"children": {
|
|
79
|
+
"ungrouped": {"hosts": {}},
|
|
80
|
+
"cloudfall_servers": {"hosts": _host_variables(inventory)},
|
|
81
|
+
"cloudfall_environments": {"children": environment_groups},
|
|
82
|
+
"cloudfall_applications": {"children": application_groups},
|
|
83
|
+
"cloudfall_components": {"children": component_groups},
|
|
84
|
+
"cloudfall_logging_backends": {"hosts": logging_backend_hosts},
|
|
85
|
+
"cloudfall_logging_collectors": {"hosts": logging_collector_hosts},
|
|
86
|
+
"cloudfall_domain_origins": {"hosts": domain_origin_hosts},
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _host_variables(inventory: PlatformInventory) -> dict[str, object]:
|
|
93
|
+
applications_by_id = {
|
|
94
|
+
application.resource_id: application for application in inventory.applications
|
|
95
|
+
}
|
|
96
|
+
server_types_by_id = {
|
|
97
|
+
server_type.resource_id: server_type for server_type in inventory.server_types
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
server.resource_id.value: {
|
|
101
|
+
"ansible_host": server.address.value,
|
|
102
|
+
"ansible_port": server.ssh_port.value,
|
|
103
|
+
"ansible_user": server.ssh_user.value,
|
|
104
|
+
"cloudfall_server_id": server.resource_id.value,
|
|
105
|
+
"cloudfall_hostname": server.hostname.value,
|
|
106
|
+
"cloudfall_environment": server.environment.value,
|
|
107
|
+
"cloudfall_lifecycle": server.lifecycle.value,
|
|
108
|
+
"cloudfall_labels": {
|
|
109
|
+
label.key.value: label.value for label in server.labels
|
|
110
|
+
},
|
|
111
|
+
"cloudfall_ssh_public_keys": [
|
|
112
|
+
key.as_dict()
|
|
113
|
+
for key in inventory.ssh_public_keys
|
|
114
|
+
if key.environment == server.environment and key.is_active
|
|
115
|
+
],
|
|
116
|
+
**_host_infrastructure(server),
|
|
117
|
+
**_host_firewall(server, server_types_by_id[server.server_type_id]),
|
|
118
|
+
**_host_logging(server, inventory.logging_stacks, inventory.alert_rules),
|
|
119
|
+
"cloudfall_server_type": server_types_by_id[
|
|
120
|
+
server.server_type_id
|
|
121
|
+
].as_dict(),
|
|
122
|
+
"cloudfall_applications": [
|
|
123
|
+
_host_application(application)
|
|
124
|
+
for application in inventory.applications
|
|
125
|
+
if _application_is_on_server(application, server, inventory)
|
|
126
|
+
],
|
|
127
|
+
"cloudfall_components": [
|
|
128
|
+
_host_component(component, applications_by_id[component.application_id])
|
|
129
|
+
for component in inventory.components
|
|
130
|
+
if server.resource_id in component.server_ids
|
|
131
|
+
],
|
|
132
|
+
"cloudfall_services": [
|
|
133
|
+
service.as_dict()
|
|
134
|
+
for service in inventory.services
|
|
135
|
+
if service.server_id == server.resource_id
|
|
136
|
+
],
|
|
137
|
+
"cloudfall_domains": [
|
|
138
|
+
domain.as_dict()
|
|
139
|
+
for domain in inventory.domains
|
|
140
|
+
if domain.proxy.server_id == server.resource_id
|
|
141
|
+
],
|
|
142
|
+
"cloudfall_origin_domains": [
|
|
143
|
+
domain.as_dict()
|
|
144
|
+
for domain in inventory.domains
|
|
145
|
+
if domain.origin.server_id == server.resource_id
|
|
146
|
+
],
|
|
147
|
+
}
|
|
148
|
+
for server in inventory.servers
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _host_infrastructure(server: ServerInventory) -> dict[str, object]:
|
|
153
|
+
variables: dict[str, object] = {}
|
|
154
|
+
if server.provider is not None:
|
|
155
|
+
variables["cloudfall_provider"] = server.provider.as_dict()
|
|
156
|
+
if server.network is not None:
|
|
157
|
+
variables["cloudfall_network"] = server.network.as_dict()
|
|
158
|
+
return variables
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _host_firewall(
|
|
162
|
+
server: ServerInventory, server_type: ServerTypeInventory
|
|
163
|
+
) -> dict[str, object]:
|
|
164
|
+
if server_type.firewall is None:
|
|
165
|
+
return {}
|
|
166
|
+
rules = firewall_rules_for_server(server_type.firewall, server.ssh_port)
|
|
167
|
+
return {
|
|
168
|
+
"cloudfall_firewall": {
|
|
169
|
+
"policy": server_type.firewall.policy.value,
|
|
170
|
+
"allowedInbound": [rule.as_dict() for rule in rules],
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _host_logging(
|
|
176
|
+
server: ServerInventory,
|
|
177
|
+
logging_stacks: tuple[LoggingStackInventory, ...],
|
|
178
|
+
alert_rules: tuple[AlertRuleInventory, ...],
|
|
179
|
+
) -> dict[str, object]:
|
|
180
|
+
variables: dict[str, object] = {}
|
|
181
|
+
backend = next(
|
|
182
|
+
(
|
|
183
|
+
stack
|
|
184
|
+
for stack in logging_stacks
|
|
185
|
+
if stack.backend.server_id == server.resource_id
|
|
186
|
+
),
|
|
187
|
+
None,
|
|
188
|
+
)
|
|
189
|
+
collector = next(
|
|
190
|
+
(
|
|
191
|
+
stack
|
|
192
|
+
for stack in logging_stacks
|
|
193
|
+
if server.resource_id in stack.collectors.server_ids
|
|
194
|
+
),
|
|
195
|
+
None,
|
|
196
|
+
)
|
|
197
|
+
if backend is not None:
|
|
198
|
+
variables["cloudfall_logging_backend"] = backend.as_dict()
|
|
199
|
+
variables["cloudfall_alert_rules"] = [
|
|
200
|
+
rule.as_dict()
|
|
201
|
+
for rule in sorted(
|
|
202
|
+
alert_rules, key=lambda rule: rule.resource_id.value
|
|
203
|
+
)
|
|
204
|
+
if rule.environment == backend.environment
|
|
205
|
+
]
|
|
206
|
+
if collector is not None:
|
|
207
|
+
variables["cloudfall_logging_collector"] = collector.as_dict()
|
|
208
|
+
return variables
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _host_application(application: ApplicationInventory) -> dict[str, object]:
|
|
212
|
+
return {
|
|
213
|
+
"id": application.resource_id.value,
|
|
214
|
+
"user": application.linux_user.value,
|
|
215
|
+
"approval": application.approval.value,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _host_component(
|
|
220
|
+
component: ComponentInventory, application: ApplicationInventory
|
|
221
|
+
) -> dict[str, object]:
|
|
222
|
+
return {
|
|
223
|
+
"id": component.resource_id.value,
|
|
224
|
+
"application": component.application_id.value,
|
|
225
|
+
"application_user": application.linux_user.value,
|
|
226
|
+
"install_root": component.install_root.value,
|
|
227
|
+
"repository": component.repository.as_dict(),
|
|
228
|
+
"runtime": component.runtime.as_dict(),
|
|
229
|
+
"service": component.service.as_dict(),
|
|
230
|
+
"healthCheck": component.health_check.as_dict(),
|
|
231
|
+
"retainUntilCleanup": component.retain_until_cleanup,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _application_is_on_server(
|
|
236
|
+
application: ApplicationInventory,
|
|
237
|
+
server: ServerInventory,
|
|
238
|
+
inventory: PlatformInventory,
|
|
239
|
+
) -> bool:
|
|
240
|
+
application_components = frozenset(application.component_ids)
|
|
241
|
+
return any(
|
|
242
|
+
component.resource_id in application_components
|
|
243
|
+
and server.resource_id in component.server_ids
|
|
244
|
+
for component in inventory.components
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _empty_host_entries(hosts: Iterable[str]) -> dict[str, object]:
|
|
249
|
+
return {host: {} for host in hosts}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _group_name(prefix: str, resource_id: ResourceId) -> str:
|
|
253
|
+
return f"{prefix}_{resource_id.value.replace('-', '_')}"
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Build verifiable release artifacts from component repositories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import tarfile
|
|
11
|
+
import tempfile
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from cloudfall.validation import (
|
|
18
|
+
ConfigValidationError,
|
|
19
|
+
SchemaCatalog,
|
|
20
|
+
ValidationIssue,
|
|
21
|
+
)
|
|
22
|
+
from jsonschema.exceptions import ValidationError
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from cloudfall.inventory import ComponentInventory, PlatformInventory
|
|
26
|
+
|
|
27
|
+
_ARTIFACT_SCHEMA = "artifact.schema.json"
|
|
28
|
+
_GIT_TIMEOUT_SECONDS = 600
|
|
29
|
+
_SHORT_COMMIT_LENGTH = 7
|
|
30
|
+
_GIT_REF_PATTERN = re.compile(r"[A-Za-z0-9][A-Za-z0-9._/@^~-]{0,199}")
|
|
31
|
+
_ERROR_COMPONENT_MISSING = "artifact_component_missing"
|
|
32
|
+
_ERROR_SUBDIRECTORY_MISSING = "artifact_subdirectory_missing"
|
|
33
|
+
_ERROR_REF_INVALID = "artifact_ref_invalid"
|
|
34
|
+
_ERROR_GIT_MISSING = "artifact_git_missing"
|
|
35
|
+
_ERROR_GIT_FAILED = "artifact_git_failed"
|
|
36
|
+
_ERROR_GIT_TIMEOUT = "artifact_git_timeout"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ArtifactBuildError(RuntimeError):
|
|
40
|
+
"""Fail-fast artifact build error with a stable machine-readable code."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, code: str, message: str) -> None:
|
|
43
|
+
"""Record the failure code and human-readable detail."""
|
|
44
|
+
self.code = code
|
|
45
|
+
self.detail = message
|
|
46
|
+
super().__init__(f"{code}: {message}")
|
|
47
|
+
|
|
48
|
+
def as_dict(self) -> dict[str, object]:
|
|
49
|
+
"""Serialize the error envelope for system boundaries."""
|
|
50
|
+
return {
|
|
51
|
+
"status": "error",
|
|
52
|
+
"error": {"code": self.code, "message": self.detail},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class BuiltArtifact:
|
|
58
|
+
"""One built and hashed release artifact."""
|
|
59
|
+
|
|
60
|
+
component_id: str
|
|
61
|
+
release: str
|
|
62
|
+
git_ref: str
|
|
63
|
+
git_commit: str
|
|
64
|
+
built_at: str
|
|
65
|
+
archive_path: Path
|
|
66
|
+
metadata_path: Path
|
|
67
|
+
archive_sha256: str
|
|
68
|
+
size_bytes: int
|
|
69
|
+
|
|
70
|
+
def as_dict(self) -> dict[str, object]:
|
|
71
|
+
"""Serialize the build result for CLI consumers."""
|
|
72
|
+
return {
|
|
73
|
+
"status": "ok",
|
|
74
|
+
"component": self.component_id,
|
|
75
|
+
"release": self.release,
|
|
76
|
+
"gitRef": self.git_ref,
|
|
77
|
+
"gitCommit": self.git_commit,
|
|
78
|
+
"builtAt": self.built_at,
|
|
79
|
+
"archive": str(self.archive_path),
|
|
80
|
+
"metadata": str(self.metadata_path),
|
|
81
|
+
"archiveSha256": self.archive_sha256,
|
|
82
|
+
"sizeBytes": self.size_bytes,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_artifact(
|
|
87
|
+
inventory: PlatformInventory,
|
|
88
|
+
component_id: str,
|
|
89
|
+
git_ref: str,
|
|
90
|
+
output_directory: Path,
|
|
91
|
+
schema_directory: Path,
|
|
92
|
+
) -> BuiltArtifact:
|
|
93
|
+
"""Clone the component repository at a ref and package a release."""
|
|
94
|
+
component = _component(inventory, component_id)
|
|
95
|
+
_validate_ref(git_ref)
|
|
96
|
+
built_at = datetime.now(tz=UTC)
|
|
97
|
+
built_at_text = built_at.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
98
|
+
with tempfile.TemporaryDirectory(prefix="cloudfall-artifact-") as workdir:
|
|
99
|
+
checkout = Path(workdir) / "source"
|
|
100
|
+
_run_git("clone", "--quiet", component.repository.url.value, str(checkout))
|
|
101
|
+
# A fresh clone has local branches only for the default branch, and
|
|
102
|
+
# bare branch names trigger git's remote-branch DWIM, which conflicts
|
|
103
|
+
# with --detach. Resolving the ref to a commit first supports
|
|
104
|
+
# branches, tags, and commits uniformly.
|
|
105
|
+
commit = _resolve_ref_commit(checkout, git_ref)
|
|
106
|
+
_run_git(
|
|
107
|
+
"-C", str(checkout), "checkout", "--quiet", "--detach", commit, "--"
|
|
108
|
+
)
|
|
109
|
+
source_root = checkout
|
|
110
|
+
if component.repository.subdirectory is not None:
|
|
111
|
+
source_root = checkout / component.repository.subdirectory
|
|
112
|
+
if not source_root.is_dir():
|
|
113
|
+
detail = (
|
|
114
|
+
"declared repository subdirectory does not exist: "
|
|
115
|
+
f"{component.repository.subdirectory}"
|
|
116
|
+
)
|
|
117
|
+
raise ArtifactBuildError(
|
|
118
|
+
_ERROR_SUBDIRECTORY_MISSING, detail
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
release = (
|
|
122
|
+
f"{built_at.strftime('%Y%m%dT%H%M%SZ')}"
|
|
123
|
+
f"-{commit[:_SHORT_COMMIT_LENGTH]}"
|
|
124
|
+
)
|
|
125
|
+
component_directory = output_directory / component_id
|
|
126
|
+
component_directory.mkdir(parents=True, exist_ok=True)
|
|
127
|
+
archive_path = component_directory / f"{release}.tar.gz"
|
|
128
|
+
_write_archive(source_root, archive_path)
|
|
129
|
+
|
|
130
|
+
digest = hashlib.sha256(archive_path.read_bytes()).hexdigest()
|
|
131
|
+
size_bytes = archive_path.stat().st_size
|
|
132
|
+
metadata = {
|
|
133
|
+
"apiVersion": "cloudfall/v1",
|
|
134
|
+
"kind": "Artifact",
|
|
135
|
+
"metadata": {
|
|
136
|
+
"id": component_id,
|
|
137
|
+
"description": f"Release artifact for {component_id}",
|
|
138
|
+
},
|
|
139
|
+
"spec": {
|
|
140
|
+
"component": component_id,
|
|
141
|
+
"release": release,
|
|
142
|
+
"gitRef": git_ref,
|
|
143
|
+
"gitCommit": commit,
|
|
144
|
+
"builtAt": built_at_text,
|
|
145
|
+
"archive": archive_path.name,
|
|
146
|
+
"archiveSha256": digest,
|
|
147
|
+
"sizeBytes": size_bytes,
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
catalog = SchemaCatalog(schema_directory)
|
|
151
|
+
try:
|
|
152
|
+
catalog.validate_named(_ARTIFACT_SCHEMA, metadata)
|
|
153
|
+
except ValidationError as error:
|
|
154
|
+
issue = ValidationIssue(
|
|
155
|
+
code="artifact_metadata_invalid",
|
|
156
|
+
message=error.message,
|
|
157
|
+
)
|
|
158
|
+
raise ConfigValidationError(issue) from error
|
|
159
|
+
metadata_path = archive_path.with_suffix("").with_suffix(".json")
|
|
160
|
+
metadata_path.write_text(
|
|
161
|
+
f"{json.dumps(metadata, indent=2, sort_keys=True)}\n",
|
|
162
|
+
encoding="utf-8",
|
|
163
|
+
)
|
|
164
|
+
return BuiltArtifact(
|
|
165
|
+
component_id=component_id,
|
|
166
|
+
release=release,
|
|
167
|
+
git_ref=git_ref,
|
|
168
|
+
git_commit=commit,
|
|
169
|
+
built_at=built_at_text,
|
|
170
|
+
archive_path=archive_path,
|
|
171
|
+
metadata_path=metadata_path,
|
|
172
|
+
archive_sha256=digest,
|
|
173
|
+
size_bytes=size_bytes,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _component(
|
|
178
|
+
inventory: PlatformInventory, component_id: str
|
|
179
|
+
) -> ComponentInventory:
|
|
180
|
+
component = next(
|
|
181
|
+
(
|
|
182
|
+
candidate
|
|
183
|
+
for candidate in inventory.components
|
|
184
|
+
if candidate.resource_id.value == component_id
|
|
185
|
+
),
|
|
186
|
+
None,
|
|
187
|
+
)
|
|
188
|
+
if component is None:
|
|
189
|
+
detail = f"component does not exist: {component_id}"
|
|
190
|
+
raise ArtifactBuildError(_ERROR_COMPONENT_MISSING, detail)
|
|
191
|
+
return component
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _validate_ref(git_ref: str) -> None:
|
|
195
|
+
if not _GIT_REF_PATTERN.fullmatch(git_ref):
|
|
196
|
+
detail = f"git ref contains unsupported characters: {git_ref!r}"
|
|
197
|
+
raise ArtifactBuildError(_ERROR_REF_INVALID, detail)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _resolve_ref_commit(checkout: Path, git_ref: str) -> str:
|
|
201
|
+
candidates = (git_ref, f"origin/{git_ref}")
|
|
202
|
+
for candidate in candidates:
|
|
203
|
+
try:
|
|
204
|
+
return _run_git(
|
|
205
|
+
"-C",
|
|
206
|
+
str(checkout),
|
|
207
|
+
"rev-parse",
|
|
208
|
+
"--verify",
|
|
209
|
+
"--quiet",
|
|
210
|
+
"--end-of-options",
|
|
211
|
+
f"{candidate}^{{commit}}",
|
|
212
|
+
).strip()
|
|
213
|
+
except ArtifactBuildError as error:
|
|
214
|
+
if error.code != _ERROR_GIT_FAILED:
|
|
215
|
+
raise
|
|
216
|
+
detail = f"git ref does not resolve to a commit: {git_ref!r}"
|
|
217
|
+
raise ArtifactBuildError(_ERROR_GIT_FAILED, detail)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _run_git(*arguments: str) -> str:
|
|
221
|
+
binary = shutil.which("git")
|
|
222
|
+
if binary is None:
|
|
223
|
+
detail = "git is not installed on the build host"
|
|
224
|
+
raise ArtifactBuildError(_ERROR_GIT_MISSING, detail)
|
|
225
|
+
try:
|
|
226
|
+
completed = subprocess.run( # noqa: S603 - fixed binary, validated args.
|
|
227
|
+
[binary, *arguments],
|
|
228
|
+
capture_output=True,
|
|
229
|
+
check=True,
|
|
230
|
+
text=True,
|
|
231
|
+
timeout=_GIT_TIMEOUT_SECONDS,
|
|
232
|
+
)
|
|
233
|
+
except subprocess.CalledProcessError as error:
|
|
234
|
+
detail = (
|
|
235
|
+
f"git {' '.join(arguments[:2])} failed: {error.stderr.strip()}"
|
|
236
|
+
)
|
|
237
|
+
raise ArtifactBuildError(_ERROR_GIT_FAILED, detail) from error
|
|
238
|
+
except subprocess.TimeoutExpired as error:
|
|
239
|
+
detail = (
|
|
240
|
+
f"git {' '.join(arguments[:2])} exceeded "
|
|
241
|
+
f"{_GIT_TIMEOUT_SECONDS} seconds"
|
|
242
|
+
)
|
|
243
|
+
raise ArtifactBuildError(_ERROR_GIT_TIMEOUT, detail) from error
|
|
244
|
+
return completed.stdout
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _write_archive(source_root: Path, archive_path: Path) -> None:
|
|
248
|
+
def _skip_git(info: tarfile.TarInfo) -> tarfile.TarInfo | None:
|
|
249
|
+
if ".git" in Path(info.name).parts:
|
|
250
|
+
return None
|
|
251
|
+
info.uid = 0
|
|
252
|
+
info.gid = 0
|
|
253
|
+
info.uname = "root"
|
|
254
|
+
info.gname = "root"
|
|
255
|
+
return info
|
|
256
|
+
|
|
257
|
+
with tarfile.open(archive_path, "w:gz") as archive:
|
|
258
|
+
archive.add(source_root, arcname=".", filter=_skip_git)
|