qontract-reconcile 0.10.1rc745__py3-none-any.whl → 0.10.1rc747__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qontract-reconcile
3
- Version: 0.10.1rc745
3
+ Version: 0.10.1rc747
4
4
  Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
5
5
  Home-page: https://github.com/app-sre/qontract-reconcile
6
6
  Author: Red Hat App-SRE Team
@@ -10,7 +10,7 @@ reconcile/aws_iam_password_reset.py,sha256=NwErtrqgBiXr7eGCAHdtGGOx0S7-4JnSc29Ie
10
10
  reconcile/aws_support_cases_sos.py,sha256=Jk6_XjDeJSYxgRGqcEAOcynt9qJF2r5HPIPcSKmoBv8,2974
11
11
  reconcile/blackbox_exporter_endpoint_monitoring.py,sha256=W_VJagnsJR1v5oqjlI3RJJE0_nhtJ0m81RS8zWA5u5c,3538
12
12
  reconcile/checkpoint.py,sha256=R2WFXUXLTB4sWMi4GeA4eegsuf_1-Q4vH8M0Toh3Ij4,5036
13
- reconcile/cli.py,sha256=Fra8s6kxIuCoLF-moSto81gDFoJ6Q-WrsaRcVxR-KRI,98281
13
+ reconcile/cli.py,sha256=Vw8P6w0nPOGkMRvxpn4lR75jUeRoXLJ4R-WspWNX6Cg,98702
14
14
  reconcile/closedbox_endpoint_monitoring_base.py,sha256=SMhkcQqprWvThrIJa3U_3uh5w1h-alleW1QnCJFY4Qw,4909
15
15
  reconcile/cluster_deployment_mapper.py,sha256=2Ah-nu-Mdig0pjuiZl_XLrmVAjYzFjORR3dMlCgkmw0,2352
16
16
  reconcile/dashdotdb_base.py,sha256=a5aPLVxyqPSbjdB0Ty-uliOtxwvEbbEljHJKxdK3-Zk,4813
@@ -47,7 +47,7 @@ reconcile/jenkins_roles.py,sha256=f8ELpZY36UjoaCpR_9LijQuIMuB6a7sVLFf_H1ct9Hc,44
47
47
  reconcile/jenkins_webhooks.py,sha256=j8vhJMWcRhOdc9XzRSm0CPj84jsF3e4Syjm7r1BIsDE,1978
48
48
  reconcile/jenkins_webhooks_cleaner.py,sha256=JsN_NVPfZJwv1JtSzZXDIHUqGiefL-DRffFnDGau9aY,1539
49
49
  reconcile/jenkins_worker_fleets.py,sha256=PMNGOX0krubFjInPiFT0za0KCiWBLEcVDuXdKRd1BrE,5378
50
- reconcile/jira_permissions_validator.py,sha256=8Nl7rSc8x_AXXVRYvRlq_arR7N5pz0YM4M7Pb60RoCg,11597
50
+ reconcile/jira_permissions_validator.py,sha256=iDsFdGqB8Zv9cjIVgYFq_N3xtRCrcR5uAZmcc51D-2o,13240
51
51
  reconcile/jira_watcher.py,sha256=eyOQ92t8TFi6gogfNTO448h_h1CUyr24E0MPHc51R-o,3617
52
52
  reconcile/ldap_users.py,sha256=uEWQ0V41tN9KCZi4ZKPamjrJ6djSpdpvDBo7yJ0e7ZI,3008
53
53
  reconcile/mr_client_gateway.py,sha256=WhjMd-sIXDFCV8-rt8CEjurJ5OYB1pOD0K3o0tZRXQg,1885
@@ -112,7 +112,7 @@ reconcile/terraform_aws_route53.py,sha256=R8eZHlvP368nvtmLd_cMSK3RGxD7jso9qE7NP9
112
112
  reconcile/terraform_cloudflare_dns.py,sha256=auU4bzeLwd4S8D8oqpqJbrCUoEdELXrgi7vHOedjYFk,13332
113
113
  reconcile/terraform_cloudflare_resources.py,sha256=EbQQaoDnZ7brvRCpbFtwlD7KLk2hDVNcjhrJGaAywEk,15023
114
114
  reconcile/terraform_cloudflare_users.py,sha256=1EbTHwJgiPkJpMP-Ag340QNgGK3mXn3dcC3DpLakudM,13987
115
- reconcile/terraform_repo.py,sha256=c0GZFuY3rCm6VHjHqYbsgOHrEkRWKF_1LrMThsn2XDw,16127
115
+ reconcile/terraform_repo.py,sha256=tQkMUEDEB6QfuWYwiW7H-NnX4SHxYK7mLZYZffKj-hE,16301
116
116
  reconcile/terraform_resources.py,sha256=BN8XuJwjOt1ztruEAHydkd0YiBlb3fHZ7n0snZtRhck,19356
117
117
  reconcile/terraform_tgw_attachments.py,sha256=k9Lf0ST65gmI6aUV6HnvxSGcKL7MGx_lN22OXuRGH9Y,16224
