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
cloudfall/cli.py
ADDED
|
@@ -0,0 +1,1748 @@
|
|
|
1
|
+
"""Command-line boundary for Cloudfall operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
import argparse
|
|
14
|
+
from argparse import Namespace
|
|
15
|
+
from collections.abc import Callable, Sequence
|
|
16
|
+
|
|
17
|
+
from cloudfall.operations import FleetOperations
|
|
18
|
+
from cloudfall.operator import AlertFeed, OperatorProposal
|
|
19
|
+
from cloudfall.validation import ValidatedConfig
|
|
20
|
+
|
|
21
|
+
from cloudfall.agent_tools import AgentConfig
|
|
22
|
+
from cloudfall.arguments import (
|
|
23
|
+
StrictArgumentParser,
|
|
24
|
+
parse_arguments,
|
|
25
|
+
project_path_argument,
|
|
26
|
+
release_id_argument,
|
|
27
|
+
resource_id_argument,
|
|
28
|
+
)
|
|
29
|
+
from cloudfall.audit import AuditStatus, audit_inventory
|
|
30
|
+
from cloudfall.authoring import (
|
|
31
|
+
AuthoringError,
|
|
32
|
+
ServerOptions,
|
|
33
|
+
ServerTypeOptions,
|
|
34
|
+
SshKeyOptions,
|
|
35
|
+
add_server,
|
|
36
|
+
add_server_type,
|
|
37
|
+
add_ssh_key,
|
|
38
|
+
)
|
|
39
|
+
from cloudfall.dashboard import RefreshInterval, build_dashboard
|
|
40
|
+
from cloudfall.dashboard_server import (
|
|
41
|
+
EvidenceSources,
|
|
42
|
+
ListenEndpoint,
|
|
43
|
+
create_dashboard_server,
|
|
44
|
+
)
|
|
45
|
+
from cloudfall.domain import (
|
|
46
|
+
ConnectionAddress,
|
|
47
|
+
Hostname,
|
|
48
|
+
LinuxUser,
|
|
49
|
+
ResourceId,
|
|
50
|
+
TcpPort,
|
|
51
|
+
)
|
|
52
|
+
from cloudfall.importer import (
|
|
53
|
+
ImportTargets,
|
|
54
|
+
RenderImportError,
|
|
55
|
+
import_render_blueprint,
|
|
56
|
+
)
|
|
57
|
+
from cloudfall.inventory import PlatformInventory
|
|
58
|
+
from cloudfall.lifecycle import (
|
|
59
|
+
DeployOptions,
|
|
60
|
+
EngineContext,
|
|
61
|
+
LifecycleError,
|
|
62
|
+
LifecyclePreview,
|
|
63
|
+
backup_service,
|
|
64
|
+
deploy,
|
|
65
|
+
health,
|
|
66
|
+
migrate_data,
|
|
67
|
+
preview_data_migration,
|
|
68
|
+
preview_deploy,
|
|
69
|
+
preview_restart,
|
|
70
|
+
preview_rollback,
|
|
71
|
+
restart,
|
|
72
|
+
rollback,
|
|
73
|
+
verify_backup,
|
|
74
|
+
)
|
|
75
|
+
from cloudfall.migrate import MigrateError, MigrateOptions, execute_migration
|
|
76
|
+
from cloudfall.observation import load_observations
|
|
77
|
+
from cloudfall.operations import UtcTimestamp, build_operations_view
|
|
78
|
+
from cloudfall.operator import (
|
|
79
|
+
ApproveOptions,
|
|
80
|
+
OperatorError,
|
|
81
|
+
ProposalStatus,
|
|
82
|
+
ProposalStore,
|
|
83
|
+
TriggerKind,
|
|
84
|
+
alert_resolution_verifier,
|
|
85
|
+
autonomous_pass,
|
|
86
|
+
drift_pass,
|
|
87
|
+
drift_resolution_verifier,
|
|
88
|
+
engine_auditor,
|
|
89
|
+
engine_executor,
|
|
90
|
+
gateway_feed,
|
|
91
|
+
)
|
|
92
|
+
from cloudfall.operator import (
|
|
93
|
+
approve as operator_approve,
|
|
94
|
+
)
|
|
95
|
+
from cloudfall.operator import (
|
|
96
|
+
run_once as operator_run_once,
|
|
97
|
+
)
|
|
98
|
+
from cloudfall.project import (
|
|
99
|
+
DEFAULT_SOURCE_URL,
|
|
100
|
+
PROJECT_DIRECTORY_VARIABLE,
|
|
101
|
+
GitRevision,
|
|
102
|
+
GitSourceUrl,
|
|
103
|
+
InitOptions,
|
|
104
|
+
ProjectDescription,
|
|
105
|
+
ProjectError,
|
|
106
|
+
ProjectName,
|
|
107
|
+
init_project,
|
|
108
|
+
project_context,
|
|
109
|
+
resolve_installed_revision,
|
|
110
|
+
)
|
|
111
|
+
from cloudfall.render_api import (
|
|
112
|
+
HttpRenderApiClient,
|
|
113
|
+
import_render_api,
|
|
114
|
+
read_api_key,
|
|
115
|
+
)
|
|
116
|
+
from cloudfall.resources import default_engine_directory, default_schema_directory
|
|
117
|
+
from cloudfall.secrets import (
|
|
118
|
+
SecretsError,
|
|
119
|
+
SopsSecretProvider,
|
|
120
|
+
load_environment_receipts,
|
|
121
|
+
render_environment,
|
|
122
|
+
)
|
|
123
|
+
from cloudfall.service_evidence import (
|
|
124
|
+
DeploymentReceiptSet,
|
|
125
|
+
DomainObservationSet,
|
|
126
|
+
EvidenceTimestamp,
|
|
127
|
+
SocketDomainNetworkClient,
|
|
128
|
+
inspect_domains,
|
|
129
|
+
load_deployment_receipts,
|
|
130
|
+
load_domain_observations,
|
|
131
|
+
)
|
|
132
|
+
from cloudfall.validation import (
|
|
133
|
+
ConfigValidationError,
|
|
134
|
+
SchemaCatalog,
|
|
135
|
+
validate_config,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _add_config_parsers(
|
|
140
|
+
commands: argparse._SubParsersAction[StrictArgumentParser],
|
|
141
|
+
) -> None:
|
|
142
|
+
config_parser = commands.add_parser("config", help="operate on the config")
|
|
143
|
+
config_commands = config_parser.add_subparsers(dest="config_command", required=True)
|
|
144
|
+
validate_parser = config_commands.add_parser(
|
|
145
|
+
"validate", help="validate the YAML config and resource references"
|
|
146
|
+
)
|
|
147
|
+
_add_project_directory_argument(validate_parser)
|
|
148
|
+
validate_parser.add_argument(
|
|
149
|
+
"--schemas",
|
|
150
|
+
type=Path,
|
|
151
|
+
default=default_schema_directory(),
|
|
152
|
+
help="versioned schema directory (default: bundled schemas)",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
inventory_parser = commands.add_parser(
|
|
156
|
+
"inventory", help="query validated platform inventory"
|
|
157
|
+
)
|
|
158
|
+
inventory_commands = inventory_parser.add_subparsers(
|
|
159
|
+
dest="inventory_command", required=True
|
|
160
|
+
)
|
|
161
|
+
show_parser = inventory_commands.add_parser(
|
|
162
|
+
"show", help="show non-secret platform inventory"
|
|
163
|
+
)
|
|
164
|
+
_add_project_directory_argument(show_parser)
|
|
165
|
+
show_parser.add_argument(
|
|
166
|
+
"--schemas",
|
|
167
|
+
type=Path,
|
|
168
|
+
default=default_schema_directory(),
|
|
169
|
+
help="versioned schema directory (default: bundled schemas)",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _add_project_directory_argument(parser: argparse.ArgumentParser) -> None:
|
|
174
|
+
parser.add_argument(
|
|
175
|
+
"--project",
|
|
176
|
+
type=Path,
|
|
177
|
+
default=None,
|
|
178
|
+
help=(
|
|
179
|
+
f"project directory to run in (default: ${PROJECT_DIRECTORY_VARIABLE}, "
|
|
180
|
+
"else the current directory when it is a project)"
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _add_init_parser(
|
|
186
|
+
commands: argparse._SubParsersAction[StrictArgumentParser],
|
|
187
|
+
) -> None:
|
|
188
|
+
init_parser = commands.add_parser(
|
|
189
|
+
"init", help="create a new project: fleet, applications, and operations"
|
|
190
|
+
)
|
|
191
|
+
init_parser.add_argument(
|
|
192
|
+
"directory",
|
|
193
|
+
type=Path,
|
|
194
|
+
nargs="?",
|
|
195
|
+
default=Path(),
|
|
196
|
+
help="project directory to create; must be empty or absent (default: .)",
|
|
197
|
+
)
|
|
198
|
+
init_parser.add_argument(
|
|
199
|
+
"--name",
|
|
200
|
+
help="package name written to pyproject.toml (default: the directory name)",
|
|
201
|
+
)
|
|
202
|
+
init_parser.add_argument(
|
|
203
|
+
"--rev",
|
|
204
|
+
help=(
|
|
205
|
+
"Cloudfall commit to pin (default: the commit the running "
|
|
206
|
+
"cloudfall was installed from)"
|
|
207
|
+
),
|
|
208
|
+
)
|
|
209
|
+
init_parser.add_argument(
|
|
210
|
+
"--description",
|
|
211
|
+
help=(
|
|
212
|
+
"one line saying what the project manages, written to the README "
|
|
213
|
+
"and pyproject.toml"
|
|
214
|
+
),
|
|
215
|
+
)
|
|
216
|
+
init_parser.add_argument(
|
|
217
|
+
"--source",
|
|
218
|
+
default=DEFAULT_SOURCE_URL,
|
|
219
|
+
help=(
|
|
220
|
+
f"git location of Cloudfall to install from (default: {DEFAULT_SOURCE_URL})"
|
|
221
|
+
),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _add_add_parsers(
|
|
226
|
+
commands: argparse._SubParsersAction[StrictArgumentParser],
|
|
227
|
+
) -> None:
|
|
228
|
+
add_parser = commands.add_parser(
|
|
229
|
+
"add", help="write a fleet resource into the project"
|
|
230
|
+
)
|
|
231
|
+
add_commands = add_parser.add_subparsers(dest="add_command", required=True)
|
|
232
|
+
|
|
233
|
+
key_parser = add_commands.add_parser(
|
|
234
|
+
"ssh-key", help="declare an SSH public key read from a file"
|
|
235
|
+
)
|
|
236
|
+
key_parser.add_argument("key_file", type=Path, help="e.g. ~/.ssh/id_ed25519.pub")
|
|
237
|
+
key_parser.add_argument("--owner", required=True, help="who the key belongs to")
|
|
238
|
+
key_parser.add_argument("--id", help="resource id (default: the owner)")
|
|
239
|
+
key_parser.add_argument(
|
|
240
|
+
"--environment", default="production", help="(default: production)"
|
|
241
|
+
)
|
|
242
|
+
key_parser.add_argument("--description")
|
|
243
|
+
|
|
244
|
+
type_parser = add_commands.add_parser(
|
|
245
|
+
"server-type",
|
|
246
|
+
help="declare a server type from the bundled Debian 13 baseline",
|
|
247
|
+
)
|
|
248
|
+
type_parser.add_argument("id", help="resource id, e.g. debian-application")
|
|
249
|
+
type_parser.add_argument("--description")
|
|
250
|
+
|
|
251
|
+
server_parser = add_commands.add_parser(
|
|
252
|
+
"server", help="declare a server; creates its server type when missing"
|
|
253
|
+
)
|
|
254
|
+
server_parser.add_argument("id", help="resource id, e.g. h1")
|
|
255
|
+
server_parser.add_argument(
|
|
256
|
+
"--address", required=True, help="IP address or hostname to connect to"
|
|
257
|
+
)
|
|
258
|
+
server_parser.add_argument(
|
|
259
|
+
"--type",
|
|
260
|
+
default="debian-application",
|
|
261
|
+
help="server type id (default: debian-application)",
|
|
262
|
+
)
|
|
263
|
+
server_parser.add_argument(
|
|
264
|
+
"--environment", default="production", help="(default: production)"
|
|
265
|
+
)
|
|
266
|
+
server_parser.add_argument(
|
|
267
|
+
"--hostname", help="(default: the address when it is a hostname, else the id)"
|
|
268
|
+
)
|
|
269
|
+
server_parser.add_argument("--ssh-user", default="root", help="(default: root)")
|
|
270
|
+
server_parser.add_argument("--ssh-port", type=int, default=22, help="(default: 22)")
|
|
271
|
+
server_parser.add_argument("--description")
|
|
272
|
+
|
|
273
|
+
for subparser in (key_parser, type_parser, server_parser):
|
|
274
|
+
_add_project_directory_argument(subparser)
|
|
275
|
+
subparser.add_argument(
|
|
276
|
+
"--schemas",
|
|
277
|
+
type=Path,
|
|
278
|
+
default=default_schema_directory(),
|
|
279
|
+
help="versioned schema directory (default: bundled schemas)",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _parser() -> StrictArgumentParser:
|
|
284
|
+
parser = StrictArgumentParser(prog="cloudfall")
|
|
285
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
286
|
+
_add_init_parser(commands)
|
|
287
|
+
_add_add_parsers(commands)
|
|
288
|
+
_add_config_parsers(commands)
|
|
289
|
+
|
|
290
|
+
audit_parser = commands.add_parser(
|
|
291
|
+
"audit", help="compare the config with observed server snapshots"
|
|
292
|
+
)
|
|
293
|
+
_add_project_directory_argument(audit_parser)
|
|
294
|
+
audit_parser.add_argument(
|
|
295
|
+
"--observed",
|
|
296
|
+
type=project_path_argument,
|
|
297
|
+
required=True,
|
|
298
|
+
help="directory containing observed-server JSON snapshots",
|
|
299
|
+
)
|
|
300
|
+
audit_parser.add_argument(
|
|
301
|
+
"--env-receipts",
|
|
302
|
+
type=project_path_argument,
|
|
303
|
+
default=Path("tmp/env-receipts"),
|
|
304
|
+
help=(
|
|
305
|
+
"rendered environment receipt directory used for env-file "
|
|
306
|
+
"drift checks (default: tmp/env-receipts)"
|
|
307
|
+
),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
_add_operator_parsers(commands)
|
|
311
|
+
_add_backup_parsers(commands)
|
|
312
|
+
_add_secrets_parsers(commands)
|
|
313
|
+
|
|
314
|
+
services_parser = commands.add_parser(
|
|
315
|
+
"services", help="inspect and report public service lifecycles"
|
|
316
|
+
)
|
|
317
|
+
services_commands = services_parser.add_subparsers(
|
|
318
|
+
dest="services_command", required=True
|
|
319
|
+
)
|
|
320
|
+
services_inspect_parser = services_commands.add_parser(
|
|
321
|
+
"inspect", help="collect DNS, TLS, origin, and public route evidence"
|
|
322
|
+
)
|
|
323
|
+
_add_project_directory_argument(services_inspect_parser)
|
|
324
|
+
services_inspect_parser.add_argument(
|
|
325
|
+
"--output",
|
|
326
|
+
type=project_path_argument,
|
|
327
|
+
default=Path("tmp/observed-services"),
|
|
328
|
+
help="service observation directory (default: tmp/observed-services)",
|
|
329
|
+
)
|
|
330
|
+
services_inspect_parser.add_argument(
|
|
331
|
+
"--schemas",
|
|
332
|
+
type=Path,
|
|
333
|
+
default=default_schema_directory(),
|
|
334
|
+
help="versioned schema directory (default: bundled schemas)",
|
|
335
|
+
)
|
|
336
|
+
services_status_parser = services_commands.add_parser(
|
|
337
|
+
"status", help="derive service lifecycle status from current evidence"
|
|
338
|
+
)
|
|
339
|
+
_add_project_directory_argument(services_status_parser)
|
|
340
|
+
services_status_parser.add_argument(
|
|
341
|
+
"--observed",
|
|
342
|
+
type=project_path_argument,
|
|
343
|
+
required=True,
|
|
344
|
+
help="directory containing observed-server JSON snapshots",
|
|
345
|
+
)
|
|
346
|
+
services_status_parser.add_argument(
|
|
347
|
+
"--service-observed",
|
|
348
|
+
type=project_path_argument,
|
|
349
|
+
default=Path("tmp/observed-services"),
|
|
350
|
+
help="domain observation directory (default: tmp/observed-services)",
|
|
351
|
+
)
|
|
352
|
+
services_status_parser.add_argument(
|
|
353
|
+
"--deployments",
|
|
354
|
+
type=project_path_argument,
|
|
355
|
+
default=Path("tmp/deployments"),
|
|
356
|
+
help="deployment receipt directory (default: tmp/deployments)",
|
|
357
|
+
)
|
|
358
|
+
services_status_parser.add_argument(
|
|
359
|
+
"--schemas",
|
|
360
|
+
type=Path,
|
|
361
|
+
default=default_schema_directory(),
|
|
362
|
+
help="versioned schema directory (default: bundled schemas)",
|
|
363
|
+
)
|
|
364
|
+
audit_parser.add_argument(
|
|
365
|
+
"--schemas",
|
|
366
|
+
type=Path,
|
|
367
|
+
default=default_schema_directory(),
|
|
368
|
+
help="versioned schema directory (default: bundled schemas)",
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
dashboard_parser = commands.add_parser(
|
|
372
|
+
"dashboard", help="build operations dashboard artifacts"
|
|
373
|
+
)
|
|
374
|
+
dashboard_commands = dashboard_parser.add_subparsers(
|
|
375
|
+
dest="dashboard_command", required=True
|
|
376
|
+
)
|
|
377
|
+
dashboard_build_parser = dashboard_commands.add_parser(
|
|
378
|
+
"build", help="build a static read-only operations dashboard"
|
|
379
|
+
)
|
|
380
|
+
_add_project_directory_argument(dashboard_build_parser)
|
|
381
|
+
dashboard_build_parser.add_argument(
|
|
382
|
+
"--observed",
|
|
383
|
+
type=project_path_argument,
|
|
384
|
+
required=True,
|
|
385
|
+
help="directory containing observed-server JSON snapshots",
|
|
386
|
+
)
|
|
387
|
+
dashboard_build_parser.add_argument(
|
|
388
|
+
"--service-observed",
|
|
389
|
+
type=project_path_argument,
|
|
390
|
+
default=Path("tmp/observed-services"),
|
|
391
|
+
help="domain observation directory (default: tmp/observed-services)",
|
|
392
|
+
)
|
|
393
|
+
dashboard_build_parser.add_argument(
|
|
394
|
+
"--deployments",
|
|
395
|
+
type=project_path_argument,
|
|
396
|
+
default=Path("tmp/deployments"),
|
|
397
|
+
help="deployment receipt directory (default: tmp/deployments)",
|
|
398
|
+
)
|
|
399
|
+
dashboard_build_parser.add_argument(
|
|
400
|
+
"--output",
|
|
401
|
+
type=project_path_argument,
|
|
402
|
+
default=Path("tmp/dashboard"),
|
|
403
|
+
help="dashboard output directory (default: tmp/dashboard)",
|
|
404
|
+
)
|
|
405
|
+
dashboard_build_parser.add_argument(
|
|
406
|
+
"--schemas",
|
|
407
|
+
type=Path,
|
|
408
|
+
default=default_schema_directory(),
|
|
409
|
+
help="versioned schema directory (default: bundled schemas)",
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
dashboard_serve_parser = dashboard_commands.add_parser(
|
|
413
|
+
"serve", help="serve a live-refreshing read-only dashboard over HTTP"
|
|
414
|
+
)
|
|
415
|
+
_add_project_directory_argument(dashboard_serve_parser)
|
|
416
|
+
dashboard_serve_parser.add_argument(
|
|
417
|
+
"--observed",
|
|
418
|
+
type=project_path_argument,
|
|
419
|
+
required=True,
|
|
420
|
+
help="directory containing observed-server JSON snapshots",
|
|
421
|
+
)
|
|
422
|
+
dashboard_serve_parser.add_argument(
|
|
423
|
+
"--service-observed",
|
|
424
|
+
type=project_path_argument,
|
|
425
|
+
default=Path("tmp/observed-services"),
|
|
426
|
+
help="domain observation directory (default: tmp/observed-services)",
|
|
427
|
+
)
|
|
428
|
+
dashboard_serve_parser.add_argument(
|
|
429
|
+
"--deployments",
|
|
430
|
+
type=project_path_argument,
|
|
431
|
+
default=Path("tmp/deployments"),
|
|
432
|
+
help="deployment receipt directory (default: tmp/deployments)",
|
|
433
|
+
)
|
|
434
|
+
dashboard_serve_parser.add_argument(
|
|
435
|
+
"--schemas",
|
|
436
|
+
type=Path,
|
|
437
|
+
default=default_schema_directory(),
|
|
438
|
+
help="versioned schema directory (default: bundled schemas)",
|
|
439
|
+
)
|
|
440
|
+
dashboard_serve_parser.add_argument(
|
|
441
|
+
"--host",
|
|
442
|
+
default="127.0.0.1",
|
|
443
|
+
help="listen address; use the VPS address to expose it (default: 127.0.0.1)",
|
|
444
|
+
)
|
|
445
|
+
dashboard_serve_parser.add_argument(
|
|
446
|
+
"--port",
|
|
447
|
+
type=int,
|
|
448
|
+
default=8100,
|
|
449
|
+
help="listen port; 0 picks a free port (default: 8100)",
|
|
450
|
+
)
|
|
451
|
+
dashboard_serve_parser.add_argument(
|
|
452
|
+
"--refresh",
|
|
453
|
+
type=int,
|
|
454
|
+
default=10,
|
|
455
|
+
help="evidence refresh interval in seconds (default: 10)",
|
|
456
|
+
)
|
|
457
|
+
dashboard_serve_parser.add_argument(
|
|
458
|
+
"--inspect-services",
|
|
459
|
+
action="store_true",
|
|
460
|
+
help="probe DNS, TLS, origin, and public routes on every refresh",
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
_add_lifecycle_parsers(commands)
|
|
464
|
+
_add_import_parsers(commands)
|
|
465
|
+
_add_migrate_parser(commands)
|
|
466
|
+
return parser
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _add_migrate_parser(
|
|
470
|
+
commands: argparse._SubParsersAction[StrictArgumentParser],
|
|
471
|
+
) -> None:
|
|
472
|
+
migrate_parser = commands.add_parser(
|
|
473
|
+
"migrate",
|
|
474
|
+
help="run the resumable end-to-end migration plan",
|
|
475
|
+
)
|
|
476
|
+
_add_project_directory_argument(migrate_parser)
|
|
477
|
+
migrate_parser.add_argument(
|
|
478
|
+
"--schemas",
|
|
479
|
+
type=Path,
|
|
480
|
+
default=default_schema_directory(),
|
|
481
|
+
help="versioned schema directory (default: bundled schemas)",
|
|
482
|
+
)
|
|
483
|
+
migrate_parser.add_argument(
|
|
484
|
+
"--engine",
|
|
485
|
+
type=Path,
|
|
486
|
+
default=default_engine_directory(),
|
|
487
|
+
help="engine directory containing ansible contracts (default: bundled engine)",
|
|
488
|
+
)
|
|
489
|
+
migrate_parser.add_argument(
|
|
490
|
+
"--inventory-file",
|
|
491
|
+
type=project_path_argument,
|
|
492
|
+
default=Path("tmp/ansible-inventory.json"),
|
|
493
|
+
help="rendered inventory path (default: tmp/ansible-inventory.json)",
|
|
494
|
+
)
|
|
495
|
+
migrate_parser.add_argument(
|
|
496
|
+
"--observed",
|
|
497
|
+
type=project_path_argument,
|
|
498
|
+
default=Path("tmp/observed"),
|
|
499
|
+
help="server observation directory (default: tmp/observed)",
|
|
500
|
+
)
|
|
501
|
+
migrate_parser.add_argument(
|
|
502
|
+
"--service-observed",
|
|
503
|
+
type=project_path_argument,
|
|
504
|
+
default=Path("tmp/observed-services"),
|
|
505
|
+
help="domain observation directory (default: tmp/observed-services)",
|
|
506
|
+
)
|
|
507
|
+
migrate_parser.add_argument(
|
|
508
|
+
"--deployments",
|
|
509
|
+
type=project_path_argument,
|
|
510
|
+
default=Path("tmp/deployments"),
|
|
511
|
+
help="domain receipt directory (default: tmp/deployments)",
|
|
512
|
+
)
|
|
513
|
+
migrate_parser.add_argument(
|
|
514
|
+
"--receipts",
|
|
515
|
+
type=project_path_argument,
|
|
516
|
+
default=Path("tmp/releases"),
|
|
517
|
+
help="release receipt directory (default: tmp/releases)",
|
|
518
|
+
)
|
|
519
|
+
migrate_parser.add_argument(
|
|
520
|
+
"--artifacts",
|
|
521
|
+
type=project_path_argument,
|
|
522
|
+
default=Path("tmp/artifacts"),
|
|
523
|
+
help="artifact directory (default: tmp/artifacts)",
|
|
524
|
+
)
|
|
525
|
+
migrate_parser.add_argument(
|
|
526
|
+
"--plan-file",
|
|
527
|
+
type=project_path_argument,
|
|
528
|
+
default=Path("tmp/migrate/plan.json"),
|
|
529
|
+
help="persisted plan location (default: tmp/migrate/plan.json)",
|
|
530
|
+
)
|
|
531
|
+
migrate_parser.add_argument(
|
|
532
|
+
"--build",
|
|
533
|
+
action="append",
|
|
534
|
+
default=[],
|
|
535
|
+
metavar="COMPONENT=GIT_REF",
|
|
536
|
+
help="build this component from a git ref (repeatable)",
|
|
537
|
+
)
|
|
538
|
+
migrate_parser.add_argument(
|
|
539
|
+
"--release",
|
|
540
|
+
action="append",
|
|
541
|
+
default=[],
|
|
542
|
+
metavar="COMPONENT=RELEASE_ID",
|
|
543
|
+
help="deploy this component from an already-built release",
|
|
544
|
+
)
|
|
545
|
+
migrate_parser.add_argument(
|
|
546
|
+
"--env-file",
|
|
547
|
+
action="append",
|
|
548
|
+
default=[],
|
|
549
|
+
metavar="COMPONENT=PATH",
|
|
550
|
+
help="environment file passed to this component's deployment",
|
|
551
|
+
)
|
|
552
|
+
migrate_parser.add_argument(
|
|
553
|
+
"--data",
|
|
554
|
+
action="append",
|
|
555
|
+
default=[],
|
|
556
|
+
metavar="DATABASE=URL_FILE",
|
|
557
|
+
help=(
|
|
558
|
+
"migrate external data into a declared database before cutover; "
|
|
559
|
+
"URL_FILE is a controller file containing only the source "
|
|
560
|
+
"connection URL"
|
|
561
|
+
),
|
|
562
|
+
)
|
|
563
|
+
migrate_parser.add_argument(
|
|
564
|
+
"--yes",
|
|
565
|
+
action="store_true",
|
|
566
|
+
help="execute the plan; without this flag only the plan is shown",
|
|
567
|
+
)
|
|
568
|
+
migrate_parser.add_argument(
|
|
569
|
+
"--restart",
|
|
570
|
+
action="store_true",
|
|
571
|
+
help="discard the persisted plan and start over",
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _add_lifecycle_parsers(
|
|
576
|
+
commands: argparse._SubParsersAction[StrictArgumentParser],
|
|
577
|
+
) -> None:
|
|
578
|
+
data_parser = commands.add_parser(
|
|
579
|
+
"data", help="migrate data into declared services"
|
|
580
|
+
)
|
|
581
|
+
data_commands = data_parser.add_subparsers(dest="data_command", required=True)
|
|
582
|
+
data_migrate_parser = data_commands.add_parser(
|
|
583
|
+
"migrate",
|
|
584
|
+
help=(
|
|
585
|
+
"dump an external PostgreSQL database and restore it into a "
|
|
586
|
+
"declared service with row-count verification"
|
|
587
|
+
),
|
|
588
|
+
)
|
|
589
|
+
_add_project_directory_argument(data_migrate_parser)
|
|
590
|
+
data_migrate_parser.add_argument("service", type=resource_id_argument)
|
|
591
|
+
_add_execute_argument(data_migrate_parser)
|
|
592
|
+
data_migrate_parser.add_argument(
|
|
593
|
+
"--database",
|
|
594
|
+
required=True,
|
|
595
|
+
help="declared database name inside the service",
|
|
596
|
+
)
|
|
597
|
+
data_migrate_parser.add_argument(
|
|
598
|
+
"--source-url-file",
|
|
599
|
+
type=Path,
|
|
600
|
+
required=True,
|
|
601
|
+
help=(
|
|
602
|
+
"controller-side file whose only content is the source "
|
|
603
|
+
"database connection URL"
|
|
604
|
+
),
|
|
605
|
+
)
|
|
606
|
+
data_migrate_parser.add_argument(
|
|
607
|
+
"--receipts",
|
|
608
|
+
type=project_path_argument,
|
|
609
|
+
default=Path("tmp/data-migrations"),
|
|
610
|
+
help="migration receipt directory (default: tmp/data-migrations)",
|
|
611
|
+
)
|
|
612
|
+
data_migrate_parser.add_argument(
|
|
613
|
+
"--schemas",
|
|
614
|
+
type=Path,
|
|
615
|
+
default=default_schema_directory(),
|
|
616
|
+
help="versioned schema directory (default: bundled schemas)",
|
|
617
|
+
)
|
|
618
|
+
data_migrate_parser.add_argument(
|
|
619
|
+
"--engine",
|
|
620
|
+
type=Path,
|
|
621
|
+
default=default_engine_directory(),
|
|
622
|
+
help="engine directory containing ansible contracts (default: bundled engine)",
|
|
623
|
+
)
|
|
624
|
+
data_migrate_parser.add_argument(
|
|
625
|
+
"--inventory-file",
|
|
626
|
+
type=project_path_argument,
|
|
627
|
+
default=Path("tmp/ansible-inventory.json"),
|
|
628
|
+
help="rendered inventory path (default: tmp/ansible-inventory.json)",
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
deploy_parser = commands.add_parser(
|
|
632
|
+
"deploy", help="deploy one built component release"
|
|
633
|
+
)
|
|
634
|
+
_add_lifecycle_arguments(deploy_parser)
|
|
635
|
+
_add_execute_argument(deploy_parser)
|
|
636
|
+
deploy_parser.add_argument(
|
|
637
|
+
"--release",
|
|
638
|
+
type=release_id_argument,
|
|
639
|
+
required=True,
|
|
640
|
+
help="release id produced by cloudfall-engine artifact build",
|
|
641
|
+
)
|
|
642
|
+
deploy_parser.add_argument(
|
|
643
|
+
"--artifacts",
|
|
644
|
+
type=project_path_argument,
|
|
645
|
+
default=Path("tmp/artifacts"),
|
|
646
|
+
help="artifact directory (default: tmp/artifacts)",
|
|
647
|
+
)
|
|
648
|
+
deploy_parser.add_argument(
|
|
649
|
+
"--env-file",
|
|
650
|
+
type=Path,
|
|
651
|
+
help="optional controller-side environment file for the component",
|
|
652
|
+
)
|
|
653
|
+
deploy_parser.add_argument(
|
|
654
|
+
"--receipts",
|
|
655
|
+
type=project_path_argument,
|
|
656
|
+
default=Path("tmp/releases"),
|
|
657
|
+
help="release receipt directory (default: tmp/releases)",
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
rollback_parser = commands.add_parser(
|
|
661
|
+
"rollback", help="switch one component back to an existing release"
|
|
662
|
+
)
|
|
663
|
+
_add_lifecycle_arguments(rollback_parser)
|
|
664
|
+
_add_execute_argument(rollback_parser)
|
|
665
|
+
rollback_parser.add_argument(
|
|
666
|
+
"--release",
|
|
667
|
+
type=release_id_argument,
|
|
668
|
+
required=True,
|
|
669
|
+
help="existing release id to activate",
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
restart_parser = commands.add_parser(
|
|
673
|
+
"restart", help="restart one component behind its health check"
|
|
674
|
+
)
|
|
675
|
+
_add_lifecycle_arguments(restart_parser)
|
|
676
|
+
_add_execute_argument(restart_parser)
|
|
677
|
+
|
|
678
|
+
health_parser = commands.add_parser(
|
|
679
|
+
"health", help="probe one component's declared health check"
|
|
680
|
+
)
|
|
681
|
+
_add_lifecycle_arguments(health_parser)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _add_import_parsers(
|
|
685
|
+
commands: argparse._SubParsersAction[StrictArgumentParser],
|
|
686
|
+
) -> None:
|
|
687
|
+
import_parser = commands.add_parser(
|
|
688
|
+
"import", help="import external platform definitions"
|
|
689
|
+
)
|
|
690
|
+
import_commands = import_parser.add_subparsers(dest="import_command", required=True)
|
|
691
|
+
render_parser = import_commands.add_parser(
|
|
692
|
+
"render", help="map a render.yaml blueprint onto Cloudfall config"
|
|
693
|
+
)
|
|
694
|
+
_add_project_directory_argument(render_parser)
|
|
695
|
+
render_parser.add_argument("blueprint", type=Path)
|
|
696
|
+
render_parser.add_argument(
|
|
697
|
+
"--application",
|
|
698
|
+
type=resource_id_argument,
|
|
699
|
+
required=True,
|
|
700
|
+
help="Cloudfall application id (also the application's Linux user)",
|
|
701
|
+
)
|
|
702
|
+
render_parser.add_argument(
|
|
703
|
+
"--server",
|
|
704
|
+
type=resource_id_argument,
|
|
705
|
+
required=True,
|
|
706
|
+
help="declared server id that receives every imported resource",
|
|
707
|
+
)
|
|
708
|
+
render_parser.add_argument(
|
|
709
|
+
"--output",
|
|
710
|
+
type=project_path_argument,
|
|
711
|
+
default=Path("tmp/import/config"),
|
|
712
|
+
help="config fragment output directory (default: tmp/import/config)",
|
|
713
|
+
)
|
|
714
|
+
render_parser.add_argument(
|
|
715
|
+
"--env-dir",
|
|
716
|
+
type=project_path_argument,
|
|
717
|
+
default=Path("tmp/import/env"),
|
|
718
|
+
help="environment file output directory (default: tmp/import/env)",
|
|
719
|
+
)
|
|
720
|
+
render_parser.add_argument(
|
|
721
|
+
"--schemas",
|
|
722
|
+
type=Path,
|
|
723
|
+
default=default_schema_directory(),
|
|
724
|
+
help="versioned schema directory (default: bundled schemas)",
|
|
725
|
+
)
|
|
726
|
+
render_api_parser = import_commands.add_parser(
|
|
727
|
+
"render-api",
|
|
728
|
+
help="map a live Render workspace onto Cloudfall config via the API",
|
|
729
|
+
)
|
|
730
|
+
_add_project_directory_argument(render_api_parser)
|
|
731
|
+
render_api_parser.add_argument(
|
|
732
|
+
"--api-key-file",
|
|
733
|
+
type=Path,
|
|
734
|
+
required=True,
|
|
735
|
+
help="file containing only the Render API key",
|
|
736
|
+
)
|
|
737
|
+
render_api_parser.add_argument(
|
|
738
|
+
"--api-url",
|
|
739
|
+
default="https://api.render.com/v1",
|
|
740
|
+
help="Render API base URL (default: https://api.render.com/v1)",
|
|
741
|
+
)
|
|
742
|
+
render_api_parser.add_argument(
|
|
743
|
+
"--application",
|
|
744
|
+
type=resource_id_argument,
|
|
745
|
+
required=True,
|
|
746
|
+
help="Cloudfall application id (also the application's Linux user)",
|
|
747
|
+
)
|
|
748
|
+
render_api_parser.add_argument(
|
|
749
|
+
"--server",
|
|
750
|
+
type=resource_id_argument,
|
|
751
|
+
required=True,
|
|
752
|
+
help="declared server id that receives every imported resource",
|
|
753
|
+
)
|
|
754
|
+
render_api_parser.add_argument(
|
|
755
|
+
"--output",
|
|
756
|
+
type=project_path_argument,
|
|
757
|
+
default=Path("tmp/import/config"),
|
|
758
|
+
help="config fragment output directory (default: tmp/import/config)",
|
|
759
|
+
)
|
|
760
|
+
render_api_parser.add_argument(
|
|
761
|
+
"--env-dir",
|
|
762
|
+
type=project_path_argument,
|
|
763
|
+
default=Path("tmp/import/env"),
|
|
764
|
+
help="environment file output directory (default: tmp/import/env)",
|
|
765
|
+
)
|
|
766
|
+
render_api_parser.add_argument(
|
|
767
|
+
"--schemas",
|
|
768
|
+
type=Path,
|
|
769
|
+
default=default_schema_directory(),
|
|
770
|
+
help="versioned schema directory (default: bundled schemas)",
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _add_operator_parsers(
|
|
775
|
+
commands: argparse._SubParsersAction[StrictArgumentParser],
|
|
776
|
+
) -> None:
|
|
777
|
+
operator_parser = commands.add_parser(
|
|
778
|
+
"operator", help="alert-driven propose-and-approve operation"
|
|
779
|
+
)
|
|
780
|
+
operator_commands = operator_parser.add_subparsers(
|
|
781
|
+
dest="operator_command", required=True
|
|
782
|
+
)
|
|
783
|
+
operator_run_parser = operator_commands.add_parser(
|
|
784
|
+
"run", help="watch declared alerts and drift, write proposals"
|
|
785
|
+
)
|
|
786
|
+
_add_operator_arguments(operator_run_parser)
|
|
787
|
+
_add_operator_feed_arguments(operator_run_parser, required=True)
|
|
788
|
+
_add_operator_engine_arguments(operator_run_parser)
|
|
789
|
+
operator_run_parser.add_argument(
|
|
790
|
+
"--interval",
|
|
791
|
+
type=float,
|
|
792
|
+
default=None,
|
|
793
|
+
help="seconds between watch passes (default: one pass, then exit)",
|
|
794
|
+
)
|
|
795
|
+
operator_run_parser.add_argument(
|
|
796
|
+
"--drift-interval",
|
|
797
|
+
type=float,
|
|
798
|
+
default=None,
|
|
799
|
+
help=(
|
|
800
|
+
"seconds between audited drift checks (default: no drift "
|
|
801
|
+
"checks; a single pass runs one when set)"
|
|
802
|
+
),
|
|
803
|
+
)
|
|
804
|
+
operator_run_parser.add_argument(
|
|
805
|
+
"--observed",
|
|
806
|
+
type=project_path_argument,
|
|
807
|
+
default=Path("tmp/operator/observed"),
|
|
808
|
+
help=(
|
|
809
|
+
"observation directory for drift checks (default: tmp/operator/observed)"
|
|
810
|
+
),
|
|
811
|
+
)
|
|
812
|
+
operator_list_parser = operator_commands.add_parser(
|
|
813
|
+
"list", help="list proposal receipts"
|
|
814
|
+
)
|
|
815
|
+
_add_operator_arguments(operator_list_parser)
|
|
816
|
+
operator_show_parser = operator_commands.add_parser(
|
|
817
|
+
"show", help="show one proposal receipt"
|
|
818
|
+
)
|
|
819
|
+
_add_operator_arguments(operator_show_parser)
|
|
820
|
+
operator_show_parser.add_argument("proposal", type=resource_id_argument)
|
|
821
|
+
operator_approve_parser = operator_commands.add_parser(
|
|
822
|
+
"approve", help="execute a proposal and verify its trigger resolves"
|
|
823
|
+
)
|
|
824
|
+
_add_operator_arguments(operator_approve_parser)
|
|
825
|
+
_add_operator_feed_arguments(operator_approve_parser, required=False)
|
|
826
|
+
_add_operator_engine_arguments(operator_approve_parser)
|
|
827
|
+
operator_approve_parser.add_argument("proposal", type=resource_id_argument)
|
|
828
|
+
operator_approve_parser.add_argument(
|
|
829
|
+
"--observed",
|
|
830
|
+
type=project_path_argument,
|
|
831
|
+
default=Path("tmp/operator/observed"),
|
|
832
|
+
help=(
|
|
833
|
+
"observation directory for drift verification "
|
|
834
|
+
"(default: tmp/operator/observed)"
|
|
835
|
+
),
|
|
836
|
+
)
|
|
837
|
+
operator_approve_parser.add_argument(
|
|
838
|
+
"--verify-timeout",
|
|
839
|
+
type=float,
|
|
840
|
+
default=180.0,
|
|
841
|
+
help="seconds to wait for the trigger to resolve (default: 180)",
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def _add_secrets_parsers(
|
|
846
|
+
commands: argparse._SubParsersAction[StrictArgumentParser],
|
|
847
|
+
) -> None:
|
|
848
|
+
secrets_parser = commands.add_parser(
|
|
849
|
+
"secrets", help="resolve declared secret references"
|
|
850
|
+
)
|
|
851
|
+
secrets_commands = secrets_parser.add_subparsers(
|
|
852
|
+
dest="secrets_command", required=True
|
|
853
|
+
)
|
|
854
|
+
render_parser = secrets_commands.add_parser(
|
|
855
|
+
"render",
|
|
856
|
+
help="render one component's references into its environment file",
|
|
857
|
+
)
|
|
858
|
+
_add_project_directory_argument(render_parser)
|
|
859
|
+
render_parser.add_argument("component", type=resource_id_argument)
|
|
860
|
+
render_parser.add_argument(
|
|
861
|
+
"--schemas",
|
|
862
|
+
type=Path,
|
|
863
|
+
default=default_schema_directory(),
|
|
864
|
+
help="versioned schema directory (default: bundled schemas)",
|
|
865
|
+
)
|
|
866
|
+
render_parser.add_argument(
|
|
867
|
+
"--secrets-dir",
|
|
868
|
+
type=Path,
|
|
869
|
+
default=Path("secrets"),
|
|
870
|
+
help="sops-encrypted secrets directory (default: secrets)",
|
|
871
|
+
)
|
|
872
|
+
render_parser.add_argument(
|
|
873
|
+
"--output",
|
|
874
|
+
type=project_path_argument,
|
|
875
|
+
default=None,
|
|
876
|
+
help=("environment file to write (default: tmp/env/<component>.env)"),
|
|
877
|
+
)
|
|
878
|
+
render_parser.add_argument(
|
|
879
|
+
"--receipts",
|
|
880
|
+
type=project_path_argument,
|
|
881
|
+
default=Path("tmp/env-receipts"),
|
|
882
|
+
help=("environment receipt directory (default: tmp/env-receipts)"),
|
|
883
|
+
)
|
|
884
|
+
render_parser.add_argument(
|
|
885
|
+
"--engine",
|
|
886
|
+
type=Path,
|
|
887
|
+
default=default_engine_directory(),
|
|
888
|
+
help="engine directory containing ansible contracts (default: bundled engine)",
|
|
889
|
+
)
|
|
890
|
+
render_parser.add_argument(
|
|
891
|
+
"--inventory-file",
|
|
892
|
+
type=project_path_argument,
|
|
893
|
+
default=Path("tmp/ansible-inventory.json"),
|
|
894
|
+
help="rendered inventory path (default: tmp/ansible-inventory.json)",
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
def _add_backup_parsers(
|
|
899
|
+
commands: argparse._SubParsersAction[StrictArgumentParser],
|
|
900
|
+
) -> None:
|
|
901
|
+
backup_parser = commands.add_parser(
|
|
902
|
+
"backup", help="run and prove declared service backups"
|
|
903
|
+
)
|
|
904
|
+
backup_commands = backup_parser.add_subparsers(dest="backup_command", required=True)
|
|
905
|
+
for name, description in (
|
|
906
|
+
("run", "run the declared backup for one service"),
|
|
907
|
+
("verify", "prove the newest backup restores for one service"),
|
|
908
|
+
):
|
|
909
|
+
subparser = backup_commands.add_parser(name, help=description)
|
|
910
|
+
_add_project_directory_argument(subparser)
|
|
911
|
+
subparser.add_argument("service", type=resource_id_argument)
|
|
912
|
+
subparser.add_argument(
|
|
913
|
+
"--schemas",
|
|
914
|
+
type=Path,
|
|
915
|
+
default=default_schema_directory(),
|
|
916
|
+
help="versioned schema directory (default: bundled schemas)",
|
|
917
|
+
)
|
|
918
|
+
subparser.add_argument(
|
|
919
|
+
"--receipts",
|
|
920
|
+
type=project_path_argument,
|
|
921
|
+
default=Path("tmp/backups"),
|
|
922
|
+
help="backup receipt directory (default: tmp/backups)",
|
|
923
|
+
)
|
|
924
|
+
_add_operator_engine_arguments(subparser)
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
def _add_operator_arguments(parser: argparse.ArgumentParser) -> None:
|
|
928
|
+
_add_project_directory_argument(parser)
|
|
929
|
+
parser.add_argument(
|
|
930
|
+
"--schemas",
|
|
931
|
+
type=Path,
|
|
932
|
+
default=default_schema_directory(),
|
|
933
|
+
help="versioned schema directory (default: bundled schemas)",
|
|
934
|
+
)
|
|
935
|
+
parser.add_argument(
|
|
936
|
+
"--proposals",
|
|
937
|
+
type=project_path_argument,
|
|
938
|
+
default=Path("tmp/operator/proposals"),
|
|
939
|
+
help="proposal receipt directory (default: tmp/operator/proposals)",
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def _add_operator_feed_arguments(
|
|
944
|
+
parser: argparse.ArgumentParser, *, required: bool
|
|
945
|
+
) -> None:
|
|
946
|
+
parser.add_argument(
|
|
947
|
+
"--gateway-url",
|
|
948
|
+
default=None,
|
|
949
|
+
help=("alerts endpoint (default: derived from the declared logging gateway)"),
|
|
950
|
+
)
|
|
951
|
+
parser.add_argument("--gateway-ca", type=Path, required=required)
|
|
952
|
+
parser.add_argument("--gateway-cert", type=Path, required=required)
|
|
953
|
+
parser.add_argument("--gateway-key", type=Path, required=required)
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def _add_operator_engine_arguments(parser: argparse.ArgumentParser) -> None:
|
|
957
|
+
parser.add_argument(
|
|
958
|
+
"--engine",
|
|
959
|
+
type=Path,
|
|
960
|
+
default=default_engine_directory(),
|
|
961
|
+
help="engine directory containing ansible contracts (default: bundled engine)",
|
|
962
|
+
)
|
|
963
|
+
parser.add_argument(
|
|
964
|
+
"--inventory-file",
|
|
965
|
+
type=project_path_argument,
|
|
966
|
+
default=Path("tmp/ansible-inventory.json"),
|
|
967
|
+
help="rendered inventory path (default: tmp/ansible-inventory.json)",
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def _add_execute_argument(parser: argparse.ArgumentParser) -> None:
|
|
972
|
+
parser.add_argument(
|
|
973
|
+
"--yes",
|
|
974
|
+
action="store_true",
|
|
975
|
+
help=(
|
|
976
|
+
"change the servers; without this flag the command validates the "
|
|
977
|
+
"request and only shows what it would do"
|
|
978
|
+
),
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def _add_lifecycle_arguments(parser: argparse.ArgumentParser) -> None:
|
|
983
|
+
_add_project_directory_argument(parser)
|
|
984
|
+
parser.add_argument("component", type=resource_id_argument)
|
|
985
|
+
parser.add_argument(
|
|
986
|
+
"--schemas",
|
|
987
|
+
type=Path,
|
|
988
|
+
default=default_schema_directory(),
|
|
989
|
+
help="versioned schema directory (default: bundled schemas)",
|
|
990
|
+
)
|
|
991
|
+
parser.add_argument(
|
|
992
|
+
"--engine",
|
|
993
|
+
type=Path,
|
|
994
|
+
default=default_engine_directory(),
|
|
995
|
+
help="engine directory containing ansible contracts (default: bundled engine)",
|
|
996
|
+
)
|
|
997
|
+
parser.add_argument(
|
|
998
|
+
"--inventory-file",
|
|
999
|
+
type=project_path_argument,
|
|
1000
|
+
default=Path("tmp/ansible-inventory.json"),
|
|
1001
|
+
help="rendered inventory path (default: tmp/ansible-inventory.json)",
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
1006
|
+
"""Run the CLI and return a process exit code."""
|
|
1007
|
+
arguments = parse_arguments(_parser(), argv)
|
|
1008
|
+
if arguments.command == "init":
|
|
1009
|
+
return _run_init(arguments)
|
|
1010
|
+
try:
|
|
1011
|
+
with project_context(arguments.project, os.environ) as project_directory:
|
|
1012
|
+
arguments.project_directory = project_directory
|
|
1013
|
+
if arguments.command == "add":
|
|
1014
|
+
return _run_add(arguments, project_directory)
|
|
1015
|
+
if arguments.command == "import":
|
|
1016
|
+
return _run_import_render(arguments)
|
|
1017
|
+
schema_directory = Path(arguments.schemas)
|
|
1018
|
+
state = validate_config(project_directory, schema_directory)
|
|
1019
|
+
return _dispatch(arguments, state, schema_directory)
|
|
1020
|
+
except (ProjectError, ConfigValidationError) as error:
|
|
1021
|
+
sys.stderr.write(f"{json.dumps(error.as_dict(), sort_keys=True)}\n")
|
|
1022
|
+
return 2
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def _run_init(arguments: Namespace) -> int:
|
|
1026
|
+
directory = Path(arguments.directory)
|
|
1027
|
+
try:
|
|
1028
|
+
name = (
|
|
1029
|
+
ProjectName.from_boundary(arguments.name)
|
|
1030
|
+
if arguments.name is not None
|
|
1031
|
+
else ProjectName.from_directory(directory)
|
|
1032
|
+
)
|
|
1033
|
+
revision = (
|
|
1034
|
+
GitRevision.from_boundary(arguments.rev)
|
|
1035
|
+
if arguments.rev is not None
|
|
1036
|
+
else resolve_installed_revision()
|
|
1037
|
+
)
|
|
1038
|
+
options = InitOptions(
|
|
1039
|
+
directory=directory,
|
|
1040
|
+
name=name,
|
|
1041
|
+
revision=revision,
|
|
1042
|
+
source=GitSourceUrl.from_boundary(arguments.source),
|
|
1043
|
+
description=(
|
|
1044
|
+
ProjectDescription.from_boundary(arguments.description)
|
|
1045
|
+
if arguments.description is not None
|
|
1046
|
+
else None
|
|
1047
|
+
),
|
|
1048
|
+
)
|
|
1049
|
+
scaffold = init_project(options)
|
|
1050
|
+
except ValueError as error:
|
|
1051
|
+
payload = {
|
|
1052
|
+
"status": "error",
|
|
1053
|
+
"error": {"code": "invalid_argument", "message": str(error)},
|
|
1054
|
+
}
|
|
1055
|
+
sys.stderr.write(f"{json.dumps(payload, sort_keys=True)}\n")
|
|
1056
|
+
return 2
|
|
1057
|
+
except ProjectError as error:
|
|
1058
|
+
sys.stderr.write(f"{json.dumps(error.as_dict(), sort_keys=True)}\n")
|
|
1059
|
+
return 2
|
|
1060
|
+
_write_json(scaffold.as_dict())
|
|
1061
|
+
return 0
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
def _run_add(arguments: Namespace, project_directory: Path) -> int:
|
|
1065
|
+
schema_directory = Path(arguments.schemas)
|
|
1066
|
+
try:
|
|
1067
|
+
if arguments.add_command == "ssh-key":
|
|
1068
|
+
key_options = SshKeyOptions(
|
|
1069
|
+
key_path=Path(arguments.key_file).expanduser(),
|
|
1070
|
+
owner=ResourceId.from_boundary(arguments.owner),
|
|
1071
|
+
environment=ResourceId.from_boundary(arguments.environment),
|
|
1072
|
+
resource_id=(
|
|
1073
|
+
ResourceId.from_boundary(arguments.id)
|
|
1074
|
+
if arguments.id is not None
|
|
1075
|
+
else None
|
|
1076
|
+
),
|
|
1077
|
+
description=arguments.description,
|
|
1078
|
+
)
|
|
1079
|
+
result = add_ssh_key(project_directory, key_options, schema_directory)
|
|
1080
|
+
elif arguments.add_command == "server-type":
|
|
1081
|
+
type_options = ServerTypeOptions(
|
|
1082
|
+
resource_id=ResourceId.from_boundary(arguments.id),
|
|
1083
|
+
description=arguments.description,
|
|
1084
|
+
)
|
|
1085
|
+
result = add_server_type(project_directory, type_options, schema_directory)
|
|
1086
|
+
else:
|
|
1087
|
+
server_options = ServerOptions(
|
|
1088
|
+
resource_id=ResourceId.from_boundary(arguments.id),
|
|
1089
|
+
address=ConnectionAddress.from_boundary(arguments.address),
|
|
1090
|
+
server_type=ResourceId.from_boundary(arguments.type),
|
|
1091
|
+
environment=ResourceId.from_boundary(arguments.environment),
|
|
1092
|
+
ssh_user=LinuxUser.from_boundary(arguments.ssh_user),
|
|
1093
|
+
ssh_port=TcpPort.from_boundary(arguments.ssh_port),
|
|
1094
|
+
hostname=(
|
|
1095
|
+
Hostname.from_boundary(arguments.hostname)
|
|
1096
|
+
if arguments.hostname is not None
|
|
1097
|
+
else None
|
|
1098
|
+
),
|
|
1099
|
+
description=arguments.description,
|
|
1100
|
+
)
|
|
1101
|
+
result = add_server(project_directory, server_options, schema_directory)
|
|
1102
|
+
except (TypeError, ValueError) as error:
|
|
1103
|
+
payload = {
|
|
1104
|
+
"status": "error",
|
|
1105
|
+
"error": {"code": "invalid_argument", "message": str(error)},
|
|
1106
|
+
}
|
|
1107
|
+
sys.stderr.write(f"{json.dumps(payload, sort_keys=True)}\n")
|
|
1108
|
+
return 2
|
|
1109
|
+
except AuthoringError as error:
|
|
1110
|
+
sys.stderr.write(f"{json.dumps(error.as_dict(), sort_keys=True)}\n")
|
|
1111
|
+
return 2
|
|
1112
|
+
_write_json(result.as_dict())
|
|
1113
|
+
return 0
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
def _run_import_render(arguments: Namespace) -> int:
|
|
1117
|
+
try:
|
|
1118
|
+
targets = ImportTargets(
|
|
1119
|
+
application_id=arguments.application,
|
|
1120
|
+
server_id=arguments.server,
|
|
1121
|
+
project_directory=Path(arguments.output),
|
|
1122
|
+
environment_directory=Path(arguments.env_dir),
|
|
1123
|
+
)
|
|
1124
|
+
if arguments.import_command == "render-api":
|
|
1125
|
+
api_key = read_api_key(Path(arguments.api_key_file))
|
|
1126
|
+
client = HttpRenderApiClient(api_key=api_key, base_url=arguments.api_url)
|
|
1127
|
+
result = import_render_api(client, targets, Path(arguments.schemas))
|
|
1128
|
+
else:
|
|
1129
|
+
result = import_render_blueprint(
|
|
1130
|
+
Path(arguments.blueprint), targets, Path(arguments.schemas)
|
|
1131
|
+
)
|
|
1132
|
+
except (RenderImportError, ConfigValidationError) as error:
|
|
1133
|
+
sys.stderr.write(f"{json.dumps(error.as_dict(), sort_keys=True)}\n")
|
|
1134
|
+
return 2
|
|
1135
|
+
_write_json(result.as_dict())
|
|
1136
|
+
return 0
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
def _dispatch(
|
|
1140
|
+
arguments: Namespace, state: ValidatedConfig, schema_directory: Path
|
|
1141
|
+
) -> int:
|
|
1142
|
+
handlers: dict[str, Callable[[Namespace, ValidatedConfig, Path], int]] = {
|
|
1143
|
+
"audit": _run_audit,
|
|
1144
|
+
"backup:run": _run_backup_run,
|
|
1145
|
+
"backup:verify": _run_backup_verify,
|
|
1146
|
+
"secrets:render": _run_secrets_render,
|
|
1147
|
+
"data:migrate": _run_data_migrate,
|
|
1148
|
+
"dashboard:build": _run_dashboard_build,
|
|
1149
|
+
"dashboard:serve": _run_dashboard_serve,
|
|
1150
|
+
"deploy": _run_deploy,
|
|
1151
|
+
"health": _run_health,
|
|
1152
|
+
"inventory:show": _run_inventory_show,
|
|
1153
|
+
"migrate": _run_migrate,
|
|
1154
|
+
"operator:approve": _run_operator_approve,
|
|
1155
|
+
"operator:list": _run_operator_list,
|
|
1156
|
+
"operator:run": _run_operator_run,
|
|
1157
|
+
"operator:show": _run_operator_show,
|
|
1158
|
+
"restart": _run_restart,
|
|
1159
|
+
"rollback": _run_rollback,
|
|
1160
|
+
"services:inspect": _run_services_inspect,
|
|
1161
|
+
"services:status": _run_services_status,
|
|
1162
|
+
"config:validate": _run_config_validate,
|
|
1163
|
+
}
|
|
1164
|
+
key = _command_key(arguments)
|
|
1165
|
+
try:
|
|
1166
|
+
handler = handlers[key]
|
|
1167
|
+
except KeyError as error:
|
|
1168
|
+
message = f"argparse accepted an unsupported command: {key}"
|
|
1169
|
+
raise RuntimeError(message) from error
|
|
1170
|
+
return handler(arguments, state, schema_directory)
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
def _command_key(arguments: Namespace) -> str:
|
|
1174
|
+
top_level = {"audit", "deploy", "health", "migrate", "restart", "rollback"}
|
|
1175
|
+
if arguments.command in top_level:
|
|
1176
|
+
return str(arguments.command)
|
|
1177
|
+
subcommand = getattr(arguments, f"{arguments.command}_command", None)
|
|
1178
|
+
return f"{arguments.command}:{subcommand}"
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
def _run_config_validate(
|
|
1182
|
+
_arguments: Namespace, state: ValidatedConfig, _schema_directory: Path
|
|
1183
|
+
) -> int:
|
|
1184
|
+
_write_json(state.as_dict())
|
|
1185
|
+
return 0
|
|
1186
|
+
|
|
1187
|
+
|
|
1188
|
+
def _run_inventory_show(
|
|
1189
|
+
_arguments: Namespace, state: ValidatedConfig, _schema_directory: Path
|
|
1190
|
+
) -> int:
|
|
1191
|
+
_write_json(
|
|
1192
|
+
{
|
|
1193
|
+
"status": "ok",
|
|
1194
|
+
"inventory": PlatformInventory.from_state(state).as_dict(),
|
|
1195
|
+
}
|
|
1196
|
+
)
|
|
1197
|
+
return 0
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
def _run_audit(
|
|
1201
|
+
arguments: Namespace, state: ValidatedConfig, schema_directory: Path
|
|
1202
|
+
) -> int:
|
|
1203
|
+
observations = load_observations(Path(arguments.observed), schema_directory)
|
|
1204
|
+
report = audit_inventory(
|
|
1205
|
+
PlatformInventory.from_state(state),
|
|
1206
|
+
observations,
|
|
1207
|
+
load_environment_receipts(Path(arguments.env_receipts), schema_directory),
|
|
1208
|
+
)
|
|
1209
|
+
_write_json(report.as_dict())
|
|
1210
|
+
if report.status is AuditStatus.COMPLIANT:
|
|
1211
|
+
return 0
|
|
1212
|
+
if report.status is AuditStatus.DRIFT:
|
|
1213
|
+
return 1
|
|
1214
|
+
return 3
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
def _run_secrets_render(
|
|
1218
|
+
arguments: Namespace, _state: ValidatedConfig, schema_directory: Path
|
|
1219
|
+
) -> int:
|
|
1220
|
+
output = (
|
|
1221
|
+
Path(arguments.output)
|
|
1222
|
+
if arguments.output is not None
|
|
1223
|
+
else Path("tmp/env") / f"{arguments.component}.env"
|
|
1224
|
+
)
|
|
1225
|
+
try:
|
|
1226
|
+
result = render_environment(
|
|
1227
|
+
_engine_context(arguments, schema_directory),
|
|
1228
|
+
arguments.component,
|
|
1229
|
+
SopsSecretProvider(secrets_directory=Path(arguments.secrets_dir)),
|
|
1230
|
+
output,
|
|
1231
|
+
receipt_directory=Path(arguments.receipts),
|
|
1232
|
+
)
|
|
1233
|
+
except SecretsError as error:
|
|
1234
|
+
sys.stderr.write(f"{json.dumps(error.as_dict(), sort_keys=True)}\n")
|
|
1235
|
+
return 2
|
|
1236
|
+
_write_json(result)
|
|
1237
|
+
return 0
|
|
1238
|
+
|
|
1239
|
+
|
|
1240
|
+
def _run_backup_run(
|
|
1241
|
+
arguments: Namespace, _state: ValidatedConfig, schema_directory: Path
|
|
1242
|
+
) -> int:
|
|
1243
|
+
return _run_backup_operation(arguments, schema_directory, backup_service)
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
def _run_backup_verify(
|
|
1247
|
+
arguments: Namespace, _state: ValidatedConfig, schema_directory: Path
|
|
1248
|
+
) -> int:
|
|
1249
|
+
return _run_backup_operation(arguments, schema_directory, verify_backup)
|
|
1250
|
+
|
|
1251
|
+
|
|
1252
|
+
def _run_backup_operation(
|
|
1253
|
+
arguments: Namespace,
|
|
1254
|
+
schema_directory: Path,
|
|
1255
|
+
operation: Callable[[EngineContext, ResourceId, Path], dict[str, object]],
|
|
1256
|
+
) -> int:
|
|
1257
|
+
try:
|
|
1258
|
+
result = operation(
|
|
1259
|
+
_engine_context(arguments, schema_directory),
|
|
1260
|
+
arguments.service,
|
|
1261
|
+
Path(arguments.receipts),
|
|
1262
|
+
)
|
|
1263
|
+
except LifecycleError as error:
|
|
1264
|
+
return _lifecycle_exit(error)
|
|
1265
|
+
_write_json(result)
|
|
1266
|
+
return 0
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
def _operator_store(arguments: Namespace, schema_directory: Path) -> ProposalStore:
|
|
1270
|
+
return ProposalStore(
|
|
1271
|
+
directory=Path(arguments.proposals),
|
|
1272
|
+
catalog=SchemaCatalog(schema_directory),
|
|
1273
|
+
)
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
def _operator_feed(arguments: Namespace, inventory: PlatformInventory) -> AlertFeed:
|
|
1277
|
+
return gateway_feed(
|
|
1278
|
+
inventory,
|
|
1279
|
+
ca_path=Path(arguments.gateway_ca),
|
|
1280
|
+
certificate_path=Path(arguments.gateway_cert),
|
|
1281
|
+
key_path=Path(arguments.gateway_key),
|
|
1282
|
+
url_override=arguments.gateway_url,
|
|
1283
|
+
)
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
def _require_gateway_material(arguments: Namespace) -> None:
|
|
1287
|
+
if (
|
|
1288
|
+
arguments.gateway_ca is None
|
|
1289
|
+
or arguments.gateway_cert is None
|
|
1290
|
+
or arguments.gateway_key is None
|
|
1291
|
+
):
|
|
1292
|
+
code = "operator_gateway_material_missing"
|
|
1293
|
+
message = (
|
|
1294
|
+
"approving an alert-triggered proposal requires --gateway-ca, "
|
|
1295
|
+
"--gateway-cert, and --gateway-key"
|
|
1296
|
+
)
|
|
1297
|
+
raise OperatorError(code, message)
|
|
1298
|
+
|
|
1299
|
+
|
|
1300
|
+
def _operator_exit(error: OperatorError) -> int:
|
|
1301
|
+
sys.stderr.write(f"{json.dumps(error.as_dict(), sort_keys=True)}\n")
|
|
1302
|
+
return 2
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
def _run_operator_run(
|
|
1306
|
+
arguments: Namespace, state: ValidatedConfig, schema_directory: Path
|
|
1307
|
+
) -> int:
|
|
1308
|
+
inventory = PlatformInventory.from_state(state)
|
|
1309
|
+
try:
|
|
1310
|
+
store = _operator_store(arguments, schema_directory)
|
|
1311
|
+
feed = _operator_feed(arguments, inventory)
|
|
1312
|
+
auditor = None
|
|
1313
|
+
if arguments.drift_interval is not None:
|
|
1314
|
+
auditor = engine_auditor(
|
|
1315
|
+
_engine_context(arguments, schema_directory),
|
|
1316
|
+
inventory,
|
|
1317
|
+
Path(arguments.observed),
|
|
1318
|
+
)
|
|
1319
|
+
context = _engine_context(arguments, schema_directory)
|
|
1320
|
+
|
|
1321
|
+
def _verifier_for(
|
|
1322
|
+
proposal: OperatorProposal,
|
|
1323
|
+
) -> Callable[[OperatorProposal], bool]:
|
|
1324
|
+
if proposal.trigger_kind is TriggerKind.ALERT:
|
|
1325
|
+
return alert_resolution_verifier(feed)
|
|
1326
|
+
return drift_resolution_verifier(
|
|
1327
|
+
engine_auditor(context, inventory, Path(arguments.observed))
|
|
1328
|
+
)
|
|
1329
|
+
|
|
1330
|
+
drift_due = 0.0
|
|
1331
|
+
while True:
|
|
1332
|
+
report = operator_run_once(feed, inventory, store)
|
|
1333
|
+
_write_json({"status": "ok", "pass": "alerts", **report.as_dict()})
|
|
1334
|
+
if auditor is not None and time.monotonic() >= drift_due:
|
|
1335
|
+
drift_report = drift_pass(auditor, store)
|
|
1336
|
+
_write_json(
|
|
1337
|
+
{
|
|
1338
|
+
"status": "ok",
|
|
1339
|
+
"pass": "drift",
|
|
1340
|
+
**drift_report.as_dict(),
|
|
1341
|
+
}
|
|
1342
|
+
)
|
|
1343
|
+
drift_due = time.monotonic() + arguments.drift_interval
|
|
1344
|
+
if inventory.operator_policies:
|
|
1345
|
+
autonomy_report = autonomous_pass(
|
|
1346
|
+
store,
|
|
1347
|
+
inventory,
|
|
1348
|
+
engine_executor(context),
|
|
1349
|
+
_verifier_for,
|
|
1350
|
+
)
|
|
1351
|
+
_write_json(
|
|
1352
|
+
{
|
|
1353
|
+
"status": "ok",
|
|
1354
|
+
"pass": "autonomy",
|
|
1355
|
+
**autonomy_report.as_dict(),
|
|
1356
|
+
}
|
|
1357
|
+
)
|
|
1358
|
+
if arguments.interval is None:
|
|
1359
|
+
return 0
|
|
1360
|
+
time.sleep(arguments.interval)
|
|
1361
|
+
except OperatorError as error:
|
|
1362
|
+
return _operator_exit(error)
|
|
1363
|
+
|
|
1364
|
+
|
|
1365
|
+
def _run_operator_list(
|
|
1366
|
+
arguments: Namespace, _state: ValidatedConfig, schema_directory: Path
|
|
1367
|
+
) -> int:
|
|
1368
|
+
try:
|
|
1369
|
+
store = _operator_store(arguments, schema_directory)
|
|
1370
|
+
proposals = [proposal.as_document() for proposal in store.list()]
|
|
1371
|
+
except OperatorError as error:
|
|
1372
|
+
return _operator_exit(error)
|
|
1373
|
+
_write_json({"status": "ok", "proposals": proposals})
|
|
1374
|
+
return 0
|
|
1375
|
+
|
|
1376
|
+
|
|
1377
|
+
def _run_operator_show(
|
|
1378
|
+
arguments: Namespace, _state: ValidatedConfig, schema_directory: Path
|
|
1379
|
+
) -> int:
|
|
1380
|
+
try:
|
|
1381
|
+
store = _operator_store(arguments, schema_directory)
|
|
1382
|
+
proposal = store.load(arguments.proposal)
|
|
1383
|
+
except OperatorError as error:
|
|
1384
|
+
return _operator_exit(error)
|
|
1385
|
+
_write_json(proposal.as_document())
|
|
1386
|
+
return 0
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
def _run_operator_approve(
|
|
1390
|
+
arguments: Namespace, state: ValidatedConfig, schema_directory: Path
|
|
1391
|
+
) -> int:
|
|
1392
|
+
inventory = PlatformInventory.from_state(state)
|
|
1393
|
+
try:
|
|
1394
|
+
store = _operator_store(arguments, schema_directory)
|
|
1395
|
+
proposal_id = arguments.proposal
|
|
1396
|
+
pending = store.load(proposal_id)
|
|
1397
|
+
context = _engine_context(arguments, schema_directory)
|
|
1398
|
+
if pending.trigger_kind is TriggerKind.ALERT:
|
|
1399
|
+
_require_gateway_material(arguments)
|
|
1400
|
+
verifier = alert_resolution_verifier(_operator_feed(arguments, inventory))
|
|
1401
|
+
else:
|
|
1402
|
+
verifier = drift_resolution_verifier(
|
|
1403
|
+
engine_auditor(context, inventory, Path(arguments.observed))
|
|
1404
|
+
)
|
|
1405
|
+
proposal = operator_approve(
|
|
1406
|
+
store,
|
|
1407
|
+
proposal_id,
|
|
1408
|
+
engine_executor(context),
|
|
1409
|
+
verifier,
|
|
1410
|
+
ApproveOptions(verify_timeout_seconds=arguments.verify_timeout),
|
|
1411
|
+
)
|
|
1412
|
+
except OperatorError as error:
|
|
1413
|
+
return _operator_exit(error)
|
|
1414
|
+
except LifecycleError as error:
|
|
1415
|
+
return _lifecycle_exit(error)
|
|
1416
|
+
_write_json(proposal.as_document())
|
|
1417
|
+
return 0 if proposal.status is ProposalStatus.VERIFIED else 1
|
|
1418
|
+
|
|
1419
|
+
|
|
1420
|
+
def _run_services_inspect(
|
|
1421
|
+
arguments: Namespace, state: ValidatedConfig, _schema_directory: Path
|
|
1422
|
+
) -> int:
|
|
1423
|
+
paths = inspect_domains(
|
|
1424
|
+
PlatformInventory.from_state(state),
|
|
1425
|
+
Path(arguments.output),
|
|
1426
|
+
SocketDomainNetworkClient(),
|
|
1427
|
+
observed_at=EvidenceTimestamp.now(),
|
|
1428
|
+
)
|
|
1429
|
+
_write_json(
|
|
1430
|
+
{
|
|
1431
|
+
"status": "ok",
|
|
1432
|
+
"observations": [str(path) for path in paths],
|
|
1433
|
+
}
|
|
1434
|
+
)
|
|
1435
|
+
return 0
|
|
1436
|
+
|
|
1437
|
+
|
|
1438
|
+
def _run_services_status(
|
|
1439
|
+
arguments: Namespace, state: ValidatedConfig, schema_directory: Path
|
|
1440
|
+
) -> int:
|
|
1441
|
+
operations = _service_operations(arguments, state, schema_directory)
|
|
1442
|
+
_write_json(
|
|
1443
|
+
{
|
|
1444
|
+
"status": "ok",
|
|
1445
|
+
"services": [domain.as_dict() for domain in operations.domains],
|
|
1446
|
+
}
|
|
1447
|
+
)
|
|
1448
|
+
return 0
|
|
1449
|
+
|
|
1450
|
+
|
|
1451
|
+
def _run_dashboard_build(
|
|
1452
|
+
arguments: Namespace, state: ValidatedConfig, schema_directory: Path
|
|
1453
|
+
) -> int:
|
|
1454
|
+
operations = _service_operations(arguments, state, schema_directory)
|
|
1455
|
+
artifacts = build_dashboard(operations, Path(arguments.output))
|
|
1456
|
+
_write_json(
|
|
1457
|
+
{
|
|
1458
|
+
"status": "ok",
|
|
1459
|
+
"health": operations.health.value,
|
|
1460
|
+
"tasks": len(operations.tasks),
|
|
1461
|
+
"dashboard": artifacts.as_dict(),
|
|
1462
|
+
}
|
|
1463
|
+
)
|
|
1464
|
+
return 0
|
|
1465
|
+
|
|
1466
|
+
|
|
1467
|
+
def _run_dashboard_serve(
|
|
1468
|
+
arguments: Namespace, _state: ValidatedConfig, schema_directory: Path
|
|
1469
|
+
) -> int:
|
|
1470
|
+
sources = EvidenceSources(
|
|
1471
|
+
project_directory=Path(arguments.project_directory),
|
|
1472
|
+
schema_directory=schema_directory,
|
|
1473
|
+
observed_directory=Path(arguments.observed),
|
|
1474
|
+
service_observed_directory=Path(arguments.service_observed),
|
|
1475
|
+
deployments_directory=Path(arguments.deployments),
|
|
1476
|
+
inspect_services=bool(arguments.inspect_services),
|
|
1477
|
+
)
|
|
1478
|
+
endpoint = ListenEndpoint(host=str(arguments.host), port=int(arguments.port))
|
|
1479
|
+
refresh = RefreshInterval.from_boundary(int(arguments.refresh))
|
|
1480
|
+
server = create_dashboard_server(sources, endpoint, refresh)
|
|
1481
|
+
bound_port = int(server.server_address[1])
|
|
1482
|
+
_write_json(
|
|
1483
|
+
{
|
|
1484
|
+
"status": "ok",
|
|
1485
|
+
"dashboard": {
|
|
1486
|
+
"url": f"http://{endpoint.host}:{bound_port}/",
|
|
1487
|
+
"refreshSeconds": refresh.seconds,
|
|
1488
|
+
"inspectServices": sources.inspect_services,
|
|
1489
|
+
},
|
|
1490
|
+
}
|
|
1491
|
+
)
|
|
1492
|
+
try:
|
|
1493
|
+
server.serve_forever()
|
|
1494
|
+
except KeyboardInterrupt:
|
|
1495
|
+
return 0
|
|
1496
|
+
finally:
|
|
1497
|
+
server.server_close()
|
|
1498
|
+
return 0
|
|
1499
|
+
|
|
1500
|
+
|
|
1501
|
+
def _service_operations(
|
|
1502
|
+
arguments: Namespace, state: ValidatedConfig, schema_directory: Path
|
|
1503
|
+
) -> FleetOperations:
|
|
1504
|
+
observations = load_observations(Path(arguments.observed), schema_directory)
|
|
1505
|
+
deployment_receipts = _optional_deployment_receipts(
|
|
1506
|
+
Path(arguments.deployments), schema_directory
|
|
1507
|
+
)
|
|
1508
|
+
domain_observations = _optional_domain_observations(
|
|
1509
|
+
Path(arguments.service_observed), schema_directory
|
|
1510
|
+
)
|
|
1511
|
+
return build_operations_view(
|
|
1512
|
+
PlatformInventory.from_state(state),
|
|
1513
|
+
observations,
|
|
1514
|
+
deployment_receipts,
|
|
1515
|
+
domain_observations,
|
|
1516
|
+
generated_at=UtcTimestamp.now(),
|
|
1517
|
+
)
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
def _engine_context(arguments: Namespace, schema_directory: Path) -> EngineContext:
|
|
1521
|
+
return EngineContext(
|
|
1522
|
+
project_directory=Path(arguments.project_directory),
|
|
1523
|
+
schema_directory=schema_directory,
|
|
1524
|
+
engine_directory=Path(arguments.engine),
|
|
1525
|
+
inventory_file=Path(arguments.inventory_file),
|
|
1526
|
+
)
|
|
1527
|
+
|
|
1528
|
+
|
|
1529
|
+
def _lifecycle_exit(error: LifecycleError) -> int:
|
|
1530
|
+
sys.stderr.write(f"{json.dumps(error.as_dict(), sort_keys=True)}\n")
|
|
1531
|
+
return 1 if error.code == "lifecycle_execution_failed" else 2
|
|
1532
|
+
|
|
1533
|
+
|
|
1534
|
+
def _run_data_migrate(
|
|
1535
|
+
arguments: Namespace, _state: ValidatedConfig, schema_directory: Path
|
|
1536
|
+
) -> int:
|
|
1537
|
+
context = _engine_context(arguments, schema_directory)
|
|
1538
|
+
try:
|
|
1539
|
+
if not arguments.yes:
|
|
1540
|
+
return _write_plan(
|
|
1541
|
+
preview_data_migration(
|
|
1542
|
+
context,
|
|
1543
|
+
arguments.service,
|
|
1544
|
+
str(arguments.database),
|
|
1545
|
+
Path(arguments.source_url_file),
|
|
1546
|
+
)
|
|
1547
|
+
)
|
|
1548
|
+
result = migrate_data(
|
|
1549
|
+
context,
|
|
1550
|
+
arguments.service,
|
|
1551
|
+
str(arguments.database),
|
|
1552
|
+
Path(arguments.source_url_file),
|
|
1553
|
+
Path(arguments.receipts),
|
|
1554
|
+
)
|
|
1555
|
+
except LifecycleError as error:
|
|
1556
|
+
return _lifecycle_exit(error)
|
|
1557
|
+
_write_json(result)
|
|
1558
|
+
return 0
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
def _run_deploy(
|
|
1562
|
+
arguments: Namespace, _state: ValidatedConfig, schema_directory: Path
|
|
1563
|
+
) -> int:
|
|
1564
|
+
context = _engine_context(arguments, schema_directory)
|
|
1565
|
+
try:
|
|
1566
|
+
if not arguments.yes:
|
|
1567
|
+
return _write_plan(
|
|
1568
|
+
preview_deploy(
|
|
1569
|
+
context,
|
|
1570
|
+
arguments.component,
|
|
1571
|
+
arguments.release,
|
|
1572
|
+
Path(arguments.artifacts),
|
|
1573
|
+
)
|
|
1574
|
+
)
|
|
1575
|
+
result = deploy(
|
|
1576
|
+
context,
|
|
1577
|
+
arguments.component,
|
|
1578
|
+
arguments.release,
|
|
1579
|
+
Path(arguments.artifacts),
|
|
1580
|
+
DeployOptions(
|
|
1581
|
+
environment_file=(
|
|
1582
|
+
Path(arguments.env_file) if arguments.env_file is not None else None
|
|
1583
|
+
),
|
|
1584
|
+
receipt_directory=Path(arguments.receipts),
|
|
1585
|
+
),
|
|
1586
|
+
)
|
|
1587
|
+
except LifecycleError as error:
|
|
1588
|
+
return _lifecycle_exit(error)
|
|
1589
|
+
_write_json(result.as_dict())
|
|
1590
|
+
return 0
|
|
1591
|
+
|
|
1592
|
+
|
|
1593
|
+
def _run_rollback(
|
|
1594
|
+
arguments: Namespace, _state: ValidatedConfig, schema_directory: Path
|
|
1595
|
+
) -> int:
|
|
1596
|
+
context = _engine_context(arguments, schema_directory)
|
|
1597
|
+
try:
|
|
1598
|
+
if not arguments.yes:
|
|
1599
|
+
return _write_plan(
|
|
1600
|
+
preview_rollback(context, arguments.component, arguments.release)
|
|
1601
|
+
)
|
|
1602
|
+
result = rollback(
|
|
1603
|
+
context,
|
|
1604
|
+
arguments.component,
|
|
1605
|
+
arguments.release,
|
|
1606
|
+
)
|
|
1607
|
+
except LifecycleError as error:
|
|
1608
|
+
return _lifecycle_exit(error)
|
|
1609
|
+
_write_json(result.as_dict())
|
|
1610
|
+
return 0
|
|
1611
|
+
|
|
1612
|
+
|
|
1613
|
+
def _run_restart(
|
|
1614
|
+
arguments: Namespace, _state: ValidatedConfig, schema_directory: Path
|
|
1615
|
+
) -> int:
|
|
1616
|
+
context = _engine_context(arguments, schema_directory)
|
|
1617
|
+
try:
|
|
1618
|
+
if not arguments.yes:
|
|
1619
|
+
return _write_plan(preview_restart(context, arguments.component))
|
|
1620
|
+
result = restart(context, arguments.component)
|
|
1621
|
+
except LifecycleError as error:
|
|
1622
|
+
return _lifecycle_exit(error)
|
|
1623
|
+
_write_json(result.as_dict())
|
|
1624
|
+
return 0
|
|
1625
|
+
|
|
1626
|
+
|
|
1627
|
+
def _run_health(
|
|
1628
|
+
arguments: Namespace, _state: ValidatedConfig, schema_directory: Path
|
|
1629
|
+
) -> int:
|
|
1630
|
+
try:
|
|
1631
|
+
result = health(
|
|
1632
|
+
_engine_context(arguments, schema_directory),
|
|
1633
|
+
arguments.component,
|
|
1634
|
+
)
|
|
1635
|
+
except LifecycleError as error:
|
|
1636
|
+
return _lifecycle_exit(error)
|
|
1637
|
+
_write_json(result.as_dict())
|
|
1638
|
+
return 0 if result.healthy else 1
|
|
1639
|
+
|
|
1640
|
+
|
|
1641
|
+
def _run_migrate(
|
|
1642
|
+
arguments: Namespace, _state: ValidatedConfig, schema_directory: Path
|
|
1643
|
+
) -> int:
|
|
1644
|
+
try:
|
|
1645
|
+
builds = _key_value_pairs(arguments.build, "--build")
|
|
1646
|
+
releases = _key_value_pairs(arguments.release, "--release")
|
|
1647
|
+
environment_files = {
|
|
1648
|
+
component: Path(value)
|
|
1649
|
+
for component, value in _key_value_pairs(
|
|
1650
|
+
arguments.env_file, "--env-file"
|
|
1651
|
+
).items()
|
|
1652
|
+
}
|
|
1653
|
+
data_migrations = {
|
|
1654
|
+
database: Path(value)
|
|
1655
|
+
for database, value in _key_value_pairs(arguments.data, "--data").items()
|
|
1656
|
+
}
|
|
1657
|
+
except ValueError as error:
|
|
1658
|
+
payload = {
|
|
1659
|
+
"status": "error",
|
|
1660
|
+
"error": {"code": "invalid_argument", "message": str(error)},
|
|
1661
|
+
}
|
|
1662
|
+
sys.stderr.write(f"{json.dumps(payload, sort_keys=True)}\n")
|
|
1663
|
+
return 2
|
|
1664
|
+
config = AgentConfig(
|
|
1665
|
+
project_directory=Path(arguments.project_directory),
|
|
1666
|
+
schema_directory=schema_directory,
|
|
1667
|
+
engine_directory=Path(arguments.engine),
|
|
1668
|
+
inventory_file=Path(arguments.inventory_file),
|
|
1669
|
+
observed_directory=Path(arguments.observed),
|
|
1670
|
+
service_observed_directory=Path(arguments.service_observed),
|
|
1671
|
+
deployments_directory=Path(arguments.deployments),
|
|
1672
|
+
releases_directory=Path(arguments.receipts),
|
|
1673
|
+
artifacts_directory=Path(arguments.artifacts),
|
|
1674
|
+
)
|
|
1675
|
+
options = MigrateOptions(
|
|
1676
|
+
plan_file=Path(arguments.plan_file),
|
|
1677
|
+
builds=builds,
|
|
1678
|
+
releases=releases,
|
|
1679
|
+
environment_files=environment_files,
|
|
1680
|
+
data_migrations=data_migrations,
|
|
1681
|
+
execute=arguments.yes,
|
|
1682
|
+
restart=arguments.restart,
|
|
1683
|
+
)
|
|
1684
|
+
try:
|
|
1685
|
+
result = execute_migration(config, options)
|
|
1686
|
+
except MigrateError as error:
|
|
1687
|
+
sys.stderr.write(f"{json.dumps(error.as_dict(), sort_keys=True)}\n")
|
|
1688
|
+
return 2
|
|
1689
|
+
_write_json(result)
|
|
1690
|
+
status = str(result["status"])
|
|
1691
|
+
if status in {"ok", "plan"}:
|
|
1692
|
+
return 0
|
|
1693
|
+
if status == "paused":
|
|
1694
|
+
return 3
|
|
1695
|
+
return 1
|
|
1696
|
+
|
|
1697
|
+
|
|
1698
|
+
def _key_value_pairs(entries: list[str], option: str) -> dict[str, str]:
|
|
1699
|
+
pairs: dict[str, str] = {}
|
|
1700
|
+
for entry in entries:
|
|
1701
|
+
component, separator, value = entry.partition("=")
|
|
1702
|
+
if not separator or not component or not value:
|
|
1703
|
+
message = f"{option} expects COMPONENT=VALUE, got {entry!r}"
|
|
1704
|
+
raise ValueError(message)
|
|
1705
|
+
pairs[component] = value
|
|
1706
|
+
return pairs
|
|
1707
|
+
|
|
1708
|
+
|
|
1709
|
+
def _write_plan(preview: LifecyclePreview) -> int:
|
|
1710
|
+
_write_json(
|
|
1711
|
+
{
|
|
1712
|
+
**preview.as_dict(),
|
|
1713
|
+
"instruction": "review the plan and re-run with --yes to execute it",
|
|
1714
|
+
}
|
|
1715
|
+
)
|
|
1716
|
+
return 0
|
|
1717
|
+
|
|
1718
|
+
|
|
1719
|
+
def _write_json(payload: object) -> None:
|
|
1720
|
+
sys.stdout.write(f"{json.dumps(payload, sort_keys=True)}\n")
|
|
1721
|
+
# A pipe makes stdout block-buffered; flush so long-running commands such
|
|
1722
|
+
# as `operator run --interval` deliver each document when it is written.
|
|
1723
|
+
sys.stdout.flush()
|
|
1724
|
+
|
|
1725
|
+
|
|
1726
|
+
def run() -> None:
|
|
1727
|
+
"""Installed console-script entry point."""
|
|
1728
|
+
raise SystemExit(main())
|
|
1729
|
+
|
|
1730
|
+
|
|
1731
|
+
def _optional_deployment_receipts(
|
|
1732
|
+
directory: Path, schema_directory: Path
|
|
1733
|
+
) -> DeploymentReceiptSet:
|
|
1734
|
+
return (
|
|
1735
|
+
load_deployment_receipts(directory, schema_directory)
|
|
1736
|
+
if directory.is_dir()
|
|
1737
|
+
else DeploymentReceiptSet.empty()
|
|
1738
|
+
)
|
|
1739
|
+
|
|
1740
|
+
|
|
1741
|
+
def _optional_domain_observations(
|
|
1742
|
+
directory: Path, schema_directory: Path
|
|
1743
|
+
) -> DomainObservationSet:
|
|
1744
|
+
return (
|
|
1745
|
+
load_domain_observations(directory, schema_directory)
|
|
1746
|
+
if directory.is_dir()
|
|
1747
|
+
else DomainObservationSet.empty()
|
|
1748
|
+
)
|