Skip to content

feat(ingest): add hotdata ingest command group#206

Open
eddietejeda wants to merge 6 commits into
mainfrom
feat/ingest-commands
Open

feat(ingest): add hotdata ingest command group#206
eddietejeda wants to merge 6 commits into
mainfrom
feat/ingest-commands

Conversation

@eddietejeda

@eddietejeda eddietejeda commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Adds a hotdata ingest command group that drives the dlthubworker ingest API (/v1/ingest/*) through the gateway, with a UX modeled on hotdata connections.

Command surface

hotdata ingest new       # guided, catalog-driven wizard
hotdata ingest list      # sources you've ingested
hotdata ingest create    # scriptable; `create list` browses the catalog
hotdata ingest import    # SQL front-door; samples a default when no query given
hotdata ingest refresh   # re-drain + poll an ingest to completion

The new wizard is catalog-driven: /ingest/connectors returns a template per REST service with base_url, auth shape, and resources pre-filled, so the wizard prompts only for the <PLACEHOLDER> secrets — the user never types a URL the catalog already knows. Keyless services onboard with zero prompts. import with no SQL synthesizes SELECT * FROM <source> LIMIT 1000.

Transport is a raw-HTTP client (src/client/ingest.rs) since these routes aren't in the generated SDK yet.

Auth model

  • Enqueue (create/import) uses the durable hd_ API key as the bearer — the server 422s JWTs there (the drain job outlives them). Without a key the CLI fails fast with a --api-key hint; read paths work on the session JWT.
  • --debug redacts source secrets (credentials/rest_config/catalog_config) from the logged request body.
  • A transport failure on enqueue prints an actionable "service may be starting up / redeploying; retry" hint instead of a bare error sending request.

Also in this branch

  • Workspace-resolution consistency fix: auth status reported the first workspace from the live /workspaces API while resolve_workspace fell back to config.yml's first — so a multi-workspace API key made the status readout name a different workspace than commands actually used. Both now share one credentials::default_workspace_id helper.
  • Connector alias de-dup (the redundant postgres/postgresql, mssql/sqlserver catalog entries) is fixed at the source in dlthubworker's list_connectors — see the companion PR in that repo. This branch carries no display-side workaround.

Testing

  • 261 unit tests (mockito for HTTP; covers enqueue auth, catalog/template handling, import query synthesis, workspace-resolution branches, --debug redaction). Full suite green; clippy/fmt clean.
  • Live E2E against prod: create list, list, request assembly from catalog templates, import query synthesis + id-pinning, refresh (drain + poll), and the workspace fix (auth status now matches the X-Workspace-Id commands send). A full POST /sources onboard is slow (~190s) / occasionally exceeds the 300s gateway timeout during worker rollouts — a dlthubworker-side issue, not the CLI.

@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 39.11846% with 663 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/commands/ingest.rs 20.02% 559 Missing ⚠️
src/client/ingest.rs 74.63% 69 Missing ⚠️
src/main.rs 0.00% 19 Missing ⚠️
src/commands/auth.rs 0.00% 13 Missing ⚠️
src/commands/databases.rs 0.00% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

Comment thread src/client/ingest.rs Outdated
let body = serde_json::to_value(req).expect("IngestRequest serializes");
self.send(
self.authed(reqwest::Method::POST, "/sources").json(&body),
Some(&body),

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.

Blocking (security): --debug leaks source credentials in plaintext.

create_source passes the full serialized IngestRequest as the body_log argument to send. util::send_debugdebug_request prints that body verbatim (see util.rs:60-65); it only masks the Authorization header and only redacts response fields via TOKEN_REDACT_KEYS. It never redacts request-body fields.

So hotdata ingest sql --url postgresql://user:PASSWORD@host/db --debug (or --config with a password, rest_config with a bearer token, catalog_config with a catalog token, or filesystem aws_secret_access_key) prints the secret in cleartext to stderr — exactly the output users paste into bug reports and CI logs.

This contradicts the repo's established convention: jwt.rs builds a redacted_form_body(&params) and passes that as the log body precisely because body_for_log is printed unredacted (its doc: "pass ... a hand-rolled redacted Value for form bodies"). The design doc §3 also promised token redaction here.

Fix: build a redacted clone of the body for logging (the redact_json_fields helper already exists), e.g. redact credentials/rest_config/catalog_config before passing to send, rather than Some(&body).

@claude claude Bot 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.

Review

Blocking Issues

src/client/ingest.rs:204 — source credentials leak in plaintext under --debug.
create_source passes the full unredacted IngestRequest as the debug log body. util::send_debug/debug_request print request bodies verbatim (they only mask the Authorization header and only redact response fields via TOKEN_REDACT_KEYS). This exposes SQL passwords/connection strings, REST bearer tokens, Iceberg catalog tokens, and filesystem secret keys to stderr — the output users routinely paste into issues and CI logs. The rest of the CLI already avoids this (jwt.rs passes a redacted_form_body), and the design doc §3 promised redaction here.

Action Required

  • Redact secret-bearing fields (credentials, rest_config, catalog_config) from the request body before passing it to send for debug logging — reuse the existing redact_json_fields helper. Apply the same treatment to any other enqueue body that can carry secrets.

Drives the dlthubworker ingest API (/v1/ingest/*) through the gateway via
a raw-HTTP client (routes aren't in the SDK yet): sql/rest/filesystem/
iceberg onboarding, restricted SQL queries, translate, connector catalog,
status, drain, schema, preview, and parquet download.

Reconciled against the stable post-gateway API (2026-07-07 rollout):

- enqueue (POST /sources|/queries) sends the durable hd_ API key as the
  bearer — the server rejects JWTs there because the drain job outlives
  them; without an API key the CLI fails fast with a --api-key hint
- verified live: the CLI's user-scoped session JWT only reaches
  /connectors — workspace-scoped endpoints 403 (the worker derives the
  workspace from the JWT's workspace:* scope and ignores X-Workspace-Id
  on the JWT route), so those 403s get an --api-key hint too
- no `ingest sources` command: GET /ingest/sources never shipped
- 2.5s status polling, drain re-kick after 30s pending (control-store
  read lag), brief 404 retry on `ingest query` right after onboarding,
  and a database-scoped-token hint when a load fails with Forbidden
@eddietejeda eddietejeda force-pushed the feat/ingest-commands branch from 1ee3697 to 8419569 Compare July 8, 2026 03:18
Comment thread src/client/ingest.rs Outdated
let body = serde_json::to_value(req).expect("IngestRequest serializes");
self.send(
self.authed(reqwest::Method::POST, "/sources").json(&body),
Some(&body),

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.

Blocking (security): still leaks source credentials in plaintext under --debug.

This is unchanged since the prior review. create_source passes Some(&body) — the full serialized IngestRequest — to send, and util::send_debuglog_request_structdebug_request (util.rs:60-65) prints the request body verbatim. Only the Authorization header is masked; request-body fields are never redacted. redact_json_fields is still private in util.rs and is not used here.

So hotdata ingest sql --url postgresql://user:PASSWORD@host/db --debug (and rest_config bearer tokens, catalog_config tokens, filesystem aws_secret_access_key) still prints the secret to stderr — the exact output users paste into bug reports and CI logs.

Fix: build a redacted clone of the body for logging (mask credentials/rest_config/catalog_config) and pass that instead of Some(&body). This mirrors jwt.rs, which builds redacted_form_body(&params) precisely because the log body is printed unredacted. Making redact_json_fields (or a small wrapper) pub(crate) would let you reuse the existing helper.

@claude claude Bot 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.

Review

Blocking Issues

  • src/client/ingest.rs:204create_source still passes the full unredacted IngestRequest body (Some(&body)) to the debug logger, leaking source credentials (credentials, rest_config bearer tokens, catalog_config tokens, filesystem aws_secret_access_key) in plaintext to stderr under --debug. This was flagged in the prior review and remains unfixed.

Action Required

Build a redacted clone of the request body for logging (mask credentials/rest_config/catalog_config) and pass that to send instead of Some(&body), mirroring the redacted_form_body pattern already used in jwt.rs. The existing redact_json_fields helper in util.rs can be reused if exposed as pub(crate).

The enqueue body's credentials / rest_config / catalog_config subtrees
(SQL passwords, connection strings, REST bearer tokens, catalog tokens,
object-store keys) were printed verbatim to stderr under --debug. Log a
view with those subtrees replaced by "***" instead; the wire body is
unchanged. Mirrors jwt.rs's redacted_form_body pattern.
@eddietejeda

Copy link
Copy Markdown
Contributor Author

Fixed in 7f0d0a4. create_source/create_query now build a debug-log view via redact_secret_fields that replaces the credentials/rest_config/catalog_config subtrees with "***" before passing it to send; the wire body is unchanged. Verified under --debug that a SQL connection-string password no longer appears in stderr, plus a unit test (redact_secret_fields_masks_all_secret_subtrees_and_keeps_the_rest).

claude[bot]
claude Bot previously approved these changes Jul 8, 2026

@claude claude Bot 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.

Cycle 3: prior blocking credential-leak issue in create_source is resolved — the enqueue body is now passed through redact_secret_fields before logging, masking credentials/rest_config/catalog_config (covering SQL passwords, filesystem aws_secret_access_key, REST bearer tokens, and Iceberg catalog tokens). Other body-carrying calls carry no secrets. LGTM.

…t/refresh

Collapse the sprawling 13-subcommand surface into the same shape as
`hotdata connections`, focused on a guided, catalog-driven onboarding
flow:

  new       interactive wizard, driven by the connector catalog
  list      the sources you've ingested (result databases)
  create    scriptable onboarding; `create list` browses the catalog
  import    SQL front-door (renamed from `query`); defaults to a
            reasonable sample (SELECT * FROM <source> LIMIT 1000) when
            no query is given
  refresh   re-drain + poll an ingest to completion

The `new` wizard is catalog-driven: the /ingest/connectors catalog
returns a `template` per REST service with base_url, auth shape, and
resources already filled in, so the wizard prompts only for the
`<PLACEHOLDER>` secrets — the user never types a URL the catalog knows.
Keyless services (auth "none") onboard with zero prompts.

Dropped as separate verbs: sql/rest/filesystem/iceberg (folded into
new/create by family), translate, connectors (→ `create list`),
status/drain (folded into the run+poll loop), schema/preview/download
(the read side is the core `query`/`databases`/`results` commands).

Client: ConnectorEntry now carries `auth` + `template`; dropped the
translate/schema/preview/download methods and types.

Catalog display drops redundant SQL dialect aliases (the server lists
both postgres/postgresql and mssql/sqlserver with identical labels):
the wizard and `create list` show the canonical name only, while
`create --service <alias>` still resolves against the full catalog.

Verified live against prod: `create list`, `list`, request assembly
from the catalog template, `import` default-query synthesis and
id-pinning, and `refresh` (drain + poll). A full `POST /sources`
onboard currently hangs server-side in the deploy path (unrelated to
the CLI — /queries, /drain, /jobs, /connectors all respond fast).
A slow enqueue (first-per-workspace runtime deploy) or a worker rollout
makes the gateway hold the connection to its timeout, surfacing as the
opaque 'error sending request'. Add a dark-grey hint pointing at the
likely cause (service starting up / redeploying) and that a timed-out
enqueue is safe to re-run.
`auth status` showed the first workspace from the live /workspaces API while `resolve_workspace` fell back to config.yml's first — so for a multi-workspace API key the status readout named a different workspace than commands used. Add one `credentials::default_workspace_id` helper (single-workspace token pins its own; multi/unrestricted key honors the saved default when it can reach it, else the credential's first authorized workspace) and use it from both paths. Drop the now-unused `config::resolve_workspace_id`.
…rce)

The redundant postgres/postgresql and mssql/sqlserver catalog entries are now collapsed in dlthubworker's list_connectors, so the CLI no longer needs to dedupe on display. Keep only the family sort.

@claude claude Bot 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.

Cycle 1–2 blocking issue resolved: create_source now redacts credentials/rest_config/catalog_config via redact_secret_fields before passing the body to --debug logging (src/client/ingest.rs:224, helper at :272), so source secrets no longer leak in plaintext. Verified create_query's unredacted body carries no credential fields.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant