Skip to content

fix(proto): nest TypeMeta in common Unknown per upstream wire format#59

Open
indyjonesnl wants to merge 2 commits into
calfonso:mainfrom
indyjonesnl:fix/protobuf-unknown-wire-format
Open

fix(proto): nest TypeMeta in common Unknown per upstream wire format#59
indyjonesnl wants to merge 2 commits into
calfonso:mainfrom
indyjonesnl:fix/protobuf-unknown-wire-format

Conversation

@indyjonesnl

Copy link
Copy Markdown
Contributor

What

Align the prost runtime.Unknown definition in common::protobuf with the
real Kubernetes wire format, and add a proptest fuzz harness covering the
JSON↔protobuf round-trip.

k8s.io/apimachinery/pkg/runtime.Unknown is:

message Unknown {
  optional TypeMeta typeMeta        = 1;   // nested message
  optional bytes    raw             = 2;
  optional string   contentEncoding = 3;
  optional string   contentType     = 4;
}

Today the prost struct flattens TypeMeta in at tags 1+2 (apiVersion,
kind) and shifts raw/contentEncoding/contentType to 3–5. Its own
encoder and decoder agree, so the round-trip tests pass — but any upstream
client (kubectl, client-go) that decodes the envelope parses the apiVersion
string bytes as a nested TypeMeta message and hits
proto: illegal wireType 6 on byte 'v' = 0x76.

Why this isn't already breaking serving (and why it still matters)

The api-server's HTTP serving path does not use this struct — it
hand-rolls the correct nested layout in
middleware.rs::wrap_json_in_protobuf_with_type_meta, whose own comment
notes the prost struct is wrong and is deliberately bypassed. So clients are
fine against the live server today.

But that leaves two parallel encoders and a latent landmine: anything that
serializes through common::encode_protobuf emits broken bytes. The struct is
currently test-only in this repo, which is exactly why the bug has stayed
invisible. This PR makes the prost struct the single correct source of truth.

Follow-up (proposed)

With the struct fixed, middleware.rs could route through the now-correct
common::Unknown and delete the duplicate hand-rolled varint encoder, leaving
one wire-format definition instead of two. Happy to do that in a follow-up if
you'd take it.

Changes

  • common/src/protobuf.rs: nested UnknownTypeMeta at field 1, raw at 2,
    contentEncoding 3, contentType 4; update encode_protobuf /
    decode_protobuf / extract_type_meta; add a wire-shape test that fails
    fast if field 1 ever stops being an embedded message.
  • common/tests/fuzz_roundtrip_jsonproto_test.rs: proptest harness (ported
    small-scale from apimachinery's roundtrip.go) — 6 resources × 50 random
    instances, asserting JSON stability and k8s\0 Unknown-envelope recovery.

Tests

cargo test -p rusternetes-common — fuzz 14/14, protobuf lib 7/7 (incl new
wire-shape test). cargo test -p rusternetes-api-server --test protobuf_test
2/2. cargo fmt --all -- --check clean.

🤖 Generated with Claude Code

indyjonesnl and others added 2 commits May 30, 2026 09:34
Port a small-scale version of upstream apimachinery's
`pkg/api/apitesting/roundtrip/roundtrip.go` (the randfill-driven
codec roundtrip harness) to rusternetes. For each of six
representative resources — Pod, Deployment, Service, ConfigMap,
Secret, Event — generate 50 random instances per case and verify:

  1. `decode(encode(decode(x)))` is stable as `serde_json::Value`.
  2. The `k8s\0`-prefixed protobuf `Unknown` envelope round-trips
     and recovers the original `apiVersion` / `kind` plus the
     wrapped object.

Strategies cap collection sizes at 2 and use a deterministic
`make_object_meta` helper so values stay reproducible across runs.
The Event strategy uses whole-second `metav1.Time` and exact-
microsecond `metav1.MicroTime` values to dodge sub-microsecond
truncation; that limitation is documented in the file header.

Adds `proptest = "1"` to `crates/common` dev-dependencies. All 14
tests run in <1s on a developer laptop, well under the 30s budget.

(cherry picked from commit 0d8f773)
`runtime.Unknown` in k8s.io/apimachinery is:

    message Unknown {
      optional TypeMeta typeMeta        = 1;   // nested message
      optional bytes    raw             = 2;
      optional string   contentEncoding = 3;
      optional string   contentType     = 4;
    }

but the prost `Unknown` in `common::protobuf` flattens TypeMeta in at
tags 1+2 (apiVersion, kind) and shifts raw/contentEncoding/contentType
to 3-5. Its own encoder and decoder agree, so the round-trip tests pass
— but any upstream client (kubectl, client-go) decoding the envelope
parses the apiVersion string bytes as a nested TypeMeta message and hits
`proto: illegal wireType 6` on byte 'v'=0x76.

The api-server serving path dodges this today by hand-rolling the
correct nested layout in `middleware.rs::wrap_json_in_protobuf_with_type_meta`
instead of using this struct — its own comment notes the prost struct is
wrong. That leaves two parallel encoders and a latent landmine: anything
that routes serialization through `common::encode_protobuf` emits broken
bytes.

Align the prost struct with the real wire format (nested `UnknownTypeMeta`
at field 1, raw at 2, contentEncoding 3, contentType 4), update
encode/decode/extract_type_meta accordingly, and add a wire-shape test
that fails fast if field 1 ever stops being an embedded message.

Co-Authored-By: Claude Code <noreply@anthropic.com>
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