odctl 1.2.2__tar.gz → 1.3.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.3.0}/.github/scripts/select-profiles.py +1 -1
- {odctl-1.2.2 → odctl-1.3.0}/.github/scripts/smoke.sh +243 -20
- {odctl-1.2.2 → odctl-1.3.0}/.github/workflows/ci.yml +1 -1
- {odctl-1.2.2 → odctl-1.3.0}/.github/workflows/e2e-test.yml +1 -1
- {odctl-1.2.2 → odctl-1.3.0}/.github/workflows/pipeline.yml +3 -3
- {odctl-1.2.2 → odctl-1.3.0}/.github/workflows/publish.yml +1 -1
- odctl-1.3.0/CLAUDE.md +20 -0
- odctl-1.3.0/CONTRIBUTING.md +99 -0
- {odctl-1.2.2 → odctl-1.3.0}/PKG-INFO +4 -25
- {odctl-1.2.2 → odctl-1.3.0}/README.md +3 -24
- {odctl-1.2.2 → odctl-1.3.0}/pyproject.toml +1 -1
- {odctl-1.2.2 → odctl-1.3.0}/scripts/gen_pages.py +2 -0
- {odctl-1.2.2 → odctl-1.3.0}/skills/odctl/SKILL.md +23 -14
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/compose-analytics.yml +1 -1
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/compose-infra.yml +1 -1
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/compose-kafka.yml +1 -1
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/compose-metadata.yml +79 -9
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/deps/build.gradle +1 -1
- odctl-1.3.0/src/odctl/resources/openmetadata/autopilot_init.py +158 -0
- odctl-1.3.0/src/odctl/resources/openmetadata/connector_sync.py +178 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/registry.yml +10 -4
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/workspace.py +14 -0
- odctl-1.3.0/tests/test_autopilot_init.py +117 -0
- odctl-1.3.0/tests/test_connector_sync.py +183 -0
- odctl-1.3.0/tests/test_metadata_profile.py +119 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_workspace.py +4 -0
- {odctl-1.2.2 → odctl-1.3.0}/uv.lock +1 -1
- odctl-1.2.2/CLAUDE.md +0 -64
- {odctl-1.2.2 → odctl-1.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/.github/renovate.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/.github/scripts/spark_avro_producer.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/.github/workflows/build-deps.yml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/.github/workflows/build-platform-images.yml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/.gitignore +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/.pre-commit-config.yaml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/.vscode/settings.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/LICENSE +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/image/demo.gif +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/image/demo.png +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/image/diagram.drawio +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/image/diagram.png +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/mkdocs.yml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/scripts/diagrams.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/scripts/llmstxt_sections.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/scripts/local_links.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/scripts/release_links.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/scripts/release_notes.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/__init__.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/config.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/docker.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/logs.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/main.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/planner.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/projects.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/registry.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/airflow/odctl_log_config.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/clickhouse/config.xml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/clickhouse/init.sql +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/clickhouse/keeper_config.xml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/clickhouse/macros-11-lite.xml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/clickhouse/macros-11.xml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/clickhouse/macros-12-lite.xml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/clickhouse/macros-12.xml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/clickhouse/macros-21.xml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/clickhouse/macros-22.xml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/clickhouse/users.xml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/compose-deps.yml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/compose-flink.yml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/compose-mlops.yml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/compose-obsv.yml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/compose-orch.yml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/compose-spark.yml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/compose-store.yml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/deps/Dockerfile +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/deps/build_connectors.sh +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/deps/fetch_flink_dependencies.sh +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/deps/fetch_spark_dependencies.sh +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/deps/fetch_standalone_plugins.sh +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/deps/versions.env +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/docker/airflow/Dockerfile +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/docker/mlflow/Dockerfile +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/docker/postgres/Dockerfile +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/docker/spark/Dockerfile +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/evidently/config.yaml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/feast/feature_store.yaml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/flink/flink-conf.yml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/airflow.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/clickhouse.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/feast.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/flink.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/fluss.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/iceberg-catalog.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/kafka-connect.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/kafka.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/karapace.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/marquez.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/metabase.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/mlflow.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/openmetadata.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/postgresql.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/seaweedfs.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/spark.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/temporal.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/trino.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/valkey.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/grafana/odctl-dashboards.yaml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/grafana/sources.yml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/jmx/jmx-exporter.yml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/marquez/marquez.yml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/otelcol/odctl-receivers.yaml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/postgres/01-init-databases.sh +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/prometheus/prometheus.yml +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/seaweed/s3.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/spark/log4j2.properties +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/spark/metrics.properties +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/spark/spark-defaults.conf +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/trino/access-control.properties +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/trino/catalog/clickhouse.properties +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/trino/catalog/iceberg.properties +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/trino/catalog/kafka.properties +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/trino/catalog/postgres.properties +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/trino/catalog/valkey.properties +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/trino/group-provider.properties +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/trino/groups.txt +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/resources/trino/rules.json +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/src/odctl/ui.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/conftest.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/docs_helpers.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_airflow_profile.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_cli.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_cli_extended.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_compose_profiles.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_config.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_confirmed_pages.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_diagrams.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_docker.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_docs.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_evidently_profile.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_feast_profile.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_feature_docs.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_grafana_dashboards.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_logs.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_metrics_collector.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_metrics_jvm.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_metrics_python_services.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_planner.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_postgres_image.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_projects.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_registry.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_release_links.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_release_notes.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_telemetry_profile.py +0 -0
- {odctl-1.2.2 → odctl-1.3.0}/tests/test_temporal_profile.py +0 -0
|
@@ -85,7 +85,7 @@ ALSO_RUNS = {
|
|
|
85
85
|
SNIPPET_PREFIX = "docs/snippets/"
|
|
86
86
|
SNIPPET_GROUPS = {
|
|
87
87
|
"getting-started": ["kafka-lite kafka-full"],
|
|
88
|
-
"how-it-works": ["storage", "catalog", "trino", "airflow"],
|
|
88
|
+
"how-it-works": ["storage", "catalog", "trino", "airflow", "metadata"],
|
|
89
89
|
"kafka": ["kafka-lite kafka-full"],
|
|
90
90
|
"kafka-connect": ["kafka-lite kafka-full"],
|
|
91
91
|
"iceberg-ingestion": ["kafka-lite kafka-full", "flink-lite flink-full"],
|
|
@@ -1094,7 +1094,7 @@ smoke_fluss() {
|
|
|
1094
1094
|
pass "Flink wrote 3 rows to a Fluss table and read them back"
|
|
1095
1095
|
}
|
|
1096
1096
|
|
|
1097
|
-
# OpenMetadata: the version endpoint answers through every failure
|
|
1097
|
+
# OpenMetadata: the version endpoint answers through every failure an
|
|
1098
1098
|
# upgrade could cause. The ingestion container runs its own Airflow 3, whose
|
|
1099
1099
|
# health path moved to /api/v2/monitor/health, and a stale image or a failed
|
|
1100
1100
|
# migration both leave a server that still reports a version. Drive the whole
|
|
@@ -1110,14 +1110,23 @@ smoke_metadata() {
|
|
|
1110
1110
|
pass "schema migration completed"
|
|
1111
1111
|
assert_metrics http://openmetadata-server:8586/prometheus
|
|
1112
1112
|
|
|
1113
|
-
# The principal domain odctl sets is
|
|
1113
|
+
# The principal domain odctl sets is odctl.io, not the upstream
|
|
1114
1114
|
# default. A wrong domain fails with a misleading invalid password error.
|
|
1115
1115
|
local token
|
|
1116
1116
|
token=$(om_step login 'echo "$TOKEN"' 2>/dev/null | tail -1)
|
|
1117
|
-
[ -n "$token" ] || fail "admin@
|
|
1117
|
+
[ -n "$token" ] || fail "admin@odctl.io could not log in"
|
|
1118
1118
|
pass "admin logged in and received a JWT"
|
|
1119
1119
|
local auth="Authorization: Bearer $token"
|
|
1120
1120
|
|
|
1121
|
+
# user is the second admin. Its entry in AUTHORIZER_ADMIN_PRINCIPALS,
|
|
1122
|
+
# user:password, sets its first password.
|
|
1123
|
+
local code
|
|
1124
|
+
code=$(curl -s -o /dev/null -w '%{http_code}' -H 'Content-Type: application/json' \
|
|
1125
|
+
-d "{\"email\":\"user@odctl.io\",\"password\":\"$(printf password | base64)\"}" \
|
|
1126
|
+
http://127.0.0.1:8585/api/v1/users/login)
|
|
1127
|
+
[ "$code" = "200" ] || fail "user@odctl.io could not log in with the password password (HTTP $code)"
|
|
1128
|
+
pass "user@odctl.io logged in with the password password"
|
|
1129
|
+
|
|
1121
1130
|
# Airflow 3 moved this path. The old /health returns 404 with a message saying so.
|
|
1122
1131
|
retry 60 5 http_ok "http://127.0.0.1:8087/api/v2/monitor/health" \
|
|
1123
1132
|
|| fail "ingestion Airflow never answered on 8087"
|
|
@@ -1126,6 +1135,20 @@ smoke_metadata() {
|
|
|
1126
1135
|
|| fail "the server cannot reach the ingestion container, so no pipeline can deploy"
|
|
1127
1136
|
pass "ingestion Airflow healthy and reachable from the server"
|
|
1128
1137
|
|
|
1138
|
+
# AIRFLOW_ADMIN_USER and AIRFLOW_ADMIN_PASSWORD set this login. A new token
|
|
1139
|
+
# answers 201, and a wrong login 401.
|
|
1140
|
+
code=$(curl -s -o /dev/null -w '%{http_code}' -H 'Content-Type: application/json' \
|
|
1141
|
+
-d '{"username":"user","password":"password"}' http://127.0.0.1:8087/auth/token)
|
|
1142
|
+
[ "$code" = "201" ] || fail "the ingestion Airflow refused user / password (HTTP $code)"
|
|
1143
|
+
pass "ingestion Airflow accepted user / password"
|
|
1144
|
+
|
|
1145
|
+
# openmetadata-init sets AutoPilot from METADATA_AUTOPILOT, false by default.
|
|
1146
|
+
retry 24 5 om_autopilot_is "$token" False \
|
|
1147
|
+
|| fail "AutoPilot does not report active: false after the start"
|
|
1148
|
+
[ "$(docker inspect -f '{{.State.ExitCode}}' openmetadata-init)" = "0" ] \
|
|
1149
|
+
|| fail "openmetadata-init did not exit with code 0"
|
|
1150
|
+
pass "AutoPilot reports active: false and openmetadata-init exited 0"
|
|
1151
|
+
|
|
1129
1152
|
# A real ingestion, with the guide's steps. Postgres is already running, so it
|
|
1130
1153
|
# is its own source.
|
|
1131
1154
|
local svc
|
|
@@ -1142,23 +1165,11 @@ smoke_metadata() {
|
|
|
1142
1165
|
# Airflow's dag_processor has to parse the new file before it can be
|
|
1143
1166
|
# triggered, so the guide's trigger can come too early. Trigger again here.
|
|
1144
1167
|
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"
|
|
1168
|
+
om_trigger "$token" "$pipe" || fail "the pipeline never triggered"
|
|
1153
1169
|
fi
|
|
1154
1170
|
|
|
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
|
|
1171
|
+
local state
|
|
1172
|
+
state=$(om_run_state "$token" om_step "")
|
|
1162
1173
|
[ "$state" = "success" ] || fail "ingestion run finished in state ${state:-none}, not success"
|
|
1163
1174
|
pass "ingestion run reached success"
|
|
1164
1175
|
|
|
@@ -1191,8 +1202,214 @@ smoke_metadata() {
|
|
|
1191
1202
|
done
|
|
1192
1203
|
pass "Context Center pages and memories both answer"
|
|
1193
1204
|
|
|
1205
|
+
smoke_metadata_connector "$token"
|
|
1206
|
+
smoke_metadata_autopilot "$token"
|
|
1207
|
+
|
|
1194
1208
|
TOKEN="$token" om_step remove >/dev/null 2>&1 || fail "the guide's remove step failed"
|
|
1195
|
-
|
|
1209
|
+
code=$(curl -s -o /dev/null -w '%{http_code}' -H "$auth" \
|
|
1210
|
+
http://127.0.0.1:8585/api/v1/services/databaseServices/name/demo_connector)
|
|
1211
|
+
[ "$code" = "404" ] || fail "the guide's remove step left the demo_connector service (HTTP $code)"
|
|
1212
|
+
retry 12 5 bash -c '! docker exec openmetadata-ingestion test -e /opt/airflow/connectors/demo_connector.py' \
|
|
1213
|
+
|| fail "the ingestion container kept the connector after the remove step deleted it from S3"
|
|
1214
|
+
pass "the guide's remove step deleted both services, their pipelines and the uploaded connector"
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
# Trigger an ingestion pipeline until the server accepts it: Airflow has to parse
|
|
1218
|
+
# a new DAG file first. Usage: om_trigger <token> <pipeline id>
|
|
1219
|
+
om_trigger() {
|
|
1220
|
+
local _
|
|
1221
|
+
for _ in $(seq 1 24); do
|
|
1222
|
+
if curl -fsS --max-time 90 -X POST -H "Authorization: Bearer $1" \
|
|
1223
|
+
"http://127.0.0.1:8585/api/v1/services/ingestionPipelines/trigger/$2" 2>/dev/null \
|
|
1224
|
+
| grep -q 'has been triggered'; then return 0; fi
|
|
1225
|
+
sleep 5
|
|
1226
|
+
done
|
|
1227
|
+
return 1
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
# The latest run of the pipeline a guide's status step reads, as "<run id> <state>",
|
|
1231
|
+
# or nothing when there is no run yet. The step is the guide's own: om_step for
|
|
1232
|
+
# demo_pg, om_connector_step for the custom connector.
|
|
1233
|
+
# Usage: om_latest_run <token> <om_step|om_connector_step>
|
|
1234
|
+
om_latest_run() {
|
|
1235
|
+
TOKEN="$1" "$2" status 2>/dev/null | python3 -c '
|
|
1236
|
+
import json, sys
|
|
1237
|
+
runs = json.load(sys.stdin).get("pipelineStatuses") or []
|
|
1238
|
+
if runs:
|
|
1239
|
+
print(runs[0]["runId"], runs[0]["pipelineState"])' 2>/dev/null
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
# Wait for the first run whose id is not <previous run id> to finish, and print its
|
|
1243
|
+
# state. Usage: om_run_state <token> <om_step|om_connector_step> <previous run id>
|
|
1244
|
+
om_run_state() {
|
|
1245
|
+
local _ run state
|
|
1246
|
+
for _ in $(seq 1 30); do
|
|
1247
|
+
read -r run state < <(om_latest_run "$1" "$2")
|
|
1248
|
+
if [ -n "${run:-}" ] && [ "$run" != "$3" ]; then
|
|
1249
|
+
case "$state" in success|failed|partialSuccess) echo "$state"; return ;; esac
|
|
1250
|
+
fi
|
|
1251
|
+
sleep 10
|
|
1252
|
+
done
|
|
1253
|
+
echo ""
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
# Whether the guide's autopilot step prints <value>. Usage: om_autopilot_is <token> <True|False>
|
|
1257
|
+
om_autopilot_is() {
|
|
1258
|
+
[ "$(TOKEN="$1" om_step autopilot 2>/dev/null | tail -1)" = "$2" ]
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
# Add a service the way the UI does: create it, then trigger AutoPilotApplication with
|
|
1262
|
+
# the service's entityLink. Usage: om_add_service_like_the_ui <token> <service name>
|
|
1263
|
+
om_add_service_like_the_ui() {
|
|
1264
|
+
local auth="Authorization: Bearer $1"
|
|
1265
|
+
curl -fsS -X PUT -H "$auth" -H 'Content-Type: application/json' \
|
|
1266
|
+
-d "{\"name\":\"$2\",\"serviceType\":\"CustomDatabase\",\"connection\":{\"config\":{\"type\":\"CustomDatabase\",\"sourcePythonClass\":\"demo_connector.DemoSource\"}}}" \
|
|
1267
|
+
http://127.0.0.1:8585/api/v1/services/databaseServices >/dev/null || fail "could not create the $2 service"
|
|
1268
|
+
curl -fsS -X POST -H "$auth" -H 'Content-Type: application/json' \
|
|
1269
|
+
-d "{\"entityLink\":\"<#E::databaseService::$2>\"}" \
|
|
1270
|
+
http://127.0.0.1:8585/api/v1/apps/trigger/AutoPilotApplication >/dev/null \
|
|
1271
|
+
|| fail "the server refused the AutoPilot trigger the UI sends"
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
# The number of agents a service has. Usage: om_agent_count <token> <service name>
|
|
1275
|
+
om_agent_count() {
|
|
1276
|
+
curl -fsS -H "Authorization: Bearer $1" "http://127.0.0.1:8585/api/v1/services/ingestionPipelines?service=$2" \
|
|
1277
|
+
| python3 -c 'import json,sys; print(json.load(sys.stdin)["paging"]["total"])'
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
om_delete_service() {
|
|
1281
|
+
curl -s -o /dev/null -X DELETE -H "Authorization: Bearer $1" \
|
|
1282
|
+
"http://127.0.0.1:8585/api/v1/services/databaseServices/name/$2?hardDelete=true&recursive=true"
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
# With AutoPilot off, a service added the way the UI adds one gets no agent. The
|
|
1286
|
+
# check waits 60 seconds, as long as the on-case below allows AutoPilot to create its agent.
|
|
1287
|
+
smoke_metadata_autopilot() {
|
|
1288
|
+
local agents
|
|
1289
|
+
om_add_service_like_the_ui "$1" demo_ui
|
|
1290
|
+
sleep 60
|
|
1291
|
+
agents=$(om_agent_count "$1" demo_ui)
|
|
1292
|
+
om_delete_service "$1" demo_ui
|
|
1293
|
+
[ "$agents" = "0" ] || fail "a service added the way the UI adds one got $agents agents with AutoPilot off"
|
|
1294
|
+
pass "a service added the way the UI adds one, with its AutoPilot trigger, got no agent"
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
# The same two calls with AutoPilot on create at least one agent. It proves
|
|
1298
|
+
# METADATA_AUTOPILOT=true works and that the off-case above could have failed.
|
|
1299
|
+
smoke_metadata_autopilot_on() {
|
|
1300
|
+
local agents=0 _
|
|
1301
|
+
om_add_service_like_the_ui "$1" demo_ui_on
|
|
1302
|
+
for _ in $(seq 1 12); do
|
|
1303
|
+
agents=$(om_agent_count "$1" demo_ui_on)
|
|
1304
|
+
[ "${agents:-0}" -ge 1 ] && break
|
|
1305
|
+
sleep 5
|
|
1306
|
+
done
|
|
1307
|
+
om_delete_service "$1" demo_ui_on
|
|
1308
|
+
[ "${agents:-0}" -ge 1 ] || fail "a service added the way the UI adds one got no agent with AutoPilot on"
|
|
1309
|
+
pass "with METADATA_AUTOPILOT true, a service added the way the UI adds one got $agents agent(s)"
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
# The How It Works example turns AutoPilot on and recreates metadata. While it is on,
|
|
1313
|
+
# check the UI's calls create an agent, then put the key back and check it is off.
|
|
1314
|
+
how_it_works_autopilot() {
|
|
1315
|
+
local out
|
|
1316
|
+
out=$(run_snippet how-it-works/commands.sh metadata-autopilot 2>&1) || { echo "$out" | tail -20; fail "the metadata-autopilot example failed"; }
|
|
1317
|
+
grep -qE -- 'AutoPilot active: true' <<<"$out" || { echo "$out" | tail -20; fail "the metadata-autopilot example printed no 'AutoPilot active: true'"; }
|
|
1318
|
+
pass "How It Works' metadata-autopilot example printed 'AutoPilot active: true'"
|
|
1319
|
+
smoke_metadata_autopilot_on "$(om_token)"
|
|
1320
|
+
mv .odctl/.env.bak .odctl/.env || fail "the metadata-autopilot example left no .env.bak"
|
|
1321
|
+
odctl recreate metadata >/dev/null 2>&1 || fail "could not recreate metadata after the metadata-autopilot example"
|
|
1322
|
+
retry 24 5 om_autopilot_is "$(om_token)" False \
|
|
1323
|
+
|| fail "AutoPilot is not off again after the key was put back"
|
|
1324
|
+
pass "AutoPilot is off again after the key was put back"
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
# Run a connector section until its output has <text>. Usage: om_connector_says <section> <text>
|
|
1328
|
+
om_connector_says() {
|
|
1329
|
+
om_connector_step "$1" 2>/dev/null | grep -qF "$2"
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
# The guide's custom connector steps, run from the folder that holds the file, as
|
|
1333
|
+
# the guide tells the reader. Usage: om_connector_step <section> [extra commands]
|
|
1334
|
+
om_connector_step() {
|
|
1335
|
+
local code
|
|
1336
|
+
code=$(snippet openmetadata/connector/commands.sh "$1")
|
|
1337
|
+
(cd "$CONNECTOR_DIR" && bash -o pipefail -c 'AUTH="Authorization: Bearer ${TOKEN:-}"
|
|
1338
|
+
'"$code"'
|
|
1339
|
+
'"${2:-}")
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
# A custom connector uploaded to s3://openmetadata/connectors is importable in the
|
|
1343
|
+
# ingestion container, and a service and an agent made through the API run it.
|
|
1344
|
+
# Then a changed connector is used by the next run with no restart, which is why
|
|
1345
|
+
# nothing here restarts the container: its start time must not move.
|
|
1346
|
+
smoke_metadata_connector() {
|
|
1347
|
+
local token="$1" auth="Authorization: Bearer $1" started svc pipe out run state agents
|
|
1348
|
+
CONNECTOR_DIR=$(mktemp -d)
|
|
1349
|
+
cp "$SNIPPETS/openmetadata/connector/demo_connector.py" "$CONNECTOR_DIR/"
|
|
1350
|
+
started=$(docker inspect -f '{{.State.StartedAt}}' openmetadata-ingestion)
|
|
1351
|
+
|
|
1352
|
+
om_connector_step upload >/dev/null 2>&1 || fail "could not upload the connector to s3://openmetadata/connectors"
|
|
1353
|
+
retry 12 5 om_connector_says copied demo_connector.py \
|
|
1354
|
+
|| fail "the ingestion container never copied the connector from S3"
|
|
1355
|
+
pass "the connector reached /opt/airflow/connectors in the ingestion container"
|
|
1356
|
+
|
|
1357
|
+
svc=$(TOKEN="$token" om_connector_step service 'echo "$SVC"' 2>/dev/null | tail -1)
|
|
1358
|
+
[ -n "$svc" ] || fail "could not create the CustomDatabase service through the API"
|
|
1359
|
+
out=$(TOKEN="$token" SVC="$svc" om_connector_step agent 'echo; echo "$PIPE"' 2>/dev/null)
|
|
1360
|
+
pipe=$(tail -1 <<<"$out")
|
|
1361
|
+
[ -n "$pipe" ] || fail "could not create the custom connector's agent"
|
|
1362
|
+
grep -q 'has been created' <<<"$out" || { echo "$out"; fail "the custom connector's agent did not deploy a DAG"; }
|
|
1363
|
+
grep -q 'has been triggered' <<<"$out" || om_trigger "$token" "$pipe" || fail "the custom connector's agent never triggered"
|
|
1364
|
+
state=$(om_run_state "$token" om_connector_step "")
|
|
1365
|
+
[ "$state" = "success" ] || fail "the custom connector's run finished in state ${state:-none}, not success"
|
|
1366
|
+
out=$(TOKEN="$token" om_connector_step table 2>/dev/null)
|
|
1367
|
+
grep -q '"fullyQualifiedName":"demo_connector.demo_db.public.orders"' <<<"$out" \
|
|
1368
|
+
&& grep -q '"name":"id"' <<<"$out" && grep -q '"name":"amount"' <<<"$out" \
|
|
1369
|
+
|| fail "the custom connector's table orders, with its columns id and amount, is not in the catalogue"
|
|
1370
|
+
pass "the custom connector's agent ran and its table orders is in the catalogue"
|
|
1371
|
+
|
|
1372
|
+
# A service made through the API starts no AutoPilot agents. The UI does.
|
|
1373
|
+
agents=$(curl -fsS -H "$auth" "http://127.0.0.1:8585/api/v1/services/ingestionPipelines?service=demo_connector" \
|
|
1374
|
+
| python3 -c 'import json,sys; print(json.load(sys.stdin)["paging"]["total"])')
|
|
1375
|
+
[ "$agents" = "1" ] || fail "the demo_connector service has $agents agents, not only its own"
|
|
1376
|
+
pass "the demo_connector service has only its own agent"
|
|
1377
|
+
|
|
1378
|
+
# Change the connector, upload it again and trigger the same agent.
|
|
1379
|
+
read -r run _ < <(om_latest_run "$token" om_connector_step)
|
|
1380
|
+
om_connector_step change >/dev/null 2>&1 || fail "could not change the connector"
|
|
1381
|
+
om_connector_step upload >/dev/null 2>&1 || fail "could not upload the changed connector"
|
|
1382
|
+
retry 12 5 om_connector_says copied-change 1 \
|
|
1383
|
+
|| fail "the ingestion container never copied the changed connector"
|
|
1384
|
+
TOKEN="$token" PIPE="$pipe" om_connector_step retrigger 2>/dev/null | grep -q 'has been triggered' \
|
|
1385
|
+
|| fail "the custom connector's agent did not trigger again"
|
|
1386
|
+
state=$(om_run_state "$token" om_connector_step "$run")
|
|
1387
|
+
[ "$state" = "success" ] || fail "the second run finished in state ${state:-none}, not success"
|
|
1388
|
+
TOKEN="$token" om_connector_step changed-table 2>/dev/null \
|
|
1389
|
+
| grep -q '"name":"orders_v2"' || fail "the changed connector's table orders_v2 is not in the catalogue"
|
|
1390
|
+
[ "$(docker inspect -f '{{.State.StartedAt}}' openmetadata-ingestion)" = "$started" ] \
|
|
1391
|
+
|| fail "the ingestion container restarted while the connector changed"
|
|
1392
|
+
pass "the changed connector was used by the next run, with no restart"
|
|
1393
|
+
rm -rf "$CONNECTOR_DIR"
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
# The guide's runnable example needs demo_connector.py in the folder it runs in.
|
|
1397
|
+
smoke_metadata_example() {
|
|
1398
|
+
local out
|
|
1399
|
+
cp "$SNIPPETS/openmetadata/connector/demo_connector.py" .
|
|
1400
|
+
out=$(bash "$SNIPPETS/openmetadata/example.sh" 2>&1) || { rm -f demo_connector.py; echo "$out" | tail -20; fail "the openmetadata example failed"; }
|
|
1401
|
+
rm -f demo_connector.py
|
|
1402
|
+
local want
|
|
1403
|
+
for want in 'autopilot active: False' 'demo_pg ingestion: success' 'demo_connector ingestion: success' \
|
|
1404
|
+
'^custom table: orders$' 'removed demo_pg and demo_connector'; do
|
|
1405
|
+
grep -qE -- "$want" <<<"$out" || { echo "$out" | tail -20; fail "the openmetadata example printed no '$want'"; }
|
|
1406
|
+
done
|
|
1407
|
+
pass "the openmetadata guide's runnable example ran both services and printed its results"
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
# An admin token, from the guide's login step.
|
|
1411
|
+
om_token() {
|
|
1412
|
+
om_step login 'echo "$TOKEN"' 2>/dev/null | tail -1
|
|
1196
1413
|
}
|
|
1197
1414
|
|
|
1198
1415
|
# The OpenMetadata guide's steps share shell variables: login sets TOKEN, the
|
|
@@ -1546,7 +1763,13 @@ case "$PROFILE" in
|
|
|
1546
1763
|
run_example mlflow '"predictions": \[0, 0\]' "$(mlflow_runner) bash" ;;
|
|
1547
1764
|
lineage) smoke_lineage; run_example lineage 'COMPLETED' ;;
|
|
1548
1765
|
telemetry) smoke_telemetry; run_example telemetry '"42"' ;;
|
|
1549
|
-
metadata)
|
|
1766
|
+
metadata)
|
|
1767
|
+
smoke_metadata
|
|
1768
|
+
smoke_metadata_example
|
|
1769
|
+
how_it_works_edit metadata-deps .odctl/.env metadata '^1\.4\.3$'
|
|
1770
|
+
how_it_works_autopilot
|
|
1771
|
+
retry 60 5 http_ok "http://127.0.0.1:8087/api/v2/monitor/health" \
|
|
1772
|
+
|| fail "ingestion Airflow did not answer after the package was put back" ;;
|
|
1550
1773
|
fluss) smoke_fluss; run_example fluss '\| +3 +\| +c +\|' ;;
|
|
1551
1774
|
feast)
|
|
1552
1775
|
smoke_feast
|
|
@@ -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
|
|
@@ -67,7 +67,7 @@ jobs:
|
|
|
67
67
|
- name: Install uv
|
|
68
68
|
# Pinned to a full version on purpose: setup-uv stopped publishing
|
|
69
69
|
# moving major tags at v8, so astral-sh/setup-uv@v10 does not exist.
|
|
70
|
-
uses: astral-sh/setup-uv@v10.3.
|
|
70
|
+
uses: astral-sh/setup-uv@v10.3.2
|
|
71
71
|
|
|
72
72
|
- name: Set up Python
|
|
73
73
|
run: uv python install 3.10
|
|
@@ -48,7 +48,7 @@ jobs:
|
|
|
48
48
|
# A tag runs every profile. A pull request runs only the groups its
|
|
49
49
|
# changed files affect, because the full matrix pulls roughly 10 GB per
|
|
50
50
|
# entry and paying that on every push is not worth the signal.
|
|
51
|
-
- uses: astral-sh/setup-uv@v10.3.
|
|
51
|
+
- uses: astral-sh/setup-uv@v10.3.2
|
|
52
52
|
- name: Choose profile groups
|
|
53
53
|
id: profiles
|
|
54
54
|
run: |
|
|
@@ -109,7 +109,7 @@ jobs:
|
|
|
109
109
|
runs-on: ubuntu-latest
|
|
110
110
|
steps:
|
|
111
111
|
- uses: actions/checkout@v7
|
|
112
|
-
- uses: astral-sh/setup-uv@v10.3.
|
|
112
|
+
- uses: astral-sh/setup-uv@v10.3.2
|
|
113
113
|
with:
|
|
114
114
|
enable-cache: true
|
|
115
115
|
cache-suffix: docs
|
|
@@ -132,7 +132,7 @@ jobs:
|
|
|
132
132
|
- uses: actions/checkout@v7
|
|
133
133
|
with:
|
|
134
134
|
fetch-depth: 0 # mike needs the full history to update the gh-pages branch
|
|
135
|
-
- uses: astral-sh/setup-uv@v10.3.
|
|
135
|
+
- uses: astral-sh/setup-uv@v10.3.2
|
|
136
136
|
- name: Deploy the versioned docs
|
|
137
137
|
run: |
|
|
138
138
|
# Commit as the owner, so the gh-pages history adds no bot contributor.
|
|
@@ -125,7 +125,7 @@ jobs:
|
|
|
125
125
|
- name: Install uv
|
|
126
126
|
# Pinned to a full version on purpose: setup-uv stopped publishing
|
|
127
127
|
# moving major tags at v8, so astral-sh/setup-uv@v10 does not exist.
|
|
128
|
-
uses: astral-sh/setup-uv@v10.3.
|
|
128
|
+
uses: astral-sh/setup-uv@v10.3.2
|
|
129
129
|
|
|
130
130
|
- name: Set up Python
|
|
131
131
|
run: uv python install 3.10
|
odctl-1.3.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# odctl
|
|
2
|
+
|
|
3
|
+
A CLI that starts a local open data stack with Docker Compose profiles: Kafka, Flink, Spark, Iceberg, Trino, ClickHouse, MLflow, Feast, Airflow, Temporal, Marquez, OpenMetadata, Grafana and more.
|
|
4
|
+
|
|
5
|
+
`CONTRIBUTING.md` holds the set-up, the code map, the test commands, the rules for code and docs, how to add a profile or a command, and the release steps. Read it before a change. This file holds only what applies to an agent.
|
|
6
|
+
|
|
7
|
+
## Running the Tests
|
|
8
|
+
|
|
9
|
+
- Run the unit suite, `pytest tests/`, in full once before each commit, never after each step.
|
|
10
|
+
- Run the strict build at most once per commit.
|
|
11
|
+
|
|
12
|
+
## Owner Decisions
|
|
13
|
+
|
|
14
|
+
- Show any Renovate config change to the owner before making it.
|
|
15
|
+
- Real projects find defects the profile e2e tests miss, so run the benchtop projects on a released odctl from time to time, when the owner asks. It is not a release step: each project starts a full compose stack, which takes too long to do at every release.
|
|
16
|
+
|
|
17
|
+
## Git
|
|
18
|
+
|
|
19
|
+
- Commit or push only when asked. Do not create branches unless asked.
|
|
20
|
+
- Merge a bot PR with the owner as author: `gh pr merge <n> --squash --author-email <maintainer email>`. A plain squash merge adds the bot to the contributors list. If a bot commit lands on `main`, rewrite it with the maintainer as author and rename the default branch and back so GitHub recounts contributors. Do this only with the owner's approval: the rewrite changes pushed history and needs a force push, and each force push is approved on its own.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Contributing to odctl
|
|
2
|
+
|
|
3
|
+
This file is for people who change odctl itself: the CLI, its compose files and images, its tests and its documentation. To use odctl, start with the [documentation](https://jaehyeon.me/odctl/). For a change larger than a fix, such as a new profile, open an issue first, so the design is agreed before the work starts. The [roadmap](https://jaehyeon.me/odctl/latest/about/roadmap/) shows what is planned, and [How It Works](https://jaehyeon.me/odctl/latest/how-it-works/) shows how the parts fit together.
|
|
4
|
+
|
|
5
|
+
## Set Up
|
|
6
|
+
|
|
7
|
+
odctl uses [uv](https://docs.astral.sh/uv/) and needs Docker. Clone the repository, install the dependencies and the project in development mode, and install the pre-commit hooks, which run the formatting and type checks on each commit:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
git clone https://github.com/jaehyeon-kim/odctl.git
|
|
11
|
+
cd odctl
|
|
12
|
+
uv sync
|
|
13
|
+
uvx pre-commit install
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Exporting the diagrams also needs draw.io, Chrome and ffmpeg.
|
|
17
|
+
|
|
18
|
+
## Code Map
|
|
19
|
+
|
|
20
|
+
| Path | Contents |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `src/odctl/main.py` | the Typer CLI (`list`, `explain`, `ps`, `info`, `init`, `pull`, `up`, `down`, `logs`, `restart`, `recreate`) |
|
|
23
|
+
| `src/odctl/registry.py`, `resources/registry.yml` | profiles, the services in each, dependencies, ports and usage text |
|
|
24
|
+
| `src/odctl/planner.py`, `docker.py`, `projects.py`, `logs.py`, `workspace.py`, `config.py`, `ui.py` | startup order, Docker Compose calls, `restart` and `recreate`, and `logs`, each with one Compose project per file as `up` runs them, the `.odctl` workspace, settings, terminal output |
|
|
25
|
+
| `src/odctl/resources/compose-*.yml` | one compose file per area (kafka, flink, spark, store, analytics, mlops, orch, obsv, metadata, infra, deps) |
|
|
26
|
+
| `src/odctl/resources/docker/` | odctl's own images (postgres, mlflow, airflow, spark), published to `ghcr.io/jaehyeon-kim/odctl/<name>:<version>` |
|
|
27
|
+
| `src/odctl/resources/deps/` | the `deps` image: connector and engine jars built or downloaded into the `odctl-shared-deps` volume; versions in `versions.env` |
|
|
28
|
+
| `scripts/gen_pages.py` | builds a page per technology and the CLI reference at docs build time, from `registry.yml`, the compose files and `--help`; run directly, it writes `skills/odctl/SKILL.md`, with every command, option and `.env` key, which a test keeps in step |
|
|
29
|
+
| `scripts/local_links.py`, `scripts/llmstxt_sections.py`, `scripts/release_links.py` | MkDocs hooks: clickable local addresses, the `llms.txt` sections from the nav, and links to files on `main` pointed at the release tag `v<version>` |
|
|
30
|
+
| `scripts/diagrams.py`, `docs/assets/diagrams/*.drawio` | exports each draw.io diagram to the PNG, or GIF for steps, that the docs use, and writes a still PNG of each GIF's last frame beside it; `_legend.drawio` holds the shared shapes; `tests/test_diagrams.py` checks the exports and the stills |
|
|
31
|
+
| `docs/images/` | screenshots of the services' web UIs and the terminal |
|
|
32
|
+
| `image/` | the README's diagram and recording, with the recording's still; they stay here because the PyPI page of every released version links them on `main` |
|
|
33
|
+
| `.github/scripts/smoke.sh` | the e2e assertions per profile group, which run the guide snippets and each guide's Runnable Example; `select-profiles.py` chooses the groups |
|
|
34
|
+
| `docs/snippets/` | the code each guide, Getting Started and How It Works show, one folder per page |
|
|
35
|
+
| `skills/odctl/SKILL.md` | the odctl skill for AI assistants, generated by `scripts/gen_pages.py` |
|
|
36
|
+
| `docs/` | MkDocs Material site, versioned with mike; `docs/about/` holds the public Roadmap and the Changelog |
|
|
37
|
+
| `scripts/release_notes.py` | prints one version's Changelog section, the text of its GitHub release |
|
|
38
|
+
|
|
39
|
+
## Tests
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv run pytest tests/ -q
|
|
43
|
+
uvx pre-commit run --all-files # ruff, ruff format, mypy, whitespace, YAML, TOML
|
|
44
|
+
DISABLE_MKDOCS_2_WARNING=true uv run mkdocs build --strict
|
|
45
|
+
uv run mkdocs serve -a 127.0.0.1:8000 # docs at http://127.0.0.1:8000/odctl/
|
|
46
|
+
uv run python scripts/gen_pages.py # regenerate skills/odctl/SKILL.md
|
|
47
|
+
uv run python scripts/diagrams.py [name ...] # export diagrams; needs draw.io, Chrome and ffmpeg
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Each layer checks one thing:
|
|
51
|
+
|
|
52
|
+
- `tests/` checks the CLI, the registry, the compose files, the docs and the generated pages, with no container running.
|
|
53
|
+
- The strict docs build fails on a broken link or anchor.
|
|
54
|
+
- The e2e tests start each profile group and run `.github/scripts/smoke.sh` on it. A running container is not enough: the script checks that the profile does its job, such as a Kafka consumer reading a message or a Flink job committing to Iceberg. It also runs each guide's snippets and Runnable Example.
|
|
55
|
+
|
|
56
|
+
Run the unit suite in full once before each commit. While fixing, run only the failed tests (`pytest --lf`) and the tests of the files you changed. Run the strict build when `docs/`, `mkdocs.yml` or an input of `scripts/gen_pages.py` changed: `registry.yml`, a compose file, or a command's `--help` text.
|
|
57
|
+
|
|
58
|
+
A pull request runs pre-commit, the unit tests on each supported Python version, the strict docs build, and the image builds when an image input changed. e2e runs only on a tag and on a manual run, because odctl runs images tagged with its own version, and the images of an unreleased version do not exist yet. To check a profile on your machine, do what the e2e job does: install the CLI with `uv tool install .`, run `odctl init`, start the profile with `odctl up <profile>`, then run `.github/scripts/smoke.sh <profile>`. Run one `odctl up` at a time. When no image definition changed since the last release, you can give the last release's images the new version's tag for the test, and remove those tags afterwards.
|
|
59
|
+
|
|
60
|
+
## Rules
|
|
61
|
+
|
|
62
|
+
- **Images follow the CLI version.** odctl runs its own images at its own version (`TAG`), never `latest`: Docker does not pull a tag it already has, so a cached `latest` keeps running old code. Third-party images are pinned to a version and updated by Renovate.
|
|
63
|
+
- **Bind to 127.0.0.1.** Every published port binds to `127.0.0.1`, not all interfaces.
|
|
64
|
+
- **One profile per tool**, with `-lite` and `-full` variants where a tool has a small and a clustered form (`kafka-lite`, `kafka-full`).
|
|
65
|
+
- **Fixed container names.** Every service has a `container_name`, so `odctl up` cannot scale a service; several copies are separate named services (Flink's `taskmanager-a` and `taskmanager-b`).
|
|
66
|
+
- **A feature merges only with its docs.** Each command, option, profile and `.env` key needs a tested example on a guide, Getting Started or How It Works, a reference entry (the CLI reference, a profile page or How It Works), and a place in the skill; a command also needs a line in the README's Commands section. `test_every_feature_has_its_docs` checks this, and its short list of exceptions gives a reason for each.
|
|
67
|
+
- **Renovate** opens its pull requests on Mondays, with image updates grouped by compose file. Agree a change to its configuration (`.github/renovate.json`) with the maintainer first.
|
|
68
|
+
|
|
69
|
+
## Add a Profile
|
|
70
|
+
|
|
71
|
+
1. Add the services to the compose file of their area in `src/odctl/resources/`, following the Rules: a pinned image, a `container_name`, and ports bound to `127.0.0.1`.
|
|
72
|
+
2. Add the profile to `src/odctl/resources/registry.yml`: its compose file, description, profiles, dependencies, role and usage text. `scripts/gen_pages.py` builds its page from this entry and the compose file.
|
|
73
|
+
3. Add its checks to `.github/scripts/smoke.sh`, and its group to `GROUPS` in `.github/scripts/select-profiles.py`.
|
|
74
|
+
4. Add the technology to the README diagram, `image/diagram.drawio`, then export it. `test_readme_diagram_names_every_technology` fails until you do.
|
|
75
|
+
5. Give it a tested example on a guide, Getting Started or How It Works, then run `scripts/gen_pages.py` to update the skill.
|
|
76
|
+
|
|
77
|
+
## Add a Command or Option
|
|
78
|
+
|
|
79
|
+
Add it to `src/odctl/main.py` with its `--help` text, which the CLI reference is built from. Give it a tested example on a guide, Getting Started or How It Works, and a command a line in the README's Commands section. Then run `scripts/gen_pages.py` to update the skill.
|
|
80
|
+
|
|
81
|
+
## Write the Docs
|
|
82
|
+
|
|
83
|
+
- **Generated pages.** The profile pages and the CLI reference are generated by `scripts/gen_pages.py` during the build, from `registry.yml`, the compose files and the commands. Change those, not the pages, and do not copy versions or ports into hand-written guides.
|
|
84
|
+
- **Code blocks are copies.** Guide code lives in `docs/snippets/<guide>/`, one section per step (`# --8<-- [start:name]`), and Getting Started and How It Works follow the same rule. A block labelled `title="path:section"` or `title="path"` is a copy of that section or file, checked by `tests/test_docs.py`. Pages use copies, not `--8<--` includes, so the Markdown reads correctly on GitHub. Every guide ends with a Runnable Example, `example.sh` or `example.py` in its folder. `smoke.sh` runs the same sections and examples, and `select-profiles.py` maps each snippet folder to the e2e groups that run it.
|
|
85
|
+
- **Guides.** Each guide says which pages it builds on, the output of every step, and where a value it quotes is set. A page has at most 10 `##` headings and 15 in all.
|
|
86
|
+
- **Writing.** Plain English for readers whose first language is not English. Headings use title case, keeping product names such as `odctl` as they are, and do not start with "The". No em or en dashes. Each Markdown paragraph is one line, never hard-wrapped.
|
|
87
|
+
- **Diagrams.** Copy shapes from `docs/assets/diagrams/_legend.drawio`, and export with `scripts/diagrams.py`.
|
|
88
|
+
|
|
89
|
+
## Pull Requests
|
|
90
|
+
|
|
91
|
+
Work on a branch from `main` and open a pull request against `main`. Before you push, run pre-commit, the unit tests, and the strict build when the docs changed. Describe each change to ports, profiles, `.env` keys, volumes or workspace files in the pull request, so that the release can add an Upgrading line to the Changelog (`docs/about/changelog.md`).
|
|
92
|
+
|
|
93
|
+
## Releases
|
|
94
|
+
|
|
95
|
+
The maintainer releases. These steps are written down so that each release is the same:
|
|
96
|
+
|
|
97
|
+
1. Bump `version` in `pyproject.toml`, run `uv lock`, and add the release to `docs/about/changelog.md`, with an Upgrading line for any change to ports, profiles, `.env` keys, volumes or workspace files. When it reaches `main`, the docs deploy as that version, aliased `latest`.
|
|
98
|
+
2. Once the `main` pipeline for that commit is green, push tag `vX.Y.Z` on it. A tag on a commit whose checks fail builds no images and cannot be released until it is moved. The tag pipeline builds and pushes the `X.Y.Z` images, then runs e2e for every profile group (about an hour).
|
|
99
|
+
3. Publish the GitHub release with that version's Changelog section as its text, so the two never differ: `python scripts/release_notes.py X.Y.Z > /tmp/notes.md`, then `gh release create vX.Y.Z --title vX.Y.Z --notes-file /tmp/notes.md`. `publish.yml` waits for a green tag pipeline and checks that every `X.Y.Z` image is in ghcr, then uploads to PyPI.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: odctl
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Run an open data stack on your machine, with its services already connected, from one command.
|
|
5
5
|
Project-URL: Homepage, https://github.com/jaehyeon-kim/odctl
|
|
6
6
|
Project-URL: Repository, https://github.com/jaehyeon-kim/odctl
|
|
@@ -106,30 +106,9 @@ Projects that use odctl:
|
|
|
106
106
|
- [desflow](https://github.com/jaehyeon-kim/desflow) ([docs](https://jaehyeon.me/desflow/)): a SimPy library for simulations that stream to Kafka, PostgreSQL, Redis and Iceberg; its examples and integration tests run their infrastructure with odctl.
|
|
107
107
|
- [agentic-analytics-system](https://github.com/jaehyeon-kim/agentic-analytics-system): conversational analytics over an Iceberg lakehouse, with Trino, Iceberg and object storage started by odctl.
|
|
108
108
|
|
|
109
|
-
##
|
|
110
|
-
|
|
111
|
-
Pull requests are welcome.
|
|
112
|
-
|
|
113
|
-
1. Clone the repository and install [uv](https://docs.astral.sh/uv/).
|
|
114
|
-
2. Install the dependencies and the project in development mode:
|
|
115
|
-
```bash
|
|
116
|
-
uv sync
|
|
117
|
-
```
|
|
118
|
-
3. Install the pre-commit hooks, which run the formatting and type checks:
|
|
119
|
-
```bash
|
|
120
|
-
uvx pre-commit install
|
|
121
|
-
```
|
|
122
|
-
4. Run the tests:
|
|
123
|
-
```bash
|
|
124
|
-
uv run pytest tests/
|
|
125
|
-
```
|
|
126
|
-
5. Build the documentation site, then serve it while you edit. It is served at `http://127.0.0.1:8000/odctl/`:
|
|
127
|
-
```bash
|
|
128
|
-
uv run mkdocs build --strict
|
|
129
|
-
uv run mkdocs serve
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
The profile pages and the CLI reference are generated by `scripts/gen_pages.py` during the build, so edit `registry.yml`, the compose files or the commands rather than those pages.
|
|
109
|
+
## Contributing
|
|
110
|
+
|
|
111
|
+
Pull requests are welcome. [CONTRIBUTING.md](https://github.com/jaehyeon-kim/odctl/blob/main/CONTRIBUTING.md) explains how to set up the repository, run the tests, add a profile or a command, and write the docs.
|
|
133
112
|
|
|
134
113
|
## License
|
|
135
114
|
|
|
@@ -69,30 +69,9 @@ Projects that use odctl:
|
|
|
69
69
|
- [desflow](https://github.com/jaehyeon-kim/desflow) ([docs](https://jaehyeon.me/desflow/)): a SimPy library for simulations that stream to Kafka, PostgreSQL, Redis and Iceberg; its examples and integration tests run their infrastructure with odctl.
|
|
70
70
|
- [agentic-analytics-system](https://github.com/jaehyeon-kim/agentic-analytics-system): conversational analytics over an Iceberg lakehouse, with Trino, Iceberg and object storage started by odctl.
|
|
71
71
|
|
|
72
|
-
##
|
|
73
|
-
|
|
74
|
-
Pull requests are welcome.
|
|
75
|
-
|
|
76
|
-
1. Clone the repository and install [uv](https://docs.astral.sh/uv/).
|
|
77
|
-
2. Install the dependencies and the project in development mode:
|
|
78
|
-
```bash
|
|
79
|
-
uv sync
|
|
80
|
-
```
|
|
81
|
-
3. Install the pre-commit hooks, which run the formatting and type checks:
|
|
82
|
-
```bash
|
|
83
|
-
uvx pre-commit install
|
|
84
|
-
```
|
|
85
|
-
4. Run the tests:
|
|
86
|
-
```bash
|
|
87
|
-
uv run pytest tests/
|
|
88
|
-
```
|
|
89
|
-
5. Build the documentation site, then serve it while you edit. It is served at `http://127.0.0.1:8000/odctl/`:
|
|
90
|
-
```bash
|
|
91
|
-
uv run mkdocs build --strict
|
|
92
|
-
uv run mkdocs serve
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
The profile pages and the CLI reference are generated by `scripts/gen_pages.py` during the build, so edit `registry.yml`, the compose files or the commands rather than those pages.
|
|
72
|
+
## Contributing
|
|
73
|
+
|
|
74
|
+
Pull requests are welcome. [CONTRIBUTING.md](https://github.com/jaehyeon-kim/odctl/blob/main/CONTRIBUTING.md) explains how to set up the repository, run the tests, add a profile or a command, and write the docs.
|
|
96
75
|
|
|
97
76
|
## License
|
|
98
77
|
|
|
@@ -418,6 +418,8 @@ ENV_KEYS = {
|
|
|
418
418
|
"TZ": "The time zone of the services that read it, `UTC` by default.",
|
|
419
419
|
"_AIRFLOW_PIP_DEPS": "Extra Python packages installed in every new Airflow container.",
|
|
420
420
|
"_MLOPS_PIP_DEPS": "Extra Python packages installed in every new `mlflow` and `mlflow-serve` container.",
|
|
421
|
+
"_METADATA_PIP_DEPS": "Extra Python packages installed in every new `openmetadata-ingestion` container.",
|
|
422
|
+
"METADATA_AUTOPILOT": "`true` turns on OpenMetadata's AutoPilot, which creates and runs default agents for a service added in the UI; it is `false` by default.",
|
|
421
423
|
"MODEL_URI": "The model `mlflow-serve` serves, such as `models:/<name>@<alias>`; empty means it waits idle.",
|
|
422
424
|
}
|
|
423
425
|
|