qontract-reconcile 0.10.2.dev303__py3-none-any.whl → 0.10.2.dev314__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 (30) hide show
  1. {qontract_reconcile-0.10.2.dev303.dist-info → qontract_reconcile-0.10.2.dev314.dist-info}/METADATA +1 -1
  2. {qontract_reconcile-0.10.2.dev303.dist-info → qontract_reconcile-0.10.2.dev314.dist-info}/RECORD +30 -30
  3. reconcile/gql_definitions/common/aws_vpc_requests.py +4 -1
  4. reconcile/gql_definitions/fragments/aws_vpc_request.py +3 -0
  5. reconcile/gql_definitions/introspection.json +36 -12
  6. reconcile/gql_definitions/status_board/status_board.py +0 -20
  7. reconcile/prometheus_rules_tester/integration.py +1 -1
  8. reconcile/rhidp/common.py +2 -0
  9. reconcile/status_board.py +9 -133
  10. reconcile/terraform_tgw_attachments.py +1 -1
  11. reconcile/terraform_users.py +3 -1
  12. reconcile/terraform_vpc_peerings.py +1 -1
  13. reconcile/terraform_vpc_resources/integration.py +19 -1
  14. reconcile/typed_queries/status_board.py +8 -43
  15. reconcile/utils/ocm/base.py +10 -0
  16. reconcile/utils/ocm/status_board.py +0 -13
  17. reconcile/utils/openssl.py +2 -2
  18. reconcile/utils/repo_owners.py +21 -29
  19. reconcile/utils/runtime/meta.py +2 -1
  20. reconcile/utils/sharding.py +1 -1
  21. reconcile/utils/sqs_gateway.py +14 -10
  22. reconcile/utils/structs.py +3 -3
  23. reconcile/utils/terraform_client.py +27 -25
  24. reconcile/utils/terrascript_aws_client.py +487 -372
  25. reconcile/utils/throughput.py +1 -1
  26. reconcile/vpc_peerings_validator.py +2 -2
  27. tools/qontract_cli.py +1 -1
  28. tools/sre_checkpoints/util.py +5 -3
  29. {qontract_reconcile-0.10.2.dev303.dist-info → qontract_reconcile-0.10.2.dev314.dist-info}/WHEEL +0 -0
  30. {qontract_reconcile-0.10.2.dev303.dist-info → qontract_reconcile-0.10.2.dev314.dist-info}/entry_points.txt +0 -0
