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,374 +0,0 @@
|
|
|
1
|
-
import contextlib
|
|
2
|
-
from collections.abc import (
|
|
3
|
-
Iterable,
|
|
4
|
-
Mapping,
|
|
5
|
-
MutableMapping,
|
|
6
|
-
)
|
|
7
|
-
from dataclasses import dataclass
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
from reconcile.gql_definitions.terraform_cloudflare_users import (
|
|
11
|
-
app_interface_setting_cloudflare_and_vault,
|
|
12
|
-
terraform_cloudflare_roles,
|
|
13
|
-
)
|
|
14
|
-
from reconcile.gql_definitions.terraform_cloudflare_users.app_interface_setting_cloudflare_and_vault import (
|
|
15
|
-
AppInterfaceSettingCloudflareAndVaultQueryData,
|
|
16
|
-
)
|
|
17
|
-
from reconcile.gql_definitions.terraform_cloudflare_users.terraform_cloudflare_roles import (
|
|
18
|
-
CloudflareAccountRoleQueryData,
|
|
19
|
-
CloudflareAccountRoleV1,
|
|
20
|
-
)
|
|
21
|
-
from reconcile.utils import gql
|
|
22
|
-
from reconcile.utils.external_resource_spec import ExternalResourceSpec
|
|
23
|
-
from reconcile.utils.runtime.integration import (
|
|
24
|
-
PydanticRunParams,
|
|
25
|
-
QontractReconcileIntegration,
|
|
26
|
-
)
|
|
27
|
-
from reconcile.utils.secret_reader import (
|
|
28
|
-
SecretReaderBase,
|
|
29
|
-
create_secret_reader,
|
|
30
|
-
)
|
|
31
|
-
from reconcile.utils.semver_helper import make_semver
|
|
32
|
-
from reconcile.utils.terraform import safe_resource_id
|
|
33
|
-
from reconcile.utils.terraform.config_client import (
|
|
34
|
-
ClientAlreadyRegisteredError,
|
|
35
|
-
TerraformConfigClientCollection,
|
|
36
|
-
)
|
|
37
|
-
from reconcile.utils.terraform_client import (
|
|
38
|
-
TerraformApplyFailedError,
|
|
39
|
-
TerraformClient,
|
|
40
|
-
TerraformDeletionDetectedError,
|
|
41
|
-
TerraformPlanFailedError,
|
|
42
|
-
)
|
|
43
|
-
from reconcile.utils.terrascript.cloudflare_client import (
|
|
44
|
-
AccountShardingStrategy,
|
|
45
|
-
IntegrationUndefinedError,
|
|
46
|
-
InvalidTerraformStateError,
|
|
47
|
-
TerrascriptCloudflareClientFactory,
|
|
48
|
-
)
|
|
49
|
-
from reconcile.utils.terrascript.models import (
|
|
50
|
-
CloudflareAccount,
|
|
51
|
-
Integration,
|
|
52
|
-
TerraformStateS3,
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
QONTRACT_INTEGRATION = "terraform_cloudflare_users"
|
|
56
|
-
QONTRACT_INTEGRATION_VERSION = make_semver(0, 1, 0)
|
|
57
|
-
QONTRACT_TF_PREFIX = "qrtfcfusers"
|
|
58
|
-
CLOUDFLARE_EMAIL_DOMAIN_ALLOW_LIST_KEY = "cloudflareEmailDomainAllowList"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@dataclass
|
|
62
|
-
class CloudflareUser:
|
|
63
|
-
email_address: str
|
|
64
|
-
account_name: str
|
|
65
|
-
org_username: str
|
|
66
|
-
roles: set[str]
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
class TerraformCloudflareUsersParams(PydanticRunParams):
|
|
70
|
-
print_to_file: str | None
|
|
71
|
-
account_name: str | None
|
|
72
|
-
thread_pool_size: int
|
|
73
|
-
enable_deletion: bool
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class TerraformCloudflareUsers(
|
|
77
|
-
QontractReconcileIntegration[TerraformCloudflareUsersParams]
|
|
78
|
-
):
|
|
79
|
-
@property
|
|
80
|
-
def name(self) -> str:
|
|
81
|
-
return QONTRACT_INTEGRATION.replace("_", "-")
|
|
82
|
-
|
|
83
|
-
def get_early_exit_desired_state(
|
|
84
|
-
self, *args: Any, **kwargs: Any
|
|
85
|
-
) -> dict[str, Any] | None:
|
|
86
|
-
cloudflare_roles, settings = self._get_desired_state()
|
|
87
|
-
|
|
88
|
-
if not settings.settings:
|
|
89
|
-
raise RuntimeError("App interface setting not defined")
|
|
90
|
-
|
|
91
|
-
early_exit_desired_state = cloudflare_roles.model_dump()
|
|
92
|
-
early_exit_desired_state.update({
|
|
93
|
-
CLOUDFLARE_EMAIL_DOMAIN_ALLOW_LIST_KEY: settings.settings
|
|
94
|
-
})
|
|
95
|
-
return early_exit_desired_state
|
|
96
|
-
|
|
97
|
-
def _get_desired_state(
|
|
98
|
-
self,
|
|
99
|
-
) -> tuple[
|
|
100
|
-
CloudflareAccountRoleQueryData, AppInterfaceSettingCloudflareAndVaultQueryData
|
|
101
|
-
]:
|
|
102
|
-
cloudflare_roles = terraform_cloudflare_roles.query(
|
|
103
|
-
query_func=gql.get_api().query
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
settings = app_interface_setting_cloudflare_and_vault.query(
|
|
107
|
-
query_func=gql.get_api().query
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
return cloudflare_roles, settings
|
|
111
|
-
|
|
112
|
-
def run(self, dry_run: bool) -> None:
|
|
113
|
-
print_to_file = self.params.print_to_file
|
|
114
|
-
account_name = self.params.account_name
|
|
115
|
-
thread_pool_size = self.params.thread_pool_size
|
|
116
|
-
enable_deletion = self.params.enable_deletion
|
|
117
|
-
|
|
118
|
-
cloudflare_roles, settings = self._get_desired_state()
|
|
119
|
-
|
|
120
|
-
if not settings.settings:
|
|
121
|
-
raise RuntimeError("App interface setting not defined")
|
|
122
|
-
|
|
123
|
-
secret_reader = create_secret_reader(use_vault=settings.settings[0].vault)
|
|
124
|
-
|
|
125
|
-
cf_clients = self._build_cloudflare_terraform_config_client_collection(
|
|
126
|
-
cloudflare_roles, secret_reader, account_name
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
users = get_cloudflare_users(
|
|
130
|
-
cloudflare_roles.cloudflare_account_roles,
|
|
131
|
-
account_name,
|
|
132
|
-
settings.settings[0].cloudflare_email_domain_allow_list,
|
|
133
|
-
)
|
|
134
|
-
specs = build_external_resource_spec_from_cloudflare_users(users)
|
|
135
|
-
|
|
136
|
-
cf_clients.add_specs(specs)
|
|
137
|
-
cf_clients.populate_resources()
|
|
138
|
-
|
|
139
|
-
working_dirs = cf_clients.dump(print_to_file=print_to_file)
|
|
140
|
-
|
|
141
|
-
if print_to_file:
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
# for storing unique CloudflareAccountV1 since cloudflare_account_role_v1 can contain duplicates due to schema
|
|
145
|
-
account_names_to_account = {
|
|
146
|
-
role.account.name: role.account
|
|
147
|
-
for role in cloudflare_roles.cloudflare_account_roles or []
|
|
148
|
-
if role.account.name in cf_clients.dump()
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
accounts = [
|
|
152
|
-
acct.model_dump(by_alias=True)
|
|
153
|
-
for _, acct in account_names_to_account.items()
|
|
154
|
-
]
|
|
155
|
-
|
|
156
|
-
self._run_terraform(
|
|
157
|
-
dry_run,
|
|
158
|
-
enable_deletion,
|
|
159
|
-
thread_pool_size,
|
|
160
|
-
working_dirs,
|
|
161
|
-
accounts,
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
def _run_terraform(
|
|
165
|
-
self,
|
|
166
|
-
dry_run: bool,
|
|
167
|
-
enable_deletion: bool,
|
|
168
|
-
thread_pool_size: int,
|
|
169
|
-
working_dirs: Mapping[str, str],
|
|
170
|
-
accounts: Iterable[Mapping[str, Any]],
|
|
171
|
-
) -> None:
|
|
172
|
-
tf = TerraformClient(
|
|
173
|
-
QONTRACT_INTEGRATION,
|
|
174
|
-
QONTRACT_INTEGRATION_VERSION,
|
|
175
|
-
QONTRACT_TF_PREFIX,
|
|
176
|
-
accounts,
|
|
177
|
-
working_dirs,
|
|
178
|
-
thread_pool_size,
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
try:
|
|
182
|
-
disabled_deletions_detected, err = tf.plan(enable_deletion)
|
|
183
|
-
if err:
|
|
184
|
-
raise TerraformPlanFailedError(
|
|
185
|
-
f"Failed to run terraform plan for integration {QONTRACT_INTEGRATION}"
|
|
186
|
-
)
|
|
187
|
-
if disabled_deletions_detected:
|
|
188
|
-
raise TerraformDeletionDetectedError(
|
|
189
|
-
"Deletions detected but they are disabled"
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
if dry_run:
|
|
193
|
-
return
|
|
194
|
-
|
|
195
|
-
err = tf.apply()
|
|
196
|
-
if err:
|
|
197
|
-
raise TerraformApplyFailedError(
|
|
198
|
-
f"Failed to run terraform apply for integration {QONTRACT_INTEGRATION}"
|
|
199
|
-
)
|
|
200
|
-
finally:
|
|
201
|
-
tf.cleanup()
|
|
202
|
-
|
|
203
|
-
def _build_cloudflare_terraform_config_client_collection(
|
|
204
|
-
self,
|
|
205
|
-
query_data: CloudflareAccountRoleQueryData,
|
|
206
|
-
secret_reader: SecretReaderBase,
|
|
207
|
-
account_name: str | None,
|
|
208
|
-
) -> TerraformConfigClientCollection:
|
|
209
|
-
cf_clients = TerraformConfigClientCollection()
|
|
210
|
-
for role in query_data.cloudflare_account_roles or []:
|
|
211
|
-
if account_name and role.account.name != account_name:
|
|
212
|
-
continue
|
|
213
|
-
cf_account = CloudflareAccount(
|
|
214
|
-
role.account.name,
|
|
215
|
-
role.account.api_credentials,
|
|
216
|
-
role.account.enforce_twofactor,
|
|
217
|
-
role.account.q_type,
|
|
218
|
-
role.account.provider_version,
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
tf_state = role.account.terraform_state_account.terraform_state
|
|
222
|
-
if not tf_state:
|
|
223
|
-
raise ValueError(
|
|
224
|
-
f"AWS account {role.account.terraform_state_account.name} cannot be used for Cloudflare "
|
|
225
|
-
f"account {cf_account.name} because it does not define a Terraform state "
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
bucket = tf_state.bucket
|
|
229
|
-
region = tf_state.region
|
|
230
|
-
integrations = tf_state.integrations
|
|
231
|
-
|
|
232
|
-
if not bucket:
|
|
233
|
-
raise InvalidTerraformStateError(
|
|
234
|
-
"Terraform state must have bucket defined"
|
|
235
|
-
)
|
|
236
|
-
if not region:
|
|
237
|
-
raise InvalidTerraformStateError(
|
|
238
|
-
"Terraform state must have region defined"
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
integration = None
|
|
242
|
-
for i in integrations:
|
|
243
|
-
if i.integration.replace("-", "_") == QONTRACT_INTEGRATION:
|
|
244
|
-
integration = i
|
|
245
|
-
break
|
|
246
|
-
|
|
247
|
-
if not integration:
|
|
248
|
-
raise IntegrationUndefinedError(
|
|
249
|
-
"Must declare integration name under Terraform state in app-interface"
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
tf_state_s3 = TerraformStateS3(
|
|
253
|
-
role.account.terraform_state_account.automation_token,
|
|
254
|
-
bucket,
|
|
255
|
-
region,
|
|
256
|
-
Integration(integration.integration, integration.key),
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
-
client = TerrascriptCloudflareClientFactory.get_client(
|
|
260
|
-
tf_state_s3,
|
|
261
|
-
cf_account,
|
|
262
|
-
AccountShardingStrategy(cf_account),
|
|
263
|
-
secret_reader,
|
|
264
|
-
False,
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
with contextlib.suppress(ClientAlreadyRegisteredError):
|
|
268
|
-
cf_clients.register_client(cf_account.name, client)
|
|
269
|
-
|
|
270
|
-
return cf_clients
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
def get_cloudflare_users(
|
|
274
|
-
cloudflare_roles: Iterable[CloudflareAccountRoleV1] | None,
|
|
275
|
-
account_name: str | None,
|
|
276
|
-
email_domain_allow_list: Iterable[str] | None,
|
|
277
|
-
) -> dict[str, dict[str, CloudflareUser]]:
|
|
278
|
-
"""
|
|
279
|
-
Returns a two-level dictionary of users with 1st level keys mapping to Cloudflare account names
|
|
280
|
-
and 2nd level keys mapping to the user's email address.
|
|
281
|
-
The method also takes into consideration :param account_name: and :param email_domain_allow_list: which can be
|
|
282
|
-
used to filter users not matching these parameters
|
|
283
|
-
"""
|
|
284
|
-
users: dict[str, dict[str, CloudflareUser]] = {}
|
|
285
|
-
|
|
286
|
-
for cf_role in cloudflare_roles or []:
|
|
287
|
-
if account_name and cf_role.account.name != account_name:
|
|
288
|
-
continue
|
|
289
|
-
for access_role in cf_role.access_roles or []:
|
|
290
|
-
for user in access_role.users:
|
|
291
|
-
if user.cloudflare_user is not None and (
|
|
292
|
-
user.cloudflare_user.split("@")[1]
|
|
293
|
-
in (email_domain_allow_list or [])
|
|
294
|
-
):
|
|
295
|
-
temp = users.get(cf_role.account.name)
|
|
296
|
-
if temp is not None:
|
|
297
|
-
if temp.get(user.cloudflare_user) is not None:
|
|
298
|
-
users[cf_role.account.name][
|
|
299
|
-
user.cloudflare_user
|
|
300
|
-
].roles.update(set(cf_role.roles))
|
|
301
|
-
else:
|
|
302
|
-
users[cf_role.account.name][user.cloudflare_user] = (
|
|
303
|
-
CloudflareUser(
|
|
304
|
-
user.cloudflare_user,
|
|
305
|
-
cf_role.account.name,
|
|
306
|
-
user.org_username,
|
|
307
|
-
set(cf_role.roles),
|
|
308
|
-
)
|
|
309
|
-
)
|
|
310
|
-
|
|
311
|
-
else:
|
|
312
|
-
users[cf_role.account.name] = {
|
|
313
|
-
user.cloudflare_user: CloudflareUser(
|
|
314
|
-
user.cloudflare_user,
|
|
315
|
-
cf_role.account.name,
|
|
316
|
-
user.org_username,
|
|
317
|
-
set(cf_role.roles),
|
|
318
|
-
)
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
return users
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
def build_external_resource_spec_from_cloudflare_users(
|
|
325
|
-
cloudflare_users: Mapping[str, Mapping[str, CloudflareUser]],
|
|
326
|
-
) -> Iterable[ExternalResourceSpec]:
|
|
327
|
-
"""
|
|
328
|
-
This method transforms :param cloudflare_users: into a list of ExternalResourceSpec
|
|
329
|
-
as TerrascriptCloudflareClient works only with the ExternalResourceSpec.
|
|
330
|
-
"""
|
|
331
|
-
specs: list[ExternalResourceSpec] = []
|
|
332
|
-
|
|
333
|
-
for v in cloudflare_users.values():
|
|
334
|
-
for cf_user in v.values():
|
|
335
|
-
data_source_cloudflare_account_roles = {
|
|
336
|
-
"identifier": safe_resource_id(cf_user.account_name),
|
|
337
|
-
"account_id": "${var.account_id}",
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
cloudflare_account_member = {
|
|
341
|
-
"provider": "account_member",
|
|
342
|
-
"identifier": safe_resource_id(cf_user.org_username),
|
|
343
|
-
"email_address": cf_user.email_address,
|
|
344
|
-
"account_id": "${var.account_id}",
|
|
345
|
-
"role_ids": [
|
|
346
|
-
# I know this is ugly :(
|
|
347
|
-
# Terrascript doesn't support local values. Hence, we have to rely on string templating
|
|
348
|
-
# (https://developer.hashicorp.com/terraform/language/expressions/strings#string-templates) to get
|
|
349
|
-
# cloudflare role ids from role name.
|
|
350
|
-
# This string template essentially uses cloudflare_account_roles (https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs/data-sources/account_roles)
|
|
351
|
-
# data source to get role id corresponding to a role name. We populate this string template for every role name listed.
|
|
352
|
-
f'%{{ for role in data.cloudflare_account_roles.{safe_resource_id(cf_user.account_name)}.roles ~}} %{{if role.name == "{each}" ~}}${{role.id}}%{{ endif ~}} %{{ endfor ~}}'
|
|
353
|
-
for each in cf_user.roles
|
|
354
|
-
],
|
|
355
|
-
"cloudflare_account_roles": data_source_cloudflare_account_roles,
|
|
356
|
-
}
|
|
357
|
-
specs.append(
|
|
358
|
-
_get_external_spec_from_resource(
|
|
359
|
-
cloudflare_account_member, cf_user.account_name
|
|
360
|
-
)
|
|
361
|
-
)
|
|
362
|
-
|
|
363
|
-
return specs
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
def _get_external_spec_from_resource(
|
|
367
|
-
resource: MutableMapping[Any, Any], account_name: str
|
|
368
|
-
) -> ExternalResourceSpec:
|
|
369
|
-
return ExternalResourceSpec(
|
|
370
|
-
provision_provider="cloudflare",
|
|
371
|
-
provisioner={"name": f"{account_name}"},
|
|
372
|
-
namespace={},
|
|
373
|
-
resource=resource,
|
|
374
|
-
)
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
from reconcile.gql_definitions.terraform_cloudflare_resources.terraform_cloudflare_accounts import (
|
|
2
|
-
CloudflareAccountV1,
|
|
3
|
-
query,
|
|
4
|
-
)
|
|
5
|
-
from reconcile.utils import gql
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def get_cloudflare_accounts() -> list[CloudflareAccountV1]:
|
|
9
|
-
data = query(gql.get_api().query)
|
|
10
|
-
return list(data.accounts or [])
|
|
File without changes
|
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
import tempfile
|
|
2
|
-
from abc import (
|
|
3
|
-
ABC,
|
|
4
|
-
abstractmethod,
|
|
5
|
-
)
|
|
6
|
-
from collections.abc import Iterable
|
|
7
|
-
from dataclasses import dataclass
|
|
8
|
-
|
|
9
|
-
from terrascript import (
|
|
10
|
-
Backend,
|
|
11
|
-
Data,
|
|
12
|
-
Output,
|
|
13
|
-
Resource,
|
|
14
|
-
Terraform,
|
|
15
|
-
Terrascript,
|
|
16
|
-
Variable,
|
|
17
|
-
provider,
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
from reconcile.cli import TERRAFORM_VERSION
|
|
21
|
-
from reconcile.utils.exceptions import SecretIncompleteError
|
|
22
|
-
from reconcile.utils.external_resource_spec import (
|
|
23
|
-
ExternalResourceSpec,
|
|
24
|
-
ExternalResourceSpecInventory,
|
|
25
|
-
)
|
|
26
|
-
from reconcile.utils.secret_reader import SecretReaderBase
|
|
27
|
-
from reconcile.utils.terraform.config import TerraformS3BackendConfig
|
|
28
|
-
from reconcile.utils.terraform.config_client import TerraformConfigClient
|
|
29
|
-
from reconcile.utils.terrascript.cloudflare_resources import (
|
|
30
|
-
cloudflare_account,
|
|
31
|
-
create_cloudflare_terrascript_resource,
|
|
32
|
-
)
|
|
33
|
-
from reconcile.utils.terrascript.models import (
|
|
34
|
-
CloudflareAccount,
|
|
35
|
-
Integration,
|
|
36
|
-
TerraformStateS3,
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
TMP_DIR_PREFIX = "terrascript-cloudflare-"
|
|
40
|
-
|
|
41
|
-
DEFAULT_CLOUDFLARE_ACCOUNT_TYPE = "standard"
|
|
42
|
-
DEFAULT_CLOUDFLARE_ACCOUNT_2FA = False
|
|
43
|
-
DEFAULT_IS_MANAGED_CLOUDFLARE_ACCOUNT = True
|
|
44
|
-
DEFAULT_PROVIDER_RPS = 4
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@dataclass
|
|
48
|
-
class CloudflareAccountConfig:
|
|
49
|
-
"""Configuration related to authenticating API calls to Cloudflare."""
|
|
50
|
-
|
|
51
|
-
name: str
|
|
52
|
-
api_token: str
|
|
53
|
-
account_id: str
|
|
54
|
-
enforce_twofactor: bool = DEFAULT_CLOUDFLARE_ACCOUNT_2FA
|
|
55
|
-
type: str = DEFAULT_CLOUDFLARE_ACCOUNT_TYPE
|
|
56
|
-
is_managed_account: bool = DEFAULT_IS_MANAGED_CLOUDFLARE_ACCOUNT
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def create_cloudflare_terrascript(
|
|
60
|
-
account_config: CloudflareAccountConfig,
|
|
61
|
-
backend_config: TerraformS3BackendConfig,
|
|
62
|
-
provider_version: str,
|
|
63
|
-
provider_rps: int = DEFAULT_PROVIDER_RPS,
|
|
64
|
-
is_managed_account: bool = True,
|
|
65
|
-
) -> Terrascript:
|
|
66
|
-
"""
|
|
67
|
-
Configures a Terrascript class with the required provider(s) and backend
|
|
68
|
-
configuration.
|
|
69
|
-
|
|
70
|
-
This is offloaded to a separate function to avoid mixing additional
|
|
71
|
-
logic into TerrascriptCloudflareClient.
|
|
72
|
-
|
|
73
|
-
:param account_config: CloudflareAccount configuration.
|
|
74
|
-
:param backend_config: S3 as backend to store Terraform state.
|
|
75
|
-
:param provider_version: Terraform Cloudflare provider version.
|
|
76
|
-
:is_managed_account:
|
|
77
|
-
If the target cloudflare account is being managed by the caller or not.
|
|
78
|
-
Currently this is deferred to terraform-cloudflare-resources.
|
|
79
|
-
Until further improvement(Tracked by APPSRE-7035),
|
|
80
|
-
this argument can be set to False in other integrations.
|
|
81
|
-
Defaults to True.
|
|
82
|
-
|
|
83
|
-
:return: a Terrascript object that contains corresponding resources
|
|
84
|
-
"""
|
|
85
|
-
terrascript = Terrascript()
|
|
86
|
-
|
|
87
|
-
backend = Backend(
|
|
88
|
-
"s3",
|
|
89
|
-
access_key=backend_config.access_key,
|
|
90
|
-
secret_key=backend_config.secret_key,
|
|
91
|
-
bucket=backend_config.bucket,
|
|
92
|
-
key=backend_config.key,
|
|
93
|
-
region=backend_config.region,
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
required_providers = {
|
|
97
|
-
"cloudflare": {
|
|
98
|
-
"source": "cloudflare/cloudflare",
|
|
99
|
-
"version": provider_version,
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
terrascript += Terraform(
|
|
104
|
-
backend=backend,
|
|
105
|
-
required_providers=required_providers,
|
|
106
|
-
required_version=TERRAFORM_VERSION[0],
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
cloudflare_provider_values = {
|
|
110
|
-
"api_token": account_config.api_token,
|
|
111
|
-
"rps": provider_rps,
|
|
112
|
-
}
|
|
113
|
-
if provider_version.startswith("3"):
|
|
114
|
-
cloudflare_provider_values["account_id"] = (
|
|
115
|
-
account_config.account_id
|
|
116
|
-
) # needed for some resources, see note below
|
|
117
|
-
|
|
118
|
-
terrascript += provider.cloudflare(**cloudflare_provider_values)
|
|
119
|
-
|
|
120
|
-
cloudflare_account_values = {
|
|
121
|
-
"name": account_config.name,
|
|
122
|
-
"enforce_twofactor": account_config.enforce_twofactor,
|
|
123
|
-
"type": account_config.type,
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if is_managed_account:
|
|
127
|
-
terrascript += cloudflare_account(
|
|
128
|
-
account_config.name,
|
|
129
|
-
**cloudflare_account_values,
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
# Some resources need "account_id" to be set at the resource level
|
|
133
|
-
# The cloudflare provider is being migrated from settings account_id at the provider
|
|
134
|
-
# level to requiring it at the resource level, for resources that needs it.
|
|
135
|
-
# This is also listed in version 4.x breaking changes:
|
|
136
|
-
# https://github.com/cloudflare/terraform-provider-cloudflare/issues/1646
|
|
137
|
-
terrascript += Variable(
|
|
138
|
-
"account_id", type="string", default=account_config.account_id
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
return terrascript
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
class TerrascriptCloudflareClient(TerraformConfigClient):
|
|
145
|
-
"""
|
|
146
|
-
Build the Terrascript configuration, collect resources, and return Terraform JSON
|
|
147
|
-
configuration.
|
|
148
|
-
|
|
149
|
-
There's actually very little that's specific to Cloudflare in this class. This could
|
|
150
|
-
become a more general TerrascriptClient that could in theory support any resource
|
|
151
|
-
types with some minor modifications to how resource classes (self._resource_classes)
|
|
152
|
-
are tracked.
|
|
153
|
-
"""
|
|
154
|
-
|
|
155
|
-
def __init__(
|
|
156
|
-
self,
|
|
157
|
-
ts_client: Terrascript,
|
|
158
|
-
) -> None:
|
|
159
|
-
self._terrascript = ts_client
|
|
160
|
-
self._resource_specs: ExternalResourceSpecInventory = {}
|
|
161
|
-
|
|
162
|
-
def add_spec(self, spec: ExternalResourceSpec) -> None:
|
|
163
|
-
self._resource_specs[spec.id_object()] = spec
|
|
164
|
-
|
|
165
|
-
def populate_resources(self) -> None:
|
|
166
|
-
"""
|
|
167
|
-
Add the resource spec to Terrascript using the resource-specific classes
|
|
168
|
-
to determine which resources to create.
|
|
169
|
-
"""
|
|
170
|
-
for spec in self._resource_specs.values():
|
|
171
|
-
resources_to_add = create_cloudflare_terrascript_resource(spec)
|
|
172
|
-
self._add_resources(resources_to_add)
|
|
173
|
-
|
|
174
|
-
def dump(self, existing_dir: str | None = None) -> str:
|
|
175
|
-
"""Write the Terraform JSON representation of the resources to disk"""
|
|
176
|
-
if existing_dir is None:
|
|
177
|
-
working_dir = tempfile.mkdtemp(prefix=TMP_DIR_PREFIX)
|
|
178
|
-
else:
|
|
179
|
-
working_dir = existing_dir
|
|
180
|
-
with open(
|
|
181
|
-
working_dir + "/config.tf.json", "w", encoding="locale"
|
|
182
|
-
) as terraform_config_file:
|
|
183
|
-
terraform_config_file.write(self.dumps())
|
|
184
|
-
|
|
185
|
-
return working_dir
|
|
186
|
-
|
|
187
|
-
def dumps(self) -> str:
|
|
188
|
-
"""Return the Terraform JSON representation of the resources"""
|
|
189
|
-
return str(self._terrascript)
|
|
190
|
-
|
|
191
|
-
def _add_resources(self, tf_resources: Iterable[Resource | Output | Data]) -> None:
|
|
192
|
-
for resource in tf_resources:
|
|
193
|
-
self._terrascript.add(resource)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
class TerraformS3StateNamingStrategy(ABC):
|
|
197
|
-
@abstractmethod
|
|
198
|
-
def get_object_key(self, qr_integration: Integration) -> str:
|
|
199
|
-
pass
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
class Default(TerraformS3StateNamingStrategy):
|
|
203
|
-
def get_object_key(self, qr_integration: Integration) -> str:
|
|
204
|
-
return qr_integration.key
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
class AccountShardingStrategy(TerraformS3StateNamingStrategy):
|
|
208
|
-
"""
|
|
209
|
-
This strategy is in place until we solve for keyStrategy as specified in
|
|
210
|
-
https://issues.redhat.com/browse/APPSRE-6933
|
|
211
|
-
"""
|
|
212
|
-
|
|
213
|
-
def __init__(self, account: CloudflareAccount):
|
|
214
|
-
super().__init__()
|
|
215
|
-
self.account: CloudflareAccount = account
|
|
216
|
-
|
|
217
|
-
def get_object_key(self, qr_integration: Integration) -> str:
|
|
218
|
-
return f"{qr_integration.name}-{self.account.name}.tfstate"
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
class DNSZoneShardingStrategy(TerraformS3StateNamingStrategy):
|
|
222
|
-
def __init__(self, account: CloudflareAccount, zone_identifier: str):
|
|
223
|
-
super().__init__()
|
|
224
|
-
self.account: CloudflareAccount = account
|
|
225
|
-
self.zone: str = zone_identifier
|
|
226
|
-
|
|
227
|
-
def get_object_key(self, qr_integration: Integration) -> str:
|
|
228
|
-
old_integration_key = qr_integration.name.replace(
|
|
229
|
-
"-", "_"
|
|
230
|
-
) # This is because the state file was already created using this name before the refactoring
|
|
231
|
-
return f"{old_integration_key}-{self.account.name}-{self.zone}.tfstate"
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
class TerrascriptCloudflareClientFactory:
|
|
235
|
-
@staticmethod
|
|
236
|
-
def _create_backend_config(
|
|
237
|
-
tf_state_s3: TerraformStateS3, key: str, secret_reader: SecretReaderBase
|
|
238
|
-
) -> TerraformS3BackendConfig:
|
|
239
|
-
aws_acct_creds = secret_reader.read_all_secret(tf_state_s3.automation_token)
|
|
240
|
-
aws_access_key_id = aws_acct_creds.get("aws_access_key_id")
|
|
241
|
-
aws_secret_access_key = aws_acct_creds.get("aws_secret_access_key")
|
|
242
|
-
if not aws_access_key_id or not aws_secret_access_key:
|
|
243
|
-
raise SecretIncompleteError(
|
|
244
|
-
f"secret {tf_state_s3.automation_token} incomplete: aws_access_key_id and/or aws_secret_access_key missing"
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
return TerraformS3BackendConfig(
|
|
248
|
-
aws_access_key_id,
|
|
249
|
-
aws_secret_access_key,
|
|
250
|
-
tf_state_s3.bucket,
|
|
251
|
-
key,
|
|
252
|
-
tf_state_s3.region,
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
@staticmethod
|
|
256
|
-
def _create_cloudflare_account_config(
|
|
257
|
-
cf_acct: CloudflareAccount, secret_reader: SecretReaderBase
|
|
258
|
-
) -> CloudflareAccountConfig:
|
|
259
|
-
cf_acct_creds = secret_reader.read_all_secret(cf_acct.api_credentials)
|
|
260
|
-
cf_acct_config = CloudflareAccountConfig(
|
|
261
|
-
cf_acct.name,
|
|
262
|
-
cf_acct_creds["api_token"],
|
|
263
|
-
cf_acct_creds["account_id"],
|
|
264
|
-
cf_acct.enforce_twofactor or DEFAULT_CLOUDFLARE_ACCOUNT_2FA,
|
|
265
|
-
cf_acct.type or DEFAULT_CLOUDFLARE_ACCOUNT_TYPE,
|
|
266
|
-
)
|
|
267
|
-
return cf_acct_config
|
|
268
|
-
|
|
269
|
-
@classmethod
|
|
270
|
-
def get_client(
|
|
271
|
-
cls,
|
|
272
|
-
tf_state_s3: TerraformStateS3,
|
|
273
|
-
cf_acct: CloudflareAccount,
|
|
274
|
-
sharding_strategy: TerraformS3StateNamingStrategy | None,
|
|
275
|
-
secret_reader: SecretReaderBase,
|
|
276
|
-
is_managed_account: bool,
|
|
277
|
-
provider_rps: int = DEFAULT_PROVIDER_RPS,
|
|
278
|
-
) -> TerrascriptCloudflareClient:
|
|
279
|
-
key = _get_terraform_s3_state_key_name(
|
|
280
|
-
tf_state_s3.integration, sharding_strategy
|
|
281
|
-
)
|
|
282
|
-
backend_config = cls._create_backend_config(tf_state_s3, key, secret_reader)
|
|
283
|
-
cf_acct_config = cls._create_cloudflare_account_config(cf_acct, secret_reader)
|
|
284
|
-
ts_config = create_cloudflare_terrascript(
|
|
285
|
-
cf_acct_config,
|
|
286
|
-
backend_config,
|
|
287
|
-
cf_acct.provider_version,
|
|
288
|
-
provider_rps=provider_rps,
|
|
289
|
-
is_managed_account=is_managed_account,
|
|
290
|
-
)
|
|
291
|
-
client = TerrascriptCloudflareClient(ts_config)
|
|
292
|
-
return client
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
def _get_terraform_s3_state_key_name(
|
|
296
|
-
integration: Integration,
|
|
297
|
-
sharding_strategy: TerraformS3StateNamingStrategy | None,
|
|
298
|
-
) -> str:
|
|
299
|
-
if sharding_strategy is None:
|
|
300
|
-
sharding_strategy = Default()
|
|
301
|
-
|
|
302
|
-
return sharding_strategy.get_object_key(integration)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
class IntegrationUndefinedError(Exception):
|
|
306
|
-
pass
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
class InvalidTerraformStateError(Exception):
|
|
310
|
-
pass
|