118
118
  reconcile/terraform_users.py,sha256=9rgbM572LfmOSnV3uCP20G_Cw6T7due94g8rhhiz904,10225
@@ -300,6 +300,7 @@ reconcile/gql_definitions/membershipsources/roles.py,sha256=d3nv3GLsj_eKgwB1glsi
300
300
  reconcile/gql_definitions/ocm_labels/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
301
301
  reconcile/gql_definitions/ocm_labels/clusters.py,sha256=MIVR5c8JxEOjsdGNpB737QyHGkjmHCycY6MAYNclPck,2916
302
302
  reconcile/gql_definitions/ocm_labels/organizations.py,sha256=mmYB5C5Fp_nPzwBDKdKG4qWiLre2VkZ26U_2O-jRKC4,2001
303
+ reconcile/gql_definitions/ocm_oidc_idp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
303
304
  reconcile/gql_definitions/ocm_subscription_labels/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
304
305
  reconcile/gql_definitions/openshift_cluster_bots/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
305
306
  reconcile/gql_definitions/openshift_cluster_bots/clusters.py,sha256=68meUg2Wgh91gplbPHAIlRWjnGg2RDrwtWd93QK6qRE,3672
@@ -345,7 +346,7 @@ reconcile/gql_definitions/terraform_cloudflare_users/terraform_cloudflare_roles.
345
346
  reconcile/gql_definitions/terraform_init/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
346
347
  reconcile/gql_definitions/terraform_init/aws_accounts.py,sha256=OJ0hDbRachRaDkL-OGT6-byr9cKdBiQDnNCpwUe3oJ8,2674
347
348
  reconcile/gql_definitions/terraform_repo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
348
- reconcile/gql_definitions/terraform_repo/terraform_repo.py,sha256=pefTRhb0ZcWm_j6dYz6m6qNqZFteqgrQ25SgUqRAVaI,3173
349
+ reconcile/gql_definitions/terraform_repo/terraform_repo.py,sha256=_rdq3efy5Q3QFpI-vcs3-wacsXo_1fu1kVix_E83h5Q,3599
349
350
  reconcile/gql_definitions/terraform_resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
350
351
  reconcile/gql_definitions/terraform_resources/database_access_manager.py,sha256=yv0_YC-LmhaKD_gyGG3le1w5BtypBjlsO894-Zgdg4U,4813
351
352
  reconcile/gql_definitions/terraform_resources/terraform_resources_namespaces.py,sha256=ElJTVH2cwNqJiLFl3Zi5zt9y7dN0yLmdtmwZC5X7ir0,42189
@@ -501,7 +502,7 @@ reconcile/test/test_terraform_aws_route53.py,sha256=xHggb8K1P76OyCfFcogbkmyKle-N
501
502
  reconcile/test/test_terraform_cloudflare_dns.py,sha256=aQTXX8Vr4h9aWvJZTnpZEhMGYoBpT2d45ZxU_ECIQ6o,3425
502
503
  reconcile/test/test_terraform_cloudflare_resources.py,sha256=NK_uktyWihkQ3gMN4bCaKerpi43CXAVYGIKTfcz05rY,13550
503
504
  reconcile/test/test_terraform_cloudflare_users.py,sha256=RAFtMMdqZha3jNnNNsqbNQQUDSqUzdoM63rCw7fs4Fo,27456
504
- reconcile/test/test_terraform_repo.py,sha256=soKFJfF8tWIimDs39RQl3Hnh-Od-bR4PfnEA2s1UprM,11552
505
+ reconcile/test/test_terraform_repo.py,sha256=HTr8HMtLgecBTxKYbRAQFSty7tGzeKsv_fxtOAbDlNA,12184
505
506
  reconcile/test/test_terraform_resources.py,sha256=EFCqPI5_G8hPRh1zmnU91o8wMeT2qK1CabDUa_X1rSk,15283
506
507
  reconcile/test/test_terraform_tgw_attachments.py,sha256=cAq6exc-K-jtLla1CZUZQzVnBkyDnIlL7jybnddhLKc,36861
507
508
  reconcile/test/test_terraform_users.py,sha256=XOAfGvITCJPI1LTlISmHbA4ONMQMkxYUMTsny7pQCFw,4319
@@ -617,7 +618,7 @@ reconcile/utils/helpers.py,sha256=k9svgFFZG7H5FvHYY0g5jJyvgvh2UDZxf0Ib221teag,11
617
618
  reconcile/utils/imap_client.py,sha256=byFAJATbITJPsGECSbvXBOcCnoeTUpDFiEjzOAxLm_U,1975
618
619
  reconcile/utils/instrumented_wrappers.py,sha256=eVwMoa6FCrYxLv3RML3WpZF9qKVfCTjMxphgVXG03OM,1073
619
620
  reconcile/utils/jenkins_api.py,sha256=MyJSB_S3uYf3sXnt9t03-gZNQ7tbdd7Wusv3MoF2fRc,7113
