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.
Files changed (41) hide show
  1. {qontract_reconcile-0.10.2.dev427.dist-info → qontract_reconcile-0.10.2.dev465.dist-info}/METADATA +2 -2
  2. {qontract_reconcile-0.10.2.dev427.dist-info → qontract_reconcile-0.10.2.dev465.dist-info}/RECORD +41 -40
  3. {qontract_reconcile-0.10.2.dev427.dist-info → qontract_reconcile-0.10.2.dev465.dist-info}/WHEEL +1 -1
  4. reconcile/aus/aus_sts_gate_handler.py +59 -0
  5. reconcile/aus/base.py +9 -8
  6. reconcile/aus/version_gate_approver.py +1 -16
  7. reconcile/aus/version_gates/sts_version_gate_handler.py +5 -125
  8. reconcile/aws_account_manager/integration.py +13 -1
  9. reconcile/aws_account_manager/utils.py +1 -1
  10. reconcile/aws_ecr_image_pull_secrets.py +1 -1
  11. reconcile/change_owners/README.md +1 -1
  12. reconcile/change_owners/change_owners.py +108 -42
  13. reconcile/change_owners/decision.py +1 -1
  14. reconcile/cli.py +1 -1
  15. reconcile/external_resources/secrets_sync.py +2 -3
  16. reconcile/gql_definitions/aws_account_manager/aws_accounts.py +9 -0
  17. reconcile/gql_definitions/common/aws_vpc_requests.py +3 -0
  18. reconcile/gql_definitions/common/clusters.py +2 -0
  19. reconcile/gql_definitions/external_resources/external_resources_namespaces.py +5 -1
  20. reconcile/gql_definitions/fragments/aws_vpc_request.py +5 -0
  21. reconcile/gql_definitions/introspection.json +60 -0
  22. reconcile/gql_definitions/rhcs/certs.py +1 -0
  23. reconcile/gql_definitions/rhcs/openshift_resource_rhcs_cert.py +1 -0
  24. reconcile/gql_definitions/terraform_resources/terraform_resources_namespaces.py +7 -1
  25. reconcile/gql_definitions/vpc_peerings_validator/vpc_peerings_validator.py +3 -0
  26. reconcile/gql_definitions/vpc_peerings_validator/vpc_peerings_validator_peered_cluster_fragment.py +1 -0
  27. reconcile/openshift_namespaces.py +3 -4
  28. reconcile/openshift_rhcs_certs.py +51 -12
  29. reconcile/templates/rosa-classic-cluster-creation.sh.j2 +1 -1
  30. reconcile/templates/rosa-hcp-cluster-creation.sh.j2 +1 -1
  31. reconcile/terraform_vpc_resources/integration.py +10 -7
  32. reconcile/typed_queries/saas_files.py +9 -4
  33. reconcile/utils/environ.py +5 -0
  34. reconcile/utils/external_resource_spec.py +2 -0
  35. reconcile/utils/gitlab_api.py +12 -0
  36. reconcile/utils/jjb_client.py +19 -3
  37. reconcile/utils/oc.py +8 -2
  38. reconcile/utils/rhcsv2_certs.py +87 -21
  39. reconcile/utils/terrascript_aws_client.py +140 -50
  40. reconcile/vpc_peerings_validator.py +13 -0
  41. {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 RhcsV2Cert, generate_cert
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, cert: Mapping[str, Any], annotations: Mapping[str, 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 = RhcsV2Cert(
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, cert_resource.service_account_name, sa_password, ca_cert_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 {{ cluster.machine_pools | length }} \
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 {{ cluster.machine_pools | length }} \
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
- accounts = [vpc.account for vpc in data]
66
-
67
- if account_name:
68
- accounts = [account for account in accounts if account.name == account_name]
69
-
70
- return accounts
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 vpc. Verify your input"
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
- return hashlib.blake2s(
82
- f"{parent_saas_file_name}:{parent_resource_template_name}:{self.name or 'default'}:{self.namespace.cluster.name}:{self.namespace.name}".encode(),
83
- digest_size=20,
84
- ).hexdigest()
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):
@@ -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:
@@ -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,
@@ -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 KeyError:
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["properties"][0]["github"]["url"]
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 KeyError:
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.is_kind_supported(PROJECT_KIND):
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.is_kind_supported(PROJECT_KIND):
678
+ if self._use_oc_project(namespace=namespace):
673
679
  cmd = ["delete", "project", namespace]
674
680
  else:
675
681
  cmd = ["delete", "namespace", namespace]
@@ -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 RhcsV2Cert(BaseModel, validate_by_name=True, validate_by_alias=True):
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 generate_cert(issuer_url: str, uid: str, pwd: str, ca_url: str) -> RhcsV2Cert:
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
- cert_pem = extract_cert(response.text)
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
- return RhcsV2Cert(
106
- private_key=private_key_pem,
107
- certificate=cert_pem.group(1)
108
- .encode()
109
- .decode("unicode_escape")
110
- .replace("\\/", "/"),
111
- ca_cert=ca_pem,
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)