qontract-reconcile 0.10.1rc763__py3-none-any.whl → 0.10.1rc764__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.1rc764.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc763.dist-info → qontract_reconcile-0.10.1rc764.dist-info}/RECORD +16 -5
- reconcile/external_resources/__init__.py +0 -0
- 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
- {qontract_reconcile-0.10.1rc763.dist-info → qontract_reconcile-0.10.1rc764.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc763.dist-info → qontract_reconcile-0.10.1rc764.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc763.dist-info → qontract_reconcile-0.10.1rc764.dist-info}/top_level.txt +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
|
+
)
|
File without changes
|
File without changes
|
{qontract_reconcile-0.10.1rc763.dist-info → qontract_reconcile-0.10.1rc764.dist-info}/top_level.txt
RENAMED
File without changes
|