qontract-reconcile 0.10.2.dev345__py3-none-any.whl → 0.10.2.dev408__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 (126) hide show
  1. {qontract_reconcile-0.10.2.dev345.dist-info → qontract_reconcile-0.10.2.dev408.dist-info}/METADATA +11 -10
  2. {qontract_reconcile-0.10.2.dev345.dist-info → qontract_reconcile-0.10.2.dev408.dist-info}/RECORD +126 -120
  3. reconcile/aus/base.py +17 -14
  4. reconcile/automated_actions/config/integration.py +12 -0
  5. reconcile/aws_account_manager/integration.py +2 -2
  6. reconcile/aws_ami_cleanup/integration.py +6 -7
  7. reconcile/aws_ami_share.py +69 -62
  8. reconcile/aws_cloudwatch_log_retention/integration.py +155 -126
  9. reconcile/aws_ecr_image_pull_secrets.py +2 -2
  10. reconcile/aws_iam_keys.py +1 -0
  11. reconcile/aws_saml_idp/integration.py +7 -1
  12. reconcile/aws_saml_roles/integration.py +9 -3
  13. reconcile/change_owners/change_owners.py +1 -1
  14. reconcile/change_owners/diff.py +2 -4
  15. reconcile/checkpoint.py +11 -3
  16. reconcile/cli.py +33 -8
  17. reconcile/dashdotdb_dora.py +4 -11
  18. reconcile/database_access_manager.py +118 -111
  19. reconcile/endpoints_discovery/integration.py +4 -1
  20. reconcile/endpoints_discovery/merge_request_manager.py +9 -11
  21. reconcile/external_resources/factories.py +5 -12
  22. reconcile/external_resources/integration.py +1 -1
  23. reconcile/external_resources/manager.py +5 -3
  24. reconcile/external_resources/meta.py +0 -1
  25. reconcile/external_resources/model.py +10 -10
  26. reconcile/external_resources/reconciler.py +5 -2
  27. reconcile/external_resources/secrets_sync.py +4 -6
  28. reconcile/external_resources/state.py +5 -4
  29. reconcile/gabi_authorized_users.py +8 -5
  30. reconcile/gitlab_housekeeping.py +13 -15
  31. reconcile/gitlab_mr_sqs_consumer.py +2 -2
  32. reconcile/gitlab_owners.py +15 -11
  33. reconcile/gql_definitions/automated_actions/instance.py +41 -2
  34. reconcile/gql_definitions/aws_ami_cleanup/aws_accounts.py +10 -0
  35. reconcile/gql_definitions/aws_cloudwatch_log_retention/aws_accounts.py +22 -61
  36. reconcile/gql_definitions/aws_saml_idp/aws_accounts.py +10 -0
  37. reconcile/gql_definitions/aws_saml_roles/aws_accounts.py +10 -0
  38. reconcile/gql_definitions/common/aws_vpc_requests.py +10 -0
  39. reconcile/gql_definitions/common/clusters.py +2 -0
  40. reconcile/gql_definitions/external_resources/external_resources_namespaces.py +84 -1
  41. reconcile/gql_definitions/external_resources/external_resources_settings.py +2 -0
  42. reconcile/gql_definitions/fragments/aws_account_common.py +2 -0
  43. reconcile/gql_definitions/fragments/aws_organization.py +33 -0
  44. reconcile/gql_definitions/fragments/aws_vpc_request.py +2 -0
  45. reconcile/gql_definitions/introspection.json +3474 -1986
  46. reconcile/gql_definitions/jira_permissions_validator/jira_boards_for_permissions_validator.py +4 -0
  47. reconcile/gql_definitions/terraform_init/aws_accounts.py +14 -0
  48. reconcile/gql_definitions/terraform_resources/terraform_resources_namespaces.py +33 -1
  49. reconcile/gql_definitions/terraform_tgw_attachments/aws_accounts.py +10 -0
  50. reconcile/jenkins_worker_fleets.py +1 -0
  51. reconcile/jira_permissions_validator.py +236 -121
  52. reconcile/ocm/types.py +6 -0
  53. reconcile/openshift_base.py +47 -1
  54. reconcile/openshift_cluster_bots.py +2 -1
  55. reconcile/openshift_resources_base.py +6 -2
  56. reconcile/openshift_saas_deploy.py +2 -2
  57. reconcile/openshift_saas_deploy_trigger_cleaner.py +3 -5
  58. reconcile/openshift_upgrade_watcher.py +3 -3
  59. reconcile/queries.py +131 -0
  60. reconcile/saas_auto_promotions_manager/subscriber.py +4 -3
  61. reconcile/slack_usergroups.py +4 -3
  62. reconcile/sql_query.py +1 -0
  63. reconcile/statuspage/integrations/maintenances.py +4 -3
  64. reconcile/statuspage/status.py +5 -8
  65. reconcile/templates/rosa-classic-cluster-creation.sh.j2 +4 -0
  66. reconcile/templates/rosa-hcp-cluster-creation.sh.j2 +3 -0
  67. reconcile/templating/renderer.py +2 -1
  68. reconcile/terraform_aws_route53.py +7 -1
  69. reconcile/terraform_init/integration.py +185 -21
  70. reconcile/terraform_resources.py +11 -1
  71. reconcile/terraform_tgw_attachments.py +7 -1
  72. reconcile/terraform_users.py +7 -0
  73. reconcile/terraform_vpc_peerings.py +14 -3
  74. reconcile/terraform_vpc_resources/integration.py +7 -0
  75. reconcile/typed_queries/aws_account_tags.py +41 -0
  76. reconcile/typed_queries/saas_files.py +2 -2
  77. reconcile/utils/aggregated_list.py +4 -3
  78. reconcile/utils/aws_api.py +51 -20
  79. reconcile/utils/aws_api_typed/api.py +38 -9
  80. reconcile/utils/aws_api_typed/cloudformation.py +149 -0
  81. reconcile/utils/aws_api_typed/logs.py +73 -0
  82. reconcile/utils/datetime_util.py +67 -0
  83. reconcile/utils/differ.py +2 -3
  84. reconcile/utils/early_exit_cache.py +3 -2
  85. reconcile/utils/expiration.py +7 -3
  86. reconcile/utils/external_resource_spec.py +24 -1
  87. reconcile/utils/filtering.py +1 -1
  88. reconcile/utils/helm.py +2 -1
  89. reconcile/utils/helpers.py +1 -1
  90. reconcile/utils/jinja2/utils.py +4 -96
  91. reconcile/utils/jira_client.py +82 -63
  92. reconcile/utils/jjb_client.py +9 -12
  93. reconcile/utils/jobcontroller/controller.py +1 -1
  94. reconcile/utils/jobcontroller/models.py +17 -1
  95. reconcile/utils/json.py +32 -0
  96. reconcile/utils/merge_request_manager/merge_request_manager.py +3 -3
  97. reconcile/utils/merge_request_manager/parser.py +2 -2
  98. reconcile/utils/mr/app_interface_reporter.py +2 -2
  99. reconcile/utils/mr/base.py +2 -2
  100. reconcile/utils/mr/notificator.py +2 -2
  101. reconcile/utils/mr/update_access_report_base.py +3 -4
  102. reconcile/utils/oc.py +113 -95
  103. reconcile/utils/oc_filters.py +3 -3
  104. reconcile/utils/ocm/products.py +6 -0
  105. reconcile/utils/ocm/search_filters.py +3 -6
  106. reconcile/utils/ocm/service_log.py +3 -5
  107. reconcile/utils/openshift_resource.py +10 -5
  108. reconcile/utils/output.py +3 -2
  109. reconcile/utils/pagerduty_api.py +5 -5
  110. reconcile/utils/runtime/integration.py +1 -2
  111. reconcile/utils/runtime/runner.py +2 -2
  112. reconcile/utils/saasherder/models.py +2 -1
  113. reconcile/utils/saasherder/saasherder.py +9 -7
  114. reconcile/utils/slack_api.py +24 -2
  115. reconcile/utils/sloth.py +171 -2
  116. reconcile/utils/sqs_gateway.py +2 -1
  117. reconcile/utils/state.py +2 -1
  118. reconcile/utils/terraform_client.py +4 -3
  119. reconcile/utils/terrascript_aws_client.py +165 -111
  120. reconcile/utils/vault.py +1 -1
  121. reconcile/vault_replication.py +107 -42
  122. tools/app_interface_reporter.py +4 -4
  123. tools/cli_commands/systems_and_tools.py +5 -1
  124. tools/qontract_cli.py +25 -13
  125. {qontract_reconcile-0.10.2.dev345.dist-info → qontract_reconcile-0.10.2.dev408.dist-info}/WHEEL +0 -0
  126. {qontract_reconcile-0.10.2.dev345.dist-info → qontract_reconcile-0.10.2.dev408.dist-info}/entry_points.txt +0 -0