620
- reconcile/utils/jira_client.py,sha256=KjsakExjlii6lkxUlHRScBtPG4wuomJOTIoNiM-edQw,7322
621
+ reconcile/utils/jira_client.py,sha256=sCswWH-SctzPSElI9_oHv9LKuNnPfjP_FIOizxqh6nE,7693
621
622
  reconcile/utils/jjb_client.py,sha256=Pdy0dLCFvD6GPCaC0tZydYgkVJPOxYXIiwWECZaFJBU,14551
622
623
  reconcile/utils/jsonpath.py,sha256=NRpAEijKN4cMDjo7qivNPqpm0__GQQ1TiE0PBEBO45s,5572
623
624
  reconcile/utils/jump_host.py,sha256=AdwmCZYNhRe53VwV2iAsUdVyUdVtSd4REmdThJDkM5w,4973
@@ -777,8 +778,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
777
778
  tools/test/test_qontract_cli.py,sha256=w2l4BHB09k1d-BGJ1jBUNCqDv7zkqYrMHojQXg-21kQ,4155
778
779
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
779
780
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
780
- qontract_reconcile-0.10.1rc745.dist-info/METADATA,sha256=K-tNwC_Bwk22pf6PCk8t7tgUuxwXMW7XSa0kPS41UmY,2382
781
- qontract_reconcile-0.10.1rc745.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
782
- qontract_reconcile-0.10.1rc745.dist-info/entry_points.txt,sha256=rIxI5zWtHNlfpDeq1a7pZXAPoqf7HG32KMTN3MeWK_8,429
783
- qontract_reconcile-0.10.1rc745.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
784
- qontract_reconcile-0.10.1rc745.dist-info/RECORD,,
781
+ qontract_reconcile-0.10.1rc747.dist-info/METADATA,sha256=faEGscy4hQmj3OUFAXW7ImzUqwGYG--n1IM8fNXYPns,2382
782
+ qontract_reconcile-0.10.1rc747.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
783
+ qontract_reconcile-0.10.1rc747.dist-info/entry_points.txt,sha256=rIxI5zWtHNlfpDeq1a7pZXAPoqf7HG32KMTN3MeWK_8,429
784
+ qontract_reconcile-0.10.1rc747.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
785
+ qontract_reconcile-0.10.1rc747.dist-info/RECORD,,
reconcile/cli.py CHANGED
@@ -1144,12 +1144,26 @@ def jenkins_webhooks_cleaner(ctx):
1144
1144
  help="Throw and error in case of board permission errors. Useful for PR checks.",
1145
1145
  default=True,
1146
1146
  )
1147
+ @enable_extended_early_exit
1148
+ @extended_early_exit_cache_ttl_seconds
1149
+ @log_cached_log_output
1147
1150
  @click.pass_context
1148
- def jira_permissions_validator(ctx, exit_on_permission_errors):
1151
+ def jira_permissions_validator(
1152
+ ctx,
1153
+ exit_on_permission_errors,
1154
+ enable_extended_early_exit,
1155
+ extended_early_exit_cache_ttl_seconds,
1156
+ log_cached_log_output,
1157
+ ):
1149
1158
  import reconcile.jira_permissions_validator
1150
1159
 
1151
1160
  run_integration(
1152
- reconcile.jira_permissions_validator, ctx.obj, exit_on_permission_errors
1161
+ reconcile.jira_permissions_validator,
1162
+ ctx.obj,
1163
+ exit_on_permission_errors,
1164
+ enable_extended_early_exit=enable_extended_early_exit,
1165
+ extended_early_exit_cache_ttl_seconds=extended_early_exit_cache_ttl_seconds,
1166
+ log_cached_log_output=log_cached_log_output,
1153
1167
  )
1154
1168
 
1155
1169
 
File without changes
@@ -52,6 +52,15 @@ query TerraformRepo {
52
52
  projectPath
53
53
  delete
54
54
  requireFips
55
+ tfVersion
56
+ variables {
57
+ inputs {
58
+ ...VaultSecret
59
+ }
60
+ outputs {
61
+ ...VaultSecret
62
+ }
63
+ }
55
64
  }
56
65
  }
