qontract-reconcile 0.10.1rc763__py3-none-any.whl → 0.10.1rc765__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.1rc763.dist-info → qontract_reconcile-0.10.1rc765.dist-info}/METADATA +1 -1
  2. {qontract_reconcile-0.10.1rc763.dist-info → qontract_reconcile-0.10.1rc765.dist-info}/RECORD +27 -31
  3. reconcile/external_resources/aws.py +85 -0
  4. reconcile/external_resources/factories.py +133 -0
  5. reconcile/external_resources/integration.py +95 -0
  6. reconcile/external_resources/manager.py +350 -0
  7. reconcile/external_resources/meta.py +4 -0
  8. reconcile/external_resources/metrics.py +20 -0
  9. reconcile/external_resources/model.py +244 -0
  10. reconcile/external_resources/reconciler.py +249 -0
  11. reconcile/external_resources/secrets_sync.py +229 -0
  12. reconcile/external_resources/state.py +246 -0
  13. reconcile/saas_auto_promotions_manager/meta.py +1 -1
  14. reconcile/saas_auto_promotions_manager/subscriber.py +52 -2
  15. reconcile/saas_auto_promotions_manager/utils/saas_files_inventory.py +4 -0
  16. reconcile/test/saas_auto_promotions_manager/conftest.py +63 -0
  17. reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/conftest.py +0 -37
  18. reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/test_desired_state.py +20 -14
  19. reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/conftest.py +0 -43
  20. reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/test_content_multiple_namespaces.py +4 -11
  21. reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/test_content_single_namespace.py +12 -19
  22. reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/test_content_single_target.py +6 -12
  23. reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/test_json_path_selector.py +8 -15
  24. reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/data_keys.py +0 -4
  25. reconcile/test/saas_auto_promotions_manager/subscriber/conftest.py +0 -89
  26. reconcile/test/saas_auto_promotions_manager/subscriber/data_keys.py +0 -11
  27. reconcile/test/saas_auto_promotions_manager/subscriber/test_content_hash.py +0 -130
  28. reconcile/test/saas_auto_promotions_manager/subscriber/test_diff.py +0 -161
  29. reconcile/test/saas_auto_promotions_manager/subscriber/test_multiple_channels_config_hash.py +0 -218
  30. reconcile/test/saas_auto_promotions_manager/subscriber/test_multiple_channels_moving_ref.py +0 -216
  31. reconcile/test/saas_auto_promotions_manager/subscriber/test_multiple_publishers_moving_ref.py +0 -129
  32. reconcile/test/saas_auto_promotions_manager/subscriber/test_single_channel_with_single_publisher.py +0 -330
  33. reconcile/test/saas_auto_promotions_manager/utils/saas_files_inventory/__init__.py +0 -0
  34. reconcile/test/saas_auto_promotions_manager/utils/saas_files_inventory/test_multiple_publishers_for_single_channel.py +0 -68
  35. reconcile/test/saas_auto_promotions_manager/utils/saas_files_inventory/test_saas_files_use_target_config_hash.py +0 -62
  36. reconcile/test/saas_auto_promotions_manager/utils/saas_files_inventory/test_saas_files_with_auto_promote.py +0 -73
  37. reconcile/test/saas_auto_promotions_manager/utils/saas_files_inventory/test_saas_files_without_auto_promote.py +0 -64
  38. {qontract_reconcile-0.10.1rc763.dist-info → qontract_reconcile-0.10.1rc765.dist-info}/WHEEL +0 -0
  39. {qontract_reconcile-0.10.1rc763.dist-info → qontract_reconcile-0.10.1rc765.dist-info}/entry_points.txt +0 -0
  40. {qontract_reconcile-0.10.1rc763.dist-info → qontract_reconcile-0.10.1rc765.dist-info}/top_level.txt +0 -0
  41. /reconcile/{test/saas_auto_promotions_manager/subscriber → external_resources}/__init__.py +0 -0
