dory-sdk 2.1.0__py3-none-any.whl → 2.1.4__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.
dory/k8s/labels.py ADDED
@@ -0,0 +1,505 @@
1
+ """Kubernetes label contract between SDK and Orchestrator.
2
+
3
+ This module defines the label contract that enables coordination between
4
+ the Dory SDK and the Dory Orchestrator. Both components must use identical
5
+ label keys and values for proper operation.
6
+
7
+ Label Categories:
8
+ 1. Identity Labels - Identify the processor and its managing controller
9
+ 2. Workload Labels - Control scheduling and workload placement
10
+ 3. Migration Labels - Track failover and migration state
11
+
12
+ Contract Version: 1.0
13
+ Orchestrator Compatibility: v1.0+
14
+ """
15
+
16
+ import os
17
+ from dataclasses import dataclass
18
+ from enum import Enum
19
+ from typing import Any
20
+
21
+ # =============================================================================
22
+ # Label Keys (must match orchestrator/pkg/utils/k8s.go)
23
+ # =============================================================================
24
+
25
+ # Identity Labels
26
+ LABEL_MANAGED_BY = "managed-by"
27
+ LABEL_APP_NAME = "app"
28
+ LABEL_PROCESSOR_ID = "processor-id"
29
+
30
+ # Workload Labels
31
+ LABEL_WORKLOAD_TYPE = "workload-type"
32
+ LABEL_WORKLOAD_LOCATION = "workload-location"
33
+ LABEL_NODE_TYPE = "node-type"
34
+ LABEL_INSTANCE_FAMILY = "instance-family"
35
+
36
+ # Migration Labels
37
+ LABEL_MIGRATED_FROM_EDGE = "migrated-from-edge"
38
+ LABEL_ORIGINAL_WORKLOAD_TYPE = "original-workload-type"
39
+ LABEL_ORIGINAL_NODE = "original-edge-node"
40
+
41
+
42
+ # =============================================================================
43
+ # Label Values (must match orchestrator/pkg/utils/k8s.go)
44
+ # =============================================================================
45
+
46
+ VALUE_ORCHESTRATOR_NAME = "dory-orchestrator"
47
+ VALUE_EDGE = "edge"
48
+ VALUE_MANAGED = "managed"
49
+ VALUE_APPLICATION = "application"
50
+
51
+
52
+ # =============================================================================
53
+ # Label Selectors (must match orchestrator/pkg/utils/k8s.go)
54
+ # =============================================================================
55
+
56
+ SELECTOR_EDGE_NODES = "node-type=edge"
57
+ SELECTOR_MANAGED_NODES = "workload-type=application"
58
+ SELECTOR_MANAGED_PODS = "managed-by=dory-orchestrator"
59
+
60
+
61
+ # =============================================================================
62
+ # Enums for Type Safety
63
+ # =============================================================================
64
+
65
+ class WorkloadLocation(Enum):
66
+ """Workload location for pods.
67
+
68
+ Determines where the pod should run:
69
+ - EDGE: Runs on externally-managed edge nodes (node-type=edge)
70
+ - MANAGED: Runs on Karpenter-managed cloud nodes (workload-type=application)
71
+ """
72
+ EDGE = VALUE_EDGE
73
+ MANAGED = VALUE_MANAGED
74
+
75
+
76
+ class NodeType(Enum):
77
+ """Node type classification.
78
+
79
+ Used for node selection:
80
+ - EDGE: Edge nodes with intermittent connectivity
81
+ - SYSTEM: System nodes (control plane, etc.)
82
+ - APPLICATION: Karpenter-managed application nodes
83
+ """
84
+ EDGE = "edge"
85
+ SYSTEM = "system"
86
+ APPLICATION = VALUE_APPLICATION
87
+
88
+
89
+ # =============================================================================
90
+ # Label Builder
91
+ # =============================================================================
92
+
93
+ @dataclass
94
+ class DoryLabels:
95
+ """Builder for Dory-compatible Kubernetes labels.
96
+
97
+ Creates labels that are compatible with the Dory Orchestrator's
98
+ label contract. Use this to ensure your pods are properly managed.
99
+
100
+ Usage:
101
+ # Create labels for an edge processor
102
+ labels = DoryLabels(
103
+ app_name="my-processor",
104
+ processor_id="proc-123",
105
+ workload_location=WorkloadLocation.EDGE,
106
+ )
107
+
108
+ # Get as dictionary for Kubernetes manifest
109
+ pod_labels = labels.to_dict()
110
+
111
+ # Create labels for a migrated pod
112
+ migrated_labels = DoryLabels(
113
+ app_name="my-processor",
114
+ processor_id="proc-123",
115
+ workload_location=WorkloadLocation.EDGE,
116
+ migrated_from_edge=True,
117
+ original_node="edge-node-1",
118
+ )
119
+ """
120
+
121
+ app_name: str
122
+ processor_id: str | None = None
123
+ workload_location: WorkloadLocation = WorkloadLocation.MANAGED
124
+ migrated_from_edge: bool = False
125
+ original_workload_type: str | None = None
126
+ original_node: str | None = None
127
+ custom_labels: dict[str, str] | None = None
128
+
129
+ def to_dict(self) -> dict[str, str]:
130
+ """Convert to Kubernetes labels dictionary.
131
+
132
+ Returns:
133
+ Dictionary of label key-value pairs
134
+ """
135
+ labels = {
136
+ LABEL_MANAGED_BY: VALUE_ORCHESTRATOR_NAME,
137
+ LABEL_APP_NAME: self.app_name,
138
+ LABEL_WORKLOAD_LOCATION: self.workload_location.value,
139
+ }
140
+
141
+ if self.processor_id:
142
+ labels[LABEL_PROCESSOR_ID] = self.processor_id
143
+
144
+ if self.migrated_from_edge:
145
+ labels[LABEL_MIGRATED_FROM_EDGE] = "true"
146
+
147
+ if self.original_workload_type:
148
+ labels[LABEL_ORIGINAL_WORKLOAD_TYPE] = self.original_workload_type
149
+
150
+ if self.original_node:
151
+ labels[LABEL_ORIGINAL_NODE] = self.original_node
152
+
153
+ if self.custom_labels:
154
+ labels.update(self.custom_labels)
155
+
156
+ return labels
157
+
158
+ @classmethod
159
+ def from_dict(cls, labels: dict[str, str]) -> "DoryLabels":
160
+ """Create from existing labels dictionary.
161
+
162
+ Args:
163
+ labels: Kubernetes labels dictionary
164
+
165
+ Returns:
166
+ DoryLabels instance
167
+ """
168
+ workload_location = WorkloadLocation.MANAGED
169
+ if labels.get(LABEL_WORKLOAD_LOCATION) == VALUE_EDGE:
170
+ workload_location = WorkloadLocation.EDGE
171
+
172
+ return cls(
173
+ app_name=labels.get(LABEL_APP_NAME, ""),
174
+ processor_id=labels.get(LABEL_PROCESSOR_ID),
175
+ workload_location=workload_location,
176
+ migrated_from_edge=labels.get(LABEL_MIGRATED_FROM_EDGE) == "true",
177
+ original_workload_type=labels.get(LABEL_ORIGINAL_WORKLOAD_TYPE),
178
+ original_node=labels.get(LABEL_ORIGINAL_NODE),
179
+ )
180
+
181
+ def is_edge_workload(self) -> bool:
182
+ """Check if this is an edge workload."""
183
+ return self.workload_location == WorkloadLocation.EDGE
184
+
185
+ def is_migrated(self) -> bool:
186
+ """Check if this pod was migrated from edge."""
187
+ return self.migrated_from_edge
188
+
189
+
190
+ # =============================================================================
191
+ # Label Reader Utilities
192
+ # =============================================================================
193
+
194
+ def get_label(labels: dict[str, str] | None, key: str, default: str = "") -> str:
195
+ """Safely get a label value.
196
+
197
+ Args:
198
+ labels: Labels dictionary (may be None)
199
+ key: Label key
200
+ default: Default value if not found
201
+
202
+ Returns:
203
+ Label value or default
204
+ """
205
+ if labels is None:
206
+ return default
207
+ return labels.get(key, default)
208
+
209
+
210
+ def get_app_name(labels: dict[str, str] | None) -> str:
211
+ """Get app name from labels."""
212
+ return get_label(labels, LABEL_APP_NAME)
213
+
214
+
215
+ def get_processor_id(labels: dict[str, str] | None) -> str:
216
+ """Get processor ID from labels."""
217
+ return get_label(labels, LABEL_PROCESSOR_ID)
218
+
219
+
220
+ def get_workload_location(labels: dict[str, str] | None) -> WorkloadLocation | None:
221
+ """Get workload location from labels.
222
+
223
+ Returns:
224
+ WorkloadLocation enum or None if not set
225
+ """
226
+ value = get_label(labels, LABEL_WORKLOAD_LOCATION)
227
+ if value == VALUE_EDGE:
228
+ return WorkloadLocation.EDGE
229
+ elif value == VALUE_MANAGED:
230
+ return WorkloadLocation.MANAGED
231
+ return None
232
+
233
+
234
+ def is_managed_by_dory(labels: dict[str, str] | None) -> bool:
235
+ """Check if pod is managed by Dory Orchestrator."""
236
+ return get_label(labels, LABEL_MANAGED_BY) == VALUE_ORCHESTRATOR_NAME
237
+
238
+
239
+ def is_edge_workload(labels: dict[str, str] | None) -> bool:
240
+ """Check if pod is an edge workload."""
241
+ return get_label(labels, LABEL_WORKLOAD_LOCATION) == VALUE_EDGE
242
+
243
+
244
+ def is_migrated_from_edge(labels: dict[str, str] | None) -> bool:
245
+ """Check if pod was migrated from edge."""
246
+ return get_label(labels, LABEL_MIGRATED_FROM_EDGE) == "true"
247
+
248
+
249
+ def get_original_node(labels: dict[str, str] | None) -> str:
250
+ """Get original edge node for migrated pod."""
251
+ return get_label(labels, LABEL_ORIGINAL_NODE)
252
+
253
+
254
+ # =============================================================================
255
+ # Environment-Based Label Detection
256
+ # =============================================================================
257
+
258
+ def get_pod_labels_from_env() -> dict[str, str]:
259
+ """Get pod labels from environment variables.
260
+
261
+ The Kubernetes Downward API can expose labels as environment variables.
262
+ This function reads common label values from the environment.
263
+
264
+ Expected environment variables:
265
+ - POD_NAME: Pod name
266
+ - POD_NAMESPACE: Pod namespace
267
+ - POD_IP: Pod IP address
268
+ - NODE_NAME: Node name
269
+ - DORY_APP_NAME: App name (from label)
270
+ - DORY_PROCESSOR_ID: Processor ID (from label)
271
+ - DORY_WORKLOAD_LOCATION: Workload location (from label)
272
+
273
+ Returns:
274
+ Dictionary of detected labels
275
+ """
276
+ labels = {}
277
+
278
+ if app_name := os.environ.get("DORY_APP_NAME"):
279
+ labels[LABEL_APP_NAME] = app_name
280
+
281
+ if processor_id := os.environ.get("DORY_PROCESSOR_ID"):
282
+ labels[LABEL_PROCESSOR_ID] = processor_id
283
+
284
+ if workload_location := os.environ.get("DORY_WORKLOAD_LOCATION"):
285
+ labels[LABEL_WORKLOAD_LOCATION] = workload_location
286
+
287
+ # Managed by is always the orchestrator if we're in Dory context
288
+ if labels:
289
+ labels[LABEL_MANAGED_BY] = VALUE_ORCHESTRATOR_NAME
290
+
291
+ return labels
292
+
293
+
294
+ def detect_workload_context() -> dict[str, Any]:
295
+ """Detect workload context from environment.
296
+
297
+ Reads environment variables set by Kubernetes Downward API and
298
+ Dory-specific variables to determine the workload context.
299
+
300
+ Returns:
301
+ Dictionary with context information:
302
+ - pod_name: str | None
303
+ - pod_namespace: str | None
304
+ - pod_ip: str | None
305
+ - node_name: str | None
306
+ - app_name: str | None
307
+ - processor_id: str | None
308
+ - workload_location: WorkloadLocation | None
309
+ - is_edge: bool
310
+ - is_kubernetes: bool
311
+ """
312
+ pod_name = os.environ.get("POD_NAME")
313
+ pod_namespace = os.environ.get("POD_NAMESPACE")
314
+ pod_ip = os.environ.get("POD_IP")
315
+ node_name = os.environ.get("NODE_NAME")
316
+ app_name = os.environ.get("DORY_APP_NAME")
317
+ processor_id = os.environ.get("DORY_PROCESSOR_ID")
318
+ workload_location_str = os.environ.get("DORY_WORKLOAD_LOCATION", "")
319
+
320
+ workload_location = None
321
+ if workload_location_str == VALUE_EDGE:
322
+ workload_location = WorkloadLocation.EDGE
323
+ elif workload_location_str == VALUE_MANAGED:
324
+ workload_location = WorkloadLocation.MANAGED
325
+
326
+ is_kubernetes = pod_name is not None and pod_namespace is not None
327
+ is_edge = workload_location == WorkloadLocation.EDGE
328
+
329
+ return {
330
+ "pod_name": pod_name,
331
+ "pod_namespace": pod_namespace,
332
+ "pod_ip": pod_ip,
333
+ "node_name": node_name,
334
+ "app_name": app_name,
335
+ "processor_id": processor_id,
336
+ "workload_location": workload_location,
337
+ "is_edge": is_edge,
338
+ "is_kubernetes": is_kubernetes,
339
+ }
340
+
341
+
342
+ # =============================================================================
343
+ # Node Selector Builders
344
+ # =============================================================================
345
+
346
+ def edge_node_selector() -> dict[str, str]:
347
+ """Get node selector for edge nodes.
348
+
349
+ Returns:
350
+ Node selector dictionary for edge node placement
351
+ """
352
+ return {LABEL_NODE_TYPE: NodeType.EDGE.value}
353
+
354
+
355
+ def managed_node_selector() -> dict[str, str]:
356
+ """Get node selector for managed (Karpenter) nodes.
357
+
358
+ Returns:
359
+ Node selector dictionary for managed node placement
360
+ """
361
+ return {LABEL_WORKLOAD_TYPE: NodeType.APPLICATION.value}
362
+
363
+
364
+ def edge_toleration() -> dict[str, Any]:
365
+ """Get toleration for edge nodes.
366
+
367
+ Edge nodes typically have a taint to prevent non-edge workloads
368
+ from scheduling on them.
369
+
370
+ Returns:
371
+ Toleration dictionary for edge nodes
372
+ """
373
+ return {
374
+ "key": "edge-node",
375
+ "operator": "Equal",
376
+ "value": "true",
377
+ "effect": "NoSchedule",
378
+ }
379
+
380
+
381
+ # =============================================================================
382
+ # Contract Documentation
383
+ # =============================================================================
384
+
385
+ CONTRACT_VERSION = "1.0"
386
+
387
+ LABEL_CONTRACT = """
388
+ Dory SDK/Orchestrator Label Contract v1.0
389
+ =========================================
390
+
391
+ This document defines the label contract between the Dory SDK and
392
+ the Dory Orchestrator. Both components MUST use these exact labels
393
+ for proper coordination.
394
+
395
+ IDENTITY LABELS
396
+ ---------------
397
+ These labels identify the processor and its managing controller.
398
+
399
+ | Label Key | Description | Required | Example Value |
400
+ |----------------|--------------------------------|----------|--------------------|
401
+ | managed-by | Managing controller | Yes | dory-orchestrator |
402
+ | app | Application name | Yes | my-processor |
403
+ | processor-id | Unique processor identifier | No | proc-123 |
404
+
405
+ WORKLOAD LABELS
406
+ ---------------
407
+ These labels control scheduling and workload placement.
408
+
409
+ | Label Key | Description | Values | Default |
410
+ |--------------------|---------------------------------|---------------|----------|
411
+ | workload-location | Where the pod runs | edge, managed | managed |
412
+ | workload-type | Node workload type (Karpenter) | application | - |
413
+ | node-type | Node classification | edge, system | - |
414
+
415
+ MIGRATION LABELS
416
+ ----------------
417
+ These labels track failover and migration state.
418
+
419
+ | Label Key | Description | Values | Set By |
420
+ |------------------------|--------------------------------|------------|---------------|
421
+ | migrated-from-edge | Pod was migrated from edge | true | Orchestrator |
422
+ | original-workload-type | Original workload type | edge | Orchestrator |
423
+ | original-edge-node | Original edge node name | <node> | Orchestrator |
424
+
425
+ NODE SELECTORS
426
+ --------------
427
+ Edge workloads:
428
+ node-type: edge
429
+
430
+ Managed workloads (Karpenter):
431
+ workload-type: application
432
+
433
+ TOLERATIONS
434
+ -----------
435
+ Edge nodes have a taint that requires this toleration:
436
+ key: edge-node
437
+ operator: Equal
438
+ value: true
439
+ effect: NoSchedule
440
+
441
+ ENVIRONMENT VARIABLES
442
+ ---------------------
443
+ The SDK expects these environment variables (set via Downward API):
444
+
445
+ | Variable | Source | Description |
446
+ |------------------------|-------------------------------|-----------------------|
447
+ | POD_NAME | metadata.name | Pod name |
448
+ | POD_NAMESPACE | metadata.namespace | Pod namespace |
449
+ | POD_IP | status.podIP | Pod IP address |
450
+ | NODE_NAME | spec.nodeName | Node name |
451
+ | DORY_APP_NAME | metadata.labels['app'] | App label value |
452
+ | DORY_PROCESSOR_ID | metadata.labels['processor-id']| Processor ID label |
453
+ | DORY_WORKLOAD_LOCATION | metadata.labels['workload-location'] | Location label |
454
+
455
+ FAILOVER FLOW
456
+ -------------
457
+ 1. Edge pod running with:
458
+ - workload-location: edge
459
+ - node-type selector: edge
460
+
461
+ 2. Edge node fails, Orchestrator:
462
+ - Deletes edge pod
463
+ - Creates new pod with:
464
+ - workload-location: edge (preserved)
465
+ - migrated-from-edge: true (added)
466
+ - original-edge-node: <old-node> (added)
467
+ - workload-type selector: application (changed)
468
+
469
+ 3. Edge node recovers, Orchestrator:
470
+ - Deletes migrated pod
471
+ - Creates new pod on edge with original labels
472
+
473
+ USAGE EXAMPLE
474
+ -------------
475
+ ```python
476
+ from dory.k8s.labels import DoryLabels, WorkloadLocation
477
+
478
+ # Create labels for edge processor
479
+ labels = DoryLabels(
480
+ app_name="my-processor",
481
+ processor_id="proc-123",
482
+ workload_location=WorkloadLocation.EDGE,
483
+ )
484
+
485
+ # Use in Kubernetes manifest
486
+ pod_spec = {
487
+ "metadata": {
488
+ "labels": labels.to_dict(),
489
+ },
490
+ "spec": {
491
+ "nodeSelector": edge_node_selector(),
492
+ "tolerations": [edge_toleration()],
493
+ },
494
+ }
495
+ ```
496
+ """
497
+
498
+
499
+ def get_contract_documentation() -> str:
500
+ """Get the label contract documentation.
501
+
502
+ Returns:
503
+ Complete label contract documentation string
504
+ """
505
+ return LABEL_CONTRACT
@@ -3,9 +3,58 @@
3
3
  from dory.migration.state_manager import StateManager
