Skip to content

Reject ingestion pipeline sourceConfig without type - #29566

Draft
ayush-shah wants to merge 4 commits into
mainfrom
ayush-shah/ingestion-source-config-type
Draft

Reject ingestion pipeline sourceConfig without type#29566
ayush-shah wants to merge 4 commits into
mainfrom
ayush-shah/ingestion-source-config-type

Conversation

@ayush-shah

@ayush-shah ayush-shah commented Jun 29, 2026

Copy link
Copy Markdown
Member

Fixes #28818.

Summary

This PR makes sourceConfig.config.type a reliable discriminator for ingestion pipelines across API writes, legacy deployment handling, and Python workflow self-registration. It also closes an initial progress-stream race that could emit the same update twice.

What changed

  • Repository validation: POST, PUT/upsert, and PATCH reject a missing, null, blank, or non-string sourceConfig.config.type with HTTP 400. Non-object configs are also rejected. Valid non-blank string discriminators remain accepted without normalization or inference.
  • Safe legacy handling during deployment: a persisted pipeline with a missing or blank discriminator can be repaired in memory when its active service relationship and pipeline type identify exactly one known source-config type. Ambiguous, deleted, malformed, or otherwise unsafe records are not guessed and continue to fail validation.
  • Python serialization: BaseWorkflow explicitly includes Pydantic-defaulted source-config discriminators during self-registration, so exclude_unset=True cannot omit the required type field.
  • Progress SSE correctness: listener registration stays ahead of snapshot retrieval so updates are not lost, while snapshot/live-event coordination suppresses a duplicate initial update. Terminal events still close the stream exactly once.
  • UI fixture: the Playwright ingestion-pipeline fixture supplies the required discriminator.

Scope and legacy data

This PR intentionally includes no database migration or data backfill. Existing persisted ingestion pipelines are not changed by normal reads or deployment. The deployment-time repair is in memory only; legacy records that cannot be identified unambiguously must be corrected explicitly before they can be deployed or updated.

No schema or generated-client changes are included.

Validation

  • mvn -pl openmetadata-service -Dtest=IngestionPipelineRepositoryTest,IngestionPipelineProgressStreamTest,IngestionProgressTrackerTest,ProgressSseManagerTest,ServiceProgressStreamerTest test89 passed.
  • pytest -c ingestion/pyproject.toml ingestion/tests/unit/workflow/test_base_workflow.py15 passed.
  • Ruff check/format and mvn -pl openmetadata-service spotless:check — passed.
  • git diff --check origin/main — passed.

@github-actions github-actions Bot added Ingestion safe to test Add this label to run secure Github workflows on PRs labels Jun 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

The Java checkstyle failed.

Please run mvn spotless:apply in the root of your repository and commit the changes to this PR.
You can also use pre-commit to automate the Java code formatting.

You can install the pre-commit hooks with make install_test precommit_install.

@ayush-shah
ayush-shah marked this pull request as ready for review June 29, 2026 13:33
@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

This PR cannot be merged until the following are addressed on its linked issue:

The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically.

Maintainers can bypass this check by adding the skip-pr-checks label.

@ayush-shah

Copy link
Copy Markdown
Member Author

Pushed a fix for the current migration review thread in 080ec95044: the source-config backfill now uses an explicit service-to-config-type mapping join instead of CASE expressions, so unmatched rows are not written with JSON null.\n\nValidation run locally:\n- git diff --check\n- rtk mvn -pl openmetadata-service -DskipTests=false -Dskip.npm -Dskip.yarn -DskipDocker -Dlicense.skip=true -Dcheckstyle.skip=true -Dspotless.check.skip=true -Dtest=org.openmetadata.service.migration.utils.v200.MigrationUtilTest#backfillsMetadataSourceConfigTypesWithMySqlJsonSet,org.openmetadata.service.migration.utils.v200.MigrationUtilTest#backfillsMetadataSourceConfigTypesWithPostgresJsonbSet test\n- rtk mvn -pl openmetadata-service -DskipTests -Dskip.npm -Dskip.yarn -DskipDocker -Dlicense.skip=true spotless:check

Copilot AI review requested due to automatic review settings July 9, 2026 08:35
@ayush-shah
ayush-shah force-pushed the ayush-shah/ingestion-source-config-type branch from 651fe14 to a6e25e2 Compare July 9, 2026 08:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Want your agent to iterate on Greptile's feedback? Try greploops.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