@@ -0,0 +1,246 @@
1
+ import logging
2
+ from collections.abc import Mapping
3
+ from datetime import datetime, timezone
4
+ from enum import Enum
5
+ from typing import Any
6
+
7
+ import boto3
8
+ from pydantic import BaseModel
9
+
10
+ from reconcile.external_resources.model import (
11
+ ExternalResourceKey,
12
+ ExternalResourceModuleConfiguration,
13
+ Reconciliation,
14
+ )
15
+
16
+ DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
17
+
18
+
19
+ class StateNotFoundError(Exception):
20
+ pass
21
+
22
+
23
+ class ReconcileStatus(str, Enum):
24
+ SUCCESS: str = "SUCCESS"
25
+ ERROR: str = "ERROR"
26
+ IN_PROGRESS: str = "IN_PROGRESS"
27
+ NOT_EXISTS: str = "NOT_EXISTS"
28
+
29
+
30
+ class ResourceStatus(str, Enum):
31
+ CREATED: str = "CREATED"
32
+ DELETED: str = "DELETED"
33
+ ABANDONED: str = "ABANDONED"
34
+ NOT_EXISTS: str = "NOT_EXISTS"
35
+ IN_PROGRESS: str = "IN_PROGRESS"
36
+ DELETE_IN_PROGRESS: str = "DELETE_IN_PROGRESS"
37
+ ERROR: str = "ERROR"
38
+
39
+
40
+ class ExternalResourceState(BaseModel):
41
+ key: ExternalResourceKey
42
+ ts: datetime
43
+ resource_status: ResourceStatus
44
+ reconciliation: Reconciliation
45
+ reconciliation_errors: int = 0
46
+
47
+
48
+ class DynamoDBStateAdapter:
49
+ # Table PK
50
+ ER_KEY_HASH = "external_resource_key_hash"
51
+
52
+ RESOURCE_STATUS = "resource_status"
53
+ TIMESTAMP = "time_stamp"
54
+ RECONCILIATION_ERRORS = "reconciliation_errors"
55
+
56
+ ER_KEY = "external_resource_key"
57
+ ER_KEY_PROVISION_PROVIDER = "provision_provider"
58
+ ER_KEY_PROVISIONER_NAME = "provisioner_name"
59
+ ER_KEY_PROVIDER = "provider"
60
+ ER_KEY_IDENTIFIER = "identifier"
61
+
62
+ RECONC = "reconciliation"
63
+ RECONC_RESOURCE_HASH = "resource_hash"
64
+ RECONC_INPUT = "input"
65
+ RECONC_ACTION = "action"
66
+
67
+ MODCONF = "module_configuration"
68
+ MODCONF_IMAGE = "image"
69
+ MODCONF_VERSION = "version"
70
+ MODCONF_DRIFT_MINS = "drift_detection_minutes"
71
+ MODCONF_TIMEOUT_MINS = "timeout_minutes"
72
+
73
+ def _get_value(self, item: Mapping[str, Any], key: str, _type: str = "S") -> Any:
74
+ return item[key][_type]
75
+
76
+ def deserialize(
77
+ self, item: Mapping[str, Any], partial_data: bool = False
78
+ ) -> ExternalResourceState:
79
+ _key = self._get_value(item, self.ER_KEY, _type="M")
80
+ key = ExternalResourceKey(
81
+ provision_provider=self._get_value(_key, self.ER_KEY_PROVISION_PROVIDER),
82
+ provisioner_name=self._get_value(_key, self.ER_KEY_PROVISIONER_NAME),
83
+ provider=self._get_value(_key, self.ER_KEY_PROVIDER),
84
+ identifier=self._get_value(_key, self.ER_KEY_IDENTIFIER),
85
+ )
86
+ _reconciliation = self._get_value(item, self.RECONC, _type="M")
87
+
88
+ if partial_data:
89
+ r = Reconciliation(
90
+ key=key,
91
+ resource_hash=self._get_value(
92
+ _reconciliation, self.RECONC_RESOURCE_HASH
93
+ ),
94
+ )
95
+ else:
96
+ _modconf = self._get_value(_reconciliation, self.MODCONF, _type="M")
97
+ r = Reconciliation(
98
+ key=key,
99
+ resource_hash=self._get_value(
100
+ _reconciliation, self.RECONC_RESOURCE_HASH
101
+ ),
102
+ input=self._get_value(_reconciliation, self.RECONC_INPUT),
103
+ action=self._get_value(_reconciliation, self.RECONC_ACTION),
104
+ module_configuration=ExternalResourceModuleConfiguration(
105
+ image=self._get_value(_modconf, self.MODCONF_IMAGE),
106
+ version=self._get_value(_modconf, self.MODCONF_VERSION),
107
+ reconcile_drift_interval_minutes=self._get_value(
108
+ _modconf, self.MODCONF_DRIFT_MINS, _type="N"
109
+ ),
110
+ reconcile_timeout_minutes=self._get_value(
111
+ _modconf, self.MODCONF_TIMEOUT_MINS, _type="N"
112
+ ),
113
+ ),
114
+ )
115
+
116
+ return ExternalResourceState(
117
+ key=key,
118
+ ts=self._get_value(item, self.TIMESTAMP),
119
+ resource_status=self._get_value(item, self.RESOURCE_STATUS),
120
+ reconciliation=r,
121
+ reconciliation_errors=int(
122
+ self._get_value(item, self.RECONCILIATION_ERRORS, _type="N")
123
+ ),
124
+ )
125
+
126
+ def serialize(self, state: ExternalResourceState) -> dict[str, Any]:
127
+ return {
128
+ self.ER_KEY_HASH: {"S": state.key.hash()},
129
+ self.TIMESTAMP: {"S": state.ts.isoformat()},
130
+ self.RESOURCE_STATUS: {"S": state.resource_status.value},
131
+ self.RECONCILIATION_ERRORS: {"N": str(state.reconciliation_errors)},
132
+ self.ER_KEY: {
133
+ "M": {
134
+ self.ER_KEY_PROVISION_PROVIDER: {"S": state.key.provision_provider},
135
+ self.ER_KEY_PROVISIONER_NAME: {"S": state.key.provisioner_name},
136
+ self.ER_KEY_PROVIDER: {"S": state.key.provider},
137
+ self.ER_KEY_IDENTIFIER: {"S": state.key.identifier},
138
+ }
139
+ },
140
+ self.RECONC: {
141
+ "M": {
142
+ self.RECONC_RESOURCE_HASH: {
143
+ "S": state.reconciliation.resource_hash
144
+ },
145
+ self.RECONC_ACTION: {"S": state.reconciliation.action.value},
146
+ self.RECONC_INPUT: {"S": state.reconciliation.input},
147
+ self.MODCONF: {
148
+ "M": {
149
+ self.MODCONF_IMAGE: {
150
+ "S": state.reconciliation.module_configuration.image
151
+ },
152
+ self.MODCONF_VERSION: {
153
+ "S": state.reconciliation.module_configuration.version
154
+ },
155
+ self.MODCONF_DRIFT_MINS: {
156
+ "N": str(
157
+ state.reconciliation.module_configuration.reconcile_drift_interval_minutes
158
+ )
159
+ },
160
+ self.MODCONF_TIMEOUT_MINS: {
161
+ "N": str(
162
+ state.reconciliation.module_configuration.reconcile_timeout_minutes
163
+ )
164
+ },
165
+ }
166
+ },
167
+ }
168
+ },
169
+ }
170
+
171
+
172
+ class ExternalResourcesStateDynamoDB:
173
+ PARTIALS_PROJECTED_VALUES = ",".join([
174
+ DynamoDBStateAdapter.ER_KEY,
175
+ DynamoDBStateAdapter.TIMESTAMP,
176
+ DynamoDBStateAdapter.RESOURCE_STATUS,
177
+ DynamoDBStateAdapter.RECONCILIATION_ERRORS,
178
+ f"{DynamoDBStateAdapter.RECONC}.{DynamoDBStateAdapter.RECONC_RESOURCE_HASH}",
179
+ ])
180
+
181
+ def __init__(self, table_name: str, region_name: str) -> None:
182
+ self.adapter = DynamoDBStateAdapter()
183
+ self.client = boto3.client("dynamodb", region_name=region_name)
184
+ self._table = table_name
185
+ self.partial_resources = self._get_partial_resources()
186
+
187
+ def get_external_resource_state(
188
+ self, key: ExternalResourceKey
189
+ ) -> ExternalResourceState:
190
+ data = self.client.get_item(
191
+ TableName=self._table,
192
+ ConsistentRead=True,
193
+ Key={self.adapter.ER_KEY_HASH: {"S": key.hash()}},
194
+ )
195
+ if "Item" in data:
196
+ return self.adapter.deserialize(data["Item"])
197
+ else:
198
+ return ExternalResourceState(
199
+ key=key,
200
+ ts=datetime.now(timezone.utc),
201
+ resource_status=ResourceStatus.NOT_EXISTS,
202
+ reconciliation=Reconciliation(key=key),
203
+ reconciliation_errors=0,
204
+ )
205
+
206
+ def set_external_resource_state(
207
+ self,
208
+ state: ExternalResourceState,
209
+ ) -> None:
210
+ self.client.put_item(TableName=self._table, Item=self.adapter.serialize(state))
211
+
212
+ def del_external_resource_state(self, key: ExternalResourceKey) -> None:
213
+ self.client.delete_item(
214
+ TableName=self._table,
215
+ Key={self.adapter.ER_KEY_HASH: {"S": key.hash()}},
216
+ )
217
+
218
+ def _get_partial_resources(
219
+ self,
220
+ ) -> dict[ExternalResourceKey, ExternalResourceState]:
221
+ """A Partial Resoure is the minimum resource data reguired
222
+ to check if a resource has been removed from the configuration.
223
+ Getting less data from DynamoDb saves money and the logic does not need it.
224
+ """
225
+ logging.info("Getting Managed resources from DynamoDb")
226
+ partials = {}
227
+ for item in self.client.scan(
228
+ TableName=self._table, ProjectionExpression=self.PARTIALS_PROJECTED_VALUES
229
+ ).get("Items", []):
230
+ s = self.adapter.deserialize(item, partial_data=True)
231
+ partials[s.key] = s
232
+ return partials
233
+
234
+ def get_all_resource_keys(self) -> set[ExternalResourceKey]:
235
+ return {k for k in self.partial_resources.keys()}
236
+
237
+ def update_resource_status(
238
+ self, key: ExternalResourceKey, status: ResourceStatus
239
+ ) -> None:
240
+ self.client.update_item(
241
+ TableName=self._table,
242
+ Key={self.adapter.ER_KEY_HASH: {"S": key.hash()}},
243
+ UpdateExpression="set resource_status=:new_value",
244
+ ExpressionAttributeValues={":new_value": {"S": status.value}},
245
+ ReturnValues="UPDATED_NEW",
246
+ )
@@ -1,4 +1,4 @@
1
1
  from reconcile.utils.semver_helper import make_semver
