qontract-reconcile 0.10.1rc805__py3-none-any.whl → 0.10.1rc806__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.1rc805.dist-info → qontract_reconcile-0.10.1rc806.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc805.dist-info → qontract_reconcile-0.10.1rc806.dist-info}/RECORD +12 -5
- {qontract_reconcile-0.10.1rc805.dist-info → qontract_reconcile-0.10.1rc806.dist-info}/entry_points.txt +1 -0
- tools/saas_metrics_exporter/__init__.py +0 -0
- tools/saas_metrics_exporter/commit_distance/__init__.py +0 -0
- tools/saas_metrics_exporter/commit_distance/channel.py +63 -0
- tools/saas_metrics_exporter/commit_distance/commit_distance.py +130 -0
- tools/saas_metrics_exporter/commit_distance/metrics.py +26 -0
- tools/saas_metrics_exporter/main.py +99 -0
- tools/test/conftest.py +40 -0
- {qontract_reconcile-0.10.1rc805.dist-info → qontract_reconcile-0.10.1rc806.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc805.dist-info → qontract_reconcile-0.10.1rc806.dist-info}/top_level.txt +0 -0
{qontract_reconcile-0.10.1rc805.dist-info → qontract_reconcile-0.10.1rc806.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: qontract-reconcile
|
3
|
-
Version: 0.10.
|
3
|
+
Version: 0.10.1rc806
|
4
4
|
Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
|
5
5
|
Home-page: https://github.com/app-sre/qontract-reconcile
|
6
6
|
Author: Red Hat App-SRE Team
|
{qontract_reconcile-0.10.1rc805.dist-info → qontract_reconcile-0.10.1rc806.dist-info}/RECORD
RENAMED
@@ -790,15 +790,22 @@ tools/cli_commands/cost_report/openshift.py,sha256=XNEJpgtIQAi3Eoej1Btnl74IWg3g-
|
|
790
790
|
tools/cli_commands/cost_report/response.py,sha256=_kbpBSjMjbPXGkjDgidTOLvFpVqfBo3VMkSOheUdmMA,1308
|
791
791
|
tools/cli_commands/cost_report/util.py,sha256=r4K8nC1S0YZNSUNro141cYG1nuG8HwkkYEqWV9GCvu8,1861
|
792
792
|
tools/cli_commands/cost_report/view.py,sha256=pKM6LDeWZJcTw2a7sWBwKSuR9p3SAk3lEb37uwMhlLw,10183
|
793
|
+
tools/saas_metrics_exporter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
794
|
+
tools/saas_metrics_exporter/main.py,sha256=piocx6meMdJxoxeNz52gQGUjt5n7Fma4kgqYamszPrM,3180
|
795
|
+
tools/saas_metrics_exporter/commit_distance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
796
|
+
tools/saas_metrics_exporter/commit_distance/channel.py,sha256=XEAh3eL8TmgMe7V2BsyxuXYWgvBBVdSJETd6Ec7cI04,2171
|
797
|
+
tools/saas_metrics_exporter/commit_distance/commit_distance.py,sha256=snkcHKS7zxSIomS7psEQ13efN-j9MxKZHe0nLw55dAk,4042
|
798
|
+
tools/saas_metrics_exporter/commit_distance/metrics.py,sha256=rQTcinrv3uGLnHFumS37NN3QyVv1z6HGqy8MtfOwcxM,544
|
793
799
|
tools/sre_checkpoints/__init__.py,sha256=CDaDaywJnmRCLyl_NCcvxi-Zc0hTi_3OdwKiFOyS39I,145
|
794
800
|
tools/sre_checkpoints/util.py,sha256=zEDbGr18ZeHNQwW8pUsr2JRjuXIPz--WAGJxZo9sv_Y,894
|
795
801
|
tools/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
802
|
+
tools/test/conftest.py,sha256=BYS85m3oTUxf7gxku14oH0ctlPMS1x1nk69kEJ6G5cc,1022
|
796
803
|
tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvftCWEEf-g1mfXOtgCog-g,1271
|
797
804
|
tools/test/test_qontract_cli.py,sha256=_D61RFGAN5x44CY1tYbouhlGXXABwYfxKSWSQx3Jrss,4941
|
798
805
|
tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
|
799
806
|
tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
|
800
|
-
qontract_reconcile-0.10.
|
801
|
-
qontract_reconcile-0.10.
|
802
|
-
qontract_reconcile-0.10.
|
803
|
-
qontract_reconcile-0.10.
|
804
|
-
qontract_reconcile-0.10.
|
807
|
+
qontract_reconcile-0.10.1rc806.dist-info/METADATA,sha256=n5MHe0IaW3GE0yf5Yy3vSdT-jrs4U3al9qbv4LUPWQ8,2314
|
808
|
+
qontract_reconcile-0.10.1rc806.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
809
|
+
qontract_reconcile-0.10.1rc806.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
|
810
|
+
qontract_reconcile-0.10.1rc806.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
|
811
|
+
qontract_reconcile-0.10.1rc806.dist-info/RECORD,,
|
@@ -5,4 +5,5 @@ glitchtip-access-reporter = tools.glitchtip_access_reporter:main
|
|
5
5
|
glitchtip-access-revalidation = tools.glitchtip_access_revalidation:main
|
6
6
|
qontract-cli = tools.qontract_cli:root
|
7
7
|
qontract-reconcile = reconcile.cli:integration
|
8
|
+
saas-metrics-exporter = tools.saas_metrics_exporter.main:main
|
8
9
|
template-validation = tools.template_validation:main
|
File without changes
|
File without changes
|
@@ -0,0 +1,63 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from collections.abc import Iterable
|
4
|
+
from dataclasses import dataclass
|
5
|
+
|
6
|
+
from reconcile.typed_queries.saas_files import SaasFile
|
7
|
+
from reconcile.utils.secret_reader import HasSecret
|
8
|
+
|
9
|
+
|
10
|
+
@dataclass
|
11
|
+
class SaasTarget:
|
12
|
+
app_name: str
|
13
|
+
repo_url: str
|
14
|
+
namespace_name: str
|
15
|
+
target_name: str
|
16
|
+
ref: str
|
17
|
+
auth_code: HasSecret | None
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class Channel:
|
22
|
+
name: str
|
23
|
+
subscribers: list[SaasTarget]
|
24
|
+
publishers: list[SaasTarget]
|
25
|
+
|
26
|
+
|
27
|
+
def build_channels(saas_files: Iterable[SaasFile]) -> list[Channel]:
|
28
|
+
channels: dict[str, Channel] = {}
|
29
|
+
for saas_file in saas_files:
|
30
|
+
for resource_template in saas_file.resource_templates:
|
31
|
+
for target in resource_template.targets:
|
32
|
+
if not target.promotion:
|
33
|
+
continue
|
34
|
+
if not (target.promotion.publish or target.promotion.subscribe):
|
35
|
+
continue
|
36
|
+
auth_code = (
|
37
|
+
saas_file.authentication.code if saas_file.authentication else None
|
38
|
+
)
|
39
|
+
target_name = target.name if target.name else "NoName"
|
40
|
+
saas_target = SaasTarget(
|
41
|
+
app_name=saas_file.app.name,
|
42
|
+
repo_url=resource_template.url,
|
43
|
+
ref=target.ref,
|
44
|
+
auth_code=auth_code,
|
45
|
+
namespace_name=target.namespace.name,
|
46
|
+
target_name=target_name,
|
47
|
+
)
|
48
|
+
|
49
|
+
for channel in target.promotion.publish or []:
|
50
|
+
if channel not in channels:
|
51
|
+
channels[channel] = Channel(
|
52
|
+
name=channel, subscribers=[], publishers=[]
|
53
|
+
)
|
54
|
+
channels[channel].publishers.append(saas_target)
|
55
|
+
|
56
|
+
for channel in target.promotion.subscribe or []:
|
57
|
+
if channel not in channels:
|
58
|
+
channels[channel] = Channel(
|
59
|
+
name=channel, subscribers=[], publishers=[]
|
60
|
+
)
|
61
|
+
channels[channel].subscribers.append(saas_target)
|
62
|
+
|
63
|
+
return list(channels.values())
|
@@ -0,0 +1,130 @@
|
|
1
|
+
from collections.abc import Iterable
|
2
|
+
from dataclasses import dataclass
|
3
|
+
|
4
|
+
from sretoolbox.utils import threaded
|
5
|
+
|
6
|
+
from reconcile.typed_queries.saas_files import SaasFile
|
7
|
+
from reconcile.utils.secret_reader import HasSecret
|
8
|
+
from reconcile.utils.vcs import VCS
|
9
|
+
from tools.saas_metrics_exporter.commit_distance.channel import (
|
10
|
+
Channel,
|
11
|
+
SaasTarget,
|
12
|
+
build_channels,
|
13
|
+
)
|
14
|
+
from tools.saas_metrics_exporter.commit_distance.metrics import SaasCommitDistanceGauge
|
15
|
+
|
16
|
+
|
17
|
+
@dataclass
|
18
|
+
class Distance:
|
19
|
+
publisher: SaasTarget
|
20
|
+
subscriber: SaasTarget
|
21
|
+
channel: Channel
|
22
|
+
distance: int = 0
|
23
|
+
|
24
|
+
|
25
|
+
@dataclass
|
26
|
+
class CommitDistanceMetric:
|
27
|
+
value: float
|
28
|
+
metric: SaasCommitDistanceGauge
|
29
|
+
|
30
|
+
|
31
|
+
@dataclass
|
32
|
+
class ThreadData:
|
33
|
+
repo_url: str
|
34
|
+
auth_code: HasSecret | None
|
35
|
+
ref_from: str
|
36
|
+
ref_to: str
|
37
|
+
distance: int = 0
|
38
|
+
|
39
|
+
def __hash__(self) -> int:
|
40
|
+
return hash((self.repo_url, self.ref_from, self.ref_to))
|
41
|
+
|
42
|
+
|
43
|
+
class CommitDistanceFetcher:
|
44
|
+
def __init__(self, vcs: VCS):
|
45
|
+
self._vcs = vcs
|
46
|
+
|
47
|
+
def _data_key(self, repo_url: str, ref_from: str, ref_to: str) -> str:
|
48
|
+
return f"{repo_url}/{ref_from}/{ref_to}"
|
49
|
+
|
50
|
+
def _calculate_commit_distance(self, data: ThreadData) -> None:
|
51
|
+
if data.ref_from == data.ref_to:
|
52
|
+
data.distance = 0
|
53
|
+
return
|
54
|
+
|
55
|
+
commits = self._vcs.get_commits_between(
|
56
|
+
repo_url=data.repo_url,
|
57
|
+
auth_code=data.auth_code,
|
58
|
+
commit_from=data.ref_from,
|
59
|
+
commit_to=data.ref_to,
|
60
|
+
)
|
61
|
+
data.distance = len(commits)
|
62
|
+
|
63
|
+
def _populate_distances(
|
64
|
+
self, distances: Iterable[Distance], thread_data: Iterable[ThreadData]
|
65
|
+
) -> None:
|
66
|
+
m = {
|
67
|
+
self._data_key(
|
68
|
+
repo_url=d.repo_url, ref_from=d.ref_from, ref_to=d.ref_to
|
69
|
+
): d.distance
|
70
|
+
for d in thread_data
|
71
|
+
}
|
72
|
+
for distance in distances:
|
73
|
+
distance.distance = m[
|
74
|
+
self._data_key(
|
75
|
+
repo_url=distance.publisher.repo_url,
|
76
|
+
ref_from=distance.subscriber.ref,
|
77
|
+
ref_to=distance.publisher.ref,
|
78
|
+
)
|
79
|
+
]
|
80
|
+
|
81
|
+
def fetch(
|
82
|
+
self, saas_files: Iterable[SaasFile], thread_pool_size: int
|
83
|
+
) -> list[CommitDistanceMetric]:
|
84
|
+
channels = build_channels(saas_files=saas_files)
|
85
|
+
distances: list[Distance] = []
|
86
|
+
thread_data: set[ThreadData] = set()
|
87
|
+
|
88
|
+
for channel in channels:
|
89
|
+
for subscriber in channel.subscribers:
|
90
|
+
for publisher in channel.publishers:
|
91
|
+
thread_data.add(
|
92
|
+
ThreadData(
|
93
|
+
repo_url=publisher.repo_url,
|
94
|
+
auth_code=publisher.auth_code,
|
95
|
+
ref_from=subscriber.ref,
|
96
|
+
ref_to=publisher.ref,
|
97
|
+
)
|
98
|
+
)
|
99
|
+
distances.append(
|
100
|
+
Distance(
|
101
|
+
publisher=publisher,
|
102
|
+
subscriber=subscriber,
|
103
|
+
channel=channel,
|
104
|
+
)
|
105
|
+
)
|
106
|
+
|
107
|
+
threaded.run(
|
108
|
+
self._calculate_commit_distance,
|
109
|
+
thread_data,
|
110
|
+
thread_pool_size=thread_pool_size,
|
111
|
+
)
|
112
|
+
|
113
|
+
self._populate_distances(distances=distances, thread_data=thread_data)
|
114
|
+
|
115
|
+
commit_distance_metrics = [
|
116
|
+
CommitDistanceMetric(
|
117
|
+
value=float(distance.distance),
|
118
|
+
metric=SaasCommitDistanceGauge(
|
119
|
+
channel=distance.channel.name,
|
120
|
+
app=distance.publisher.app_name,
|
121
|
+
publisher=distance.publisher.target_name,
|
122
|
+
publisher_namespace=distance.publisher.namespace_name,
|
123
|
+
subscriber=distance.subscriber.target_name,
|
124
|
+
subscriber_namespace=distance.subscriber.namespace_name,
|
125
|
+
),
|
126
|
+
)
|
127
|
+
for distance in distances
|
128
|
+
]
|
129
|
+
|
130
|
+
return commit_distance_metrics
|
@@ -0,0 +1,26 @@
|
|
1
|
+
from pydantic import BaseModel
|
2
|
+
|
3
|
+
from reconcile.utils.metrics import (
|
4
|
+
GaugeMetric,
|
5
|
+
)
|
6
|
+
|
7
|
+
|
8
|
+
class SaasBaseMetric(BaseModel):
|
9
|
+
"Base class for Saas metrics"
|
10
|
+
|
11
|
+
integration: str = "saas_metrics_exporter"
|
12
|
+
|
13
|
+
|
14
|
+
class SaasCommitDistanceGauge(SaasBaseMetric, GaugeMetric):
|
15
|
+
"Gauge for the commit distance between saas targets in a channel"
|
16
|
+
|
17
|
+
channel: str
|
18
|
+
publisher: str
|
19
|
+
publisher_namespace: str
|
20
|
+
subscriber: str
|
21
|
+
subscriber_namespace: str
|
22
|
+
app: str
|
23
|
+
|
24
|
+
@classmethod
|
25
|
+
def name(cls) -> str:
|
26
|
+
return "commit_distance"
|
@@ -0,0 +1,99 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from collections.abc import Callable
|
4
|
+
|
5
|
+
import click
|
6
|
+
|
7
|
+
from reconcile.cli import (
|
8
|
+
config_file,
|
9
|
+
dry_run,
|
10
|
+
log_level,
|
11
|
+
)
|
12
|
+
from reconcile.typed_queries.app_interface_repo_url import get_app_interface_repo_url
|
13
|
+
from reconcile.typed_queries.app_interface_vault_settings import (
|
14
|
+
get_app_interface_vault_settings,
|
15
|
+
)
|
16
|
+
from reconcile.typed_queries.github_orgs import get_github_orgs
|
17
|
+
from reconcile.typed_queries.gitlab_instances import get_gitlab_instances
|
18
|
+
from reconcile.typed_queries.saas_files import get_saas_files
|
19
|
+
from reconcile.utils import metrics
|
20
|
+
from reconcile.utils.defer import defer
|
21
|
+
from reconcile.utils.runtime.environment import init_env
|
22
|
+
from reconcile.utils.secret_reader import create_secret_reader
|
23
|
+
from reconcile.utils.vcs import VCS
|
24
|
+
from tools.saas_metrics_exporter.commit_distance.commit_distance import (
|
25
|
+
CommitDistanceFetcher,
|
26
|
+
)
|
27
|
+
|
28
|
+
|
29
|
+
class SaasMetricsExporter:
|
30
|
+
"""
|
31
|
+
This tool is responsible for exposing/exporting saas metrics and data.
|
32
|
+
|
33
|
+
Note, that by design we store metrics exporters as a tool in the tools directory.
|
34
|
+
"""
|
35
|
+
|
36
|
+
def __init__(self, vcs: VCS, dry_run: bool) -> None:
|
37
|
+
self._vcs = vcs
|
38
|
+
self._dry_run = dry_run
|
39
|
+
|
40
|
+
@staticmethod
|
41
|
+
def create(dry_run: bool) -> SaasMetricsExporter:
|
42
|
+
vault_settings = get_app_interface_vault_settings()
|
43
|
+
secret_reader = create_secret_reader(use_vault=vault_settings.vault)
|
44
|
+
vcs = VCS(
|
45
|
+
secret_reader=secret_reader,
|
46
|
+
github_orgs=get_github_orgs(),
|
47
|
+
gitlab_instances=get_gitlab_instances(),
|
48
|
+
app_interface_repo_url=get_app_interface_repo_url(),
|
49
|
+
dry_run=dry_run,
|
50
|
+
)
|
51
|
+
return SaasMetricsExporter(vcs=vcs, dry_run=dry_run)
|
52
|
+
|
53
|
+
@defer
|
54
|
+
def run(
|
55
|
+
self,
|
56
|
+
env_name: str | None,
|
57
|
+
app_name: str | None,
|
58
|
+
thread_pool_size: int,
|
59
|
+
defer: Callable | None = None,
|
60
|
+
) -> None:
|
61
|
+
saas_files = get_saas_files(env_name=env_name, app_name=app_name)
|
62
|
+
if defer:
|
63
|
+
defer(self._vcs.cleanup)
|
64
|
+
|
65
|
+
commit_distance_fetcher = CommitDistanceFetcher(vcs=self._vcs)
|
66
|
+
commit_distance_metrics = commit_distance_fetcher.fetch(
|
67
|
+
saas_files=saas_files, thread_pool_size=thread_pool_size
|
68
|
+
)
|
69
|
+
for m in commit_distance_metrics:
|
70
|
+
metrics.set_gauge(
|
71
|
+
metric=m.metric,
|
72
|
+
value=m.value,
|
73
|
+
)
|
74
|
+
|
75
|
+
|
76
|
+
@click.command()
|
77
|
+
@click.option("--env-name", default=None, help="environment to filter saas files by")
|
78
|
+
@click.option("--app-name", default=None, help="app to filter saas files by")
|
79
|
+
@click.option("--thread-pool-size", default=1, help="threadpool size")
|
80
|
+
@dry_run
|
81
|
+
@config_file
|
82
|
+
@log_level
|
83
|
+
def main(
|
84
|
+
env_name: str | None,
|
85
|
+
app_name: str | None,
|
86
|
+
dry_run: bool,
|
87
|
+
thread_pool_size: int,
|
88
|
+
configfile: str,
|
89
|
+
log_level: str | None,
|
90
|
+
) -> None:
|
91
|
+
init_env(log_level=log_level, config_file=configfile)
|
92
|
+
exporter = SaasMetricsExporter.create(dry_run=dry_run)
|
93
|
+
exporter.run(
|
94
|
+
env_name=env_name, app_name=app_name, thread_pool_size=thread_pool_size
|
95
|
+
)
|
96
|
+
|
97
|
+
|
98
|
+
if __name__ == "__main__":
|
99
|
+
main() # pylint: disable=no-value-for-parameter
|
tools/test/conftest.py
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
from collections.abc import (
|
2
|
+
Callable,
|
3
|
+
MutableMapping,
|
4
|
+
)
|
5
|
+
from typing import (
|
6
|
+
Any,
|
7
|
+
Optional,
|
8
|
+
)
|
9
|
+
|
10
|
+
import pytest
|
11
|
+
from pydantic import BaseModel
|
12
|
+
from pydantic.error_wrappers import ValidationError
|
13
|
+
|
14
|
+
from reconcile.utils.models import data_default_none
|
15
|
+
|
16
|
+
|
17
|
+
class GQLClassFactoryError(Exception):
|
18
|
+
pass
|
19
|
+
|
20
|
+
|
21
|
+
@pytest.fixture
|
22
|
+
def gql_class_factory() -> (
|
23
|
+
Callable[
|
24
|
+
[type[BaseModel], Optional[MutableMapping[str, Any]]],
|
25
|
+
BaseModel,
|
26
|
+
]
|
27
|
+
):
|
28
|
+
"""Create a GQL class from a fixture and set default values to None."""
|
29
|
+
|
30
|
+
def _gql_class_factory(
|
31
|
+
klass: type[BaseModel], data: Optional[MutableMapping[str, Any]] = None
|
32
|
+
) -> BaseModel:
|
33
|
+
try:
|
34
|
+
return klass(**data_default_none(klass, data or {}))
|
35
|
+
except ValidationError as e:
|
36
|
+
msg = "[gql_class_factory] Your given data does not match the class ...\n"
|
37
|
+
msg += "\n".join([str(m) for m in list(e.raw_errors)])
|
38
|
+
raise GQLClassFactoryError(msg) from e
|
39
|
+
|
40
|
+
return _gql_class_factory
|
File without changes
|
{qontract_reconcile-0.10.1rc805.dist-info → qontract_reconcile-0.10.1rc806.dist-info}/top_level.txt
RENAMED
File without changes
|