From d98d5fe745c811d737d52cf60d3a37ba60378574 Mon Sep 17 00:00:00 2001 From: "kubernetes-prow[bot]" <181008794+kubernetes-prow[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 02:32:17 +0000 Subject: [PATCH] Merge pull request #139964 from liggitt/cri-api-env-json [master] Restore string JSON encoding of cri-api KeyValue (cherry picked from commit e62c2b04709d77f028b2d2dd3b57be567b4e7a60) Signed-off-by: Brad Davidson --- .../cri-api/pkg/apis/runtime/v1/api_json.go | 47 +++++++ .../pkg/apis/runtime/v1/api_json_test.go | 115 ++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 staging/src/k8s.io/cri-api/pkg/apis/runtime/v1/api_json.go create mode 100644 staging/src/k8s.io/cri-api/pkg/apis/runtime/v1/api_json_test.go diff --git a/staging/src/k8s.io/cri-api/pkg/apis/runtime/v1/api_json.go b/staging/src/k8s.io/cri-api/pkg/apis/runtime/v1/api_json.go new file mode 100644 index 00000000000..2e0b6dc2fd8 --- /dev/null +++ b/staging/src/k8s.io/cri-api/pkg/apis/runtime/v1/api_json.go @@ -0,0 +1,47 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import "encoding/json" + +// MarshalJSON() preserves pre-1.34 JSON encoding of value as a string (not base64), +// stomping non-utf-8 data with the utf8 replacement character. +func (k *KeyValue) MarshalJSON() ([]byte, error) { + return json.Marshal(stringKeyValue{ + Key: k.GetKey(), + Value: string(k.GetValue()), + }) +} + +// UnmarshalJSON preserves pre-1.34 JSON decoding of value as a string (not base64), +// stomping non-utf-8 data with the utf8 replacement character. +func (k *KeyValue) UnmarshalJSON(data []byte) error { + v := stringKeyValue{} + if err := json.Unmarshal(data, &v); err != nil { + return err + } + k.Key = v.Key + k.Value = []byte(v.Value) + return nil +} + +// stringKeyValue matches the structure used to json-encode pre-1.34. +// Non-UTF-8 characters in Value are coerced to the replacement character on encode/decode. +type stringKeyValue struct { + Key string `json:"key,omitempty"` + Value string `json:"value,omitempty"` +} diff --git a/staging/src/k8s.io/cri-api/pkg/apis/runtime/v1/api_json_test.go b/staging/src/k8s.io/cri-api/pkg/apis/runtime/v1/api_json_test.go new file mode 100644 index 00000000000..bfa4494afe7 --- /dev/null +++ b/staging/src/k8s.io/cri-api/pkg/apis/runtime/v1/api_json_test.go @@ -0,0 +1,115 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "bytes" + "encoding/json" + "testing" +) + +func TestKeyValueCompat(t *testing.T) { + testcases := []struct { + name string + envs []*KeyValue + variantJSON string + expectedJSON string + expectedRoundTripped []*KeyValue + }{ + { + name: "null", + envs: nil, + expectedJSON: `null`, + expectedRoundTripped: nil, + }, + { + name: "zero-length list", + envs: []*KeyValue{}, + expectedJSON: `[]`, + expectedRoundTripped: []*KeyValue{}, + }, + { + name: "zero-value env", + envs: []*KeyValue{{}}, + expectedJSON: `[{}]`, + expectedRoundTripped: []*KeyValue{{}}, + }, + { + name: "ascii env", + envs: []*KeyValue{{Key: "key", Value: []byte("value")}}, + expectedJSON: `[{"key":"key","value":"value"}]`, + expectedRoundTripped: []*KeyValue{{Key: "key", Value: []byte("value")}}, + }, + { + name: "utf8 env", + envs: []*KeyValue{{Key: "key", Value: []byte("Iñtërnâtiônàlizætiøn🐹")}}, + expectedJSON: `[{"key":"key","value":"Iñtërnâtiônàlizætiøn🐹"}]`, + expectedRoundTripped: []*KeyValue{{Key: "key", Value: []byte("Iñtërnâtiônàlizætiøn🐹")}}, + }, + { + name: "non-utf8 env", + envs: []*KeyValue{{Key: "key", Value: []byte{'A', 0x80, 'Z'}}}, // invalid utf8 continuation byte (0x80) + variantJSON: `[{"key":"key","value":"A` + "\x80" + `Z"}]`, // an alternate JSON input containing the invalid utf8 byte that should coerce to the same result + expectedJSON: `[{"key":"key","value":"A\ufffdZ"}]`, // coerced to utf8 replacement character (\ufffd) on marshal + expectedRoundTripped: []*KeyValue{{Key: "key", Value: []byte("A\ufffdZ")}}, // round-trips to replacement character (\ufffd) on unmarshal + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + data, err := json.Marshal(tc.envs) + if err != nil { + t.Fatal(err) + } + + if string(data) != tc.expectedJSON { + t.Fatalf("json differed:\nwant: %s\ngot: %s", tc.expectedJSON, string(data)) + } + + verifyJSON(t, data, tc.expectedRoundTripped) + if len(tc.variantJSON) > 0 { + verifyJSON(t, []byte(tc.variantJSON), tc.expectedRoundTripped) + } + }) + } +} + +func verifyJSON(t *testing.T, data []byte, expectedRoundTripped []*KeyValue) { + t.Helper() + + var rt []*KeyValue + if err := json.Unmarshal(data, &rt); err != nil { + t.Fatal(err) + } + if (rt == nil) != (expectedRoundTripped == nil) { + t.Fatalf("expected value (%#v) does not match actual round-tripped value (%#v) for nil", expectedRoundTripped, rt) + } + if rt == nil { + return + } + if len(rt) != len(expectedRoundTripped) { + t.Fatalf("length of expected value (%#v) does not match length of actual round-tripped value (%#v)", expectedRoundTripped, rt) + } + for i := range expectedRoundTripped { + if want, got := expectedRoundTripped[i].Key, rt[i].Key; want != got { + t.Fatalf("item[%d].key does not match: %s vs %s", i, want, got) + } + if want, got := expectedRoundTripped[i].Value, rt[i].Value; !bytes.Equal(want, got) { + t.Fatalf("item[%d].value does not match: %v vs %v", i, want, got) + } + } +}