2
2
 
3
3
  QONTRACT_INTEGRATION = "saas-auto-promotions-manager"
4
- QONTRACT_INTEGRATION_VERSION = make_semver(2, 1, 4)
4
+ QONTRACT_INTEGRATION_VERSION = make_semver(2, 2, 0)
@@ -1,8 +1,8 @@
1
1
  import hashlib
2
2
  import logging
3
- from collections.abc import Iterable
3
+ from collections.abc import Iterable, Mapping
4
4
  from dataclasses import dataclass
5
- from typing import Optional
5
+ from typing import Any, Optional
6
6
 
7
7
  from reconcile.gql_definitions.fragments.saas_target_namespace import (
8
8
  SaasTargetNamespace,
@@ -42,6 +42,7 @@ class Subscriber:
42
42
  target_file_path: str,
43
43
  target_namespace: SaasTargetNamespace,
44
44
  use_target_config_hash: bool,
45
+ uid: str,
45
46
  ):
46
47
  self.saas_name = saas_name
47
48
  self.template_name = template_name
@@ -52,6 +53,7 @@ class Subscriber:
52
53
  self.desired_ref = ""
53
54
  self.desired_hashes: list[ConfigHash] = []
54
55
  self.target_namespace = target_namespace
56
+ self.uid = uid
55
57
  self._content_hash = ""
