qontract-reconcile 0.10.2.dev504__py3-none-any.whl → 0.10.2.dev505__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 (28) hide show
  1. {qontract_reconcile-0.10.2.dev504.dist-info → qontract_reconcile-0.10.2.dev505.dist-info}/METADATA +1 -4
  2. {qontract_reconcile-0.10.2.dev504.dist-info → qontract_reconcile-0.10.2.dev505.dist-info}/RECORD +10 -28
  3. reconcile/cli.py +0 -108
  4. reconcile/gql_definitions/integrations/integrations.py +1 -31
  5. reconcile/integrations_manager.py +0 -2
  6. reconcile/utils/external_resource_spec.py +1 -2
  7. reconcile/utils/runtime/sharding.py +0 -80
  8. tools/cli_commands/systems_and_tools.py +0 -23
  9. reconcile/gql_definitions/terraform_cloudflare_dns/__init__.py +0 -0
  10. reconcile/gql_definitions/terraform_cloudflare_dns/app_interface_cloudflare_dns_settings.py +0 -62
  11. reconcile/gql_definitions/terraform_cloudflare_dns/terraform_cloudflare_zones.py +0 -193
  12. reconcile/gql_definitions/terraform_cloudflare_resources/__init__.py +0 -0
  13. reconcile/gql_definitions/terraform_cloudflare_resources/terraform_cloudflare_accounts.py +0 -127
  14. reconcile/gql_definitions/terraform_cloudflare_resources/terraform_cloudflare_resources.py +0 -359
  15. reconcile/gql_definitions/terraform_cloudflare_users/__init__.py +0 -0
  16. reconcile/gql_definitions/terraform_cloudflare_users/app_interface_setting_cloudflare_and_vault.py +0 -62
  17. reconcile/gql_definitions/terraform_cloudflare_users/terraform_cloudflare_roles.py +0 -139
  18. reconcile/terraform_cloudflare_dns.py +0 -379
  19. reconcile/terraform_cloudflare_resources.py +0 -445
  20. reconcile/terraform_cloudflare_users.py +0 -374
  21. reconcile/typed_queries/cloudflare.py +0 -10
  22. reconcile/utils/terrascript/__init__.py +0 -0
  23. reconcile/utils/terrascript/cloudflare_client.py +0 -310
  24. reconcile/utils/terrascript/cloudflare_resources.py +0 -432
  25. reconcile/utils/terrascript/models.py +0 -26
  26. reconcile/utils/terrascript/resources.py +0 -43
  27. {qontract_reconcile-0.10.2.dev504.dist-info → qontract_reconcile-0.10.2.dev505.dist-info}/WHEEL +0 -0
  28. {qontract_reconcile-0.10.2.dev504.dist-info → qontract_reconcile-0.10.2.dev505.dist-info}/entry_points.txt +0 -0
