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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qontract-reconcile
3
- Version: 0.10.1rc805
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
@@ -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.1rc805.dist-info/METADATA,sha256=hQqEKCmIWG-75Lp4rO5VuV1Utx4ciyWOVz3hcotymnM,2314
801
- qontract_reconcile-0.10.1rc805.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
802
- qontract_reconcile-0.10.1rc805.dist-info/entry_points.txt,sha256=rIxI5zWtHNlfpDeq1a7pZXAPoqf7HG32KMTN3MeWK_8,429
803
- qontract_reconcile-0.10.1rc805.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
804
- qontract_reconcile-0.10.1rc805.dist-info/RECORD,,
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
@@ -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