56
58
  self._use_target_config_hash = use_target_config_hash
57
59
 
@@ -73,6 +75,54 @@ class Subscriber:
73
75
  self._compute_desired_ref()
74
76
  self._compute_desired_config_hashes()
75
77
 
78
+ @staticmethod
79
+ def from_exported_dict(data: Mapping[str, Any]) -> "Subscriber":
80
+ subscriber = Subscriber(
81
+ saas_name=data["1"],
82
+ template_name=data["2"],
83
+ ref=data["3"],
84
+ target_file_path=data["4"],
85
+ use_target_config_hash=data["5"],
86
+ target_namespace=SaasTargetNamespace(**data["6"]),
87
+ uid=data["7"],
88
+ )
89
+ subscriber.desired_hashes = data["8"]
90
+ subscriber.desired_ref = data["9"]
91
+ return subscriber
92
+
93
+ def to_exportable_dict(self) -> dict[str, Any]:
94
+ """
95
+ We will later persist subscriber data as json in MRs. We keep key size small to use less space.
96
+ Note, the data will be encoded and encrypted in another component.
97
+ """
98
+ data: dict[str, Any] = {}
99
+ data["1"] = self.saas_name
100
+ data["2"] = self.template_name
101
+ data["3"] = self.ref
102
+ data["4"] = self.target_file_path
103
+ data["5"] = self._use_target_config_hash
104
+ data["6"] = self.target_namespace.dict(by_alias=True)
105
+ data["7"] = self.uid
106
+ data["8"] = self.desired_hashes
107
+ data["9"] = self.desired_ref
108
+ return data
109
+
110
+ def __eq__(self, other: object) -> bool:
111
+ if not isinstance(other, Subscriber):
112
+ # don't attempt to compare against unrelated types
113
+ return False
114
+ return (
115
+ self.saas_name == other.saas_name
116
+ and self.template_name == other.template_name
117
+ and self.ref == other.ref
118
+ and self.target_file_path == other.target_file_path
119
+ and self._use_target_config_hash == other._use_target_config_hash
120
+ and self.desired_ref == other.desired_ref
121
+ and self.desired_hashes == other.desired_hashes
122
+ and self.target_namespace == other.target_namespace
123
+ and self.uid == other.uid
124
+ )
125
+
76
126
  def _validate_deployment(
77
127
  self, publisher: Publisher, channel: Channel
78
128
  ) -> Optional[DeploymentInfo]:
