qontract-reconcile 0.10.1rc1127__py3-none-any.whl → 0.10.1rc1129__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.
- {qontract_reconcile-0.10.1rc1127.dist-info → qontract_reconcile-0.10.1rc1129.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc1127.dist-info → qontract_reconcile-0.10.1rc1129.dist-info}/RECORD +12 -10
- reconcile/aus/ocm_addons_upgrade_scheduler_org.py +3 -1
- reconcile/external_resources/integration.py +3 -3
- reconcile/external_resources/integration_secrets_sync.py +1 -1
- reconcile/typed_queries/external_resources.py +4 -4
- tools/cli_commands/erv2.py +473 -0
- tools/qontract_cli.py +175 -87
- tools/test/test_erv2.py +80 -0
- {qontract_reconcile-0.10.1rc1127.dist-info → qontract_reconcile-0.10.1rc1129.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc1127.dist-info → qontract_reconcile-0.10.1rc1129.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc1127.dist-info → qontract_reconcile-0.10.1rc1129.dist-info}/top_level.txt +0 -0
{qontract_reconcile-0.10.1rc1127.dist-info → qontract_reconcile-0.10.1rc1129.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: qontract-reconcile
|
3
|
-
Version: 0.10.
|
3
|
+
Version: 0.10.1rc1129
|
4
4
|
Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
|
5
5
|
Home-page: https://github.com/app-sre/qontract-reconcile
|
6
6
|
Author: Red Hat App-SRE Team
|
{qontract_reconcile-0.10.1rc1127.dist-info → qontract_reconcile-0.10.1rc1129.dist-info}/RECORD
RENAMED
@@ -128,7 +128,7 @@ reconcile/aus/healthchecks.py,sha256=jR9c-syh9impnkV0fd6XW3Bnk7iRN5zv8oCRYM-yIRY
|
|
128
128
|
reconcile/aus/metrics.py,sha256=nKT4m2zGT-QOMR0c-z-npVNKWsNMubzdffpU_f9n4II,3927
|
129
129
|
reconcile/aus/models.py,sha256=MSKX7SY0GDw7BgpIuKeLT3i_v8E1LFz4M0DQAL7JWUM,7783
|
130
130
|
reconcile/aus/node_pool_spec.py,sha256=FkMggklG-4BgQwud2Swp2m3AAAKzZmeaXgohl9uwxZ8,1138
|
131
|
-
reconcile/aus/ocm_addons_upgrade_scheduler_org.py,sha256
|
131
|
+
reconcile/aus/ocm_addons_upgrade_scheduler_org.py,sha256=-xliq44ev35P6YzwrGLppReRUWrKDTNptNjaivwICIc,10263
|
132
132
|
reconcile/aus/ocm_upgrade_scheduler.py,sha256=2uPn13y3QGCHLoKwCc1Z7q9wQsoQf_F1HATMYUbl53s,3695
|
133
133
|
reconcile/aus/ocm_upgrade_scheduler_org.py,sha256=QeZAQ1wdDPcwZ77b3Xmt4yBV60rWi3qd8h-vGwnwoCs,3948
|
134
134
|
reconcile/aus/upgrades.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -194,8 +194,8 @@ reconcile/endpoints_discovery/merge_request_manager.py,sha256=wUMsumxv8RnWaRatta
|
|
194
194
|
reconcile/external_resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
195
195
|
reconcile/external_resources/aws.py,sha256=7W-6d-lXO6JGwaxtO1Uc3Lw0p8csJ1EVgz__O1YWUOc,4793
|
196
196
|
reconcile/external_resources/factories.py,sha256=nhdTqf1WEfRfgd5-70KAUJVz0ZvZ19C3Pz7wmotSdrs,4857
|
197
|
-
reconcile/external_resources/integration.py,sha256=
|
198
|
-
reconcile/external_resources/integration_secrets_sync.py,sha256=
|
197
|
+
reconcile/external_resources/integration.py,sha256=Ne_D5eXtR2a4kFg2Us5h7Zc_WIRgOBxaKbVPic0c6uw,6619
|
198
|
+
reconcile/external_resources/integration_secrets_sync.py,sha256=dX09O3r6KURziUYYfiki10orNjOGVma-XojhVqd0ww4,1667
|
199
199
|
reconcile/external_resources/manager.py,sha256=cs6QEirz9EaLiuxybZ_1ugUn61vNWlKAC4NKouqpd5I,16148
|
200
200
|
reconcile/external_resources/meta.py,sha256=noaytFzmShpzLA_ebGh7wuP45mOfHIOnnoUxivjDa1I,672
|
201
201
|
reconcile/external_resources/metrics.py,sha256=jsN3IF78Su23d3Qp4BTKXkgZPN6AKjdS8sZFEXmYCek,863
|
@@ -616,7 +616,7 @@ reconcile/typed_queries/clusters_with_peering.py,sha256=lIai7SJJD0bqIJbe7virgrbY
|
|
616
616
|
reconcile/typed_queries/dynatrace.py,sha256=8vXDXDIDf9_vN_efYwysDr4gLN7SCx4I2bOoNxQhbio,312
|
617
617
|
reconcile/typed_queries/dynatrace_environments.py,sha256=VV_7KzKG9wqGDV9wZLbcCJtfuzPhTV1wdg0YwAOaq3A,413
|
618
618
|
reconcile/typed_queries/dynatrace_token_provider_token_specs.py,sha256=x41KG6JRDNYw5QGJYtIFNwSeejUUgxrL-agS8qFf6q0,433
|
619
|
-
reconcile/typed_queries/external_resources.py,sha256=
|
619
|
+
reconcile/typed_queries/external_resources.py,sha256=AT5md8nH5gX56UrdWnU4T3_aVF_FanHorXNFkU6s6KY,1573
|
620
620
|
reconcile/typed_queries/get_state_aws_account.py,sha256=CSJjVPWsUZ2rkGIt8ehoQt7hokFqrUDgG9HFlg2lVD8,492
|
621
621
|
reconcile/typed_queries/github_orgs.py,sha256=UZhoPl8qvA_tcO7CZlN8GuMKckt3ywd47Suu61rgHsc,258
|
622
622
|
reconcile/typed_queries/gitlab_instances.py,sha256=ZVQHy2W9xIp53f5qYkjKLHLHgOVtQpxTfcmM1C2046g,291
|
@@ -838,10 +838,11 @@ tools/app_interface_metrics_exporter.py,sha256=zkwkxdAUAxjdc-pzx2_oJXG25fo0Fnyd5
|
|
838
838
|
tools/app_interface_reporter.py,sha256=oZPib4HPq0aZ2Zui1QGJGk6qQdfpeihujGDBnSdKyGE,17627
|
839
839
|
tools/glitchtip_access_reporter.py,sha256=oPBnk_YoDuljU3v0FaChzOwwnk4vap1xEE67QEjzdqs,2948
|
840
840
|
tools/glitchtip_access_revalidation.py,sha256=8kbBJk04mkq28kWoRDDkfCGIF3GRg3pJrFAh1sW0dbk,2821
|
841
|
-
tools/qontract_cli.py,sha256=
|
841
|
+
tools/qontract_cli.py,sha256=_OqTpZ57TGdWR_0Oc9sT83JVilLKbAz5NbPm1EyL34I,133385
|
842
842
|
tools/sd_app_sre_alert_report.py,sha256=e9vAdyenUz2f5c8-z-5WY0wv-SJ9aePKDH2r4IwB6pc,5063
|
843
843
|
tools/template_validation.py,sha256=qpKYaTgk0GOPGa2Ct5_5sKdwIHtCAKIBGzsMPuJU5fw,3371
|
844
844
|
tools/cli_commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
845
|
+
tools/cli_commands/erv2.py,sha256=vHYBYkTaS3h2qEStuAE6iThCt54LD2o3-0bJLcYODKY,16393
|
845
846
|
tools/cli_commands/gpg_encrypt.py,sha256=x02JOMn834z89YSNvr5B-oJky7rR1C0begCkPh45eHk,4958
|
846
847
|
tools/cli_commands/systems_and_tools.py,sha256=EMHOF1AtUDaoSk0bbjl6oUKYAz4rTZjIBaF-6E6GspM,16816
|
847
848
|
tools/cli_commands/cost_report/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -865,12 +866,13 @@ tools/sre_checkpoints/util.py,sha256=zEDbGr18ZeHNQwW8pUsr2JRjuXIPz--WAGJxZo9sv_Y
|
|
865
866
|
tools/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
866
867
|
tools/test/conftest.py,sha256=YLtiauk_StNFE-lirLnfG_BpJmlB2NGMZISE9A4zwvk,2421
|
867
868
|
tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvftCWEEf-g1mfXOtgCog-g,1271
|
869
|
+
tools/test/test_erv2.py,sha256=EAS7QuJkHisRVO9bMGxm662L5B6i66wF_mT9PAjVzrU,3128
|
868
870
|
tools/test/test_qontract_cli.py,sha256=_D61RFGAN5x44CY1tYbouhlGXXABwYfxKSWSQx3Jrss,4941
|
869
871
|
tools/test/test_saas_promotion_state.py,sha256=dy4kkSSAQ7bC0Xp2CociETGN-2aABEfL6FU5D9Jl00Y,6056
|
870
872
|
tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
|
871
873
|
tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
|
872
|
-
qontract_reconcile-0.10.
|
873
|
-
qontract_reconcile-0.10.
|
874
|
-
qontract_reconcile-0.10.
|
875
|
-
qontract_reconcile-0.10.
|
876
|
-
qontract_reconcile-0.10.
|
874
|
+
qontract_reconcile-0.10.1rc1129.dist-info/METADATA,sha256=Z1h9eqAPOgiPFSvnkYjuHeRDc1Xe5s3E1tX1VHVx1sc,2213
|
875
|
+
qontract_reconcile-0.10.1rc1129.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
|
876
|
+
qontract_reconcile-0.10.1rc1129.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
|
877
|
+
qontract_reconcile-0.10.1rc1129.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
|
878
|
+
qontract_reconcile-0.10.1rc1129.dist-info/RECORD,,
|
@@ -251,7 +251,9 @@ def calculate_diff(
|
|
251
251
|
addon_id,
|
252
252
|
)
|
253
253
|
for current in addon_current_state:
|
254
|
-
if addon_id == current.addon_id and
|
254
|
+
if addon_id == current.addon_id and (
|
255
|
+
current.schedule_type == "automatic" or current.state == "completed"
|
256
|
+
):
|
255
257
|
diffs.append(
|
256
258
|
aus.UpgradePolicyHandler(
|
257
259
|
action="delete",
|
@@ -84,7 +84,7 @@ def create_er_manager(
|
|
84
84
|
) -> ExternalResourcesManager:
|
85
85
|
vault_settings = get_app_interface_vault_settings()
|
86
86
|
secret_reader = create_secret_reader(use_vault=vault_settings.vault)
|
87
|
-
er_settings = get_settings()
|
87
|
+
er_settings = get_settings()
|
88
88
|
m_inventory = load_module_inventory(get_modules())
|
89
89
|
namespaces = [ns for ns in get_namespaces() if ns.external_resources]
|
90
90
|
er_inventory = ExternalResourcesInventory(namespaces)
|
@@ -139,7 +139,7 @@ def run(
|
|
139
139
|
) -> None:
|
140
140
|
vault_settings = get_app_interface_vault_settings()
|
141
141
|
secret_reader = create_secret_reader(use_vault=vault_settings.vault)
|
142
|
-
er_settings = get_settings()
|
142
|
+
er_settings = get_settings()
|
143
143
|
|
144
144
|
if not workers_cluster:
|
145
145
|
workers_cluster = er_settings.workers_cluster.name
|
@@ -173,7 +173,7 @@ def run(
|
|
173
173
|
def early_exit_desired_state(*args: Any, **kwargs: Any) -> dict[str, Any]:
|
174
174
|
vault_settings = get_app_interface_vault_settings()
|
175
175
|
secret_reader = create_secret_reader(use_vault=vault_settings.vault)
|
176
|
-
er_settings = get_settings()
|
176
|
+
er_settings = get_settings()
|
177
177
|
|
178
178
|
with get_aws_api(
|
179
179
|
query_func=gql.get_api().query,
|
@@ -25,7 +25,7 @@ def run(dry_run: bool, thread_pool_size: int) -> None:
|
|
25
25
|
"""
|
26
26
|
vault_settings = get_app_interface_vault_settings()
|
27
27
|
secret_reader = create_secret_reader(use_vault=vault_settings.vault)
|
28
|
-
er_settings = get_settings()
|
28
|
+
er_settings = get_settings()
|
29
29
|
|
30
30
|
namespaces = [ns for ns in get_namespaces() if ns.external_resources]
|
31
31
|
er_inventory = ExternalResourcesInventory(namespaces)
|
@@ -28,13 +28,13 @@ def get_namespaces(query_func: Callable | None = None) -> list[NamespaceV1]:
|
|
28
28
|
return list(data.namespaces or [])
|
29
29
|
|
30
30
|
|
31
|
-
def get_settings(
|
32
|
-
query_func: Callable | None = None,
|
33
|
-
) -> list[ExternalResourcesSettingsV1]:
|
31
|
+
def get_settings(query_func: Callable | None = None) -> ExternalResourcesSettingsV1:
|
34
32
|
if not query_func:
|
35
33
|
query_func = gql.get_api().query
|
36
34
|
data = query_settings(query_func=query_func)
|
37
|
-
|
35
|
+
if not data.settings:
|
36
|
+
raise ValueError("No external resources settings found.")
|
37
|
+
return data.settings[0]
|
38
38
|
|
39
39
|
|
40
40
|
def get_modules(
|
@@ -0,0 +1,473 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import json
|
4
|
+
import os
|
5
|
+
import sys
|
6
|
+
from collections.abc import Iterator
|
7
|
+
from contextlib import contextmanager
|
8
|
+
from difflib import get_close_matches
|
9
|
+
from enum import Enum
|
10
|
+
from pathlib import Path
|
11
|
+
from subprocess import CalledProcessError, run
|
12
|
+
from typing import Protocol
|
13
|
+
|
14
|
+
from pydantic import BaseModel
|
15
|
+
from rich import print as rich_print
|
16
|
+
from rich.console import Console
|
17
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
18
|
+
from rich.prompt import Confirm, IntPrompt
|
19
|
+
|
20
|
+
from reconcile.external_resources.integration import get_aws_api
|
21
|
+
from reconcile.external_resources.manager import setup_factories
|
22
|
+
from reconcile.external_resources.meta import FLAG_RESOURCE_MANAGED_BY_ERV2
|
23
|
+
from reconcile.external_resources.model import (
|
24
|
+
ExternalResourceKey,
|
25
|
+
ExternalResourceModuleConfiguration,
|
26
|
+
ExternalResourcesInventory,
|
27
|
+
load_module_inventory,
|
28
|
+
)
|
29
|
+
from reconcile.external_resources.state import (
|
30
|
+
ExternalResourcesStateDynamoDB,
|
31
|
+
ResourceStatus,
|
32
|
+
)
|
33
|
+
from reconcile.typed_queries.external_resources import (
|
34
|
+
get_modules,
|
35
|
+
get_namespaces,
|
36
|
+
get_settings,
|
37
|
+
)
|
38
|
+
from reconcile.utils import gql
|
39
|
+
from reconcile.utils.exceptions import FetchResourceError
|
40
|
+
from reconcile.utils.secret_reader import SecretReaderBase
|
41
|
+
|
42
|
+
|
43
|
+
def progress_spinner() -> Progress:
|
44
|
+
"""Display shiny progress spinner."""
|
45
|
+
console = Console(record=True)
|
46
|
+
return Progress(
|
47
|
+
SpinnerColumn(),
|
48
|
+
TextColumn("[progress.description]{task.description}"),
|
49
|
+
TimeElapsedColumn(),
|
50
|
+
console=console,
|
51
|
+
)
|
52
|
+
|
53
|
+
|
54
|
+
@contextmanager
|
55
|
+
def pause_progress_spinner(progress: Progress | None) -> Iterator:
|
56
|
+
"""Pause the progress spinner."""
|
57
|
+
if progress:
|
58
|
+
progress.stop()
|
59
|
+
UP = "\x1b[1A"
|
60
|
+
CLEAR = "\x1b[2K"
|
61
|
+
for task in progress.tasks:
|
62
|
+
if task.finished:
|
63
|
+
continue
|
64
|
+
print(UP + CLEAR + UP)
|
65
|
+
yield
|
66
|
+
if progress:
|
67
|
+
progress.start()
|
68
|
+
|
69
|
+
|
70
|
+
@contextmanager
|
71
|
+
def task(progress: Progress | None, description: str) -> Iterator:
|
72
|
+
"""Display a task in the progress spinner."""
|
73
|
+
if progress:
|
74
|
+
task = progress.add_task(description, total=1)
|
75
|
+
yield
|
76
|
+
if progress:
|
77
|
+
progress.advance(task)
|
78
|
+
|
79
|
+
|
80
|
+
class Erv2Cli:
|
81
|
+
def __init__(
|
82
|
+
self,
|
83
|
+
provision_provider: str,
|
84
|
+
provisioner: str,
|
85
|
+
provider: str,
|
86
|
+
identifier: str,
|
87
|
+
secret_reader: SecretReaderBase,
|
88
|
+
temp_dir: Path | None = None,
|
89
|
+
progress_spinner: Progress | None = None,
|
90
|
+
) -> None:
|
91
|
+
self._provision_provider = provision_provider
|
92
|
+
self._provisioner = provisioner
|
93
|
+
self._provider = provider
|
94
|
+
self._identifier = identifier
|
95
|
+
self._temp_dir = temp_dir
|
96
|
+
self.progress_spinner = progress_spinner
|
97
|
+
|
98
|
+
namespaces = [ns for ns in get_namespaces() if ns.external_resources]
|
99
|
+
er_inventory = ExternalResourcesInventory(namespaces)
|
100
|
+
|
101
|
+
try:
|
102
|
+
spec = er_inventory.get_inventory_spec(
|
103
|
+
provision_provider=provision_provider,
|
104
|
+
provisioner=provisioner,
|
105
|
+
provider=provider,
|
106
|
+
identifier=identifier,
|
107
|
+
)
|
108
|
+
except FetchResourceError:
|
109
|
+
rich_print(
|
110
|
+
f"[b red]Resource {provision_provider}/{provisioner}/{provider}/{identifier} not found[/]. Ensure `managed_by_erv2: true` is set!"
|
111
|
+
)
|
112
|
+
sys.exit(1)
|
113
|
+
|
114
|
+
self._secret_reader = secret_reader
|
115
|
+
self._er_settings = get_settings()
|
116
|
+
m_inventory = load_module_inventory(get_modules())
|
117
|
+
factories = setup_factories(
|
118
|
+
self._er_settings, m_inventory, er_inventory, self._secret_reader
|
119
|
+
)
|
120
|
+
f = factories.get_factory(spec.provision_provider)
|
121
|
+
self._resource = f.create_external_resource(spec)
|
122
|
+
f.validate_external_resource(self._resource)
|
123
|
+
self._module_configuration = (
|
124
|
+
ExternalResourceModuleConfiguration.resolve_configuration(
|
125
|
+
m_inventory.get_from_spec(spec), spec
|
126
|
+
)
|
127
|
+
)
|
128
|
+
|
129
|
+
@property
|
130
|
+
def input_data(self) -> str:
|
131
|
+
return self._resource.json(exclude={"data": {FLAG_RESOURCE_MANAGED_BY_ERV2}})
|
132
|
+
|
133
|
+
@property
|
134
|
+
def image(self) -> str:
|
135
|
+
return self._module_configuration.image_version
|
136
|
+
|
137
|
+
@property
|
138
|
+
def temp(self) -> Path:
|
139
|
+
if not self._temp_dir:
|
140
|
+
raise ValueError("Temp directory is not set!")
|
141
|
+
return self._temp_dir
|
142
|
+
|
143
|
+
def reconcile(self) -> None:
|
144
|
+
with get_aws_api(
|
145
|
+
query_func=gql.get_api().query,
|
146
|
+
account_name=self._er_settings.state_dynamodb_account.name,
|
147
|
+
region=self._er_settings.state_dynamodb_region,
|
148
|
+
secret_reader=self._secret_reader,
|
149
|
+
) as aws_api:
|
150
|
+
state_manager = ExternalResourcesStateDynamoDB(
|
151
|
+
aws_api=aws_api,
|
152
|
+
table_name=self._er_settings.state_dynamodb_table,
|
153
|
+
)
|
154
|
+
key = ExternalResourceKey(
|
155
|
+
provision_provider=self._provision_provider,
|
156
|
+
provisioner_name=self._provisioner,
|
157
|
+
provider=self._provider,
|
158
|
+
identifier=self._identifier,
|
159
|
+
)
|
160
|
+
current_state = state_manager.get_external_resource_state(key)
|
161
|
+
if current_state.resource_status != ResourceStatus.NOT_EXISTS:
|
162
|
+
state_manager.update_resource_status(
|
163
|
+
key, ResourceStatus.RECONCILIATION_REQUESTED
|
164
|
+
)
|
165
|
+
else:
|
166
|
+
rich_print("[b red]External Resource does not exist")
|
167
|
+
|
168
|
+
def build_cdktf(self, credentials: Path) -> None:
|
169
|
+
"""Run the CDKTF container and return the generated CDKTF json."""
|
170
|
+
input_file = self.temp / "input.json"
|
171
|
+
input_file.write_text(self.input_data)
|
172
|
+
|
173
|
+
# delete previous ERv2 container
|
174
|
+
run(["docker", "rm", "-f", "erv2"], capture_output=True, check=True)
|
175
|
+
|
176
|
+
try:
|
177
|
+
cdktf_outdir = "/tmp/cdktf.out"
|
178
|
+
|
179
|
+
# run cdktf synth
|
180
|
+
with task(self.progress_spinner, "-- Running CDKTF synth"):
|
181
|
+
run(
|
182
|
+
[
|
183
|
+
"docker",
|
184
|
+
"run",
|
185
|
+
"--name",
|
186
|
+
"erv2",
|
187
|
+
"--mount",
|
188
|
+
f"type=bind,source={input_file!s},target=/inputs/input.json",
|
189
|
+
"--mount",
|
190
|
+
f"type=bind,source={credentials!s},target=/credentials",
|
191
|
+
"-e",
|
192
|
+
"AWS_SHARED_CREDENTIALS_FILE=/credentials",
|
193
|
+
"--entrypoint",
|
194
|
+
"cdktf",
|
195
|
+
self.image,
|
196
|
+
"synth",
|
197
|
+
"--output",
|
198
|
+
cdktf_outdir,
|
199
|
+
],
|
200
|
+
check=True,
|
201
|
+
capture_output=True,
|
202
|
+
)
|
203
|
+
|
204
|
+
# # get the cdk.tf.json
|
205
|
+
with task(self.progress_spinner, "-- Copying the generated cdk.tf.json"):
|
206
|
+
run(
|
207
|
+
[
|
208
|
+
"docker",
|
209
|
+
"cp",
|
210
|
+
f"erv2:{cdktf_outdir}/stacks/CDKTF/cdk.tf.json",
|
211
|
+
str(self.temp),
|
212
|
+
],
|
213
|
+
check=True,
|
214
|
+
capture_output=True,
|
215
|
+
)
|
216
|
+
except CalledProcessError as e:
|
217
|
+
print(e.stderr.decode("utf-8"))
|
218
|
+
print(e.stdout.decode("utf-8"))
|
219
|
+
raise
|
220
|
+
|
221
|
+
|
222
|
+
class TfRun(Protocol):
|
223
|
+
def __call__(self, path: Path, cmd: list[str]) -> str: ...
|
224
|
+
|
225
|
+
|
226
|
+
def tf_run(path: Path, cmd: list[str]) -> str:
|
227
|
+
env = os.environ.copy()
|
228
|
+
env["TF_CLI_ARGS"] = "-no-color"
|
229
|
+
try:
|
230
|
+
return run(
|
231
|
+
["terraform", *cmd],
|
232
|
+
cwd=path,
|
233
|
+
check=True,
|
234
|
+
capture_output=True,
|
235
|
+
env=env,
|
236
|
+
).stdout.decode("utf-8")
|
237
|
+
except CalledProcessError as e:
|
238
|
+
print(e.stderr.decode("utf-8"))
|
239
|
+
print(e.stdout.decode("utf-8"))
|
240
|
+
raise
|
241
|
+
|
242
|
+
|
243
|
+
class TfAction(Enum):
|
244
|
+
CREATE = "create"
|
245
|
+
DESTROY = "delete"
|
246
|
+
|
247
|
+
|
248
|
+
class TfResource(BaseModel):
|
249
|
+
address: str
|
250
|
+
|
251
|
+
@property
|
252
|
+
def id(self) -> str:
|
253
|
+
return self.address.split(".")[1]
|
254
|
+
|
255
|
+
@property
|
256
|
+
def type(self) -> str:
|
257
|
+
return self.address.split(".")[0]
|
258
|
+
|
259
|
+
def __str__(self) -> str:
|
260
|
+
return self.address
|
261
|
+
|
262
|
+
def __repr__(self) -> str:
|
263
|
+
return str(self)
|
264
|
+
|
265
|
+
|
266
|
+
class TfResourceList(BaseModel):
|
267
|
+
resources: list[TfResource]
|
268
|
+
|
269
|
+
def __iter__(self) -> Iterator[TfResource]: # type: ignore
|
270
|
+
return iter(self.resources)
|
271
|
+
|
272
|
+
def _get_resource_by_address(self, address: str) -> TfResource | None:
|
273
|
+
for resource in self.resources:
|
274
|
+
if resource.address == address:
|
275
|
+
return resource
|
276
|
+
return None
|
277
|
+
|
278
|
+
def _get_resources_by_type(self, type: str) -> list[TfResource]:
|
279
|
+
results = [resource for resource in self.resources if resource.type == type]
|
280
|
+
if not results:
|
281
|
+
raise KeyError(f"Resource type '{type}' not found!")
|
282
|
+
return results
|
283
|
+
|
284
|
+
def __getitem__(self, tf_resource: TfResource) -> list[TfResource]:
|
285
|
+
"""Get a resource by searching the resource list.
|
286
|
+
|
287
|
+
self holds the source resources (terraform-resources).
|
288
|
+
The tf_resource is the destination resource (ERv2).
|
289
|
+
"""
|
290
|
+
if resource := self._get_resource_by_address(tf_resource.address):
|
291
|
+
# exact match by AWS address
|
292
|
+
return [resource]
|
293
|
+
|
294
|
+
# a resource with the same ID does not exist
|
295
|
+
# let's try to find the resource by the AWS type
|
296
|
+
results = self._get_resources_by_type(tf_resource.type)
|
297
|
+
if len(results) == 1:
|
298
|
+
# there is just one resource with the same type
|
299
|
+
# this must be the searched resource.
|
300
|
+
return results
|
301
|
+
|
302
|
+
# ok, now it's getting tricky. Let's use difflib and let the user decide.
|
303
|
+
possible_matches_ids = get_close_matches(
|
304
|
+
tf_resource.id, [r.id for r in results]
|
305
|
+
)
|
306
|
+
return [r for r in results if r.id in possible_matches_ids]
|
307
|
+
|
308
|
+
def __len__(self) -> int:
|
309
|
+
return len(self.resources)
|
310
|
+
|
311
|
+
|
312
|
+
class TerraformCli:
|
313
|
+
def __init__(
|
314
|
+
self,
|
315
|
+
path: Path,
|
316
|
+
dry_run: bool = True,
|
317
|
+
tf_run: TfRun = tf_run,
|
318
|
+
progress_spinner: Progress | None = None,
|
319
|
+
) -> None:
|
320
|
+
self._path = path
|
321
|
+
self._dry_run = dry_run
|
322
|
+
self._tf_run = tf_run
|
323
|
+
self.progress_spinner = progress_spinner
|
324
|
+
self.initialized = False
|
325
|
+
|
326
|
+
def init(self) -> None:
|
327
|
+
"""Initialize the terraform modules."""
|
328
|
+
self._tf_init()
|
329
|
+
self._tf_plan()
|
330
|
+
self._tf_state_pull()
|
331
|
+
self.initialized = True
|
332
|
+
|
333
|
+
@property
|
334
|
+
def state_file(self) -> Path:
|
335
|
+
return self._path / "state.json"
|
336
|
+
|
337
|
+
def _tf_init(self) -> None:
|
338
|
+
with task(self.progress_spinner, "-- Running terraform init"):
|
339
|
+
self._tf_run(self._path, ["init"])
|
340
|
+
|
341
|
+
def _tf_plan(self) -> None:
|
342
|
+
with task(self.progress_spinner, "-- Running terraform plan"):
|
343
|
+
self._tf_run(self._path, ["plan", "-out=plan.out"])
|
344
|
+
|
345
|
+
def _tf_state_pull(self) -> None:
|
346
|
+
with task(self.progress_spinner, "-- Retrieving the terraform state"):
|
347
|
+
self.state_file.write_text(self._tf_run(self._path, ["state", "pull"]))
|
348
|
+
|
349
|
+
def _tf_state_push(self) -> None:
|
350
|
+
with task(
|
351
|
+
self.progress_spinner,
|
352
|
+
f"-- Uploading the terraform state {'[b red](DRY-RUN)' if self._dry_run else ''}",
|
353
|
+
):
|
354
|
+
if not self._dry_run:
|
355
|
+
self._tf_run(self._path, ["state", "push", str(self.state_file)])
|
356
|
+
|
357
|
+
def upload_state(self) -> None:
|
358
|
+
self._tf_state_push()
|
359
|
+
|
360
|
+
def resource_changes(self, action: TfAction) -> TfResourceList:
|
361
|
+
"""Get the resource changes."""
|
362
|
+
plan = json.loads(self._tf_run(self._path, ["show", "-json", "plan.out"]))
|
363
|
+
return TfResourceList(
|
364
|
+
resources=[
|
365
|
+
TfResource(address=r["address"])
|
366
|
+
for r in plan["resource_changes"]
|
367
|
+
if action.value.lower() in r["change"]["actions"]
|
368
|
+
]
|
369
|
+
)
|
370
|
+
|
371
|
+
def move_resource(
|
372
|
+
self, source_state_file: Path, source: TfResource, destination: TfResource
|
373
|
+
) -> None:
|
374
|
+
"""Move the resource from source state file to destination state file."""
|
375
|
+
if self.progress_spinner:
|
376
|
+
self.progress_spinner.log(
|
377
|
+
f"-- Moving {destination} {'[b red](DRY-RUN)' if self._dry_run else ''}"
|
378
|
+
)
|
379
|
+
if not self._dry_run:
|
380
|
+
self._tf_run(
|
381
|
+
self._path,
|
382
|
+
[
|
383
|
+
"state",
|
384
|
+
"mv",
|
385
|
+
"-lock=false",
|
386
|
+
f"-state={source_state_file!s}",
|
387
|
+
f"-state-out={self.state_file!s}",
|
388
|
+
f"{source.address}",
|
389
|
+
f"{destination.address}",
|
390
|
+
],
|
391
|
+
)
|
392
|
+
|
393
|
+
def migrate_resources(self, source: TerraformCli) -> None:
|
394
|
+
"""Migrate the resources from source."""
|
395
|
+
# if not self.initialized or not source.initialized:
|
396
|
+
# raise ValueError("Terraform must be initialized before!")
|
397
|
+
|
398
|
+
source_resources = source.resource_changes(TfAction.DESTROY)
|
399
|
+
destination_resources = self.resource_changes(TfAction.CREATE)
|
400
|
+
|
401
|
+
if not source_resources or not destination_resources:
|
402
|
+
raise ValueError("No resource changes found!")
|
403
|
+
|
404
|
+
if len(source_resources) != len(destination_resources):
|
405
|
+
with pause_progress_spinner(self.progress_spinner):
|
406
|
+
rich_print(
|
407
|
+
"[b red]The number of changes (ERv2 vs terraform-resource) does not match! Please review them carefully![/]"
|
408
|
+
)
|
409
|
+
rich_print("ERv2:")
|
410
|
+
rich_print(
|
411
|
+
"\n".join([
|
412
|
+
f" {i}: {r.address}"
|
413
|
+
for i, r in enumerate(destination_resources, start=1)
|
414
|
+
])
|
415
|
+
)
|
416
|
+
rich_print("Terraform:")
|
417
|
+
rich_print(
|
418
|
+
"\n".join([
|
419
|
+
f" {i}: {r.address}"
|
420
|
+
for i, r in enumerate(source_resources, start=1)
|
421
|
+
])
|
422
|
+
)
|
423
|
+
if not Confirm.ask("Would you like to continue anyway?", default=False):
|
424
|
+
return
|
425
|
+
|
426
|
+
for destination_resource in destination_resources:
|
427
|
+
possible_source_resouces = source_resources[destination_resource]
|
428
|
+
if not possible_source_resouces:
|
429
|
+
raise ValueError(
|
430
|
+
f"Source resource for {destination_resource} not found!"
|
431
|
+
)
|
432
|
+
elif len(possible_source_resouces) == 1:
|
433
|
+
# just one resource found.
|
434
|
+
source_resource = possible_source_resouces[0]
|
435
|
+
else:
|
436
|
+
# more than one resource found. Let the user decide.
|
437
|
+
with pause_progress_spinner(self.progress_spinner):
|
438
|
+
rich_print(
|
439
|
+
f"[b red]{destination_resource.address} not found![/] Please select the related source ID manually!"
|
440
|
+
)
|
441
|
+
for i, r in enumerate(possible_source_resouces, start=1):
|
442
|
+
print(f"{i}: {r.address}")
|
443
|
+
|
444
|
+
index = IntPrompt.ask(
|
445
|
+
":boom: Enter the number",
|
446
|
+
choices=[
|
447
|
+
str(i) for i in range(1, len(possible_source_resouces) + 1)
|
448
|
+
],
|
449
|
+
show_choices=False,
|
450
|
+
)
|
451
|
+
source_resource = possible_source_resouces[index - 1]
|
452
|
+
|
453
|
+
self.move_resource(
|
454
|
+
source_state_file=source.state_file,
|
455
|
+
source=source_resource,
|
456
|
+
destination=destination_resource,
|
457
|
+
)
|
458
|
+
|
459
|
+
if not self._dry_run:
|
460
|
+
if self.progress_spinner:
|
461
|
+
self.progress_spinner.stop()
|
462
|
+
if not Confirm.ask(
|
463
|
+
"\nEverything ok? Would you like to upload the modified terraform states",
|
464
|
+
default=False,
|
465
|
+
):
|
466
|
+
return
|
467
|
+
|
468
|
+
if self.progress_spinner:
|
469
|
+
self.progress_spinner.start()
|
470
|
+
|
471
|
+
# finally push the terraform states
|
472
|
+
self.upload_state()
|
473
|
+
source.upload_state()
|
tools/qontract_cli.py
CHANGED
@@ -14,7 +14,9 @@ from datetime import (
|
|
14
14
|
timedelta,
|
15
15
|
)
|
16
16
|
from operator import itemgetter
|
17
|
+
from pathlib import Path
|
17
18
|
from statistics import median
|
19
|
+
from textwrap import dedent
|
18
20
|
from typing import Any
|
19
21
|
|
20
22
|
import boto3
|
@@ -23,10 +25,9 @@ import click.core
|
|
23
25
|
import requests
|
24
26
|
import yaml
|
25
27
|
from rich import box
|
26
|
-
from rich
|
27
|
-
|
28
|
-
|
29
|
-
)
|
28
|
+
from rich import print as rich_print
|
29
|
+
from rich.console import Console, Group
|
30
|
+
from rich.prompt import Confirm
|
30
31
|
from rich.table import Table
|
31
32
|
from rich.tree import Tree
|
32
33
|
|
@@ -59,25 +60,11 @@ from reconcile.change_owners.change_owners import (
|
|
59
60
|
)
|
60
61
|
from reconcile.checkpoint import report_invalid_metadata
|
61
62
|
from reconcile.cli import (
|
63
|
+
TERRAFORM_VERSION,
|
64
|
+
TERRAFORM_VERSION_REGEX,
|
62
65
|
config_file,
|
63
66
|
use_jump_host,
|
64
67
|
)
|
65
|
-
from reconcile.external_resources.integration import (
|
66
|
-
get_aws_api,
|
67
|
-
)
|
68
|
-
from reconcile.external_resources.manager import (
|
69
|
-
setup_factories,
|
70
|
-
)
|
71
|
-
from reconcile.external_resources.meta import FLAG_RESOURCE_MANAGED_BY_ERV2
|
72
|
-
from reconcile.external_resources.model import (
|
73
|
-
ExternalResourceKey,
|
74
|
-
ExternalResourcesInventory,
|
75
|
-
load_module_inventory,
|
76
|
-
)
|
77
|
-
from reconcile.external_resources.state import (
|
78
|
-
ExternalResourcesStateDynamoDB,
|
79
|
-
ResourceStatus,
|
80
|
-
)
|
81
68
|
from reconcile.gql_definitions.advanced_upgrade_service.aus_clusters import (
|
82
69
|
query as aus_clusters_query,
|
83
70
|
)
|
@@ -99,11 +86,6 @@ from reconcile.typed_queries.app_quay_repos_escalation_policies import (
|
|
99
86
|
get_apps_quay_repos_escalation_policies,
|
100
87
|
)
|
101
88
|
from reconcile.typed_queries.clusters import get_clusters
|
102
|
-
from reconcile.typed_queries.external_resources import (
|
103
|
-
get_modules,
|
104
|
-
get_namespaces,
|
105
|
-
get_settings,
|
106
|
-
)
|
107
89
|
from reconcile.typed_queries.saas_files import get_saas_files
|
108
90
|
from reconcile.typed_queries.slo_documents import get_slo_documents
|
109
91
|
from reconcile.typed_queries.status_board import get_status_board
|
@@ -168,6 +150,12 @@ from reconcile.utils.state import init_state
|
|
168
150
|
from reconcile.utils.terraform_client import TerraformClient as Terraform
|
169
151
|
from tools.cli_commands.cost_report.aws import AwsCostReportCommand
|
170
152
|
from tools.cli_commands.cost_report.openshift import OpenShiftCostReportCommand
|
153
|
+
from tools.cli_commands.erv2 import (
|
154
|
+
Erv2Cli,
|
155
|
+
TerraformCli,
|
156
|
+
progress_spinner,
|
157
|
+
task,
|
158
|
+
)
|
171
159
|
from tools.cli_commands.gpg_encrypt import (
|
172
160
|
GPGEncryptCommand,
|
173
161
|
GPGEncryptCommandData,
|
@@ -3945,85 +3933,185 @@ def remove(ctx, sso_client_vault_secret_path: str):
|
|
3945
3933
|
|
3946
3934
|
|
3947
3935
|
@root.group()
|
3936
|
+
@click.option(
|
3937
|
+
"--provision-provider",
|
3938
|
+
required=True,
|
3939
|
+
help="externalResources.provider",
|
3940
|
+
default="aws",
|
3941
|
+
)
|
3942
|
+
@click.option(
|
3943
|
+
"--provisioner",
|
3944
|
+
required=True,
|
3945
|
+
help="externalResources.provisioner.name. E.g. app-sre-stage",
|
3946
|
+
prompt=True,
|
3947
|
+
)
|
3948
|
+
@click.option(
|
3949
|
+
"--provider",
|
3950
|
+
required=True,
|
3951
|
+
help="externalResources.resources.provider. E.g. rds, msk, ...",
|
3952
|
+
prompt=True,
|
3953
|
+
)
|
3954
|
+
@click.option(
|
3955
|
+
"--identifier",
|
3956
|
+
required=True,
|
3957
|
+
help="externalResources.resources.identifier. E.g. erv2-example",
|
3958
|
+
prompt=True,
|
3959
|
+
)
|
3948
3960
|
@click.pass_context
|
3949
|
-
def external_resources(
|
3961
|
+
def external_resources(
|
3962
|
+
ctx, provision_provider: str, provisioner: str, provider: str, identifier: str
|
3963
|
+
):
|
3950
3964
|
"""External resources commands"""
|
3965
|
+
ctx.obj["provision_provider"] = provision_provider
|
3966
|
+
ctx.obj["provisioner"] = provisioner
|
3967
|
+
ctx.obj["provider"] = provider
|
3968
|
+
ctx.obj["identifier"] = identifier
|
3969
|
+
vault_settings = get_app_interface_vault_settings()
|
3970
|
+
ctx.obj["secret_reader"] = create_secret_reader(use_vault=vault_settings.vault)
|
3951
3971
|
|
3952
3972
|
|
3953
3973
|
@external_resources.command()
|
3954
|
-
@click.argument("provision-provider", required=True)
|
3955
|
-
@click.argument("provisioner", required=True)
|
3956
|
-
@click.argument("provider", required=True)
|
3957
|
-
@click.argument("identifier", required=True)
|
3958
3974
|
@click.pass_context
|
3959
|
-
def get_input(
|
3960
|
-
ctx, provision_provider: str, provisioner: str, provider: str, identifier: str
|
3961
|
-
):
|
3975
|
+
def get_input(ctx):
|
3962
3976
|
"""Gets the input data for an external resource asset. Input data is what is used
|
3963
|
-
in the Reconciliation Job to manage the resource.
|
3964
|
-
|
3965
|
-
|
3966
|
-
|
3967
|
-
|
3968
|
-
|
3969
|
-
|
3970
|
-
spec = er_inventory.get_inventory_spec(
|
3971
|
-
provision_provider=provision_provider,
|
3972
|
-
provisioner=provisioner,
|
3973
|
-
provider=provider,
|
3974
|
-
identifier=identifier,
|
3977
|
+
in the Reconciliation Job to manage the resource."""
|
3978
|
+
erv2cli = Erv2Cli(
|
3979
|
+
provision_provider=ctx.obj["provision_provider"],
|
3980
|
+
provisioner=ctx.obj["provisioner"],
|
3981
|
+
provider=ctx.obj["provider"],
|
3982
|
+
identifier=ctx.obj["identifier"],
|
3983
|
+
secret_reader=ctx.obj["secret_reader"],
|
3975
3984
|
)
|
3976
|
-
|
3977
|
-
secret_reader = create_secret_reader(use_vault=vault_settings.vault)
|
3978
|
-
er_settings = get_settings()[0]
|
3979
|
-
m_inventory = load_module_inventory(get_modules())
|
3980
|
-
factories = setup_factories(er_settings, m_inventory, er_inventory, secret_reader)
|
3981
|
-
f = factories.get_factory(spec.provision_provider)
|
3982
|
-
resource = f.create_external_resource(spec)
|
3983
|
-
f.validate_external_resource(resource)
|
3984
|
-
print(resource.json(exclude={"data": {FLAG_RESOURCE_MANAGED_BY_ERV2}}))
|
3985
|
+
print(erv2cli.input_data)
|
3985
3986
|
|
3986
3987
|
|
3987
3988
|
@external_resources.command()
|
3988
|
-
@click.argument("provision-provider", required=True)
|
3989
|
-
@click.argument("provisioner", required=True)
|
3990
|
-
@click.argument("provider", required=True)
|
3991
|
-
@click.argument("identifier", required=True)
|
3992
3989
|
@click.pass_context
|
3993
|
-
def request_reconciliation(
|
3994
|
-
ctx, provision_provider: str, provisioner: str, provider: str, identifier: str
|
3995
|
-
):
|
3990
|
+
def request_reconciliation(ctx):
|
3996
3991
|
"""Marks a resource as it needs to get reconciled. The itegration will reconcile the resource at
|
3997
|
-
its next iteration.
|
3992
|
+
its next iteration."""
|
3993
|
+
erv2cli = Erv2Cli(
|
3994
|
+
provision_provider=ctx.obj["provision_provider"],
|
3995
|
+
provisioner=ctx.obj["provisioner"],
|
3996
|
+
provider=ctx.obj["provider"],
|
3997
|
+
identifier=ctx.obj["identifier"],
|
3998
|
+
secret_reader=ctx.obj["secret_reader"],
|
3999
|
+
)
|
4000
|
+
erv2cli.reconcile()
|
4001
|
+
|
4002
|
+
|
4003
|
+
@external_resources.command()
|
4004
|
+
@binary(["terraform"])
|
4005
|
+
@binary_version("terraform", ["version"], TERRAFORM_VERSION_REGEX, TERRAFORM_VERSION)
|
4006
|
+
@click.option(
|
4007
|
+
"--dry-run/--no-dry-run",
|
4008
|
+
help="Enable/Disable dry-run. Default: dry-run enabled!",
|
4009
|
+
default=True,
|
4010
|
+
)
|
4011
|
+
@click.option(
|
4012
|
+
"--skip-build/--no-skip-build",
|
4013
|
+
help="Skip/Do not skip the terraform and CDKTF builds. Default: build everything!",
|
4014
|
+
default=False,
|
4015
|
+
)
|
4016
|
+
@click.pass_context
|
4017
|
+
def migrate(ctx, dry_run: bool, skip_build: bool) -> None:
|
4018
|
+
"""Migrate an existing external resource managed by terraform-resources to ERv2.
|
4019
|
+
|
3998
4020
|
|
3999
|
-
|
4021
|
+
E.g: qontract-reconcile --config=<config> external-resources migrate aws app-sre-stage rds dashdotdb-stage
|
4000
4022
|
"""
|
4001
|
-
|
4002
|
-
|
4003
|
-
|
4004
|
-
|
4005
|
-
|
4006
|
-
|
4007
|
-
|
4008
|
-
|
4009
|
-
|
4010
|
-
|
4011
|
-
|
4012
|
-
|
4013
|
-
)
|
4014
|
-
|
4015
|
-
|
4016
|
-
|
4017
|
-
|
4018
|
-
|
4023
|
+
if ctx.obj["provider"] == "rds":
|
4024
|
+
# The "random_password" is not an AWS resource. It's just in the outputs and can't be migrated :(
|
4025
|
+
raise NotImplementedError("RDS migration is not supported yet!")
|
4026
|
+
|
4027
|
+
if not Confirm.ask(
|
4028
|
+
dedent("""
|
4029
|
+
Please disable terraform-resources: [i blue]https://app-interface.unleash.devshift.net/projects/default/features/terraform-resources[/]
|
4030
|
+
in Unleash before proceeding!
|
4031
|
+
|
4032
|
+
Do you want to proceed?"""),
|
4033
|
+
default=True,
|
4034
|
+
):
|
4035
|
+
sys.exit(0)
|
4036
|
+
|
4037
|
+
# use a temporary directory in $HOME. The MacOS colima default configuration allows docker mounts from $HOME.
|
4038
|
+
tempdir = Path.home() / ".erv2-migration"
|
4039
|
+
rich_print(f"Using temporary directory: [b]{tempdir}[/]")
|
4040
|
+
tempdir.mkdir(exist_ok=True)
|
4041
|
+
temp_erv2 = Path(tempdir) / "erv2"
|
4042
|
+
temp_erv2.mkdir(exist_ok=True)
|
4043
|
+
temp_tfr = tempdir / "terraform-resources"
|
4044
|
+
temp_tfr.mkdir(exist_ok=True)
|
4045
|
+
|
4046
|
+
with progress_spinner() as progress:
|
4047
|
+
with task(progress, "Preparing AWS credentials for CDKTF and local terraform"):
|
4048
|
+
# prepare AWS credentials for CDKTF and local terraform
|
4049
|
+
credentials_file = tempdir / "credentials"
|
4050
|
+
credentials_file.write_text(
|
4051
|
+
ctx.obj["secret_reader"].read_with_parameters(
|
4052
|
+
path=f"app-sre/external-resources/{ctx.obj['provisioner']}",
|
4053
|
+
field="credentials",
|
4054
|
+
format=None,
|
4055
|
+
version=None,
|
4056
|
+
)
|
4057
|
+
)
|
4058
|
+
os.environ["AWS_SHARED_CREDENTIALS_FILE"] = str(credentials_file)
|
4059
|
+
|
4060
|
+
erv2cli = Erv2Cli(
|
4061
|
+
provision_provider=ctx.obj["provision_provider"],
|
4062
|
+
provisioner=ctx.obj["provisioner"],
|
4063
|
+
provider=ctx.obj["provider"],
|
4064
|
+
identifier=ctx.obj["identifier"],
|
4065
|
+
secret_reader=ctx.obj["secret_reader"],
|
4066
|
+
temp_dir=temp_erv2,
|
4067
|
+
progress_spinner=progress,
|
4019
4068
|
)
|
4020
|
-
|
4021
|
-
|
4022
|
-
|
4023
|
-
|
4069
|
+
|
4070
|
+
with task(progress, "(erv2) Building the terraform configuration"):
|
4071
|
+
if not skip_build:
|
4072
|
+
# build the CDKTF output
|
4073
|
+
erv2cli.build_cdktf(credentials_file)
|
4074
|
+
erv2_tf_cli = TerraformCli(
|
4075
|
+
temp_erv2, dry_run=dry_run, progress_spinner=progress
|
4024
4076
|
)
|
4025
|
-
|
4026
|
-
|
4077
|
+
if not skip_build:
|
4078
|
+
erv2_tf_cli.init()
|
4079
|
+
|
4080
|
+
with task(
|
4081
|
+
progress, "(terraform-resources) Building the terraform configuration"
|
4082
|
+
):
|
4083
|
+
# build the terraform-resources output
|
4084
|
+
conf_tf = temp_tfr / "conf.tf.json"
|
4085
|
+
if not skip_build:
|
4086
|
+
tfr.run(
|
4087
|
+
dry_run=True,
|
4088
|
+
print_to_file=str(conf_tf),
|
4089
|
+
account_name=[ctx.obj["provisioner"]],
|
4090
|
+
)
|
4091
|
+
# remove comments
|
4092
|
+
conf_tf.write_text(
|
4093
|
+
"\n".join(
|
4094
|
+
line
|
4095
|
+
for line in conf_tf.read_text().splitlines()
|
4096
|
+
if not line.startswith("#")
|
4097
|
+
)
|
4098
|
+
)
|
4099
|
+
tfr_tf_cli = TerraformCli(
|
4100
|
+
temp_tfr, dry_run=dry_run, progress_spinner=progress
|
4101
|
+
)
|
4102
|
+
if not skip_build:
|
4103
|
+
tfr_tf_cli.init()
|
4104
|
+
|
4105
|
+
with progress_spinner() as progress:
|
4106
|
+
# start a new spinner instance for clean output
|
4107
|
+
erv2_tf_cli.progress_spinner = progress
|
4108
|
+
with task(
|
4109
|
+
progress,
|
4110
|
+
"Migrating the resources from terraform-resources to ERv2",
|
4111
|
+
):
|
4112
|
+
erv2_tf_cli.migrate_resources(source=tfr_tf_cli)
|
4113
|
+
|
4114
|
+
rich_print(f"[b red]Please remove the temporary directory ({tempdir}) manually!")
|
4027
4115
|
|
4028
4116
|
|
4029
4117
|
if __name__ == "__main__":
|
tools/test/test_erv2.py
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
import pytest
|
2
|
+
|
3
|
+
from tools.cli_commands.erv2 import TfResource, TfResourceList
|
4
|
+
|
5
|
+
|
6
|
+
def test_erv2_model_tfresource_list() -> None:
|
7
|
+
# terraform_resource_list
|
8
|
+
terraform_resource_list = TfResourceList(
|
9
|
+
resources=[
|
10
|
+
TfResource(address="postfix-module.user-1"),
|
11
|
+
TfResource(address="postfix-module.user-2"),
|
12
|
+
TfResource(address="prefix-module.user-1"),
|
13
|
+
TfResource(address="prefix-module.user-2"),
|
14
|
+
TfResource(address="exact-module.identifier"),
|
15
|
+
TfResource(address="something.else"),
|
16
|
+
# real life examples
|
17
|
+
TfResource(
|
18
|
+
address="aws_secretsmanager_secret.AmazonMSK_playground-msk-stage-playground-msk-stage"
|
19
|
+
),
|
20
|
+
TfResource(
|
21
|
+
address="aws_secretsmanager_secret.AmazonMSK_playground-msk-stage-playground-msk-stage-another-user"
|
22
|
+
),
|
23
|
+
TfResource(address="aws_secretsmanager_secret.playground-user"),
|
24
|
+
TfResource(address="aws_secretsmanager_secret.foobar-playground-user"),
|
25
|
+
]
|
26
|
+
)
|
27
|
+
assert len(terraform_resource_list) == 10
|
28
|
+
# exact match
|
29
|
+
assert terraform_resource_list[TfResource(address="exact-module.identifier")] == [
|
30
|
+
TfResource(address="exact-module.identifier")
|
31
|
+
]
|
32
|
+
# with postfix
|
33
|
+
assert terraform_resource_list[
|
34
|
+
TfResource(address="postfix-module.user-1-postfix")
|
35
|
+
] == [TfResource(address="postfix-module.user-1")]
|
36
|
+
# with prefix
|
37
|
+
assert terraform_resource_list[
|
38
|
+
TfResource(address="prefix-module.prefix-user-1")
|
39
|
+
] == [TfResource(address="prefix-module.user-1")]
|
40
|
+
|
41
|
+
# real life examples
|
42
|
+
assert terraform_resource_list[
|
43
|
+
TfResource(
|
44
|
+
address="aws_secretsmanager_secret.AmazonMSK_playground-msk-stage-playground-msk-stage-secret"
|
45
|
+
)
|
46
|
+
] == [
|
47
|
+
TfResource(
|
48
|
+
address="aws_secretsmanager_secret.AmazonMSK_playground-msk-stage-playground-msk-stage"
|
49
|
+
),
|
50
|
+
TfResource(
|
51
|
+
address="aws_secretsmanager_secret.AmazonMSK_playground-msk-stage-playground-msk-stage-another-user"
|
52
|
+
),
|
53
|
+
]
|
54
|
+
assert terraform_resource_list[
|
55
|
+
TfResource(
|
56
|
+
address="aws_secretsmanager_secret.AmazonMSK_playground-msk-stage-playground-msk-stage-another-user-secret"
|
57
|
+
)
|
58
|
+
] == [
|
59
|
+
TfResource(
|
60
|
+
address="aws_secretsmanager_secret.AmazonMSK_playground-msk-stage-playground-msk-stage"
|
61
|
+
),
|
62
|
+
TfResource(
|
63
|
+
address="aws_secretsmanager_secret.AmazonMSK_playground-msk-stage-playground-msk-stage-another-user"
|
64
|
+
),
|
65
|
+
]
|
66
|
+
|
67
|
+
assert terraform_resource_list[
|
68
|
+
TfResource(address="aws_secretsmanager_secret.secret-foobar-playground-user")
|
69
|
+
] == [
|
70
|
+
TfResource(address="aws_secretsmanager_secret.playground-user"),
|
71
|
+
TfResource(address="aws_secretsmanager_secret.foobar-playground-user"),
|
72
|
+
]
|
73
|
+
with pytest.raises(KeyError):
|
74
|
+
# unknown resource type
|
75
|
+
terraform_resource_list[TfResource(address="not.found")]
|
76
|
+
|
77
|
+
assert (
|
78
|
+
terraform_resource_list[TfResource(address="aws_secretsmanager_secret.found")]
|
79
|
+
== []
|
80
|
+
)
|
{qontract_reconcile-0.10.1rc1127.dist-info → qontract_reconcile-0.10.1rc1129.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|
File without changes
|