57
66
  """
@@ -82,6 +91,11 @@ class AWSAccountV1(ConfiguredBaseModel):
82
91
  terraform_state: Optional[TerraformStateAWSV1] = Field(..., alias="terraformState")
83
92
 
84
93
 
94
+ class TerraformRepoVariablesV1(ConfiguredBaseModel):
95
+ inputs: VaultSecret = Field(..., alias="inputs")
96
+ outputs: VaultSecret = Field(..., alias="outputs")
97
+
98
+
85
99
  class TerraformRepoV1(ConfiguredBaseModel):
86
100
  account: AWSAccountV1 = Field(..., alias="account")
87
101
  name: str = Field(..., alias="name")
@@ -90,6 +104,8 @@ class TerraformRepoV1(ConfiguredBaseModel):
90
104
  project_path: str = Field(..., alias="projectPath")
91
105
  delete: Optional[bool] = Field(..., alias="delete")
92
106
  require_fips: Optional[bool] = Field(..., alias="requireFips")
107
+ tf_version: str = Field(..., alias="tfVersion")
108
+ variables: Optional[TerraformRepoVariablesV1] = Field(..., alias="variables")
93
109
 
94
110
 
95
111
  class TerraformRepoQueryData(ConfiguredBaseModel):
@@ -2,14 +2,11 @@ import logging
2
2
  import sys
3
3
  from collections.abc import Callable, Iterable
4
4
  from enum import IntFlag, auto
5
- from typing import Any
5
+ from typing import Any, TypedDict
6
6
 
7
7
  from jira import JIRAError
8
8
  from pydantic import BaseModel
9
9
 
10
- from reconcile.gql_definitions.jira_permissions_validator.jira_boards_for_permissions_validator import (
11
- DEFINITION as JIRA_BOARDS_DEFINITION,
12
- )
13
10
  from reconcile.gql_definitions.jira_permissions_validator.jira_boards_for_permissions_validator import (
14
11
  JiraBoardV1,
15
12
  )
@@ -24,10 +21,17 @@ from reconcile.typed_queries.jira_settings import get_jira_settings
24
21
  from reconcile.typed_queries.jiralert_settings import get_jiralert_settings
25
22
  from reconcile.utils import gql, metrics
26
23
  from reconcile.utils.disabled_integrations import integration_is_enabled
24
+ from reconcile.utils.extended_early_exit import (
25
+ ExtendedEarlyExitRunnerResult,
26
+ extended_early_exit_run,
27
+ )
27
28
  from reconcile.utils.jira_client import JiraClient, JiraWatcherSettings
28
29
  from reconcile.utils.secret_reader import SecretReaderBase, create_secret_reader
30
+ from reconcile.utils.semver_helper import make_semver
31
+ from reconcile.utils.unleash import get_feature_toggle_state
29
32
 
30
33
  QONTRACT_INTEGRATION = "jira-permissions-validator"
34
+ QONTRACT_INTEGRATION_VERSION = make_semver(1, 0, 0)
31
35
 
32
36
  NameToIdMap = dict[str, str]
33
37
 
@@ -59,6 +63,15 @@ class ValidationError(IntFlag):
59
63
  INVALID_COMPONENT = auto()
60
64
 
61
65
 
66
+ class RunnerParams(TypedDict):
67
+ exit_on_permission_errors: bool
68
+ boards: list[JiraBoardV1]
69
+
70
+
71
+ class CacheSource(TypedDict):
72
+ boards: list
73
+
74
+
62
75
  def board_is_valid(
63
76
  jira: JiraClient,
64
77
  board: JiraBoardV1,
@@ -90,7 +103,7 @@ def board_is_valid(
90
103
  error |= ValidationError.INVALID_COMPONENT
91
104
 
92
105
  issue_type = board.issue_type if board.issue_type else default_issue_type
93
- project_issue_types = jira.project_issue_types(board.name)
106
+ project_issue_types = jira.project_issue_types()
94
107
  project_issue_types_str = [i.name for i in project_issue_types]
95
108
  if issue_type not in project_issue_types_str:
96
109
  logging.error(
@@ -192,7 +205,6 @@ def validate_boards(
192
205
  ) -> bool:
193
206
  error = False
194
207
  jira_clients: dict[str, JiraClient] = {}
195
- public_projects_cache: dict[str, list[str]] = {}
196
208
  for board in jira_boards:
197
209
  logging.debug(f"[{board.name}] checking ...")
198
210
  if board.server.server_url not in jira_clients:
@@ -204,10 +216,6 @@ def validate_boards(
204
216
  )
205
217
  jira = jira_clients[board.server.server_url]
206
218
  jira.project = board.name
207
- if board.server.server_url not in public_projects_cache:
208
- public_projects_cache[board.server.server_url] = jira.public_projects()
209
- public_projects = public_projects_cache[board.server.server_url]
210
-
211
219
  try:
212
220
  error_flags = board_is_valid(
213
221
  jira=jira,
@@ -215,7 +223,7 @@ def validate_boards(
215
223
  default_issue_type=default_issue_type,
216
224
  default_reopen_state=default_reopen_state,
217
225
  jira_server_priorities={p.name: p.id for p in jira.priorities()},
218
- public_projects=public_projects,
226
+ public_projects=jira.public_projects(),
219
227
  )
220
228
  match error_flags:
221
229
  case 0:
@@ -256,13 +264,59 @@ def get_jira_boards(query_func: Callable) -> list[JiraBoardV1]:
256
264
  ]
257
265
 
258
266
 
259
- def run(dry_run: bool, exit_on_permission_errors: bool) -> None:
267
+ def export_boards(boards: list[JiraBoardV1]) -> list[dict]:
268
+ return [board.dict() for board in boards]
269
+
270
+
271
+ def run(
272
+ dry_run: bool,
273
+ exit_on_permission_errors: bool,
274
+ enable_extended_early_exit: bool = False,
275
+ extended_early_exit_cache_ttl_seconds: int = 3600,
276
+ log_cached_log_output: bool = False,
277
+ ) -> None:
278
+ gql_api = gql.get_api()
279
+ boards = get_jira_boards(query_func=gql_api.query)
280
+ runner_params: RunnerParams = dict(
281
+ exit_on_permission_errors=exit_on_permission_errors,
282
+ boards=boards,
283
+ )
284
+ if enable_extended_early_exit and get_feature_toggle_state(
285
+ "jira-permissions-validator-extended-early-exit",
286
+ default=True,
287
+ ):
288
+ vault_settings = get_app_interface_vault_settings()
289
+ secret_reader = create_secret_reader(use_vault=vault_settings.vault)
290
+
291
+ cache_source = CacheSource(
292
+ boards=export_boards(boards),
293
+ )
294
+ extended_early_exit_run(
295
+ integration=QONTRACT_INTEGRATION,
296
+ integration_version=QONTRACT_INTEGRATION_VERSION,
297
+ # don't use `dry_run` in the cache key because this is a read-only integration
298
+ dry_run=False,
299
+ cache_source=cache_source,
300
+ shard="",
301
+ ttl_seconds=extended_early_exit_cache_ttl_seconds,
302
+ logger=logging.getLogger(),
303
+ runner=runner,
304
+ runner_params=runner_params,
305
+ secret_reader=secret_reader,
306
+ log_cached_log_output=log_cached_log_output,
307
+ )
308
+ else:
309
+ runner(**runner_params)
310
+
311
+
312
+ def runner(
313
+ exit_on_permission_errors: bool, boards: list[JiraBoardV1]
314
+ ) -> ExtendedEarlyExitRunnerResult:
260
315
  gql_api = gql.get_api()
261
316
  settings = get_jira_settings(gql_api=gql_api)
262
317
  jiralert_settings = get_jiralert_settings(query_func=gql_api.query)
263
318
  vault_settings = get_app_interface_vault_settings()
264
319
  secret_reader = create_secret_reader(use_vault=vault_settings.vault)
265
- boards = get_jira_boards(query_func=gql_api.query)
266
320
 
267
321
  with metrics.transactional_metrics("jira-boards") as metrics_container:
268
322
  error = validate_boards(
@@ -278,8 +332,8 @@ def run(dry_run: bool, exit_on_permission_errors: bool) -> None:
278
332
  if error:
279
333
  sys.exit(ExitCodes.ERROR)
280
334
 
335
+ return ExtendedEarlyExitRunnerResult(payload=export_boards(boards), applied_count=0)
336
+
281
337
 
282
338
  def early_exit_desired_state(*args: Any, **kwargs: Any) -> dict[str, Any]:
283
- return {
284
- "boards": gql.get_api().query(JIRA_BOARDS_DEFINITION)["jira_boards"],
285
- }
339
+ return {"boards": export_boards(get_jira_boards(query_func=gql.get_api().query))}
@@ -12,8 +12,10 @@ from pydantic import (
12
12
  )
13
13
 
14
14
  from reconcile import queries
15
+ from reconcile.gql_definitions.fragments.vault_secret import VaultSecret
15
16
  from reconcile.gql_definitions.terraform_repo.terraform_repo import (
16
17
  TerraformRepoV1,
18
+ TerraformRepoVariablesV1,
17
19
  query,
18
20
  )
19
21
  from reconcile.utils import gql
@@ -35,22 +37,19 @@ from reconcile.utils.state import (
35
37
  )
36
38
 
37
39
 
38
- class RepoSecret(BaseModel):
39
- path: str
40
- version: Optional[int]
41
-
42
-
43
40
  class RepoOutput(BaseModel):
44
41
  repository: str
45
42
  name: str
46
43
  ref: str
47
44
  project_path: str
48
45
  delete: bool
49
- secret: RepoSecret
46
+ aws_creds: VaultSecret
47
+ variables: Optional[TerraformRepoVariablesV1]
50
48
  bucket: Optional[str]
51
49
  region: Optional[str]
52
50
  bucket_path: Optional[str]
53
51
  require_fips: bool
52
+ tf_version: str
54
53
 
55
54
 
56
55
  class OutputFile(BaseModel):
@@ -126,13 +125,16 @@ class TerraformRepoIntegration(
126
125
  recreate_state=True,
127
126
  )
128
127
 
129
- def print_output(self, diff: list[TerraformRepoV1], dry_run: bool) -> None:
128
+ def print_output(self, diff: list[TerraformRepoV1], dry_run: bool) -> OutputFile:
130
129
  """Parses and prints the output of a Terraform Repo diff for the executor