@@ -92,6 +92,10 @@ class SaasFilesInventory:
92
92
  if not target.promotion.auto:
93
93
  continue
94
94
  subscriber = Subscriber(
95
+ uid=target.uid(
96
+ parent_saas_file_name=saas_file.name,
97
+ parent_resource_template_name=resource_template.name,
98
+ ),
95
99
  saas_name=saas_file.name,
96
100
  template_name=resource_template.name,
97
101
  target_file_path=file_path,
@@ -1,9 +1,11 @@
1
+ from collections import defaultdict
1
2
  from collections.abc import (
2
3
  Callable,
3
4
  Iterable,
4
5
  Mapping,
5
6
  MutableMapping,
6
7
  )
8
+ from typing import Any
7
9
  from unittest.mock import (
8
10
  MagicMock,
9
11
  create_autospec,
@@ -14,6 +16,12 @@ import pytest
14
16
  from reconcile.gql_definitions.fragments.saas_target_namespace import (
15
17
  SaasTargetNamespace,
16
18
  )
19
+ from reconcile.saas_auto_promotions_manager.publisher import DeploymentInfo, Publisher
20
+ from reconcile.saas_auto_promotions_manager.subscriber import (
21
+ Channel,
22
+ ConfigHash,
23
+ Subscriber,
24
+ )
17
25
  from reconcile.typed_queries.saas_files import SaasFile
18
26
  from reconcile.utils.gitlab_api import GitLabApi
19
27
  from reconcile.utils.promotion_state import (
@@ -100,3 +108,58 @@ def promotion_state_builder() -> Callable[..., PromotionState]:
100
108
  return promotion_state
101
109
 
102
110
  return builder
111
+
112
+
113
+ @pytest.fixture
114
+ def subscriber_builder(
115
+ saas_target_namespace_builder: Callable[..., SaasTargetNamespace],
116
+ ) -> Callable[[Mapping[str, Any]], Subscriber]:
117
+ def builder(data: Mapping[str, Any]) -> Subscriber:
118
+ channels: list[Channel] = []
119
+ for channel_name, channel_data in data.get("CHANNELS", {}).items():
120
+ channel = Channel(name=channel_name, publishers=[])
121
+ for publisher_name, publisher_data in channel_data.items():
122
+ publisher = Publisher(
123
+ ref="",
124
+ uid="",
125
+ repo_url="",
126
+ cluster_name="",
127
+ namespace_name="",
128
+ saas_name="",
129
+ saas_file_path="",
130
+ app_name="",
131
+ resource_template_name="",
132
+ target_name=None,
133
+ publish_job_logs=True,
134
+ has_subscriber=True,
135
+ auth_code=None,
136
+ )
137
+ publisher.commit_sha = publisher_data["REAL_WORLD_SHA"]
138
+ publisher.deployment_info_by_channel[channel_name] = DeploymentInfo(
139
+ success=publisher_data.get("SUCCESSFUL_DEPLOYMENT", True),
140
+ target_config_hash=publisher_data.get("CONFIG_HASH", ""),
141
+ saas_file=publisher_name,
142
+ )
143
+ channel.publishers.append(publisher)
144
+ channels.append(channel)
145
+ cur_config_hashes_by_channel: dict[str, list[ConfigHash]] = defaultdict(list)
146
+ for cur_config_hash in data.get("CUR_CONFIG_HASHES", []):
147
+ cur_config_hashes_by_channel[cur_config_hash.channel].append(
148
+ cur_config_hash
149
+ )
150
+ subscriber = Subscriber(
151
+ uid=data.get("SUB_UID", "default"),
152
+ target_namespace=saas_target_namespace_builder(data.get("NAMESPACE", {})),
153
+ ref=data.get("CUR_SUBSCRIBER_REF", ""),
154
+ saas_name="",
155
+ target_file_path=data.get("TARGET_FILE_PATH", ""),
156
+ template_name="",
157
+ use_target_config_hash=data.get("USE_TARGET_CONFIG_HASH", True),
158
+ )
159
+ subscriber.channels = channels
160
+ subscriber.config_hashes_by_channel_name = cur_config_hashes_by_channel
161
+ subscriber.desired_ref = data.get("DESIRED_REF", "")
162
+ subscriber.desired_hashes = data.get("DESIRED_TARGET_HASHES", [])
163
+ return subscriber
164
+
165
+ return builder
@@ -8,9 +8,6 @@ from unittest.mock import create_autospec
8
8
  import pytest
9
9
  from gitlab.v4.objects import ProjectMergeRequest
10
10
 
11
- from reconcile.gql_definitions.fragments.saas_target_namespace import (
12
- SaasTargetNamespace,
13
- )
14
11
  from reconcile.saas_auto_promotions_manager.merge_request_manager.merge_request_manager_v2 import (
15
12
  SAPM_LABEL,
16
13
  )
@@ -31,24 +28,17 @@ from reconcile.saas_auto_promotions_manager.merge_request_manager.renderer impor
31
28
  VERSION_REF,
32
29
  Renderer,
33
30
  )
34
- from reconcile.saas_auto_promotions_manager.subscriber import (
35
- Channel,
36
- Subscriber,
37
- )
38
31
  from reconcile.utils.vcs import VCS, MRCheckStatus
39
32
 
40
33
  from .data_keys import (
41
- CHANNEL,
42
34
  DESCRIPTION,
43
35
  HAS_CONFLICTS,
44
36
  LABELS,
45
37
  OPEN_MERGE_REQUESTS,
46
38
  PIPELINE_RESULTS,
47
- REF,
48
39
  SUBSCRIBER_BATCHABLE,
49
40
  SUBSCRIBER_CHANNELS,
50
41
  SUBSCRIBER_CONTENT_HASH,
51
- SUBSCRIBER_TARGET_PATH,
52
42
  )
53
43
 
54
44
 
@@ -120,33 +110,6 @@ def reconciler_builder() -> Callable[[Diff], Reconciler]:
120
110
  return builder
121
111
 
122
112
 
123
- @pytest.fixture
124
- def subscriber_builder(
125
- saas_target_namespace_builder: Callable[..., SaasTargetNamespace],
126
- ) -> Callable[..., Subscriber]:
127
- def builder(data: Mapping) -> Subscriber:
128
- subscriber = Subscriber(
129
- saas_name="",
130
- template_name="",
131
- target_namespace=saas_target_namespace_builder({}),
132
- ref="",
133
- target_file_path=data.get(SUBSCRIBER_TARGET_PATH, ""),
134
- use_target_config_hash=True,
135
- )
136
- subscriber.desired_hashes = []
137
- subscriber.desired_ref = data.get(REF, "")
138
- for channel in data.get(CHANNEL, []):
139
- subscriber.channels.append(
140
- Channel(
141
- name=channel,
142
- publishers=[],
143
- )
144
- )
145
- return subscriber
146
-
147
- return builder
148
-
149
-
150
113
  @pytest.fixture
151
114
  def renderer() -> Renderer:
152
115
  return create_autospec(spec=Renderer)
@@ -5,10 +5,6 @@ from reconcile.saas_auto_promotions_manager.merge_request_manager.desired_state
5
5
  )
6
6
  from reconcile.saas_auto_promotions_manager.subscriber import Subscriber
7
7
 
8
- from .data_keys import (
9
- CHANNEL,
10
- )
11
-
12
8
 
13
9
  def test_desired_state_empty() -> None:
14
10
  desired_state = DesiredState(subscribers=[])
@@ -29,10 +25,14 @@ def test_desired_state_single_subscriber(
29
25
  def test_desired_state_multiple_subscribers_same_channel_combo(
30
26
  subscriber_builder: Callable[..., Subscriber],
31
27
  ) -> None:
32
- subscriber_a = subscriber_builder({CHANNEL: ["channel-a", "channel-b"]})
33
- subscriber_a.desired_ref = "ref-a"
34
- subscriber_b = subscriber_builder({CHANNEL: ["channel-a", "channel-b"]})
35
- subscriber_b.desired_ref = "ref-b"
28
+ subscriber_a = subscriber_builder({
29
+ "CHANNELS": {"channel-a": {}, "channel-b": {}},
30
+ "DESIRED_REF": "ref-a",
31
+ })
32
+ subscriber_b = subscriber_builder({
33
+ "CHANNELS": {"channel-a": {}, "channel-b": {}},
34
+ "DESIRED_REF": "ref-b",
35
+ })
36
36
  desired_state = DesiredState(subscribers=[subscriber_a, subscriber_b])
37
37
  assert len(desired_state.promotions) == 1
38
38
  assert desired_state.promotions[0].content_hashes == {
@@ -43,12 +43,18 @@ def test_desired_state_multiple_subscribers_same_channel_combo(
43
43
  def test_desired_state_multiple_subscribers_different_channel_combo(
44
44
  subscriber_builder: Callable[..., Subscriber],
45
45
  ) -> None:
46
- subscriber_a = subscriber_builder({CHANNEL: ["channel-a", "channel-b"]})
47
- subscriber_a.desired_ref = "ref-a"
48
- subscriber_b = subscriber_builder({CHANNEL: ["channel-a", "channel-b"]})
49
- subscriber_b.desired_ref = "ref-b"
50
- subscriber_c = subscriber_builder({CHANNEL: ["channel-b", "channel-c"]})
51
- subscriber_c.desired_ref = "ref-c"
46
+ subscriber_a = subscriber_builder({
47
+ "CHANNELS": {"channel-a": {}, "channel-b": {}},
48
+ "DESIRED_REF": "ref-a",
49
+ })
50
+ subscriber_b = subscriber_builder({
51
+ "CHANNELS": {"channel-a": {}, "channel-b": {}},
52
+ "DESIRED_REF": "ref-b",
53
+ })
54
+ subscriber_c = subscriber_builder({
55
+ "CHANNELS": {"channel-b": {}, "channel-c": {}},
56
+ "DESIRED_REF": "ref-c",
57
+ })
52
58
  desired_state = DesiredState(subscribers=[subscriber_a, subscriber_b, subscriber_c])
53
59
  sorted_promotions = sorted(desired_state.promotions)
54
60
  assert len(desired_state.promotions) == 2
@@ -1,26 +1,10 @@
1
1
  import os
2
2
  from collections.abc import (
3
3
  Callable,
4
- Mapping,
5
4
  )
6
5
 
7
6
  import pytest
8
7
 
9
- from reconcile.gql_definitions.fragments.saas_target_namespace import (
10
- SaasTargetNamespace,
11
- )
12
- from reconcile.saas_auto_promotions_manager.subscriber import (
13
- Channel,
14
- Subscriber,
15
- )
16
-
17
- from .data_keys import (
18
- CHANNELS,
19
- CONFIG_HASHES,
20
- NAMESPACE,
21
- REF,
22
- )
23
-
24
8
 
25
9
  @pytest.fixture
26
10
  def file_contents() -> Callable[[str], tuple[str, str]]:
@@ -39,30 +23,3 @@ def file_contents() -> Callable[[str], tuple[str, str]]:
39
23
  return (a, b)
40
24
 
41
25
  return contents
42
-
43
-
44
- @pytest.fixture
45
- def subscriber_builder(
46
- saas_target_namespace_builder: Callable[..., SaasTargetNamespace],
47
- ) -> Callable[[Mapping], Subscriber]:
48
- def builder(data: Mapping) -> Subscriber:
49
- subscriber = Subscriber(
50
- target_namespace=saas_target_namespace_builder(data.get(NAMESPACE, {})),
51
- ref="",
52
- saas_name="",
53
- target_file_path="",
54
- template_name="",
55
- use_target_config_hash=True,
56
- )
57
- subscriber.desired_ref = data[REF]
58
- subscriber.desired_hashes = data[CONFIG_HASHES]
59
- for channel in data.get(CHANNELS, []):
60
- subscriber.channels.append(
61
- Channel(
62
- name=channel,
63
- publishers=[],
64
- )
65
- )
66
- return subscriber
67
-
68
- return builder
@@ -11,29 +11,22 @@ from reconcile.saas_auto_promotions_manager.subscriber import (
11
11
  Subscriber,
12
12
  )
13
13
 
14
- from .data_keys import (
15
- CHANNELS,
16
- CONFIG_HASHES,
17
- NAMESPACE,
18
- REF,
19
- )
20
-
21
14
 
22
15
  def test_content_multiple_namespaces(
23
16
  file_contents: Callable[[str], tuple[str, str]],
24
17
  subscriber_builder: Callable[[Mapping], Subscriber],
25
18
  ):
26
19
  subscriber = subscriber_builder({
27
- NAMESPACE: {"path": "/some/namespace.yml"},
28
- REF: "new_sha",
29
- CONFIG_HASHES: [
20
+ "NAMESPACE": {"path": "/some/namespace.yml"},
21
+ "DESIRED_REF": "new_sha",
22
+ "DESIRED_TARGET_HASHES": [
30
23
  ConfigHash(
31
24
  channel="channel-a",
32
25
  target_config_hash="new_hash",
33
26
  parent_saas="parent_saas",
34
27
  )
35
28
  ],
36
- CHANNELS: ["channel-a"],
29
+ "CHANNELS": {"channel-a": {}},
37
30
  })
38
31
  saas_content, expected = file_contents("multiple_namespaces")
39
32
  renderer = Renderer()