@@ -1,445 +0,0 @@
1
- import logging
2
- import sys
3
- from collections.abc import Callable, Iterable
4
- from typing import (
5
- Any,
6
- )
7
-
8
- from sretoolbox.utils import threaded
9
-
10
- from reconcile.gql_definitions.terraform_cloudflare_resources import (
11
- terraform_cloudflare_accounts,
12
- terraform_cloudflare_resources,
13
- )
14
- from reconcile.gql_definitions.terraform_cloudflare_resources.terraform_cloudflare_accounts import (
15
- AWSAccountV1,
16
- CloudflareAccountV1,
17
- TerraformCloudflareAccountsQueryData,
18
- )
19
- from reconcile.gql_definitions.terraform_cloudflare_resources.terraform_cloudflare_resources import (
20
- NamespaceTerraformProviderResourceCloudflareV1,
21
- NamespaceV1,
22
- TerraformCloudflareResourcesQueryData,
23
- )
24
- from reconcile.openshift_base import (
25
- CurrentStateSpec,
26
- init_specs_to_fetch,
27
- realize_data,
28
- )
29
- from reconcile.status import ExitCodes
30
- from reconcile.typed_queries.app_interface_vault_settings import (
31
- get_app_interface_vault_settings,
32
- )
33
- from reconcile.utils import gql
34
- from reconcile.utils.defer import defer
35
- from reconcile.utils.exceptions import SecretIncompleteError
36
- from reconcile.utils.external_resource_spec import ExternalResourceSpecInventory
37
- from reconcile.utils.external_resources import (
38
- PROVIDER_CLOUDFLARE,
39
- get_external_resource_specs,
40
- publish_metrics,
41
- )
42
- from reconcile.utils.oc import StatusCodeError
43
- from reconcile.utils.oc_map import (
44
- OCMap,
45
- init_oc_map_from_namespaces,
46
- )
47
- from reconcile.utils.openshift_resource import (
48
- OpenshiftResource,
49
- ResourceInventory,
50
- )
51
- from reconcile.utils.secret_reader import (
52
- SecretReaderBase,
53
- create_secret_reader,
54
- )
55
- from reconcile.utils.semver_helper import make_semver
56
- from reconcile.utils.terraform.config_client import TerraformConfigClientCollection
57
- from reconcile.utils.terraform_client import TerraformClient
58
- from reconcile.utils.terrascript.cloudflare_client import (
59
- DEFAULT_CLOUDFLARE_ACCOUNT_2FA,
60
- DEFAULT_CLOUDFLARE_ACCOUNT_TYPE,
61
- CloudflareAccountConfig,
62
- TerraformS3BackendConfig,
63
- TerrascriptCloudflareClient,
64
- create_cloudflare_terrascript,
65
- )
66
- from reconcile.utils.vault import (
67
- VaultClient,
68
- )
69
-
70
- QONTRACT_INTEGRATION = "terraform_cloudflare_resources"
71
- QONTRACT_INTEGRATION_VERSION = make_semver(0, 1, 0)
72
- QONTRACT_TF_PREFIX = "qrtfcf"
73
-
74
-
75
- def create_backend_config(
76
- secret_reader: SecretReaderBase,
77
- aws_acct: AWSAccountV1,
78
- cf_acct: CloudflareAccountV1,
79
- ) -> TerraformS3BackendConfig:
80
- aws_acct_creds = secret_reader.read_all_secret(aws_acct.automation_token)
81
-
82
- # default from AWS account file
83
- tf_state = aws_acct.terraform_state
84
- if tf_state is None:
85
- raise ValueError(
86
- f"AWS account {aws_acct.name} cannot be used for Cloudflare "
87
- f"account {cf_acct.name} because it does define a terraform state "
88
- )
89
-
90
- integrations = tf_state.integrations or []
91
- bucket_key = bucket_name = bucket_region = None
92
- for i in integrations or []:
93
- name = i.integration
94
- if name.replace("-", "_") == QONTRACT_INTEGRATION:
95
- # we have to ensure the bucket key(file) is unique across
96
- # Cloudflare accounts to support running per-account
97
- bucket_key = f"{QONTRACT_INTEGRATION}-{cf_acct.name}.tfstate"
98
- bucket_name = tf_state.bucket
99
- bucket_region = tf_state.region
100
- break
101
-
102
- if bucket_name and bucket_key and bucket_region:
103
- backend_config = TerraformS3BackendConfig(
104
- aws_acct_creds["aws_access_key_id"],
105
- aws_acct_creds["aws_secret_access_key"],
106
- bucket_name,
107
- bucket_key,
108
- bucket_region,
109
- )
110
- else:
111
- raise ValueError(f"No state bucket config found for account {aws_acct.name}")
112
-
113
- return backend_config
114
-
115
-
116
- def build_clients(
117
- secret_reader: SecretReaderBase,
118
- query_accounts: TerraformCloudflareAccountsQueryData,
119
- selected_account: str | None = None,
120
- ) -> list[tuple[str, TerrascriptCloudflareClient]]:
121
- clients = []
122
- for cf_acct in query_accounts.accounts or []:
123
- if selected_account and cf_acct.name != selected_account:
124
- continue
125
- cf_acct_creds = secret_reader.read_all_secret(cf_acct.api_credentials)
126
- if not cf_acct_creds.get("api_token") or not cf_acct_creds.get("account_id"):
127
- raise SecretIncompleteError(
128
- f"secret {cf_acct.api_credentials.path} incomplete: api_token and/or account_id missing"
129
- )
130
- cf_acct_config = CloudflareAccountConfig(
131
- cf_acct.name,
132
- cf_acct_creds["api_token"],
133
- cf_acct_creds["account_id"],
134
- cf_acct.enforce_twofactor or DEFAULT_CLOUDFLARE_ACCOUNT_2FA,
135
- cf_acct.q_type or DEFAULT_CLOUDFLARE_ACCOUNT_TYPE,
136
- )
137
-
138
- aws_acct = cf_acct.terraform_state_account
139
- aws_backend_config = create_backend_config(secret_reader, aws_acct, cf_acct)
140
-
141
- ts_config = create_cloudflare_terrascript(
142
- cf_acct_config,
143
- aws_backend_config,
144
- cf_acct.provider_version,
145
- )
146
-
147
- ts_client = TerrascriptCloudflareClient(ts_config)
148
- clients.append((cf_acct.name, ts_client))
149
- return clients
150
-
151
-
152
- def _build_oc_resources(
153
- cloudflare_namespaces: Iterable[NamespaceV1],
154
- secret_reader: SecretReaderBase,
155
- use_jump_host: bool,
156
- thread_pool_size: int,
157
- internal: bool | None = None,
158
- account_names: Iterable[str] | None = None,
159
- ) -> tuple[ResourceInventory, OCMap]:
160
- ri = ResourceInventory()
161
-
162
- oc_map = init_oc_map_from_namespaces(
163
- cloudflare_namespaces,
164
- secret_reader,
165
- integration=QONTRACT_INTEGRATION,
166
- use_jump_host=use_jump_host,
167
- thread_pool_size=thread_pool_size,
168
- internal=internal,
169
- )
170
-
171
- namespace_mapping = [ns.model_dump() for ns in cloudflare_namespaces]
172
-
173
- state_specs = init_specs_to_fetch(
174
- ri, oc_map, namespaces=namespace_mapping, override_managed_types=["Secret"]
175
- )
176
- current_state_specs: list[CurrentStateSpec] = [
177
- s for s in state_specs if isinstance(s, CurrentStateSpec)
178
- ]
179
- threaded.run(
180
- _populate_oc_resources,
181
- current_state_specs,
182
- thread_pool_size,
183
- ri=ri,
184
- account_names=account_names,
185
- )
186
-
187
- return ri, oc_map
188
-
189
-
190
- def _populate_oc_resources(
191
- spec: CurrentStateSpec,
192
- ri: ResourceInventory,
193
- account_names: Iterable[str] | None,
194
- ) -> None:
195
- """
196
- This was taken from terraform_resources and might be a later candidate for DRY.
197
- """
198
- if spec.oc is None:
199
- return
200
- logging.debug(
201
- "[populate_oc_resources] cluster: "
202
- + spec.cluster
203
- + " namespace: "
204
- + spec.namespace
205
- + " resource: "
206
- + spec.kind
207
- )
208
-
209
- try:
210
- for item in spec.oc.get_items(spec.kind, namespace=spec.namespace):
211
- openshift_resource = OpenshiftResource(
212
- item, QONTRACT_INTEGRATION, QONTRACT_INTEGRATION_VERSION
213
- )
214
- if account_names:
215
- caller = openshift_resource.caller
216
- if caller and caller not in account_names:
217
- continue
218
-
219
- ri.add_current(
220
- spec.cluster,
221
- spec.namespace,
222
- spec.kind,
223
- openshift_resource.name,
224
- openshift_resource,
225
- )
226
- except StatusCodeError as e:
227
- ri.register_error(cluster=spec.cluster)
228
- msg = "cluster: {},"
229
- msg += "namespace: {},"
230
- msg += "resource: {},"
231
- msg += "exception: {}"
232
- msg = msg.format(spec.cluster, spec.namespace, spec.kind, str(e))
233
- logging.error(msg)
234
-
235
-
236
- def _populate_desired_state(
237
- ri: ResourceInventory, resource_specs: ExternalResourceSpecInventory
238
- ) -> None:
239
- for spec in resource_specs.values():
240
- if ri.is_cluster_present(spec.cluster_name):
241
- oc_resource = spec.build_oc_secret(
242
- QONTRACT_INTEGRATION, QONTRACT_INTEGRATION_VERSION
243
- )
244
-
245
- if not oc_resource.body.get("data"):
246
- logging.debug(
247
- "Skipping oc_resource %s because there is no Secret data (not all resources have outputs)",
248
- oc_resource.name,
249
- )
250
- continue
251
-
252
- ri.add_desired(
253
- cluster=spec.cluster_name,
254
- namespace=spec.namespace_name,
255
- resource_type=oc_resource.kind,
256
- name=spec.output_resource_name,
257
- value=oc_resource,
258
- privileged=spec.namespace.get("clusterAdmin") or False,
259
- )
260
-
261
-
262
- def _write_external_resource_secrets_to_vault(
263
- vault_path: str,
264
- resource_specs: ExternalResourceSpecInventory,
265
- integration_name: str,
266
- ) -> None:
267
- """
268
- Write the secrets associated with an external resource to Vault. This was taken
269
- from terraform-resources with minor modifications. We can consider moving this to a
270
- separate module if we have additional needs for a similar function.
271
- """
272
- integration_name = integration_name.replace("_", "-")
273
- vault_client = VaultClient.get_instance()
274
- for spec in resource_specs.values():
275
- # A secret can be empty if the terraform-* integrations are not enabled on the cluster
276
- # the resource is defined on - lets skip vault writes for those right now and
277
- # give this more thought - e.g. not processing such specs at all when the integration
278
- # is disabled
279
- if spec.secret:
280
- secret_path = f"{vault_path}/{integration_name}/{spec.cluster_name}/{spec.namespace_name}/{spec.output_resource_name}"
281
- # vault only stores strings as values - by converting to str upfront, we can compare current to desired
282
- stringified_secret = {k: str(v) for k, v in spec.secret.items()}
283
- desired_secret = {"path": secret_path, "data": stringified_secret}
284
- vault_client.write(desired_secret, decode_base64=False)
285
-
286
-
287
- def _filter_cloudflare_namespaces(
288
- namespaces: Iterable[NamespaceV1], account_names: set[str]
289
- ) -> list[NamespaceV1]:
290
- """
291
- Get only the namespaces that have Cloudflare resources and that match account_names.
292
- """
293
- cloudflare_namespaces: list[NamespaceV1] = []
294
- for ns in namespaces:
295
- if ns.external_resources:
296
- for resource in ns.external_resources:
297
- if isinstance(resource, NamespaceTerraformProviderResourceCloudflareV1):
298
- if (
299
- resource.provider == PROVIDER_CLOUDFLARE
300
- and resource.provisioner.name in account_names
301
- ):
302
- cloudflare_namespaces.append(ns)
303
- return cloudflare_namespaces
304
-
305
-
306
- @defer
307
- def run(
308
- dry_run: bool,
309
- print_to_file: str | None,
310
- enable_deletion: bool,
311
- thread_pool_size: int,
312
- selected_account: str | None = None,
313
- vault_output_path: str = "",
314
- internal: bool | None = None,
315
- use_jump_host: bool = True,
316
- defer: Callable | None = None,
317
- ) -> None:
318
- vault_settings = get_app_interface_vault_settings()
319
- secret_reader = create_secret_reader(use_vault=vault_settings.vault)
320
-
321
- query_accounts, query_resources = _get_cloudflare_desired_state()
322
-
323
- if not query_accounts.accounts:
324
- logging.info("No Cloudflare accounts were detected, nothing to do.")
325
- sys.exit(ExitCodes.SUCCESS)
326
-
327
- if not query_resources.namespaces:
328
- logging.info("No namespaces were detected, nothing to do.")
329
- sys.exit(ExitCodes.SUCCESS)
330
-
331
- if selected_account:
332
- account_names = [selected_account]
333
- else:
334
- account_names = [acct.name for acct in query_accounts.accounts]
335
-
336
- cloudflare_namespaces = _filter_cloudflare_namespaces(
337
- query_resources.namespaces, set(account_names)
338
- )
339
-
340
- if not cloudflare_namespaces:
341
- logging.debug("No cloudflare namespaces were detected, nothing to do.")
342
- sys.exit(ExitCodes.SUCCESS)
343
-
344
- # Build Cloudflare clients
345
- cf_clients = TerraformConfigClientCollection()
346
- for client in build_clients(secret_reader, query_accounts, selected_account):
347
- cf_clients.register_client(*client)
348
-
349
- # Register Cloudflare resources
350
- cf_specs = [
351
- spec
352
- for namespace in query_resources.namespaces
353
- for spec in get_external_resource_specs(
354
- namespace.model_dump(by_alias=True), PROVIDER_CLOUDFLARE
355
- )
356
- if not selected_account or spec.provisioner_name == selected_account
357
- ]
358
- cf_clients.add_specs(cf_specs)
359
-
360
- cf_clients.populate_resources()
361
-
362
- publish_metrics(cf_clients.resource_spec_inventory, QONTRACT_INTEGRATION)
363
-
364
- ri, oc_map = _build_oc_resources(
365
- cloudflare_namespaces,
366
- secret_reader,
367
- use_jump_host=use_jump_host,
368
- thread_pool_size=thread_pool_size,
369
- internal=internal,
370
- account_names=account_names,
371
- )
372
-
373
- if defer:
374
- defer(oc_map.cleanup)
375
-
376
- working_dirs = cf_clients.dump(print_to_file=print_to_file)
377
-
378
- if print_to_file:
379
- sys.exit(ExitCodes.SUCCESS)
380
-
381
- tf = TerraformClient(
382
- QONTRACT_INTEGRATION,
383
- QONTRACT_INTEGRATION_VERSION,
384
- QONTRACT_TF_PREFIX,
385
- [
386
- acct.model_dump(by_alias=True) # convert CloudflareAccountV1 to dict
387
- for acct in query_accounts.accounts or []
388
- if acct.name in cf_clients.dump() # use only if it is a registered client
389
- ],
390
- working_dirs,
391
- thread_pool_size,
392
- )
393
- if defer:
394
- defer(tf.cleanup)
395
-
396
- disabled_deletions_detected, err = tf.plan(enable_deletion)
397
- if err:
398
- sys.exit(ExitCodes.ERROR)
399
- if disabled_deletions_detected:
400
- logging.error("Deletions detected but they are disabled")
401
- sys.exit(ExitCodes.ERROR)
402
-
403
- if dry_run:
404
- sys.exit(ExitCodes.SUCCESS)
405
-
406
- err = tf.apply()
407
- if err:
408
- sys.exit(ExitCodes.ERROR)
409
-
410
- # refresh output data after terraform apply
411
- tf.populate_terraform_output_secrets(
412
- resource_specs=cf_clients.resource_spec_inventory
413
- )
414
-
415
- # populate the resource inventory with latest output data
416
- _populate_desired_state(ri, cf_clients.resource_spec_inventory)
417
-
418
- actions = realize_data(
419
- dry_run, oc_map, ri, thread_pool_size, caller=selected_account
420
- )
421
-
422
- if actions and vault_output_path:
423
- _write_external_resource_secrets_to_vault(
424
- vault_output_path,
425
- cf_clients.resource_spec_inventory,
426
- QONTRACT_INTEGRATION.replace("_", "-"),
427
- )
428
-
429
-
430
- def _get_cloudflare_desired_state() -> tuple[
431
- TerraformCloudflareAccountsQueryData,
432
- TerraformCloudflareResourcesQueryData,
433
- ]:
434
- query_accounts = terraform_cloudflare_accounts.query(query_func=gql.get_api().query)
435
- query_resources = terraform_cloudflare_resources.query(
436
- query_func=gql.get_api().query
437
- )
438
-
439
- return query_accounts, query_resources
440
-
441
-
442
- def early_exit_desired_state(*args: Any, **kwargs: Any) -> dict[str, Any]:
443
- desired_state = _get_cloudflare_desired_state()
444
-
445
- return {str(state): state.model_dump() for state in desired_state}