qontract-reconcile 0.10.2.dev504__py3-none-any.whl → 0.10.2.dev506__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.2.dev504.dist-info → qontract_reconcile-0.10.2.dev506.dist-info}/METADATA +1 -4
- {qontract_reconcile-0.10.2.dev504.dist-info → qontract_reconcile-0.10.2.dev506.dist-info}/RECORD +11 -29
- reconcile/cli.py +0 -108
- reconcile/gql_definitions/integrations/integrations.py +1 -31
- reconcile/gql_definitions/introspection.json +530 -2107
- reconcile/integrations_manager.py +0 -2
- reconcile/utils/external_resource_spec.py +1 -2
- reconcile/utils/runtime/sharding.py +0 -80
- tools/cli_commands/systems_and_tools.py +0 -23
- reconcile/gql_definitions/terraform_cloudflare_dns/__init__.py +0 -0
- reconcile/gql_definitions/terraform_cloudflare_dns/app_interface_cloudflare_dns_settings.py +0 -62
- reconcile/gql_definitions/terraform_cloudflare_dns/terraform_cloudflare_zones.py +0 -193
- reconcile/gql_definitions/terraform_cloudflare_resources/__init__.py +0 -0
- reconcile/gql_definitions/terraform_cloudflare_resources/terraform_cloudflare_accounts.py +0 -127
- reconcile/gql_definitions/terraform_cloudflare_resources/terraform_cloudflare_resources.py +0 -359
- reconcile/gql_definitions/terraform_cloudflare_users/__init__.py +0 -0
- reconcile/gql_definitions/terraform_cloudflare_users/app_interface_setting_cloudflare_and_vault.py +0 -62
- reconcile/gql_definitions/terraform_cloudflare_users/terraform_cloudflare_roles.py +0 -139
- reconcile/terraform_cloudflare_dns.py +0 -379
- reconcile/terraform_cloudflare_resources.py +0 -445
- reconcile/terraform_cloudflare_users.py +0 -374
- reconcile/typed_queries/cloudflare.py +0 -10
- reconcile/utils/terrascript/__init__.py +0 -0
- reconcile/utils/terrascript/cloudflare_client.py +0 -310
- reconcile/utils/terrascript/cloudflare_resources.py +0 -432
- reconcile/utils/terrascript/models.py +0 -26
- reconcile/utils/terrascript/resources.py +0 -43
- {qontract_reconcile-0.10.2.dev504.dist-info → qontract_reconcile-0.10.2.dev506.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.2.dev504.dist-info → qontract_reconcile-0.10.2.dev506.dist-info}/entry_points.txt +0 -0
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
import contextlib
|
|
2
|
-
import logging
|
|
3
|
-
import sys
|
|
4
|
-
from collections.abc import (
|
|
5
|
-
Callable,
|
|
6
|
-
Mapping,
|
|
7
|
-
Sequence,
|
|
8
|
-
)
|
|
9
|
-
from typing import Any
|
|
10
|
-
|
|
11
|
-
from deepdiff import DeepHash
|
|
12
|
-
|
|
13
|
-
from reconcile.gql_definitions.terraform_cloudflare_dns import (
|
|
14
|
-
app_interface_cloudflare_dns_settings,
|
|
15
|
-
terraform_cloudflare_zones,
|
|
16
|
-
)
|
|
17
|
-
from reconcile.gql_definitions.terraform_cloudflare_dns.app_interface_cloudflare_dns_settings import (
|
|
18
|
-
AppInterfaceSettingCloudflareDNSQueryData,
|
|
19
|
-
)
|
|
20
|
-
from reconcile.gql_definitions.terraform_cloudflare_dns.terraform_cloudflare_zones import (
|
|
21
|
-
CloudflareDnsRecordV1,
|
|
22
|
-
CloudflareDnsZoneQueryData,
|
|
23
|
-
CloudflareDnsZoneV1,
|
|
24
|
-
)
|
|
25
|
-
from reconcile.status import ExitCodes
|
|
26
|
-
from reconcile.utils import gql
|
|
27
|
-
from reconcile.utils.defer import defer
|
|
28
|
-
from reconcile.utils.external_resources import ExternalResourceSpec
|
|
29
|
-
from reconcile.utils.runtime.integration import (
|
|
30
|
-
DesiredStateShardConfig,
|
|
31
|
-
PydanticRunParams,
|
|
32
|
-
QontractReconcileIntegration,
|
|
33
|
-
)
|
|
34
|
-
from reconcile.utils.secret_reader import (
|
|
35
|
-
SecretReaderBase,
|
|
36
|
-
create_secret_reader,
|
|
37
|
-
)
|
|
38
|
-
from reconcile.utils.semver_helper import make_semver
|
|
39
|
-
from reconcile.utils.terraform.config_client import (
|
|
40
|
-
ClientAlreadyRegisteredError,
|
|
41
|
-
TerraformConfigClientCollection,
|
|
42
|
-
)
|
|
43
|
-
from reconcile.utils.terraform_client import TerraformClient
|
|
44
|
-
from reconcile.utils.terrascript.cloudflare_client import (
|
|
45
|
-
DEFAULT_PROVIDER_RPS,
|
|
46
|
-
DNSZoneShardingStrategy,
|
|
47
|
-
IntegrationUndefinedError,
|
|
48
|
-
InvalidTerraformStateError,
|
|
49
|
-
TerrascriptCloudflareClientFactory,
|
|
50
|
-
)
|
|
51
|
-
from reconcile.utils.terrascript.models import (
|
|
52
|
-
CloudflareAccount,
|
|
53
|
-
Integration,
|
|
54
|
-
TerraformStateS3,
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
DEFAULT_NAMESPACE: Mapping[str, Any] = {
|
|
58
|
-
"name": None,
|
|
59
|
-
"cluster": {"name": None},
|
|
60
|
-
"environment": {"name": None},
|
|
61
|
-
"app": {"name": None},
|
|
62
|
-
}
|
|
63
|
-
DEFAULT_PROVISIONER_PROVIDER = "cloudflare"
|
|
64
|
-
DEFAULT_PROVIDER = "zone"
|
|
65
|
-
DEFAULT_EXCLUDE_KEY = {
|
|
66
|
-
"account",
|
|
67
|
-
"max_records",
|
|
68
|
-
} # These two keys are added for App Interface, not part of Terraform resource specs.
|
|
69
|
-
DEFAULT_CLOUDFLARE_ZONE_RECORDS_MAX = 500
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
class TerraformCloudflareDNSIntegrationParams(PydanticRunParams):
|
|
73
|
-
enable_deletion: bool
|
|
74
|
-
thread_pool_size: int
|
|
75
|
-
selected_account: str | None = None
|
|
76
|
-
selected_zone: str | None = None
|
|
77
|
-
print_to_file: str | None
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
class TerraformCloudflareDNSIntegration(
|
|
81
|
-
QontractReconcileIntegration[TerraformCloudflareDNSIntegrationParams]
|
|
82
|
-
):
|
|
83
|
-
def __init__(self, params: TerraformCloudflareDNSIntegrationParams) -> None:
|
|
84
|
-
super().__init__(params)
|
|
85
|
-
self.qontract_integration = "terraform_cloudflare_dns"
|
|
86
|
-
self.qontract_integration_version = make_semver(0, 1, 0)
|
|
87
|
-
self.qontract_tf_prefix = "qrtfcfdns"
|
|
88
|
-
|
|
89
|
-
@property
|
|
90
|
-
def name(self) -> str:
|
|
91
|
-
return self.qontract_integration.replace("_", "-")
|
|
92
|
-
|
|
93
|
-
@defer
|
|
94
|
-
def run(self, dry_run: bool, defer: Callable | None = None) -> None:
|
|
95
|
-
settings = self._get_app_interface_settings()
|
|
96
|
-
|
|
97
|
-
if not settings.settings:
|
|
98
|
-
raise RuntimeError("App interface setting undefined.")
|
|
99
|
-
|
|
100
|
-
if settings.settings[0].vault is None:
|
|
101
|
-
raise RuntimeError("App interface vault setting undefined.")
|
|
102
|
-
|
|
103
|
-
default_max_records = (
|
|
104
|
-
settings.settings[0].cloudflare_dns_zone_max_records
|
|
105
|
-
or DEFAULT_CLOUDFLARE_ZONE_RECORDS_MAX
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
if not settings.settings[0].cloudflare_dns_zone_max_records:
|
|
109
|
-
logging.debug(
|
|
110
|
-
f"Setting the App Interface default Cloudflare DNS zone to the default {DEFAULT_CLOUDFLARE_ZONE_RECORDS_MAX}"
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
secret_reader = create_secret_reader(use_vault=settings.settings[0].vault)
|
|
114
|
-
|
|
115
|
-
query_zones = self._get_cloudflare_desired_state()
|
|
116
|
-
|
|
117
|
-
if not query_zones.zones:
|
|
118
|
-
sys.exit(ExitCodes.SUCCESS)
|
|
119
|
-
|
|
120
|
-
ensure_record_number_not_exceed_max(query_zones.zones, default_max_records)
|
|
121
|
-
|
|
122
|
-
if are_record_identifiers_duplicated_within_zone(query_zones):
|
|
123
|
-
logging.error("Duplicate DNS record identifier(s) detected.")
|
|
124
|
-
sys.exit(ExitCodes.ERROR)
|
|
125
|
-
|
|
126
|
-
# Build Cloudflare clients
|
|
127
|
-
cf_clients = build_cloudflare_terraform_config_collection(
|
|
128
|
-
secret_reader,
|
|
129
|
-
query_zones,
|
|
130
|
-
self.qontract_integration,
|
|
131
|
-
self.params.selected_account,
|
|
132
|
-
self.params.selected_zone,
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
zone_external_resource_specs = cloudflare_dns_zone_to_external_resource(
|
|
136
|
-
query_zones.zones
|
|
137
|
-
)
|
|
138
|
-
cf_specs = [
|
|
139
|
-
spec
|
|
140
|
-
for spec in zone_external_resource_specs
|
|
141
|
-
if not self.params.selected_account
|
|
142
|
-
or spec.provisioner_name == self.params.selected_account
|
|
143
|
-
if not self.params.selected_zone
|
|
144
|
-
or spec.identifier == self.params.selected_zone
|
|
145
|
-
]
|
|
146
|
-
|
|
147
|
-
cf_clients.add_specs(cf_specs)
|
|
148
|
-
|
|
149
|
-
cf_clients.populate_resources()
|
|
150
|
-
|
|
151
|
-
working_dirs = cf_clients.dump(print_to_file=self.params.print_to_file)
|
|
152
|
-
|
|
153
|
-
if self.params.print_to_file:
|
|
154
|
-
sys.exit(ExitCodes.SUCCESS)
|
|
155
|
-
|
|
156
|
-
accts_per_zone = []
|
|
157
|
-
for zone in query_zones.zones or []:
|
|
158
|
-
acct = zone.account.model_dump(by_alias=True)
|
|
159
|
-
acct["name"] = f"{zone.account.name}-{zone.identifier}"
|
|
160
|
-
accts_per_zone.append(acct)
|
|
161
|
-
|
|
162
|
-
tf = TerraformClient(
|
|
163
|
-
self.qontract_integration,
|
|
164
|
-
self.qontract_integration_version,
|
|
165
|
-
self.qontract_tf_prefix,
|
|
166
|
-
accts_per_zone,
|
|
167
|
-
working_dirs,
|
|
168
|
-
self.params.thread_pool_size,
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
if defer:
|
|
172
|
-
defer(tf.cleanup)
|
|
173
|
-
|
|
174
|
-
disabled_deletions_detected, err = tf.plan(self.params.enable_deletion)
|
|
175
|
-
if err:
|
|
176
|
-
sys.exit(ExitCodes.ERROR)
|
|
177
|
-
if disabled_deletions_detected:
|
|
178
|
-
logging.error("Deletions detected but they are disabled")
|
|
179
|
-
sys.exit(ExitCodes.ERROR)
|
|
180
|
-
|
|
181
|
-
if dry_run:
|
|
182
|
-
sys.exit(ExitCodes.SUCCESS)
|
|
183
|
-
|
|
184
|
-
err = tf.apply()
|
|
185
|
-
if err:
|
|
186
|
-
sys.exit(ExitCodes.ERROR)
|
|
187
|
-
|
|
188
|
-
def _get_cloudflare_desired_state(self) -> CloudflareDnsZoneQueryData:
|
|
189
|
-
query_zones = terraform_cloudflare_zones.query(query_func=gql.get_api().query)
|
|
190
|
-
logging.debug(query_zones)
|
|
191
|
-
|
|
192
|
-
return query_zones
|
|
193
|
-
|
|
194
|
-
def _get_app_interface_settings(self) -> AppInterfaceSettingCloudflareDNSQueryData:
|
|
195
|
-
query_app_interface_settings = app_interface_cloudflare_dns_settings.query(
|
|
196
|
-
query_func=gql.get_api().query
|
|
197
|
-
)
|
|
198
|
-
logging.debug(query_app_interface_settings)
|
|
199
|
-
|
|
200
|
-
return query_app_interface_settings
|
|
201
|
-
|
|
202
|
-
def get_early_exit_desired_state(
|
|
203
|
-
self, *args: tuple, **kwargs: dict[str, Any]
|
|
204
|
-
) -> dict[str, Any]:
|
|
205
|
-
desired_state = self._get_cloudflare_desired_state()
|
|
206
|
-
|
|
207
|
-
return {
|
|
208
|
-
"state": {
|
|
209
|
-
z.identifier: {"shard": z.identifier, "hash": DeepHash(z).get(z)}
|
|
210
|
-
for z in desired_state.zones or []
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
def get_desired_state_shard_config(self) -> DesiredStateShardConfig:
|
|
215
|
-
return DesiredStateShardConfig(
|
|
216
|
-
shard_arg_name="selected_zone",
|
|
217
|
-
shard_path_selectors={"state.*.shard"},
|
|
218
|
-
sharded_run_review=lambda proposal: len(proposal.proposed_shards) <= 2,
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
def are_record_identifiers_duplicated_within_zone(
|
|
223
|
-
zone_query_data: CloudflareDnsZoneQueryData,
|
|
224
|
-
) -> bool:
|
|
225
|
-
duplicate_exist = False
|
|
226
|
-
for zone in zone_query_data.zones or []:
|
|
227
|
-
existing_records = set()
|
|
228
|
-
for record in zone.records or []:
|
|
229
|
-
record_id = record.identifier
|
|
230
|
-
if record_id not in existing_records:
|
|
231
|
-
existing_records.add(record_id)
|
|
232
|
-
else:
|
|
233
|
-
logging.warning(f"{record_id} already exists in zone {zone.identifier}")
|
|
234
|
-
duplicate_exist = True
|
|
235
|
-
return duplicate_exist
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
def ensure_record_number_not_exceed_max(
|
|
239
|
-
zones: list[CloudflareDnsZoneV1], default_max_records: int
|
|
240
|
-
) -> None:
|
|
241
|
-
for zone in zones:
|
|
242
|
-
if not zone.records:
|
|
243
|
-
continue
|
|
244
|
-
num_records = len(zone.records)
|
|
245
|
-
if not zone.max_records:
|
|
246
|
-
max_records = default_max_records
|
|
247
|
-
logging.debug(
|
|
248
|
-
f"Setting max_records for zone {zone.identifier} to the default max records {default_max_records}"
|
|
249
|
-
)
|
|
250
|
-
else:
|
|
251
|
-
max_records = zone.max_records
|
|
252
|
-
if max_records < num_records:
|
|
253
|
-
raise RuntimeError(
|
|
254
|
-
f"The number of records ({num_records}) in zone {zone.identifier} exceeds the configured max_items: {max_records}"
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def get_cloudflare_provider_rps(
|
|
259
|
-
records: Sequence[CloudflareDnsRecordV1] | None,
|
|
260
|
-
) -> int:
|
|
261
|
-
"""
|
|
262
|
-
Setting Cloudlare Terraform provider's RPS based on the size of the zone to improve performance of MR checks.
|
|
263
|
-
Specifically it was observed that 1000 records zone will result in around 250 seconds build time, and it become
|
|
264
|
-
problematic for MR merge throughput when exceeding 5 minutes. Therefore setting rps lower for smaller zone to
|
|
265
|
-
save throttle quota, and higher for the large zones so MR checks won't take more than 250 seconds.
|
|
266
|
-
"""
|
|
267
|
-
|
|
268
|
-
if not records:
|
|
269
|
-
return DEFAULT_PROVIDER_RPS
|
|
270
|
-
size = len(records)
|
|
271
|
-
return min(-(-size // 50), DEFAULT_PROVIDER_RPS)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def build_cloudflare_terraform_config_collection(
|
|
275
|
-
secret_reader: SecretReaderBase,
|
|
276
|
-
query_zones: CloudflareDnsZoneQueryData,
|
|
277
|
-
qontract_integration: str,
|
|
278
|
-
selected_account: str | None,
|
|
279
|
-
selected_zone: str | None,
|
|
280
|
-
) -> TerraformConfigClientCollection:
|
|
281
|
-
cf_clients = TerraformConfigClientCollection()
|
|
282
|
-
cf_accounts: dict[str, CloudflareAccount] = {}
|
|
283
|
-
for zone in query_zones.zones or []:
|
|
284
|
-
cf_acct = zone.account
|
|
285
|
-
cf_acct_name = cf_acct.name
|
|
286
|
-
|
|
287
|
-
if selected_account and cf_acct_name != selected_account:
|
|
288
|
-
continue
|
|
289
|
-
if selected_zone and zone.identifier != selected_zone:
|
|
290
|
-
continue
|
|
291
|
-
|
|
292
|
-
if cf_acct_name in cf_accounts:
|
|
293
|
-
cf_account = cf_accounts[cf_acct_name]
|
|
294
|
-
else:
|
|
295
|
-
cf_account = CloudflareAccount(
|
|
296
|
-
cf_acct_name,
|
|
297
|
-
zone.account.api_credentials,
|
|
298
|
-
zone.account.enforce_twofactor,
|
|
299
|
-
zone.account.q_type,
|
|
300
|
-
zone.account.provider_version,
|
|
301
|
-
)
|
|
302
|
-
cf_accounts[cf_acct_name] = cf_account
|
|
303
|
-
|
|
304
|
-
tf_state = zone.account.terraform_state_account.terraform_state
|
|
305
|
-
if not tf_state:
|
|
306
|
-
raise ValueError(
|
|
307
|
-
f"AWS account {zone.account.terraform_state_account.name} cannot be used for Cloudflare "
|
|
308
|
-
f"account {cf_account.name} because it does not define a Terraform state "
|
|
309
|
-
)
|
|
310
|
-
bucket = tf_state.bucket
|
|
311
|
-
region = tf_state.region
|
|
312
|
-
integrations = tf_state.integrations
|
|
313
|
-
|
|
314
|
-
if not bucket:
|
|
315
|
-
raise InvalidTerraformStateError("Terraform state must have bucket defined")
|
|
316
|
-
if not region:
|
|
317
|
-
raise InvalidTerraformStateError("Terraform state must have region defined")
|
|
318
|
-
|
|
319
|
-
integration = None
|
|
320
|
-
for i in integrations:
|
|
321
|
-
if i.integration.replace("-", "_") == qontract_integration:
|
|
322
|
-
integration = i
|
|
323
|
-
break
|
|
324
|
-
|
|
325
|
-
if not integration:
|
|
326
|
-
raise IntegrationUndefinedError(
|
|
327
|
-
f"Must declare integration name under Terraform state in {zone.account.terraform_state_account.name} AWS account for {cf_account.name} Cloudflare account in app-interface"
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
tf_state_s3 = TerraformStateS3(
|
|
331
|
-
zone.account.terraform_state_account.automation_token,
|
|
332
|
-
bucket,
|
|
333
|
-
region,
|
|
334
|
-
Integration(integration.integration.replace("-", "_"), integration.key),
|
|
335
|
-
)
|
|
336
|
-
|
|
337
|
-
rps = get_cloudflare_provider_rps(zone.records)
|
|
338
|
-
|
|
339
|
-
client = TerrascriptCloudflareClientFactory.get_client(
|
|
340
|
-
tf_state_s3,
|
|
341
|
-
cf_account,
|
|
342
|
-
DNSZoneShardingStrategy(cf_account, zone.identifier),
|
|
343
|
-
secret_reader,
|
|
344
|
-
False,
|
|
345
|
-
rps,
|
|
346
|
-
)
|
|
347
|
-
|
|
348
|
-
with contextlib.suppress(ClientAlreadyRegisteredError):
|
|
349
|
-
cf_clients.register_client(f"{cf_account.name}-{zone.identifier}", client)
|
|
350
|
-
|
|
351
|
-
return cf_clients
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
def cloudflare_dns_zone_to_external_resource(
|
|
355
|
-
zones: list[CloudflareDnsZoneV1] | None,
|
|
356
|
-
) -> list[ExternalResourceSpec]:
|
|
357
|
-
"""
|
|
358
|
-
This is a method that massage a list of CloudflareDnsZoneV1 into ExternalResourceSpec
|
|
359
|
-
by filling in some fake namespace data. It is needed because cloudflare_client's add_spec
|
|
360
|
-
method only takes ExternalResourceSpec, which was designed that way since most of our
|
|
361
|
-
cloud resource is tied to a namespace. If more use cases like this come up,
|
|
362
|
-
we can add new classes for this purpose using Adapter pattern
|
|
363
|
-
"""
|
|
364
|
-
external_resource_specs: list[ExternalResourceSpec] = []
|
|
365
|
-
for zone in zones or []:
|
|
366
|
-
if zone.delete:
|
|
367
|
-
continue
|
|
368
|
-
external_resource_spec = ExternalResourceSpec(
|
|
369
|
-
provision_provider=DEFAULT_PROVISIONER_PROVIDER,
|
|
370
|
-
provisioner={"name": f"{zone.account.name}-{zone.identifier}"},
|
|
371
|
-
namespace=DEFAULT_NAMESPACE,
|
|
372
|
-
resource=zone.model_dump(by_alias=True, exclude=DEFAULT_EXCLUDE_KEY),
|
|
373
|
-
)
|
|
374
|
-
external_resource_spec.resource["provider"] = DEFAULT_PROVIDER
|
|
375
|
-
external_resource_spec.resource["records"] = [
|
|
376
|
-
record.model_dump(by_alias=True) for record in zone.records or []
|
|
377
|
-
]
|
|
378
|
-
external_resource_specs.append(external_resource_spec)
|
|
379
|
-
return external_resource_specs
|