@github-actions

Copy link
Copy Markdown
Contributor

✅ TypeScript Types Auto-Updated

The generated TypeScript types have been automatically updated based on JSON schema changes in this PR.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 15 changed files in this pull request and generated 1 comment.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 15 changed files in this pull request and generated no new comments.

@sonarqubecloud

Copy link
Copy Markdown

Comment thread openmetadata-spec/src/main/resources/json/schema/metadataIngestion/workflow.json Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@ayush-shah

Copy link
Copy Markdown
Member Author

Follow-up on the latest Gitar review summary: it reflects the superseded broad implementation. Current head 45260c629f contains only the repository validation, focused tests, and Playwright fixture alignment; it has no Python serialization, schema/codegen, migration, legacy-repair, or progress-streaming changes.

The cited streamProgress listener-before-snapshot ordering is unchanged from the current main base. A concurrent update can indeed be emitted live and then repeated by the snapshot, but moving the snapshot before listener registration can lose an update in the opposite race window. If this pre-existing edge case is addressed, it should use atomic registration/snapshot semantics or sequence-aware de-duplication in a separate change. No progress-streaming change is included in this PR.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@ayush-shah

Copy link
Copy Markdown
Member Author

Follow-up for the review rerun on rebased head 73c3e589: this is the same three-file patch (stable patch ID unchanged), now based directly on current main. The PR contains no streamProgress or listener-registration change; that code is byte-for-byte unchanged from the merge base and is absent from this diff.

The duplicate-event race is plausible in the pre-existing implementation, but reading the snapshot first trades it for a lost-update race. A correct fix would need atomic snapshot/registration semantics or sequence-aware de-duplication and belongs in a separate progress-streaming change. No progress-streaming modification is applicable to this PR.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