131
130
 
132
131
  :param diff: list of terraform repos to be acted on
133
132
  :type diff: list[TerraformRepoV1]
134
133
  :param dry_run: whether the executor should perform a tf apply
135
134
  :type dry_run: bool
135
+
136
+ :return: output of diff (used for testing)
137
+ :rtype: OutputFile
136
138
  """
137
139
  actions_list: list[RepoOutput] = []
138
140
 
@@ -144,10 +146,9 @@ class TerraformRepoIntegration(
144
146
  project_path=repo.project_path,
145
147
  delete=repo.delete or False,
146
148
  require_fips=repo.require_fips or False,
147
- secret=RepoSecret(
148
- path=repo.account.automation_token.path,
149
- version=repo.account.automation_token.version,
150
- ),
149
+ tf_version=repo.tf_version,
150
+ aws_creds=repo.account.automation_token,
151
+ variables=repo.variables,
151
152
  )
152
153
  # terraform-repo will store its statefiles in a specified directory if there is a
153
154
  # terraform-state yaml file associated with the AWS account and a configuration is
@@ -179,6 +180,8 @@ class TerraformRepoIntegration(
179
180
  else:
180
181
  print(yaml.safe_dump(data=output.dict(), explicit_start=True))
181
182
 
183
+ return output
184
+
182
185
  def get_repos(self, query_func: Callable) -> list[TerraformRepoV1]:
183
186
  """Gets a list of terraform repos defined in App Interface
