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.
- {qontract_reconcile-0.10.2.dev345.dist-info → qontract_reconcile-0.10.2.dev408.dist-info}/METADATA +11 -10
- {qontract_reconcile-0.10.2.dev345.dist-info → qontract_reconcile-0.10.2.dev408.dist-info}/RECORD +126 -120
- reconcile/aus/base.py +17 -14
- reconcile/automated_actions/config/integration.py +12 -0
- reconcile/aws_account_manager/integration.py +2 -2
- reconcile/aws_ami_cleanup/integration.py +6 -7
- reconcile/aws_ami_share.py +69 -62
- reconcile/aws_cloudwatch_log_retention/integration.py +155 -126
- reconcile/aws_ecr_image_pull_secrets.py +2 -2
- reconcile/aws_iam_keys.py +1 -0
- reconcile/aws_saml_idp/integration.py +7 -1
- reconcile/aws_saml_roles/integration.py +9 -3
- reconcile/change_owners/change_owners.py +1 -1
- reconcile/change_owners/diff.py +2 -4
- reconcile/checkpoint.py +11 -3
- reconcile/cli.py +33 -8
- reconcile/dashdotdb_dora.py +4 -11
- reconcile/database_access_manager.py +118 -111
- reconcile/endpoints_discovery/integration.py +4 -1
- reconcile/endpoints_discovery/merge_request_manager.py +9 -11
- reconcile/external_resources/factories.py +5 -12
- reconcile/external_resources/integration.py +1 -1
- reconcile/external_resources/manager.py +5 -3
- reconcile/external_resources/meta.py +0 -1
- reconcile/external_resources/model.py +10 -10
- reconcile/external_resources/reconciler.py +5 -2
- reconcile/external_resources/secrets_sync.py +4 -6
- reconcile/external_resources/state.py +5 -4
- reconcile/gabi_authorized_users.py +8 -5
- reconcile/gitlab_housekeeping.py +13 -15
- reconcile/gitlab_mr_sqs_consumer.py +2 -2
- reconcile/gitlab_owners.py +15 -11
- reconcile/gql_definitions/automated_actions/instance.py +41 -2
- reconcile/gql_definitions/aws_ami_cleanup/aws_accounts.py +10 -0
- reconcile/gql_definitions/aws_cloudwatch_log_retention/aws_accounts.py +22 -61
- reconcile/gql_definitions/aws_saml_idp/aws_accounts.py +10 -0
- reconcile/gql_definitions/aws_saml_roles/aws_accounts.py +10 -0
- reconcile/gql_definitions/common/aws_vpc_requests.py +10 -0
- reconcile/gql_definitions/common/clusters.py +2 -0
- reconcile/gql_definitions/external_resources/external_resources_namespaces.py +84 -1
- reconcile/gql_definitions/external_resources/external_resources_settings.py +2 -0
- reconcile/gql_definitions/fragments/aws_account_common.py +2 -0
- reconcile/gql_definitions/fragments/aws_organization.py +33 -0
- reconcile/gql_definitions/fragments/aws_vpc_request.py +2 -0
- reconcile/gql_definitions/introspection.json +3474 -1986
- reconcile/gql_definitions/jira_permissions_validator/jira_boards_for_permissions_validator.py +4 -0
- reconcile/gql_definitions/terraform_init/aws_accounts.py +14 -0
- reconcile/gql_definitions/terraform_resources/terraform_resources_namespaces.py +33 -1
- reconcile/gql_definitions/terraform_tgw_attachments/aws_accounts.py +10 -0
- reconcile/jenkins_worker_fleets.py +1 -0
- reconcile/jira_permissions_validator.py +236 -121
- reconcile/ocm/types.py +6 -0
- reconcile/openshift_base.py +47 -1
- reconcile/openshift_cluster_bots.py +2 -1
- reconcile/openshift_resources_base.py +6 -2
- reconcile/openshift_saas_deploy.py +2 -2
- reconcile/openshift_saas_deploy_trigger_cleaner.py +3 -5
- reconcile/openshift_upgrade_watcher.py +3 -3
- reconcile/queries.py +131 -0
- reconcile/saas_auto_promotions_manager/subscriber.py +4 -3
- reconcile/slack_usergroups.py +4 -3
- reconcile/sql_query.py +1 -0
- reconcile/statuspage/integrations/maintenances.py +4 -3
- reconcile/statuspage/status.py +5 -8
- reconcile/templates/rosa-classic-cluster-creation.sh.j2 +4 -0
- reconcile/templates/rosa-hcp-cluster-creation.sh.j2 +3 -0
- reconcile/templating/renderer.py +2 -1
- reconcile/terraform_aws_route53.py +7 -1
- reconcile/terraform_init/integration.py +185 -21
- reconcile/terraform_resources.py +11 -1
- reconcile/terraform_tgw_attachments.py +7 -1
- reconcile/terraform_users.py +7 -0
- reconcile/terraform_vpc_peerings.py +14 -3
- reconcile/terraform_vpc_resources/integration.py +7 -0
- reconcile/typed_queries/aws_account_tags.py +41 -0
- reconcile/typed_queries/saas_files.py +2 -2
- reconcile/utils/aggregated_list.py +4 -3
- reconcile/utils/aws_api.py +51 -20
- reconcile/utils/aws_api_typed/api.py +38 -9
- reconcile/utils/aws_api_typed/cloudformation.py +149 -0
- reconcile/utils/aws_api_typed/logs.py +73 -0
- reconcile/utils/datetime_util.py +67 -0
- reconcile/utils/differ.py +2 -3
- reconcile/utils/early_exit_cache.py +3 -2
- reconcile/utils/expiration.py +7 -3
- reconcile/utils/external_resource_spec.py +24 -1
- reconcile/utils/filtering.py +1 -1
- reconcile/utils/helm.py +2 -1
- reconcile/utils/helpers.py +1 -1
- reconcile/utils/jinja2/utils.py +4 -96
- reconcile/utils/jira_client.py +82 -63
- reconcile/utils/jjb_client.py +9 -12
- reconcile/utils/jobcontroller/controller.py +1 -1
- reconcile/utils/jobcontroller/models.py +17 -1
- reconcile/utils/json.py +32 -0
- reconcile/utils/merge_request_manager/merge_request_manager.py +3 -3
- reconcile/utils/merge_request_manager/parser.py +2 -2
- reconcile/utils/mr/app_interface_reporter.py +2 -2
- reconcile/utils/mr/base.py +2 -2
- reconcile/utils/mr/notificator.py +2 -2
- reconcile/utils/mr/update_access_report_base.py +3 -4
- reconcile/utils/oc.py +113 -95
- reconcile/utils/oc_filters.py +3 -3
- reconcile/utils/ocm/products.py +6 -0
- reconcile/utils/ocm/search_filters.py +3 -6
- reconcile/utils/ocm/service_log.py +3 -5
- reconcile/utils/openshift_resource.py +10 -5
- reconcile/utils/output.py +3 -2
- reconcile/utils/pagerduty_api.py +5 -5
- reconcile/utils/runtime/integration.py +1 -2
- reconcile/utils/runtime/runner.py +2 -2
- reconcile/utils/saasherder/models.py +2 -1
- reconcile/utils/saasherder/saasherder.py +9 -7
- reconcile/utils/slack_api.py +24 -2
- reconcile/utils/sloth.py +171 -2
- reconcile/utils/sqs_gateway.py +2 -1
- reconcile/utils/state.py +2 -1
- reconcile/utils/terraform_client.py +4 -3
- reconcile/utils/terrascript_aws_client.py +165 -111
- reconcile/utils/vault.py +1 -1
- reconcile/vault_replication.py +107 -42
- tools/app_interface_reporter.py +4 -4
- tools/cli_commands/systems_and_tools.py +5 -1
- tools/qontract_cli.py +25 -13
- {qontract_reconcile-0.10.2.dev345.dist-info → qontract_reconcile-0.10.2.dev408.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.2.dev345.dist-info → qontract_reconcile-0.10.2.dev408.dist-info}/entry_points.txt +0 -0
reconcile/gql_definitions/jira_permissions_validator/jira_boards_for_permissions_validator.py
CHANGED
|
@@ -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,
|
|
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 {
|
|
@@ -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
|
-
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
error |= ValidationError.CANT_CREATE_ISSUE
|
|
274
|
+
# Validate permissions
|
|
275
|
+
error |= _validate_permissions(jira, board)
|
|
99
276
|
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
117
|
-
project_issue_type =
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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
|
-
|
|
162
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
if
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|