odctl 1.2.2__tar.gz → 1.4.0__tar.gz
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.
- {odctl-1.2.2 → odctl-1.4.0}/.github/scripts/select-profiles.py +23 -7
- {odctl-1.2.2 → odctl-1.4.0}/.github/scripts/smoke.sh +414 -34
- {odctl-1.2.2 → odctl-1.4.0}/.github/workflows/ci.yml +1 -1
- {odctl-1.2.2 → odctl-1.4.0}/.github/workflows/e2e-test.yml +9 -5
- {odctl-1.2.2 → odctl-1.4.0}/.github/workflows/pipeline.yml +3 -3
- {odctl-1.2.2 → odctl-1.4.0}/.github/workflows/publish.yml +1 -1
- odctl-1.4.0/CLAUDE.md +20 -0
- odctl-1.4.0/CONTRIBUTING.md +99 -0
- {odctl-1.2.2 → odctl-1.4.0}/PKG-INFO +5 -26
- {odctl-1.2.2 → odctl-1.4.0}/README.md +4 -25
- {odctl-1.2.2 → odctl-1.4.0}/mkdocs.yml +4 -0
- {odctl-1.2.2 → odctl-1.4.0}/pyproject.toml +1 -1
- {odctl-1.2.2 → odctl-1.4.0}/scripts/gen_pages.py +2 -0
- {odctl-1.2.2 → odctl-1.4.0}/skills/odctl/SKILL.md +25 -14
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/compose-analytics.yml +1 -1
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/compose-infra.yml +1 -1
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/compose-kafka.yml +1 -1
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/compose-metadata.yml +79 -9
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/deps/build.gradle +1 -1
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/feast/feature_store.yaml +10 -5
- odctl-1.4.0/src/odctl/resources/openmetadata/autopilot_init.py +158 -0
- odctl-1.4.0/src/odctl/resources/openmetadata/connector_sync.py +178 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/registry.yml +12 -4
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/workspace.py +14 -0
- odctl-1.4.0/tests/test_autopilot_init.py +117 -0
- odctl-1.4.0/tests/test_connector_sync.py +183 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_docs.py +35 -3
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_feast_profile.py +11 -0
- odctl-1.4.0/tests/test_metadata_profile.py +119 -0
- odctl-1.4.0/tests/test_named_paths.py +62 -0
- odctl-1.4.0/tests/test_roadmap.py +55 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_workspace.py +4 -0
- {odctl-1.2.2 → odctl-1.4.0}/uv.lock +1 -1
- odctl-1.2.2/CLAUDE.md +0 -64
- {odctl-1.2.2 → odctl-1.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/.github/renovate.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/.github/scripts/spark_avro_producer.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/.github/workflows/build-deps.yml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/.github/workflows/build-platform-images.yml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/.gitignore +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/.pre-commit-config.yaml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/.vscode/settings.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/LICENSE +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/image/demo.gif +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/image/demo.png +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/image/diagram.drawio +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/image/diagram.png +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/scripts/diagrams.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/scripts/llmstxt_sections.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/scripts/local_links.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/scripts/release_links.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/scripts/release_notes.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/__init__.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/config.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/docker.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/logs.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/main.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/planner.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/projects.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/registry.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/airflow/odctl_log_config.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/clickhouse/config.xml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/clickhouse/init.sql +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/clickhouse/keeper_config.xml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/clickhouse/macros-11-lite.xml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/clickhouse/macros-11.xml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/clickhouse/macros-12-lite.xml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/clickhouse/macros-12.xml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/clickhouse/macros-21.xml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/clickhouse/macros-22.xml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/clickhouse/users.xml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/compose-deps.yml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/compose-flink.yml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/compose-mlops.yml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/compose-obsv.yml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/compose-orch.yml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/compose-spark.yml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/compose-store.yml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/deps/Dockerfile +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/deps/build_connectors.sh +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/deps/fetch_flink_dependencies.sh +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/deps/fetch_spark_dependencies.sh +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/deps/fetch_standalone_plugins.sh +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/deps/versions.env +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/docker/airflow/Dockerfile +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/docker/mlflow/Dockerfile +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/docker/postgres/Dockerfile +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/docker/spark/Dockerfile +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/evidently/config.yaml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/flink/flink-conf.yml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/grafana/dashboards/airflow.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/grafana/dashboards/clickhouse.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/grafana/dashboards/feast.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/grafana/dashboards/flink.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/grafana/dashboards/fluss.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/grafana/dashboards/iceberg-catalog.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/grafana/dashboards/kafka-connect.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/grafana/dashboards/kafka.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/grafana/dashboards/karapace.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/grafana/dashboards/marquez.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/grafana/dashboards/metabase.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/grafana/dashboards/mlflow.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/grafana/dashboards/openmetadata.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/grafana/dashboards/postgresql.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/grafana/dashboards/seaweedfs.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/grafana/dashboards/spark.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/grafana/dashboards/temporal.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/grafana/dashboards/trino.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/grafana/dashboards/valkey.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/grafana/odctl-dashboards.yaml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/grafana/sources.yml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/jmx/jmx-exporter.yml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/marquez/marquez.yml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/otelcol/odctl-receivers.yaml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/postgres/01-init-databases.sh +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/prometheus/prometheus.yml +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/seaweed/s3.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/spark/log4j2.properties +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/spark/metrics.properties +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/spark/spark-defaults.conf +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/trino/access-control.properties +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/trino/catalog/clickhouse.properties +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/trino/catalog/iceberg.properties +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/trino/catalog/kafka.properties +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/trino/catalog/postgres.properties +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/trino/catalog/valkey.properties +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/trino/group-provider.properties +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/trino/groups.txt +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/resources/trino/rules.json +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/src/odctl/ui.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/conftest.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/docs_helpers.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_airflow_profile.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_cli.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_cli_extended.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_compose_profiles.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_config.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_confirmed_pages.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_diagrams.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_docker.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_evidently_profile.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_feature_docs.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_grafana_dashboards.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_logs.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_metrics_collector.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_metrics_jvm.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_metrics_python_services.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_planner.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_postgres_image.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_projects.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_registry.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_release_links.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_release_notes.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_telemetry_profile.py +0 -0
- {odctl-1.2.2 → odctl-1.4.0}/tests/test_temporal_profile.py +0 -0
|
@@ -45,6 +45,11 @@ GROUPS = [
|
|
|
45
45
|
"lineage",
|
|
46
46
|
"telemetry",
|
|
47
47
|
"metadata",
|
|
48
|
+
# The part before the colon is the profile to start. metadata:deps and
|
|
49
|
+
# metadata:autopilot start the same profile as metadata. Each examples group
|
|
50
|
+
# recreates metadata twice, which is why each runs alone, in parallel with the others.
|
|
51
|
+
"metadata:deps",
|
|
52
|
+
"metadata:autopilot",
|
|
48
53
|
"airflow",
|
|
49
54
|
"temporal",
|
|
50
55
|
"postgres",
|
|
@@ -70,13 +75,14 @@ REGISTRY = Path("src/odctl/resources/registry.yml")
|
|
|
70
75
|
|
|
71
76
|
# smoke.sh starts some profiles from another group's case: the kafka case starts
|
|
72
77
|
# catalog, the flink case starts kafka-lite and feast, the spark case starts
|
|
73
|
-
# kafka-lite,
|
|
74
|
-
# compose file needs these groups as well as the
|
|
78
|
+
# kafka-lite, the fluss and feast cases start flink-lite, and the airflow case
|
|
79
|
+
# starts feast. A change to that compose file needs these groups as well as the
|
|
80
|
+
# ones the registry maps to it.
|
|
75
81
|
ALSO_RUNS = {
|
|
76
82
|
"compose-infra.yml": ["kafka-lite kafka-full"],
|
|
77
83
|
"compose-kafka.yml": ["flink-lite flink-full", "spark-lite spark-full"],
|
|
78
84
|
"compose-flink.yml": ["fluss", "feast"],
|
|
79
|
-
"compose-mlops.yml": ["flink-lite flink-full"],
|
|
85
|
+
"compose-mlops.yml": ["flink-lite flink-full", "airflow"],
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
# smoke.sh runs the guide commands in docs/snippets/<guide>/, so a change there
|
|
@@ -85,7 +91,15 @@ ALSO_RUNS = {
|
|
|
85
91
|
SNIPPET_PREFIX = "docs/snippets/"
|
|
86
92
|
SNIPPET_GROUPS = {
|
|
87
93
|
"getting-started": ["kafka-lite kafka-full"],
|
|
88
|
-
"how-it-works": [
|
|
94
|
+
"how-it-works": [
|
|
95
|
+
"storage",
|
|
96
|
+
"catalog",
|
|
97
|
+
"trino",
|
|
98
|
+
"airflow",
|
|
99
|
+
"mlflow",
|
|
100
|
+
"metadata:deps",
|
|
101
|
+
"metadata:autopilot",
|
|
102
|
+
],
|
|
89
103
|
"kafka": ["kafka-lite kafka-full"],
|
|
90
104
|
"kafka-connect": ["kafka-lite kafka-full"],
|
|
91
105
|
"iceberg-ingestion": ["kafka-lite kafka-full", "flink-lite flink-full"],
|
|
@@ -100,10 +114,11 @@ SNIPPET_GROUPS = {
|
|
|
100
114
|
"airflow-dags": ["airflow"],
|
|
101
115
|
"temporal-workers": ["temporal"],
|
|
102
116
|
"mlflow": ["mlflow"],
|
|
103
|
-
"feast-iceberg": ["feast", "flink-lite flink-full", "mlflow"],
|
|
117
|
+
"feast-iceberg": ["feast", "flink-lite flink-full", "mlflow", "airflow"],
|
|
118
|
+
"feast-vector-retrieval": ["feast"],
|
|
104
119
|
"evidently-reports": ["evidently"],
|
|
105
120
|
"lineage": ["lineage"],
|
|
106
|
-
"openmetadata": ["metadata"],
|
|
121
|
+
"openmetadata": ["metadata", "metadata:autopilot"],
|
|
107
122
|
"telemetry": ["telemetry"],
|
|
108
123
|
}
|
|
109
124
|
|
|
@@ -174,7 +189,8 @@ def select(changed: list, registry_path: Path = REGISTRY) -> list:
|
|
|
174
189
|
|
|
175
190
|
selected = []
|
|
176
191
|
for group in GROUPS:
|
|
177
|
-
|
|
192
|
+
# A group name may carry a suffix after a colon, as metadata:deps does.
|
|
193
|
+
files = {mapping.get(p.split(":")[0]) for p in group.split()}
|
|
178
194
|
also = any(group in ALSO_RUNS.get(name, []) for name in touched_files)
|
|
179
195
|
if files & touched_files or group in snippet_groups or also:
|
|
180
196
|
selected.append(group)
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
# committed nothing while its checkpoints succeeded. Each assertion below exists
|
|
10
10
|
# because something shipped broken past a liveness check.
|
|
11
11
|
#
|
|
12
|
-
# Usage: smoke.sh <profile>
|
|
12
|
+
# Usage: smoke.sh <profile or group name, such as metadata:deps>
|
|
13
13
|
set -uo pipefail
|
|
14
14
|
|
|
15
|
-
PROFILE="${1:?usage: smoke.sh <profile>}"
|
|
15
|
+
PROFILE="${1:?usage: smoke.sh <profile or group name, such as metadata:deps>}"
|
|
16
16
|
MB_USER="smoke@example.com"
|
|
17
17
|
MB_PASS="Smoke-test-1234"
|
|
18
18
|
|
|
@@ -880,6 +880,104 @@ if bad:
|
|
|
880
880
|
|
|
881
881
|
# Airflow pushes OTLP to the telemetry collector rather than being scraped.
|
|
882
882
|
assert_pushed_metrics 'airflow_.+' Airflow
|
|
883
|
+
|
|
884
|
+
smoke_airflow_materialize
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
# Feast guide's On a Schedule section: a DAG that builds a FeatureStore from
|
|
888
|
+
# FEAST_REGISTRY and calls materialize_incremental. It needs the registry entry and
|
|
889
|
+
# the Iceberg table the guide's earlier steps make, so this runs those from the
|
|
890
|
+
# host as the guide does, then uploads the DAG, runs it and reads the value it
|
|
891
|
+
# copied into Valkey. The feature view source has no catalog address, so the
|
|
892
|
+
# registry entry works from the host here and from the Airflow container in the DAG.
|
|
893
|
+
# Its own project, so that the guide's Runnable Example can still register demo.
|
|
894
|
+
smoke_airflow_materialize() {
|
|
895
|
+
odctl up feast >/dev/null 2>&1 || fail "could not start feast for the materialise DAG"
|
|
896
|
+
retry 30 5 http_ok "http://127.0.0.1:6566/health" || fail "Feast never answered on :6566"
|
|
897
|
+
|
|
898
|
+
local work="${TMPDIR:-/tmp}/odctl-airflow-feast-smoke"
|
|
899
|
+
rm -rf "$work"; mkdir -p "$work"
|
|
900
|
+
uv venv "$work/.venv" >/dev/null 2>&1 || fail "could not create a venv for the feast client"
|
|
901
|
+
VIRTUAL_ENV="$work/.venv" uv pip install -q \
|
|
902
|
+
"feast[duckdb,iceberg,redis,postgres]==0.66.0" "pyarrow" >/dev/null 2>&1 \
|
|
903
|
+
|| fail "could not install the feast client"
|
|
904
|
+
|
|
905
|
+
cp -r "$SNIPPETS/feast-iceberg/feature_repo" "$work/"
|
|
906
|
+
sed 's/^project: demo$/project: airflow_smoke/' "$work/feature_repo/feature_store.yaml" \
|
|
907
|
+
> "$work/feature_store.yaml" && mv "$work/feature_store.yaml" "$work/feature_repo/feature_store.yaml"
|
|
908
|
+
sed 's/project="demo"/project="airflow_smoke"/' "$SNIPPETS/feast-iceberg/feast_materialize.py" \
|
|
909
|
+
> "$work/feast_materialize.py"
|
|
910
|
+
snippet feast-iceberg/steps.py create-table > "$work/build.py"
|
|
911
|
+
snippet feast-iceberg/steps.py append > "$work/append.py"
|
|
912
|
+
snippet feast-iceberg/steps.py read > "$work/read.py"
|
|
913
|
+
|
|
914
|
+
local py="$work/.venv/bin/python" log="$work/step.log"
|
|
915
|
+
run_step() {
|
|
916
|
+
local what="$1"; shift
|
|
917
|
+
if ! (cd "$work" && "$@") >"$log" 2>&1; then
|
|
918
|
+
echo "---- $what ----"
|
|
919
|
+
tail -25 "$log"
|
|
920
|
+
fail "$what"
|
|
921
|
+
fi
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
# A table left by the Runnable Example would make the guide's create step fail.
|
|
925
|
+
local code
|
|
926
|
+
code=$(curl -sS --max-time 10 -o /dev/null -w '%{http_code}' -X DELETE "http://127.0.0.1:8181/v1/namespaces/demo/tables/driver_stats?purgeRequested=true")
|
|
927
|
+
case "$code" in 204|404) ;; *) fail "could not delete demo.driver_stats before the guide's create step (HTTP $code)";; esac
|
|
928
|
+
eval "$(snippet feast-iceberg/commands.sh catalog-env)"
|
|
929
|
+
run_step "could not create the Iceberg table through the catalog" "$py" build.py
|
|
930
|
+
# Delete the view, so that materialise starts again and the check reads this run's values.
|
|
931
|
+
(cd "$work" && "$work/.venv/bin/feast" -c feature_repo delete driver_stats) >/dev/null 2>&1 || true
|
|
932
|
+
run_step "feast apply or materialize failed without a catalog address in the source" \
|
|
933
|
+
env PATH="$work/.venv/bin:$PATH" bash -e -o pipefail -c "$(snippet feast-iceberg/commands.sh apply-materialize)"
|
|
934
|
+
pass "the guide's source, with no catalog address, applied and materialised from the host"
|
|
935
|
+
|
|
936
|
+
run_step "could not upload the materialise DAG to s3://airflow/dags" \
|
|
937
|
+
run_snippet feast-iceberg/commands.sh dag-upload
|
|
938
|
+
retry 30 10 bash -c \
|
|
939
|
+
"docker exec airflow airflow dags list 2>/dev/null | grep -q feast_materialize" \
|
|
940
|
+
|| fail "the S3 DAG bundle never delivered the materialise DAG, or it was never parsed"
|
|
941
|
+
|
|
942
|
+
# The new row is newer than the host's materialise, so only the DAG can copy it.
|
|
943
|
+
run_step "could not append a row to the Iceberg table" "$py" append.py
|
|
944
|
+
run_step "the guide's unpause and trigger commands failed" \
|
|
945
|
+
run_snippet feast-iceberg/commands.sh dag-run
|
|
946
|
+
|
|
947
|
+
# Unpausing also starts a scheduled run, so wait until every run has finished.
|
|
948
|
+
local states=""
|
|
949
|
+
for _ in $(seq 1 30); do
|
|
950
|
+
states=$(docker exec airflow airflow dags list-runs feast_materialize -o plain 2>/dev/null \
|
|
951
|
+
| grep -E "^feast_materialize" | tr -s ' ' | cut -d' ' -f3)
|
|
952
|
+
grep -qE 'queued|running' <<<"$states" || [ -z "$states" ] || break
|
|
953
|
+
sleep 10
|
|
954
|
+
done
|
|
955
|
+
[ -n "$states" ] && ! grep -qvx success <<<"$states" \
|
|
956
|
+
|| fail "the materialise DAG runs ended in states: ${states:-none}"
|
|
957
|
+
pass "the materialise DAG ran to success inside the Airflow container"
|
|
958
|
+
|
|
959
|
+
local got
|
|
960
|
+
got=$(cd "$work" && "$py" read.py 2>"$log") || { tail -25 "$log"; fail "the online read failed"; }
|
|
961
|
+
[ "$got" = "{'driver_id': [1001, 1002], 'conv_rate': [0.6000000238418579, 0.75]}" ] \
|
|
962
|
+
|| fail "Valkey holds $got, not the 0.6 the DAG should have copied"
|
|
963
|
+
pass "the DAG copied the new value from Iceberg into Valkey"
|
|
964
|
+
|
|
965
|
+
# Leave no hourly run behind: a DAG that stays unpaused fails on its next run once
|
|
966
|
+
# the table below is gone, and the next smoke run would read that failed run.
|
|
967
|
+
docker exec airflow airflow dags pause feast_materialize >/dev/null 2>&1 \
|
|
968
|
+
|| fail "could not pause the materialise DAG"
|
|
969
|
+
docker exec airflow python -c '
|
|
970
|
+
import boto3
|
|
971
|
+
boto3.client("s3", endpoint_url="http://seaweed:8333").delete_object(
|
|
972
|
+
Bucket="airflow", Key="dags/feast_materialize.py")' >/dev/null 2>&1 \
|
|
973
|
+
|| fail "could not delete the materialise DAG from s3://airflow/dags"
|
|
974
|
+
|
|
975
|
+
# No feast teardown: it leaves the view's row in feature_view_version_history, and
|
|
976
|
+
# a second run of this check would then fail to apply the view again.
|
|
977
|
+
curl -fsS --max-time 10 -X DELETE \
|
|
978
|
+
"http://127.0.0.1:8181/v1/namespaces/demo/tables/driver_stats?purgeRequested=true" >/dev/null 2>&1 || true
|
|
979
|
+
curl -fsS --max-time 10 -X DELETE "http://127.0.0.1:8181/v1/namespaces/demo" >/dev/null 2>&1 || true
|
|
980
|
+
rm -rf "$work"
|
|
883
981
|
}
|
|
884
982
|
|
|
885
983
|
# deps is a one-shot copy into a shared volume, so there is no container to
|
|
@@ -1094,7 +1192,7 @@ smoke_fluss() {
|
|
|
1094
1192
|
pass "Flink wrote 3 rows to a Fluss table and read them back"
|
|
1095
1193
|
}
|
|
1096
1194
|
|
|
1097
|
-
# OpenMetadata: the version endpoint answers through every failure
|
|
1195
|
+
# OpenMetadata: the version endpoint answers through every failure an
|
|
1098
1196
|
# upgrade could cause. The ingestion container runs its own Airflow 3, whose
|
|
1099
1197
|
# health path moved to /api/v2/monitor/health, and a stale image or a failed
|
|
1100
1198
|
# migration both leave a server that still reports a version. Drive the whole
|
|
@@ -1110,14 +1208,23 @@ smoke_metadata() {
|
|
|
1110
1208
|
pass "schema migration completed"
|
|
1111
1209
|
assert_metrics http://openmetadata-server:8586/prometheus
|
|
1112
1210
|
|
|
1113
|
-
# The principal domain odctl sets is
|
|
1211
|
+
# The principal domain odctl sets is odctl.io, not the upstream
|
|
1114
1212
|
# default. A wrong domain fails with a misleading invalid password error.
|
|
1115
1213
|
local token
|
|
1116
1214
|
token=$(om_step login 'echo "$TOKEN"' 2>/dev/null | tail -1)
|
|
1117
|
-
[ -n "$token" ] || fail "admin@
|
|
1215
|
+
[ -n "$token" ] || fail "admin@odctl.io could not log in"
|
|
1118
1216
|
pass "admin logged in and received a JWT"
|
|
1119
1217
|
local auth="Authorization: Bearer $token"
|
|
1120
1218
|
|
|
1219
|
+
# user is the second admin. Its entry in AUTHORIZER_ADMIN_PRINCIPALS,
|
|
1220
|
+
# user:password, sets its first password.
|
|
1221
|
+
local code
|
|
1222
|
+
code=$(curl -s -o /dev/null -w '%{http_code}' -H 'Content-Type: application/json' \
|
|
1223
|
+
-d "{\"email\":\"user@odctl.io\",\"password\":\"$(printf password | base64)\"}" \
|
|
1224
|
+
http://127.0.0.1:8585/api/v1/users/login)
|
|
1225
|
+
[ "$code" = "200" ] || fail "user@odctl.io could not log in with the password password (HTTP $code)"
|
|
1226
|
+
pass "user@odctl.io logged in with the password password"
|
|
1227
|
+
|
|
1121
1228
|
# Airflow 3 moved this path. The old /health returns 404 with a message saying so.
|
|
1122
1229
|
retry 60 5 http_ok "http://127.0.0.1:8087/api/v2/monitor/health" \
|
|
1123
1230
|
|| fail "ingestion Airflow never answered on 8087"
|
|
@@ -1126,6 +1233,20 @@ smoke_metadata() {
|
|
|
1126
1233
|
|| fail "the server cannot reach the ingestion container, so no pipeline can deploy"
|
|
1127
1234
|
pass "ingestion Airflow healthy and reachable from the server"
|
|
1128
1235
|
|
|
1236
|
+
# AIRFLOW_ADMIN_USER and AIRFLOW_ADMIN_PASSWORD set this login. A new token
|
|
1237
|
+
# answers 201, and a wrong login 401.
|
|
1238
|
+
code=$(curl -s -o /dev/null -w '%{http_code}' -H 'Content-Type: application/json' \
|
|
1239
|
+
-d '{"username":"user","password":"password"}' http://127.0.0.1:8087/auth/token)
|
|
1240
|
+
[ "$code" = "201" ] || fail "the ingestion Airflow refused user / password (HTTP $code)"
|
|
1241
|
+
pass "ingestion Airflow accepted user / password"
|
|
1242
|
+
|
|
1243
|
+
# openmetadata-init sets AutoPilot from METADATA_AUTOPILOT, false by default.
|
|
1244
|
+
retry 24 5 om_autopilot_is "$token" False \
|
|
1245
|
+
|| fail "AutoPilot does not report active: false after the start"
|
|
1246
|
+
[ "$(docker inspect -f '{{.State.ExitCode}}' openmetadata-init)" = "0" ] \
|
|
1247
|
+
|| fail "openmetadata-init did not exit with code 0"
|
|
1248
|
+
pass "AutoPilot reports active: false and openmetadata-init exited 0"
|
|
1249
|
+
|
|
1129
1250
|
# A real ingestion, with the guide's steps. Postgres is already running, so it
|
|
1130
1251
|
# is its own source.
|
|
1131
1252
|
local svc
|
|
@@ -1142,23 +1263,11 @@ smoke_metadata() {
|
|
|
1142
1263
|
# Airflow's dag_processor has to parse the new file before it can be
|
|
1143
1264
|
# triggered, so the guide's trigger can come too early. Trigger again here.
|
|
1144
1265
|
if ! grep -q 'has been triggered' <<<"$out"; then
|
|
1145
|
-
|
|
1146
|
-
for _ in $(seq 1 24); do
|
|
1147
|
-
if curl -fsS --max-time 90 -X POST -H "$auth" \
|
|
1148
|
-
"http://127.0.0.1:8585/api/v1/services/ingestionPipelines/trigger/$pipe" 2>/dev/null \
|
|
1149
|
-
| grep -q 'has been triggered'; then triggered=yes; break; fi
|
|
1150
|
-
sleep 5
|
|
1151
|
-
done
|
|
1152
|
-
[ "$triggered" = "yes" ] || fail "the pipeline never triggered"
|
|
1266
|
+
om_trigger "$token" "$pipe" || fail "the pipeline never triggered"
|
|
1153
1267
|
fi
|
|
1154
1268
|
|
|
1155
|
-
local state
|
|
1156
|
-
|
|
1157
|
-
state=$(TOKEN="$token" om_step status 2>/dev/null \
|
|
1158
|
-
| python3 -c 'import json,sys; s=json.load(sys.stdin).get("pipelineStatuses") or []; print(s[0]["pipelineState"] if s else "")' 2>/dev/null)
|
|
1159
|
-
case "$state" in success|failed|partialSuccess) break ;; esac
|
|
1160
|
-
sleep 10
|
|
1161
|
-
done
|
|
1269
|
+
local state
|
|
1270
|
+
state=$(om_run_state "$token" om_step "")
|
|
1162
1271
|
[ "$state" = "success" ] || fail "ingestion run finished in state ${state:-none}, not success"
|
|
1163
1272
|
pass "ingestion run reached success"
|
|
1164
1273
|
|
|
@@ -1191,8 +1300,214 @@ smoke_metadata() {
|
|
|
1191
1300
|
done
|
|
1192
1301
|
pass "Context Center pages and memories both answer"
|
|
1193
1302
|
|
|
1303
|
+
smoke_metadata_connector "$token"
|
|
1304
|
+
smoke_metadata_autopilot "$token"
|
|
1305
|
+
|
|
1194
1306
|
TOKEN="$token" om_step remove >/dev/null 2>&1 || fail "the guide's remove step failed"
|
|
1195
|
-
|
|
1307
|
+
code=$(curl -s -o /dev/null -w '%{http_code}' -H "$auth" \
|
|
1308
|
+
http://127.0.0.1:8585/api/v1/services/databaseServices/name/demo_connector)
|
|
1309
|
+
[ "$code" = "404" ] || fail "the guide's remove step left the demo_connector service (HTTP $code)"
|
|
1310
|
+
retry 12 5 bash -c '! docker exec openmetadata-ingestion test -e /opt/airflow/connectors/demo_connector.py' \
|
|
1311
|
+
|| fail "the ingestion container kept the connector after the remove step deleted it from S3"
|
|
1312
|
+
pass "the guide's remove step deleted both services, their pipelines and the uploaded connector"
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
# Trigger an ingestion pipeline until the server accepts it: Airflow has to parse
|
|
1316
|
+
# a new DAG file first. Usage: om_trigger <token> <pipeline id>
|
|
1317
|
+
om_trigger() {
|
|
1318
|
+
local _
|
|
1319
|
+
for _ in $(seq 1 24); do
|
|
1320
|
+
if curl -fsS --max-time 90 -X POST -H "Authorization: Bearer $1" \
|
|
1321
|
+
"http://127.0.0.1:8585/api/v1/services/ingestionPipelines/trigger/$2" 2>/dev/null \
|
|
1322
|
+
| grep -q 'has been triggered'; then return 0; fi
|
|
1323
|
+
sleep 5
|
|
1324
|
+
done
|
|
1325
|
+
return 1
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
# The latest run of the pipeline a guide's status step reads, as "<run id> <state>",
|
|
1329
|
+
# or nothing when there is no run yet. The step is the guide's own: om_step for
|
|
1330
|
+
# demo_pg, om_connector_step for the custom connector.
|
|
1331
|
+
# Usage: om_latest_run <token> <om_step|om_connector_step>
|
|
1332
|
+
om_latest_run() {
|
|
1333
|
+
TOKEN="$1" "$2" status 2>/dev/null | python3 -c '
|
|
1334
|
+
import json, sys
|
|
1335
|
+
runs = json.load(sys.stdin).get("pipelineStatuses") or []
|
|
1336
|
+
if runs:
|
|
1337
|
+
print(runs[0]["runId"], runs[0]["pipelineState"])' 2>/dev/null
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
# Wait for the first run whose id is not <previous run id> to finish, and print its
|
|
1341
|
+
# state. Usage: om_run_state <token> <om_step|om_connector_step> <previous run id>
|
|
1342
|
+
om_run_state() {
|
|
1343
|
+
local _ run state
|
|
1344
|
+
for _ in $(seq 1 30); do
|
|
1345
|
+
read -r run state < <(om_latest_run "$1" "$2")
|
|
1346
|
+
if [ -n "${run:-}" ] && [ "$run" != "$3" ]; then
|
|
1347
|
+
case "$state" in success|failed|partialSuccess) echo "$state"; return ;; esac
|
|
1348
|
+
fi
|
|
1349
|
+
sleep 10
|
|
1350
|
+
done
|
|
1351
|
+
echo ""
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
# Whether the guide's autopilot step prints <value>. Usage: om_autopilot_is <token> <True|False>
|
|
1355
|
+
om_autopilot_is() {
|
|
1356
|
+
[ "$(TOKEN="$1" om_step autopilot 2>/dev/null | tail -1)" = "$2" ]
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
# Add a service the way the UI does: create it, then trigger AutoPilotApplication with
|
|
1360
|
+
# the service's entityLink. Usage: om_add_service_like_the_ui <token> <service name>
|
|
1361
|
+
om_add_service_like_the_ui() {
|
|
1362
|
+
local auth="Authorization: Bearer $1"
|
|
1363
|
+
curl -fsS -X PUT -H "$auth" -H 'Content-Type: application/json' \
|
|
1364
|
+
-d "{\"name\":\"$2\",\"serviceType\":\"CustomDatabase\",\"connection\":{\"config\":{\"type\":\"CustomDatabase\",\"sourcePythonClass\":\"demo_connector.DemoSource\"}}}" \
|
|
1365
|
+
http://127.0.0.1:8585/api/v1/services/databaseServices >/dev/null || fail "could not create the $2 service"
|
|
1366
|
+
curl -fsS -X POST -H "$auth" -H 'Content-Type: application/json' \
|
|
1367
|
+
-d "{\"entityLink\":\"<#E::databaseService::$2>\"}" \
|
|
1368
|
+
http://127.0.0.1:8585/api/v1/apps/trigger/AutoPilotApplication >/dev/null \
|
|
1369
|
+
|| fail "the server refused the AutoPilot trigger the UI sends"
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
# The number of agents a service has. Usage: om_agent_count <token> <service name>
|
|
1373
|
+
om_agent_count() {
|
|
1374
|
+
curl -fsS -H "Authorization: Bearer $1" "http://127.0.0.1:8585/api/v1/services/ingestionPipelines?service=$2" \
|
|
1375
|
+
| python3 -c 'import json,sys; print(json.load(sys.stdin)["paging"]["total"])'
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
om_delete_service() {
|
|
1379
|
+
curl -s -o /dev/null -X DELETE -H "Authorization: Bearer $1" \
|
|
1380
|
+
"http://127.0.0.1:8585/api/v1/services/databaseServices/name/$2?hardDelete=true&recursive=true"
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
# With AutoPilot off, a service added the way the UI adds one gets no agent. The
|
|
1384
|
+
# check waits 60 seconds, as long as the on-case below allows AutoPilot to create its agent.
|
|
1385
|
+
smoke_metadata_autopilot() {
|
|
1386
|
+
local agents
|
|
1387
|
+
om_add_service_like_the_ui "$1" demo_ui
|
|
1388
|
+
sleep 60
|
|
1389
|
+
agents=$(om_agent_count "$1" demo_ui)
|
|
1390
|
+
om_delete_service "$1" demo_ui
|
|
1391
|
+
[ "$agents" = "0" ] || fail "a service added the way the UI adds one got $agents agents with AutoPilot off"
|
|
1392
|
+
pass "a service added the way the UI adds one, with its AutoPilot trigger, got no agent"
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
# The same two calls with AutoPilot on create at least one agent. It proves
|
|
1396
|
+
# METADATA_AUTOPILOT=true works and that the off-case above could have failed.
|
|
1397
|
+
smoke_metadata_autopilot_on() {
|
|
1398
|
+
local agents=0 _
|
|
1399
|
+
om_add_service_like_the_ui "$1" demo_ui_on
|
|
1400
|
+
for _ in $(seq 1 12); do
|
|
1401
|
+
agents=$(om_agent_count "$1" demo_ui_on)
|
|
1402
|
+
[ "${agents:-0}" -ge 1 ] && break
|
|
1403
|
+
sleep 5
|
|
1404
|
+
done
|
|
1405
|
+
om_delete_service "$1" demo_ui_on
|
|
1406
|
+
[ "${agents:-0}" -ge 1 ] || fail "a service added the way the UI adds one got no agent with AutoPilot on"
|
|
1407
|
+
pass "with METADATA_AUTOPILOT true, a service added the way the UI adds one got $agents agent(s)"
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
# The How It Works example turns AutoPilot on and recreates metadata. While it is on,
|
|
1411
|
+
# check the UI's calls create an agent, then put the key back and check it is off.
|
|
1412
|
+
how_it_works_autopilot() {
|
|
1413
|
+
local out
|
|
1414
|
+
out=$(run_snippet how-it-works/commands.sh metadata-autopilot 2>&1) || { echo "$out" | tail -20; fail "the metadata-autopilot example failed"; }
|
|
1415
|
+
grep -qE -- 'AutoPilot active: true' <<<"$out" || { echo "$out" | tail -20; fail "the metadata-autopilot example printed no 'AutoPilot active: true'"; }
|
|
1416
|
+
pass "How It Works' metadata-autopilot example printed 'AutoPilot active: true'"
|
|
1417
|
+
smoke_metadata_autopilot_on "$(om_token)"
|
|
1418
|
+
mv .odctl/.env.bak .odctl/.env || fail "the metadata-autopilot example left no .env.bak"
|
|
1419
|
+
odctl recreate metadata >/dev/null 2>&1 || fail "could not recreate metadata after the metadata-autopilot example"
|
|
1420
|
+
retry 24 5 om_autopilot_is "$(om_token)" False \
|
|
1421
|
+
|| fail "AutoPilot is not off again after the key was put back"
|
|
1422
|
+
pass "AutoPilot is off again after the key was put back"
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
# Run a connector section until its output has <text>. Usage: om_connector_says <section> <text>
|
|
1426
|
+
om_connector_says() {
|
|
1427
|
+
om_connector_step "$1" 2>/dev/null | grep -qF "$2"
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
# The guide's custom connector steps, run from the folder that holds the file, as
|
|
1431
|
+
# the guide tells the reader. Usage: om_connector_step <section> [extra commands]
|
|
1432
|
+
om_connector_step() {
|
|
1433
|
+
local code
|
|
1434
|
+
code=$(snippet openmetadata/connector/commands.sh "$1")
|
|
1435
|
+
(cd "$CONNECTOR_DIR" && bash -o pipefail -c 'AUTH="Authorization: Bearer ${TOKEN:-}"
|
|
1436
|
+
'"$code"'
|
|
1437
|
+
'"${2:-}")
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
# A custom connector uploaded to s3://openmetadata/connectors is importable in the
|
|
1441
|
+
# ingestion container, and a service and an agent made through the API run it.
|
|
1442
|
+
# Then a changed connector is used by the next run with no restart, which is why
|
|
1443
|
+
# nothing here restarts the container: its start time must not move.
|
|
1444
|
+
smoke_metadata_connector() {
|
|
1445
|
+
local token="$1" auth="Authorization: Bearer $1" started svc pipe out run state agents
|
|
1446
|
+
CONNECTOR_DIR=$(mktemp -d)
|
|
1447
|
+
cp "$SNIPPETS/openmetadata/connector/demo_connector.py" "$CONNECTOR_DIR/"
|
|
1448
|
+
started=$(docker inspect -f '{{.State.StartedAt}}' openmetadata-ingestion)
|
|
1449
|
+
|
|
1450
|
+
om_connector_step upload >/dev/null 2>&1 || fail "could not upload the connector to s3://openmetadata/connectors"
|
|
1451
|
+
retry 12 5 om_connector_says copied demo_connector.py \
|
|
1452
|
+
|| fail "the ingestion container never copied the connector from S3"
|
|
1453
|
+
pass "the connector reached /opt/airflow/connectors in the ingestion container"
|
|
1454
|
+
|
|
1455
|
+
svc=$(TOKEN="$token" om_connector_step service 'echo "$SVC"' 2>/dev/null | tail -1)
|
|
1456
|
+
[ -n "$svc" ] || fail "could not create the CustomDatabase service through the API"
|
|
1457
|
+
out=$(TOKEN="$token" SVC="$svc" om_connector_step agent 'echo; echo "$PIPE"' 2>/dev/null)
|
|
1458
|
+
pipe=$(tail -1 <<<"$out")
|
|
1459
|
+
[ -n "$pipe" ] || fail "could not create the custom connector's agent"
|
|
1460
|
+
grep -q 'has been created' <<<"$out" || { echo "$out"; fail "the custom connector's agent did not deploy a DAG"; }
|
|
1461
|
+
grep -q 'has been triggered' <<<"$out" || om_trigger "$token" "$pipe" || fail "the custom connector's agent never triggered"
|
|
1462
|
+
state=$(om_run_state "$token" om_connector_step "")
|
|
1463
|
+
[ "$state" = "success" ] || fail "the custom connector's run finished in state ${state:-none}, not success"
|
|
1464
|
+
out=$(TOKEN="$token" om_connector_step table 2>/dev/null)
|
|
1465
|
+
grep -q '"fullyQualifiedName":"demo_connector.demo_db.public.orders"' <<<"$out" \
|
|
1466
|
+
&& grep -q '"name":"id"' <<<"$out" && grep -q '"name":"amount"' <<<"$out" \
|
|
1467
|
+
|| fail "the custom connector's table orders, with its columns id and amount, is not in the catalogue"
|
|
1468
|
+
pass "the custom connector's agent ran and its table orders is in the catalogue"
|
|
1469
|
+
|
|
1470
|
+
# A service made through the API starts no AutoPilot agents. The UI does.
|
|
1471
|
+
agents=$(curl -fsS -H "$auth" "http://127.0.0.1:8585/api/v1/services/ingestionPipelines?service=demo_connector" \
|
|
1472
|
+
| python3 -c 'import json,sys; print(json.load(sys.stdin)["paging"]["total"])')
|
|
1473
|
+
[ "$agents" = "1" ] || fail "the demo_connector service has $agents agents, not only its own"
|
|
1474
|
+
pass "the demo_connector service has only its own agent"
|
|
1475
|
+
|
|
1476
|
+
# Change the connector, upload it again and trigger the same agent.
|
|
1477
|
+
read -r run _ < <(om_latest_run "$token" om_connector_step)
|
|
1478
|
+
om_connector_step change >/dev/null 2>&1 || fail "could not change the connector"
|
|
1479
|
+
om_connector_step upload >/dev/null 2>&1 || fail "could not upload the changed connector"
|
|
1480
|
+
retry 12 5 om_connector_says copied-change 1 \
|
|
1481
|
+
|| fail "the ingestion container never copied the changed connector"
|
|
1482
|
+
TOKEN="$token" PIPE="$pipe" om_connector_step retrigger 2>/dev/null | grep -q 'has been triggered' \
|
|
1483
|
+
|| fail "the custom connector's agent did not trigger again"
|
|
1484
|
+
state=$(om_run_state "$token" om_connector_step "$run")
|
|
1485
|
+
[ "$state" = "success" ] || fail "the second run finished in state ${state:-none}, not success"
|
|
1486
|
+
TOKEN="$token" om_connector_step changed-table 2>/dev/null \
|
|
1487
|
+
| grep -q '"name":"orders_v2"' || fail "the changed connector's table orders_v2 is not in the catalogue"
|
|
1488
|
+
[ "$(docker inspect -f '{{.State.StartedAt}}' openmetadata-ingestion)" = "$started" ] \
|
|
1489
|
+
|| fail "the ingestion container restarted while the connector changed"
|
|
1490
|
+
pass "the changed connector was used by the next run, with no restart"
|
|
1491
|
+
rm -rf "$CONNECTOR_DIR"
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
# The guide's runnable example needs demo_connector.py in the folder it runs in.
|
|
1495
|
+
smoke_metadata_example() {
|
|
1496
|
+
local out
|
|
1497
|
+
cp "$SNIPPETS/openmetadata/connector/demo_connector.py" .
|
|
1498
|
+
out=$(bash "$SNIPPETS/openmetadata/example.sh" 2>&1) || { rm -f demo_connector.py; echo "$out" | tail -20; fail "the openmetadata example failed"; }
|
|
1499
|
+
rm -f demo_connector.py
|
|
1500
|
+
local want
|
|
1501
|
+
for want in 'autopilot active: False' 'demo_pg ingestion: success' 'demo_connector ingestion: success' \
|
|
1502
|
+
'^custom table: orders$' 'removed demo_pg and demo_connector'; do
|
|
1503
|
+
grep -qE -- "$want" <<<"$out" || { echo "$out" | tail -20; fail "the openmetadata example printed no '$want'"; }
|
|
1504
|
+
done
|
|
1505
|
+
pass "the openmetadata guide's runnable example ran both services and printed its results"
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
# An admin token, from the guide's login step.
|
|
1509
|
+
om_token() {
|
|
1510
|
+
om_step login 'echo "$TOKEN"' 2>/dev/null | tail -1
|
|
1196
1511
|
}
|
|
1197
1512
|
|
|
1198
1513
|
# The OpenMetadata guide's steps share shell variables: login sets TOKEN, the
|
|
@@ -1210,7 +1525,9 @@ om_step() {
|
|
|
1210
1525
|
# Feast keeps no feature values of its own, so liveness proves nothing here.
|
|
1211
1526
|
# This builds a real Iceberg table through the catalog, registers a feature view
|
|
1212
1527
|
# over it, takes a point-in-time join, materializes to Valkey and reads it back.
|
|
1213
|
-
#
|
|
1528
|
+
# It also retrieves the nearest rows from a second repository on PostgreSQL with
|
|
1529
|
+
# pgvector. A failure in any of those is a broken profile even when both
|
|
1530
|
+
# containers are up.
|
|
1214
1531
|
smoke_feast() {
|
|
1215
1532
|
retry 60 5 http_ok "http://127.0.0.1:8890/api/v1/projects" || fail "no HTTP response from the feast UI on :8890"
|
|
1216
1533
|
pass "feast UI answering, registry reachable"
|
|
@@ -1225,17 +1542,16 @@ smoke_feast() {
|
|
|
1225
1542
|
# the published image carries feast[minimal], which has no iceberg or duckdb.
|
|
1226
1543
|
uv venv "$work/.venv" >/dev/null 2>&1 || fail "could not create a venv for the feast client"
|
|
1227
1544
|
VIRTUAL_ENV="$work/.venv" uv pip install -q \
|
|
1228
|
-
"feast[duckdb,iceberg,redis,postgres]==0.66.0" "pyarrow" >/dev/null 2>&1 \
|
|
1229
|
-
|| fail "could not install the feast
|
|
1545
|
+
"feast[duckdb,iceberg,redis,postgres]==0.66.0" "pyarrow" "mcp>=1.24,<2" >/dev/null 2>&1 \
|
|
1546
|
+
|| fail "could not install the feast and mcp clients"
|
|
1230
1547
|
|
|
1231
|
-
# Repository from the guide,
|
|
1232
|
-
#
|
|
1233
|
-
# catalogs, and Iceberg's REST fixture server serves the spec with no prefix,
|
|
1234
|
-
# so a real warehouse gives HTTP 400 "Ambiguous URI empty segment".
|
|
1548
|
+
# Repository from the guide, whose source passes warehouse="" so that PyIceberg
|
|
1549
|
+
# takes the warehouse from PYICEBERG_CATALOG__ODCTL__WAREHOUSE.
|
|
1235
1550
|
cp "$SNIPPETS/feast-iceberg/feature_repo/"* "$work/feature_repo/"
|
|
1236
|
-
# Its own project,
|
|
1237
|
-
#
|
|
1238
|
-
# feature_view_version_history, so
|
|
1551
|
+
# Its own project, so that the guide's Runnable Example can still register demo.
|
|
1552
|
+
# No feast teardown: it deletes every project in the registry, and it leaves each
|
|
1553
|
+
# view's row in feature_view_version_history, so the next apply of that view fails
|
|
1554
|
+
# on its key. feast delete removes one view and its online values, and the view applies again.
|
|
1239
1555
|
sed 's/^project: demo$/project: smoke/' "$work/feature_repo/feature_store.yaml" \
|
|
1240
1556
|
> "$work/feature_store.yaml" && mv "$work/feature_store.yaml" "$work/feature_repo/feature_store.yaml"
|
|
1241
1557
|
|
|
@@ -1286,11 +1602,17 @@ PYONLINE
|
|
|
1286
1602
|
fi
|
|
1287
1603
|
}
|
|
1288
1604
|
|
|
1605
|
+
# A table left by the Runnable Example or by a reader would make the guide's create step fail.
|
|
1606
|
+
local code
|
|
1607
|
+
code=$(curl -sS --max-time 10 -o /dev/null -w '%{http_code}' -X DELETE "http://127.0.0.1:8181/v1/namespaces/demo/tables/driver_stats?purgeRequested=true")
|
|
1608
|
+
case "$code" in 204|404) ;; *) fail "could not delete demo.driver_stats before the guide's create step (HTTP $code)";; esac
|
|
1289
1609
|
run_step "could not create the Iceberg table through the catalog" "$py" build.py
|
|
1290
1610
|
pass "Iceberg table created through the REST catalog on SeaweedFS"
|
|
1291
1611
|
|
|
1612
|
+
# Delete the view, so that materialise starts again and the check reads this run's values.
|
|
1613
|
+
(cd "$work" && "$feast_bin" -c feature_repo delete driver_stats) >/dev/null 2>&1 || true
|
|
1292
1614
|
run_step "feast apply or materialize failed" \
|
|
1293
|
-
env PATH="$work/.venv/bin:$PATH" bash -o pipefail -c "$(snippet feast-iceberg/commands.sh apply-materialize)"
|
|
1615
|
+
env PATH="$work/.venv/bin:$PATH" bash -e -o pipefail -c "$(snippet feast-iceberg/commands.sh apply-materialize)"
|
|
1294
1616
|
pass "feast apply registered the feature view, and materialize wrote to valkey"
|
|
1295
1617
|
|
|
1296
1618
|
run_step "the point-in-time join returned the wrong values" "$py" check.py
|
|
@@ -1299,7 +1621,25 @@ PYONLINE
|
|
|
1299
1621
|
run_step "the online read from valkey returned the wrong values" "$py" online.py
|
|
1300
1622
|
pass "get_online_features read the materialised values from valkey"
|
|
1301
1623
|
|
|
1302
|
-
|
|
1624
|
+
# Vector retrieval, from the vector page: a second repository, with
|
|
1625
|
+
# PostgreSQL and pgvector as its online store. Its own project, because the
|
|
1626
|
+
# Runnable Example registers vector_demo afterwards.
|
|
1627
|
+
mkdir -p "$work/vector_repo"
|
|
1628
|
+
cp "$SNIPPETS/feast-vector-retrieval/vector_repo/"* "$work/vector_repo/"
|
|
1629
|
+
sed 's/^project: vector_demo$/project: vector_smoke/' "$work/vector_repo/feature_store.yaml" \
|
|
1630
|
+
> "$work/feature_store.yaml" && mv "$work/feature_store.yaml" "$work/vector_repo/feature_store.yaml"
|
|
1631
|
+
snippet feast-vector-retrieval/steps.py write > "$work/vector_write.py"
|
|
1632
|
+
snippet feast-vector-retrieval/steps.py retrieve > "$work/vector_retrieve.py"
|
|
1633
|
+
run_step "feast apply failed on the vector repository" \
|
|
1634
|
+
env PATH="$work/.venv/bin:$PATH" bash -o pipefail -c "$(snippet feast-vector-retrieval/commands.sh apply)"
|
|
1635
|
+
run_step "writing the documents to the PostgreSQL online store failed" "$py" vector_write.py
|
|
1636
|
+
local nearest
|
|
1637
|
+
nearest=$(cd "$work" && "$py" vector_retrieve.py 2>"$log") \
|
|
1638
|
+
|| { tail -25 "$log"; fail "retrieve_online_documents_v2 failed"; }
|
|
1639
|
+
[ "$nearest" = "['Feast', 'Iceberg']" ] \
|
|
1640
|
+
|| fail "retrieve_online_documents_v2 returned $nearest, not the two nearest documents"
|
|
1641
|
+
pass "retrieve_online_documents_v2 returned the nearest rows from pgvector"
|
|
1642
|
+
|
|
1303
1643
|
curl -fsS --max-time 10 -X DELETE \
|
|
1304
1644
|
"http://127.0.0.1:8181/v1/namespaces/demo/tables/driver_stats?purgeRequested=true" >/dev/null 2>&1 || true
|
|
1305
1645
|
curl -fsS --max-time 10 -X DELETE "http://127.0.0.1:8181/v1/namespaces/demo" >/dev/null 2>&1 || true
|
|
@@ -1307,6 +1647,31 @@ PYONLINE
|
|
|
1307
1647
|
# case removes $work after that.
|
|
1308
1648
|
}
|
|
1309
1649
|
|
|
1650
|
+
# The checks run after the guide's Runnable Example, so they reuse its push
|
|
1651
|
+
# definitions in feast-serve; the HTTP push then stores a newer value than the
|
|
1652
|
+
# example's Flink push. Read it back until the feature server's cached registry
|
|
1653
|
+
# knows the feature view, then read it through the MCP endpoint.
|
|
1654
|
+
smoke_feast_push_and_mcp() {
|
|
1655
|
+
local work="${TMPDIR:-/tmp}/odctl-feast-smoke"
|
|
1656
|
+
local out
|
|
1657
|
+
out=$(cd "$work" && run_snippet feast-iceberg/commands.sh push-http 2>&1) \
|
|
1658
|
+
|| { echo "$out" | tail -20; fail "the HTTP push to /push failed"; }
|
|
1659
|
+
[ "$out" = "200" ] || { echo "$out" | tail -20; fail "the HTTP push to /push answered '$out', not 200"; }
|
|
1660
|
+
retry 24 5 snippet_says feast-iceberg/commands.sh push-read -F 0.699999988079071 \
|
|
1661
|
+
|| fail "the value pushed over HTTP was not read back from /get-online-features"
|
|
1662
|
+
pass "a value pushed over HTTP was read back from the feature server"
|
|
1663
|
+
|
|
1664
|
+
cp "$SNIPPETS/feast-iceberg/mcp_client.py" "$work/"
|
|
1665
|
+
out=$(cd "$work" && "$work/.venv/bin/python" mcp_client.py 2>&1) \
|
|
1666
|
+
|| { echo "$out" | tail -20; fail "the MCP client failed"; }
|
|
1667
|
+
grep -qx 'get_online_features_get_online_features_post' <<<"$out" \
|
|
1668
|
+
&& grep -qx 'push_push_post' <<<"$out" \
|
|
1669
|
+
|| { echo "$out" | tail -20; fail "the MCP endpoint did not list Feast's tools"; }
|
|
1670
|
+
grep -qxF '[0.699999988079071]' <<<"$out" \
|
|
1671
|
+
|| { echo "$out" | tail -20; fail "the MCP tool did not return the pushed value"; }
|
|
1672
|
+
pass "an MCP client listed Feast's tools and read the pushed value"
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1310
1675
|
# Temporal: a healthy server proves only that it answers. Workflows run in the
|
|
1311
1676
|
# caller's worker, so this runs one with the temporalio SDK from the host: an
|
|
1312
1677
|
# activity that fails once and succeeds on retry, and a workflow that waits for
|
|
@@ -1546,11 +1911,26 @@ case "$PROFILE" in
|
|
|
1546
1911
|
run_example mlflow '"predictions": \[0, 0\]' "$(mlflow_runner) bash" ;;
|
|
1547
1912
|
lineage) smoke_lineage; run_example lineage 'COMPLETED' ;;
|
|
1548
1913
|
telemetry) smoke_telemetry; run_example telemetry '"42"' ;;
|
|
1549
|
-
metadata)
|
|
1914
|
+
metadata)
|
|
1915
|
+
smoke_metadata
|
|
1916
|
+
smoke_metadata_example ;;
|
|
1917
|
+
# Two How It Works examples that recreate metadata, each in a group of its own
|
|
1918
|
+
# so that they run in parallel with the checks above and with each other. Each
|
|
1919
|
+
# recreates metadata twice, once for the example and once to restore it.
|
|
1920
|
+
metadata:deps)
|
|
1921
|
+
how_it_works_edit metadata-deps .odctl/.env metadata '^1\.4\.3$' ;;
|
|
1922
|
+
metadata:autopilot)
|
|
1923
|
+
how_it_works_autopilot
|
|
1924
|
+
retry 60 5 http_ok "http://127.0.0.1:8087/api/v2/monitor/health" \
|
|
1925
|
+
|| fail "ingestion Airflow did not answer after the package was put back" ;;
|
|
1550
1926
|
fluss) smoke_fluss; run_example fluss '\| +3 +\| +c +\|' ;;
|
|
1551
1927
|
feast)
|
|
1552
1928
|
smoke_feast
|
|
1553
1929
|
run_example feast-iceberg '0.8999999761581421' "env PATH=${TMPDIR:-/tmp}/odctl-feast-smoke/.venv/bin:$PATH bash"
|
|
1930
|
+
smoke_feast_push_and_mcp
|
|
1931
|
+
run_example feast-vector-retrieval "\['Feast', 'Iceberg'\]" "env PATH=${TMPDIR:-/tmp}/odctl-feast-smoke/.venv/bin:$PATH bash"
|
|
1932
|
+
# The two examples write their repositories into the current folder, as a reader's would.
|
|
1933
|
+
rm -rf feature_repo vector_repo push_definitions.py
|
|
1554
1934
|
rm -rf "${TMPDIR:-/tmp}/odctl-feast-smoke" ;;
|
|
1555
1935
|
evidently) smoke_evidently; run_example evidently-reports 'report pushed to project' "$(evidently_runner)" ;;
|
|
1556
1936
|
*)
|
|
@@ -20,7 +20,7 @@ jobs:
|
|
|
20
20
|
- name: Install uv
|
|
21
21
|
# Pinned to a full version on purpose: setup-uv stopped publishing
|
|
22
22
|
# moving major tags at v8, so astral-sh/setup-uv@v10 does not exist.
|
|
23
|
-
uses: astral-sh/setup-uv@v10.3.
|
|
23
|
+
uses: astral-sh/setup-uv@v10.3.2
|
|
24
24
|
with:
|
|
25
25
|
enable-cache: true
|
|
26
26
|
# The default key holds the runner's Python, not the matrix one, so
|