@@ -31,7 +31,11 @@ from reconcile.utils.runtime.integration import (
31
31
  from reconcile.utils.secret_reader import create_secret_reader
32
32
  from reconcile.utils.semver_helper import make_semver
33
33
  from reconcile.utils.terraform_client import TerraformClient
34
- from reconcile.utils.terrascript_aws_client import TerrascriptClient
34
+ from reconcile.utils.terrascript_aws_client import (
35
+ VPC_REQUEST_DEFAULT_PRIVATE_SUBNET_TAGS,
36
+ VPC_REQUEST_DEFAULT_PUBLIC_SUBNET_TAGS,
37
+ TerrascriptClient,
38
+ )
35
39
  from reconcile.utils.vcs import VCS
36
40
 
37
41
  QONTRACT_INTEGRATION = "terraform_vpc_resources"
@@ -90,14 +94,28 @@ class TerraformVpcResources(QontractReconcileIntegration[TerraformVpcResourcesPa
90
94
  f"{request.identifier}-public_subnets", {}
91
95
  ).get("value", [])
92
96
 
97
+ if request.subnets:
98
+ private_subnet_tags = VPC_REQUEST_DEFAULT_PRIVATE_SUBNET_TAGS | (
99
+ request.subnets.private_subnet_tags or {}
100
+ )
101
+ public_subnet_tags = VPC_REQUEST_DEFAULT_PUBLIC_SUBNET_TAGS | (
102
+ request.subnets.public_subnet_tags or {}
103
+ )
104
+ else:
105
+ private_subnet_tags = VPC_REQUEST_DEFAULT_PRIVATE_SUBNET_TAGS
106
+ public_subnet_tags = VPC_REQUEST_DEFAULT_PUBLIC_SUBNET_TAGS
107
+
93
108
  values = {
94
109
  "static": {
95
110
  "vpc_id": outputs_per_account.get(
96
111
  f"{request.identifier}-vpc_id", {}
97
112
  ).get("value"),
113
+ "vpc_tags": request.vpc_tags or {},
98
114
  "subnets": {
99
115
  "private": private_subnets,
100
116
  "public": public_subnets,
117
+ "private_subnet_tags": private_subnet_tags,
118
+ "public_subnet_tags": public_subnet_tags,
101
119
  },
102
120
  "account_name": request.account.name,
103
121
  "region": request.region,
@@ -9,10 +9,6 @@ from reconcile.gql_definitions.status_board.status_board import (
9
9
  query,
10
10
  )
11
11
  from reconcile.utils import gql
12
- from reconcile.utils.ocm.status_board import (
13
- METADATA_MANAGED_BY_KEY,
14
- METADATA_MANAGED_BY_VALUE,
15
- )
16
12
 
17
13
 
18
14
  def get_status_board(
@@ -23,11 +19,11 @@ def get_status_board(
23
19
  return query(query_func).status_board_v1 or []
24
20
 
25
21
 
26
- def get_selected_app_data(
22
+ def get_selected_app_names(
27
23
  global_selectors: Iterable[str],
28
24
  product: StatusBoardProductV1,
29
- ) -> dict[str, dict[str, dict[str, set[str]]]]:
30
- selected_app_data: dict[str, dict[str, dict[str, Any]]] = {}
25
+ ) -> set[str]:
26
+ selected_app_names: set[str] = set()
31
27
 
32
28
  apps: dict[str, Any] = {"apps": []}
33
29
  for namespace in product.product_environment.namespaces or []:
@@ -35,45 +31,15 @@ def get_selected_app_data(
35
31
  if namespace.app.parent_app:
36
32
  prefix = f"{namespace.app.parent_app.name}-"
37
33
  name = f"{prefix}{namespace.app.name}"
38
-
39
- deployment_saas_files = set()
40
- if namespace.app.saas_files:
41
- deployment_saas_files = {
42
- saas_file.name
43
- for saas_file in namespace.app.saas_files
44
- if "Deployment" in saas_file.managed_resource_types
45
- or "ClowdApp" in saas_file.managed_resource_types
46
- }
47
-
48
- selected_app_data[name] = {
49
- "metadata": {
50
- METADATA_MANAGED_BY_KEY: METADATA_MANAGED_BY_VALUE,
51
- "deploymentSaasFiles": set(deployment_saas_files),
52
- },
53
- }
54
-
34
+ selected_app_names.add(name)
55
35
  app = namespace.app.dict(by_alias=True)
56
36
  app["name"] = name
57
37
  apps["apps"].append(app)
58
38
 
59
39
  for child in namespace.app.children_apps or []:
60
40
  name = f"{namespace.app.name}-{child.name}"
61
- if name not in selected_app_data:
62
- deployment_saas_files = set()
63
- if child.saas_files:
64
- deployment_saas_files = {
65
- saas_file.name
66
- for saas_file in child.saas_files
67
- if "Deployment" in saas_file.managed_resource_types
68
- }
69
-
70
- selected_app_data[name] = {
71
- "metadata": {
72
- METADATA_MANAGED_BY_KEY: METADATA_MANAGED_BY_VALUE,
73
- "deploymentSaasFiles": set(deployment_saas_files),
74
- },
75
- }
76
-
41
+ if name not in selected_app_names:
42
+ selected_app_names.add(f"{namespace.app.name}-{child.name}")
77
43
  child_dict = child.dict(by_alias=True)
78
44
  child_dict["name"] = name
79
45
  apps["apps"].append(child_dict)
@@ -86,7 +52,6 @@ def get_selected_app_data(
86
52
  apps_to_remove: set[str] = set()
87
53
  results = parser.parse(selector).find(apps)
88
54
  apps_to_remove.update(match.value["name"] for match in results)
89
- for app_name in apps_to_remove:
90
- selected_app_data.pop(app_name, None)
55
+ selected_app_names -= apps_to_remove
91
56
 
92
- return selected_app_data
57
+ return selected_app_names
@@ -215,6 +215,10 @@ class OCMExternalConfiguration(BaseModel):
215
215
  syncsets: dict
216
216
 
217
217
 
218
+ class OCMExternalAuthConfig(BaseModel):
219
+ enabled: bool
220
+
221
+
218
222
  PRODUCT_ID_OSD = "osd"
219
223
  PRODUCT_ID_ROSA = "rosa"
220
224
 
@@ -274,6 +278,8 @@ class OCMCluster(BaseModel):
274
278
 
275
279
  external_configuration: OCMExternalConfiguration | None
276
280
 
281
+ external_auth_config: OCMExternalAuthConfig | None
282
+
277
283
  def minor_version(self) -> str:
278
284
  version_info = parse_semver(self.version.raw_id)
279
285
  return f"{version_info.major}.{version_info.minor}"
@@ -315,6 +321,10 @@ class OCMCluster(BaseModel):
315
321
  def base_domain(self) -> str | None:
316
322
  return self.dns.base_domain if self.dns else None
317
323
 
324
+ @property
325
+ def external_auth_enabled(self) -> bool:
326
+ return self.external_auth_config.enabled if self.external_auth_config else False
327
+
318
328
 
319
329
  class OCMLabel(BaseModel):
320
330
  """
@@ -21,7 +21,6 @@ class IDSpec(TypedDict):
21
21
 
22
22
 
23
23
  class ApplicationOCMSpec(BaseOCMSpec):
24
- metadata: dict[str, Any]
25
24
  product: IDSpec
26
25
 
27
26
 
@@ -118,18 +117,6 @@ def create_service(ocm_api: OCMBaseClient, spec: ServiceOCMSpec) -> str:
118
117
  return resp["id"]
119
118
 
120
119
 
121
- def update_application(
122
- ocm_api: OCMBaseClient,
123
- application_id: str,
124
- spec: ApplicationOCMSpec,
125
- ) -> None:
126
- data = spec | {
127
- "metadata": spec.get("metadata", {})
128
- | {METADATA_MANAGED_BY_KEY: METADATA_MANAGED_BY_VALUE}
129
- }
130
- ocm_api.patch(f"/api/status-board/v1/applications/{application_id}", data=data)
131
-
132
-
133
120
  def update_service(
134
121
  ocm_api: OCMBaseClient,
135
122
  service_id: str,
@@ -1,12 +1,12 @@
1
1
  from OpenSSL import crypto
2
2
 
3
3
 
4
- def certificate_matches_host(certificate, host):
4
+ def certificate_matches_host(certificate: bytes, host: str) -> bool:
5
5
  common_name = get_certificate_common_name(certificate)
6
6
  return host.endswith(common_name.replace("*.", ""))
7
7
 
8
8
 
9
- def get_certificate_common_name(certificate):
9
+ def get_certificate_common_name(certificate: bytes) -> str:
10
10
  cert = crypto.load_certificate(crypto.FILETYPE_PEM, certificate)
11
11
  subject = cert.get_subject()
12
12
  return subject.CN
@@ -1,9 +1,13 @@
1
1
  import logging
2
2
  import os
3
3
  import pathlib
4
+ from collections.abc import Iterable, Mapping
4
5
 
5
6
  from ruamel import yaml
6
7
 
8
+ from reconcile.utils.github_api import GithubRepositoryApi
9
+ from reconcile.utils.gitlab_api import GitLabApi
10
+
7
11
  _LOG = logging.getLogger(__name__)
8
12
 
9
13
 
@@ -12,38 +16,24 @@ class RepoOwners:
12
16
  Abstracts the owners of a repository with per-path granularity.
13
17
  """
14
18
 
15
- def __init__(self, git_cli, ref="master", recursive=True):
19
+ def __init__(
20
+ self,
21
+ git_cli: GitLabApi | GithubRepositoryApi,
22
+ ref: str = "master",
23
+ recursive: bool = True,
24
+ ) -> None:
16
25
  self._git_cli = git_cli
17
26
  self._ref = ref
18
- self._owners_map = None
27
+ self._owners_map: dict[str, dict[str, set[str]]] | None = None
19
28
  self._recursive = recursive
20
29
 
21
30
  @property
22
- def owners_map(self):
31
+ def owners_map(self) -> dict[str, dict[str, set[str]]]:
23
32
  if self._owners_map is None:
24
33
  self._owners_map = self._get_owners_map()
25
34
  return self._owners_map
26
35
 
27
- def get_owners(self):
28
- """
29
- Gets all the owners of the repository.
30
-
31
- :return: the repository owners
32
- :rtype: dict
33
- """
34
- repo_owners = {"approvers": set(), "reviewers": set()}
35
-
36
- if "." in self.owners_map:
37
- repo_owners["approvers"].update(self.owners_map["."]["approvers"])
38
- repo_owners["reviewers"].update(self.owners_map["."]["reviewers"])
39
-
40
- for owners in self.owners_map.values():
41
- repo_owners["approvers"].update(owners["approvers"])
42
- repo_owners["reviewers"].update(owners["reviewers"])
43
-
44
- return repo_owners
45
-
46
- def get_root_owners(self):
36
+ def get_root_owners(self) -> dict[str, list[str]]:
47
37
  """
48
38
  Gets all the owners defined in the repository root.
49
39
 
@@ -56,7 +46,7 @@ class RepoOwners:
56
46
 
57
47
  return {"approvers": [], "reviewers": []}
58
48
 
59
- def get_path_owners(self, path):
49
+ def get_path_owners(self, path: str) -> dict[str, list[str]]:
60
50
  """
61
51
  Gets all the owners of a given path, no matter in which
62
52
  level of the filesystem tree the owner was specified.
@@ -67,7 +57,7 @@ class RepoOwners:
67
57
  :return: the path owners
68
58
  :rtype: dict
69
59
  """
70
- path_owners = {"approvers": set(), "reviewers": set()}
60
+ path_owners: dict[str, set[str]] = {"approvers": set(), "reviewers": set()}
71
61
 
72
62
  if "." in self.owners_map:
73
63
  path_owners["approvers"].update(self.owners_map["."]["approvers"])
@@ -80,7 +70,7 @@ class RepoOwners:
80
70
 
81
71
  return self._set_to_sorted_list(path_owners)
82
72
 
83
- def get_path_closest_owners(self, path):
73
+ def get_path_closest_owners(self, path: str) -> dict[str, list[str]]:
84
74
  """
85
75
  Gets all closest owners of a given path, no matter in which
86
76
  level of the filesystem tree the owner was specified.
@@ -108,7 +98,7 @@ class RepoOwners:
108
98
 
109
99
  return {"approvers": [], "reviewers": []}
110
100
 
111
- def _get_owners_map(self):
101
+ def _get_owners_map(self) -> dict[str, dict[str, set[str]]]:
112
102
  """
113
103
  Maps all the OWNERS files content to their respective
114
104
  owned directory.
@@ -173,7 +163,7 @@ class RepoOwners:
173
163
  }
174
164
  return owners_map
175
165
 
176
- def _get_aliases(self):
166
+ def _get_aliases(self) -> dict[str, list[str]] | None:
177
167
  """
178
168
  Retrieves the approvers aliases from the OWNERS_ALIASES file.
179
169
 
@@ -191,7 +181,9 @@ class RepoOwners:
191
181
  return aliases["aliases"]
192
182
 
193
183
  @staticmethod
194
- def _set_to_sorted_list(owners):
184
+ def _set_to_sorted_list(
185
+ owners: Mapping[str, Iterable[str]],
186
+ ) -> dict[str, list[str]]:
195
187
  approvers = owners["approvers"]
196
188
  sorted_approvers = sorted(approvers) if approvers else []
197
189
 
@@ -1,5 +1,6 @@
1
1
  import dataclasses
2
2
  from dataclasses import dataclass
3
+ from typing import Any
3
4
 
4
5
 
5
6
  @dataclass
@@ -8,5 +9,5 @@ class IntegrationMeta:
8
9
  args: list[str]
9
10
  short_help: str | None
10
11
 
11
- def to_dict(self):
12
+ def to_dict(self) -> dict[str, Any]:
12
13
  return dataclasses.asdict(self)
@@ -8,7 +8,7 @@ SHARDS = int(os.environ.get("SHARDS", "1"))
8
8
  SHARD_ID = int(os.environ.get("SHARD_ID", "0"))
9
9
 
10
10
 
11
- def is_in_shard(value):
11
+ def is_in_shard(value: str) -> bool:
12
12
  if SHARDS == 1:
13
13
  return True
14
14
 
@@ -1,5 +1,7 @@
1
1
  import json
2
2
  import os
3
+ from collections.abc import Iterable, Mapping
4
+ from typing import Any, Self
3
5
 
4
6
  from reconcile.utils.aws_api import AWSApi
5
7
  from reconcile.utils.secret_reader import SecretReader
@@ -12,7 +14,9 @@ class SQSGatewayInitError(Exception):
12
14
  class SQSGateway:
13
15
  """Wrapper around SQS AWS SDK"""
14
16
 
15
- def __init__(self, accounts, secret_reader: SecretReader):
17
+ def __init__(
18
+ self, accounts: Iterable[Mapping[str, Any]], secret_reader: SecretReader
19
+ ) -> None:
16
20
  queue_url = os.environ.get("gitlab_pr_submitter_queue_url") # noqa: SIM112
17
21
  if not queue_url:
18
22
  raise SQSGatewayInitError(
@@ -30,17 +34,17 @@ class SQSGateway:
30
34
  self.sqs = self._aws_api.get_session_client(session, "sqs")
31
35
  self.queue_url = queue_url
32
36
 
33
- def __enter__(self):
37
+ def __enter__(self) -> Self:
34
38
  return self
35
39
 
36
- def __exit__(self, *ext):
40
+ def __exit__(self, *ext: Any) -> None:
37
41
  self.cleanup()
38
42
 
39
- def cleanup(self):
43
+ def cleanup(self) -> None:
40
44
  self._aws_api.cleanup()
41
45
 
42
46
  @staticmethod
43
- def get_queue_account(accounts, queue_url):
47
+ def get_queue_account(accounts: Iterable[Mapping[str, Any]], queue_url: str) -> str:
44
48
  queue_account_uid = queue_url.split("/")[3]
45
49
  queue_account_name = [
46
50
  a["name"] for a in accounts if a["uid"] == queue_account_uid
@@ -49,14 +53,14 @@ class SQSGateway:
49
53
  raise SQSGatewayInitError(f"account uid not found: {queue_account_uid}")
50
54
  return queue_account_name[0]
51
55
 
52
- def send_message(self, body):
56
+ def send_message(self, body: Mapping[str, Any]) -> None:
53
57
  self.sqs.send_message(QueueUrl=self.queue_url, MessageBody=json.dumps(body))
54
58
 
55
59
  def receive_messages(
56
60
  self,
57
- visibility_timeout=30,
58
- wait_time_seconds=20,
59
- ):
61
+ visibility_timeout: int = 30,
62
+ wait_time_seconds: int = 20,
63
+ ) -> list[tuple[str, dict[str, Any]]]:
60
64
  messages = self.sqs.receive_message(
61
65
  QueueUrl=self.queue_url,
62
66
  VisibilityTimeout=visibility_timeout,
@@ -64,5 +68,5 @@ class SQSGateway:
64
68
  ).get("Messages", [])
65
69
  return [(m["ReceiptHandle"], json.loads(m["Body"])) for m in messages]
66
70
 
67
- def delete_message(self, receipt_handle):
71
+ def delete_message(self, receipt_handle: str) -> None:
68
72
  self.sqs.delete_message(QueueUrl=self.queue_url, ReceiptHandle=receipt_handle)
@@ -5,12 +5,12 @@ from pydantic.dataclasses import dataclass
5
5
  class CommandExecutionResult:
6
6
  """This class represents a command execution result"""
7
7
 
8
- def __init__(self, is_ok, message):
8
+ def __init__(self, is_ok: bool, message: str) -> None:
9
9
  self.is_ok = is_ok
10
10
  self.message = message
11
11
 
12
- def __str__(self):
12
+ def __str__(self) -> str:
13
13
  return str(self.message)
14
14
 
15
- def __bool__(self):
15
+ def __bool__(self) -> bool:
16
16
  return self.is_ok
@@ -5,8 +5,8 @@ import shutil
5
5
  import tempfile
6
6
  from collections import defaultdict
7
7
  from collections.abc import (
8
- Generator,
9
8
  Iterable,
9
+ Iterator,
10
10
  Mapping,
11
11
  )
12
12
  from contextlib import contextmanager
@@ -86,8 +86,8 @@ class TerraformClient:
86
86
  working_dirs: Mapping[str, str],
87
87
  thread_pool_size: int,
88
88
  aws_api: AWSApi | None = None,
89
- init_users=False,
90
- ):
89
+ init_users: bool = False,
90
+ ) -> None:
91
91
  self.integration = integration
92
92
  self.integration_version = integration_version
93
93
  self.integration_prefix = integration_prefix
@@ -101,7 +101,7 @@ class TerraformClient:
101
101
 
102
102
  self.specs: list[TerraformSpec] = []
103
103
  self.init_specs()
104
- self.outputs: dict = {}
104
+ self.outputs: dict[str, Any] = {}
105
105
  self.init_outputs()
106
106
 
107
107
  self.OUTPUT_TYPE_SECRETS = "Secrets"
@@ -112,19 +112,19 @@ class TerraformClient:
112
112
  if init_users:
113
113
  self.init_existing_users()
114
114
 
115
- def init_existing_users(self):
115
+ def init_existing_users(self) -> None:
116
116
  self.users = {
117
117
  account: list(self.format_output(output, self.OUTPUT_TYPE_PASSWORDS).keys())
118
118
  for account, output in self.outputs.items()
119
119
  }
120
120
 
121
- def increment_apply_count(self):
121
+ def increment_apply_count(self) -> None:
122
122
  self.apply_count += 1
123
123
 
124
124
  def should_apply(self) -> bool:
125
125
  return self.apply_count > 0
126
126
 
127
- def get_new_users(self):
127
+ def get_new_users(self) -> list[tuple[str, Any, str, Any]]:
128
128
  new_users = []
129
129
  self.init_outputs() # get updated output
130
130
  for account, output in self.outputs.items():
@@ -141,7 +141,7 @@ class TerraformClient:
141
141
  ))
142
142
  return new_users
143
143
 
144
- def init_specs(self):
144
+ def init_specs(self) -> None:
145
145
  self.specs = [
146
146
  TerraformSpec(name=name, working_dir=wd)
147
147
  for name, wd in self.working_dirs.items()
@@ -152,7 +152,7 @@ class TerraformClient:
152
152
  @contextmanager
153
153
  def _terraform_log_file(
154
154
  self, working_dir: str
155
- ) -> Generator[tuple[IO, dict[str, str]], None, None]:
155
+ ) -> Iterator[tuple[IO[bytes], dict[str, str]]]:
156
156
  with tempfile.NamedTemporaryFile(dir=working_dir) as f:
157
157
  env = {
158
158
  "TF_LOG": TERRAFORM_LOG_LEVEL,
@@ -161,7 +161,7 @@ class TerraformClient:
161
161
  yield f, env
162
162
 
163
163
  @retry(exceptions=TerraformCommandError)
164
- def terraform_init(self, spec: TerraformSpec):
164
+ def terraform_init(self, spec: TerraformSpec) -> None:
165
165
  with self._terraform_log_file(spec.working_dir) as (f, env):
166
166
  return_code, stdout, stderr = lean_tf.init(spec.working_dir, env=env)
167
167
  log = f.read().decode("utf-8")
@@ -171,12 +171,12 @@ class TerraformClient:
171
171
  return_code, "init", output=stdout, stderr=stderr
172
172
  )
173
173
 
174
- def init_outputs(self):
174
+ def init_outputs(self) -> None:
175
175
  results = threaded.run(self.terraform_output, self.specs, self.thread_pool_size)
176
176
  self.outputs = dict(results)
177
177
 
178
178
  @retry(exceptions=TerraformCommandError)
179
- def terraform_output(self, spec: TerraformSpec):
179
+ def terraform_output(self, spec: TerraformSpec) -> tuple[str, Any]:
180
180
  with self._terraform_log_file(spec.working_dir) as (f, env):
181
181
  return_code, stdout, stderr = lean_tf.output(spec.working_dir, env=env)
182
182
  log = f.read().decode("utf-8")
@@ -194,17 +194,17 @@ class TerraformClient:
194
194
  return spec.name, json.loads(stdout)
195
195
 
196
196
  # terraform plan
197
- def plan(self, enable_deletion):
197
+ def plan(self, enable_deletion: bool) -> tuple[bool, bool]:
198
198
  errors = False
199
199
  disabled_deletions_detected = False
200
- results = threaded.run(
200
+ results: list[tuple[bool, list[AccountUser], bool]] = threaded.run(
201
201
  self.terraform_plan,
202
202
  self.specs,
203
203
  self.thread_pool_size,
204
204
  enable_deletion=enable_deletion,
205
205
  )
206
206
 
207
- self.created_users = []
207
+ self.created_users: list[AccountUser] = []
208
208
  for disabled_deletion_detected, created_users, error in results:
209
209
  if error:
210
210
  errors = True
@@ -278,7 +278,7 @@ class TerraformClient:
278
278
  self,
279
279
  spec: TerraformSpec,
280
280
  enable_deletion: bool,
281
- ) -> tuple[bool, list]:
281
+ ) -> tuple[bool, list[AccountUser]]:
282
282
  disabled_deletion_detected = False
283
283
  name = spec.name
284
284
  account_enable_deletion = self.accounts[name].get("enableDeletion") or False
@@ -412,7 +412,9 @@ class TerraformClient:
412
412
  )
413
413
  return disabled_deletion_detected, created_users
414
414
 
415
- def deletion_approved(self, account_name, resource_type, resource_name):
415
+ def deletion_approved(
416
+ self, account_name: str, resource_type: str, resource_name: str
417
+ ) -> bool:
416
418
  account = self.accounts[account_name]
417
419
  deletion_approvals = account.get("deletionApprovals")
418
420
  if not deletion_approvals:
@@ -439,11 +441,11 @@ class TerraformClient:
439
441
  return False
440
442
 
441
443
  # terraform apply
442
- def apply(self):
444
+ def apply(self) -> bool:
443
445
  errors = threaded.run(self.terraform_apply, self.specs, self.thread_pool_size)
444
446
  return any(errors)
445
447
 
446
- def terraform_apply(self, spec: TerraformSpec):
448
+ def terraform_apply(self, spec: TerraformSpec) -> bool:
447
449
  with self._terraform_log_file(spec.working_dir) as (f, env):
448
450
  return_code, stdout, stderr = lean_tf.apply(
449
451
  spec.working_dir,
@@ -486,9 +488,9 @@ class TerraformClient:
486
488
 
487
489
  return replicas_info
488
490
 
489
- def format_output(self, output, type):
491
+ def format_output(self, output: Any, type: str) -> dict[str, dict[str, Any]]:
490
492
  # data is a dictionary of dictionaries
491
- data = {}
493
+ data: dict[str, dict[str, Any]] = {}
492
494
  if output is None:
493
495
  return data
494
496
 
@@ -643,7 +645,7 @@ class TerraformClient:
643
645
  return error_occured
644
646
 
645
647
  @staticmethod
646
- def split_to_lines(*outputs):
648
+ def split_to_lines(*outputs: str) -> Any:
647
649
  split_outputs = []
648
650
  try:
649
651
  for output in outputs:
@@ -656,7 +658,7 @@ class TerraformClient:
656
658
  return split_outputs[0]
657
659
  return split_outputs
658
660
 
659
- def cleanup(self):
661
+ def cleanup(self) -> None:
660
662
  if self._aws_api is not None:
661
663
  self._aws_api.cleanup()
662
664
  for wd in self.working_dirs.values():
@@ -757,7 +759,7 @@ class TerraformClient:
757
759
 
758
760
  def validate_db_upgrade(
759
761
  self, account_name: str, resource_name: str, resource_change: Mapping[str, Any]
760
- ):
762
+ ) -> None:
761
763
  """
762
764
  Determine whether the RDS engine version upgrade is valid.
763
765
 
@@ -862,7 +864,7 @@ class TerraformClient:
862
864
  ],
863
865
  }
864
866
 
865
- def is_supported(engine, version):
867
+ def is_supported(engine: str, version: str) -> bool:
866
868
  parsed_version = pkg_version.parse(version)
867
869
  if engine == "mysql":
868
870
  return any(