184
187
 
@@ -1,7 +1,6 @@
1
1
  from unittest.mock import MagicMock
2
2
 
3
3
  import pytest
4
- import yaml
5
4
 
6
5
  from reconcile.gql_definitions.fragments.terraform_state import (
7
6
  AWSTerraformStateIntegrationsV1,
@@ -10,9 +9,12 @@ from reconcile.gql_definitions.fragments.vault_secret import VaultSecret
10
9
  from reconcile.gql_definitions.terraform_repo.terraform_repo import (
11
10
  AWSAccountV1,
12
11
  TerraformRepoV1,
12
+ TerraformRepoVariablesV1,
13
13
  TerraformStateAWSV1,
14
14
  )
15
15
  from reconcile.terraform_repo import (
16
+ OutputFile,
17
+ RepoOutput,
16
18
  TerraformRepoIntegration,
17
19
  TerraformRepoIntegrationParams,
18
20
  )
@@ -21,8 +23,10 @@ from reconcile.utils.state import State
21
23
 
22
24
  A_REPO = "https://git-example/tf-repo-example"
23
25
  A_REPO_SHA = "a390f5cb20322c90861d6d80e9b70c6a579be1d0"
26
+ A_REPO_VERSION = "1.4.5"
24
27
  B_REPO = "https://git-example/tf-repo-example2"
25
28
  B_REPO_SHA = "94edb90815e502b387c25358f5ec602e52d0bfbb"
29
+ B_REPO_VERSION = "1.5.7"
26
30
  AWS_UID = "000000000000"
27
31
  AUTOMATION_TOKEN_PATH = "aws-secrets/terraform/foo"
28
32
  STATE_REGION = "us-east-1"
@@ -31,7 +35,7 @@ STATE_PROVIDER = "s3"
31
35
 
32
36
 
33
37
  @pytest.fixture
34
- def existing_repo(aws_account) -> TerraformRepoV1:
38
+ def existing_repo(aws_account, tf_variables) -> TerraformRepoV1:
35
39
  return TerraformRepoV1(
36
40
  name="a_repo",
37
41
  repository=A_REPO,
@@ -40,27 +44,34 @@ def existing_repo(aws_account) -> TerraformRepoV1:
40
44
  projectPath="tf",
41
45
  delete=False,
42
46
  requireFips=True,
47
+ tfVersion=A_REPO_VERSION,
48
+ variables=tf_variables,
43
49
  )
44
50
 
45
51
 
46
52
  @pytest.fixture
47
- def existing_repo_output() -> str:
48
- return f"""
49
- dry_run: true
50
- repos:
51
- - repository: {A_REPO}
52
- name: a_repo
53
- ref: {A_REPO_SHA}
54
- project_path: tf
55
- delete: false
56
- secret:
57
- path: {AUTOMATION_TOKEN_PATH}
58
- version: 1
59
- bucket: {STATE_BUCKET}
60
- region: {STATE_REGION}
61
- bucket_path: tf-repo
62
- require_fips: true
63
- """
53
+ def existing_repo_output(tf_variables) -> OutputFile:
54
+ return OutputFile(
55
+ dry_run=True,
56
+ repos=[
57
+ RepoOutput(
58
+ repository=A_REPO,
59
+ name="a_repo",
60
+ ref=A_REPO_SHA,
61
+ project_path="tf",
62
+ delete=False,
63
+ aws_creds=VaultSecret(
64
+ path=AUTOMATION_TOKEN_PATH, version=1, field="all", format=None
65
+ ),
66
+ bucket=STATE_BUCKET,
67
+ region=STATE_REGION,
68
+ bucket_path="tf-repo",
69
+ require_fips=True,
70
+ tf_version=A_REPO_VERSION,
71
+ variables=tf_variables,
72
+ )
73
+ ],
74
+ )
64
75
 
65
76
 
66
77
  @pytest.fixture
@@ -73,27 +84,34 @@ def new_repo(aws_account_no_state) -> TerraformRepoV1:
73
84
  projectPath="tf",
74
85
  delete=False,
75
86
  requireFips=False,
87
+ tfVersion=B_REPO_VERSION,
88
+ variables=None,
76
89
  )
