midstream-platform 1.0.0rc1__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.
- midstream/__init__.py +5 -0
- midstream/config.py +577 -0
- midstream/handlers/__init__.py +34 -0
- midstream/handlers/identity.py +60 -0
- midstream/handlers/maintenance.py +207 -0
- midstream/handlers/request_intake.py +51 -0
- midstream/handlers/resource_observation.py +272 -0
- midstream/handlers/runtime_status.py +14 -0
- midstream/handlers/settings.py +29 -0
- midstream/handlers/table_events.py +34 -0
- midstream/handlers/workflow_apply.py +276 -0
- midstream/identity/__init__.py +72 -0
- midstream/identity/access_tokens.py +72 -0
- midstream/identity/authorization.py +296 -0
- midstream/identity/directory.py +144 -0
- midstream/identity/passwords.py +169 -0
- midstream/identity/permissions.py +519 -0
- midstream/identity/principals.py +375 -0
- midstream/identity/secrets.py +119 -0
- midstream/identity/tokens.py +167 -0
- midstream/identity/validation.py +121 -0
- midstream/kafka/__init__.py +25 -0
- midstream/kafka/admin.py +403 -0
- midstream/kafka/metadata.py +429 -0
- midstream/kubernetes/__init__.py +5 -0
- midstream/kubernetes/client.py +5 -0
- midstream/kubernetes/kaspr_apps.py +341 -0
- midstream/lakehouse/__init__.py +5 -0
- midstream/lakehouse/catalog.py +406 -0
- midstream/lakehouse/commits.py +595 -0
- midstream/lakehouse/files.py +241 -0
- midstream/lakehouse/gcs.py +116 -0
- midstream/lakehouse/io.py +180 -0
- midstream/lakehouse/lifecycle.py +81 -0
- midstream/lakehouse/maintenance.py +604 -0
- midstream/lakehouse/offsets.py +147 -0
- midstream/lakehouse/parquet.py +103 -0
- midstream/lakehouse/pointer.py +228 -0
- midstream/lakehouse/rest.py +137 -0
- midstream/lakehouse/rows.py +82 -0
- midstream/lakehouse/schema/__init__.py +1 -0
- midstream/lakehouse/schema/conform.py +225 -0
- midstream/lakehouse/schema/forkmap.py +111 -0
- midstream/lakehouse/schema/index.py +304 -0
- midstream/lakehouse/schema/infer.py +124 -0
- midstream/lakehouse/schema/policy.py +867 -0
- midstream/lakehouse/schema/render.py +166 -0
- midstream/lakehouse/tables.py +280 -0
- midstream/maintenance_app/__init__.py +1 -0
- midstream/maintenance_app/compact_worker.py +234 -0
- midstream/maintenance_app/config.py +54 -0
- midstream/maintenance_app/handlers.py +42 -0
- midstream/maintenance_app/worker.py +981 -0
- midstream/models/__init__.py +163 -0
- midstream/models/autoscale.py +85 -0
- midstream/models/binding.py +98 -0
- midstream/models/envelopes.py +44 -0
- midstream/models/events.py +624 -0
- midstream/models/identity.py +893 -0
- midstream/models/live_tail.py +162 -0
- midstream/models/maintenance.py +139 -0
- midstream/models/materialization.py +429 -0
- midstream/models/offsets.py +43 -0
- midstream/models/settings.py +374 -0
- midstream/models/status.py +641 -0
- midstream/models/table.py +299 -0
- midstream/observation/__init__.py +41 -0
- midstream/observation/mapping.py +332 -0
- midstream/observation/policy.py +73 -0
- midstream/observation/progress.py +328 -0
- midstream/observation/reader.py +46 -0
- midstream/observation/restarts.py +101 -0
- midstream/protocol_logging.py +66 -0
- midstream/runtime/__init__.py +51 -0
- midstream/runtime/bindings.py +33 -0
- midstream/runtime/naming.py +86 -0
- midstream/runtime/templates.py +434 -0
- midstream/runtime_app/__init__.py +1 -0
- midstream/runtime_app/acks.py +329 -0
- midstream/runtime_app/committer.py +885 -0
- midstream/runtime_app/config.py +207 -0
- midstream/runtime_app/flow.py +100 -0
- midstream/runtime_app/handlers/__init__.py +11 -0
- midstream/runtime_app/handlers/runtime_settings.py +124 -0
- midstream/runtime_app/handlers/status.py +207 -0
- midstream/runtime_app/handlers/writer.py +366 -0
- midstream/runtime_app/live_settings.py +222 -0
- midstream/runtime_app/models/__init__.py +23 -0
- midstream/runtime_app/models/processing_state.py +402 -0
- midstream/runtime_app/outbox.py +74 -0
- midstream/runtime_app/performance.py +180 -0
- midstream/runtime_app/pipeline.py +995 -0
- midstream/runtime_app/schema/__init__.py +1 -0
- midstream/runtime_app/schema/drift.py +146 -0
- midstream/runtime_app/state.py +478 -0
- midstream/runtime_app/table_state.py +366 -0
- midstream/runtime_app/templates/commit-timer-task.yaml.j2 +40 -0
- midstream/runtime_app/templates/committer-agent.yaml.j2 +68 -0
- midstream/runtime_app/templates/kaspr-app.yaml.j2 +11 -0
- midstream/runtime_app/templates/pipeline-task.yaml.j2 +45 -0
- midstream/runtime_app/templates/settings-agent.yaml.j2 +39 -0
- midstream/runtime_app/templates/state-table.yaml.j2 +15 -0
- midstream/runtime_app/templates/status-task.yaml.j2 +57 -0
- midstream/runtime_app/templates/writer-agent.yaml.j2 +58 -0
- midstream/runtime_app/writer/__init__.py +19 -0
- midstream/runtime_app/writer/batches.py +230 -0
- midstream/runtime_app/writer/context.py +145 -0
- midstream/runtime_app/writer/seal.py +261 -0
- midstream/runtime_app/writer/seal_worker.py +45 -0
- midstream/runtime_app/writer/upload.py +58 -0
- midstream/scale.py +180 -0
- midstream/scheduler/__init__.py +81 -0
- midstream/schema/__init__.py +8 -0
- midstream/schema/discovery.py +423 -0
- midstream/services/__init__.py +51 -0
- midstream/services/autoscaling.py +294 -0
- midstream/services/consumer_lag.py +349 -0
- midstream/services/identity.py +1998 -0
- midstream/services/live_tail.py +139 -0
- midstream/services/observation.py +550 -0
- midstream/services/request_intake.py +608 -0
- midstream/services/retention.py +216 -0
- midstream/services/schema_projection.py +43 -0
- midstream/services/settings.py +456 -0
- midstream/services/sweep_cursor.py +39 -0
- midstream/services/table_claims.py +153 -0
- midstream/services/table_events.py +384 -0
- midstream/services/table_maintenance.py +155 -0
- midstream/services/topic_analysis.py +121 -0
- midstream/services/topic_catalog.py +174 -0
- midstream/services/topic_changes.py +210 -0
- midstream/services/workflow.py +3231 -0
- midstream/settings/__init__.py +49 -0
- midstream/settings/catalog.py +1332 -0
- midstream/settings/effective.py +221 -0
- midstream/storage/__init__.py +139 -0
- midstream/storage/autoscale.py +38 -0
- midstream/storage/bindings.py +47 -0
- midstream/storage/envelopes.py +72 -0
- midstream/storage/identity.py +749 -0
- midstream/storage/keys.py +79 -0
- midstream/storage/materializations.py +230 -0
- midstream/storage/objects.py +550 -0
- midstream/storage/operations.py +72 -0
- midstream/storage/resolve.py +42 -0
- midstream/storage/schemas.py +180 -0
- midstream/storage/settings.py +119 -0
- midstream/storage/status.py +49 -0
- midstream/storage/table.py +20 -0
- midstream/storage/tables.py +66 -0
- midstream/table_names.py +251 -0
- midstream/tail_app/__init__.py +1 -0
- midstream/tail_app/auth.py +149 -0
- midstream/tail_app/buffer.py +445 -0
- midstream/tail_app/config.py +191 -0
- midstream/tail_app/consumer.py +238 -0
- midstream/tail_app/control_plane.py +90 -0
- midstream/tail_app/events.py +62 -0
- midstream/tail_app/handlers.py +97 -0
- midstream/tail_app/http.py +71 -0
- midstream/tail_app/ingest.py +127 -0
- midstream/tail_app/peek.py +218 -0
- midstream/tail_app/routing.py +213 -0
- midstream/tail_app/serve.py +233 -0
- midstream/tail_app/table_tail.py +625 -0
- midstream/tail_app/views.py +396 -0
- midstream/tail_app/worker.py +808 -0
- midstream/utils/__init__.py +1 -0
- midstream/utils/background_loop.py +127 -0
- midstream/version.py +33 -0
- midstream/web/__init__.py +52 -0
- midstream/web/auth.py +554 -0
- midstream/web/catalog.py +299 -0
- midstream/web/datasets.py +455 -0
- midstream/web/errors.py +136 -0
- midstream/web/http.py +29 -0
- midstream/web/identity.py +870 -0
- midstream/web/materializations.py +1287 -0
- midstream/web/responses.py +327 -0
- midstream/web/settings.py +424 -0
- midstream/web/system.py +75 -0
- midstream/web/tokens.py +251 -0
- midstream/web/topics.py +122 -0
- midstream_platform-1.0.0rc1.dist-info/METADATA +38 -0
- midstream_platform-1.0.0rc1.dist-info/RECORD +187 -0
- midstream_platform-1.0.0rc1.dist-info/WHEEL +5 -0
- midstream_platform-1.0.0rc1.dist-info/top_level.txt +1 -0
midstream/__init__.py
ADDED
midstream/config.py
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
"""Control-plane configuration and shared defaults."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
ALLOWED_LIST_LIMITS = ("10", "30", "100", "all")
|
|
15
|
+
DEFAULT_LIST_LIMIT = "30"
|
|
16
|
+
# Where a pod finds its own namespace; the deployment can name another for the runtimes.
|
|
17
|
+
IN_CLUSTER_NAMESPACE_FILE = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
|
|
18
|
+
DEFAULT_RUNTIME_NAME_PREFIX = "kaspr"
|
|
19
|
+
DEFAULT_RUNTIME_TEMPLATE_VERSION = "v1"
|
|
20
|
+
DEFAULT_RUNTIME_APP_SPEC: dict[str, Any] = {}
|
|
21
|
+
CONTROL_PLANE_TABLE_NAME = "state"
|
|
22
|
+
MATERIALIZATION_REQUESTS_TOPIC = "materialization-requests"
|
|
23
|
+
MATERIALIZATION_WORK_TOPIC = "materialization-work"
|
|
24
|
+
MATERIALIZATION_STATUS_TOPIC = "materialization-status"
|
|
25
|
+
# Carries runtime settings (and maintenance requests) to every runtime. The name predates 1.0, when
|
|
26
|
+
# it also carried schema decisions; it is kept so no overlay has to rename a topic.
|
|
27
|
+
SCHEMA_AUTHORITY_UPDATES_TOPIC = "materialization-schema-authority-updates"
|
|
28
|
+
TABLE_EVENTS_TOPIC = "materialization-table-events"
|
|
29
|
+
SETTINGS_REQUESTS_TOPIC = "materializer-settings-requests"
|
|
30
|
+
IDENTITY_REQUESTS_TOPIC = "materializer-identity-requests"
|
|
31
|
+
PROTOCOL_LOGGING_ENABLED = False
|
|
32
|
+
PROTOCOL_LOG_SLOW_WAIT_SECONDS = 30
|
|
33
|
+
SCHEMA_HISTORY_LIMIT = 50
|
|
34
|
+
# Iceberg: the prefix in the destination every materialization's tables are stored under (it keeps
|
|
35
|
+
# deployments sharing a bucket apart; datasets, not this, are the tables' namespaces), and the
|
|
36
|
+
# tables' partitioning.
|
|
37
|
+
STORAGE_PREFIX = "materializer"
|
|
38
|
+
DEFAULT_PARTITION_GRANULARITY = "day"
|
|
39
|
+
PARTITION_GRANULARITIES = ("hour", "day", "month", "none")
|
|
40
|
+
# Discovery samples up to this many records, from both ends of every partition, for schema 0.
|
|
41
|
+
DISCOVERY_SAMPLE_RECORDS = 200
|
|
42
|
+
DISCOVERY_SAMPLE_SECONDS = 5
|
|
43
|
+
# The Iceberg REST catalog facade the control plane serves at /catalog/v1.
|
|
44
|
+
CATALOG_API_ENABLED = True
|
|
45
|
+
CATALOG_METADATA_CACHE_SECONDS = 5
|
|
46
|
+
# The public base URL of that facade, for the snippets the console shows; unset hides them.
|
|
47
|
+
CATALOG_BASE_URL: str | None = None
|
|
48
|
+
DELETED_RETENTION_HOURS = 168
|
|
49
|
+
# Deleting a materialization removes everything it owns, including its control-plane records.
|
|
50
|
+
# Disable to keep a tombstone until the retention sweep collects it instead.
|
|
51
|
+
PURGE_RECORDS_ON_DELETE = True
|
|
52
|
+
DELETED_PURGE_BATCH_LIMIT = 50
|
|
53
|
+
# The persistent volume of every runtime member: it holds only the runtime's state table. The control
|
|
54
|
+
# plane's own volume is its KasprApp's, set in its manifest.
|
|
55
|
+
DEFAULT_RUNTIME_STORAGE_SIZE = "64Mi"
|
|
56
|
+
DEFAULT_RUNTIME_REPLICAS = 1
|
|
57
|
+
MAX_RUNTIME_REPLICAS = 64
|
|
58
|
+
# Runtime autoscaling: a runtime far behind its topics gets more members, up to
|
|
59
|
+
# RUNTIME_AUTOSCALE_MAX_REPLICAS, so it catches up within the target; it goes back to its own replica
|
|
60
|
+
# count once it has. Each materialization can turn it off for itself (services/autoscaling.py).
|
|
61
|
+
RUNTIME_AUTOSCALING_ENABLED = True
|
|
62
|
+
RUNTIME_AUTOSCALE_MAX_REPLICAS = 16
|
|
63
|
+
RUNTIME_AUTOSCALE_TARGET_MINUTES = 30
|
|
64
|
+
RUNTIME_AUTOSCALE_MIN_LAG_RECORDS = 100_000
|
|
65
|
+
RUNTIME_AUTOSCALE_COOLDOWN_SECONDS = 300
|
|
66
|
+
# Most source topics one materialization may read. Each is a table its one committer commits to and
|
|
67
|
+
# maintains, one at a time, so the bound keeps a commit cycle and the maintenance rotation short.
|
|
68
|
+
MAX_MATERIALIZATION_TOPICS = 20
|
|
69
|
+
# Where every new materialization's tables are created. Unset, no materialization can be created.
|
|
70
|
+
DEFAULT_DESTINATION_URI: str | None = None
|
|
71
|
+
DESTINATION_CLEANUP_ENABLED = True
|
|
72
|
+
CONSUMER_LAG_REFRESH_BATCH_LIMIT = 100
|
|
73
|
+
TOPIC_ANALYSIS_MAX_TOPICS = 20
|
|
74
|
+
# The topic catalog answers from memory for this long before asking Kafka again; a forced refresh
|
|
75
|
+
# still waits this many seconds after the previous load, however many clients ask for one.
|
|
76
|
+
TOPIC_CATALOG_TTL_SECONDS = 15
|
|
77
|
+
TOPIC_CATALOG_MIN_REFRESH_SECONDS = 5
|
|
78
|
+
# Sign-in: a session ends after this long without a request, and after the maximum however
|
|
79
|
+
# active it is. Clients renew an active session at the refresh interval, which is also how often
|
|
80
|
+
# a person's last activity is recorded.
|
|
81
|
+
SESSION_IDLE_TIMEOUT_MINUTES = 8 * 60
|
|
82
|
+
SESSION_MAX_LIFETIME_HOURS = 7 * 24
|
|
83
|
+
SESSION_REFRESH_MINUTES = 10
|
|
84
|
+
PASSWORD_MIN_LENGTH = 12
|
|
85
|
+
# Failed sign-ins for one email, in a row, that lock it out for the lockout duration.
|
|
86
|
+
SIGN_IN_MAX_FAILED_ATTEMPTS = 10
|
|
87
|
+
SIGN_IN_LOCKOUT_MINUTES = 15
|
|
88
|
+
AUDIT_LOG_RETENTION_DAYS = 90
|
|
89
|
+
# The live tail (handbook 19): the newest records of a materialization's tables, served from Kafka
|
|
90
|
+
# by the tail app before they are committed. Every materialization has one; `LIVE_TAIL_ENABLED` is
|
|
91
|
+
# the deployment's switch, for all of them. The rest travel with the tail app's
|
|
92
|
+
# ticks: what one table's tail may hold, how long committed records stay in it, how long a read of
|
|
93
|
+
# Kafka waits, how fast one table is read, the most records one peek at a topic's head reads, and how
|
|
94
|
+
# long a table stays tailed after the last query asked for it.
|
|
95
|
+
LIVE_TAIL_ENABLED = True
|
|
96
|
+
LIVE_TAIL_MAX_ROWS_PER_TABLE = 250_000
|
|
97
|
+
LIVE_TAIL_MAX_BYTES_PER_TABLE = 256 * 1024 * 1024
|
|
98
|
+
LIVE_TAIL_RETENTION_SECONDS = 120
|
|
99
|
+
LIVE_TAIL_FETCH_WAIT_MS = 200
|
|
100
|
+
LIVE_TAIL_MAX_RECORDS_PER_SECOND_PER_TABLE = 5_000
|
|
101
|
+
LIVE_TAIL_PEEK_MAX_ROWS = 1_000
|
|
102
|
+
LIVE_TAIL_IDLE_SECONDS = 600
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
#: A destination a table can live under: a GCS bucket, and optionally a prefix in it.
|
|
106
|
+
GCS_DESTINATION_PATTERN = re.compile(r"^gs://[a-z0-9][a-z0-9._-]*[a-z0-9](/\S*)?$")
|
|
107
|
+
GCS_DESTINATION_MAX_LENGTH = 1024
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def is_gcs_destination_uri(value: str | None) -> bool:
|
|
111
|
+
"""Whether `value` (normalized) is a `gs://bucket[/prefix]` a table can be created under."""
|
|
112
|
+
return value is not None and len(value) <= GCS_DESTINATION_MAX_LENGTH and GCS_DESTINATION_PATTERN.match(value) is not None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def normalize_destination_uri(value: str | None) -> str | None:
|
|
116
|
+
"""Trim a destination prefix, dropping trailing slashes so object paths join cleanly.
|
|
117
|
+
|
|
118
|
+
Blank is no destination at all, which is how an unset default reads from the environment.
|
|
119
|
+
"""
|
|
120
|
+
if value is None:
|
|
121
|
+
return None
|
|
122
|
+
trimmed = value.strip()
|
|
123
|
+
if not trimmed:
|
|
124
|
+
return None
|
|
125
|
+
return trimmed.rstrip("/") or trimmed
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def normalize_list_limit(limit: str | None) -> str:
|
|
129
|
+
"""Validate and normalize the list page-size value used by web views."""
|
|
130
|
+
if limit in (None, ""):
|
|
131
|
+
return DEFAULT_LIST_LIMIT
|
|
132
|
+
if limit not in ALLOWED_LIST_LIMITS:
|
|
133
|
+
allowed = ", ".join(ALLOWED_LIST_LIMITS)
|
|
134
|
+
raise ValueError(f"limit must be one of: {allowed}")
|
|
135
|
+
return limit
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass(frozen=True, slots=True)
|
|
139
|
+
class ControlPlaneConfig:
|
|
140
|
+
"""Immutable runtime configuration loaded from environment variables."""
|
|
141
|
+
|
|
142
|
+
#: The namespace every runtime is created in: the deployment's, or the pod's own
|
|
143
|
+
#: (`_resolve_default_namespace`). Nothing in the code names one.
|
|
144
|
+
default_namespace: str = ""
|
|
145
|
+
default_runtime_name_prefix: str = DEFAULT_RUNTIME_NAME_PREFIX
|
|
146
|
+
default_runtime_template_version: str = DEFAULT_RUNTIME_TEMPLATE_VERSION
|
|
147
|
+
runtime_app_spec: dict[str, Any] = field(default_factory=dict)
|
|
148
|
+
default_list_limit: str = DEFAULT_LIST_LIMIT
|
|
149
|
+
control_plane_table_name: str = CONTROL_PLANE_TABLE_NAME
|
|
150
|
+
materialization_requests_topic: str = MATERIALIZATION_REQUESTS_TOPIC
|
|
151
|
+
materialization_work_topic: str = MATERIALIZATION_WORK_TOPIC
|
|
152
|
+
materialization_status_topic: str = MATERIALIZATION_STATUS_TOPIC
|
|
153
|
+
schema_authority_updates_topic: str = SCHEMA_AUTHORITY_UPDATES_TOPIC
|
|
154
|
+
table_events_topic: str = TABLE_EVENTS_TOPIC
|
|
155
|
+
settings_requests_topic: str = SETTINGS_REQUESTS_TOPIC
|
|
156
|
+
protocol_logging_enabled: bool = PROTOCOL_LOGGING_ENABLED
|
|
157
|
+
protocol_log_slow_wait_seconds: int = PROTOCOL_LOG_SLOW_WAIT_SECONDS
|
|
158
|
+
schema_history_limit: int = SCHEMA_HISTORY_LIMIT
|
|
159
|
+
storage_prefix: str = STORAGE_PREFIX
|
|
160
|
+
default_partition_granularity: str = DEFAULT_PARTITION_GRANULARITY
|
|
161
|
+
discovery_sample_records: int = DISCOVERY_SAMPLE_RECORDS
|
|
162
|
+
discovery_sample_seconds: int = DISCOVERY_SAMPLE_SECONDS
|
|
163
|
+
catalog_api_enabled: bool = CATALOG_API_ENABLED
|
|
164
|
+
catalog_metadata_cache_seconds: int = CATALOG_METADATA_CACHE_SECONDS
|
|
165
|
+
catalog_base_url: str | None = CATALOG_BASE_URL
|
|
166
|
+
purge_records_on_delete: bool = PURGE_RECORDS_ON_DELETE
|
|
167
|
+
deleted_retention_hours: int = DELETED_RETENTION_HOURS
|
|
168
|
+
deleted_purge_batch_limit: int = DELETED_PURGE_BATCH_LIMIT
|
|
169
|
+
default_runtime_storage_size: str = DEFAULT_RUNTIME_STORAGE_SIZE
|
|
170
|
+
default_runtime_replicas: int = DEFAULT_RUNTIME_REPLICAS
|
|
171
|
+
max_runtime_replicas: int = MAX_RUNTIME_REPLICAS
|
|
172
|
+
runtime_autoscaling_enabled: bool = RUNTIME_AUTOSCALING_ENABLED
|
|
173
|
+
runtime_autoscale_max_replicas: int = RUNTIME_AUTOSCALE_MAX_REPLICAS
|
|
174
|
+
runtime_autoscale_target_minutes: int = RUNTIME_AUTOSCALE_TARGET_MINUTES
|
|
175
|
+
runtime_autoscale_min_lag_records: int = RUNTIME_AUTOSCALE_MIN_LAG_RECORDS
|
|
176
|
+
runtime_autoscale_cooldown_seconds: int = RUNTIME_AUTOSCALE_COOLDOWN_SECONDS
|
|
177
|
+
max_materialization_topics: int = MAX_MATERIALIZATION_TOPICS
|
|
178
|
+
default_destination_uri: str | None = DEFAULT_DESTINATION_URI
|
|
179
|
+
destination_cleanup_enabled: bool = DESTINATION_CLEANUP_ENABLED
|
|
180
|
+
consumer_lag_refresh_batch_limit: int = CONSUMER_LAG_REFRESH_BATCH_LIMIT
|
|
181
|
+
topic_analysis_max_topics: int = TOPIC_ANALYSIS_MAX_TOPICS
|
|
182
|
+
topic_catalog_ttl_seconds: int = TOPIC_CATALOG_TTL_SECONDS
|
|
183
|
+
topic_catalog_min_refresh_seconds: int = TOPIC_CATALOG_MIN_REFRESH_SECONDS
|
|
184
|
+
#: This control plane's own Kaspr app name; its internal topics are prefixed with it.
|
|
185
|
+
kaspr_app_name: str | None = None
|
|
186
|
+
identity_requests_topic: str = IDENTITY_REQUESTS_TOPIC
|
|
187
|
+
session_idle_timeout_minutes: int = SESSION_IDLE_TIMEOUT_MINUTES
|
|
188
|
+
session_max_lifetime_hours: int = SESSION_MAX_LIFETIME_HOURS
|
|
189
|
+
session_refresh_minutes: int = SESSION_REFRESH_MINUTES
|
|
190
|
+
password_min_length: int = PASSWORD_MIN_LENGTH
|
|
191
|
+
sign_in_max_failed_attempts: int = SIGN_IN_MAX_FAILED_ATTEMPTS
|
|
192
|
+
sign_in_lockout_minutes: int = SIGN_IN_LOCKOUT_MINUTES
|
|
193
|
+
audit_log_retention_days: int = AUDIT_LOG_RETENTION_DAYS
|
|
194
|
+
live_tail_enabled: bool = LIVE_TAIL_ENABLED
|
|
195
|
+
live_tail_max_rows_per_table: int = LIVE_TAIL_MAX_ROWS_PER_TABLE
|
|
196
|
+
live_tail_max_bytes_per_table: int = LIVE_TAIL_MAX_BYTES_PER_TABLE
|
|
197
|
+
live_tail_retention_seconds: int = LIVE_TAIL_RETENTION_SECONDS
|
|
198
|
+
live_tail_fetch_wait_ms: int = LIVE_TAIL_FETCH_WAIT_MS
|
|
199
|
+
live_tail_max_records_per_second_per_table: int = LIVE_TAIL_MAX_RECORDS_PER_SECOND_PER_TABLE
|
|
200
|
+
live_tail_peek_max_rows: int = LIVE_TAIL_PEEK_MAX_ROWS
|
|
201
|
+
live_tail_idle_seconds: int = LIVE_TAIL_IDLE_SECONDS
|
|
202
|
+
|
|
203
|
+
def __post_init__(self) -> None:
|
|
204
|
+
if self.default_partition_granularity not in PARTITION_GRANULARITIES:
|
|
205
|
+
raise ValueError(
|
|
206
|
+
"default_partition_granularity must be one of: " + ", ".join(PARTITION_GRANULARITIES)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def runtime_name_for_materialization(self, materialization_id: str) -> str:
|
|
210
|
+
"""The name of a materialization's dedicated runtime: always generated, never chosen."""
|
|
211
|
+
return f"{self.default_runtime_name_prefix}-{materialization_id}"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _resolve_default_namespace(*, in_cluster_namespace_file: str | None = None) -> str:
|
|
215
|
+
"""The namespace the control plane creates runtimes in.
|
|
216
|
+
|
|
217
|
+
The deployment names it with `CONTROL_PLANE_DEFAULT_NAMESPACE`; without that, a pod
|
|
218
|
+
reads its own namespace from the service-account mount Kubernetes gives every pod. With neither,
|
|
219
|
+
the configuration cannot be loaded: no namespace is ever assumed.
|
|
220
|
+
"""
|
|
221
|
+
configured = (os.getenv("CONTROL_PLANE_DEFAULT_NAMESPACE") or "").strip()
|
|
222
|
+
if configured:
|
|
223
|
+
return configured
|
|
224
|
+
path = IN_CLUSTER_NAMESPACE_FILE if in_cluster_namespace_file is None else in_cluster_namespace_file
|
|
225
|
+
try:
|
|
226
|
+
with open(path, encoding="utf-8") as handle:
|
|
227
|
+
own = handle.read().strip()
|
|
228
|
+
except OSError:
|
|
229
|
+
own = ""
|
|
230
|
+
if own:
|
|
231
|
+
return own
|
|
232
|
+
raise ValueError(
|
|
233
|
+
"CONTROL_PLANE_DEFAULT_NAMESPACE is not set and no in-cluster namespace file was "
|
|
234
|
+
"found: the control plane needs the namespace its runtimes are created in"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@lru_cache(maxsize=1)
|
|
239
|
+
def get_config() -> ControlPlaneConfig:
|
|
240
|
+
"""Load and cache the control-plane configuration from the environment."""
|
|
241
|
+
return ControlPlaneConfig(
|
|
242
|
+
control_plane_table_name=os.getenv(
|
|
243
|
+
"CONTROL_PLANE_TABLE_NAME",
|
|
244
|
+
CONTROL_PLANE_TABLE_NAME,
|
|
245
|
+
),
|
|
246
|
+
default_namespace=_resolve_default_namespace(),
|
|
247
|
+
default_runtime_name_prefix=os.getenv(
|
|
248
|
+
"CONTROL_PLANE_RUNTIME_NAME_PREFIX",
|
|
249
|
+
DEFAULT_RUNTIME_NAME_PREFIX,
|
|
250
|
+
),
|
|
251
|
+
default_runtime_template_version=os.getenv(
|
|
252
|
+
"CONTROL_PLANE_RUNTIME_TEMPLATE_VERSION",
|
|
253
|
+
DEFAULT_RUNTIME_TEMPLATE_VERSION,
|
|
254
|
+
),
|
|
255
|
+
runtime_app_spec=_load_runtime_app_spec(
|
|
256
|
+
os.getenv("CONTROL_PLANE_RUNTIME_APP_SPEC")
|
|
257
|
+
),
|
|
258
|
+
default_list_limit=normalize_list_limit(
|
|
259
|
+
os.getenv(
|
|
260
|
+
"CONTROL_PLANE_DEFAULT_LIST_LIMIT",
|
|
261
|
+
DEFAULT_LIST_LIMIT,
|
|
262
|
+
)
|
|
263
|
+
),
|
|
264
|
+
materialization_requests_topic=os.getenv(
|
|
265
|
+
"CONTROL_PLANE_MATERIALIZATION_REQUESTS_TOPIC",
|
|
266
|
+
MATERIALIZATION_REQUESTS_TOPIC,
|
|
267
|
+
),
|
|
268
|
+
materialization_work_topic=os.getenv(
|
|
269
|
+
"CONTROL_PLANE_MATERIALIZATION_WORK_TOPIC",
|
|
270
|
+
MATERIALIZATION_WORK_TOPIC,
|
|
271
|
+
),
|
|
272
|
+
materialization_status_topic=os.getenv(
|
|
273
|
+
"CONTROL_PLANE_MATERIALIZATION_STATUS_TOPIC",
|
|
274
|
+
MATERIALIZATION_STATUS_TOPIC,
|
|
275
|
+
),
|
|
276
|
+
schema_authority_updates_topic=os.getenv(
|
|
277
|
+
"CONTROL_PLANE_SCHEMA_AUTHORITY_UPDATES_TOPIC",
|
|
278
|
+
SCHEMA_AUTHORITY_UPDATES_TOPIC,
|
|
279
|
+
),
|
|
280
|
+
table_events_topic=os.getenv(
|
|
281
|
+
"CONTROL_PLANE_TABLE_EVENTS_TOPIC",
|
|
282
|
+
TABLE_EVENTS_TOPIC,
|
|
283
|
+
),
|
|
284
|
+
settings_requests_topic=os.getenv(
|
|
285
|
+
"CONTROL_PLANE_SETTINGS_REQUESTS_TOPIC",
|
|
286
|
+
SETTINGS_REQUESTS_TOPIC,
|
|
287
|
+
),
|
|
288
|
+
protocol_logging_enabled=_parse_bool_env(
|
|
289
|
+
os.getenv("PROTOCOL_LOGGING_ENABLED"),
|
|
290
|
+
default=PROTOCOL_LOGGING_ENABLED,
|
|
291
|
+
),
|
|
292
|
+
protocol_log_slow_wait_seconds=_parse_int_env(
|
|
293
|
+
os.getenv("PROTOCOL_LOG_SLOW_WAIT_SECONDS"),
|
|
294
|
+
default=PROTOCOL_LOG_SLOW_WAIT_SECONDS,
|
|
295
|
+
minimum=1,
|
|
296
|
+
name="PROTOCOL_LOG_SLOW_WAIT_SECONDS",
|
|
297
|
+
),
|
|
298
|
+
schema_history_limit=_parse_int_env(
|
|
299
|
+
os.getenv("CONTROL_PLANE_SCHEMA_HISTORY_LIMIT"),
|
|
300
|
+
default=SCHEMA_HISTORY_LIMIT,
|
|
301
|
+
minimum=1,
|
|
302
|
+
name="CONTROL_PLANE_SCHEMA_HISTORY_LIMIT",
|
|
303
|
+
),
|
|
304
|
+
storage_prefix=os.getenv("CONTROL_PLANE_STORAGE_PREFIX", STORAGE_PREFIX).strip().strip("/")
|
|
305
|
+
or STORAGE_PREFIX,
|
|
306
|
+
default_partition_granularity=os.getenv(
|
|
307
|
+
"CONTROL_PLANE_DEFAULT_PARTITION_GRANULARITY",
|
|
308
|
+
DEFAULT_PARTITION_GRANULARITY,
|
|
309
|
+
).strip(),
|
|
310
|
+
discovery_sample_records=_parse_int_env(
|
|
311
|
+
os.getenv("CONTROL_PLANE_DISCOVERY_SAMPLE_RECORDS"),
|
|
312
|
+
default=DISCOVERY_SAMPLE_RECORDS,
|
|
313
|
+
minimum=1,
|
|
314
|
+
name="CONTROL_PLANE_DISCOVERY_SAMPLE_RECORDS",
|
|
315
|
+
),
|
|
316
|
+
discovery_sample_seconds=_parse_int_env(
|
|
317
|
+
os.getenv("CONTROL_PLANE_DISCOVERY_SAMPLE_SECONDS"),
|
|
318
|
+
default=DISCOVERY_SAMPLE_SECONDS,
|
|
319
|
+
minimum=1,
|
|
320
|
+
name="CONTROL_PLANE_DISCOVERY_SAMPLE_SECONDS",
|
|
321
|
+
),
|
|
322
|
+
catalog_api_enabled=_parse_bool_env(
|
|
323
|
+
os.getenv("CONTROL_PLANE_CATALOG_API_ENABLED"),
|
|
324
|
+
default=CATALOG_API_ENABLED,
|
|
325
|
+
),
|
|
326
|
+
catalog_metadata_cache_seconds=_parse_int_env(
|
|
327
|
+
os.getenv("CONTROL_PLANE_CATALOG_METADATA_CACHE_SECONDS"),
|
|
328
|
+
default=CATALOG_METADATA_CACHE_SECONDS,
|
|
329
|
+
minimum=1,
|
|
330
|
+
name="CONTROL_PLANE_CATALOG_METADATA_CACHE_SECONDS",
|
|
331
|
+
),
|
|
332
|
+
catalog_base_url=(os.getenv("CONTROL_PLANE_CATALOG_BASE_URL") or "").strip().rstrip("/") or None,
|
|
333
|
+
purge_records_on_delete=_parse_bool_env(
|
|
334
|
+
os.getenv("CONTROL_PLANE_PURGE_RECORDS_ON_DELETE"),
|
|
335
|
+
default=PURGE_RECORDS_ON_DELETE,
|
|
336
|
+
),
|
|
337
|
+
deleted_retention_hours=_parse_int_env(
|
|
338
|
+
os.getenv("CONTROL_PLANE_DELETED_RETENTION_HOURS"),
|
|
339
|
+
default=DELETED_RETENTION_HOURS,
|
|
340
|
+
minimum=1,
|
|
341
|
+
name="CONTROL_PLANE_DELETED_RETENTION_HOURS",
|
|
342
|
+
),
|
|
343
|
+
deleted_purge_batch_limit=_parse_int_env(
|
|
344
|
+
os.getenv("CONTROL_PLANE_DELETED_PURGE_BATCH_LIMIT"),
|
|
345
|
+
default=DELETED_PURGE_BATCH_LIMIT,
|
|
346
|
+
minimum=1,
|
|
347
|
+
name="CONTROL_PLANE_DELETED_PURGE_BATCH_LIMIT",
|
|
348
|
+
),
|
|
349
|
+
default_runtime_storage_size=os.getenv(
|
|
350
|
+
"CONTROL_PLANE_DEFAULT_RUNTIME_STORAGE_SIZE",
|
|
351
|
+
DEFAULT_RUNTIME_STORAGE_SIZE,
|
|
352
|
+
),
|
|
353
|
+
default_runtime_replicas=_parse_int_env(
|
|
354
|
+
os.getenv("CONTROL_PLANE_DEFAULT_RUNTIME_REPLICAS"),
|
|
355
|
+
default=DEFAULT_RUNTIME_REPLICAS,
|
|
356
|
+
minimum=1,
|
|
357
|
+
name="CONTROL_PLANE_DEFAULT_RUNTIME_REPLICAS",
|
|
358
|
+
),
|
|
359
|
+
max_runtime_replicas=_parse_int_env(
|
|
360
|
+
os.getenv("CONTROL_PLANE_MAX_RUNTIME_REPLICAS"),
|
|
361
|
+
default=MAX_RUNTIME_REPLICAS,
|
|
362
|
+
minimum=1,
|
|
363
|
+
name="CONTROL_PLANE_MAX_RUNTIME_REPLICAS",
|
|
364
|
+
),
|
|
365
|
+
runtime_autoscaling_enabled=_parse_bool_env(
|
|
366
|
+
os.getenv("CONTROL_PLANE_RUNTIME_AUTOSCALING_ENABLED"),
|
|
367
|
+
default=RUNTIME_AUTOSCALING_ENABLED,
|
|
368
|
+
),
|
|
369
|
+
runtime_autoscale_max_replicas=_parse_int_env(
|
|
370
|
+
os.getenv("CONTROL_PLANE_RUNTIME_AUTOSCALE_MAX_REPLICAS"),
|
|
371
|
+
default=RUNTIME_AUTOSCALE_MAX_REPLICAS,
|
|
372
|
+
minimum=1,
|
|
373
|
+
name="CONTROL_PLANE_RUNTIME_AUTOSCALE_MAX_REPLICAS",
|
|
374
|
+
),
|
|
375
|
+
runtime_autoscale_target_minutes=_parse_int_env(
|
|
376
|
+
os.getenv("CONTROL_PLANE_RUNTIME_AUTOSCALE_TARGET_MINUTES"),
|
|
377
|
+
default=RUNTIME_AUTOSCALE_TARGET_MINUTES,
|
|
378
|
+
minimum=1,
|
|
379
|
+
name="CONTROL_PLANE_RUNTIME_AUTOSCALE_TARGET_MINUTES",
|
|
380
|
+
),
|
|
381
|
+
runtime_autoscale_min_lag_records=_parse_int_env(
|
|
382
|
+
os.getenv("CONTROL_PLANE_RUNTIME_AUTOSCALE_MIN_LAG_RECORDS"),
|
|
383
|
+
default=RUNTIME_AUTOSCALE_MIN_LAG_RECORDS,
|
|
384
|
+
minimum=1,
|
|
385
|
+
name="CONTROL_PLANE_RUNTIME_AUTOSCALE_MIN_LAG_RECORDS",
|
|
386
|
+
),
|
|
387
|
+
runtime_autoscale_cooldown_seconds=_parse_int_env(
|
|
388
|
+
os.getenv("CONTROL_PLANE_RUNTIME_AUTOSCALE_COOLDOWN_SECONDS"),
|
|
389
|
+
default=RUNTIME_AUTOSCALE_COOLDOWN_SECONDS,
|
|
390
|
+
minimum=60,
|
|
391
|
+
name="CONTROL_PLANE_RUNTIME_AUTOSCALE_COOLDOWN_SECONDS",
|
|
392
|
+
),
|
|
393
|
+
max_materialization_topics=_parse_int_env(
|
|
394
|
+
os.getenv("CONTROL_PLANE_MAX_MATERIALIZATION_TOPICS"),
|
|
395
|
+
default=MAX_MATERIALIZATION_TOPICS,
|
|
396
|
+
minimum=1,
|
|
397
|
+
name="CONTROL_PLANE_MAX_MATERIALIZATION_TOPICS",
|
|
398
|
+
),
|
|
399
|
+
default_destination_uri=normalize_destination_uri(
|
|
400
|
+
os.getenv("CONTROL_PLANE_DEFAULT_DESTINATION_URI")
|
|
401
|
+
),
|
|
402
|
+
destination_cleanup_enabled=_parse_bool_env(
|
|
403
|
+
os.getenv("CONTROL_PLANE_DESTINATION_CLEANUP_ENABLED"),
|
|
404
|
+
default=DESTINATION_CLEANUP_ENABLED,
|
|
405
|
+
),
|
|
406
|
+
consumer_lag_refresh_batch_limit=_parse_int_env(
|
|
407
|
+
os.getenv("CONTROL_PLANE_CONSUMER_LAG_REFRESH_BATCH_LIMIT"),
|
|
408
|
+
default=CONSUMER_LAG_REFRESH_BATCH_LIMIT,
|
|
409
|
+
minimum=1,
|
|
410
|
+
name="CONTROL_PLANE_CONSUMER_LAG_REFRESH_BATCH_LIMIT",
|
|
411
|
+
),
|
|
412
|
+
topic_analysis_max_topics=_parse_int_env(
|
|
413
|
+
os.getenv("CONTROL_PLANE_TOPIC_ANALYSIS_MAX_TOPICS"),
|
|
414
|
+
default=TOPIC_ANALYSIS_MAX_TOPICS,
|
|
415
|
+
minimum=1,
|
|
416
|
+
name="CONTROL_PLANE_TOPIC_ANALYSIS_MAX_TOPICS",
|
|
417
|
+
),
|
|
418
|
+
topic_catalog_ttl_seconds=_parse_int_env(
|
|
419
|
+
os.getenv("CONTROL_PLANE_TOPIC_CATALOG_TTL_SECONDS"),
|
|
420
|
+
default=TOPIC_CATALOG_TTL_SECONDS,
|
|
421
|
+
minimum=1,
|
|
422
|
+
name="CONTROL_PLANE_TOPIC_CATALOG_TTL_SECONDS",
|
|
423
|
+
),
|
|
424
|
+
topic_catalog_min_refresh_seconds=_parse_int_env(
|
|
425
|
+
os.getenv("CONTROL_PLANE_TOPIC_CATALOG_MIN_REFRESH_SECONDS"),
|
|
426
|
+
default=TOPIC_CATALOG_MIN_REFRESH_SECONDS,
|
|
427
|
+
minimum=1,
|
|
428
|
+
name="CONTROL_PLANE_TOPIC_CATALOG_MIN_REFRESH_SECONDS",
|
|
429
|
+
),
|
|
430
|
+
kaspr_app_name=os.getenv("K_APP_NAME") or None,
|
|
431
|
+
identity_requests_topic=os.getenv(
|
|
432
|
+
"CONTROL_PLANE_IDENTITY_REQUESTS_TOPIC",
|
|
433
|
+
IDENTITY_REQUESTS_TOPIC,
|
|
434
|
+
),
|
|
435
|
+
session_idle_timeout_minutes=_parse_int_env(
|
|
436
|
+
os.getenv("CONTROL_PLANE_SESSION_IDLE_TIMEOUT_MINUTES"),
|
|
437
|
+
default=SESSION_IDLE_TIMEOUT_MINUTES,
|
|
438
|
+
minimum=1,
|
|
439
|
+
name="CONTROL_PLANE_SESSION_IDLE_TIMEOUT_MINUTES",
|
|
440
|
+
),
|
|
441
|
+
session_max_lifetime_hours=_parse_int_env(
|
|
442
|
+
os.getenv("CONTROL_PLANE_SESSION_MAX_LIFETIME_HOURS"),
|
|
443
|
+
default=SESSION_MAX_LIFETIME_HOURS,
|
|
444
|
+
minimum=1,
|
|
445
|
+
name="CONTROL_PLANE_SESSION_MAX_LIFETIME_HOURS",
|
|
446
|
+
),
|
|
447
|
+
session_refresh_minutes=_parse_int_env(
|
|
448
|
+
os.getenv("CONTROL_PLANE_SESSION_REFRESH_MINUTES"),
|
|
449
|
+
default=SESSION_REFRESH_MINUTES,
|
|
450
|
+
minimum=1,
|
|
451
|
+
name="CONTROL_PLANE_SESSION_REFRESH_MINUTES",
|
|
452
|
+
),
|
|
453
|
+
password_min_length=_parse_int_env(
|
|
454
|
+
os.getenv("CONTROL_PLANE_PASSWORD_MIN_LENGTH"),
|
|
455
|
+
default=PASSWORD_MIN_LENGTH,
|
|
456
|
+
minimum=8,
|
|
457
|
+
name="CONTROL_PLANE_PASSWORD_MIN_LENGTH",
|
|
458
|
+
),
|
|
459
|
+
sign_in_max_failed_attempts=_parse_int_env(
|
|
460
|
+
os.getenv("CONTROL_PLANE_SIGN_IN_MAX_FAILED_ATTEMPTS"),
|
|
461
|
+
default=SIGN_IN_MAX_FAILED_ATTEMPTS,
|
|
462
|
+
minimum=1,
|
|
463
|
+
name="CONTROL_PLANE_SIGN_IN_MAX_FAILED_ATTEMPTS",
|
|
464
|
+
),
|
|
465
|
+
sign_in_lockout_minutes=_parse_int_env(
|
|
466
|
+
os.getenv("CONTROL_PLANE_SIGN_IN_LOCKOUT_MINUTES"),
|
|
467
|
+
default=SIGN_IN_LOCKOUT_MINUTES,
|
|
468
|
+
minimum=1,
|
|
469
|
+
name="CONTROL_PLANE_SIGN_IN_LOCKOUT_MINUTES",
|
|
470
|
+
),
|
|
471
|
+
audit_log_retention_days=_parse_int_env(
|
|
472
|
+
os.getenv("CONTROL_PLANE_AUDIT_LOG_RETENTION_DAYS"),
|
|
473
|
+
default=AUDIT_LOG_RETENTION_DAYS,
|
|
474
|
+
minimum=1,
|
|
475
|
+
name="CONTROL_PLANE_AUDIT_LOG_RETENTION_DAYS",
|
|
476
|
+
),
|
|
477
|
+
live_tail_enabled=_parse_bool_env(
|
|
478
|
+
os.getenv("CONTROL_PLANE_LIVE_TAIL_ENABLED"),
|
|
479
|
+
default=LIVE_TAIL_ENABLED,
|
|
480
|
+
),
|
|
481
|
+
live_tail_max_rows_per_table=_parse_int_env(
|
|
482
|
+
os.getenv("CONTROL_PLANE_LIVE_TAIL_MAX_ROWS_PER_TABLE"),
|
|
483
|
+
default=LIVE_TAIL_MAX_ROWS_PER_TABLE,
|
|
484
|
+
minimum=1000,
|
|
485
|
+
name="CONTROL_PLANE_LIVE_TAIL_MAX_ROWS_PER_TABLE",
|
|
486
|
+
),
|
|
487
|
+
live_tail_max_bytes_per_table=_parse_int_env(
|
|
488
|
+
os.getenv("CONTROL_PLANE_LIVE_TAIL_MAX_BYTES_PER_TABLE"),
|
|
489
|
+
default=LIVE_TAIL_MAX_BYTES_PER_TABLE,
|
|
490
|
+
minimum=8 * 1024 * 1024,
|
|
491
|
+
name="CONTROL_PLANE_LIVE_TAIL_MAX_BYTES_PER_TABLE",
|
|
492
|
+
),
|
|
493
|
+
live_tail_retention_seconds=_parse_int_env(
|
|
494
|
+
os.getenv("CONTROL_PLANE_LIVE_TAIL_RETENTION_SECONDS"),
|
|
495
|
+
default=LIVE_TAIL_RETENTION_SECONDS,
|
|
496
|
+
minimum=10,
|
|
497
|
+
name="CONTROL_PLANE_LIVE_TAIL_RETENTION_SECONDS",
|
|
498
|
+
),
|
|
499
|
+
live_tail_fetch_wait_ms=_parse_int_env(
|
|
500
|
+
os.getenv("CONTROL_PLANE_LIVE_TAIL_FETCH_WAIT_MS"),
|
|
501
|
+
default=LIVE_TAIL_FETCH_WAIT_MS,
|
|
502
|
+
minimum=50,
|
|
503
|
+
name="CONTROL_PLANE_LIVE_TAIL_FETCH_WAIT_MS",
|
|
504
|
+
),
|
|
505
|
+
live_tail_max_records_per_second_per_table=_parse_int_env(
|
|
506
|
+
os.getenv("CONTROL_PLANE_LIVE_TAIL_MAX_RECORDS_PER_SECOND_PER_TABLE"),
|
|
507
|
+
default=LIVE_TAIL_MAX_RECORDS_PER_SECOND_PER_TABLE,
|
|
508
|
+
minimum=100,
|
|
509
|
+
name="CONTROL_PLANE_LIVE_TAIL_MAX_RECORDS_PER_SECOND_PER_TABLE",
|
|
510
|
+
),
|
|
511
|
+
live_tail_peek_max_rows=_parse_int_env(
|
|
512
|
+
os.getenv("CONTROL_PLANE_LIVE_TAIL_PEEK_MAX_ROWS"),
|
|
513
|
+
default=LIVE_TAIL_PEEK_MAX_ROWS,
|
|
514
|
+
minimum=10,
|
|
515
|
+
name="CONTROL_PLANE_LIVE_TAIL_PEEK_MAX_ROWS",
|
|
516
|
+
),
|
|
517
|
+
live_tail_idle_seconds=_parse_int_env(
|
|
518
|
+
os.getenv("CONTROL_PLANE_LIVE_TAIL_IDLE_SECONDS"),
|
|
519
|
+
default=LIVE_TAIL_IDLE_SECONDS,
|
|
520
|
+
minimum=0,
|
|
521
|
+
name="CONTROL_PLANE_LIVE_TAIL_IDLE_SECONDS",
|
|
522
|
+
),
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def deployment_environment() -> Mapping[str, str]:
|
|
527
|
+
"""The process environment, to report which settings the deployment sets explicitly.
|
|
528
|
+
|
|
529
|
+
`get_config()` folds the environment and the built-in defaults together; the settings page
|
|
530
|
+
also needs to say which of the two a value came from.
|
|
531
|
+
"""
|
|
532
|
+
return os.environ
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _load_runtime_app_spec(raw_value: str | None) -> dict[str, Any]:
|
|
536
|
+
if raw_value in (None, ""):
|
|
537
|
+
return dict(DEFAULT_RUNTIME_APP_SPEC)
|
|
538
|
+
parsed = yaml.safe_load(raw_value)
|
|
539
|
+
if parsed is None:
|
|
540
|
+
return {}
|
|
541
|
+
if not isinstance(parsed, dict):
|
|
542
|
+
raise ValueError(
|
|
543
|
+
"CONTROL_PLANE_RUNTIME_APP_SPEC must parse to a mapping"
|
|
544
|
+
)
|
|
545
|
+
return parsed
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _parse_bool_env(raw_value: str | None, *, default: bool) -> bool:
|
|
549
|
+
if raw_value is None:
|
|
550
|
+
return default
|
|
551
|
+
normalized = raw_value.strip().lower()
|
|
552
|
+
if normalized in {"1", "true", "yes", "on"}:
|
|
553
|
+
return True
|
|
554
|
+
if normalized in {"0", "false", "no", "off"}:
|
|
555
|
+
return False
|
|
556
|
+
raise ValueError(
|
|
557
|
+
"boolean environment values must be one of: "
|
|
558
|
+
"1, 0, true, false, yes, no, on, off"
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _parse_int_env(
|
|
563
|
+
raw_value: str | None,
|
|
564
|
+
*,
|
|
565
|
+
default: int,
|
|
566
|
+
minimum: int,
|
|
567
|
+
name: str,
|
|
568
|
+
) -> int:
|
|
569
|
+
if raw_value is None:
|
|
570
|
+
return default
|
|
571
|
+
try:
|
|
572
|
+
parsed = int(raw_value)
|
|
573
|
+
except ValueError as exc:
|
|
574
|
+
raise ValueError(f"{name} must be an integer") from exc
|
|
575
|
+
if parsed < minimum:
|
|
576
|
+
raise ValueError(f"{name} must be >= {minimum}")
|
|
577
|
+
return parsed
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Kaspr-facing adapter handlers live here."""
|
|
2
|
+
|
|
3
|
+
from midstream.handlers.identity import handle_identity_request
|
|
4
|
+
from midstream.handlers.request_intake import handle_request_event, handle_request_event_for_output
|
|
5
|
+
from midstream.handlers.resource_observation import (
|
|
6
|
+
handle_observation_work_for_output,
|
|
7
|
+
handle_observation_work_for_output_async,
|
|
8
|
+
handle_resource_observation,
|
|
9
|
+
)
|
|
10
|
+
from midstream.handlers.runtime_status import handle_runtime_status
|
|
11
|
+
from midstream.handlers.settings import handle_settings_request_for_output
|
|
12
|
+
from midstream.handlers.table_events import handle_table_event
|
|
13
|
+
from midstream.handlers.workflow_apply import (
|
|
14
|
+
handle_work_event,
|
|
15
|
+
handle_work_event_for_output,
|
|
16
|
+
handle_work_event_for_routed_output,
|
|
17
|
+
handle_work_event_for_routed_output_async,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"handle_identity_request",
|
|
22
|
+
"handle_request_event",
|
|
23
|
+
"handle_request_event_for_output",
|
|
24
|
+
"handle_observation_work_for_output",
|
|
25
|
+
"handle_observation_work_for_output_async",
|
|
26
|
+
"handle_resource_observation",
|
|
27
|
+
"handle_settings_request_for_output",
|
|
28
|
+
"handle_runtime_status",
|
|
29
|
+
"handle_table_event",
|
|
30
|
+
"handle_work_event",
|
|
31
|
+
"handle_work_event_for_output",
|
|
32
|
+
"handle_work_event_for_routed_output",
|
|
33
|
+
"handle_work_event_for_routed_output_async",
|
|
34
|
+
]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Thin Kaspr-facing handler for identity requests, and the triggers the identity tasks send."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from midstream.config import ControlPlaneConfig
|
|
8
|
+
from midstream.models.identity import IdentityRequest
|
|
9
|
+
from midstream.services.identity import apply_identity_request
|
|
10
|
+
from midstream.services.table_claims import claim_outcome_work
|
|
11
|
+
from midstream.settings import resolve_config
|
|
12
|
+
from midstream.storage.identity import SYSTEM_ACTOR
|
|
13
|
+
from midstream.storage.table import TableProtocol
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def handle_identity_request(
|
|
17
|
+
value: dict,
|
|
18
|
+
directory_table: TableProtocol,
|
|
19
|
+
audit_table: TableProtocol,
|
|
20
|
+
settings_table: TableProtocol | None,
|
|
21
|
+
control_plane_table: TableProtocol | None = None,
|
|
22
|
+
*,
|
|
23
|
+
config: ControlPlaneConfig | None = None,
|
|
24
|
+
) -> dict[str, Any] | None:
|
|
25
|
+
"""Deserialize an identity request and apply it with the effective configuration.
|
|
26
|
+
|
|
27
|
+
Returns the work event a claim of table names answers its materialization with, routed for the
|
|
28
|
+
agent's output; None for every other request. `control_plane_table` is only read, by the hourly
|
|
29
|
+
sweep of claims.
|
|
30
|
+
"""
|
|
31
|
+
cfg = resolve_config(settings_table) if config is None else config
|
|
32
|
+
request = IdentityRequest.from_dict(value)
|
|
33
|
+
outcome = apply_identity_request(
|
|
34
|
+
directory_table,
|
|
35
|
+
audit_table,
|
|
36
|
+
request,
|
|
37
|
+
config=cfg,
|
|
38
|
+
control_plane_table=control_plane_table,
|
|
39
|
+
)
|
|
40
|
+
return claim_outcome_work(request, outcome)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_identity_bootstrap_request(*, requested_at: str) -> dict:
|
|
44
|
+
"""The startup trigger: store the built-in groups and make sure an administrator exists."""
|
|
45
|
+
return _system_request("bootstrap", requested_at)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def build_identity_maintenance_request(*, requested_at: str) -> dict:
|
|
49
|
+
"""The hourly trigger: remove expired sessions, stale sign-in counts and old audit entries."""
|
|
50
|
+
return _system_request("maintenance", requested_at)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _system_request(action: str, requested_at: str) -> dict:
|
|
54
|
+
return IdentityRequest(
|
|
55
|
+
request_id=f"idr-{action}-{requested_at}",
|
|
56
|
+
action=action,
|
|
57
|
+
submitted_at=requested_at,
|
|
58
|
+
actor_id=SYSTEM_ACTOR,
|
|
59
|
+
actor_email=SYSTEM_ACTOR,
|
|
60
|
+
).to_dict()
|