qontract-reconcile 0.10.2.dev427__py3-none-any.whl → 0.10.2.dev465__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.dev427.dist-info → qontract_reconcile-0.10.2.dev465.dist-info}/METADATA +2 -2
- {qontract_reconcile-0.10.2.dev427.dist-info → qontract_reconcile-0.10.2.dev465.dist-info}/RECORD +41 -40
- {qontract_reconcile-0.10.2.dev427.dist-info → qontract_reconcile-0.10.2.dev465.dist-info}/WHEEL +1 -1
- reconcile/aus/aus_sts_gate_handler.py +59 -0
- reconcile/aus/base.py +9 -8
- reconcile/aus/version_gate_approver.py +1 -16
- reconcile/aus/version_gates/sts_version_gate_handler.py +5 -125
- reconcile/aws_account_manager/integration.py +13 -1
- reconcile/aws_account_manager/utils.py +1 -1
- reconcile/aws_ecr_image_pull_secrets.py +1 -1
- reconcile/change_owners/README.md +1 -1
- reconcile/change_owners/change_owners.py +108 -42
- reconcile/change_owners/decision.py +1 -1
- reconcile/cli.py +1 -1
- reconcile/external_resources/secrets_sync.py +2 -3
- reconcile/gql_definitions/aws_account_manager/aws_accounts.py +9 -0
- reconcile/gql_definitions/common/aws_vpc_requests.py +3 -0
- reconcile/gql_definitions/common/clusters.py +2 -0
- reconcile/gql_definitions/external_resources/external_resources_namespaces.py +5 -1
- reconcile/gql_definitions/fragments/aws_vpc_request.py +5 -0
- reconcile/gql_definitions/introspection.json +60 -0
- reconcile/gql_definitions/rhcs/certs.py +1 -0
- reconcile/gql_definitions/rhcs/openshift_resource_rhcs_cert.py +1 -0
- reconcile/gql_definitions/terraform_resources/terraform_resources_namespaces.py +7 -1
- reconcile/gql_definitions/vpc_peerings_validator/vpc_peerings_validator.py +3 -0
- reconcile/gql_definitions/vpc_peerings_validator/vpc_peerings_validator_peered_cluster_fragment.py +1 -0
- reconcile/openshift_namespaces.py +3 -4
- reconcile/openshift_rhcs_certs.py +51 -12
- reconcile/templates/rosa-classic-cluster-creation.sh.j2 +1 -1
- reconcile/templates/rosa-hcp-cluster-creation.sh.j2 +1 -1
- reconcile/terraform_vpc_resources/integration.py +10 -7
- reconcile/typed_queries/saas_files.py +9 -4
- reconcile/utils/environ.py +5 -0
- reconcile/utils/external_resource_spec.py +2 -0
- reconcile/utils/gitlab_api.py +12 -0
- reconcile/utils/jjb_client.py +19 -3
- reconcile/utils/oc.py +8 -2
- reconcile/utils/rhcsv2_certs.py +87 -21
- reconcile/utils/terrascript_aws_client.py +140 -50
- reconcile/vpc_peerings_validator.py +13 -0
- {qontract_reconcile-0.10.2.dev427.dist-info → qontract_reconcile-0.10.2.dev465.dist-info}/entry_points.txt +0 -0
|
@@ -32,7 +32,12 @@ from reconcile.utils.openshift_resource import (
|
|
|
32
32
|
ResourceInventory,
|
|
33
33
|
base64_encode_secret_field_value,
|
|
34
34
|
)
|
|
35
|
-
from reconcile.utils.rhcsv2_certs import
|
|
35
|
+
from reconcile.utils.rhcsv2_certs import (
|
|
36
|
+
CertificateFormat,
|
|
37
|
+
RhcsV2CertPem,
|
|
38
|
+
RhcsV2CertPkcs12,
|
|
39
|
+
generate_cert,
|
|
40
|
+
)
|
|
36
41
|
from reconcile.utils.runtime.integration import DesiredStateShardConfig
|
|
37
42
|
from reconcile.utils.secret_reader import create_secret_reader
|
|
38
43
|
from reconcile.utils.semver_helper import make_semver
|
|
@@ -66,6 +71,31 @@ class OpenshiftRhcsCertExpiration(GaugeMetric):
|
|
|
66
71
|
return "qontract_reconcile_rhcs_cert_expiration_timestamp"
|
|
67
72
|
|
|
68
73
|
|
|
74
|
+
def _generate_placeholder_cert(
|
|
75
|
+
cert_format: CertificateFormat,
|
|
76
|
+
) -> RhcsV2CertPem | RhcsV2CertPkcs12:
|
|
77
|
+
match cert_format:
|
|
78
|
+
case CertificateFormat.PKCS12:
|
|
79
|
+
return RhcsV2CertPkcs12(
|
|
80
|
+
pkcs12_keystore="PLACEHOLDER_KEYSTORE",
|
|
81
|
+
pkcs12_truststore="PLACEHOLDER_TRUSTSTORE",
|
|
82
|
+
expiration_timestamp=int(time.time()),
|
|
83
|
+
)
|
|
84
|
+
case CertificateFormat.PEM:
|
|
85
|
+
return RhcsV2CertPem(
|
|
86
|
+
certificate="PLACEHOLDER_CERT",
|
|
87
|
+
private_key="PLACEHOLDER_PRIVATE_KEY",
|
|
88
|
+
ca_cert="PLACEHOLDER_CA_CERT",
|
|
89
|
+
expiration_timestamp=int(time.time()),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_certificate_format(
|
|
94
|
+
cert_resource: OpenshiftResourceRhcsCert,
|
|
95
|
+
) -> CertificateFormat:
|
|
96
|
+
return CertificateFormat(cert_resource.certificate_format or "PEM")
|
|
97
|
+
|
|
98
|
+
|
|
69
99
|
def get_namespaces_with_rhcs_certs(
|
|
70
100
|
query_func: Callable,
|
|
71
101
|
cluster_name: Iterable[str] | None = None,
|
|
@@ -84,14 +114,21 @@ def get_namespaces_with_rhcs_certs(
|
|
|
84
114
|
|
|
85
115
|
|
|
86
116
|
def construct_rhcs_cert_oc_secret(
|
|
87
|
-
secret_name: str,
|
|
117
|
+
secret_name: str,
|
|
118
|
+
cert: Mapping[str, Any],
|
|
119
|
+
annotations: Mapping[str, str],
|
|
120
|
+
certificate_format: CertificateFormat,
|
|
88
121
|
) -> OR:
|
|
89
122
|
body: dict[str, Any] = {
|
|
90
123
|
"apiVersion": "v1",
|
|
91
124
|
"kind": "Secret",
|
|
92
|
-
"type": "kubernetes.io/tls",
|
|
93
125
|
"metadata": {"name": secret_name, "annotations": annotations},
|
|
94
126
|
}
|
|
127
|
+
match certificate_format:
|
|
128
|
+
case CertificateFormat.PKCS12:
|
|
129
|
+
body["type"] = "Opaque"
|
|
130
|
+
case CertificateFormat.PEM:
|
|
131
|
+
body["type"] = "kubernetes.io/tls"
|
|
95
132
|
for k, v in cert.items():
|
|
96
133
|
v = base64_encode_secret_field_value(v)
|
|
97
134
|
body.setdefault("data", {})[k] = v
|
|
@@ -145,17 +182,18 @@ def generate_vault_cert_secret(
|
|
|
145
182
|
f"Creating cert with service account credentials for '{cert_resource.service_account_name}'. cluster='{ns.cluster.name}', namespace='{ns.name}', secret='{cert_resource.secret_name}'"
|
|
146
183
|
)
|
|
147
184
|
sa_password = vault.read(cert_resource.service_account_password.model_dump())
|
|
185
|
+
cert_format = get_certificate_format(cert_resource)
|
|
186
|
+
|
|
148
187
|
if dry_run:
|
|
149
|
-
rhcs_cert =
|
|
150
|
-
certificate="PLACEHOLDER_CERT",
|
|
151
|
-
private_key="PLACEHOLDER_PRIVATE_KEY",
|
|
152
|
-
ca_cert="PLACEHOLDER_CA_CERT",
|
|
153
|
-
expiration_timestamp=int(time.time()),
|
|
154
|
-
)
|
|
188
|
+
rhcs_cert = _generate_placeholder_cert(cert_format)
|
|
155
189
|
else:
|
|
156
190
|
try:
|
|
157
191
|
rhcs_cert = generate_cert(
|
|
158
|
-
issuer_url,
|
|
192
|
+
issuer_url=issuer_url,
|
|
193
|
+
uid=cert_resource.service_account_name,
|
|
194
|
+
pwd=sa_password,
|
|
195
|
+
ca_url=ca_cert_url,
|
|
196
|
+
cert_format=cert_format,
|
|
159
197
|
)
|
|
160
198
|
except ValueError as e:
|
|
161
199
|
raise Exception(
|
|
@@ -166,12 +204,12 @@ def generate_vault_cert_secret(
|
|
|
166
204
|
)
|
|
167
205
|
vault.write(
|
|
168
206
|
secret={
|
|
169
|
-
"data": rhcs_cert.model_dump(by_alias=True),
|
|
207
|
+
"data": rhcs_cert.model_dump(by_alias=True, exclude_none=True),
|
|
170
208
|
"path": f"{vault_base_path}/{ns.cluster.name}/{ns.name}/{cert_resource.secret_name}",
|
|
171
209
|
},
|
|
172
210
|
decode_base64=False,
|
|
173
211
|
)
|
|
174
|
-
return rhcs_cert.model_dump(by_alias=True)
|
|
212
|
+
return rhcs_cert.model_dump(by_alias=True, exclude_none=True)
|
|
175
213
|
|
|
176
214
|
|
|
177
215
|
def fetch_openshift_resource_for_cert_resource(
|
|
@@ -213,6 +251,7 @@ def fetch_openshift_resource_for_cert_resource(
|
|
|
213
251
|
secret_name=cert_resource.secret_name,
|
|
214
252
|
cert=vault_cert_secret,
|
|
215
253
|
annotations=cert_resource.annotations or {},
|
|
254
|
+
certificate_format=get_certificate_format(cert_resource),
|
|
216
255
|
)
|
|
217
256
|
|
|
218
257
|
|
|
@@ -47,7 +47,7 @@ rosa create cluster -y --cluster-name={{ cluster_name }} \
|
|
|
47
47
|
--service-cidr {{ cluster.network.service }} \
|
|
48
48
|
--pod-cidr {{ cluster.network.pod }} \
|
|
49
49
|
--host-prefix 23 \
|
|
50
|
-
--replicas
|
|
50
|
+
--replicas 3 \
|
|
51
51
|
--compute-machine-type {{ cluster.machine_pools[0].instance_type }} \
|
|
52
52
|
{% if cluster.spec.disable_user_workload_monitoring -%}
|
|
53
53
|
--disable-workload-monitoring \
|
|
@@ -47,7 +47,7 @@ rosa create cluster --cluster-name={{ cluster_name }} \
|
|
|
47
47
|
--service-cidr {{ cluster.network.service }} \
|
|
48
48
|
--pod-cidr {{ cluster.network.pod }} \
|
|
49
49
|
--host-prefix 23 \
|
|
50
|
-
--replicas
|
|
50
|
+
--replicas 3 \
|
|
51
51
|
--compute-machine-type {{ cluster.machine_pools[0].instance_type }} \
|
|
52
52
|
{% if cluster.spec.private -%}
|
|
53
53
|
--private \
|
|
@@ -24,6 +24,7 @@ from reconcile.typed_queries.external_resources import get_settings
|
|
|
24
24
|
from reconcile.typed_queries.github_orgs import get_github_orgs
|
|
25
25
|
from reconcile.typed_queries.gitlab_instances import get_gitlab_instances
|
|
26
26
|
from reconcile.utils import gql
|
|
27
|
+
from reconcile.utils.disabled_integrations import integration_is_enabled
|
|
27
28
|
from reconcile.utils.runtime.integration import (
|
|
28
29
|
DesiredStateShardConfig,
|
|
29
30
|
PydanticRunParams,
|
|
@@ -62,12 +63,14 @@ class TerraformVpcResources(QontractReconcileIntegration[TerraformVpcResourcesPa
|
|
|
62
63
|
) -> list[AWSAccountV1]:
|
|
63
64
|
"""Return a list of accounts extracted from the provided VPCRequests.
|
|
64
65
|
If account_name is given returns the account object with that name."""
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
return [
|
|
67
|
+
vpc.account
|
|
68
|
+
for vpc in data
|
|
69
|
+
if (
|
|
70
|
+
integration_is_enabled(self.name, vpc.account)
|
|
71
|
+
and (not account_name or vpc.account.name == account_name)
|
|
72
|
+
)
|
|
73
|
+
]
|
|
71
74
|
|
|
72
75
|
def _handle_outputs(
|
|
73
76
|
self, requests: Iterable[VPCRequest], outputs: Mapping[str, Any]
|
|
@@ -155,7 +158,7 @@ class TerraformVpcResources(QontractReconcileIntegration[TerraformVpcResourcesPa
|
|
|
155
158
|
if data:
|
|
156
159
|
accounts = self._filter_accounts(data, account_name)
|
|
157
160
|
if account_name and not accounts:
|
|
158
|
-
msg = f"The account {account_name} doesn't have any managed
|
|
161
|
+
msg = f"The account {account_name} doesn't have any managed vpcs or the {QONTRACT_INTEGRATION} integration is disabled for this account. Verify your input"
|
|
159
162
|
logging.debug(msg)
|
|
160
163
|
sys.exit(ExitCodes.SUCCESS)
|
|
161
164
|
else:
|
|
@@ -42,6 +42,7 @@ from reconcile.gql_definitions.fragments.saas_target_namespace import (
|
|
|
42
42
|
SaasTargetNamespace,
|
|
43
43
|
)
|
|
44
44
|
from reconcile.utils import gql
|
|
45
|
+
from reconcile.utils.environ import used_for_security_is_enabled
|
|
45
46
|
from reconcile.utils.exceptions import (
|
|
46
47
|
AppInterfaceSettingsError,
|
|
47
48
|
ParameterError,
|
|
@@ -78,10 +79,14 @@ class SaasResourceTemplateTarget(
|
|
|
78
79
|
self, parent_saas_file_name: str, parent_resource_template_name: str
|
|
79
80
|
) -> str:
|
|
80
81
|
"""Returns a unique identifier for a target."""
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
digest_size
|
|
84
|
-
|
|
82
|
+
data = f"{parent_saas_file_name}:{parent_resource_template_name}:{self.name or 'default'}:{self.namespace.cluster.name}:{self.namespace.name}".encode()
|
|
83
|
+
if used_for_security_is_enabled():
|
|
84
|
+
# When USED_FOR_SECURITY is enabled, use blake2s without digest_size and truncate to 20 bytes
|
|
85
|
+
# This is needed for FIPS compliance where digest_size parameter is not supported
|
|
86
|
+
return hashlib.sha256(data).digest()[:20].hex()
|
|
87
|
+
else:
|
|
88
|
+
# Default behavior: use blake2s with digest_size=20
|
|
89
|
+
return hashlib.blake2s(data, digest_size=20).hexdigest()
|
|
85
90
|
|
|
86
91
|
|
|
87
92
|
class SaasResourceTemplate(ConfiguredBaseModel, validate_by_alias=True):
|
reconcile/utils/environ.py
CHANGED
|
@@ -4,6 +4,11 @@ from functools import wraps
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
def used_for_security_is_enabled() -> bool:
|
|
8
|
+
used_for_security_env = os.getenv("USED_FOR_SECURITY", "false")
|
|
9
|
+
return used_for_security_env.lower() == "true"
|
|
10
|
+
|
|
11
|
+
|
|
7
12
|
def environ(variables: Iterable[str] | None = None) -> Callable:
|
|
8
13
|
"""Check that environment variables are set before execution."""
|
|
9
14
|
if variables is None:
|
|
@@ -157,6 +157,8 @@ class ExternalResourceSpec:
|
|
|
157
157
|
tags["cost-center"] = cost_center
|
|
158
158
|
if service_phase := self.namespace["environment"].get("servicePhase"):
|
|
159
159
|
tags["service-phase"] = service_phase
|
|
160
|
+
if cost_center := self.namespace["environment"].get("costCenter"):
|
|
161
|
+
tags["cost-center"] = cost_center
|
|
160
162
|
|
|
161
163
|
resource_tags_str = self.resource.get("tags")
|
|
162
164
|
if resource_tags_str:
|
reconcile/utils/gitlab_api.py
CHANGED
|
@@ -444,6 +444,8 @@ class GitLabApi:
|
|
|
444
444
|
def get_merge_request_comments(
|
|
445
445
|
merge_request: ProjectMergeRequest,
|
|
446
446
|
include_description: bool = False,
|
|
447
|
+
include_approvals: bool = False,
|
|
448
|
+
approval_body: str = "",
|
|
447
449
|
) -> list[Comment]:
|
|
448
450
|
comments = []
|
|
449
451
|
if include_description:
|
|
@@ -455,6 +457,16 @@ class GitLabApi:
|
|
|
455
457
|
created_at=merge_request.created_at,
|
|
456
458
|
)
|
|
457
459
|
)
|
|
460
|
+
if include_approvals:
|
|
461
|
+
comments.extend(
|
|
462
|
+
Comment(
|
|
463
|
+
id=approval["user"]["id"],
|
|
464
|
+
username=approval["user"]["username"],
|
|
465
|
+
body=approval_body,
|
|
466
|
+
created_at=approval["approved_at"],
|
|
467
|
+
)
|
|
468
|
+
for approval in merge_request.approvals.get().approved_by
|
|
469
|
+
)
|
|
458
470
|
comments.extend(
|
|
459
471
|
Comment(
|
|
460
472
|
id=note.id,
|
reconcile/utils/jjb_client.py
CHANGED
|
@@ -33,6 +33,10 @@ from reconcile.utils.vcs import GITHUB_BASE_URL
|
|
|
33
33
|
JJB_INI = "[jenkins]\nurl = https://JENKINS_URL"
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
class MissingJobUrlError(Exception):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
36
40
|
class JJB:
|
|
37
41
|
"""Wrapper around Jenkins Jobs"""
|
|
38
42
|
|
|
@@ -335,7 +339,7 @@ class JJB:
|
|
|
335
339
|
job_name = job["name"]
|
|
336
340
|
try:
|
|
337
341
|
repos.add(self.get_repo_url(job))
|
|
338
|
-
except
|
|
342
|
+
except MissingJobUrlError:
|
|
339
343
|
logging.debug(f"missing github url: {job_name}")
|
|
340
344
|
return repos
|
|
341
345
|
|
|
@@ -355,7 +359,19 @@ class JJB:
|
|
|
355
359
|
|
|
356
360
|
@staticmethod
|
|
357
361
|
def get_repo_url(job: Mapping[str, Any]) -> str:
|
|
358
|
-
repo_url_raw = job
|
|
362
|
+
repo_url_raw = job.get("properties", [{}])[0].get("github", {}).get("url")
|
|
363
|
+
|
|
364
|
+
# we may be in a Github Branch Source type of job
|
|
365
|
+
if not repo_url_raw:
|
|
366
|
+
gh_org = job.get("scm", [{}])[0].get("github", {}).get("repo-owner")
|
|
367
|
+
gh_repo = job.get("scm", [{}])[0].get("github", {}).get("repo")
|
|
368
|
+
if gh_org and gh_repo:
|
|
369
|
+
repo_url_raw = f"https://github.com/{gh_org}/{gh_repo}/"
|
|
370
|
+
else:
|
|
371
|
+
raise MissingJobUrlError(
|
|
372
|
+
f"Cannot find job url for {job['display-name']}"
|
|
373
|
+
)
|
|
374
|
+
|
|
359
375
|
return repo_url_raw.strip("/").replace(".git", "")
|
|
360
376
|
|
|
361
377
|
@staticmethod
|
|
@@ -404,7 +420,7 @@ class JJB:
|
|
|
404
420
|
try:
|
|
405
421
|
if self.get_repo_url(job).lower() == repo_url.rstrip("/").lower():
|
|
406
422
|
return job
|
|
407
|
-
except
|
|
423
|
+
except MissingJobUrlError:
|
|
408
424
|
# something wrong here. ignore this job
|
|
409
425
|
pass
|
|
410
426
|
raise ValueError(f"job with {job_type=} and {repo_url=} not found")
|
reconcile/utils/oc.py
CHANGED
|
@@ -651,9 +651,15 @@ class OCCli:
|
|
|
651
651
|
raise e
|
|
652
652
|
return True
|
|
653
653
|
|
|
654
|
+
def _use_oc_project(self, namespace: str) -> bool:
|
|
655
|
+
# Note, that openshift-* namespaces cannot be created via new-project
|
|
656
|
+
return self.is_kind_supported(PROJECT_KIND) and not namespace.startswith(
|
|
657
|
+
"openshift-"
|
|
658
|
+
)
|
|
659
|
+
|
|
654
660
|
@OCDecorators.process_reconcile_time
|
|
655
661
|
def new_project(self, namespace: str) -> OCProcessReconcileTimeDecoratorMsg:
|
|
656
|
-
if self.
|
|
662
|
+
if self._use_oc_project(namespace=namespace):
|
|
657
663
|
cmd = ["new-project", namespace]
|
|
658
664
|
else:
|
|
659
665
|
cmd = ["create", "namespace", namespace]
|
|
@@ -669,7 +675,7 @@ class OCCli:
|
|
|
669
675
|
|
|
670
676
|
@OCDecorators.process_reconcile_time
|
|
671
677
|
def delete_project(self, namespace: str) -> OCProcessReconcileTimeDecoratorMsg:
|
|
672
|
-
if self.
|
|
678
|
+
if self._use_oc_project(namespace=namespace):
|
|
673
679
|
cmd = ["delete", "project", namespace]
|
|
674
680
|
else:
|
|
675
681
|
cmd = ["delete", "namespace", namespace]
|
reconcile/utils/rhcsv2_certs.py
CHANGED
|
@@ -1,21 +1,35 @@
|
|
|
1
|
+
import base64
|
|
1
2
|
import re
|
|
2
3
|
from datetime import UTC
|
|
4
|
+
from enum import StrEnum
|
|
3
5
|
|
|
4
6
|
import requests
|
|
5
7
|
from cryptography import x509
|
|
6
8
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
7
9
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
10
|
+
from cryptography.hazmat.primitives.serialization import pkcs12
|
|
8
11
|
from cryptography.x509.oid import NameOID
|
|
9
12
|
from pydantic import BaseModel, Field
|
|
10
13
|
|
|
11
14
|
|
|
12
|
-
class
|
|
15
|
+
class CertificateFormat(StrEnum):
|
|
16
|
+
PEM = "PEM"
|
|
17
|
+
PKCS12 = "PKCS12"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RhcsV2CertPem(BaseModel, validate_by_name=True, validate_by_alias=True):
|
|
13
21
|
certificate: str = Field(alias="tls.crt")
|
|
14
22
|
private_key: str = Field(alias="tls.key")
|
|
15
23
|
ca_cert: str = Field(alias="ca.crt")
|
|
16
24
|
expiration_timestamp: int
|
|
17
25
|
|
|
18
26
|
|
|
27
|
+
class RhcsV2CertPkcs12(BaseModel, validate_by_name=True, validate_by_alias=True):
|
|
28
|
+
pkcs12_keystore: str = Field(alias="keystore.pkcs12.b64")
|
|
29
|
+
pkcs12_truststore: str = Field(alias="truststore.pkcs12.b64")
|
|
30
|
+
expiration_timestamp: int
|
|
31
|
+
|
|
32
|
+
|
|
19
33
|
def extract_cert(text: str) -> re.Match:
|
|
20
34
|
# The CA webform returns an HTML page with inline JS that builds an array of “outputList”
|
|
21
35
|
# objects. Each object looks roughly like:
|
|
@@ -67,7 +81,66 @@ def get_cert_expiry_timestamp(js_escaped_pem: str) -> int:
|
|
|
67
81
|
return int(dt_expiry.timestamp())
|
|
68
82
|
|
|
69
83
|
|
|
70
|
-
def
|
|
84
|
+
def _format_pem(
|
|
85
|
+
private_key: rsa.RSAPrivateKey,
|
|
86
|
+
cert_pem: str,
|
|
87
|
+
ca_pem: str,
|
|
88
|
+
cert_expiry_timestamp: int,
|
|
89
|
+
) -> RhcsV2CertPem:
|
|
90
|
+
"""Generate RhcsV2Cert with PEM components."""
|
|
91
|
+
private_key_pem = private_key.private_bytes(
|
|
92
|
+
encoding=serialization.Encoding.PEM,
|
|
93
|
+
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
94
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
95
|
+
).decode()
|
|
96
|
+
return RhcsV2CertPem(
|
|
97
|
+
private_key=private_key_pem,
|
|
98
|
+
certificate=cert_pem.encode().decode("unicode_escape").replace("\\/", "/"),
|
|
99
|
+
ca_cert=ca_pem,
|
|
100
|
+
expiration_timestamp=cert_expiry_timestamp,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _format_pkcs12(
|
|
105
|
+
private_key: rsa.RSAPrivateKey,
|
|
106
|
+
cert_pem: str,
|
|
107
|
+
ca_pem: str,
|
|
108
|
+
uid: str,
|
|
109
|
+
pwd: str,
|
|
110
|
+
cert_expiry_timestamp: int,
|
|
111
|
+
) -> RhcsV2CertPkcs12:
|
|
112
|
+
"""Generate PKCS#12 keystore and truststore components, returns base64-encoded strings."""
|
|
113
|
+
clean_cert_pem = cert_pem.encode().decode("unicode_escape").replace("\\/", "/")
|
|
114
|
+
cert_obj = x509.load_pem_x509_certificate(clean_cert_pem.encode())
|
|
115
|
+
ca_obj = x509.load_pem_x509_certificate(ca_pem.encode())
|
|
116
|
+
keystore_p12 = pkcs12.serialize_key_and_certificates(
|
|
117
|
+
name=uid.encode("utf-8"),
|
|
118
|
+
key=private_key,
|
|
119
|
+
cert=cert_obj,
|
|
120
|
+
cas=[ca_obj],
|
|
121
|
+
encryption_algorithm=serialization.BestAvailableEncryption(pwd.encode("utf-8")),
|
|
122
|
+
)
|
|
123
|
+
truststore_p12 = pkcs12.serialize_key_and_certificates(
|
|
124
|
+
name=b"ca-trust",
|
|
125
|
+
key=None,
|
|
126
|
+
cert=None,
|
|
127
|
+
cas=[ca_obj],
|
|
128
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
129
|
+
)
|
|
130
|
+
return RhcsV2CertPkcs12(
|
|
131
|
+
pkcs12_keystore=base64.b64encode(keystore_p12).decode("utf-8"),
|
|
132
|
+
pkcs12_truststore=base64.b64encode(truststore_p12).decode("utf-8"),
|
|
133
|
+
expiration_timestamp=cert_expiry_timestamp,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def generate_cert(
|
|
138
|
+
issuer_url: str,
|
|
139
|
+
uid: str,
|
|
140
|
+
pwd: str,
|
|
141
|
+
ca_url: str,
|
|
142
|
+
cert_format: CertificateFormat = CertificateFormat.PEM,
|
|
143
|
+
) -> RhcsV2CertPem | RhcsV2CertPkcs12:
|
|
71
144
|
private_key = rsa.generate_private_key(65537, 4096)
|
|
72
145
|
csr = (
|
|
73
146
|
x509.CertificateSigningRequestBuilder()
|
|
@@ -78,6 +151,7 @@ def generate_cert(issuer_url: str, uid: str, pwd: str, ca_url: str) -> RhcsV2Cer
|
|
|
78
151
|
)
|
|
79
152
|
.sign(private_key, hashes.SHA256())
|
|
80
153
|
)
|
|
154
|
+
|
|
81
155
|
data = {
|
|
82
156
|
"uid": uid,
|
|
83
157
|
"pwd": pwd,
|
|
@@ -87,27 +161,19 @@ def generate_cert(issuer_url: str, uid: str, pwd: str, ca_url: str) -> RhcsV2Cer
|
|
|
87
161
|
"renewal": "false",
|
|
88
162
|
"xmlOutput": "false",
|
|
89
163
|
}
|
|
90
|
-
response = requests.post(issuer_url, data=data)
|
|
164
|
+
response = requests.post(issuer_url, data=data, timeout=30)
|
|
91
165
|
response.raise_for_status()
|
|
166
|
+
cert_pem = extract_cert(response.text).group(1)
|
|
167
|
+
cert_expiry_timestamp = get_cert_expiry_timestamp(cert_pem)
|
|
92
168
|
|
|
93
|
-
|
|
94
|
-
cert_expiry_timestamp = get_cert_expiry_timestamp(cert_pem.group(1))
|
|
95
|
-
private_key_pem = private_key.private_bytes(
|
|
96
|
-
encoding=serialization.Encoding.PEM,
|
|
97
|
-
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
98
|
-
encryption_algorithm=serialization.NoEncryption(),
|
|
99
|
-
).decode()
|
|
100
|
-
|
|
101
|
-
response = requests.get(ca_url)
|
|
169
|
+
response = requests.get(ca_url, timeout=30)
|
|
102
170
|
response.raise_for_status()
|
|
103
171
|
ca_pem = response.text
|
|
104
172
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
.
|
|
111
|
-
|
|
112
|
-
expiration_timestamp=cert_expiry_timestamp,
|
|
113
|
-
)
|
|
173
|
+
match cert_format:
|
|
174
|
+
case CertificateFormat.PKCS12:
|
|
175
|
+
return _format_pkcs12(
|
|
176
|
+
private_key, cert_pem, ca_pem, uid, pwd, cert_expiry_timestamp
|
|
177
|
+
)
|
|
178
|
+
case CertificateFormat.PEM:
|
|
179
|
+
return _format_pem(private_key, cert_pem, ca_pem, cert_expiry_timestamp)
|