Kubernetes Restores Quantity Decoding And Lets Clients Drop Managed Fields


kubernetes/kubernetes took 91 commits on master in this window. The one that can fail a list is in ParseQuantity. A 1.37 apiserver stored a few extreme quantity spellings that a later master build refused to decode, and the etcd store fails the whole LIST on the first object it cannot read. Clients can also ask for responses without metadata.managedFields, and the watch cache stops walking the btree twice per event.

141203 refused a decimal exponent that does not fit the int32 scale. Keep decoding exponents written before 1.38 puts the narrowing back for every spelling except one. A 1.37 apiserver narrowed the exponent and stored the object with the text it was given. The apiserver then failed ParseQuantity on the read from etcd. The etcd3 decoder does not skip a bad object. One undecodable quantity fails the list.

Narrowing is back. 1e4294967296 decodes to 1 again. 1e4294967297 narrows the way 1.37 did, the exponent becomes 1, and the value is 10, written back as 10. JSON and protobuf both have to survive a decode and an encode. Encoding again must keep a spelling that is already stored. The new compatibility test passes unchanged against v1.37.1.

A scale that lands on math.MinInt32 still returns ErrSuffix from ParseQuantity. Negating that scale overflows int32, and 1.37 hung while canonicalizing it, so no stored object should carry it. Fraction digits move the scale off that value. 1.25e2147483648, which 1.37 did store, decodes again. It does not fit an int64, so only the spelling has to survive. 1.5e2147483648 comes back as 150e2147483646.

100m and 1Gi never took this path. The objects at risk are ones a 1.37 apiserver already wrote with an exponent outside int32. If one of those objects is in etcd, every list of that resource fails until this decode is present.

ManagedFieldsOptOut is the gate for KEP-5958. It is alpha, and it is off by default in 1.38. The definition is in pkg/features/kube_features.go. While the gate is off, drop=metadata.managedFields is recorded as unrecognized and ignored. The media type still matches. The body is unchanged.

With the gate on, negotiation copies that exact value into MediaTypeOptions.Drop. Any other drop value fails that Accept clause, so application/json;drop=spec, application/json falls through to plain JSON. An empty drop= is ignored.

Accept: application/json;drop=metadata.managedFields

The transform runs in doTransformObject before as= conversions. Get, list, watch, Table, and PartialObjectMetadata honor it. The watch encoder cache key includes the drop flag, so a full object encoding is not reused for a client that asked to omit the field. dropManagedFields clears metadata.managedFields without copying the whole object. Lists are not cached, so the field is cleared in place. A single object is copied only along the path to that field. A follow up guards Unstructured lists and covers Table, protobuf, and CRDs.

Server side apply reads managedFields to see who owns which field. A client that sends this parameter and then applies has no manager history. The benchmark encodes one Pod and a list of 1000, typed and unstructured, in JSON, protobuf, and CBOR, with the field and without it. The commit records the cases and no timings. Leave the gate off until list clients and apply clients are separate.

processEvent used to call storage.Get for the previous object, then UpdateStoreLocked under the watch cache lock. The btree replace already returns the previous element. The update uses that return value and deletes the extra Get. Each watch event loses one lock acquisition and one tree walk. The event still carries the previous object, labels, and fields.

Flattening the indexer removes the generic Indexer wrapper. WatchCacheStorage now holds a btreeStore and a separate indexer behind its own RWMutex. The moved code is store_btree.go and watch_cache_storage.go. The same series deletes StoreLocked and passes the snapshot straight into the snapshotter. List and watch results stay the same.

GetList called DefaultFeatureGate.Enabled for ShardedListAndWatch on every object. The gate is read once before the loop. Shard matching is unchanged.

CertificateSigningRequestMLDSA is beta in 1.38. The versioned spec defaults to off. A second spec at the same version turns the gate on only when --min-compatibility-version is at least 1.38. A stock apiserver sets that flag one minor behind the binary, so a 1.38 process that still tolerates 1.37 peers leaves the gate off. The gate covers ML-DSA keys in spec.request.

Validation follows RFC 9881. Usages must include at least one of digitalSignature, contentCommitment, keyCertSign, or cRLSign. They must exclude keyEncipherment, dataEncipherment, keyAgreement, encipherOnly, and decipherOnly. With the gate off, a new request that carries an ML-DSA key is rejected. An object already stored with that key still validates, so turning the gate off later does not strand it.

The built in signers use the same usage rules when the public key is ML-DSA. Kubelet serving, kubelet client, and the apiserver client signer call ValidateMLDSAKeyUsages. A generic CSR update treats spec and status as immutable and skips the old field recheck. Status and approval subresources still run their own validators. Declarative validation marks spec immutable from 1.38.

GenerateSelfSignedCertKeyWithOptions no longer hardcodes a 2048 bit RSA key for --cert-dir and the loopback certificate. A GenerateKey function on SelfSignedCertKeyOptions defaults to that RSA key, so current files stay the same. Callers can pass ECDSA P-256 or ML-DSA-65. Key encipherment is set only for RSA. PEM encoding goes through keyutil.MarshalPrivateKeyToPEM.

DRA drivers that publish an attribute or a capacity as <driverName>/foo will stop publishing. The apiserver still accepts that name. The check sits in the ResourceSlice controller in k8s.io/dynamic-resource-allocation, and it is on by default. The refusal exists because the qualified name and the bare name are the same attribute. Constraint matching in 1.37 prefers the fully qualified form, and CEL lookup is not deterministic. The qualified form alone is also rejected, because it only makes the slice larger. Invalid slices are not retried. Publishing stops until the driver replaces them. The opt out is ValidateQualifiedNames(false) on the kubelet plugin and on resourceslice.Options. Use it for an old driver that already emits the prefixed name. Upgrade the apiserver before the driver, and downgrade in reverse.

Pod binding now returns the pod resourceVersion from GuaranteedUpdate on the status ListMeta. validation-gen accepts time.Duration bounds as quoted Go strings such as +k8s:minimum="1s", and it rejects a bare integer on those fields. Bounds print with %v, so a duration shows up as 1s. Changelog directory moves for 1.34.12, 1.35.9, 1.36.5, and 1.37.1 are release robot commits.