2727from rptest .clients .rpk import RpkTool
2828from rptest .services .admin import Admin
2929from rptest .services .cluster import TestContext , cluster
30- from rptest .services .multi_cluster_services import SecondaryClusterArgs
30+ from rptest .services .confluent_schema_registry import ConfluentSchemaRegistryService
31+ from rptest .services .multi_cluster_services import (
32+ SecondaryClusterArgs ,
33+ SecondaryClusterSpec ,
34+ ServiceType ,
35+ )
3136from rptest .services .redpanda import SchemaRegistryConfig
3237from rptest .tests .cluster_linking_test_base import ShadowLinkTestBase
3338from rptest .tests .schema_registry_test import SchemaRegistryRedpandaClient
3843Pair = tuple [str , int ]
3944
4045
41- class SchemaRegistrySyncE2ETest (ShadowLinkTestBase ):
46+ class SchemaRegistrySyncE2EBase (ShadowLinkTestBase ):
4247 """Source cluster SR -> destination cluster SR over the shadow-link HTTP
43- source reader, at scale and with concurrent / post-sync additions."""
48+ source reader, at scale and with concurrent / post-sync additions.
49+
50+ Shared implementation for the per-source-vendor leaf classes below. Ducktape
51+ only collects leaf Test classes (no subclasses), so this base is not run
52+ directly; the concrete `SchemaRegistrySyncE2ETest` (Redpanda source) and
53+ `ConfluentSchemaRegistrySyncE2ETest` (Confluent source) leaves are."""
4454
4555 # A layered diamond DAG (top -> mid -> leaf), like the reconciler
4656 # concurrent_stress unit test but larger: adjacent referrers share referents,
@@ -67,6 +77,19 @@ def __init__(self, test_context: TestContext, *args: Any, **kwargs: Any):
6777 ** kwargs ,
6878 )
6979
80+ # --- source Schema Registry hooks --------------------------------------
81+ # Overridden by cross-vendor subclasses (e.g. a Confluent Platform source)
82+ # to point the seeding client and the shadow-link source URL at a
83+ # non-Redpanda registry. The destination side stays Redpanda throughout.
84+
85+ def _make_source_client (self ) -> SchemaRegistryRedpandaClient :
86+ return SchemaRegistryRedpandaClient (self .source_cluster_service )
87+
88+ def _source_sr_url (self ) -> str :
89+ # Derive from the seeding client so the link source_url and the client
90+ # can never point at different registries.
91+ return self ._make_source_client ().base_uri ()
92+
7093 # --- schema builders ---------------------------------------------------
7194
7295 def _record (self , name : str , fields : list [dict ]) -> dict :
@@ -199,7 +222,7 @@ def _create_sr_link(
199222 # subsequent full syncs land quickly. An optional exact-subject filter
200223 # scopes replication. Returns the source URL used.
201224 if source_url is None :
202- source_url = self .source_cluster_service . schema_reg ( limit = 1 )
225+ source_url = self ._source_sr_url ( )
203226 req = self .create_default_link_request (
204227 link_name = LINK_NAME ,
205228 mirror_all_topics = False ,
@@ -440,7 +463,7 @@ def _verify_rpk_status(self, expected_subjects: int, expected_versions: int):
440463
441464 @cluster (num_nodes = 6 )
442465 def test_schema_registry_api_sync_e2e (self ):
443- src = SchemaRegistryRedpandaClient ( self .source_cluster_service )
466+ src = self ._make_source_client ( )
444467 dest = SchemaRegistryRedpandaClient (self .target_cluster_service )
445468
446469 # The destination `_schemas` topic is NOT warmed up here on purpose:
@@ -517,7 +540,7 @@ def test_schema_registry_api_sync_e2e(self):
517540
518541 @cluster (num_nodes = 6 )
519542 def test_schema_registry_api_sync_soft_delete (self ):
520- src = SchemaRegistryRedpandaClient ( self .source_cluster_service )
543+ src = self ._make_source_client ( )
521544 dest = SchemaRegistryRedpandaClient (self .target_cluster_service )
522545
523546 # seeded-value: mixed active/deleted before the first sync.
@@ -559,7 +582,7 @@ def _v2(name: str) -> dict:
559582
560583 @cluster (num_nodes = 6 )
561584 def test_schema_registry_api_sync_out_of_scope_reference (self ):
562- src = SchemaRegistryRedpandaClient ( self .source_cluster_service )
585+ src = self ._make_source_client ( )
563586 dest = SchemaRegistryRedpandaClient (self .target_cluster_service )
564587
565588 # leaf-0 is a referent; mid-0 references it; ok-value is independent.
@@ -601,7 +624,7 @@ def errored() -> bool:
601624
602625 @cluster (num_nodes = 6 )
603626 def test_schema_registry_api_sync_recovers_after_source_unavailable (self ):
604- src = SchemaRegistryRedpandaClient ( self .source_cluster_service )
627+ src = self ._make_source_client ( )
605628 dest = SchemaRegistryRedpandaClient (self .target_cluster_service )
606629
607630 self ._register (
@@ -630,7 +653,7 @@ def parked() -> bool:
630653
631654 # Repoint the link at the real source. The config change forces a fresh
632655 # full sync, which now reaches the source and replicates.
633- good_url = self .source_cluster_service . schema_reg ( limit = 1 )
656+ good_url = self ._source_sr_url ( )
634657 link = self .get_link (LINK_NAME )
635658 link .configurations .schema_registry_sync_options .shadow_schema_registry_api .source_url = good_url
636659 self .update_link (
@@ -659,7 +682,7 @@ def recovered() -> bool:
659682
660683 @cluster (num_nodes = 6 )
661684 def test_schema_registry_api_sync_memory_backpressure (self ):
662- src = SchemaRegistryRedpandaClient ( self .source_cluster_service )
685+ src = self ._make_source_client ( )
663686 dest = SchemaRegistryRedpandaClient (self .target_cluster_service )
664687
665688 # Shrink the reconcile memory budget to its 1 MiB floor and raise
@@ -689,7 +712,7 @@ def test_schema_registry_api_sync_memory_backpressure(self):
689712
690713 @cluster (num_nodes = 6 )
691714 def test_schema_registry_api_sync_survives_leadership_change (self ):
692- src = SchemaRegistryRedpandaClient ( self .source_cluster_service )
715+ src = self ._make_source_client ( )
693716 dest = SchemaRegistryRedpandaClient (self .target_cluster_service )
694717 admin = Admin (self .target_cluster_service )
695718
@@ -836,3 +859,65 @@ def returning_leader_fresh() -> bool:
836859 backoff_sec = 1 ,
837860 err_msg = "reused instance did not reset state on regaining leadership" ,
838861 )
862+
863+
864+ class SchemaRegistrySyncE2ETest (SchemaRegistrySyncE2EBase ):
865+ """Redpanda source cluster variant: the base's default source hooks point at
866+ the secondary Redpanda cluster's Schema Registry."""
867+
868+
869+ class ConfluentSchemaRegistrySyncE2ETest (SchemaRegistrySyncE2EBase ):
870+ """Runs the full SchemaRegistrySyncE2ETest validation surface with a
871+ Confluent Platform Schema Registry (backed by an Apache Kafka source
872+ cluster) as the shadow-link source instead of a secondary Redpanda cluster.
873+ The destination stays Redpanda. See CORE-16776 / CORE-16357.
874+
875+ Node budget: 3 (destination Redpanda) + 2 (Apache Kafka source) + 1
876+ (Confluent SR) = 6, matching the inherited @cluster(num_nodes=6).
877+ """
878+
879+ # Two-broker Kafka source so the topology uses exactly the inherited
880+ # num_nodes budget (see class docstring).
881+ secondary_num_brokers = 2
882+
883+ def __init__ (self , test_context : TestContext , * args : Any , ** kwargs : Any ):
884+ # Skip SchemaRegistrySyncE2ETest.__init__, which wires a Redpanda
885+ # secondary SR; the source here is Apache Kafka + Confluent SR, so go
886+ # straight to the shadow-link base with only the destination SR set.
887+ ShadowLinkTestBase .__init__ (
888+ self ,
889+ test_context ,
890+ schema_registry_config = SchemaRegistryConfig (),
891+ * args ,
892+ ** kwargs ,
893+ )
894+ self ._confluent_sr : ConfluentSchemaRegistryService | None = None
895+
896+ def get_source_cluster_spec (self ) -> SecondaryClusterSpec :
897+ # Source cluster is Apache Kafka (KRaft, secondary_num_brokers nodes);
898+ # the Confluent SR is attached to it in setUp. Mirrors the Kafka-source
899+ # topology used by cluster_linking_e2e_test.
900+ return SecondaryClusterSpec (
901+ ServiceType .KAFKA , kafka_version = "3.8.0" , kafka_quorum = "COMBINED_KRAFT"
902+ )
903+
904+ def setUp (self ):
905+ # Starts the Apache Kafka source + Redpanda destination clusters.
906+ super ().setUp ()
907+ # Attach a Confluent Schema Registry to the Kafka source and start it.
908+ self ._confluent_sr = ConfluentSchemaRegistryService (
909+ self .test_context , bootstrap_provider = self .source_cluster_service
910+ )
911+ self ._confluent_sr .start ()
912+
913+ def _confluent (self ) -> ConfluentSchemaRegistryService :
914+ assert self ._confluent_sr is not None , "Confluent SR not started"
915+ return self ._confluent_sr
916+
917+ def _make_source_client (self ) -> SchemaRegistryRedpandaClient :
918+ sr = self ._confluent ()
919+ # The base client hardcodes port 8081 and only needs
920+ # .nodes[*].account.hostname + .logger, so the ducktape service
921+ # duck-types cleanly; the Confluent SR must use the default port.
922+ assert sr .port == 8081 , "Confluent SR must listen on 8081 for the SR client"
923+ return SchemaRegistryRedpandaClient (sr ) # type: ignore[arg-type]
0 commit comments