public void prepare(IngestionPipeline ingestionPipeline, boolean update) {
var service = getCachedParentOrLoad(ingestionPipeline.getService(), "", Include.NON_DELETED);
ingestionPipeline.setService(service.getEntityReference());
validateSourceConfigHasType(ingestionPipeline);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve self-registration for defaulted source config types

BaseWorkflow.get_or_create_ingestion_pipeline() forwards self.config.source.sourceConfig, while the shared Pydantic serializer defaults to exclude_unset=True. A YAML workflow that relies on the generated default discriminator therefore sends sourceConfig.config without type, and this new validation returns 400 during self-registration. The workflow then loses ingestion-pipeline status/progress tracking. Materialize the discriminator before the request (or coordinate the producer fix) and add a regression test for YAML that omits the defaulted type.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (2)

ingestion/src/metadata/workflow/base.py:382

  • The PR description states there are “no Python serialization changes” and that producer-side handling for BaseWorkflow self-registration is deferred, but this change adds _source_config_with_explicit_type() and wires it into get_or_create_ingestion_pipeline() to force the discriminator into the emitted payload. Please update the PR description (and linked issue resolution narrative) to match the actual scope, or revert this Python-side behavior if it truly must be deferred.
    def _source_config_with_explicit_type(self) -> SourceConfig:
        source_config = self.config.source.sourceConfig
        config = source_config.config
        if not isinstance(config, OpenMetadataBaseModel):
            return source_config

        config_type = getattr(config, "type", None)
        if config_type is None:
            return source_config

        return source_config.model_copy(update={"config": config.model_copy(update={"type": config_type})})

openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/IngestionPipelineRepository.java:1687

  • repairLegacySourceConfig() mutates the in-memory pipeline to add an inferred sourceConfig.config.type (and is invoked on every deployIngestionPipeline() call). This appears to be an automatic legacy repair path, and in the REST deploy flow the mutated entity is persisted via createOrUpdate(...) after a successful deploy (see IngestionPipelineResource#deployPipelineInternal). That conflicts with the PR description’s stated scope of “no automatic legacy repairs / existing invalid rows require operational DB repair only”. Consider either (a) dropping this repair step and consistently rejecting legacy invalid configs, or (b) keeping the repair but updating the PR description and ensuring the inferred type is not persisted implicitly (e.g., repair a copy used only for deployment).
  public PipelineServiceClientResponse deployIngestionPipeline(
      IngestionPipeline ingestionPipeline, ServiceEntityInterface service) {
    repairLegacySourceConfig(ingestionPipeline);
    validateSourceConfigHasType(ingestionPipeline);
    applyStreamableLogsConfig(ingestionPipeline);

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (5)

openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/IngestionPipelineRepository.java:1726

  • The PR description says there are no database migrations and no automatic legacy repairs, but this method now performs an in-memory legacy repair (repairLegacySourceConfig) before deploy, and the PR also adds a native DB backfill under bootstrap/sql/migrations/native/2.1.1/. Please either update the PR description/scope to include these legacy-repair + migration changes, or drop them to match the stated contract.
  public PipelineServiceClientResponse deployIngestionPipeline(
      IngestionPipeline ingestionPipeline, ServiceEntityInterface service) {
    repairLegacySourceConfig(ingestionPipeline);
    validateSourceConfigHasType(ingestionPipeline);

openmetadata-service/src/test/java/org/openmetadata/service/migration/v211/IngestionPipelineSourceConfigTypeBackfillTest.java:375

  • runBackfill hard-codes an exact statement count (3), which will make this test fail as soon as the 2.1.1 migration grows (even if the backfill is still correct). It’s safer to assert a minimum count (or assert presence of the expected backfill statements) while still executing everything.
    List<String> statements = backfillStatements(database);
    assertEquals(
        3, statements.size(), "Expected reverse, service, and pipeline-only backfill statements");

openmetadata-service/src/test/java/org/openmetadata/service/migration/v211/IngestionPipelineSourceConfigTypeBackfillTest.java:402

  • backfillStatements() filters post-DDL scripts by a few hard-coded substrings. If the migration ever adds more backfill statements (e.g., for new service types), this test will silently stop executing/validating them, giving a false sense of coverage. Prefer returning/executing the full getPostDDLScripts() list (or filtering only empty statements).
    return migrationFile.getPostDDLScripts().stream()
        .filter(
            statement ->
                statement.contains("ReverseIngestion")
                    || statement.contains("service_parent")

openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/IngestionPipelineRepository.java:1609

  • This PR is scoped/described as enforcing sourceConfig.config.type validation/backfill, but it also changes the ingestion progress SSE streaming behavior (streamProgress now uses ProgressStreamEventEmitter) and adds a dedicated test class for it. Please either document this behavioral change in the PR description or split it into a separate PR so the ingestion-pipeline validation change can be reviewed/rolled out independently.

This issue also appears on line 1723 of the same file.

  public void streamProgress(String pipelineFQN, UUID runId, SseEventSink eventSink, Sse sse) {
    ProgressStreamEventEmitter emitter = new ProgressStreamEventEmitter(eventSink, sse);
    Consumer<ProgressUpdate> listener = emitter::emitLiveUpdate;
    Runnable onClose =
        () -> progressTracker.unregisterProgressListener(pipelineFQN, runId, listener);
    if (ProgressSseManager.getInstance().register(eventSink, sse, onClose)) {
      progressTracker.registerProgressListener(pipelineFQN, runId, listener);
      ProgressUpdate snapshot = getLatestProgressUpdate(pipelineFQN, runId);
      if (snapshot != null) {
        emitter.emitSnapshot(snapshot);

ingestion/src/metadata/workflow/base.py:382

  • PR description states that producer-side handling for Python BaseWorkflow self-registration is intentionally deferred, but this change adds _source_config_with_explicit_type() and wires it into get_or_create_ingestion_pipeline(), which is exactly that producer-side fix. Please update the PR description/scope to reflect that Python behavior is now included (or drop this change if it truly belongs in a follow-up).
    def _source_config_with_explicit_type(self) -> SourceConfig:
        source_config = self.config.source.sourceConfig
        config = source_config.config
        if not isinstance(config, OpenMetadataBaseModel):
            return source_config

        config_type = getattr(config, "type", None)
        if config_type is None:
            return source_config

        return source_config.model_copy(update={"config": config.model_copy(update={"type": config_type})})

@gitar-bot

gitar-bot Bot commented Aug 21, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 4 resolved / 4 findings

Requires an explicit type discriminator in ingestion pipeline source configurations at repository persistence time, preventing future invalid writes. No issues found.

✅ 4 resolved
Bug: Backfill added to already-released v1131 migration may never run

📄 openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1131/Migration.java:26 📄 openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1131/Migration.java:26 📄 openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v1131/MigrationUtil.java:111-125
backfillDatabaseMetadataSourceConfigType is wired into mysql/v1131/Migration and postgres/v1131/Migration. On main the migration tree already contains much newer versions (up through v200 / 2.0.0). OpenMetadata's MigrationWorkflow/MigrationProcessImpl records applied versions in SERVER_CHANGE_LOG and skips runDataMigration() for any version already processed. Any deployment that has already upgraded past 1.13.1 (i.e. nearly every target user of this fix) will never re-execute v1131's runDataMigration(), so the backfill of the 'known persisted bad database metadata pipeline shape' will silently not run for them. Only brand-new installs upgrading through v1131 would get it. The backfill should be placed in the current in-development/unreleased migration version (e.g. v200 / 2.0.0) so it actually executes for existing deployments. Please confirm which version is the active upgrade target before merging.

Edge Case: Backfill no-ops on scalar sourceConfig.config rows, leaving them typeless

📄 openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v1131/MigrationUtil.java:121-124 📄 openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v1131/MigrationUtil.java:126-135 📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/IngestionPipelineRepository.java:298-312
The backfill WHERE clause only guards sourceConfig.config IS NOT NULL (MySQL JSON_EXTRACT(... '$.sourceConfig.config') IS NOT NULL, Postgres i.json #> '{sourceConfig,config}' IS NOT NULL). If a persisted row stored sourceConfig.config as a scalar (e.g. the string "DatabaseMetadata") rather than an object, the row still matches the predicate, but JSON_SET(..., '$.sourceConfig.config.type', ...) / jsonb_set(..., '{sourceConfig,config,type}', ...) cannot add a member to a scalar and effectively no-ops (MySQL returns the document unchanged; Postgres jsonb_set on a non-object path also yields no member). Those rows therefore remain without a usable type, yet the new validateSourceConfigHasType will reject any subsequent create/update of them with HTTP 400. If the scalar shape is among the 'known bad' shapes this PR intends to fix, it is not covered. Consider detecting/handling the scalar-config case (or asserting it never occurs) and add a test for it.

Edge Case: Backfill only types databaseService metadata pipelines

📄 openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v200/MigrationUtil.java:103-117
backfillDatabaseMetadataSourceConfigType only sets sourceConfig.config.type = 'DatabaseMetadata' for pipelines joined via er.fromEntity = 'databaseService' with pipelineType = 'metadata'. Metadata pipelines for other service types (dashboardService, messagingService, etc.) that were also persisted without a sourceConfig.config.type discriminator are not backfilled. If the new repository-level validation (described in the PR summary but not part of this delta) rejects any untyped sourceConfig.config across all service types, those existing pipelines would start failing on their next create/update without a corresponding backfill. If the intent is genuinely database-only, this is fine; otherwise consider broadening the backfill (or adding parallel statements) to cover the other service-type metadata config discriminators. Flagging as minor since the validation code is not in this diff and cannot be verified here.

Edge Case: Snapshot emitted after listener registration can duplicate a live event

📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/IngestionPipelineRepository.java:1290-1303 📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/IngestionPipelineRepository.java:1316-1322
In the refactored IngestionPipelineRepository.streamProgress, the progress listener is registered before the current snapshot is read and emitted:

progressTracker.registerProgressListener(pipelineFQN, runId, listener);
ProgressUpdate snapshot = getLatestProgressUpdate(pipelineFQN, runId);
if (snapshot != null) {
  emitProgressUpdate(eventSink, sse, snapshot);
}

The previous implementation emitted the snapshot before registering the listener. With the new ordering, if a live ProgressUpdate fires in the window between registerProgressListener and getLatestProgressUpdate, the client receives that update via the listener and then receives the snapshot (which reflects the same or newer state), producing a duplicate initial event. For progress rendering this is benign, but if a terminal (PIPELINE_COMPLETE/ERROR) update arrives via the listener in that window it will close the sink, and the subsequent snapshot emit becomes a no-op via the isClosed() guard — so correctness is preserved, only a possible duplicate remains. Consider reading the snapshot before registering the listener, or de-duplicating by tracking the last emitted update, to avoid the redundant event.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ingestion safe to test Add this label to run secure Github workflows on PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Ingestion workflow redeployment fails due to missing sourceConfig.config.type in automated pipeline creation

2 participants