77
90
 
78
91
 
79
92
  @pytest.fixture
80
- def new_repo_output() -> str:
81
- return f"""
82
- dry_run: true
83
- repos:
84
- - repository: {B_REPO}
85
- name: b_repo
86
- ref: {B_REPO_SHA}
87
- project_path: tf
88
- delete: false
89
- secret:
90
- path: {AUTOMATION_TOKEN_PATH}
91
- version: 1
92
- bucket: null
93
- region: null
94
- bucket_path: null
95
- require_fips: false
96
- """
93
+ def new_repo_output() -> OutputFile:
94
+ return OutputFile(
95
+ dry_run=True,
96
+ repos=[
97
+ RepoOutput(
98
+ repository=B_REPO,
99
+ name="b_repo",
100
+ ref=B_REPO_SHA,
101
+ project_path="tf",
102
+ delete=False,
103
+ aws_creds=VaultSecret(
104
+ path=AUTOMATION_TOKEN_PATH, version=1, field="all", format=None
105
+ ),
106
+ bucket=None,
107
+ region=None,
108
+ bucket_path=None,
109
+ require_fips=False,
110
+ tf_version=B_REPO_VERSION,
111
+ variables=None,
112
+ )
113
+ ],
114
+ )
97
115
 
98
116
 
99
117
  @pytest.fixture()
@@ -101,6 +119,18 @@ def automation_token() -> VaultSecret:
101
119
  return VaultSecret(path=AUTOMATION_TOKEN_PATH, version=1, field="all", format=None)
102
120
 
103
121
 
122
+ @pytest.fixture()
123
+ def tf_variables() -> TerraformRepoVariablesV1:
124
+ return TerraformRepoVariablesV1(
125
+ inputs=VaultSecret(
126
+ path="terraform-repo/inputs/abc", field="all", version=2, format=None
127
+ ),
128
+ outputs=VaultSecret(
129
+ path="terraform-repo/outputs/abc", field="all", version=2, format=None
130
+ ),
131
+ )
132
+
133
+
104
134
  @pytest.fixture()
105
135
  def terraform_state(terraform_state_integrations) -> TerraformStateAWSV1:
106
136
  return TerraformStateAWSV1(
@@ -143,13 +173,6 @@ def int_params() -> TerraformRepoIntegrationParams:
143
173
  return TerraformRepoIntegrationParams(output_file=None, validate_git=False)
144
174
 
145
175
 
146
- @pytest.fixture
147
- def int_params_print_to_tmp(tmp_path) -> TerraformRepoIntegrationParams:
148
- return TerraformRepoIntegrationParams(
149
- output_file=f"{tmp_path}/tf-repo.yaml", validate_git=False
150
- )
151
-
152
-
153
176
  @pytest.fixture()
154
177
  def state_mock() -> MagicMock:
155
178
  return MagicMock(spec=State)
@@ -253,7 +276,7 @@ def test_delete_repo_without_flag(existing_repo, int_params):
253
276
  )
254
277
 
255
278
 
