blob: 81df370957c46917fa5bd52f584b389be506e395 [file]
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# !/usr/bin/python
"""
Comprehensive connector registration completeness check.
Verifies that every new connector module is registered in ALL required configuration
files. A connector that misses even one of these registrations will be silently broken:
not distributed, not discoverable by name, and not installable via install-plugin.sh.
Registration points checked (all are ERRORS):
1. seatunnel-connectors-v2/pom.xml -- Maven module (required for compile/build)
2. seatunnel-dist/pom.xml <scope>provided> -- Dist packaging (connectors/ directory)
3. plugin-mapping.properties -- Runtime connector-name -> jar resolution
4. config/plugin_config -- install-plugin.sh downloadable list
Modes of operation:
--base-ref <ref> CI diff mode: check only connectors newly added against <ref>.
Use this in CI via GITHUB_BASE_REF. Avoids false positives from
pre-existing historical omissions in the repository.
(no arg) Static mode: check all connector modules. Some checks may report
pre-existing omissions that are known and intentional.
Usage:
python tools/check_connector_registration.py
python tools/check_connector_registration.py --base-ref origin/dev
python tools/check_connector_registration.py --base-ref upstream/dev
Exit codes:
0 - All checked connectors pass all critical registration checks
1 - One or more critical registrations are missing
"""
import argparse
import os
import re
import subprocess
import sys
# ---------------------------------------------------------------------------
# Exclusion sets: modules that are intentionally NOT standalone connectors
# and therefore do NOT need all registration entries.
# ---------------------------------------------------------------------------
# Base/utility modules that are never deployed as standalone connectors.
# These lack factory implementations and are dependencies of real connectors.
EXCLUDED_FROM_ALL = {
"connector-common",
"connector-file-base",
"connector-file-base-hadoop",
"connector-cdc-base",
}
# Connectors bundled into the seatunnel-dist core jar (always available,
# no separate install-plugin.sh download needed).
# These appear in seatunnel-dist/pom.xml but not in config/plugin_config.
BUNDLED_IN_DIST = {
"connector-console",
"connector-fake",
"connector-assert",
"connector-socket",
}
# ---------------------------------------------------------------------------
# Parsing helpers
# ---------------------------------------------------------------------------
def get_artifact_id(pom_path):
"""
Extract the project-level artifactId from a pom.xml file.
Strips the <parent> block first to avoid matching the parent artifactId.
"""
with open(pom_path, "r", encoding="utf-8") as f:
content = f.read()
# Remove <parent> block so we only match the project-level artifactId
content_no_parent = re.sub(r"<parent>.*?</parent>", "", content, flags=re.DOTALL)
m = re.search(r"<artifactId>\s*([^\s<]+)\s*</artifactId>", content_no_parent)
return m.group(1).strip() if m else None
def find_all_connector_modules(connectors_dir):
"""
Walk seatunnel-connectors-v2/ and return a dict of {artifactId -> abs_dir_path}
for every connector module that has both a pom.xml and a src/ directory.
Excludes modules in EXCLUDED_FROM_ALL.
"""
modules = {}
for root, dirs, files in os.walk(connectors_dir):
# Avoid descending into build artifacts
dirs[:] = [d for d in dirs if d != "target"]
if "pom.xml" not in files:
continue
if not os.path.isdir(os.path.join(root, "src")):
continue
artifact_id = get_artifact_id(os.path.join(root, "pom.xml"))
if (
artifact_id
and artifact_id.startswith("connector-")
and artifact_id not in EXCLUDED_FROM_ALL
):
modules[artifact_id] = root
return modules
def get_new_connector_modules_from_diff(repo_root, base_ref, connectors_dir):
"""
Return {artifactId -> abs_dir_path} for connector modules whose pom.xml was
ADDED (not modified) in the diff between base_ref and HEAD.
Only top-level connector directories (depth=1 under seatunnel-connectors-v2/)
are considered as "new" connectors; subdirectory additions within an existing
connector are not treated as new standalone connectors.
"""
try:
result = subprocess.run(
["git", "diff", "--name-only", "--diff-filter=A", f"{base_ref}...HEAD"],
capture_output=True,
text=True,
cwd=repo_root,
check=True,
)
except subprocess.CalledProcessError as e:
print(
f"WARNING: git diff failed (base_ref={base_ref}): {e}. "
"Falling back to static full scan."
)
return None
new_modules = {}
# Match exactly: seatunnel-connectors-v2/connector-<name>/pom.xml (no deeper path)
pattern = re.compile(
r"seatunnel-connectors-v2/(connector-[^/]+)/pom\.xml$"
)
for line in result.stdout.strip().splitlines():
m = pattern.match(line)
if not m:
continue
connector_dir = m.group(1)
abs_pom = os.path.join(repo_root, "seatunnel-connectors-v2", connector_dir, "pom.xml")
if not os.path.isfile(abs_pom):
continue
artifact_id = get_artifact_id(abs_pom)
if (
artifact_id
and artifact_id.startswith("connector-")
and artifact_id not in EXCLUDED_FROM_ALL
):
new_modules[artifact_id] = os.path.join(
repo_root, "seatunnel-connectors-v2", connector_dir
)
return new_modules
# ---------------------------------------------------------------------------
# Per-registration-point checkers
# ---------------------------------------------------------------------------
def is_module_declared_in_parent_pom(connector_abs_dir):
"""
Return True if the connector's parent directory has a pom.xml that declares
this connector as a <module>. This handles both top-level connectors
(parent = seatunnel-connectors-v2/) and nested sub-connectors
(parent = seatunnel-connectors-v2/connector-cdc/, etc.).
"""
parent_dir = os.path.dirname(connector_abs_dir)
parent_pom = os.path.join(parent_dir, "pom.xml")
if not os.path.isfile(parent_pom):
return False
connector_dir_name = os.path.basename(connector_abs_dir)
with open(parent_pom, "r", encoding="utf-8") as f:
content = f.read()
# Match <module>connector-dir-name</module> (exact name, may have whitespace)
return bool(
re.search(
r"<module>\s*" + re.escape(connector_dir_name) + r"\s*</module>",
content,
)
)
def build_dist_registered(dist_pom_path):
"""
Return set of artifactIds with scope=provided in seatunnel-dist/pom.xml.
The assembly-bin-ci.xml collects all scope=provided connector-* dependencies
from this pom into the connectors/ directory of the dist package.
"""
with open(dist_pom_path, "r", encoding="utf-8") as f:
content = f.read()
# Find all <dependency> blocks that contain scope=provided and a connector- artifactId
registered = set()
for dep_block in re.findall(r"<dependency>.*?</dependency>", content, re.DOTALL):
if "<scope>provided</scope>" not in dep_block:
continue
m = re.search(r"<artifactId>\s*(connector-[^\s<]+)\s*</artifactId>", dep_block)
if m:
registered.add(m.group(1).strip())
return registered
def build_plugin_mapping_jars(plugin_mapping_path):
"""
Return set of jar names (connector-*) that appear as VALUES in plugin-mapping.properties.
e.g. 'seatunnel.source.Kafka = connector-kafka' → 'connector-kafka'
"""
with open(plugin_mapping_path, "r", encoding="utf-8") as f:
content = f.read()
return set(re.findall(r"=\s*(connector-[^\s#\n]+)", content))
def build_plugin_config_set(plugin_config_path):
"""Return set of connector names listed in config/plugin_config."""
names = set()
with open(plugin_config_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line.startswith("connector-"):
names.add(line)
return names
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def parse_args():
parser = argparse.ArgumentParser(
description="Check connector registration completeness."
)
parser.add_argument(
"--base-ref",
metavar="REF",
default=os.environ.get("GITHUB_BASE_REF", ""),
help=(
"Git ref to diff against (e.g. 'origin/dev'). "
"When set, only newly added connectors are checked. "
"Defaults to GITHUB_BASE_REF env var."
),
)
return parser.parse_args()
def main():
args = parse_args()
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
connectors_dir = os.path.join(repo_root, "seatunnel-connectors-v2")
dist_pom = os.path.join(repo_root, "seatunnel-dist", "pom.xml")
plugin_mapping = os.path.join(repo_root, "plugin-mapping.properties")
plugin_config = os.path.join(repo_root, "config", "plugin_config")
# --- Validate required files exist ---
for path, label in [
(connectors_dir, "seatunnel-connectors-v2/"),
(dist_pom, "seatunnel-dist/pom.xml"),
(plugin_mapping, "plugin-mapping.properties"),
(plugin_config, "config/plugin_config"),
]:
if not os.path.exists(path):
print(f"ERROR: Required file not found: {path}")
sys.exit(1)
# --- Determine which connectors to check ---
base_ref = args.base_ref.strip()
diff_mode = False
if base_ref:
# CI diff mode: only check connectors newly added in this PR
# Prefix with 'origin/' if it looks like a plain branch name and not already prefixed
if "/" not in base_ref:
base_ref = "origin/" + base_ref
print(f"CI diff mode: checking connectors newly added against {base_ref}")
connectors_to_check = get_new_connector_modules_from_diff(
repo_root, base_ref, connectors_dir
)
if connectors_to_check is None:
# git diff failed; fall back to static mode
connectors_to_check = find_all_connector_modules(connectors_dir)
elif not connectors_to_check:
print("No new connector modules detected in this diff. Nothing to check.")
sys.exit(0)
else:
diff_mode = True
print(
f"Found {len(connectors_to_check)} new connector(s): "
+ ", ".join(sorted(connectors_to_check))
)
else:
# Static mode: check all connectors with source code
print("Static mode: checking all connector modules with source code.")
connectors_to_check = find_all_connector_modules(connectors_dir)
print(f"Total connectors to check: {len(connectors_to_check)}")
print()
# --- Load registration data from all files ---
dist_registered = build_dist_registered(dist_pom)
pm_jars = build_plugin_mapping_jars(plugin_mapping)
pc_names = build_plugin_config_set(plugin_config)
# --- Check each connector ---
errors = {} # {connector: [missing_locations]}
for artifact_id, connector_dir in sorted(connectors_to_check.items()):
connector_errors = []
# Check 1: parent pom.xml module declaration
# Each connector must be declared as <module> in its parent directory's pom.xml.
# Connectors discovered via os.walk but missing from the pom hierarchy
# will be silently ignored by Maven (not compiled, not packaged).
if not is_module_declared_in_parent_pom(connector_dir):
connector_errors.append(
"parent pom.xml (missing <module> entry; "
"Maven will not compile this connector)"
)
# Check 2: seatunnel-dist/pom.xml scope=provided dependency
# This controls whether the connector jar ends up in the dist connectors/ dir.
if artifact_id not in dist_registered:
connector_errors.append(
"seatunnel-dist/pom.xml (missing <scope>provided</scope> dependency; "
"connector jar will NOT be included in the distribution package)"
)
# Check 3: plugin-mapping.properties name->jar mapping
# Required for users to reference the connector by PluginName in job configs.
if artifact_id not in pm_jars:
connector_errors.append(
"plugin-mapping.properties (missing name→jar mapping; "
"users cannot reference this connector by name in job configs)"
)
# Check 4: config/plugin_config install list
# Required for install-plugin.sh to download the connector.
# Skip connectors that are bundled into the dist core (always available).
if artifact_id not in BUNDLED_IN_DIST and artifact_id not in pc_names:
connector_errors.append(
"config/plugin_config (missing entry; "
"install-plugin.sh cannot download this connector)"
)
if connector_errors:
errors[artifact_id] = connector_errors
# --- Print results ---
if not errors:
mode_desc = "newly added" if diff_mode else "all"
print(
f"OK: All {len(connectors_to_check)} {mode_desc} connector module(s) "
"pass all critical registration checks."
)
sys.exit(0)
print("ERRORS: The following connectors are missing critical registrations.")
print(
"Each missing registration silently breaks a specific aspect of the connector."
)
print()
for connector, locs in sorted(errors.items()):
print(f" {connector}:")
for loc in locs:
print(f" ERROR: missing from {loc}")
print()
print("-" * 70)
print(f"Total connectors with registration errors: {len(errors)}")
print()
print("Fix guide:")
print(
" 1. seatunnel-connectors-v2/pom.xml: add <module>CONNECTOR_NAME</module>"
)
print(
" 2. seatunnel-dist/pom.xml: add <dependency> with <scope>provided</scope>"
)
print(
" 3. plugin-mapping.properties: add 'seatunnel.source.NAME = CONNECTOR_NAME'"
)
print(
" and/or 'seatunnel.sink.NAME = CONNECTOR_NAME'"
)
print(
" 4. config/plugin_config: add a line with CONNECTOR_NAME under --connectors-v2--"
)
sys.exit(1)
if __name__ == "__main__":
main()