@@ -34,6 +34,9 @@ query JiraBoardsForPermissionValidation {
34
34
  name
35
35
  server {
36
36
  serverUrl
37
+ email {
38
+ ...VaultSecret
39
+ }
37
40
  token {
38
41
  ...VaultSecret
39
42
  }
@@ -73,6 +76,7 @@ class ConfiguredBaseModel(BaseModel):
73
76
 
74
77
  class JiraServerV1(ConfiguredBaseModel):
75
78
  server_url: str = Field(..., alias="serverUrl")
79
+ email: Optional[VaultSecret] = Field(..., alias="email")
76
80
  token: VaultSecret = Field(..., alias="token")
77
81
 
78
82
 
@@ -17,10 +17,18 @@ from pydantic import ( # noqa: F401 # pylint: disable=W0611
17
17
  Json,
18
18
  )
19
19
 
20
+ from reconcile.gql_definitions.fragments.aws_organization import AWSOrganization
20
21
  from reconcile.gql_definitions.fragments.vault_secret import VaultSecret
21
22
 
22
23
 
23
24
  DEFINITION = """
25
+ fragment AWSOrganization on AWSOrganization_v1 {
26
+ payerAccount {
27
+ organizationAccountTags
28
+ }
29
+ tags
30
+ }
31
+
24
32
  fragment VaultSecret on VaultSecret_v1 {
25
33
  path
26
34
  field
@@ -33,6 +41,7 @@ query TerraformInitAWSAccounts {
33
41
  name
34
42
  terraformUsername
35
43
  terraformState {
44
+ bucket
36
45
  region
37
46
  }
38
47
  resourcesDefaultRegion
@@ -42,6 +51,9 @@ query TerraformInitAWSAccounts {
42
51
  disable {
43
52
  integrations
44
53
  }
54
+ organization {
55
+ ...AWSOrganization
56
+ }
45
57
  }
46
58
  }
47
59
  """
@@ -54,6 +66,7 @@ class ConfiguredBaseModel(BaseModel):
54
66
 
55
67
 
56
68
  class TerraformStateAWSV1(ConfiguredBaseModel):
69
+ bucket: str = Field(..., alias="bucket")
57
70
  region: str = Field(..., alias="region")
58
71
 
59
72
 
@@ -68,6 +81,7 @@ class AWSAccountV1(ConfiguredBaseModel):
68
81
  resources_default_region: str = Field(..., alias="resourcesDefaultRegion")
69
82
  automation_token: VaultSecret = Field(..., alias="automationToken")
70
83
  disable: Optional[DisableClusterAutomationsV1] = Field(..., alias="disable")
84
+ organization: Optional[AWSOrganization] = Field(..., alias="organization")
71
85
 
72
86
 
73
87
  class TerraformInitAWSAccountsQueryData(ConfiguredBaseModel):
@@ -80,6 +80,7 @@ query TerraformResourcesNamespaces {
80
80
  }
81
81
  }
82
82
  provider
83
+ tags
83
84
  ... on NamespaceTerraformResourceRDS_v1 {
84
85
  region
85
86
  identifier
@@ -107,6 +108,17 @@ query TerraformResourcesNamespaces {
107
108
  }
108
109
  managed_by_erv2
109
110
  }
111
+ ... on NamespaceTerraformResourceRDSProxy_v1 {
112
+ region
113
+ identifier
114
+ defaults
115
+ overrides
116
+ output_resource_name
117
+ annotations
118
+ tags
119
+ managed_by_erv2
120
+ }
121
+
110
122
  ... on NamespaceTerraformResourceS3_v1 {
111
123
  region
112
124
  identifier
@@ -396,6 +408,7 @@ query TerraformResourcesNamespaces {
396
408
  }
397
409
  output_resource_name
398
410
  annotations
411
+ secret_format
399
412
  }
400
413
  ... on NamespaceTerraformResourceASG_v1 {
401
414
  region
@@ -505,9 +518,12 @@ query TerraformResourcesNamespaces {
505
518
  }
506
519
  environment {
507
520
  name
521
+ servicePhase
508
522
  }
509
523
  app {
510
524
  name
525
+ appCode
526
+ costCenter
511
527
  }
512
528
  cluster {
513
529
  name
@@ -561,6 +577,7 @@ class NamespaceTerraformResourceGenericSecretOutputFormatV1(NamespaceTerraformRe
561
577
  class NamespaceTerraformResourceAWSV1(ConfiguredBaseModel):
562
578
  output_format: Optional[Union[NamespaceTerraformResourceGenericSecretOutputFormatV1, NamespaceTerraformResourceOutputFormatV1]] = Field(..., alias="output_format")
563
579
  provider: str = Field(..., alias="provider")
580
+ tags: Optional[str] = Field(..., alias="tags")
564
581
 
565
582
 
566
583
  class AWSRDSEventNotificationV1(ConfiguredBaseModel):
@@ -593,6 +610,17 @@ class NamespaceTerraformResourceRDSV1(NamespaceTerraformResourceAWSV1):
593
610
  managed_by_erv2: Optional[bool] = Field(..., alias="managed_by_erv2")
594
611
 
595
612
 
613
+ class NamespaceTerraformResourceRDSProxyV1(NamespaceTerraformResourceAWSV1):
614
+ region: Optional[str] = Field(..., alias="region")
615
+ identifier: str = Field(..., alias="identifier")
616
+ defaults: str = Field(..., alias="defaults")
617
+ overrides: Optional[str] = Field(..., alias="overrides")
618
+ output_resource_name: Optional[str] = Field(..., alias="output_resource_name")
619
+ annotations: Optional[str] = Field(..., alias="annotations")
620
+ tags: Optional[str] = Field(..., alias="tags")
621
+ managed_by_erv2: Optional[bool] = Field(..., alias="managed_by_erv2")
622
+
623
+
596
624
  class AWSS3EventNotificationV1(ConfiguredBaseModel):
597
625
  destination_type: str = Field(..., alias="destination_type")
598
626
  destination: str = Field(..., alias="destination")
@@ -944,6 +972,7 @@ class NamespaceTerraformResourceSecretsManagerV1(NamespaceTerraformResourceAWSV1
944
972
  secret: Optional[VaultSecret] = Field(..., alias="secret")
945
973
  output_resource_name: Optional[str] = Field(..., alias="output_resource_name")
946
974
  annotations: Optional[str] = Field(..., alias="annotations")
975
+ secret_format: Optional[str] = Field(..., alias="secret_format")
947
976
 
948
977
 
949
978
  class CloudinitConfigV1(ConfiguredBaseModel):
@@ -1071,15 +1100,18 @@ class NamespaceTerraformResourceMskV1(NamespaceTerraformResourceAWSV1):
1071
1100
 
1072
1101
 
1073
1102
  class NamespaceTerraformProviderResourceAWSV1(NamespaceExternalResourceV1):
1074
- resources: list[Union[NamespaceTerraformResourceRDSV1, NamespaceTerraformResourceALBV1, NamespaceTerraformResourceRosaAuthenticatorV1, NamespaceTerraformResourceRoleV1, NamespaceTerraformResourceS3V1, NamespaceTerraformResourceASGV1, NamespaceTerraformResourceElastiCacheV1, NamespaceTerraformResourceSNSTopicV1, NamespaceTerraformResourceCloudWatchV1, NamespaceTerraformResourceServiceAccountV1, NamespaceTerraformResourceS3SQSV1, NamespaceTerraformResourceKMSV1, NamespaceTerraformResourceRosaAuthenticatorVPCEV1, NamespaceTerraformResourceMskV1, NamespaceTerraformResourceS3CloudFrontV1, NamespaceTerraformResourceElasticSearchV1, NamespaceTerraformResourceACMV1, NamespaceTerraformResourceKinesisV1, NamespaceTerraformResourceRoute53ZoneV1, NamespaceTerraformResourceSQSV1, NamespaceTerraformResourceDynamoDBV1, NamespaceTerraformResourceECRV1, NamespaceTerraformResourceS3CloudFrontPublicKeyV1, NamespaceTerraformResourceSecretsManagerV1, NamespaceTerraformResourceSecretsManagerServiceAccountV1, NamespaceTerraformResourceAWSV1]] = Field(..., alias="resources")
1103
+ resources: list[Union[NamespaceTerraformResourceRDSV1, NamespaceTerraformResourceALBV1, NamespaceTerraformResourceRosaAuthenticatorV1, NamespaceTerraformResourceRoleV1, NamespaceTerraformResourceS3V1, NamespaceTerraformResourceASGV1, NamespaceTerraformResourceRDSProxyV1, NamespaceTerraformResourceElastiCacheV1, NamespaceTerraformResourceSNSTopicV1, NamespaceTerraformResourceCloudWatchV1, NamespaceTerraformResourceServiceAccountV1, NamespaceTerraformResourceS3SQSV1, NamespaceTerraformResourceKMSV1, NamespaceTerraformResourceRosaAuthenticatorVPCEV1, NamespaceTerraformResourceMskV1, NamespaceTerraformResourceS3CloudFrontV1, NamespaceTerraformResourceElasticSearchV1, NamespaceTerraformResourceACMV1, NamespaceTerraformResourceKinesisV1, NamespaceTerraformResourceSecretsManagerV1, NamespaceTerraformResourceRoute53ZoneV1, NamespaceTerraformResourceSQSV1, NamespaceTerraformResourceDynamoDBV1, NamespaceTerraformResourceECRV1, NamespaceTerraformResourceS3CloudFrontPublicKeyV1, NamespaceTerraformResourceSecretsManagerServiceAccountV1, NamespaceTerraformResourceAWSV1]] = Field(..., alias="resources")
1075
1104
 
1076
1105
 
1077
1106
  class EnvironmentV1(ConfiguredBaseModel):
1078
1107
  name: str = Field(..., alias="name")
1108
+ service_phase: str = Field(..., alias="servicePhase")
1079
1109
 
1080
1110
 
1081
1111
  class AppV1(ConfiguredBaseModel):
1082
1112
  name: str = Field(..., alias="name")
1113
+ app_code: str = Field(..., alias="appCode")
1114
+ cost_center: str = Field(..., alias="costCenter")
1083
1115
 
1084
1116
 
1085
1117
  class ClusterSpecV1(ConfiguredBaseModel):
@@ -51,6 +51,16 @@ fragment AWSAccountCommon on AWSAccount_v1 {
51
51
  deleteKeys
52
52
  premiumSupport
53
53
  partition
54
+ organization {
55
+ ...AWSOrganization
56
+ }
57
+ }
58
+
59
+ fragment AWSOrganization on AWSOrganization_v1 {
60
+ payerAccount {
61
+ organizationAccountTags
62
+ }
63
+ tags
54
64
  }
55
65
 
56
66
  fragment TerraformState on TerraformStateAWS_v1 {
@@ -195,6 +195,7 @@ def run(dry_run: bool) -> None:
195
195
  accounts=[],
196
196
  settings=settings,
197
197
  prefetch_resources_by_schemas=["/aws/asg-defaults-1.yml"],
198
+ default_tags=None,
198
199
  )
199
200
 
200
201
  for instance in jenkins_instances:
@@ -24,7 +24,7 @@ from reconcile.typed_queries.jiralert_settings import get_jiralert_settings
24
24
  from reconcile.utils import gql, metrics
25
25
  from reconcile.utils.defer import defer
26
26
  from reconcile.utils.disabled_integrations import integration_is_enabled
27
- from reconcile.utils.jira_client import JiraClient, JiraWatcherSettings
27
+ from reconcile.utils.jira_client import IssueType, JiraClient, JiraWatcherSettings
28
28
  from reconcile.utils.runtime.integration import DesiredStateShardConfig
29
29
  from reconcile.utils.secret_reader import SecretReaderBase, create_secret_reader
30
30
  from reconcile.utils.semver_helper import make_semver
@@ -33,8 +33,6 @@ from reconcile.utils.state import State, init_state
33
33
  QONTRACT_INTEGRATION = "jira-permissions-validator"
34
34
  QONTRACT_INTEGRATION_VERSION = make_semver(1, 2, 0)
35
35
 
36
- NameToIdMap = dict[str, str]
37
-
38
36
 
39
37
  class BaseMetric(BaseModel):
40
38
  """Base class for metrics"""
@@ -64,9 +62,6 @@ class ValidationError(IntFlag):
64
62
  PROJECT_ARCHIVED = auto()
65
63
 
66
64
 
67
- CustomIssueFields = dict[str, dict[str, str]]
68
-
69
-
70
65
  class RunnerParams(TypedDict):
71
66
  boards: list[JiraBoardV1]
72
67
  board_check_interval_sec: int
@@ -77,6 +72,179 @@ class CacheSource(TypedDict):
77
72
  boards: list
78
73
 
79
74
 
75
+ NameToIdMap = dict[str, str]
76
+ CustomIssueFields = dict[str, dict[str, str]]
77
+ NO_ERRORS = ValidationError(0)
78
+
79
+
80
+ def _validate_project_archived(jira: JiraClient, board: JiraBoardV1) -> ValidationError:
81
+ """Validate that the project is not archived."""
82
+ if jira.is_archived:
83
+ logging.error(f"[{board.name}] project is archived")
84
+ return ValidationError.PROJECT_ARCHIVED
85
+ return NO_ERRORS
86
+
87
+
88
+ def _validate_permissions(jira: JiraClient, board: JiraBoardV1) -> ValidationError:
89
+ """Validate that the bot has necessary permissions to create and transition issues."""
90
+ error = NO_ERRORS
91
+
92
+ if not jira.can_create_issues():
93
+ logging.error(f"[{board.name}] can not create issues in project")
94
+ error |= ValidationError.CANT_CREATE_ISSUE
95
+
96
+ if not jira.can_transition_issues():
97
+ logging.error(
98
+ f"[{board.name}] AppSRE Jira Bot user does not have the permission to change the issue status."
99
+ )
100
+ error |= ValidationError.CANT_TRANSITION_ISSUES
101
+
102
+ return error
103
+
104
+
105
+ def _validate_components(jira: JiraClient, board: JiraBoardV1) -> ValidationError:
106
+ """Validate that all escalation policy components exist in the project."""
107
+ error = NO_ERRORS
108
+ components = jira.components()
109
+
110
+ for escalation_policy in board.escalation_policies or []:
111
+ for jira_component in escalation_policy.channels.jira_components or []:
112
+ if jira_component not in components:
113
+ logging.error(
114
+ f"[{board.name}] escalation policy '{escalation_policy.name}' references a non existing Jira component "
115
+ f"'{jira_component}'. Valid components: {components}"
116
+ )
117
+ error |= ValidationError.INVALID_COMPONENT
118
+
119
+ return error
120
+
121
+
122
+ def _validate_issue_type(
123
+ jira: JiraClient, board: JiraBoardV1, default_issue_type: str
124
+ ) -> tuple[ValidationError, IssueType | None]:
125
+ """Validate that the issue type exists and has statuses."""
126
+ error = NO_ERRORS
127
+ issue_type = board.issue_type or default_issue_type
128
+ project_issue_type = jira.get_issue_type(issue_type)
129
+
130
+ if not project_issue_type:
131
+ project_issue_types_str = ", ".join(t.name for t in jira.project_issue_types())
132
+ logging.error(
133
+ f"[{board.name}] '{issue_type}' is not a valid issue type in project. Valid issue types: {project_issue_types_str}"
134
+ )
135
+ error |= ValidationError.INVALID_ISSUE_TYPE
136
+ return error, None
137
+
138
+ if not project_issue_type.statuses:
139
+ logging.error(
140
+ f"[{board.name}] '{issue_type}' doesn't have any status. Choose a different issue type."
141
+ )
142
+ error |= ValidationError.INVALID_ISSUE_TYPE
143
+
144
+ return error, project_issue_type
145
+
146
+
147
+ def _validate_issue_states(
148
+ board: JiraBoardV1, project_issue_type: IssueType, default_reopen_state: str
149
+ ) -> ValidationError:
150
+ """Validate that reopen and resolve states are valid for the issue type."""
151
+ error = NO_ERRORS
152
+
153
+ reopen_state = board.issue_reopen_state or default_reopen_state
154
+ if reopen_state.lower() not in [t.lower() for t in project_issue_type.statuses]:
155
+ logging.error(
156
+ f"[{board.name}] '{reopen_state}' is not a valid state in project. Valid states: {project_issue_type.statuses}"
157
+ )
158
+ error |= ValidationError.INVALID_ISSUE_STATE
159
+
160
+ if board.issue_resolve_state and board.issue_resolve_state.lower() not in [
161
+ t.lower() for t in project_issue_type.statuses
162
+ ]:
163
+ logging.error(
164
+ f"[{board.name}] '{board.issue_resolve_state}' is not a valid state in project. Valid states: {project_issue_type.statuses}"
165
+ )
166
+ error |= ValidationError.INVALID_ISSUE_STATE
167
+
168
+ return error
169
+
170
+
171
+ def _validate_issue_fields(
172
+ jira: JiraClient, board: JiraBoardV1, project_issue_type: IssueType
173
+ ) -> tuple[ValidationError, CustomIssueFields]:
174
+ """Validate that custom fields exist and have valid values."""
175
+ error = NO_ERRORS
176
+ custom_fields: CustomIssueFields = {}
177
+
178
+ for field in board.issue_fields or []:
179
+ project_issue_field = jira.project_issue_field(
180
+ issue_type_id=project_issue_type.id, field=field.name
181
+ )
182
+ if not project_issue_field:
183
+ logging.error(
184
+ f"[{board.name}] '{field.name}' is not a valid field for '{project_issue_type.name}' in this project."
185
+ )
186
+ error |= ValidationError.INVALID_ISSUE_FIELD
187
+ continue
188
+
189
+ for option in project_issue_field.options:
190
+ if option == field.value:
191
+ custom_fields[project_issue_field.id] = option.dict()
192
+ break
193
+ else:
194
+ logging.error(
195
+ f"[{board.name}] '{field.name}' has an invalid value '{field.value}'. Valid values: {project_issue_field.options}"
196
+ )
197
+ error |= ValidationError.INVALID_ISSUE_FIELD
198
+
199
+ return error, custom_fields
200
+
201
+
202
+ def _validate_security_level(
203
+ board: JiraBoardV1, public_projects: Iterable[str]
204
+ ) -> ValidationError:
205
+ """Validate that public projects have a security level defined."""
206
+ if board.name in public_projects and "Security Level" not in [
207
+ f.name for f in board.issue_fields or []
208
+ ]:
209
+ logging.error(
210
+ f"[{board.name}] is a public project, but the security level is not defined. Please add 'Security Level' field to the issueFields!"
211
+ )
212
+ return ValidationError.PUBLIC_PROJECT_NO_SECURITY_LEVEL
213
+ return NO_ERRORS
214
+
215
+
216
+ def _validate_priorities(
217
+ jira: JiraClient, board: JiraBoardV1, jira_server_priorities: NameToIdMap
218
+ ) -> ValidationError:
219
+ """Validate that all priority mappings are valid for the project."""
220
+ error = NO_ERRORS
221
+ project_priorities = jira.project_priority_scheme()
222
+ project_priorities_names = [
223
+ p_name
224
+ for project_p_id in project_priorities
225
+ for p_name, p_id in jira_server_priorities.items()
226
+ if p_id == project_p_id
227
+ ]
228
+
229
+ for priority in board.severity_priority_mappings.mappings:
230
+ if priority.priority not in jira_server_priorities:
231
+ logging.error(
232
+ f"[{board.name}] {priority.priority} is not a valid Jira priority. Valid priorities: {project_priorities_names}"
233
+ )
234
+ error |= ValidationError.INVALID_PRIORITY
235
+ continue
236
+ if (
237
+ project_priorities
238
+ and jira_server_priorities[priority.priority] not in project_priorities
239
+ ):
240
+ logging.error(
241
+ f"[{board.name}] {priority.priority} is not a valid priority in project. Valid priorities: {project_priorities_names}"
242
+ )
243
+ error |= ValidationError.INVALID_PRIORITY
244
+
245
+ return error
246
+
247
+
80
248
  def board_is_valid(
81
249
  jira: JiraClient,
82
250
  board: JiraBoardV1,
@@ -85,123 +253,52 @@ def board_is_valid(
85
253
  jira_server_priorities: NameToIdMap,
86
254
  public_projects: Iterable[str],
87
255
  ) -> tuple[ValidationError, CustomIssueFields]:
88
- error = ValidationError(0)
256
+ """Validate all Jira board settings.
257
+
258
+ This method orchestrates multiple validation checks for a Jira board configuration.
259
+ Each validation is performed by a dedicated helper function that focuses on a specific
260
+ aspect of the board configuration.
261
+
262
+ Returns:
263
+ A tuple of (ValidationError flags, custom_fields dict)
264
+ """
265
+ error = NO_ERRORS
89
266
  custom_fields: CustomIssueFields = {}
90
267
 
91
268
  try:
92
- if jira.is_archived:
93
- logging.error(f"[{board.name}] project is archived")
94
- return ValidationError.PROJECT_ARCHIVED, custom_fields
269
+ # Check if project is archived (early exit)
270
+ archived_error = _validate_project_archived(jira, board)
271
+ if archived_error:
272
+ return archived_error, custom_fields
95
273
 
96
- if not jira.can_create_issues():
97
- logging.error(f"[{board.name}] can not create issues in project")
98
- error |= ValidationError.CANT_CREATE_ISSUE
274
+ # Validate permissions
275
+ error |= _validate_permissions(jira, board)
99
276
 
100
- if not jira.can_transition_issues():
101
- logging.error(
102
- f"[{board.name}] AppSRE Jira Bot user does not have the permission to change the issue status."
103
- )
104
- error |= ValidationError.CANT_TRANSITION_ISSUES
105
-
106
- components = jira.components()
107
- for escalation_policy in board.escalation_policies or []:
108
- for jira_component in escalation_policy.channels.jira_components or []:
109
- if jira_component not in components:
110
- logging.error(
111
- f"[{board.name}] escalation policy '{escalation_policy.name}' references a non existing Jira component "
112
- f"'{jira_component}'. Valid components: {components}"
113
- )
114
- error |= ValidationError.INVALID_COMPONENT
277
+ # Validate components
278
+ error |= _validate_components(jira, board)
115
279
 
116
- issue_type = board.issue_type or default_issue_type
117
- project_issue_type = jira.get_issue_type(issue_type)
118
- if not project_issue_type:
119
- project_issue_types_str = ", ".join(
120
- t.name for t in jira.project_issue_types()
121
- )
122
- logging.error(
123
- f"[{board.name}] '{issue_type}' is not a valid issue type in project. Valid issue types: {project_issue_types_str}"
124
- )
125
- error |= ValidationError.INVALID_ISSUE_TYPE
280
+ # Validate issue type
281
+ issue_type_error, project_issue_type = _validate_issue_type(
282
+ jira, board, default_issue_type
283
+ )
284
+ error |= issue_type_error
126
285
 
286
+ # If issue type is valid, perform additional validations
127
287
  if project_issue_type:
128
- # Check issue attributes
129
- if not project_issue_type.statuses:
130
- logging.error(
131
- f"[{board.name}] '{issue_type}' doesn't have any status. Choose a different issue type."
132
- )
133
- error |= ValidationError.INVALID_ISSUE_TYPE
134
-
135
- reopen_state = board.issue_reopen_state or default_reopen_state
136
- if reopen_state.lower() not in [
137
- t.lower() for t in project_issue_type.statuses
138
- ]:
139
- logging.error(
140
- f"[{board.name}] '{reopen_state}' is not a valid state in project. Valid states: {project_issue_type.statuses}"
141
- )
142
- error |= ValidationError.INVALID_ISSUE_STATE
143
-
144
- if board.issue_resolve_state and board.issue_resolve_state.lower() not in [
145
- t.lower() for t in project_issue_type.statuses
146
- ]:
147
- logging.error(
148
- f"[{board.name}] '{board.issue_resolve_state}' is not a valid state in project. Valid states: {project_issue_type.statuses}"
149
- )
150
- error |= ValidationError.INVALID_ISSUE_STATE
288
+ error |= _validate_issue_states(
289
+ board, project_issue_type, default_reopen_state
290
+ )
291
+ fields_error, custom_fields = _validate_issue_fields(
292
+ jira, board, project_issue_type
293
+ )
294
+ error |= fields_error
151
295
 
152
- for field in board.issue_fields or []:
153
- project_issue_field = jira.project_issue_field(
154
- issue_type_id=project_issue_type.id, field=field.name
155
- )
156
- if not project_issue_field:
157
- logging.error(
158
- f"[{board.name}] '{field.name}' is not a valid field for '{project_issue_type.name}' in this project."
159
- )
296
+ # Validate security level for public projects
297
+ error |= _validate_security_level(board, public_projects)
160
298
 
161
- error |= ValidationError.INVALID_ISSUE_FIELD
162
- continue
299
+ # Validate priorities
300
+ error |= _validate_priorities(jira, board, jira_server_priorities)
163
301
 
164
- for option in project_issue_field.options:
165
- if option == field.value:
166
- # cache the field id and option value/name for later use, e.g. in jiralert templates
167
- custom_fields[project_issue_field.id] = option.dict()
168
- break
169
- else:
170
- # field.value is not in the allowed options
171
- logging.error(
172
- f"[{board.name}] '{field.name}' has an invalid value '{field.value}'. Valid values: {project_issue_field.options}"
173
- )
174
- error |= ValidationError.INVALID_ISSUE_FIELD
175
- continue
176
-
177
- if board.name in public_projects and "Security Level" not in [
178
- f.name for f in board.issue_fields or []
179
- ]:
180
- logging.error(
181
- f"[{board.name}] is a public project, but the security level is not defined. Please add 'Security Level' field to the issueFields!"
182
- )
183
- error |= ValidationError.PUBLIC_PROJECT_NO_SECURITY_LEVEL
184
-
185
- project_priorities = jira.project_priority_scheme()
186
- # get the priority names from the project priorities ids
187
- project_priorities_names = [
188
- p_name
189
- for project_p_id in project_priorities
190
- for p_name, p_id in jira_server_priorities.items()
191
- if p_id == project_p_id
192
- ]
193
- for priority in board.severity_priority_mappings.mappings:
194
- if priority.priority not in jira_server_priorities:
195
- logging.error(
196
- f"[{board.name}] {priority.priority} is not a valid Jira priority. Valid priorities: {project_priorities_names}"
197
- )
198
- error |= ValidationError.INVALID_PRIORITY
199
- continue
200
- if jira_server_priorities[priority.priority] not in project_priorities:
201
- logging.error(
202
- f"[{board.name}] {priority.priority} is not a valid priority in project. Valid priorities: {project_priorities_names}"
203
- )
204
- error |= ValidationError.INVALID_PRIORITY
205
302
  except JIRAError as e:
206
303
  if e.status_code == 401:
207
304
  # sporadic 401 errors, retrying
@@ -228,25 +325,41 @@ def validate_boards(
228
325
  dry_run: bool,
229
326
  state: State,
230
327
  jira_client_class: type[JiraClient] = JiraClient,
328
+ use_cache: bool = False,
231
329
  ) -> bool:
330
+ """Validate all Jira boards.
331
+
332
+ The method iterates over all Jira boards and checks if the configuration is valid. If no errors
333
+ are found, it will skip the next check for the board until the next run time is reached.
334
+ The next run time is calculated based on the board's check interval and some randomness to avoid
335
+ all boards checking at the same time.
336
+
337
+ Additionally, for any Jira board with a permission error, a Prometheus metric will be set to trigger an alert.
338
+
339
+ Returns True if there were any errors. See ValidationError and log messages for details.
340
+ """
232
341
  error = False
233
342
  jira_clients: dict[str, JiraClient] = {}
234
343
  for board in jira_boards:
235
- next_run_time = state.get(board.name, 0)
236
- if time.time() <= next_run_time:
237
- if not dry_run:
238
- # always skip for non-dry-run mode
239
- continue
240
- # dry-run mode
241
- elif len(jira_boards) > 1:
242
- logging.info(f"[{board.name}] Use cache results. Skipping ...")
243
- continue
344
+ if use_cache:
345
+ next_run_time = state.get(board.name, 0)
346
+ if time.time() <= next_run_time:
347
+ if not dry_run:
348
+ # always skip for non-dry-run mode
349
+ continue
350
+ # dry-run mode
351
+ elif len(jira_boards) > 1:
352
+ logging.info(f"[{board.name}] Use cache results. Skipping ...")
353
+ continue
244
354
 
245
355
  logging.debug(f"[{board.name}] checking ...")
246
356
  if board.server.server_url not in jira_clients:
247
357
  jira_clients[board.server.server_url] = jira_client_class.create(
248
358
  project_name=board.name,
249
359
  token=secret_reader.read_secret(board.server.token),
360
+ email=secret_reader.read_secret(board.server.email)
361
+ if board.server.email
362
+ else None,
250
363
  server_url=board.server.server_url,
251
364
  jira_watcher_settings=jira_client_settings,
252
365
  )
@@ -326,6 +439,7 @@ def run(
326
439
  dry_run: bool,
327
440
  jira_board_name: list[str] | None = None,
328
441
  board_check_interval_sec: int = 3600,
442
+ use_cache: bool = False,
329
443
  defer: Callable | None = None,
330
444
  ) -> None:
331
445
  gql_api = gql.get_api()
@@ -349,6 +463,7 @@ def run(
349
463
  board_check_interval_sec=board_check_interval_sec,
350
464
  dry_run=dry_run,
351
465
  state=state,
466
+ use_cache=use_cache,
352
467
  )
353
468
 
354
469
  if error:
reconcile/ocm/types.py CHANGED
@@ -4,6 +4,7 @@ from pydantic import (
4
4
  BaseModel,
5
5
  Extra,
6
6
  Field,
7
+ validator,
7
8
  )
8
9
 
9
10
 
@@ -36,6 +37,11 @@ class OCMClusterSpec(BaseModel):
36
37
  initial_version: str | None
37
38
  version: str
38
39
  hypershift: bool | None
40
+ fips: bool = False
41
+
42
+ @validator("fips", pre=True)
43
+ def set_fips_default(cls, v: bool | None) -> bool:
44
+ return v or False
39
45
 
40
46
  class Config:
41
47
  extra = Extra.forbid