fhan688 commented on code in PR #20419:
URL: https://github.com/apache/hudi/pull/20419#discussion_r4387192598
##########
hudi-ai-operator/skills/hudi-architect/validate_flink_design.py:
##########
@@ -382,11 +408,620 @@ def _sql_string(value: str) -> str:
return "'" + value.replace("'", "''") + "'"
+def _has_sensitive_catalog_key(value: Any) -> bool:
+ if isinstance(value, dict):
+ return any(
+ SENSITIVE_CATALOG_KEY.search(str(key)) is not None
+ or _has_sensitive_catalog_key(item)
+ for key, item in value.items()
+ )
+ if isinstance(value, list):
+ return any(_has_sensitive_catalog_key(item) for item in value)
+ return False
+
+
+def _metastore_uris_valid(value: Any) -> bool:
+ if not _check_concrete_value(value):
+ return False
+ for raw_uri in value.split(","):
+ uri = raw_uri.strip()
+ try:
+ parsed = urlsplit(uri)
+ except ValueError:
+ return False
+ if (
+ parsed.scheme.lower() != "thrift"
+ or not parsed.hostname
+ or parsed.username is not None
+ or parsed.password is not None
+ or parsed.query
+ or parsed.fragment
+ ):
+ return False
+ return True
+
+
+def _metastore_uris_have_credentials(value: Any) -> bool:
+ if not isinstance(value, str):
+ return False
+ for raw_uri in value.split(","):
+ try:
+ parsed = urlsplit(raw_uri.strip())
+ except ValueError:
+ continue
+ if parsed.username is not None or parsed.password is not None:
+ return True
+ parameters = [
+ *parse_qsl(parsed.query, keep_blank_values=True),
+ *parse_qsl(parsed.fragment, keep_blank_values=True),
+ ]
+ if any(_is_sensitive_query_key(key) for key, _ in parameters):
+ return True
+ return False
+
+
+def _catalog_profile_id(resolution_mode: Any, external_discovery: Any) -> str
| None:
+ if not isinstance(resolution_mode, str) or not
isinstance(external_discovery, str):
+ return None
+ profiles = {
+ ("direct_sql", "none"): "direct_sql_none",
+ ("direct_sql", "hive_metastore"): "direct_sql_hive_metastore",
+ ("hudi_dfs_catalog", "none"): "hudi_dfs_catalog_none",
+ ("hudi_dfs_catalog", "hive_metastore"):
+ "hudi_dfs_catalog_hive_metastore",
+ ("hudi_hms_catalog", "hive_metastore"):
+ "hudi_hms_catalog_hive_metastore",
+ }
+ return profiles.get((resolution_mode, external_discovery))
+
+
+def _mor_sync_strategy(contract: dict[str, Any]) -> str | None:
+ raw_read = contract.get("read")
+ read = raw_read if isinstance(raw_read, dict) else {}
+ raw_modes = read.get("query_modes")
+ modes = (
+ set(raw_modes)
+ if isinstance(raw_modes, list)
+ and all(isinstance(mode, str) for mode in raw_modes)
+ else set()
+ )
+ if modes == {"snapshot"}:
+ return "RT"
+ if modes == {"read_optimized"}:
+ return "RO"
+ if modes == {"snapshot", "read_optimized"}:
+ return "ALL"
+ return None
+
+
+def _registered_table_names(
+ table_name: Any, table_type: Any, strategy: str | None
+) -> list[str]:
+ if not _non_empty_string(table_name):
+ return []
+ base_name = table_name.split(".")[-1]
+ if table_type != "MERGE_ON_READ":
+ return [base_name]
+ if strategy == "RO":
+ return [base_name]
+ if strategy == "RT":
+ return [base_name]
+ if strategy == "ALL":
+ return [f"{base_name}_ro", f"{base_name}_rt", base_name]
+ return []
+
+
+def _assess_catalog(
+ contract: dict[str, Any],
+ manifest: dict[str, Any],
+ assessment: Assessment,
+ *,
+ external_catalog: Any,
+ expected_table_lifecycle: str,
+) -> dict[str, Any]:
+ """Validate the orthogonal PR6 catalog contract without selecting it."""
+
+ explicit = "catalog" in contract
+ raw_catalog = contract.get("catalog")
+ if not explicit:
+ if external_catalog is True:
+ assessment.add(
+ "FLINK_CATALOG_CONTRACT_REQUIRED",
+ "safety.external_catalog=true requires an explicit catalog
contract",
+ )
+ legacy_profile = external_catalog is False
+ resolution = "direct_sql" if legacy_profile else "unresolved"
+ discovery = "none" if legacy_profile else "unresolved"
+ normalized_sync_owner = "none" if legacy_profile else "unresolved"
+ normalized_lifecycle_owner = "application" if legacy_profile else
"unresolved"
+ return {
+ "contract_schema": CATALOG_CONTRACT_SCHEMA_VERSION,
+ "profile": "direct_sql_none" if legacy_profile else "unresolved",
+ "resolution_mode": resolution,
+ "external_discovery": discovery,
+ "sync_owner": normalized_sync_owner,
+ "data_lifecycle_owner": normalized_lifecycle_owner,
+ "catalog_setup_sql": "",
+ "effective_configuration": {
+ "catalog_freshness": "not_applicable",
+ "data_lifecycle_owner": normalized_lifecycle_owner,
+ "external_discovery": discovery,
+ "resolution_mode": resolution,
+ "sync_owner": normalized_sync_owner,
+ },
+ "deployment_checks": [],
+ }
+
+ if isinstance(raw_catalog, dict):
+ catalog = raw_catalog
+ else:
+ assessment.add(
+ "FLINK_CATALOG_CONTRACT_INVALID", "catalog must be an object"
+ )
+ catalog = {}
+ _check_catalog_keys(
+ catalog,
+ {
+ "catalog_name",
+ "catalog_path",
+ "contract_schema",
+ "data_lifecycle_owner",
+ "database",
+ "external_discovery",
+ "hive_configuration",
+ "mor_sync_strategy",
+ "resolution_mode",
+ "sync_owner",
+ "table_external",
+ },
+ "catalog",
+ assessment,
+ )
+ if _has_sensitive_catalog_key(catalog):
+ assessment.add(
+ "FLINK_CATALOG_CREDENTIAL_REJECTED",
+ "Catalog contracts must reference credential-free deployment
configuration",
+ )
+ if catalog.get("contract_schema") != CATALOG_CONTRACT_SCHEMA_VERSION:
+ assessment.add(
+ "FLINK_CATALOG_CONTRACT_INVALID",
+ f"catalog.contract_schema must be
{CATALOG_CONTRACT_SCHEMA_VERSION}",
+ )
+
+ resolution_mode = catalog.get("resolution_mode")
+ external_discovery = catalog.get("external_discovery")
+ sync_owner = catalog.get("sync_owner")
+ lifecycle_owner = catalog.get("data_lifecycle_owner")
+ resolution_modes = {
+ "direct_sql",
+ "hudi_dfs_catalog",
+ "hudi_hms_catalog",
+ }
+ discovery_modes = {"none", "hive_metastore", "aws_glue"}
+ sync_owners = {"none", "flink_sink", "hudi_hms_catalog"}
+ lifecycle_owners = {"application", "catalog"}
+ resolution_valid = isinstance(resolution_mode, str) and resolution_mode in
resolution_modes
+ discovery_valid = (
+ isinstance(external_discovery, str)
+ and external_discovery in discovery_modes
+ )
+ sync_owner_valid = isinstance(sync_owner, str) and sync_owner in
sync_owners
+ lifecycle_owner_valid = (
+ isinstance(lifecycle_owner, str) and lifecycle_owner in
lifecycle_owners
+ )
+ if not _non_empty_string(resolution_mode):
+ assessment.add(
+ "FLINK_CATALOG_CONTRACT_INVALID",
+ "catalog.resolution_mode must be explicit",
+ )
+ elif not resolution_valid:
+ assessment.add(
+ "FLINK_CATALOG_MODE_UNSUPPORTED",
+ "catalog.resolution_mode is outside the Hudi 1.2.0 PR6 profiles",
+ )
+ if not _non_empty_string(external_discovery):
+ assessment.add(
+ "FLINK_CATALOG_CONTRACT_INVALID",
+ "catalog.external_discovery must be explicit",
+ )
+ elif not discovery_valid:
+ assessment.add(
+ "FLINK_CATALOG_MODE_UNSUPPORTED",
+ "catalog.external_discovery is unsupported",
+ )
+ if not _non_empty_string(sync_owner):
+ assessment.add(
+ "FLINK_CATALOG_CONTRACT_INVALID",
+ "catalog.sync_owner must be explicit",
+ )
+ elif not sync_owner_valid:
+ assessment.add(
+ "FLINK_CATALOG_MODE_UNSUPPORTED", "catalog.sync_owner is
unsupported"
+ )
+ if not _non_empty_string(lifecycle_owner):
+ assessment.add(
+ "FLINK_CATALOG_CONTRACT_INVALID",
+ "catalog.data_lifecycle_owner must be explicit",
+ )
+ elif not lifecycle_owner_valid:
+ assessment.add(
+ "FLINK_CATALOG_MODE_UNSUPPORTED",
+ "catalog.data_lifecycle_owner is unsupported",
+ )
+ if external_discovery == "aws_glue":
+ assessment.add(
+ "FLINK_GLUE_CATALOG_UNVERIFIED",
+ "AWS Glue is present in Hudi 1.2.0 but is not executable without a
"
+ "Flink-specific fixture",
+ )
+
+ profile = _catalog_profile_id(resolution_mode, external_discovery)
+ if profile is None:
+ if external_discovery == "aws_glue":
+ profile = "aws_glue_unverified"
+ elif resolution_valid and discovery_valid:
+ assessment.add(
+ "FLINK_CATALOG_COMPOSITION_CONFLICT",
+ "The requested table-resolution and external-discovery
composition is unsupported",
+ )
+ profile = "unsupported"
+ else:
+ profile = "unresolved"
+
+ expected_external_gate = (
+ resolution_mode != "direct_sql" or external_discovery != "none"
+ )
+ if (
+ resolution_valid
+ and discovery_valid
+ and isinstance(external_catalog, bool)
+ and external_catalog != expected_external_gate
+ ):
+ assessment.add(
+ "FLINK_CATALOG_COMPOSITION_CONFLICT",
+ "safety.external_catalog conflicts with the explicit catalog
composition",
+ )
+
+ expected_owners = {
+ "direct_sql_none": ("none", "application"),
+ "direct_sql_hive_metastore": ("flink_sink", "application"),
+ "hudi_dfs_catalog_none": ("none", "catalog"),
+ "hudi_dfs_catalog_hive_metastore": ("flink_sink", "catalog"),
+ "hudi_hms_catalog_hive_metastore": ("hudi_hms_catalog", "application"),
+ }
+ if profile in expected_owners:
+ expected_sync_owner, expected_lifecycle_owner =
expected_owners[profile]
+ if sync_owner_valid and sync_owner != expected_sync_owner:
+ assessment.add(
+ "FLINK_CATALOG_COMPOSITION_CONFLICT",
+ f"{profile} requires sync_owner={expected_sync_owner}",
+ )
+ if not _non_empty_string(lifecycle_owner):
+ assessment.add(
+ "FLINK_CATALOG_LIFECYCLE_UNRESOLVED",
+ f"{profile} requires an explicit data lifecycle owner",
+ )
+ elif lifecycle_owner_valid and lifecycle_owner !=
expected_lifecycle_owner:
+ code = (
+ "FLINK_CATALOG_LIFECYCLE_UNRESOLVED"
+ if resolution_mode == "hudi_dfs_catalog"
+ else "FLINK_CATALOG_COMPOSITION_CONFLICT"
+ )
+ assessment.add(
+ code,
+ f"{profile} requires
data_lifecycle_owner={expected_lifecycle_owner}",
+ )
+
+ catalog_name = catalog.get("catalog_name")
+ database = catalog.get("database")
+ catalog_path = catalog.get("catalog_path")
+ if resolution_mode == "direct_sql":
+ if "catalog_name" in catalog or "catalog_path" in catalog or
"table_external" in catalog:
+ assessment.add(
+ "FLINK_CATALOG_CONTRACT_INVALID",
+ "Direct SQL forbids catalog_name, catalog_path, and
table_external",
+ )
+ else:
+ if not _check_concrete_value(catalog_name):
+ assessment.add(
+ "FLINK_CATALOG_CONTRACT_INVALID",
+ "Catalog resolution requires a concrete catalog_name",
+ )
+ elif re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", catalog_name) is None:
+ assessment.add(
+ "FLINK_CATALOG_CONTRACT_INVALID",
+ "catalog.catalog_name must be a simple Flink SQL identifier",
+ )
+ if resolution_mode != "direct_sql" or external_discovery ==
"hive_metastore":
+ if not _check_concrete_value(database):
+ assessment.add(
+ "FLINK_CATALOG_CONTRACT_INVALID",
+ "Catalog or Hive metastore composition requires a concrete
database",
+ )
+ elif re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", database) is None:
+ assessment.add(
+ "FLINK_CATALOG_CONTRACT_INVALID",
+ "catalog.database must be a simple Flink SQL identifier",
+ )
+ elif "database" in catalog:
+ assessment.add(
+ "FLINK_CATALOG_CONTRACT_INVALID",
+ "catalog.database is not applicable to direct SQL without external
discovery",
+ )
+
+ raw_table = contract.get("table")
+ table: dict[str, Any] = raw_table if isinstance(raw_table, dict) else {}
+ table_name = table.get("name")
+ table_path = table.get("path")
+ if resolution_mode in {"hudi_dfs_catalog", "hudi_hms_catalog"} and (
Review Comment:
Thanks for catching this. Resolution-mode membership checks are now
type-guarded, so malformed non-string values produce deterministic validation
findings instead of raising a `TypeError`. Regression cases have also been
added.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]