256
- def test_get_repo_state(s3_state_builder, int_params, existing_repo):
279
+ def test_get_repo_state(s3_state_builder, int_params, existing_repo, tf_variables):
257
280
  state = s3_state_builder({
258
281
  "ls": [
259
282
  "/a_repo",
@@ -266,6 +289,8 @@ def test_get_repo_state(s3_state_builder, int_params, existing_repo):
266
289
  "projectPath": "tf",
267
290
  "delete": False,
268
291
  "requireFips": True,
292
+ "tfVersion": A_REPO_VERSION,
293
+ "variables": tf_variables,
269
294
  "account": {
270
295
  "name": "foo",
271
296
  "uid": AWS_UID,
@@ -319,15 +344,13 @@ def test_update_repo_state(int_params, existing_repo, state_mock):
319
344
  # these two output tests are to ensure that there isn't a sudden change to outputs that throws
320
345
  # off tf-executor
321
346
  def test_output_correct_statefile(
322
- int_params_print_to_tmp, existing_repo, existing_repo_output, tmp_path, state_mock
347
+ int_params, existing_repo, existing_repo_output, state_mock
323
348
  ):
324
- integration = TerraformRepoIntegration(params=int_params_print_to_tmp)
349
+ integration = TerraformRepoIntegration(params=int_params)
325
350
 
326
351
  existing_state: list = []
327
352
  desired_state = [existing_repo]
328
353
 
329
- expected_output = yaml.safe_load(existing_repo_output)
330
-
331
354
  diff = integration.calculate_diff(
332
355
  existing_state=existing_state,
333
356
  desired_state=desired_state,
@@ -337,24 +360,19 @@ def test_output_correct_statefile(
337
360
  )
338
361
 
339
362
  assert diff
340
- integration.print_output(diff, True)
341
-
342
- with open(f"{tmp_path}/tf-repo.yaml", "r", encoding="locale") as output:
343
- yaml_rep = yaml.safe_load(output)
363
+ current_output = integration.print_output(diff, True)
344
364
 
345
- assert expected_output == yaml_rep
365
+ assert existing_repo_output == current_output
346
366
 
347
367
 
348
368
  def test_output_correct_no_statefile(
349
- int_params_print_to_tmp, new_repo, new_repo_output, tmp_path, state_mock
369
+ int_params, new_repo, new_repo_output, tmp_path, state_mock
350
370
  ):
351
- integration = TerraformRepoIntegration(params=int_params_print_to_tmp)
371
+ integration = TerraformRepoIntegration(params=int_params)
352
372
 
353
373
  existing_state: list = []
354
374
  desired_state = [new_repo]
355
375
 
356
- expected_output = yaml.safe_load(new_repo_output)
357
-
358
376
  diff = integration.calculate_diff(
359
377
  existing_state=existing_state,
360
378
  desired_state=desired_state,
@@ -364,12 +382,9 @@ def test_output_correct_no_statefile(
364
382
  )
365
383
 
366
384
  assert diff
367
- integration.print_output(diff, True)
368
-
369
- with open(f"{tmp_path}/tf-repo.yaml", "r", encoding="locale") as output:
370
- yaml_rep = yaml.safe_load(output)
385
+ current_output = integration.print_output(diff, True)
371
386
 
372
- assert expected_output == yaml_rep
387
+ assert new_repo_output == current_output
373
388
 
374
389
 
375
390
  def test_fail_on_multiple_repos_dry_run(int_params, existing_repo, new_repo):
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import functools
3
4
  import logging
4
5
  from collections.abc import (
5
6
  Iterable,
@@ -82,6 +83,11 @@ class JiraClient:
82
83
  self.project = project
83
84
  self.jira = jira_api
84
85
 
86
+ # some caches
87
+ self.priorities = functools.lru_cache(maxsize=None)(self._priorities)
88
+ self.public_projects = functools.lru_cache(maxsize=None)(self._public_projects)
89
+ self.my_permissions = functools.lru_cache(maxsize=None)(self._my_permissions)
90
+
85
91
  def _deprecated_init(
86
92
  self, jira_board: Mapping[str, Any], settings: Optional[Mapping]
87
93
  ) -> None:
@@ -174,11 +180,12 @@ class JiraClient:
174
180
  )
175
181
  return issue
176
182
 
183
+ def _my_permissions(self, project: str) -> dict[str, Any]:
184
+ return self.jira.my_permissions(projectKey=project)["permissions"]
185
+
177
186
  def can_i(self, permission: str) -> bool:
178
187
  return bool(
179
- self.jira.my_permissions(projectKey=self.project)["permissions"][
180
- permission
181
- ]["havePermission"]
188
+ self.my_permissions(project=self.project)[permission]["havePermission"]
182
189
  )
183
190
 
184
191
  def can_create_issues(self) -> bool:
@@ -187,10 +194,10 @@ class JiraClient:
187
194
  def can_transition_issues(self) -> bool:
188
195
  return self.can_i("TRANSITION_ISSUES")
189
196
 
190
- def project_issue_types(self, project: str) -> list[IssueType]:
197
+ def project_issue_types(self) -> list[IssueType]:
191
198
  return [
192
199
  IssueType(id=t.id, name=t.name, statuses=[s.name for s in t.statuses])
193
- for t in self.jira.issue_types_for_project(project)
200
+ for t in self.jira.issue_types_for_project(self.project)
194
201
  ]
195
202
 
196
203
  def security_levels(self) -> list[SecurityLevel]:
@@ -201,7 +208,7 @@ class JiraClient:
201
208
  scheme = self.jira.project_issue_security_level_scheme(self.project)
202
209
  return [SecurityLevel(id=level.id, name=level.name) for level in scheme.levels]
203
210
 
204
- def priorities(self) -> list[Priority]:
211
+ def _priorities(self) -> list[Priority]:
205
212
  """Return a list of all available Jira priorities."""
206
213
  return [Priority(id=p.id, name=p.name) for p in self.jira.priorities()]
207
214
 
@@ -210,7 +217,7 @@ class JiraClient:
210
217
  scheme = self.jira.project_priority_scheme(self.project)
211
218
  return scheme.optionIds
212
219
 
213
- def public_projects(self) -> list[str]:
220
+ def _public_projects(self) -> list[str]:
214
221
  """Return a list of all public available projects."""
215
222
  if not self.server:
216
223
  raise RuntimeError("JiraClient.server is not set.")