Lesson 8 · Kafka Connect & the sink · the repo signature

hephaestus, the control plane

The generated JSON is just a wish. hephaestus is the job that makes the live cluster match it — and refuses to do the one destructive thing.

Your win: explain the git-as-desired-state reconcile loop, the create-or-update logic, and the delete-guard that makes running it safe — the design that turns 248 config files into a live pipeline.

hephaestus is a job, not a server

Despite being a "service," hephaestus doesn't serve requests — it only exposes a dummy health check. It's a job runner: reconcile jobs registered via bootstrap.RegisterJob, run as Helm hooks on deploy (Go-techstack course, Lesson 10). The core job is hephaestus_upsert_kafka_connect.

Git-as-desired-state

The whole design in one idea There is no database of connectors. The generated JSON files in the repo are the desired state; the live Kafka Connect cluster is the actual state; hephaestus's only job is to make actual match desired. If that sounds like Kubernetes or GitOps — it is, applied to Kafka Connect. Commit a connector file, deploy, and it appears; delete the file, and it's removed. The repo is the source of truth.

The reconcile: delete stale, then upsert everything

cmd/server/hephaestus/upsert_kafka_connect.go:74-154 (condensed)
// ① DELETE: connectors live in the cluster but no longer on disk (live − onDisk)
deleteConnecter.DeleteConnectors(client, GenSinkConfigDir, customSinkDir, SourceConfigDir)   // + Slack alert

// ② UPSERT: every connector JSON in each config dir
upsert(client, SourceConfigDir)     // the Debezium sources
upsert(client, GenSinkConfigDir)    // the ~237 generated sinks
if DeployCustomSinkConnector { upsert(client, SinkConfigDir) }   // hand-written custom sinks

upsert(dir) reads every JSON file in the directory and applies it. Applying one connector is a GET-then-create-or-update:

internal/golibs/kafkaconnect/connector.go:93-101, 238-241 (condensed)
old := GetConnector(name)              // does it exist? (404 → nil)
if old == nil { TryCreateConnector(c) } // absent → POST create
else          { TryUpdateConnector(old, c) }

// TryUpdateConnector: PUT only if the config actually changed…
if !anyChangeInConfig(old, c) { return nil }   // no diff → no-op (idempotent reconcile)

So re-running the job when nothing changed does nothing — it only touches connectors whose config genuinely differs. After a change it waits for IsConnectorReady (the connector and all its tasks RUNNING), and — for a source with a newly-added table — fires the incremental-snapshot signal from Lesson 3 (the --sendIncrementalSnapshot path). Reconcile, verify, back-fill: one loop.

The delete-guard — the safety that makes this OK

Deleting the wrong connector could be catastrophic (Lesson 2: dropping a source drops its slot). So the delete logic is deliberately narrow:

connector.go:153-169 (condensed)
connectorType := old.Config["connector.class"]
if !found || connector == strings.ToUpper(connector) { return nil }   // ← skip ksqlDB (ALL-CAPS names)
if connectorType == "io.confluent.connect.jdbc.JdbcSinkConnector" {
    TryDeleteConnector(connector)     // ← ONLY delete JDBC sink connectors
}                                     //   (sources fall through → never deleted)
Read the guard as a list of "never"s hephaestus will never delete: a source connector (the code comment says it plainly — "will make us lost data" via the dropped publication/slot); a ksqlDB-owned connector (recognized by its all-caps name — those are managed by ksqlDB, Lesson 9). It will only delete a JdbcSinkConnector. A sink is safe to drop and recreate (its data is a derived copy); a source is not. That single asymmetry is why an automated reconcile loop is trusted to run on every deploy.

The deploy sequence & self-healing

On deploy, numbered Helm hooks run in order (deployments/helm/backend/hephaestus/templates/): 2 init ksqlDB migration → 3 upsert kafka connect (--deployCustomSinkConnector --sendIncrementalSnapshot) → 4 validate → 5 migrate ksqlDB → 6 sync data warehouse. And between deploys, an auto-heal cron runs every 10 minutes: it lists connector status, finds tasks in FAILED/UNASSIGNED, and restarts them — so a transient failure recovers without a human.

Read this next

The reconcile loop, in code

Read the upsert job and the connector client together — it's a compact, real GitOps-style controller for Kafka Connect.

→ in-repo cmd/server/hephaestus/upsert_kafka_connect.go + internal/golibs/kafkaconnect/connector.go:84-352
Confluent — the Connect REST API (what the reconcile drives)

Check yourself (from memory)

Q1. Where does hephaestus keep the "desired" set of connectors?

Git-as-desired-state: the files are the truth; hephaestus makes the live cluster match them (GitOps for Connect).

Q2. Which connectors will the delete-guard actually delete?

Only JdbcSinkConnector. Deleting a source drops its slot (data loss); ksqlDB connectors (all-caps) are managed elsewhere.

Q3. What happens when you re-run the reconcile and nothing changed?

anyChangeInConfig gates the PUT — a no-diff reconcile is a no-op. Idempotent, safe to run on every deploy.
Recall: hephaestus — the reconcile loop + the delete-guard.
git-state + reconcile + guard + deploy, then reveal
hephaestus = a job runner (not a server; dummy healthcheck), run as Helm hooks. Git-as-desired-state: no connectors DB — the generated JSON files are the desired state; the job makes the live cluster match (GitOps for Connect). Reconcile (upsert_kafka_connect.go): ① delete stale (live − onDisk, Slack-alerted) → ② upsert every JSON in source/gen-sink/custom-sink dirs. Upsert = GET → create if absent, else TryUpdateConnector which PUTs only if the config changed (anyChangeInConfig → idempotent no-op), waits for IsConnectorReady (connector + all tasks RUNNING), and fires the incremental snapshot for a source's new tables. Delete-guard (connector.go:153-169): only JdbcSinkConnector; never sources (slot/data loss) or ksqlDB (all-caps). Deploy hooks: 2-init-ksql → 3-upsert → 4-validate → 5-migrate-ksql → 6-sync-dwh. Auto-heal cron restarts FAILED tasks every 10 min.
🏁 Part 2 complete — Kafka Connect & the sink You can now explain the transport-and-apply half: the Connect framework (L5), the JDBC sink config line by line (L6), the generation of hundreds of connectors from YAML (L7), and hephaestus reconciling them git-as-desired-state with a slot-protecting delete-guard (L8). The change event now reaches its destination — and the whole fleet is managed. Part 3 reshapes it and lands it in the warehouse.
🎯 Interview one-liner "How do you manage a fleet of connectors safely?" → "Git-as-desired-state. The connector JSON in the repo is the desired state; a reconcile job diffs it against the live cluster over the REST API and converges — creating, updating only on a real config change, and deleting. The key safety is a delete-guard: it only ever deletes JDBC sinks, never a source (which would drop the replication slot and lose data) or a ksqlDB-managed connector."
Part 3 opens the transform and destination end — ksqlDB stream processing, the warehouse and chained CDC, the full end-to-end trace, and operating it all. Tell me "build Part 3" when you're ready, or ask me anything about the control plane first. Re-take Part 2's quizzes cold tomorrow.

1. In-repo: cmd/server/hephaestus/upsert_kafka_connect.go:43-157, internal/golibs/kafkaconnect/connector.go:84-352 (Upsert/Delete/TryUpdateConnector).