Skip to content

Add an Iceberg REST Catalog front door - #14

Merged
balookrd merged 50 commits into
mainfrom
feature/iceberg-rest-fe-phase1
Jul 29, 2026
Merged

Add an Iceberg REST Catalog front door#14
balookrd merged 50 commits into
mainfrom
feature/iceberg-rest-fe-phase1

Conversation

@balookrd

Copy link
Copy Markdown
Owner

Adds an Iceberg REST Catalog front door to the proxy: a parallel HTTP listener that speaks the
Iceberg REST spec and is backed by the same routing, federation, security and observability
pipeline as the Thrift front door. PyIceberg, Spark and Trino can read Iceberg tables through it,
and — in the default catalog only — create and commit to them.

Disabled by default (rest-catalog.enabled=false); nothing changes for existing deployments
unless the listener is switched on.

What it serves

Every configured catalog is exposed as its own prefix, /v1/<catalog>/..., with
GET /v1/config?warehouse=<catalog> for discovery. The default catalog's prefix keeps the
federated view (its own databases plus other catalogs' under <catalog><separator><db> names);
every other prefix is a clean per-catalog view.

Reads: namespace and table listing and load, view listing and load, HEAD existence checks, and
listing pagination.

Writes, default catalog only: table create, commit, rename, register and drop; view create,
update, rename and drop; namespace create, update and drop; multi-table transaction commit.

/v1/config advertises exactly what the front door serves, so a client learns the read/write
asymmetry between catalogs from discovery instead of from refusals.

Why writes are restricted to the default catalog

This is the load-bearing design decision, not a configuration preference.

Iceberg commits atomically one of two ways. NoLock relies on the metastore doing a
compare-and-swap on metadata_location through alter_table's expected-parameter check —
expected_parameter appears nowhere in HiveAlterHandler of either backend jar, so that path is
unavailable. The remaining option, MetastoreLock, takes a real HMS lock.

Only the default catalog has real locks: it owns the TxnHandler. Every other catalog is served by
the proxy's synthetic lock shim, which grants EXCLUSIVE locks without conflict checking. A commit
routed there would take an always-granted lock, believe it owned the table, race a concurrent
writer, and silently lose an update. Tolerable for reads; not for writes.

WriteRouteGate therefore refuses every write route whose target namespace resolves to another
catalog. It keys on the resolved catalog, never the URL prefix — under the default prefix the
federated view exposes other catalogs' databases as apache__default, and a write there would
otherwise reach the shim through the back door. Resolution reuses the proxy's own CatalogRouter
rather than a second name parser. The gate covers all thirteen write routes the adapter exposes,
fails closed when a namespace cannot be resolved, and a drift-guard test fails the build if a
future Iceberg upgrade adds a write route that nobody classified.

Catalog access modes and write-db whitelists are inherited from the routing layer, so a REST client
faces exactly what a Thrift client faces.

Defects found and fixed along the way

Several were pre-existing and only surfaced because these code paths had never been exercised:

  • Every HDFS write from the proxy JVM failed with NoSuchMethodError on FSOutputSummer:
    orc-core dragged hadoop-hdfs:2.2.0 alongside hadoop-common:2.6.0, and Maven never compared
    them (different artifact IDs). Aligned to 2.6.0; the transitive Xerces that came with the fix is
    excluded, since its JAXP services file would have made a 2007-era parser the process-wide XML
    provider.
  • REST error bodies leaked full server stack traces — internal packages, file names, line
    numbers — on a listener that may be unauthenticated.
  • HEAD responses wrote a body, so every HEAD that errored logged a stack trace per request;
    the same defect existed silently on the management listener.
  • ?pageSize= without pageToken returned 500 — the shape a client's first paginated request
    takes.
  • Unparseable request bodies returned 500 instead of 400.
  • Under Kerberos the REST catalog had no Kerberos configuration at all (a bare Configuration
    instead of the catalog's HiveConf), plus a missing dfs.data.transfer.protection.

Validation

  • 637 unit tests, 109 of them in the restcatalog package.
  • Smoke stand, section G of smoke-stand/TEST-MATRIX.md: 38 rows covering discovery, reads,
    the full write surface, nine gate refusals, error shapes and metrics. Write assertions check
    effects — a commit must advance metadata-location, a rename must leave the old name gone — so
    a no-op that returns 200 fails the run.
  • Both stand profiles: plain and Kerberos. Under SPNEGO an unauthenticated request is refused,
    writes succeed, the audit log carries the authenticated principal, and the commit lock is served
    by the real backend rather than the shim.
  • The SQL layer through both HiveServer2 instances, as the regression check for the Hadoop
    dependency change.

Observability

Three Prometheus series — hms_proxy_rest_requests_total{prefix,route,status},
hms_proxy_rest_request_duration_seconds{prefix,route} and
hms_proxy_rest_listener_info{bind_host,port} — plus an Iceberg REST row in the bundled Grafana
dashboard. Labels carry the parsed route, never a raw URL, so cardinality stays bounded.

Known limits

  • Writes outside the default catalog are refused by design, as above.
  • RoutingHiveCatalog injects its client pool into HiveCatalog's private clients field by
    reflection, pinned to Iceberg 1.9.2; RoutingHiveCatalogTest must be re-run on any version bump.
  • Non-Iceberg Hive tables stay invisible over REST by design — use the Thrift listener for those.
  • The stand validates protocol and routing behaviour, not stack integration: no Ranger, Atlas or
    HA, and queries run as local MapReduce.

balookrd added 30 commits July 27, 2026 15:48
--scenario rest drives the listener with curl: config discovery (the
advertised prefix is reused for every later path), namespace and table
listings, a table load asserting metadata-location comes back, and the
negative shapes - an unknown prefix, an unknown table and a write route
all have to fail cleanly. Plain Hive tables of the same database must
stay invisible through REST, which pins down the HiveCatalog filter.
The step joins --scenario all and is skipped when HMS_SMOKE_REST_URL is
not set.

The stand enables the listener on its plain profile (host port 19183)
and registers a minimal Iceberg table for the load check: a hand-written
format-version-2 metadata.json in HDFS plus a Hive table shell carrying
table_type=ICEBERG and metadata_location. A passing load therefore
proves the whole chain - REST route, HiveCatalog, the proxy's routing
layer, HMS, HDFS. The Kerberos profile keeps the listener off: SPNEGO
would need a GSS-enabled curl in-network and is already covered by
SpnegoIntegrationTest.

Recorded as section G in TEST-MATRIX; both README/CHANGELOG locales
updated.
RoutingMetaStoreClient.create gains an overload that accepts a
CatalogNameTranslation. When present, db-name arguments are translated to
the proxy's external federated form before delegating, and Database/Table
results coming back are rewritten to the catalog's internal names. The
untranslated single-arg create keeps delegating with a null translation, so
existing behavior is unchanged.
Adds IcebergRestServices, an eager prefix -> IcebergRestService registry
built from ProxyConfig: the default catalog keeps the untranslated view,
every other catalog gets a CatalogNameTranslation. Backs multi-catalog
prefix resolution for the REST endpoint (byWarehouse/serviceFor) ahead
of wiring it into HTTP routing.
Add an optional second-prefix block to run_rest_smoke, driven by
HMS_SMOKE_REST_SECOND_PREFIX (+ HMS_SMOKE_REST_SECOND_ICEBERG_TABLE): warehouse
discovery advertises the chosen prefix, an unknown warehouse gets a clean 400,
the clean view under the second prefix lists the namespace without leaking
catalog__ external names, and the second-prefix table load returns
metadata-location.

Wire the stand's simple.env to the apache catalog and its second Iceberg table
(smoke_iceberg_tbl_ap, registered on the second HDFS cluster namenode-b) and
document the registration in both stand README locales. Extend TEST-MATRIX
(EN+RU) section G with rows G8-G11 and note the same-day revalidation.
- IcebergRestService: rewrite class Javadoc to describe the actual
  per-prefix service model instead of the stale MVP/single-catalog note.
- IcebergRestServices#close: attempt close() on every service instead of
  stopping at the first failure; collect the first IOException, add later
  ones as suppressed, and rethrow.
- TEST-MATRIX.ru.md: keep "prefix" in Latin script in the G10 row instead
  of the Cyrillic "префиксом".
- AGENTS.md: document the restcatalog module in the architecture notes.
The second-prefix block now checks the federation side too: the
<catalog><separator><db> name stays visible under the default prefix,
and the second catalog's Iceberg table is listed and loaded through
that federated name. The clean-view side gains the cross-catalog
negatives - a default-catalog table and the external namespace name
both 404 under the second prefix - and the second catalog's plain Hive
table must stay out of its listing
(HMS_SMOKE_REST_SECOND_NON_ICEBERG_TABLE). The catalog-db separator is
no longer hardwired into the leak grep (HMS_SMOKE_REST_SEPARATOR,
default __). Recorded as rows G12-G16 in TEST-MATRIX, both locales.
The REST smoke now optionally fetches the management /metrics endpoint
(HMS_SMOKE_REST_METRICS_URL) and checks it carries the
hms_proxy_rest_requests_total and hms_proxy_rest_listener_info series,
proving the Iceberg REST listener's own Prometheus metrics actually show up
where an operator would look for them. Documented in both READMEs (new
table rows plus a one-line note in the Iceberg REST section), the
changelog, and the stand's test matrix (row G17 plus a revalidation-log
note).
Rate/error-ratio/latency stats, a listener-up stat driven by
hms_proxy_rest_listener_info, latency quantiles, and breakdowns by HTTP
status, catalog prefix and route (pseudo-routes mark refusals). Panels
are cloned from the dashboard's existing reference panels; series names
were checked against a live /metrics scrape, including the histogram
_bucket series the quantile panels rely on.
Bump iceberg.version to 1.9.2, pin Jackson core/databind to 2.18.3 to
match what Iceberg 1.9.2 is compiled against (Hive 3.1.3 otherwise
drags in databind 2.12), and exclude the newly-transitive slf4j-api
from the three Iceberg dependencies so the tree keeps a single
provider.

Re-vendor RESTCatalogAdapter from the matching apache-iceberg-1.9.2
tag. Route stays nested and gets the same public-promotion patch as
before; HTTPMethod moved out into HTTPRequest.HTTPMethod, and the
adapter's public entry point is now handleRequest(...) instead of the
removed execute(...) overload.

Migrate IcebergRestService.dispatch and IcebergHttpHandler's route
dispatch to the new signature: the handler now merges query params
and route path vars itself, calls dispatch(route, vars, body, type)
directly, and maps thrown RuntimeExceptions to error responses via
RESTCatalogAdapter.configureResponseFromException instead of the old
error-handler callback. Request outcome/metrics bookkeeping is
unchanged.

Include the phase 4a design spec and plan alongside the code change.
Update the README caveats/endpoint table (both locales) for the REST
front door's move to Iceberg 1.9.2: the RoutingHiveCatalog reflection
pin now names 1.9.2, and view GET routes are documented as supported
(real data, empty listing) instead of unsupported, since HiveCatalog
became a ViewCatalog.

Add a Changed subsection under CHANGELOG 2026-07-27 (both locales)
covering the version bump, the Jackson 2.18.3 pin, the re-vendored
RESTCatalogAdapter's handleRequest migration, the view-route behavior
delta, and client compatibility.

Record in smoke-stand/TEST-MATRIX (both locales) that sections A-D and
G were revalidated green on the upgraded jar, including the SQL layer.
The EXISTS routes went live with the 1.9.2 upgrade because
IcebergHttpHandler forwards any resolved route with no allowlist;
CHANGELOG.md/.ru.md wrongly said they were not yet wired in. Rewrite
both to state HEAD now answers 204/404 per the REST spec across every
prefix, and note the user-visible fix: HEAD on an existing table used
to 404 under 1.5.2, so PyIceberg's table_exists() misreported existing
tables as absent.

Also fix the pleonasm "пересобран заново" in CHANGELOG.ru.md, and add
a G18 row to both TEST-MATRIX files recording the exists-route checks.
The phase-4a Changed bullet claimed HEAD on an existing view returns
204, but no view exists on the smoke stand, so that case was never
observed. Scope the 204/404 claim to namespaces and tables (matching
matrix row G18) and note that VIEW_EXISTS is served by the same
unconditional dispatch and was only exercised for the 404 (missing
view) case in both locales.
…pper

CatalogHandlers.paginate() in Iceberg 1.9.2 parses pageToken with
Integer.parseInt and throws NumberFormatException when it is null, which
is exactly what a first-page request (pageSize without a prior token)
sends. IcebergHttpHandler now defaults pageToken to an empty string when
pageSize is present and pageToken is absent, without disturbing the
existing path-vars-win-over-query-params merge order.

Also:
- README.md/README.ru.md: add the three HEAD exists routes (namespace,
  table, view) to the Supported endpoints table; they are already live
  and covered by CHANGELOG and TEST-MATRIX row G18.
- CHANGELOG.md/CHANGELOG.ru.md: note that listings now honor
  pageSize/pageToken and return next-page-token, which Iceberg 1.5.2 did
  not support at all.
- IcebergHttpHandler: remove the unreachable statusForException helper
  and its RESTException import, dead since the error-mapping rewrite to
  RESTCatalogAdapter.configureResponseFromException.
The Iceberg REST listener may be unauthenticated, so error bodies must
not include internal package/file/line details. Blank the stack after
RESTCatalogAdapter maps the exception, and give readBody's parse
failures a targeted 400 instead of falling through to the 500
catch-all.
Iceberg 1.9.2's ConfigResponse gained an endpoints list; a silent server
leaves modern clients assuming the full write-capable endpoint set. List
only the nine routes this read-only front door actually serves, and route
/v1/{prefix}/config through the same config writer used by
/v1/config?warehouse=<catalog> instead of falling through to the vendored
adapter's own CONFIG case, which advertised writes and ignored the prefix.
Add a HEAD helper and five tests for namespace-exists and table-exists
(existing/missing namespace, existing/missing table, second-prefix
table). Seed "t1" and "events" with a real metadata_location backed by
an actual local metadata.json, since HiveCatalog's default tableExists
does a full loadTable and the existing "orders"/"shipments" fixtures
are deliberately non-loadable placeholders for the non-Iceberg case.
RFC 9110 forbids a body on HEAD responses. writeJson/writeError/
writeErrorResponse always wrote one, so the JDK HttpServer rejected the
write with IOException: stream closed for every HEAD error (e.g. exists
probes on missing namespaces/tables), which escaped into doHandle's
catch-all as a WARN with a full stack trace on every occurrence. Route
all three writers through a shared sendBody helper that, for HEAD,
sends only the status with content-length -1 and closes the body
unwritten, mirroring the existing 204 exists-response.
response.body() is always "" for a HEAD response regardless of what the
server sent (java.net.http.HttpClient discards it per RFC 9110), so it
proved nothing about the writeErrorResponse HEAD fix. Empirically, the
JDK HttpServer also never sets a Content-Length header for HEAD
responses, so that alternative doesn't discriminate either - reverting
the production fix left both assertions green.

The one thing that does differ is that the reverted code's failed body
write throws, which the handler's catch-all logs as "Unhandled error
serving ..." at WARN. Assert on that instead by attaching a log4j
appender to the handler's logger for the duration of the request and
requiring it to have logged nothing.
RFC 9110 forbids a body on HEAD responses. respond() in
ManagementHttpServer always wrote one, so the JDK HttpServer rejected
the write with IOException: stream closed on every HEAD request to
/healthz, /metrics and /readyz - a shape health checkers commonly use.
This server has no catch-all logger, so the failure was discarded
silently. Route the write through a sendBody helper that, for HEAD,
sends only the status with content-length -1 and closes the body
unwritten, mirroring the equivalent fix just applied to
IcebergHttpHandler.
balookrd added 20 commits July 28, 2026 00:18
Add three run_rest_smoke assertions for the phase-4b hardening: no server
stack trace in error responses, 400 on an unparseable request body, and
GET /v1/config advertising the served endpoints. Document all three plus
the prefixed /v1/{prefix}/config form and the no-body HEAD fix in
README/CHANGELOG (EN+RU), and record the stand rerun in TEST-MATRIX
(EN+RU) with rows G19-G21.
Extract the duplicated HEAD-safe response writer from ManagementHttpServer
and IcebergHttpHandler into util.HttpResponseWriter, and unit-test it
directly against a recording HttpExchange. The prior
ManagementHttpServerTest only compared HEAD vs GET status codes, which stay
identical whether or not the "stream closed" guard exists, so it could not
fail on a regression; HttpResponseWriterTest asserts the actual
(status, contentLength) pair and body bytes and does fail without the guard.

Strengthen run_rest_smoke's /v1/config assertion: instead of only checking
that an "endpoints" key exists, assert a concrete read route is advertised
and that no write route (POST .../namespaces, DELETE) is, against both the
unprefixed and prefixed config endpoints. Update TEST-MATRIX row G21 and its
revalidation log (EN/RU) to match what the runner actually checks.
Both config branches in IcebergHttpHandler.doHandle dispatched on path
only, never on method, so POST/DELETE/HEAD on /v1/config and
/v1/{prefix}/config answered 200 instead of 404. For the prefixed form
this was a phase-4b regression: before this phase such requests fell
through to Route.from(method, path), which is GET-only for CONFIG and
returned null, producing the unknown-route 404.

Require GET on both forms; any other method now falls through to the
same unknown-route 404 as before. The unprefixed form's fix is a
deliberate correction of pre-existing behavior (POST /v1/config already
answered 200 prior to this phase), not part of the original phase scope.
RoutingMetaStoreClient now answers createTable, dropTable,
alter_table_with_environmentContext, lock, checkLock, unlock, showLocks and
heartbeat instead of throwing UnsupportedOperationException, so Iceberg's
HiveCatalog write path and MetastoreLock can operate through the proxy's
in-process IMetaStoreClient bridge. Table dbName arguments are translated on
a copy for non-default catalogs, matching the existing rewriteTable
convention; LockRequest is passed through untranslated since the proxy's own
LockHandler resolves its database names downstream.

This is client plumbing only: no gate restricts writes to the default
catalog yet, that follows in a later task.
…, coverage

alter_table_with_environmentContext's translation test could not fail: the
fixture recorded only the two String db/tbl arguments and never the Table
argument's own dbName, so deleting the translateDbName(args[2]) call left the
test green. Record the Table's dbName in the fixture and assert on it.

Drop the unrequested showLocks() no-arg overload; Iceberg only calls the
1-arg showLocks(ShowLocksRequest), so it now falls through to the same
UnsupportedOperationException as every other unused overload.

Add missing coverage for checkLock, showLocks(ShowLocksRequest) and
heartbeat, and align the lock branch with its siblings by adding the
paramTypes.length guard it was missing.
Iceberg commits need a real HMS lock, and only the default catalog has
one: every other catalog is served by the synthetic shim, which grants
locks without conflict checking, so a commit there would take an
EXCLUSIVE lock that is always granted, believe it owns the table, and
race a concurrent writer into lost updates.

The gate keys on the catalog a namespace resolves to, never on the URL
prefix. The default prefix exposes other catalogs' databases as
<catalog><separator><db>, so a write to hdp/namespaces/apache__default
carries the default prefix but routes into the apache catalog; checking
the prefix would wave it through. Resolution reuses the proxy's own
CatalogRouter rather than a second name parser. RENAME_TABLE names no
namespace in its path, so both the source and the destination in its
body are checked. REPORT_METRICS is a POST but not a catalog write and
is deliberately outside the write-route set.
WriteRouteGate only checked CREATE_TABLE/UPDATE_TABLE/DROP_TABLE/
RENAME_TABLE/REGISTER_TABLE, but IcebergHttpHandler dispatches on the
full RESTCatalogAdapter.Route enum with no allowlist, so every other
write route reached dispatch() unguarded. Worst case: COMMIT_TRANSACTION,
the standard multi-table atomic-commit endpoint, let a client bypass the
gate entirely by bundling a federated-catalog table into an otherwise
legitimate commit. View routes are gated too, since HiveCatalog now
extends BaseMetastoreViewCatalog and takes the same lock path.

Extend the gate to the remaining write routes across their three body
shapes: path-shaped (CREATE_VIEW, UPDATE_VIEW, DROP_VIEW, DROP_NAMESPACE,
UPDATE_NAMESPACE - namespace from vars, same as the existing table
routes), rename-shaped (RENAME_VIEW - source and destination in the
body, both checked, same as RENAME_TABLE), and body-shaped
(CREATE_NAMESPACE's own namespace; COMMIT_TRANSACTION's tableChanges(),
where every identifier must resolve to the default catalog or the whole
request is refused).

Confirmed the hole with a COMMIT_TRANSACTION request mixing one
default-catalog table with one federated table: against the unpatched
gate it returned 404 (dispatched unguarded, failed for an unrelated
reason) instead of 403. It now refuses with 403 ForbiddenException.
hive-standalone-metastore pulls hadoop-hdfs 2.2.0 transitively via
orc-core, while hadoop-common resolves to 2.6.0 elsewhere in the tree.
2.2.0's DFSOutputStream calls the Hadoop-2.2-era
FSOutputSummer(Checksum,int,int) constructor, which 2.6.0's
FSOutputSummer no longer has, so every HDFS write from this JVM failed
with NoSuchMethodError. Exclude the transitive hadoop-hdfs and pin it
explicitly at 2.6.0 to match hadoop-common.

Independently, IcebergHttpHandler.doHandle only caught Exception, so an
Error such as this NoSuchMethodError escaped the handler entirely and
the JDK HTTP server abandoned the exchange with no response, hanging
the client instead of returning a 5xx. Widen the catch-all to Throwable
so no failure mode leaves a request unanswered, and add a test that
drives an Error through the real dispatch path (via a proxy-based test
fake, which passes Errors through unwrapped) and asserts a 500 comes
back rather than a hang.
Add HMS_SMOKE_REST_WRITE_TABLE-guarded coverage to run_rest_smoke: create,
load (asserting metadata-location) and drop a table on the default catalog,
then two negatives - a direct create under a non-default prefix and a
create under that prefix's federated namespace name reached through the
default prefix - both asserting 403. Also fix the pre-existing config
assertions, which still expected a read-only listing: the default catalog's
GET /v1/config and GET /v1/{prefix}/config now must advertise the five
table-write routes, and a new check on the non-default prefix's own config
proves the write/read asymmetry is actually observable via discovery.

Document, in both locales, that table writes are default-catalog only and
why (every other catalog is served by the synthetic lock shim, which grants
locks without conflict checking), that the gate is enforced on the resolved
catalog so a federated name cannot bypass it, and that GET /v1/config
advertises the asymmetry. Record the two fixes phase 5a needed to make
writes work at all: the hadoop-hdfs/hadoop-common version alignment (writes
are the first code path opening an HDFS output stream from the proxy's own
JVM) and the IcebergHttpHandler catch-all widened from Exception to
Throwable, so a JVM Error can no longer hang a client's connection forever.

Verified on the local stand: --scenario rest and --scenario all both
completed successfully, and the SQL layer (both HiveServer2 instances)
passed as the regression check for the Hadoop dependency change, since
table writes and Hive's own ACID commits share the same lock path.
hadoop-hdfs:2.6.0 pulls in xerces:xercesImpl/xml-apis for its offline
image/edits viewer only, never for the DFSClient runtime path this proxy
uses. xercesImpl ships META-INF/services/javax.xml.parsers.* entries, which
in the shaded fat jar would make a 2007-vintage parser with known CVEs the
JVM-wide JAXP provider for every DocumentBuilderFactory/SAXParserFactory
lookup in the process. Exclude both from the hadoop-hdfs dependency block
and document the convention in AGENTS.md.

Also add an exhaustiveness test tying WriteRouteGate.WRITE_ROUTES to every
RESTCatalogAdapter.Route constant, so a route added on the next vendored
adapter upgrade fails the test until explicitly classified as a write or a
deliberate non-write - closing the exact drift that once let
COMMIT_TRANSACTION and the view/namespace writes through unguarded.
…moke

The REST write smoke previously only drove create/load/drop plus two
CREATE_TABLE negatives. Extend the HMS_SMOKE_REST_WRITE_TABLE-guarded
block to add a real commit against the just-created table (asserting
the returned metadata-location changed, proving HiveTableOperations.commit
actually wrote a new metadata file instead of silently no-oping), a
rename round trip, and three more write-route gate negatives -
COMMIT_TRANSACTION on a federated table, CREATE_NAMESPACE with a
federated name, and rename with a federated destination namespace.
COMMIT_TRANSACTION was a critical bypass found during phase 5a and had
so far only been covered by unit tests. The block now drops both the
write table's original and renamed names defensively before creating,
so a rerun on a dirty stand cannot half-fail.

Document the new coverage in TEST-MATRIX.md/.ru.md (rows G26-G30) and
the 2026-07-28 revalidation-log entry.
The stand's Kerberos profile has kept the Iceberg REST listener off since
phase 1 because SPNEGO needed a GSS-capable curl inside the network; the
proxy container's curl is GSS-capable now, so there is no reason left to
keep the listener untested there while phase 5a's REST writes and the
hadoop-hdfs 2.6.0 bump both land on paths the Kerberos profile also uses.

Give the proxy an HTTP/proxy@REALM principal in its existing keytab, enable
rest-catalog.* with SPNEGO in the Kerberos properties, and fix two real bugs
the first real write under Kerberos surfaced: IcebergRestService was
building its own Kerberos-blind Configuration instead of reusing the
catalog's HiveConf (breaks with "Failed to specify server's Kerberos
principal name"), and the per-catalog conf was missing
dfs.data.transfer.protection, which the datanodes require for SASL data
transfer once an actual block write - not just a NameNode RPC - is
involved.

Update TEST-MATRIX(.ru).md and README(.ru).md to reflect the checks that
were actually run against the Kerberos profile.
RoutingMetaStoreClient now answers IMetaStoreClient.createDatabase,
dropDatabase(String,boolean,boolean,boolean) and alterDatabase instead of
throwing UnsupportedOperationException, delegating to the Thrift
create_database/drop_database/alter_database RPCs and translating names
through the existing CatalogNameTranslation. The Database payload passed to
create/alter is translated on a copy via the Thrift copy constructor, never
by mutating the caller's object.

RecordingThriftIface now records both the argument and the Database
payload's own name for alter_database, so a missing payload translation
fails the test (verified: removing it breaks
alterDatabaseTranslatesBothArgumentAndPayload with the expected mismatch,
restoring it goes back to green).
The dropDatabase case matched on 4-arg overloads by length alone, so
IMetaStoreClient's (String,String,boolean,boolean) default overload
(catName, dbName, ...) also matched and cast the dbName String to
Boolean, throwing ClassCastException instead of the usual
UnsupportedOperationException. Narrow the match to the exact
(String,boolean,boolean,boolean) signature, mirroring dropTable's
explicit paramTypes checks.

Also mirror dropTable's ignoreUnknownTab handling for
ignoreUnknownDb: catch NoSuchObjectException from the delegate call
and rethrow only when the flag is false, instead of silently
discarding it.

Strengthen RecordingThriftIface's drop_database case to record the
deleteData/cascade booleans (and throw NoSuchObjectException for an
unknown database) so argument-order regressions and the
ignoreUnknownDb behavior are covered by tests.
…erves

The front door's client plumbing already dispatches view writes, namespace DDL
and multi-table transaction commits through the same generic
RESTCatalogAdapter/RoutingHiveCatalog path table writes use, and
WriteRouteGate already gates all thirteen write routes for the default
catalog - but GET /v1/config only advertised the five table-write routes,
so a spec-compliant client would conclude the rest were unsupported.

Extend WRITE_ENDPOINTS with the eight confirmed Iceberg 1.9.2 Endpoint
constants for view CRUD+rename, namespace CRUD and transaction commit. Other
catalogs are unaffected: they still advertise reads only, matching the
synthetic lock shim that backs their writes with no real conflict checking.
Extend the HMS_SMOKE_REST_WRITE_TABLE-guarded block with namespace DDL,
view write and multi-table transaction-commit round trips, each asserting
the effect (property actually set, namespace actually gone, view actually
listed, metadata-location actually changed) rather than trusting the status
code alone. Prove the new transaction assertion discriminates by
temporarily demanding a no-op commit, confirming the runner fails, then
restoring it.

Rebuilt and restaged the jar and validated on the stand: namespace DDL had
never before reached a real metastore (the pre-phase jar answered 406 for
createDatabase), and now passes end to end on both the plain and Kerberos
profiles, the latter confirmed by genuine create_database/alter_database/
drop_database audit entries.

Correct stale "read-only" claims in the smoke-stand README (both locales)
and proxy properties comments left over from before phase 5a's write gate
and the Kerberos REST turn-on. Update README/CHANGELOG (both locales) to
state that view writes and transaction commit were already reachable after
phase 5a and are now officially advertised and covered, while namespace DDL
is genuinely new this phase. Update TEST-MATRIX (both locales) with the new
rows and a revalidation-log entry for this run.
…the smoke

Two advertised REST routes (view rename, view property update) were never
exercised by the smoke, and WriteRouteGate had thirteen write routes but only
five wire-level negatives. Extend the existing view round trip to update and
rename the view, asserting the property actually stuck and that the rename
moved the view (200 under the new name, 404 under the old) rather than
copying it. Add four more 403 negatives against a federated namespace under
the default prefix: CREATE_VIEW (using the full valid view body, since a stub
body 400s before the gate is even consulted), DROP_VIEW, DROP_NAMESPACE and
UPDATE_NAMESPACE.
…t drift

WriteRouteGate previously permitted a write when the namespace's owning
catalog could not be resolved; make that case a refusal instead, since an
unknown catalog is exactly the ambiguous case the synthetic lock shim's
lack of conflict checking makes unsafe to guess about (currently
unreachable in production, but the gate's shape should not rely on that).

Add a test tying WriteRouteGate.WRITE_ROUTES to
IcebergRestService.WRITE_ENDPOINTS one-to-one so a future write route added
upstream cannot silently lag in the advertised endpoint list, and pin the
integration test's positive endpoint assertions to the exact JSON-quoted
rendering so a dropped route constant cannot hide behind a sibling whose
longer rendering happens to share the same prefix (e.g. CREATE_VIEW vs
UPDATE_VIEW).

Also reorder IcebergHttpHandler.handle()'s finally block so the per-request
thread-local context is restored before the metric is recorded, so a
throwing recordRestRequest can no longer leak remoteAddress/remoteUser into
the next request served by that pool thread.
The Iceberg REST write round trip hardcoded the namespace ("smoke_rest_ns")
and view ("smoke_rest_view") names it creates and defensively pre-deletes,
unlike the write table which was already configurable via
HMS_SMOKE_REST_WRITE_TABLE. Against a real installation this could silently
drop a pre-existing namespace sharing the default name. Add
HMS_SMOKE_REST_WRITE_NAMESPACE and HMS_SMOKE_REST_WRITE_VIEW (defaulting to
the previous hardcoded values), document them in the runner's usage text and
env example, and set them explicitly in the stand's simple.env. The
defensive namespace pre-delete now only runs when the namespace is absent or
already empty (no tables, no views), refusing with a clear error otherwise
instead of destroying real content.

Also fix mktemp templates that put a literal suffix after the X's
(...XXXXXX.json / .sql / .out): BSD mktemp on macOS does not randomize X's
followed by a suffix, so every run reused the same filename, and a leftover
from an interrupted run (fail() bypasses the cleanup trap) broke the next
run. Put the X's last and append the suffix with mv after creation.

Finally, reword "write-роут" back to "write-поверхность"/"все write-роуты"
in three RU-only spots (README.ru.md, CHANGELOG.ru.md,
smoke-stand/README.ru.md) where "write surface" had been mistranslated as a
single route rather than the whole set of write routes.
@balookrd
balookrd merged commit 14af4de into main Jul 29, 2026
2 checks passed
@balookrd
balookrd deleted the feature/iceberg-rest-fe-phase1 branch July 29, 2026 07:17
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