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.
- {qontract_reconcile-0.10.1rc763.dist-info → qontract_reconcile-0.10.1rc765.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc763.dist-info → qontract_reconcile-0.10.1rc765.dist-info}/RECORD +27 -31
- reconcile/external_resources/aws.py +85 -0
- reconcile/external_resources/factories.py +133 -0
- reconcile/external_resources/integration.py +95 -0
- reconcile/external_resources/manager.py +350 -0
- reconcile/external_resources/meta.py +4 -0
- reconcile/external_resources/metrics.py +20 -0
- reconcile/external_resources/model.py +244 -0
- reconcile/external_resources/reconciler.py +249 -0
- reconcile/external_resources/secrets_sync.py +229 -0
- reconcile/external_resources/state.py +246 -0
- reconcile/saas_auto_promotions_manager/meta.py +1 -1
- reconcile/saas_auto_promotions_manager/subscriber.py +52 -2
- reconcile/saas_auto_promotions_manager/utils/saas_files_inventory.py +4 -0
- reconcile/test/saas_auto_promotions_manager/conftest.py +63 -0
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/conftest.py +0 -37
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/test_desired_state.py +20 -14
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/conftest.py +0 -43
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/test_content_multiple_namespaces.py +4 -11
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/test_content_single_namespace.py +12 -19
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/test_content_single_target.py +6 -12
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/test_json_path_selector.py +8 -15
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/data_keys.py +0 -4
- reconcile/test/saas_auto_promotions_manager/subscriber/conftest.py +0 -89
- reconcile/test/saas_auto_promotions_manager/subscriber/data_keys.py +0 -11
- reconcile/test/saas_auto_promotions_manager/subscriber/test_content_hash.py +0 -130
- reconcile/test/saas_auto_promotions_manager/subscriber/test_diff.py +0 -161
- reconcile/test/saas_auto_promotions_manager/subscriber/test_multiple_channels_config_hash.py +0 -218
- reconcile/test/saas_auto_promotions_manager/subscriber/test_multiple_channels_moving_ref.py +0 -216
- reconcile/test/saas_auto_promotions_manager/subscriber/test_multiple_publishers_moving_ref.py +0 -129
- reconcile/test/saas_auto_promotions_manager/subscriber/test_single_channel_with_single_publisher.py +0 -330
- reconcile/test/saas_auto_promotions_manager/utils/saas_files_inventory/__init__.py +0 -0
- reconcile/test/saas_auto_promotions_manager/utils/saas_files_inventory/test_multiple_publishers_for_single_channel.py +0 -68
- reconcile/test/saas_auto_promotions_manager/utils/saas_files_inventory/test_saas_files_use_target_config_hash.py +0 -62
- reconcile/test/saas_auto_promotions_manager/utils/saas_files_inventory/test_saas_files_with_auto_promote.py +0 -73
- reconcile/test/saas_auto_promotions_manager/utils/saas_files_inventory/test_saas_files_without_auto_promote.py +0 -64
- {qontract_reconcile-0.10.1rc763.dist-info → qontract_reconcile-0.10.1rc765.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc763.dist-info → qontract_reconcile-0.10.1rc765.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc763.dist-info → qontract_reconcile-0.10.1rc765.dist-info}/top_level.txt +0 -0
- /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,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
|
reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/conftest.py
CHANGED
@@ -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({
|
33
|
-
|
34
|
-
|
35
|
-
|
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({
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
29
|
-
|
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:
|
29
|
+
"CHANNELS": {"channel-a": {}},
|
37
30
|
})
|
38
31
|
saas_content, expected = file_contents("multiple_namespaces")
|
39
32
|
renderer = Renderer()
|