4
4
  from dory.migration.serialization import StateSerializer
5
5
  from dory.migration.configmap import ConfigMapStore
6
+ from dory.migration.s3_store import S3Store, S3Config, OfflineBuffer
7
+ from dory.migration.transfer import (
8
+ TransferConfig,
9
+ TransferMetrics,
10
+ StateTransferError,
11
+ StateTransferTimeout,
12
+ StateSizeExceeded,
13
+ validate_state_size,
14
+ ORCHESTRATOR_STATE_TIMEOUT_SEC,
15
+ ORCHESTRATOR_MAX_STATE_SIZE,
16
+ )
17
+ from dory.migration.versioning import (
18
+ StateFormatVersion,
19
+ VersionedState,
20
+ StateMetadata,
21
+ VersionNegotiator,
22
+ VersionNegotiationResult,
23
+ get_sdk_version,
24
+ get_supported_versions,
25
+ is_version_supported,
26
+ serialize_state,
27
+ deserialize_state,
28
+ get_version_info,
29
+ SDK_STATE_VERSION,
30
+ )
6
31
 
7
32
  __all__ = [
8
33
  "StateManager",
9
34
  "StateSerializer",
10
35
  "ConfigMapStore",
36
+ "S3Store",
37
+ "S3Config",
38
+ "OfflineBuffer",
39
+ "TransferConfig",
40
+ "TransferMetrics",
41
+ "StateTransferError",
42
+ "StateTransferTimeout",
43
+ "StateSizeExceeded",
44
+ "validate_state_size",
45
+ "ORCHESTRATOR_STATE_TIMEOUT_SEC",
46
+ "ORCHESTRATOR_MAX_STATE_SIZE",
47
+ # Versioning
48
+ "StateFormatVersion",
49
+ "VersionedState",
50
+ "StateMetadata",
51
+ "VersionNegotiator",
52
+ "VersionNegotiationResult",
53
+ "get_sdk_version",
54
+ "get_supported_versions",
55
+ "is_version_supported",
56
+ "serialize_state",
57
+ "deserialize_state",
58
+ "get_version_info",
59
+ "SDK_STATE_VERSION",
11
60
  ]