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.
@@ -0,0 +1,350 @@
1
+ import json
2
+ import logging
3
+ from collections.abc import Iterable
4
+ from datetime import datetime, timezone
5
+ from enum import Enum
6
+
7
+ from sretoolbox.utils import threaded
8
+
9
+ from reconcile.external_resources.factories import (
10
+ AWSExternalResourceFactory,
11
+ ExternalResourceFactory,
12
+ ModuleProvisionDataFactory,
13
+ ObjectFactory,
14
+ TerraformModuleProvisionDataFactory,
15
+ setup_aws_resource_factories,
16
+ )
17
+ from reconcile.external_resources.metrics import ExternalResourcesReconcileErrorsGauge
18
+ from reconcile.external_resources.model import (
19
+ Action,
20
+ ExternalResource,
21
+ ExternalResourceKey,
22
+ ExternalResourceModuleConfiguration,
23
+ ExternalResourcesInventory,
24
+ ExternalResourceValidationError,
25
+ ModuleInventory,
26
+ Reconciliation,
27
+ )
28
+ from reconcile.external_resources.reconciler import ExternalResourcesReconciler
29
+ from reconcile.external_resources.secrets_sync import InClusterSecretsReconciler
30
+ from reconcile.external_resources.state import (
31
+ ExternalResourcesStateDynamoDB,
32
+ ExternalResourceState,
33
+ ReconcileStatus,
34
+ ResourceStatus,
35
+ )
36
+ from reconcile.gql_definitions.external_resources.external_resources_settings import (
37
+ ExternalResourcesSettingsV1,
38
+ )
39
+ from reconcile.utils import metrics
40
+ from reconcile.utils.external_resource_spec import (
41
+ ExternalResourceSpec,
42
+ )
43
+ from reconcile.utils.secret_reader import SecretReaderBase
44
+
45
+ FLAG_RESOURCE_MANAGED_BY_ERV2 = "managed_by_erv2"
46
+
47
+
48
+ def setup_factories(
49
+ settings: ExternalResourcesSettingsV1,
50
+ module_inventory: ModuleInventory,
51
+ er_inventory: ExternalResourcesInventory,
52
+ secret_reader: SecretReaderBase,
53
+ ) -> ObjectFactory[ExternalResourceFactory]:
54
+ tf_factory = TerraformModuleProvisionDataFactory(settings=settings)
55
+
56
+ aws_provision_factories = ObjectFactory[ModuleProvisionDataFactory]()
57
+ aws_provision_factories.register_factory("terraform", tf_factory)
58
+ aws_provision_factories.register_factory("cdktf", tf_factory)
59
+
60
+ of = ObjectFactory[ExternalResourceFactory]()
61
+ of.register_factory(
62
+ "aws",
63
+ AWSExternalResourceFactory(
64
+ module_inventory=module_inventory,
65
+ er_inventory=er_inventory,
66
+ secret_reader=secret_reader,
67
+ provision_factories=aws_provision_factories,
68
+ resource_factories=setup_aws_resource_factories(
69
+ er_inventory, secret_reader
70
+ ),
71
+ ),
72
+ )
73
+ return of
74
+
75
+
76
+ class ReconcileAction(str, Enum):
77
+ NOOP = "NOOP"
78
+ APPLY_NOT_EXISTS = "Resource does not exist"
79
+ APPLY_ERROR = "Resource status in ERROR state"
80
+ APPLY_SPEC_CHANGED = "Resource spec has changed"
81
+ APPLY_DRIFT_DETECTION = "Resource drift detection run"
82
+ DESTROY_CREATED = "Resource no longer exists in the configuration"
83
+ DESTROY_ERROR = "Resource status in ERROR state"
84
+
85
+
86
+ class ExternalResourcesManager:
87
+ def __init__(
88
+ self,
89
+ state_manager: ExternalResourcesStateDynamoDB,
90
+ settings: ExternalResourcesSettingsV1,
91
+ module_inventory: ModuleInventory,
92
+ reconciler: ExternalResourcesReconciler,
93
+ secret_reader: SecretReaderBase,
94
+ er_inventory: ExternalResourcesInventory,
95
+ factories: ObjectFactory[ExternalResourceFactory],
96
+ secrets_reconciler: InClusterSecretsReconciler,
97
+ thread_pool_size: int,
98
+ ) -> None:
99
+ self.state_mgr = state_manager
100
+ self.settings = settings
101
+ self.module_inventory = module_inventory
102
+ self.reconciler = reconciler
103
+ self.er_inventory = er_inventory
104
+ self.factories = factories
105
+ self.secret_reader = secret_reader
106
+ self.secrets_reconciler = secrets_reconciler
107
+ self.errors: dict[ExternalResourceKey, ExternalResourceValidationError] = {}
108
+ self.thread_pool_size = thread_pool_size
109
+
110
+ def _get_reconcile_action(
111
+ self, reconciliation: Reconciliation, state: ExternalResourceState
112
+ ) -> ReconcileAction:
113
+ if reconciliation.action == Action.APPLY:
114
+ match state.resource_status:
115
+ case ResourceStatus.NOT_EXISTS:
116
+ return ReconcileAction.APPLY_NOT_EXISTS
117
+ case ResourceStatus.ERROR:
118
+ return ReconcileAction.APPLY_ERROR
119
+ case ResourceStatus.CREATED:
120
+ if (
121
+ reconciliation.resource_hash
122
+ != state.reconciliation.resource_hash
123
+ ):
124
+ return ReconcileAction.APPLY_SPEC_CHANGED
125
+ elif (
126
+ (datetime.now(state.ts.tzinfo) - state.ts).total_seconds()
127
+ > reconciliation.module_configuration.reconcile_drift_interval_minutes
128
+ * 60
129
+ ):
130
+ return ReconcileAction.APPLY_DRIFT_DETECTION
131
+ elif reconciliation.action == Action.DESTROY:
132
+ match state.resource_status:
133
+ case ResourceStatus.CREATED:
134
+ return ReconcileAction.DESTROY_CREATED
135
+ case ResourceStatus.ERROR:
136
+ return ReconcileAction.DESTROY_ERROR
137
+
138
+ return ReconcileAction.NOOP
139
+
140
+ def _resource_needs_reconciliation(
141
+ self,
142
+ reconciliation: Reconciliation,
143
+ state: ExternalResourceState,
144
+ ) -> bool:
145
+ reconcile_action = self._get_reconcile_action(reconciliation, state)
146
+ reconcile = reconcile_action != ReconcileAction.NOOP
147
+ if reconcile:
148
+ logging.info(
149
+ "Reconciling: Status: [%s], Action: [%s], reason: [%s], key:[%s]",
150
+ state.resource_status.value,
151
+ reconciliation.action.value,
152
+ reconcile_action.value,
153
+ reconciliation.key,
154
+ )
155
+ return reconcile
156
+
157
+ def _get_desired_objects_reconciliations(self) -> set[Reconciliation]:
158
+ r: set[Reconciliation] = set()
159
+ for key, spec in self.er_inventory.items():
160
+ module = self.module_inventory.get_from_spec(spec)
161
+
162
+ try:
163
+ resource = self._build_external_resource(spec, self.er_inventory)
164
+ except ExternalResourceValidationError as e:
165
+ k = ExternalResourceKey.from_spec(spec)
166
+ self.errors[k] = e
167
+ continue
168
+
169
+ reconciliation = Reconciliation(
170
+ key=key,
171
+ resource_hash=resource.hash(),
172
+ input=self._serialize_resource_input(resource),
173
+ action=Action.APPLY,
174
+ module_configuration=ExternalResourceModuleConfiguration.resolve_configuration(
175
+ module, spec
176
+ ),
177
+ )
178
+ r.add(reconciliation)
179
+ return r
180
+
181
+ def _get_deleted_objects_reconciliations(self) -> set[Reconciliation]:
182
+ desired_keys = set(self.er_inventory.keys())
183
+ state_resource_keys = self.state_mgr.get_all_resource_keys()
184
+ deleted_keys = state_resource_keys - desired_keys
185
+ r: set[Reconciliation] = set()
186
+ for key in deleted_keys:
187
+ state = self.state_mgr.get_external_resource_state(key)
188
+ reconciliation = Reconciliation(
189
+ key=key,
190
+ resource_hash=state.reconciliation.resource_hash,
191
+ module_configuration=state.reconciliation.module_configuration,
192
+ input=state.reconciliation.input,
193
+ action=Action.DESTROY,
194
+ )
195
+ r.add(reconciliation)
196
+ return r
197
+
198
+ def _update_in_progress_state(
199
+ self, r: Reconciliation, state: ExternalResourceState
200
+ ) -> bool:
201
+ """Gets the resource reconciliation state from the Job and updates the
202
+ Resource state accordingly. It also returns if the target outputs secret needs
203
+ to be reconciled.
204
+
205
+ :param r: Reconciliation object
206
+ :param state: State object
207
+ :return: True/False if target Secret needs to be reconciled.
208
+ """
209
+ need_secret_sync = False
210
+
211
+ if state.resource_status not in set([
212
+ ResourceStatus.DELETE_IN_PROGRESS,
213
+ ResourceStatus.IN_PROGRESS,
214
+ ]):
215
+ return need_secret_sync
216
+
217
+ logging.info(
218
+ "Reconciliation In progress. Action: %s, Key:%s",
219
+ state.reconciliation.action,
220
+ state.reconciliation.key,
221
+ )
222
+
223
+ # Need to check the reconciliation set in the state, not the desired one
224
+ # as the reconciliation object might be from a previous desired state
225
+ error = False
226
+ match self.reconciler.get_resource_reconcile_status(state.reconciliation):
227
+ case ReconcileStatus.SUCCESS:
228
+ logging.info(
229
+ "Reconciliation ended SUCCESSFULLY. Action: %s, key:%s",
230
+ r.action.value,
231
+ r.key,
232
+ )
233
+ if r.action == Action.APPLY:
234
+ state.resource_status = ResourceStatus.CREATED
235
+ state.reconciliation_errors = 0
236
+ self.state_mgr.set_external_resource_state(state)
237
+ need_secret_sync = True
238
+ elif r.action == Action.DESTROY:
239
+ state.resource_status = ResourceStatus.DELETED
240
+ self.state_mgr.del_external_resource_state(r.key)
241
+ case ReconcileStatus.ERROR:
242
+ logging.info(
243
+ "Reconciliation ended with ERROR: Action:%s, Key:%s",
244
+ r.action.value,
245
+ r.key,
246
+ )
247
+ error = True
248
+ case ReconcileStatus.NOT_EXISTS:
249
+ logging.info(
250
+ "Reconciliation should exist but it doesn't. Marking as ERROR to retrigger: Action:%s, Key:%s",
251
+ r.action.value,
252
+ r.key,
253
+ )
254
+ error = True
255
+ if error:
256
+ state.resource_status = ResourceStatus.ERROR
257
+ state.reconciliation_errors += 1
258
+ self.state_mgr.set_external_resource_state(state)
259
+
260
+ return need_secret_sync
261
+
262
+ def _update_state(self, r: Reconciliation, state: ExternalResourceState) -> None:
263
+ state.ts = datetime.now(timezone.utc)
264
+ if r.action == Action.APPLY:
265
+ state.resource_status = ResourceStatus.IN_PROGRESS
266
+ elif r.action == Action.DESTROY:
267
+ state.resource_status = ResourceStatus.DELETE_IN_PROGRESS
268
+ state.reconciliation = r
269
+ self.state_mgr.set_external_resource_state(state)
270
+
271
+ def _need_secret_sync(
272
+ self, r: Reconciliation, state: ExternalResourceState
273
+ ) -> bool:
274
+ return (
275
+ r.action == Action.APPLY and state.resource_status == ResourceStatus.CREATED
276
+ )
277
+
278
+ def _sync_secrets(self, keys: Iterable[ExternalResourceKey]) -> None:
279
+ specs = [spec for key in keys if (spec := self.er_inventory.get(key))]
280
+ self.secrets_reconciler.sync_secrets(specs=specs)
281
+
282
+ def _build_external_resource(
283
+ self, spec: ExternalResourceSpec, er_inventory: ExternalResourcesInventory
284
+ ) -> ExternalResource:
285
+ f = self.factories.get_factory(spec.provision_provider)
286
+ resource = f.create_external_resource(spec)
287
+ f.validate_external_resource(resource)
288
+ return resource
289
+
290
+ def _serialize_resource_input(self, resource: ExternalResource) -> str:
291
+ return json.dumps(
292
+ resource.dict(exclude={"data": {FLAG_RESOURCE_MANAGED_BY_ERV2}})
293
+ )
294
+
295
+ def handle_resources(self) -> None:
296
+ desired_r = self._get_desired_objects_reconciliations()
297
+ deleted_r = self._get_deleted_objects_reconciliations()
298
+ to_sync: set[ExternalResourceKey] = set()
299
+ for r in desired_r.union(deleted_r):
300
+ state = self.state_mgr.get_external_resource_state(r.key)
301
+ need_sync = self._update_in_progress_state(r, state)
302
+ if need_sync:
303
+ to_sync.add(r.key)
304
+
305
+ metrics.set_gauge(
306
+ ExternalResourcesReconcileErrorsGauge(
307
+ provision_provider=r.key.provision_provider,
308
+ provisioner_name=r.key.provisioner_name,
309
+ provider=r.key.provider,
310
+ identifier=r.key.identifier,
311
+ ),
312
+ float(state.reconciliation_errors),
313
+ )
314
+
315
+ if self._resource_needs_reconciliation(reconciliation=r, state=state):
316
+ self.reconciler.reconcile_resource(reconciliation=r)
317
+ self._update_state(r, state)
318
+
319
+ if to_sync:
320
+ self._sync_secrets(keys=to_sync)
321
+
322
+ def handle_dry_run_resources(self) -> None:
323
+ desired_r = self._get_desired_objects_reconciliations()
324
+ deleted_r = self._get_deleted_objects_reconciliations()
325
+ reconciliations = desired_r.union(deleted_r)
326
+ triggered = set[Reconciliation]()
327
+
328
+ for r in reconciliations:
329
+ state = self.state_mgr.get_external_resource_state(key=r.key)
330
+ if (
331
+ r.action == Action.APPLY
332
+ and state.reconciliation.resource_hash != r.resource_hash
333
+ ) or r.action == Action.DESTROY:
334
+ triggered.add(r)
335
+
336
+ threaded.run(
337
+ self.reconciler.reconcile_resource,
338
+ triggered,
339
+ thread_pool_size=self.thread_pool_size,
340
+ )
341
+
342
+ results = self.reconciler.wait_for_reconcile_list_completion(
343
+ triggered, check_interval_seconds=10, timeout_seconds=-1
344
+ )
345
+
346
+ for r in triggered:
347
+ self.reconciler.get_resource_reconcile_logs(reconciliation=r)
348
+
349
+ if ReconcileStatus.ERROR in [rs for rs in results.values()]:
350
+ raise Exception("Some Resources have reconciliation errors.")
@@ -0,0 +1,4 @@
1
+ from reconcile.utils.semver_helper import make_semver
2
+
3
+ QONTRACT_INTEGRATION = "external_resources"
4
+ QONTRACT_INTEGRATION_VERSION = make_semver(0, 1, 0)
@@ -0,0 +1,20 @@
1
+ from pydantic import BaseModel
2
+
3
+ from reconcile.utils.metrics import (
4
+ GaugeMetric,
5
+ )
6
+
7
+
8
+ class ExternalResourcesBaseMetric(BaseModel):
9
+ integration = "external_resources"
10
+
11
+
12
+ class ExternalResourcesReconcileErrorsGauge(ExternalResourcesBaseMetric, GaugeMetric):
13
+ provision_provider: str
14
+ provisioner_name: str
15
+ provider: str
16
+ identifier: str
17
+
18
+ @classmethod
19
+ def name(cls) -> str:
20
+ return "external_resources_reconcile_status"
@@ -0,0 +1,244 @@
1
+ import base64
2
+ import hashlib
3
+ import json
4
+ from abc import (
5
+ ABC,
6
+ )
7
+ from collections.abc import (
8
+ Iterable,
9
+ Iterator,
10
+ MutableMapping,
11
+ )
12
+ from enum import Enum
13
+ from typing import Any
14
+
15
+ from pydantic import BaseModel
16
+
17
+ from reconcile.gql_definitions.external_resources.external_resources_modules import (
18
+ ExternalResourcesModuleV1,
19
+ )
20
+ from reconcile.gql_definitions.external_resources.external_resources_namespaces import (
21
+ NamespaceTerraformProviderResourceAWSV1,
22
+ NamespaceTerraformResourceRDSV1,
23
+ NamespaceTerraformResourceRoleV1,
24
+ NamespaceV1,
25
+ )
26
+ from reconcile.utils.exceptions import FetchResourceError
27
+ from reconcile.utils.external_resource_spec import (
28
+ ExternalResourceSpec,
29
+ )
30
+
31
+
32
+ class ExternalResourceValidationError(Exception):
33
+ errors: list[str] = []
34
+
35
+ def add_validation_error(self, msg: str) -> None:
36
+ self.errors.append(msg)
37
+
38
+
39
+ class ExternalResourceKey(BaseModel, frozen=True):
40
+ provision_provider: str
41
+ provisioner_name: str
42
+ provider: str
43
+ identifier: str
44
+
45
+ @staticmethod
46
+ def from_spec(spec: ExternalResourceSpec) -> "ExternalResourceKey":
47
+ return ExternalResourceKey(
48
+ provision_provider=spec.provision_provider,
49
+ provisioner_name=spec.provisioner_name,
50
+ identifier=spec.identifier,
51
+ provider=spec.provider,
52
+ )
53
+
54
+ def hash(self) -> str:
55
+ return hashlib.md5(
56
+ json.dumps(self.dict(), sort_keys=True).encode("utf-8")
57
+ ).hexdigest()
58
+
59
+ @property
60
+ def state_path(self) -> str:
61
+ return f"{self.provision_provider}/{self.provisioner_name}/{self.provider}/{self.identifier}"
62
+
63
+
64
+ class ExternalResourcesInventory(MutableMapping):
65
+ _inventory: dict[ExternalResourceKey, ExternalResourceSpec] = {}
66
+
67
+ def __init__(self, namespaces: Iterable[NamespaceV1]) -> None:
68
+ desired_providers = [
69
+ (p, ns)
70
+ for ns in namespaces
71
+ for p in ns.external_resources or []
72
+ if isinstance(p, NamespaceTerraformProviderResourceAWSV1) and p.resources
73
+ ]
74
+
75
+ desired_specs = [
76
+ ExternalResourceSpec(
77
+ provision_provider=p.provider,
78
+ provisioner=p.provisioner.dict(),
79
+ resource=r.dict(),
80
+ namespace=ns.dict(),
81
+ )
82
+ for (p, ns) in desired_providers
83
+ for r in p.resources
84
+ if isinstance(
85
+ r, (NamespaceTerraformResourceRDSV1, NamespaceTerraformResourceRoleV1)
86
+ )
87
+ and r.managed_by_erv2
88
+ ]
89
+
90
+ for spec in desired_specs:
91
+ # self.set(ExternalResourceKey.from_spec(spec), spec)
92
+ self._inventory[ExternalResourceKey.from_spec(spec)] = spec
93
+
94
+ def __getitem__(self, key: ExternalResourceKey) -> ExternalResourceSpec | None:
95
+ return self._inventory[key]
96
+
97
+ def __setitem__(self, key: ExternalResourceKey, spec: ExternalResourceSpec) -> None:
98
+ self._inventory[key] = spec
99
+
100
+ def __delitem__(self, key: ExternalResourceKey) -> None:
101
+ del self._inventory[key]
102
+
103
+ def __iter__(self) -> Iterator[ExternalResourceKey]:
104
+ return iter(self._inventory)
105
+
106
+ def __len__(self) -> int:
107
+ return len(self._inventory)
108
+
109
+ def get_inventory_spec(
110
+ self, provision_provider: str, provisioner: str, provider: str, identifier: str
111
+ ) -> ExternalResourceSpec:
112
+ """Convinience method to find referenced specs in the inventory. For example, finding a sourcedb reference for an RDS instance."""
113
+ key = ExternalResourceKey(
114
+ provision_provider=provision_provider,
115
+ provisioner_name=provisioner,
116
+ provider=provider,
117
+ identifier=identifier,
118
+ )
119
+ try:
120
+ return self._inventory[key]
121
+ except KeyError:
122
+ msg = f"Resource spec not found: Provider {provider}, Id: {identifier}"
123
+ raise FetchResourceError(msg)
124
+
125
+
126
+ class Action(str, Enum):
127
+ DESTROY: str = "Destroy"
128
+ APPLY: str = "Apply"
129
+
130
+
131
+ class ExternalResourceModuleKey(BaseModel, frozen=True):
132
+ provision_provider: str
133
+ provider: str
134
+
135
+
136
+ class ModuleInventory:
137
+ inventory: dict[ExternalResourceModuleKey, ExternalResourcesModuleV1]
138
+
139
+ def __init__(
140
+ self, inventory: dict[ExternalResourceModuleKey, ExternalResourcesModuleV1]
141
+ ):
142
+ self.inventory = inventory
143
+
144
+ def get_from_external_resource_key(
145
+ self, key: ExternalResourceKey
146
+ ) -> ExternalResourcesModuleV1:
147
+ return self.inventory[
148
+ ExternalResourceModuleKey(
149
+ provision_provider=key.provision_provider, provider=key.provider
150
+ )
151
+ ]
152
+
153
+ def get_from_spec(self, spec: ExternalResourceSpec) -> ExternalResourcesModuleV1:
154
+ return self.inventory[
155
+ ExternalResourceModuleKey(
156
+ provision_provider=spec.provision_provider, provider=spec.provider
157
+ )
158
+ ]
159
+
160
+
161
+ def load_module_inventory(
162
+ modules: Iterable[ExternalResourcesModuleV1],
163
+ ) -> ModuleInventory:
164
+ return ModuleInventory({
165
+ ExternalResourceModuleKey(
166
+ provision_provider=m.provision_provider, provider=m.provider
167
+ ): m
168
+ for m in modules
169
+ })
170
+
171
+
172
+ class ExternalResourceModuleConfiguration(BaseModel, frozen=True):
173
+ image: str = ""
174
+ version: str = ""
175
+ reconcile_drift_interval_minutes: int = -1000
176
+ reconcile_timeout_minutes: int = -1000
177
+
178
+ @property
179
+ def image_version(self) -> str:
180
+ return f"{self.image}:{self.version}"
181
+
182
+ @staticmethod
183
+ def resolve_configuration(
184
+ module: ExternalResourcesModuleV1, spec: ExternalResourceSpec
185
+ ) -> "ExternalResourceModuleConfiguration":
186
+ # TODO: Modify resource schemas to include this attributes
187
+ data = {
188
+ "image": module.image,
189
+ "version": module.default_version,
190
+ "reconcile_drift_interval_minutes": module.reconcile_drift_interval_minutes,
191
+ "reconcile_timeout_minutes": module.reconcile_timeout_minutes,
192
+ }
193
+ return ExternalResourceModuleConfiguration.parse_obj(data)
194
+
195
+
196
+ class Reconciliation(BaseModel, frozen=True):
197
+ key: ExternalResourceKey
198
+ resource_hash: str = ""
199
+ input: str = ""
200
+ action: Action = Action.APPLY
201
+ module_configuration: ExternalResourceModuleConfiguration = (
202
+ ExternalResourceModuleConfiguration()
203
+ )
204
+
205
+
206
+ class ModuleProvisionData(ABC, BaseModel):
207
+ pass
208
+
209
+
210
+ class TerraformModuleProvisionData(ModuleProvisionData):
211
+ """Specific Provision Options for modules based on Terraform or CDKTF"""
212
+
213
+ tf_state_bucket: str
214
+ tf_state_region: str
215
+ tf_state_dynamodb_table: str
216
+ tf_state_key: str
217
+
218
+
219
+ class ExternalResourceProvision(BaseModel):
220
+ """External resource app-interface attributes. They are not part of the resource but are needed
221
+ for annotating secrets or other stuff
222
+ """
223
+
224
+ provision_provider: str
225
+ provisioner: str
226
+ provider: str
227
+ identifier: str
228
+ target_cluster: str
229
+ target_namespace: str
230
+ target_secret_name: str
231
+ module_provision_data: ModuleProvisionData
232
+
233
+
234
+ class ExternalResource(BaseModel):
235
+ data: dict[str, Any]
236
+ provision: ExternalResourceProvision
237
+
238
+ def hash(self) -> str:
239
+ return hashlib.md5(
240
+ json.dumps(self.data, sort_keys=True).encode("utf-8")
241
+ ).hexdigest()
242
+
243
+ def serialize_input(self) -> str:
244
+ return base64.b64encode(json.dumps(self.dict()).encode()).decode()