qontract-reconcile 0.10.2.dev159__py3-none-any.whl → 0.10.2.dev173__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.dev159.dist-info → qontract_reconcile-0.10.2.dev173.dist-info}/METADATA +2 -2
- {qontract_reconcile-0.10.2.dev159.dist-info → qontract_reconcile-0.10.2.dev173.dist-info}/RECORD +34 -24
- reconcile/acs_rbac.py +1 -0
- reconcile/aws_cloudwatch_log_retention/integration.py +39 -25
- reconcile/cli.py +4 -6
- reconcile/dashdotdb_slo.py +45 -156
- reconcile/gcp_image_mirror.py +252 -0
- reconcile/gitlab_housekeeping.py +1 -1
- reconcile/gql_definitions/aws_cloudwatch_log_retention/__init__.py +0 -0
- reconcile/gql_definitions/aws_cloudwatch_log_retention/aws_accounts.py +158 -0
- reconcile/gql_definitions/common/saas_files.py +49 -0
- reconcile/gql_definitions/dashdotdb_slo/slo_documents_query.py +15 -67
- reconcile/gql_definitions/fragments/container_image_mirror.py +33 -0
- reconcile/gql_definitions/fragments/saas_slo_document.py +82 -0
- reconcile/gql_definitions/gcp/__init__.py +0 -0
- reconcile/gql_definitions/gcp/gcp_docker_repos.py +128 -0
- reconcile/gql_definitions/gcp/gcp_projects.py +77 -0
- reconcile/gql_definitions/introspection.json +380 -230
- reconcile/quay_mirror.py +3 -42
- reconcile/quay_mirror_org.py +3 -2
- reconcile/slack_base.py +2 -2
- reconcile/typed_queries/aws_cloudwatch_log_retention/aws_accounts.py +12 -0
- reconcile/utils/dynatrace/client.py +0 -31
- reconcile/utils/quay_mirror.py +42 -0
- reconcile/utils/saasherder/interfaces.py +2 -0
- reconcile/utils/saasherder/saasherder.py +5 -0
- reconcile/utils/slack_api.py +3 -1
- reconcile/utils/slo_document_manager.py +278 -0
- reconcile/utils/terrascript_aws_client.py +57 -0
- tools/{sd_app_sre_alert_report.py → alert_report.py} +1 -1
- tools/cli_commands/erv2.py +61 -0
- tools/qontract_cli.py +15 -5
- reconcile/gcr_mirror.py +0 -278
- {qontract_reconcile-0.10.2.dev159.dist-info → qontract_reconcile-0.10.2.dev173.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.2.dev159.dist-info → qontract_reconcile-0.10.2.dev173.dist-info}/entry_points.txt +0 -0
reconcile/quay_mirror.py
CHANGED
@@ -28,9 +28,9 @@ from reconcile.utils import (
|
|
28
28
|
metrics,
|
29
29
|
sharding,
|
30
30
|
)
|
31
|
-
from reconcile.utils.helpers import match_patterns
|
32
31
|
from reconcile.utils.instrumented_wrappers import InstrumentedImage as Image
|
33
32
|
from reconcile.utils.instrumented_wrappers import InstrumentedSkopeo as Skopeo
|
33
|
+
from reconcile.utils.quay_mirror import record_timestamp, sync_tag
|
34
34
|
from reconcile.utils.secret_reader import SecretReader
|
35
35
|
|
36
36
|
_LOG = logging.getLogger(__name__)
|
@@ -136,7 +136,7 @@ class QuayMirror:
|
|
136
136
|
_LOG.error("skopeo command error message: '%s'", details)
|
137
137
|
|
138
138
|
if self.is_compare_tags and not self.dry_run:
|
139
|
-
|
139
|
+
record_timestamp(self.control_file_path)
|
140
140
|
|
141
141
|
@classmethod
|
142
142
|
def process_repos_query(
|
@@ -203,40 +203,6 @@ class QuayMirror:
|
|
203
203
|
})
|
204
204
|
return summary
|
205
205
|
|
206
|
-
@staticmethod
|
207
|
-
def sync_tag(
|
208
|
-
tags: Iterable[str] | None,
|
209
|
-
tags_exclude: Iterable[str] | None,
|
210
|
-
candidate: str,
|
211
|
-
) -> bool:
|
212
|
-
"""
|
213
|
-
Determine if the candidate tag should sync, tags_exclude check take precedence.
|
214
|
-
:param tags: regex patterns to filter, match means to sync, None means no filter
|
215
|
-
:param tags_exclude: regex patterns to filter, match means not to sync, None means no filter
|
216
|
-
:param candidate: tag to check
|
217
|
-
:return: bool, True means to sync, False means not to sync
|
218
|
-
"""
|
219
|
-
if not tags and not tags_exclude:
|
220
|
-
return True
|
221
|
-
|
222
|
-
if not tags:
|
223
|
-
# only tags_exclude provided
|
224
|
-
assert tags_exclude # mypy can't infer not None
|
225
|
-
return not match_patterns(tags_exclude, candidate)
|
226
|
-
|
227
|
-
if not tags_exclude:
|
228
|
-
# only tags provided
|
229
|
-
return match_patterns(tags, candidate)
|
230
|
-
|
231
|
-
# both tags and tags_exclude provided
|
232
|
-
return not match_patterns(
|
233
|
-
tags_exclude,
|
234
|
-
candidate,
|
235
|
-
) and match_patterns(
|
236
|
-
tags,
|
237
|
-
candidate,
|
238
|
-
)
|
239
|
-
|
240
206
|
def process_sync_tasks(self):
|
241
207
|
if self.is_compare_tags:
|
242
208
|
_LOG.warning("Making a compare-tags run. This is a slow operation.")
|
@@ -282,7 +248,7 @@ class QuayMirror:
|
|
282
248
|
tags_exclude = item["mirror"].get("tagsExclude")
|
283
249
|
|
284
250
|
for tag in image_mirror:
|
285
|
-
if not
|
251
|
+
if not sync_tag(
|
286
252
|
tags=tags, tags_exclude=tags_exclude, candidate=tag
|
287
253
|
):
|
288
254
|
continue
|
@@ -390,11 +356,6 @@ class QuayMirror:
|
|
390
356
|
next_compare_tags = last_compare_tags + interval
|
391
357
|
return time.time() >= next_compare_tags
|
392
358
|
|
393
|
-
@staticmethod
|
394
|
-
def record_timestamp(path) -> None:
|
395
|
-
with open(path, "w", encoding="locale") as file_object:
|
396
|
-
file_object.write(str(time.time()))
|
397
|
-
|
398
359
|
def _get_push_creds(self):
|
399
360
|
result = self.gqlapi.query(self.QUAY_ORG_CATALOG_QUERY)
|
400
361
|
|
reconcile/quay_mirror_org.py
CHANGED
@@ -18,6 +18,7 @@ from sretoolbox.container.skopeo import SkopeoCmdError
|
|
18
18
|
|
19
19
|
from reconcile.quay_base import get_quay_api_store
|
20
20
|
from reconcile.quay_mirror import QuayMirror
|
21
|
+
from reconcile.utils.quay_mirror import record_timestamp, sync_tag
|
21
22
|
|
22
23
|
_LOG = logging.getLogger(__name__)
|
23
24
|
|
@@ -80,7 +81,7 @@ class QuayMirrorOrg:
|
|
80
81
|
_LOG.error("skopeo command error message: '%s'", details)
|
81
82
|
|
82
83
|
if self.is_compare_tags and not self.dry_run:
|
83
|
-
|
84
|
+
record_timestamp(self.control_file_path)
|
84
85
|
|
85
86
|
def process_org_mirrors(self, summary):
|
86
87
|
"""adds new keys to the summary dict with information about mirrored
|
@@ -183,7 +184,7 @@ class QuayMirrorOrg:
|
|
183
184
|
upstream = image_mirror[tag]
|
184
185
|
downstream = image[tag]
|
185
186
|
|
186
|
-
if not
|
187
|
+
if not sync_tag(
|
187
188
|
tags=tags, tags_exclude=tags_exclude, candidate=tag
|
188
189
|
):
|
189
190
|
_LOG.debug(
|
reconcile/slack_base.py
CHANGED
@@ -14,12 +14,12 @@ from reconcile.utils.slack_api import (
|
|
14
14
|
|
15
15
|
|
16
16
|
def slackapi_from_queries(
|
17
|
-
integration_name: str, init_usergroups: bool = True
|
17
|
+
integration_name: str, init_usergroups: bool = True, channel: str | None = None
|
18
18
|
) -> SlackApi:
|
19
19
|
secret_reader = SecretReader(queries.get_secret_reader_settings())
|
20
20
|
slack_workspace = {"workspace": queries.get_slack_workspace()}
|
21
21
|
return slackapi_from_slack_workspace(
|
22
|
-
slack_workspace, secret_reader, integration_name, init_usergroups
|
22
|
+
slack_workspace, secret_reader, integration_name, init_usergroups, channel
|
23
23
|
)
|
24
24
|
|
25
25
|
|
@@ -0,0 +1,12 @@
|
|
1
|
+
from reconcile.gql_definitions.aws_cloudwatch_log_retention.aws_accounts import (
|
2
|
+
AWSAccountV1,
|
3
|
+
query,
|
4
|
+
)
|
5
|
+
from reconcile.utils.gql import GqlApi
|
6
|
+
|
7
|
+
|
8
|
+
def get_aws_accounts(
|
9
|
+
gql_api: GqlApi,
|
10
|
+
) -> list[AWSAccountV1]:
|
11
|
+
data = query(query_func=gql_api.query)
|
12
|
+
return data.accounts or []
|
@@ -1,8 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from collections.abc import Iterable
|
4
|
-
from datetime import datetime
|
5
|
-
from unittest.mock import patch
|
6
4
|
|
7
5
|
from dynatrace import Dynatrace
|
8
6
|
from dynatrace.environment_v2.tokens_api import ApiTokenUpdate
|
@@ -35,32 +33,11 @@ class DynatraceAPIToken(BaseModel):
|
|
35
33
|
scopes: list[str]
|
36
34
|
|
37
35
|
|
38
|
-
# TODO: Remove once APPSRE-11428 is resolved #######
|
39
|
-
ISO_8601 = "%Y-%m-%dT%H:%M:%S.%fZ"
|
40
|
-
FIXED_ISO_8601 = "%Y-%m-%dT%H:%M:%SZ"
|
41
|
-
|
42
|
-
|
43
|
-
def custom_iso8601_to_datetime(timestamp: str | None) -> datetime | None:
|
44
|
-
if isinstance(timestamp, str):
|
45
|
-
try:
|
46
|
-
return datetime.strptime(timestamp, ISO_8601)
|
47
|
-
except ValueError:
|
48
|
-
return datetime.strptime(timestamp, FIXED_ISO_8601)
|
49
|
-
return timestamp
|
50
|
-
|
51
|
-
|
52
|
-
################################################
|
53
|
-
|
54
|
-
|
55
36
|
class DynatraceClient:
|
56
37
|
def __init__(self, environment_url: str, api: Dynatrace) -> None:
|
57
38
|
self._environment_url = environment_url
|
58
39
|
self._api = api
|
59
40
|
|
60
|
-
@patch(
|
61
|
-
"dynatrace.environment_v2.tokens_api.iso8601_to_datetime",
|
62
|
-
custom_iso8601_to_datetime,
|
63
|
-
)
|
64
41
|
def create_api_token(
|
65
42
|
self, name: str, scopes: Iterable[str]
|
66
43
|
) -> DynatraceAPITokenCreated:
|
@@ -72,10 +49,6 @@ class DynatraceClient:
|
|
72
49
|
) from e
|
73
50
|
return DynatraceAPITokenCreated(token=token.token, id=token.id)
|
74
51
|
|
75
|
-
@patch(
|
76
|
-
"dynatrace.environment_v2.tokens_api.iso8601_to_datetime",
|
77
|
-
custom_iso8601_to_datetime,
|
78
|
-
)
|
79
52
|
def get_token_ids_map_for_name_prefix(self, prefix: str) -> dict[str, str]:
|
80
53
|
try:
|
81
54
|
dt_tokens = self._api.tokens.list()
|
@@ -87,10 +60,6 @@ class DynatraceClient:
|
|
87
60
|
token.id: token.name for token in dt_tokens if token.name.startswith(prefix)
|
88
61
|
}
|
89
62
|
|
90
|
-
@patch(
|
91
|
-
"dynatrace.environment_v2.tokens_api.iso8601_to_datetime",
|
92
|
-
custom_iso8601_to_datetime,
|
93
|
-
)
|
94
63
|
def get_token_by_id(self, token_id: str) -> DynatraceAPIToken:
|
95
64
|
try:
|
96
65
|
token = self._api.tokens.get(token_id=token_id)
|
@@ -0,0 +1,42 @@
|
|
1
|
+
import time
|
2
|
+
from collections.abc import Iterable
|
3
|
+
|
4
|
+
from reconcile.utils.helpers import match_patterns
|
5
|
+
|
6
|
+
|
7
|
+
def record_timestamp(path: str) -> None:
|
8
|
+
with open(path, "w", encoding="locale") as file_object:
|
9
|
+
file_object.write(str(time.time()))
|
10
|
+
|
11
|
+
|
12
|
+
def sync_tag(
|
13
|
+
tags: Iterable[str] | None,
|
14
|
+
tags_exclude: Iterable[str] | None,
|
15
|
+
candidate: str,
|
16
|
+
) -> bool:
|
17
|
+
"""
|
18
|
+
Determine if the candidate tag should sync, tags_exclude check take precedence.
|
19
|
+
:param tags: regex patterns to filter, match means to sync, None means no filter
|
20
|
+
:param tags_exclude: regex patterns to filter, match means not to sync, None means no filter
|
21
|
+
:param candidate: tag to check
|
22
|
+
:return: bool, True means to sync, False means not to sync
|
23
|
+
"""
|
24
|
+
if tags:
|
25
|
+
if tags_exclude:
|
26
|
+
# both tags and tags_exclude provided
|
27
|
+
return not match_patterns(
|
28
|
+
tags_exclude,
|
29
|
+
candidate,
|
30
|
+
) and match_patterns(
|
31
|
+
tags,
|
32
|
+
candidate,
|
33
|
+
)
|
34
|
+
else:
|
35
|
+
# only tags provided
|
36
|
+
return match_patterns(tags, candidate)
|
37
|
+
elif tags_exclude:
|
38
|
+
# only tags_exclude provided
|
39
|
+
return not match_patterns(tags_exclude, candidate)
|
40
|
+
else:
|
41
|
+
# neither tags nor tags_exclude provided
|
42
|
+
return True
|
@@ -1784,6 +1784,11 @@ class SaasHerder: # pylint: disable=too-many-public-methods
|
|
1784
1784
|
desired_target_config["saas_file_managed_resource_types"] = (
|
1785
1785
|
saas_file.managed_resource_types
|
1786
1786
|
)
|
1787
|
+
if saas_file.managed_resource_names:
|
1788
|
+
desired_target_config["saas_file_managed_resource_names"] = [
|
1789
|
+
m.dict() for m in saas_file.managed_resource_names
|
1790
|
+
]
|
1791
|
+
|
1787
1792
|
desired_target_config["url"] = rt.url
|
1788
1793
|
desired_target_config["path"] = rt.path
|
1789
1794
|
# before the GQL classes are introduced, the parameters attribute
|
reconcile/utils/slack_api.py
CHANGED
@@ -491,6 +491,8 @@ class SlackApi:
|
|
491
491
|
from_timestamp to to_timestamp ignoring threads"""
|
492
492
|
if not self.channel:
|
493
493
|
raise ValueError("Expecting self.channel to be set")
|
494
|
+
channels_found = self.get_channels_by_names(self.channel)
|
495
|
+
[channel_id] = [k for k in channels_found if channels_found[k] == self.channel]
|
494
496
|
|
495
497
|
cursor = ""
|
496
498
|
responses = []
|
@@ -499,7 +501,7 @@ class SlackApi:
|
|
499
501
|
slack_request.labels("conversations.history", "GET").inc()
|
500
502
|
|
501
503
|
response = self._sc.conversations_history(
|
502
|
-
cursor=cursor, channel=
|
504
|
+
cursor=cursor, channel=channel_id, **self.chat_kwargs
|
503
505
|
)
|
504
506
|
|
505
507
|
for r in response["messages"]:
|
@@ -0,0 +1,278 @@
|
|
1
|
+
import itertools
|
2
|
+
import logging
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from math import isnan
|
5
|
+
from typing import Any, Self
|
6
|
+
|
7
|
+
import jinja2
|
8
|
+
import requests
|
9
|
+
from sretoolbox.utils import threaded
|
10
|
+
|
11
|
+
from reconcile.gql_definitions.fragments.saas_slo_document import (
|
12
|
+
SLODocument,
|
13
|
+
SLODocumentSLOV1,
|
14
|
+
SLOExternalPrometheusAccessV1,
|
15
|
+
SLONamespacesV1,
|
16
|
+
)
|
17
|
+
from reconcile.utils.rest_api_base import ApiBase, BearerTokenAuth
|
18
|
+
from reconcile.utils.secret_reader import SecretReaderBase
|
19
|
+
|
20
|
+
PROM_QUERY_URL = "api/v1/query"
|
21
|
+
|
22
|
+
DEFAULT_READ_TIMEOUT = 30
|
23
|
+
DEFAULT_RETRIES = 3
|
24
|
+
DEFAULT_THREAD_POOL_SIZE = 10
|
25
|
+
|
26
|
+
|
27
|
+
class EmptySLOResult(Exception):
|
28
|
+
pass
|
29
|
+
|
30
|
+
|
31
|
+
class EmptySLOValue(Exception):
|
32
|
+
pass
|
33
|
+
|
34
|
+
|
35
|
+
class InvalidSLOValue(Exception):
|
36
|
+
pass
|
37
|
+
|
38
|
+
|
39
|
+
@dataclass
|
40
|
+
class SLODetails:
|
41
|
+
namespace_name: str
|
42
|
+
slo_document_name: str
|
43
|
+
cluster_name: str
|
44
|
+
slo: SLODocumentSLOV1
|
45
|
+
service_name: str
|
46
|
+
current_slo_value: float
|
47
|
+
|
48
|
+
|
49
|
+
@dataclass
|
50
|
+
class NamespaceSLODocument:
|
51
|
+
name: str
|
52
|
+
namespace: SLONamespacesV1
|
53
|
+
slos: list[SLODocumentSLOV1] | None
|
54
|
+
|
55
|
+
def get_host_url(self) -> str:
|
56
|
+
return (
|
57
|
+
self.namespace.prometheus_access.url
|
58
|
+
if self.namespace.prometheus_access
|
59
|
+
else self.namespace.namespace.cluster.prometheus_url
|
60
|
+
)
|
61
|
+
|
62
|
+
|
63
|
+
class PrometheusClient(ApiBase):
|
64
|
+
def get_current_slo_value(
|
65
|
+
self,
|
66
|
+
slo: SLODocumentSLOV1,
|
67
|
+
slo_document_name: str,
|
68
|
+
namespace_name: str,
|
69
|
+
service_name: str,
|
70
|
+
cluster_name: str,
|
71
|
+
) -> SLODetails | None:
|
72
|
+
"""
|
73
|
+
Retrieve the current SLO value from Prometheus for provided SLO configuration.
|
74
|
+
Returns an SLODetails instance if successful, or None on error.
|
75
|
+
"""
|
76
|
+
template = jinja2.Template(slo.expr)
|
77
|
+
prom_query = template.render({"window": slo.slo_parameters.window})
|
78
|
+
try:
|
79
|
+
current_slo_response = self._get(
|
80
|
+
url=PROM_QUERY_URL, params={"query": (prom_query)}
|
81
|
+
)
|
82
|
+
current_slo_value = self._extract_current_slo_value(
|
83
|
+
data=current_slo_response
|
84
|
+
)
|
85
|
+
return SLODetails(
|
86
|
+
namespace_name=namespace_name,
|
87
|
+
slo=slo,
|
88
|
+
slo_document_name=slo_document_name,
|
89
|
+
current_slo_value=current_slo_value,
|
90
|
+
cluster_name=cluster_name,
|
91
|
+
service_name=service_name,
|
92
|
+
)
|
93
|
+
except requests.exceptions.ConnectionError:
|
94
|
+
logging.error(
|
95
|
+
f"Connection error getting current value for SLO: {slo.name} of document: {slo_document_name} for namespace: {namespace_name}"
|
96
|
+
)
|
97
|
+
raise
|
98
|
+
except Exception as e:
|
99
|
+
logging.error(
|
100
|
+
f"Unexpected error getting current value for SLO: {slo.name} of document: {slo_document_name} for namespace: {namespace_name} details: {e}"
|
101
|
+
)
|
102
|
+
return None
|
103
|
+
|
104
|
+
def _extract_current_slo_value(self, data: dict[str, Any]) -> float:
|
105
|
+
result = data["data"]["result"]
|
106
|
+
if not result:
|
107
|
+
raise EmptySLOResult("prometheus returned empty result")
|
108
|
+
slo_value = result[0]["value"]
|
109
|
+
if not slo_value:
|
110
|
+
raise EmptySLOValue("prometheus returned empty SLO value")
|
111
|
+
slo_value = float(slo_value[1])
|
112
|
+
if isnan(slo_value):
|
113
|
+
raise InvalidSLOValue("slo value should be a number")
|
114
|
+
return slo_value
|
115
|
+
|
116
|
+
|
117
|
+
class PrometheusClientMap:
|
118
|
+
"""
|
119
|
+
A mapping from Prometheus URLs to PrometheusClient instances.
|
120
|
+
"""
|
121
|
+
|
122
|
+
def __init__(
|
123
|
+
self,
|
124
|
+
secret_reader: SecretReaderBase,
|
125
|
+
namespace_slo_documents: list[NamespaceSLODocument],
|
126
|
+
read_timeout: int = DEFAULT_READ_TIMEOUT,
|
127
|
+
max_retries: int = DEFAULT_RETRIES,
|
128
|
+
):
|
129
|
+
self.secret_reader = secret_reader
|
130
|
+
self.read_timeout = read_timeout
|
131
|
+
self.max_retries = max_retries
|
132
|
+
self.pc_map: dict[str, PrometheusClient] = self._build_pc_map(
|
133
|
+
namespace_slo_documents
|
134
|
+
)
|
135
|
+
|
136
|
+
def __enter__(self) -> Self:
|
137
|
+
return self
|
138
|
+
|
139
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
140
|
+
self.cleanup()
|
141
|
+
|
142
|
+
def get_prometheus_client(self, prom_url: str) -> PrometheusClient:
|
143
|
+
return self.pc_map[prom_url]
|
144
|
+
|
145
|
+
def _build_pc_map(
|
146
|
+
self, namespace_slo_documents: list[NamespaceSLODocument]
|
147
|
+
) -> dict[str, PrometheusClient]:
|
148
|
+
pc_map: dict[str, PrometheusClient] = {}
|
149
|
+
for doc in namespace_slo_documents:
|
150
|
+
key = doc.get_host_url()
|
151
|
+
if key not in pc_map:
|
152
|
+
prom_client = self.build_prom_client_from_namespace(doc.namespace)
|
153
|
+
pc_map[key] = prom_client
|
154
|
+
return pc_map
|
155
|
+
|
156
|
+
def cleanup(self) -> None:
|
157
|
+
for prom_client in self.pc_map.values():
|
158
|
+
prom_client.cleanup()
|
159
|
+
|
160
|
+
def build_auth_for_prometheus_access(
|
161
|
+
self, prometheus_access: SLOExternalPrometheusAccessV1
|
162
|
+
) -> requests.auth.HTTPBasicAuth | None:
|
163
|
+
"""
|
164
|
+
Build authentication for Prometheus endpoint referred in prometheusAccess section.
|
165
|
+
"""
|
166
|
+
if prometheus_access.username and prometheus_access.password:
|
167
|
+
username = self.secret_reader.read_secret(prometheus_access.username)
|
168
|
+
password = self.secret_reader.read_secret(prometheus_access.password)
|
169
|
+
return requests.auth.HTTPBasicAuth(username, password)
|
170
|
+
return None
|
171
|
+
|
172
|
+
def build_prom_client_from_namespace(
|
173
|
+
self, namespace: SLONamespacesV1
|
174
|
+
) -> PrometheusClient:
|
175
|
+
auth: requests.auth.HTTPBasicAuth | BearerTokenAuth | None
|
176
|
+
if namespace.prometheus_access:
|
177
|
+
prom_url = namespace.prometheus_access.url
|
178
|
+
auth = self.build_auth_for_prometheus_access(namespace.prometheus_access)
|
179
|
+
return PrometheusClient(
|
180
|
+
host=prom_url,
|
181
|
+
read_timeout=self.read_timeout,
|
182
|
+
max_retries=self.max_retries,
|
183
|
+
auth=auth,
|
184
|
+
)
|
185
|
+
if not namespace.namespace.cluster.automation_token:
|
186
|
+
raise Exception(
|
187
|
+
f"cluster {namespace.namespace.cluster.name} does not have automation token set"
|
188
|
+
)
|
189
|
+
auth = BearerTokenAuth(
|
190
|
+
self.secret_reader.read_secret(namespace.namespace.cluster.automation_token)
|
191
|
+
)
|
192
|
+
return PrometheusClient(
|
193
|
+
host=namespace.namespace.cluster.prometheus_url,
|
194
|
+
read_timeout=self.read_timeout,
|
195
|
+
max_retries=self.max_retries,
|
196
|
+
auth=auth,
|
197
|
+
)
|
198
|
+
|
199
|
+
|
200
|
+
class SLODocumentManager:
|
201
|
+
"""
|
202
|
+
Manages SLO document including authentication, querying, and SLO value extraction.
|
203
|
+
"""
|
204
|
+
|
205
|
+
def __init__(
|
206
|
+
self,
|
207
|
+
slo_documents: list[SLODocument],
|
208
|
+
secret_reader: SecretReaderBase,
|
209
|
+
thread_pool_size: int = DEFAULT_THREAD_POOL_SIZE,
|
210
|
+
read_timeout: int = DEFAULT_READ_TIMEOUT,
|
211
|
+
max_retries: int = DEFAULT_RETRIES,
|
212
|
+
):
|
213
|
+
self.namespace_slo_documents = self._build_namespace_slo_documents(
|
214
|
+
slo_documents
|
215
|
+
)
|
216
|
+
self.thread_pool_size = thread_pool_size
|
217
|
+
self.secret_reader = secret_reader
|
218
|
+
self.max_retries = max_retries
|
219
|
+
self.read_timeout = read_timeout
|
220
|
+
|
221
|
+
@staticmethod
|
222
|
+
def _build_namespace_slo_documents(
|
223
|
+
slo_documents: list[SLODocument],
|
224
|
+
) -> list[NamespaceSLODocument]:
|
225
|
+
return [
|
226
|
+
NamespaceSLODocument(
|
227
|
+
name=slo_document.name,
|
228
|
+
namespace=namespace,
|
229
|
+
slos=slo_document.slos,
|
230
|
+
)
|
231
|
+
for slo_document in slo_documents
|
232
|
+
for namespace in slo_document.namespaces
|
233
|
+
]
|
234
|
+
|
235
|
+
def get_current_slo_list(self) -> list[SLODetails | None]:
|
236
|
+
with PrometheusClientMap(
|
237
|
+
secret_reader=self.secret_reader,
|
238
|
+
namespace_slo_documents=self.namespace_slo_documents,
|
239
|
+
read_timeout=self.read_timeout,
|
240
|
+
max_retries=self.max_retries,
|
241
|
+
) as pc_map:
|
242
|
+
current_slo_list_iterable = threaded.run(
|
243
|
+
func=self._get_current_slo_details_list,
|
244
|
+
pc_map=pc_map,
|
245
|
+
iterable=self.namespace_slo_documents,
|
246
|
+
thread_pool_size=self.thread_pool_size,
|
247
|
+
)
|
248
|
+
return list(itertools.chain.from_iterable(current_slo_list_iterable))
|
249
|
+
|
250
|
+
def get_breached_slos(self) -> list[SLODetails]:
|
251
|
+
current_slo_details_list = self.get_current_slo_list()
|
252
|
+
missing_slos = [slo for slo in current_slo_details_list if not slo]
|
253
|
+
if missing_slos:
|
254
|
+
raise RuntimeError("slo validation failed due to retrival errors")
|
255
|
+
return [
|
256
|
+
slo
|
257
|
+
for slo in current_slo_details_list
|
258
|
+
if slo and slo.current_slo_value < slo.slo.slo_target
|
259
|
+
]
|
260
|
+
|
261
|
+
@staticmethod
|
262
|
+
def _get_current_slo_details_list(
|
263
|
+
slo_document: NamespaceSLODocument,
|
264
|
+
pc_map: PrometheusClientMap,
|
265
|
+
) -> list[SLODetails | None]:
|
266
|
+
key = slo_document.get_host_url()
|
267
|
+
prom_client = pc_map.get_prometheus_client(key)
|
268
|
+
slo_details_list: list[SLODetails | None] = [
|
269
|
+
prom_client.get_current_slo_value(
|
270
|
+
slo=slo,
|
271
|
+
slo_document_name=slo_document.name,
|
272
|
+
namespace_name=slo_document.namespace.namespace.name,
|
273
|
+
service_name=slo_document.namespace.namespace.app.name,
|
274
|
+
cluster_name=slo_document.namespace.namespace.cluster.name,
|
275
|
+
)
|
276
|
+
for slo in slo_document.slos or []
|
277
|
+
]
|
278
|
+
return slo_details_list
|
@@ -6138,6 +6138,11 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
|
|
6138
6138
|
callback_urls=[f"{bucket_url}/token.html"],
|
6139
6139
|
depends_on=["aws_cognito_resource_server.userpool_gateway_resource_server"],
|
6140
6140
|
**pool_client_args,
|
6141
|
+
token_validity_units={
|
6142
|
+
"access_token": "minutes",
|
6143
|
+
"id_token": "minutes",
|
6144
|
+
"refresh_token": "days",
|
6145
|
+
},
|
6141
6146
|
)
|
6142
6147
|
tf_resources.append(cognito_user_pool_client)
|
6143
6148
|
|
@@ -6149,9 +6154,15 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
|
|
6149
6154
|
user_pool_id=f"${{{cognito_user_pool_resource.id}}}",
|
6150
6155
|
callback_urls=insights_callback_urls,
|
6151
6156
|
**pool_client_args,
|
6157
|
+
token_validity_units={
|
6158
|
+
"access_token": "minutes",
|
6159
|
+
"id_token": "minutes",
|
6160
|
+
"refresh_token": "days",
|
6161
|
+
},
|
6152
6162
|
)
|
6153
6163
|
tf_resources.append(insights_cognito_user_pool_client)
|
6154
6164
|
|
6165
|
+
# todo: these scopes should be defined in an external resource file
|
6155
6166
|
# POOL RESOURCE SERVER
|
6156
6167
|
cognito_resource_server_resource = aws_cognito_resource_server(
|
6157
6168
|
"userpool_service_resource_server",
|
@@ -6215,6 +6226,11 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
|
|
6215
6226
|
allowed_oauth_scopes=["ocm/AccountManagement"],
|
6216
6227
|
depends_on=["aws_cognito_resource_server.userpool_service_resource_server"],
|
6217
6228
|
**pool_client_service_account_common_args,
|
6229
|
+
token_validity_units={
|
6230
|
+
"access_token": "minutes",
|
6231
|
+
"id_token": "minutes",
|
6232
|
+
"refresh_token": "days",
|
6233
|
+
},
|
6218
6234
|
)
|
6219
6235
|
tf_resources.append(ams_service_account_pool_client_resource)
|
6220
6236
|
|
@@ -6226,6 +6242,11 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
|
|
6226
6242
|
allowed_oauth_scopes=["ocm/ClusterService"],
|
6227
6243
|
depends_on=["aws_cognito_resource_server.userpool_service_resource_server"],
|
6228
6244
|
**pool_client_service_account_common_args,
|
6245
|
+
token_validity_units={
|
6246
|
+
"access_token": "minutes",
|
6247
|
+
"id_token": "minutes",
|
6248
|
+
"refresh_token": "days",
|
6249
|
+
},
|
6229
6250
|
)
|
6230
6251
|
tf_resources.append(cs_service_account_pool_client_resource)
|
6231
6252
|
|
@@ -6237,6 +6258,11 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
|
|
6237
6258
|
allowed_oauth_scopes=["ocm/ServiceLogService"],
|
6238
6259
|
depends_on=["aws_cognito_resource_server.userpool_service_resource_server"],
|
6239
6260
|
**pool_client_service_account_common_args,
|
6261
|
+
token_validity_units={
|
6262
|
+
"access_token": "minutes",
|
6263
|
+
"id_token": "minutes",
|
6264
|
+
"refresh_token": "days",
|
6265
|
+
},
|
6240
6266
|
)
|
6241
6267
|
tf_resources.append(osl_service_account_pool_client_resource)
|
6242
6268
|
|
@@ -6251,6 +6277,11 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
|
|
6251
6277
|
"aws_cognito_resource_server.userpool_service_resource_server"
|
6252
6278
|
],
|
6253
6279
|
**pool_client_service_account_common_args,
|
6280
|
+
token_validity_units={
|
6281
|
+
"access_token": "minutes",
|
6282
|
+
"id_token": "minutes",
|
6283
|
+
"refresh_token": "days",
|
6284
|
+
},
|
6254
6285
|
)
|
6255
6286
|
)
|
6256
6287
|
tf_resources.append(backplane_cli_service_account_pool_client_resource)
|
@@ -6266,6 +6297,11 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
|
|
6266
6297
|
"aws_cognito_resource_server.userpool_service_resource_server"
|
6267
6298
|
],
|
6268
6299
|
**pool_client_service_account_common_args,
|
6300
|
+
token_validity_units={
|
6301
|
+
"access_token": "minutes",
|
6302
|
+
"id_token": "minutes",
|
6303
|
+
"refresh_token": "days",
|
6304
|
+
},
|
6269
6305
|
)
|
6270
6306
|
)
|
6271
6307
|
tf_resources.append(backplane_api_service_account_pool_client_resource)
|
@@ -6278,9 +6314,30 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
|
|
6278
6314
|
allowed_oauth_scopes=["ocm/InsightsServiceAccount"],
|
6279
6315
|
depends_on=["aws_cognito_resource_server.userpool_service_resource_server"],
|
6280
6316
|
**pool_client_service_account_common_args,
|
6317
|
+
token_validity_units={
|
6318
|
+
"access_token": "minutes",
|
6319
|
+
"id_token": "minutes",
|
6320
|
+
"refresh_token": "days",
|
6321
|
+
},
|
6281
6322
|
)
|
6282
6323
|
tf_resources.append(insights_service_account_pool_client_resource)
|
6283
6324
|
|
6325
|
+
# OSD FLEET MANAGER
|
6326
|
+
osdfm_service_account_pool_client_resource = aws_cognito_user_pool_client(
|
6327
|
+
"osdfm_service_account",
|
6328
|
+
name=f"ocm-{identifier}-osdfm-service-account",
|
6329
|
+
user_pool_id=f"${{{cognito_user_pool_resource.id}}}",
|
6330
|
+
allowed_oauth_scopes=["ocm/OSDFleetManagerService"],
|
6331
|
+
depends_on=["aws_cognito_resource_server.userpool_service_resource_server"],
|
6332
|
+
**pool_client_service_account_common_args,
|
6333
|
+
token_validity_units={
|
6334
|
+
"access_token": "minutes",
|
6335
|
+
"id_token": "minutes",
|
6336
|
+
"refresh_token": "days",
|
6337
|
+
},
|
6338
|
+
)
|
6339
|
+
tf_resources.append(osdfm_service_account_pool_client_resource)
|
6340
|
+
|
6284
6341
|
# USER POOL COMPLETE
|
6285
6342
|
|
6286
6343
|
# rosa-authenticator-vpce provider OR pre-created vpce resources are required for this
|