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.
Files changed (29) hide show
  1. {qontract_reconcile-0.10.2.dev504.dist-info → qontract_reconcile-0.10.2.dev506.dist-info}/METADATA +1 -4
  2. {qontract_reconcile-0.10.2.dev504.dist-info → qontract_reconcile-0.10.2.dev506.dist-info}/RECORD +11 -29
  3. reconcile/cli.py +0 -108
  4. reconcile/gql_definitions/integrations/integrations.py +1 -31
  5. reconcile/gql_definitions/introspection.json +530 -2107
  6. reconcile/integrations_manager.py +0 -2
  7. reconcile/utils/external_resource_spec.py +1 -2
  8. reconcile/utils/runtime/sharding.py +0 -80
  9. tools/cli_commands/systems_and_tools.py +0 -23
  10. reconcile/gql_definitions/terraform_cloudflare_dns/__init__.py +0 -0
  11. reconcile/gql_definitions/terraform_cloudflare_dns/app_interface_cloudflare_dns_settings.py +0 -62
  12. reconcile/gql_definitions/terraform_cloudflare_dns/terraform_cloudflare_zones.py +0 -193
  13. reconcile/gql_definitions/terraform_cloudflare_resources/__init__.py +0 -0
  14. reconcile/gql_definitions/terraform_cloudflare_resources/terraform_cloudflare_accounts.py +0 -127
  15. reconcile/gql_definitions/terraform_cloudflare_resources/terraform_cloudflare_resources.py +0 -359
  16. reconcile/gql_definitions/terraform_cloudflare_users/__init__.py +0 -0
  17. reconcile/gql_definitions/terraform_cloudflare_users/app_interface_setting_cloudflare_and_vault.py +0 -62
  18. reconcile/gql_definitions/terraform_cloudflare_users/terraform_cloudflare_roles.py +0 -139
  19. reconcile/terraform_cloudflare_dns.py +0 -379
  20. reconcile/terraform_cloudflare_resources.py +0 -445
  21. reconcile/terraform_cloudflare_users.py +0 -374
  22. reconcile/typed_queries/cloudflare.py +0 -10
  23. reconcile/utils/terrascript/__init__.py +0 -0
  24. reconcile/utils/terrascript/cloudflare_client.py +0 -310
  25. reconcile/utils/terrascript/cloudflare_resources.py +0 -432
  26. reconcile/utils/terrascript/models.py +0 -26
  27. reconcile/utils/terrascript/resources.py +0 -43
  28. {qontract_reconcile-0.10.2.dev504.dist-info → qontract_reconcile-0.10.2.dev506.dist-info}/WHEEL +0 -0
  29. {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