qontract-reconcile 0.10.1rc762__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.
@@ -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
+ )
@@ -80,6 +80,8 @@ query AWSAccountManagerAccounts {
80
80
  quotaLimits {
81
81
  path
82
82
  }
83
+ resourcesDefaultRegion
84
+ supportedDeploymentRegions
83
85
  }
84
86
  organization_accounts {
85
87
  ... AWSAccountManaged
@@ -129,6 +131,8 @@ class AWSAccountRequestV1(ConfiguredBaseModel):
129
131
  account_owner: OwnerV1 = Field(..., alias="accountOwner")
130
132
  organization: AWSOrganizationV1 = Field(..., alias="organization")
131
133
  quota_limits: Optional[list[AWSQuotaLimitsV1]] = Field(..., alias="quotaLimits")
134
+ resources_default_region: str = Field(..., alias="resourcesDefaultRegion")
135
+ supported_deployment_regions: Optional[list[str]] = Field(..., alias="supportedDeploymentRegions")
132
136
 
133
137
 
134
138
  class AWSAccountV1(AWSAccountManaged):