Compare commits

...

212 Commits

Author SHA1 Message Date
Kubernetes Publisher 04507a37f6 Merge pull request #132942 from thockin/kyaml
Add KYAML support to kubectl

Kubernetes-commit: 1451dd1b0873e801e082f3a06a52685bcd68dcac
2025-07-25 02:42:58 +00:00
Kubernetes Publisher 50e39b11cd Merge pull request #132935 from benluddy/cbor-bump-custom-marshalers
KEP-4222: Adopt text and JSON transcoding support for CBOR.

Kubernetes-commit: dfc0998baa4d6c2cd630aa3c5b8def4e9b1fcd8e
2025-07-24 22:42:58 +00:00
Tim Hockin 7d108e8f2c Re-vendor sigs.k8s.io/yaml @ v1.6.0
Kubernetes-commit: 8182a27f3b0769cefe1bcebfb938a7bafd51c88e
2025-07-24 11:46:03 -07:00
Kubernetes Publisher 58c4eb072e Merge pull request #133130 from ylink-lfs/chore/residual_boolptr_removal
chore: residual boolptr and intptr removal with ptr.To

Kubernetes-commit: c5ffd67cd212b6455436315d5f569742eb1da2e3
2025-07-22 17:56:33 -07:00
ylink-lfs 38a24e68ed chore: residual boolptr and intptr removal
Kubernetes-commit: b070b0a5c5ff3d6e356852c67c8ef325aa750daa
2025-07-23 00:57:50 +08:00
Kubernetes Publisher c04562bf9e Merge pull request #133112 from yongruilin/master_vg-cleanup
chore(validation-gen): cleanup and fix for comments and doc

Kubernetes-commit: 405edb50ca70b733ff9b4d71636943d2978e9f26
2025-07-21 18:08:32 -07:00
yongruilin c91403d681 fix(validation-gen): correct typos in comments and documentation
Kubernetes-commit: 8b558a1bc3e29610dd53a46291dc94b00da0573d
2025-07-21 21:24:45 +00:00
Kubernetes Publisher b92abb2d81 Merge pull request #133008 from aaron-prindle/master-pr-vg-union-tags-and-item-tag-chaining-support
feat(validation-gen): Add union validation rule tags and enable +k8s:item chaining to union tags

Kubernetes-commit: 34b9a9958dc269a09bb6ad6dc992712bd2a51d25
2025-07-17 21:02:44 +00:00
Kubernetes Publisher 9b71f293aa Merge pull request #133020 from pohly/apimachinery-list-map-keys
support optional listMapKeys in server-side apply

Kubernetes-commit: d33af7f7efc7ff5d8813a85290189883167a5226
2025-07-17 17:02:43 +00:00
Aaron Prindle c58c5e474b add validate/zeroorone_test.go and add +k8s:zeroOrOneOfMember output tests
Kubernetes-commit: be72d963b887139619bfd404c507ef9d92cf8914
2025-07-16 22:00:31 +00:00
Aaron Prindle ada13e71d1 feat(validation-gen): add +k8s:zeroOrOneOfMember tag validator and associated validate method
Kubernetes-commit: 10b20852e3c5e424534cdd0a9eda611a1dde9b9a
2025-07-16 21:59:57 +00:00
Aaron Prindle d9c68830cd add validate/union_test.go and add +k8s:unionMember and +k8s:unionDiscriminator output tests
Kubernetes-commit: 81f18759e6eabb4b1e14c91a895118176e5d4866
2025-07-16 21:59:24 +00:00
Aaron Prindle 76e5596af4 feat(validation-gen): add +k8s:unionMember and +k8s:unionDiscriminator tag validators and associated validate methods
Kubernetes-commit: 5bc9b69114102e10df03f30552c93caa44d69622
2025-07-16 21:57:30 +00:00
Ben Luddy fa4b7ae54f KEP-4222: Adopt text and JSON transcoding support for CBOR.
Kubernetes-commit: 0b60c12194766e1473e65790369d5a8bcee537c6
2025-07-11 13:23:36 -04:00
Patrick Ohly 3d4cbfbb07 SSA: test optional map keys
As of structured-merge-diff v6.3.0, list map keys may be optional, as long as
at least one key is provided.

Kubernetes-commit: a1a85ddb161c33adaabf6758f8787f86951df601
2025-07-08 11:58:24 +02:00
Jordan Liggitt 2e350ba61b sigs.k8s.io/structured-merge-diff/v6 v6.3.0
Kubernetes-commit: 4d34975a46658efd90274c5fe05d46732e04ca67
2025-07-16 16:57:41 -04:00
Patrick Ohly 2ee6665f26 SSA: add integration tests
test/integration/apiserver/apply covers the behavior of server-side-apply (SSA)
for official APIs. But there seem to be no integration tests which cover the
semantic of SSA like adding/removing/updating entries in a list map. This adds
such a test.

It needs an API which is under control of the test and uses
k8s.io/apimachinery/pkg/apis/testapigroup for that purpose, with some issues
fixed (OpenAPI code generation complained) and a new list map added.

Registering that API group in the apiserver needs a REST storage and
strategy. The API group only gets added in the test. However, the production
code has to know about it. In particular,
pkg/generated/openapi/zz_generated.openapi.go has to describe it.

Kubernetes-commit: 3357e8fc057168fef06b08eaef696511502dcafd
2025-07-08 10:36:53 +02:00
Kubernetes Publisher a75d3d8a0f Merge pull request #132979 from ylink-lfs/chore/residual_intptr_removal
chore: residual intptr removal with ptr.To

Kubernetes-commit: 2fdba619aba197381fd284426e1a1bd4022c3e23
2025-07-16 21:02:36 +00:00
ylink-lfs 3130ae84d8 chore: residual intptr removal with ptr.To
Kubernetes-commit: 5b4c1872a0fd628c53cb8978bbc6ed61bbd5674b
2025-07-16 09:07:25 +08:00
Kubernetes Publisher 1ebcba2516 Merge pull request #132871 from dims/bump-k8s.io/kube-openapi-to-latest-SHA-f3f2b991d03b
Bump k8s.io/kube-openapi to latest SHA (f3f2b991d03b) and sigs.k8s.io/structured-merge-diff/{v4 => v6}

Kubernetes-commit: 48e04d0d6c43cfc4729857775252b89b68d65b87
2025-07-15 09:02:35 +00:00
Kubernetes Publisher 8b4e5d086b Merge pull request #132513 from xiaoweim/validation-cleanup-invalid
Cleanup: Remove field name from invalid field detail message

Kubernetes-commit: 9f97857669c94332f2695c6216885937e4e9556d
2025-07-15 05:02:28 +00:00
Kubernetes Publisher 3b00b62c4b Merge pull request #132934 from ylink-lfs/chore/residual_strptr_removal
chore: residual strPtr utility removal with ptr.To

Kubernetes-commit: 63da3857486aa9b8e36ed06c8ba9aec337bf41d9
2025-07-14 16:42:53 -07:00
ylink-lfs c73d18dbc8 chore: residual strPtr utility removal with ptr.To
Kubernetes-commit: ad03cb87336bfdfd7fd994de697763e1390e2ca2
2025-07-15 00:31:20 +08:00
Kubernetes Publisher d5a1ab82b5 Merge pull request #132907 from PatrickLaabs/132749-boolPtrFn
chore: removed boolPtrFn helpers with ptr package implementation

Kubernetes-commit: 07f3e2f01b715549bf93a8b4ee08642052cae6f9
2025-07-13 05:02:20 +00:00
Kubernetes Publisher a3a3a5d4bd Merge pull request #132469 from yongruilin/master_vg_ratcheting-list
feat(validation-gen): Enhance validation with new rules and core refactoring

Kubernetes-commit: 2b7de3ba74a9706abdc82cbe4573f9266487df14
2025-07-12 20:04:21 -07:00
PatrickLaabs ba86525683 chore: removed boolPtrFn helpers with ptr package implementation
Kubernetes-commit: ba45e37b24bf378c2b4d0a7dec059d35b6774883
2025-07-12 11:35:07 +02:00
Davanum Srinivas 4962c2625e Bump k8s.io/kube-openapi to latest SHA (f3f2b991d03b)
Signed-off-by: Davanum Srinivas <davanum@gmail.com>

Kubernetes-commit: ebc1ccc491c944fa0633f147698e0dc02675051d
2025-07-10 09:21:52 -04:00
xiaoweim 5937fdff41 Cleanup: Remove field name from invalid field detail message
Kubernetes-commit: 61542e7a98760726736e48897e0fab85e8ad443a
2025-06-24 19:55:28 +00:00
Ben Luddy b025858346 Bump to github.com/fxamacker/cbor/v2 v2.9.0.
Kubernetes-commit: 917659269af60f8ca960deeb0991df93e5ad1635
2025-06-24 14:25:43 -04:00
Aaron Prindle c904feb835 feat: add +k8s:neq tag which enforces field is neq to a specified comparable value
Kubernetes-commit: fc1c832c4955c77c242a0afff2dfb75110cf004d
2025-06-16 16:06:55 +00:00
yongruilin e6bf2975f4 feat(validation-gen): add k8s:item
- Added new validation functions for items in slices and maps, allowing for flexible matching and validation based on specified criteria.
- Introduced `SliceItem` function to handle item validation logic, including support for matching and validating based on unique identifiers.

Co-authored-by: Aaron Prindle <aprindle@google.com>

Kubernetes-commit: 5cc2721f6c6913644f7ed051d0e522145153933a
2025-07-12 04:15:23 +00:00
yongruilin c904af427c feat: Add validation ratcheting for subfields tag
Kubernetes-commit: af05aa61d9cf9a1f6fdfa130512b9b1db1b08860
2025-06-04 23:20:50 +00:00
yongruilin 3bd79837f5 chore: improve error rendering and add unit tests for ErrorMatcher
Kubernetes-commit: daef13ecc36554d4d1b6b3efeb235820d7a3ad22
2025-07-12 04:11:41 +00:00
yongruilin b31e2b6043 feat(validation-gen): enhance validation functions for slices and maps
- Introduced MatchFunc type for flexible comparison in EachSliceVal and EachMapVal.
- Support ratcheting for list(eachSliceVal)
- Support ratcheting for map(eachMapVal)
- Added new test cases for various comparable and non-comparable structures.
- Improved error handling and validation checks in the validation generation process.

Co-authored-by: Tim Hockin <thockin@google.com>
Co-authored-by: Aaron Prindle <aprindle@google.com>

Kubernetes-commit: b059bb55143149c8d08e5ca2e3e3287dc0477648
2025-07-12 04:08:36 +00:00
Kubernetes Publisher ed63805e81 Merge pull request #132823 from yongruilin/master_vg_enum
feat(validation-gen): add k8s:enum validators

Kubernetes-commit: bb415b6933d31355276efac2edb71d70c0314036
2025-07-09 17:53:35 -07:00
yongruilin 882f988ee5 feat(validation-gen): add Enum validator function
Kubernetes-commit: 345641f106bc174b55d40d87bd9a0a8a78f10e6c
2025-07-08 18:37:42 +00:00
Kubernetes Publisher b18bb6a9e8 Merge pull request #132792 from ylink-lfs/chore/typo_invaILd
chore: typo 'invaILd' occurrence replacement

Kubernetes-commit: 7948fec34bad799f47a644023cdfefa26bb30684
2025-07-08 05:02:02 +00:00
Kubernetes Publisher cd1624cbbf Merge pull request #132794 from PatrickLaabs/132749-boolptr
chore: replacement of boolPtr helper functions to ptr packge

Kubernetes-commit: 5f34f9233b2e2f9714b71dc1e1a0dbf24548866c
2025-07-07 20:13:26 -07:00
PatrickLaabs f90b6830ca chore: replacement of helper functions to ptr packge
Kubernetes-commit: cfd65c5f74dbb5fa02e7b20c3b1039193567ba53
2025-07-07 21:28:54 +02:00
ylink-lfs e76ac9371e chore: typo invaILd occurrence replacement
Kubernetes-commit: d9de37d9316e25cad5bea7ab8694b64bf76b43e4
2025-07-08 00:08:34 +08:00
Kubernetes Publisher a9de165b70 Merge pull request #132314 from thockin/jp_nicer_api_errors
Nicer value rendering in API errors

Kubernetes-commit: 2a4b5f64761f1578409c3f69ef7aaab614cdd907
2025-07-03 09:01:49 +00:00
Kubernetes Publisher b86b632271 Merge pull request #132675 from dims/bump-sigs-k8s-io-json-no-code-changes
Bump sigs.k8s.io/json to latest - no code changes

Kubernetes-commit: e47ac3eb6faa97874658dc281c72b5623f994801
2025-07-03 01:01:50 +00:00
Kubernetes Publisher 7548d4da2f Merge pull request #132676 from dims/bump-go.yaml.in/yaml/v3-to-v3.0.4
Bump go.yaml.in/yaml/v3 to v3.0.4

Kubernetes-commit: 01c03ae9cf7b1371c8bc2bdf12d9244e63e83750
2025-07-02 17:01:56 +00:00
Davanum Srinivas 45e4ebb75e Bump go.yaml.in/yaml/v3 to v3.0.4
Signed-off-by: Davanum Srinivas <davanum@gmail.com>

Kubernetes-commit: 58e620cc4403d30f9fb6aab245cfb47db17957de
2025-07-02 07:37:06 -04:00
Davanum Srinivas a864156cb7 Bump sigs.k8s.io/json to latest - no code changes
Signed-off-by: Davanum Srinivas <davanum@gmail.com>

Kubernetes-commit: 00f8cbae6b8fd3799a1a044abcefdbb572d35b27
2025-07-02 07:32:24 -04:00
Kubernetes Publisher 852f12619b Merge pull request #132654 from Jefftree/b-openapi
Bump kube-openapi

Kubernetes-commit: db49c25956df36c777213251c4a47d6d9ee1c5ea
2025-07-01 21:01:44 +00:00
Kubernetes Publisher 1880ea593e Merge pull request #132585 from jpbetz/drop-openapi-name-dependant-test
Drop test that checks openAPI resource that checks OpenAPI resource name

Kubernetes-commit: e140f056dc861754c966560638d09021a1f351f9
2025-07-01 17:01:51 +00:00
Jefftree 7f21f07c1c Update vendor
Kubernetes-commit: d04ee27c98ba91680ac6c6a8ade9e33d7ee44569
2025-07-01 15:23:58 +00:00
Jefftree 48f740b63c pin kube-openapi to v0.0.0-20250628140032-d90c4fd18f59
Kubernetes-commit: b41d375b8881f25ff5fe7775b4dedaba1eaa3f02
2025-07-01 15:21:22 +00:00
Kubernetes Publisher 10cef9d38d Merge pull request #132472 from xiaoweim/validation-cleanup
Cleanup: Remove redundant detail messages in field.Required

Kubernetes-commit: eb1b603cda3b956e52bddf3b51748191e80a59a6
2025-07-01 09:22:56 +00:00
Kubernetes Publisher 6af1141f10 Merge pull request #132465 from yongruilin/master_vg_fix-fuzz-test
fix: versioned validation test avoid incorrect conversion

Kubernetes-commit: bc9a78479fb7d5317ee2d5c8ac95acc0d1661272
2025-06-30 20:58:29 -07:00
Joe Betz b0db85e7d4 Drop test that checks openAPI resource name since we currently don't guarantee name stability in the API
Kubernetes-commit: f93b4408a77a66fbeccfff99406b0dea10991e79
2025-06-27 15:02:34 -04:00
xiaoweim cfeb5db0a7 Cleanup: Remove redundant detail messages in field.Required
Kubernetes-commit: 8632257c9340aeae824c99642376a78f69b3ea5d
2025-06-26 19:41:17 +00:00
yongruilin 5c20c365a1 fix: versioned validation test avoid incorrect conversion
Kubernetes-commit: a55318fe1493728ea0f6752375f3e41d821b4d0d
2025-06-18 05:43:31 +00:00
Kubernetes Publisher d6651abdfe Merge pull request #132357 from dims/drop-usage-of-forked-copies-of-goyaml.v2-and-goyaml.v3
Drop usage of forked copies of goyaml.v2 and goyaml.v3

Kubernetes-commit: c1afec6a0b15ca1ed853c1321ac2c972488bf5b8
2025-06-25 17:22:36 +00:00
Kubernetes Publisher f3d86859ab Merge pull request #132504 from jpbetz/name-formats
Introduce OpenAPI format support for k8s-short-name and k8s-long-name

Kubernetes-commit: 1d932bd6cc951b9182d07d701946aebaf667df94
2025-06-25 17:22:35 +00:00
Davanum Srinivas e6ac37c004 switch to latest sigs.k8s.io/yaml v1.5.0 (run update-gofmt.sh as well)
Signed-off-by: Davanum Srinivas <davanum@gmail.com>

Kubernetes-commit: c5b4b133ce3252ee19b7167eb69a99d88fdefda8
2025-06-25 08:03:06 -04:00
Kubernetes Publisher c21f374a5e Merge pull request #132255 from likakuli/feat-nothingfastreturn
feat: optimize ListAll and ListAllByNamespace to return directly when nothing to select

Kubernetes-commit: c379296bd1b29ee8e890aff393959398ac905b2f
2025-06-24 17:22:42 +00:00
Kubernetes Publisher f545e563a3 Merge pull request #132376 from tico88612/cleanup/message-count-map
apimachinery/pkg/util/errors: deprecated MessageCountMap

Kubernetes-commit: 885db534cabb9c1975553f43e32c90f0be256a51
2025-06-24 17:22:41 +00:00
Joe Betz 5f2744fe1e Bump to latest kube-openapi
Kubernetes-commit: dc323756cea2d1ebe32d7acb5a14a1769c14486f
2025-06-24 09:24:27 -04:00
Kubernetes Publisher ae7698643b Merge pull request #132194 from alvaroaleman/local0dev
Add an interface that all apply configurations implement

Kubernetes-commit: 7b04d7faefcc28a90fb092e789abda467b086403
2025-06-18 15:36:50 -07:00
ChengHao Yang 36c0fecd85 apimachinery/pkg/util/errors: deprecated MessageCountMap
Signed-off-by: ChengHao Yang <17496418+tico88612@users.noreply.github.com>

Kubernetes-commit: df32f10e0695ea49bf4d99b4fdc4531c87192c73
2025-06-19 01:23:10 +08:00
Tim Hockin 7fecabd51e Don't panic in case of an unknown API error code
Kubernetes-commit: e68d601344965e67fc09da9d7e4979c4bf72c9c3
2025-06-14 18:12:36 -07:00
Tim Hockin 0084b28976 WIP: Fix tests
Notes:
* For types that define String() - should we prefer that or JSON?
* metav1.Time has a MarshalJSON() and inhereits a String() and they are
  different
* Since validation runs on internal types, we still get some GoNames
  instead of goNames.

Kubernetes-commit: 4ca91a03052ebf31d373a0de6e12891ae15966b9
2025-06-18 10:23:01 +09:00
Tim Hockin c58e197ee8 Nicer value rendering in API errors
Today, if the value passed is a struct, map, or list, we get Go's vative
rendering which is clunky.

This uses JSON (could be kyaml when that is ready) instead.

I hear it already: "But JSON is slow!".  I benchmarked it -- for an
simple int or string field, JSON is only a little slower (~20%) than a
type assertion, but it IS slower, so I left the type assertion in.
Remember that this is only called when an API error has occurred.

The type assertions do not handle typedefs-to{string, int64, etc} so
those will fall back on JSON.  Almost all of our errors go thru standard
functions which demand string or int64 anyway, so mostly pointless.

I also benchmarked using reflect to check `CanInt()` and that is almost
exactly as fast as type-switch but handles more cases, so we COULD
switch to that instead, if we wanted. I thought it wasn't worth the
complexity.

JSON is really there to handle composite types.

Kubernetes-commit: 2b2c9adef385c064c04bf456fa483dbad742a048
2025-06-14 17:53:02 -07:00
likakuli d3c271abfa feat: optimize ListAll and ListAllByNamespace to return directly when nothing to select
Signed-off-by: likakuli <1154584512@qq.com>

Kubernetes-commit: 5a7e04b6cc70bd7624536e09b4e977e8f25cb476
2025-06-12 17:33:32 +08:00
Alvaro Aleman 07890ccffb Add an interface that all applyconfigs implement
Kubernetes-commit: 3fe4ea550e8267d11ab2825be3770678bf868e37
2025-05-28 15:22:17 -04:00
Kubernetes Publisher 1025cde0fd Merge pull request #132349 from p0lyn0mial/upstream-rm-initial-events-list-blueprint
apimachinery/meta/types.go: remove InitialEventsListBlueprintAnnotationKey

Kubernetes-commit: 70fc3f2bba771248054ce97807e986e3fc5311c1
2025-06-17 13:30:59 -07:00
Lukasz Szaszkiewicz 47b0b24556 apimachinery/meta/types.go: remove InitialEventsListBlueprintAnnotationKey const
Kubernetes-commit: 15ca38b521d94abfac00fcf57031fcaa28b8d7df
2025-06-16 15:29:10 +02:00
Kubernetes Publisher d2d60b8eb2 Merge pull request #132221 from dims/new-cmp-diff-impl
New implementation for `Diff` (drop in replacement for `cmp.Diff`)

Kubernetes-commit: 3e39d1074fc717a883aaf57b966dd7a06dfca2ec
2025-06-17 03:24:54 +00:00
Davanum Srinivas 6c33c81b53 Add a replacement for cmp.Diff using json+go-difflib
Co-authored-by: Jordan Liggitt <jordan@liggitt.net>
Signed-off-by: Davanum Srinivas <davanum@gmail.com>

Kubernetes-commit: 03afe6471bdbf6462b7035fdaae5aa0dd9545396
2025-06-10 23:08:41 -04:00
Kubernetes Publisher e0270fe44c Merge pull request #132269 from dims/update-to-latest-github.com/modern-go/reflect2
Update to latest github.com/modern-go/reflect2

Kubernetes-commit: d55b119d34883bbad2a3436dcb6c62339d963031
2025-06-12 19:54:03 +00:00
Kubernetes Publisher 01cb050342 Merge pull request #132232 from likakuli/feat-optlabelselector
feat: optimize label selector match performance

Kubernetes-commit: e33933fa977d876df1093d6b893ec4c0b443f7e9
2025-06-12 19:54:02 +00:00
Davanum Srinivas 9dd84ae6f3 Update to latest github.com/modern-go/reflect2
Signed-off-by: Davanum Srinivas <davanum@gmail.com>

Kubernetes-commit: 3908550c0dc189cfa9de38a84bee508fa0659463
2025-06-12 11:20:39 -04:00
Kubernetes Publisher 9ad036216e Merge pull request #132135 from jpbetz/options-to-slice-master
Simplify options in declarative validation

Kubernetes-commit: c06a41b31667e83cbdd19734d5716775f9c3937e
2025-06-12 07:54:05 +00:00
likakuli e172d59e75 feat: optimize label selector match performance
Signed-off-by: likakuli <1154584512@qq.com>

Kubernetes-commit: e75ccce83fcb4323f480581644a5933f3d9cf2f6
2025-06-11 21:20:32 +08:00
Kubernetes Publisher a1eb4c90eb Merge pull request #132217 from yongruilin/master_vg_refactor-test
feat(validation-gen): Improve validation test helpers for validation-gen

Kubernetes-commit: 8052df02af256b79f31c65ee0430a7373a6f38b4
2025-06-10 23:36:55 -07:00
Tim Hockin e22a1dfe78 Better formatting of matcher errors
Kubernetes-commit: 28e973c04472bc31097d1483db97e3344e9cc764
2025-04-14 09:51:47 -07:00
Kubernetes Publisher d5745c2f38 Merge pull request #132103 from nojnhuh/typed-ring-buffer
Replace queue.FIFOs with k8s.io/utils/buffer.Ring

Kubernetes-commit: 5090812df4fb6cf09a9181635d90c2e154eab8cc
2025-06-06 19:54:23 +00:00
Kubernetes Publisher 1ea980407b Merge pull request #132034 from ChosenFoam/dev-fix
Fix the IsDNS1123SubdomainWithUnderscore function to return the correct error message

Kubernetes-commit: 2d3ca18f645edd9065c32f07f02de2ba6273ab1d
2025-06-06 03:54:17 +00:00
Kubernetes Publisher 90f1d78adf Merge pull request #132110 from jpbetz/gengo-bump
Bump gengo/v2 to latest, pick up related validation-gen fixes

Kubernetes-commit: 4fff091ce7c8b22e6a511231e400adb865a7b300
2025-06-05 11:58:45 -07:00
Jon Huhn e15237071b Update k8s.io/utils for new generic ring buffer
Kubernetes-commit: 8cdbbf5cdaef7e37cfd432e9044aa52f4d42adcd
2025-06-04 12:09:53 -05:00
Tim Hockin 4af193d7f5 Fix field path for embedded fields in root types
Given the following:

```
type Struct type {
    TypeMeta int

    // +k8s:validateFalse="embedded"
    OtherStruct
}
```

What should the field-path be for the error?

or:

```
type Struct type {
    TypeMeta int

    OtherStruct // embedded
}

// +k8s:validateFalse="otherstruct"
type OtherStruct {
  I int
}
```

Conclusion: If we find an embedded field, we look for a nil fldPath, and
if so we set it to the type name.  Embedded values in root types (e.g.
above `spec`) should not be a things we actually do.

Implementation: Add `safe.Value(*T, func() *T)`

Kubernetes-commit: 2c4c3037b6a5dbcd9dcc739607ac8580571d3d09
2025-06-03 17:39:05 -07:00
Kubernetes Publisher a925cd7fb3 Merge pull request #132039 from alvaroaleman/fix-w-unstructured
FieldManagedObjectTracker: Fix to work with unstructured

Kubernetes-commit: fd53f7292c7d5899135fddd928c0dc3844126820
2025-06-02 15:04:42 -07:00
huyurui17 855a9556cb add unit test for IsDNS1123SubdomainWithUnderscore function
Kubernetes-commit: 899f76159f130dba4fdf93d374884d8b8c704487
2025-06-01 00:48:16 +08:00
Alvaro Aleman 251918081b FieldManagedObjectTracker: Fix to work with unstructured
Prior to this patch, this fails because the skipnonappliedfieldmanager
uses the `objectcreater` (aka `*runtime.Scheme`) to create a new object
for which it never sets the GVK. In the case of
`*unstructured.Unstructured`, the GVK can not be derived from the object
itself so the operation would subsequently fail [here][0] with an
```
Object 'Kind' is missing in 'unstructured object has no kind'
```

error. Fix this by explicitly setting the GVK in case of unstructured.

[0]: 02eb7d424a/staging/src/k8s.io/apimachinery/pkg/util/managedfields/internal/structuredmerge.go (L98)

Kubernetes-commit: dbdd6a3b4358d91a064de9c0f01d3050e606d553
2025-05-30 15:30:33 -04:00
huyurui17 add5cf846c Fix IsDNS1123SubdomainWithUnderscore to return correct error message
Kubernetes-commit: be049397dfd0752f90ec85eef938ab54d04d005c
2025-05-31 00:14:07 +08:00
Kubernetes Publisher 09ff13941c Merge pull request #131664 from jpbetz/subresources-enable-replicas
Migrate to declarative validation: ReplicationController/scale spec.replicas field

Kubernetes-commit: 144926558984ae41a7328d53bd9fc8602328f10e
2025-05-27 09:14:16 -07:00
Joe Betz cb4ef17d4e Simplify subresource matching
Kubernetes-commit: 8ae7171041bb1a24d9436e3f576fced4aa604b79
2025-05-23 19:30:09 -04:00
Joe Betz dc0ccb55f8 Clarify errors and improve tests
Kubernetes-commit: 9715c90b31ccc121ba910044c03ecb9936b1f858
2025-05-19 14:12:09 -04:00
Joe Betz 52e0cea356 Update testing to fully track subresources
Kubernetes-commit: 7dc8660d03635a3cd61ae6db8c586acc90ddba97
2025-05-19 11:20:46 -04:00
Joe Betz 460ba78fbe Add +k8s:isSubresource and +k8s:supportsSubresource tags
Kubernetes-commit: 6ca6b7bb6ab7865ace03601c5d34571196126c8b
2025-05-19 11:18:06 -04:00
Kubernetes Publisher da3bba9054 Merge pull request #128419 from liggitt/etcd-3.6
etcd 3.6 client update

Kubernetes-commit: 09ca440a450e9103a8f835f598c09237dba6ecbb
2025-05-16 03:29:56 +00:00
Jordan Liggitt 0d18dbd72a bump etcd client to 3.6
hack/pin-dependency.sh go.etcd.io/etcd/api/v3 v3.6.0
hack/pin-dependency.sh go.etcd.io/etcd/client/pkg/v3 v3.6.0
hack/pin-dependency.sh go.etcd.io/etcd/client/v3 v3.6.0
hack/pin-dependency.sh go.etcd.io/etcd/pkg/v3 v3.6.0
hack/pin-dependency.sh go.etcd.io/etcd/server/v3 v3.6.0

hack/pin-dependency.sh github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0

hack/update-vendor.sh

Kubernetes-commit: cf0bbf1171e918d5d7ba1d3c83b5f347fc8333b0
2025-05-15 21:19:11 -04:00
Kubernetes Publisher b482a2c0d9 Merge pull request #131706 from karlkfi/karl-close-watch-tests
test: Close response body in watch tests

Kubernetes-commit: 5f1a5174c508f6fad9d6c06dc8b5675ef04b9a03
2025-05-15 19:12:48 +00:00
Kubernetes Publisher 1bb0bf9dfa Merge pull request #131715 from thockin/kk_yaml_decoder_nits
Account consumed newlines properly in YAML decoder

Kubernetes-commit: 1a0eeb90e46a97ae296282b907c3362380aedb13
2025-05-12 16:01:15 -07:00
Tim Hockin f4fdaa0c9a Account consumed newlines properly in YAML decoder
This isn't a significant functional bug in that the StreamDecoder is
still offset to account for it (which is why we never saw it before).
But it's a trap for future use-cases.

Also move one LOC to match a parallel-shaped block above.

Kubernetes-commit: cfe7d0424356f2e75660525d100617eae45f09a8
2025-05-11 16:16:46 -07:00
Kubernetes Publisher 202cba0f14 Merge pull request #131702 from tigrato/master
Panic in `NewYAMLToJSONDecoder`

Kubernetes-commit: 2fed387a900bf8004fde4e577cd8b77c93aeca1f
2025-05-09 15:41:18 -07:00
Tiago Silva a49178112c fix: fixes a possible panic in `NewYAMLToJSONDecoder`
This PR fixes a possible panic caused by decoding a JSON document
followed by a YAML document that is shorter than the first json
document.

This can cause a panic because the stream already consumed the JSON
data. When we fallback to YAML reader, the YAML starts with a zero
offset while the stream consumed data is non-zero. This could lead into
consuming negative bytes because `d.yaml.InputOffset() -
d.stream.Consumed()` is negative which will cause a panic.

Signed-off-by: Tiago Silva <tiago.silva@goteleport.com>

Kubernetes-commit: d1fb42a4000333ced2b2b26ac65fe64958a4259e
2025-05-09 18:27:20 +01:00
Kubernetes Publisher f7c4380031 Merge pull request #131678 from karlkfi/karl-api-methods
refactor: Use http method constants in apimachinery

Kubernetes-commit: ebb13a2ad7a7a9a8c62b7e56bbb88f85c208cea6
2025-05-09 00:31:28 -07:00
Karl Isenberg c4b7aee3fc refactor: Use http method constants in apimachinery
- Linter catches this sometimes, but this change catches a few more.
- Avoids using string literals when standard constants are available.

Kubernetes-commit: 3f188e5d86b35993c79d23dbcdfb264af0141241
2025-05-08 12:54:06 -07:00
Kubernetes Publisher d56afd172a Merge pull request #131616 from jpbetz/typeconverter-cleanup
Reorganize scheme type converter into apimachinery utils

Kubernetes-commit: 7cb2bd78b22c4ac8d9a401920fbcf7e2b240522d
2025-05-08 03:11:40 +00:00
Kubernetes Publisher 37c3ecc10a Merge pull request #131635 from thockin/kk_cleanup_codegen
Accumulated cleanups of validation-gen

Kubernetes-commit: 51fff4f8bdb7c39988dd1661bc6252fbd671f0eb
2025-05-07 19:11:35 +00:00
Joe Betz 0a090da87e Reorganize scheme type converter into apimachinery utils
This removes a dependency from generated applyconfigurations to a testing
package. To do this, the type converter in the testing package has been
moved out to the apimachinery package and the utilities the converter
depend on have been reorganized.

Kubernetes-commit: 4821604f83a6f4764497879b666087ba7cb05060
2025-05-07 10:07:55 -04:00
Kubernetes Publisher 2b45e0daa3 Merge pull request #131443 from jpbetz/lockfree-error-backoff
Avoid lock contention for error backoff

Kubernetes-commit: 1cc003ae4880c040e3ec47f5afcb0bf013d6a0e3
2025-05-06 23:12:03 +00:00
Joe Betz e303f8a95d Fix rudimentaryErrorBackoff to only be created once
Kubernetes-commit: c5efc843daca2997104e617a3dda204dddef0f2c
2025-05-06 15:56:29 -04:00
Kubernetes Publisher e07849993d Merge pull request #131560 from jpbetz/validation-gen-subresource-simplification
Declarative validation: Simplify handling of subresources

Kubernetes-commit: c6739dd54d56549a8460737f66b1c6aa0fa697bf
2025-05-06 19:11:57 +00:00
Kubernetes Publisher 863c50fec7 Merge pull request #131399 from thockin/kk_realType
Fixes for bad handling of pointers and aliases in validation

Kubernetes-commit: 07704ee9b1ca4c3b4048e99f171c3736bf57584a
2025-05-06 03:12:21 +00:00
Kubernetes Publisher 512f488de3 Merge pull request #131595 from aojea/utils_fake_clock
update k8s.io/utils to bring fakeClock.Waiters()

Kubernetes-commit: e3e1f80c0110c847acf4381b1790c1c667395010
2025-05-03 03:11:11 +00:00
Tim Hockin 126acd266c Fix typo in comment
Kubernetes-commit: 97e64e80c3e5ac98c6d123401f4d9ab1393c89aa
2025-05-02 10:48:33 -07:00
Antonio Ojea 5b059a2317 update k8s.io/utils to bring fakeClock.Waiters()
Change-Id: I7e25338df225c2c27457403fbc2f158d08638f87

Kubernetes-commit: c2c003a71fc52fa79c2fff0109afad58573d0216
2025-05-02 11:21:11 +00:00
Kubernetes Publisher 7b819a3b82 Merge pull request #130989 from liggitt/creationTimestamp-omitzero
Omit null creationTimestamp

Kubernetes-commit: 01899a7c86337b05a16a4155c9351cf947beaee9
2025-05-02 23:11:39 +00:00
Karl Isenberg b9dc8ce33c test: Close response body in watch tests
- Close response body after http calls in the watch tests
- Add apitesting utility methods for closing and testing errors
  from response body and websocket.
- Validate read after close errors.

Kubernetes-commit: 9d963298a3b7b828f01a9b02af57863a7480eb0b
2025-04-29 17:32:48 -07:00
Tim Hockin 1feb0afd7e Fix immutable validation for structs with pointers
Comparing pointers is not the same as deep-equal.  This was an
embarassing oversight.

Kubernetes-commit: 9d519c7c46e5e064daa8a4098d238775802746c3
2025-04-23 19:27:12 -07:00
Joe Betz f639733311 Rewrite Subresources godoc.
Kubernetes-commit: 2e8b409a5ffa3791bafaceafef9570606f137825
2025-04-14 14:42:34 -04:00
Joe Betz 4390983008 Change option to a slice
Kubernetes-commit: 501393810064f96808eba093ae5c00780020c667
2025-03-29 08:59:30 -04:00
Joe Betz e514dfafab Use slice.Contains()
Kubernetes-commit: 990cb7547ca3235d245d31b1a1e4a8db7abeee84
2025-04-14 14:17:15 -04:00
Joe Betz 43d4698fb9 Add subresource to operation, do not special case subresources in validation-gen
Kubernetes-commit: 2119555e02b357db58e460cd8f38bf187b5f837b
2025-03-26 21:26:14 -04:00
Jordan Liggitt 9386fa7b71 Drop null creationTimestamp from test fixtures
Kubernetes-commit: 6bb6c9934294d8265197c9dfc4c9dd3adaca147a
2025-03-24 09:37:26 -04:00
Jordan Liggitt 4a2766af85 Update runtime convertor to honor IsZero()
Kubernetes-commit: 41805aff9158b976f32611d36812215257c35707
2025-03-24 09:37:00 -04:00
Jordan Liggitt b5d3fcfd1f bump cbor to add omitzero support
Kubernetes-commit: bc6051717137cef288b82305588e675de4a32c0d
2025-03-25 12:27:43 -04:00
Jordan Liggitt 0aaa0d40ad bump structured-merge-diff to add omitzero support
Kubernetes-commit: 06b0784062f68566daa8eed83c475b738dcf620c
2025-03-24 16:34:01 -04:00
Jordan Liggitt 503b989732 Omit null metadata.creationTimestamp
Kubernetes-commit: fdf0bb41a4488057b6f44dc6aa456f0e977ae2be
2025-03-21 14:57:11 -04:00
Kubernetes Publisher 70566753bf Merge pull request #131029 from liggitt/to-unstructured-stdlib
Make ToUnstructured match stdlib omitempty and anonymous behavior

Kubernetes-commit: ef239ae62b86ee03b8b5e7a01d7c4cf4dcdd4a92
2025-05-02 08:07:55 -07:00
Jordan Liggitt a0fdacfb32 Make ToUnstructured match stdlib omitempty and anonymous behavior
Kubernetes-commit: 09912f3521932db99fe19f993db240fd43f3240e
2025-03-24 11:52:44 -04:00
Kubernetes Publisher 9549609199 Merge pull request #130995 from xigang/utils
bump k8s.io/utils for improvements

Kubernetes-commit: 43a7d3be12425cc80ca6ad3599809a19728c5566
2025-04-23 23:15:24 +00:00
Kubernetes Publisher 27a82ebc8b Merge pull request #130994 from BenTheElder/host-network-no-port
Remove inaccurate doc comment from podspec hostNetwork field

Kubernetes-commit: b775f9b92f98f7b3acbb3864ed53c2f5b835e917
2025-04-23 23:15:22 +00:00
Kubernetes Publisher 7b4292bd2e Merge pull request #131103 from ahrtr/etcd_sdk_20250328
Bump etcd 3.5.21 sdk

Kubernetes-commit: f4d1686120d2367dd4c00df53e93dad51c414435
2025-04-01 10:18:05 +00:00
Benjamin Wang 90c4280d8b bump etcd 3.5.21 sdk
Signed-off-by: Benjamin Wang <benjamin.ahrtr@gmail.com>

Kubernetes-commit: f3b80a858225178e3f7a3ae07bd1b9894e7b3456
2025-03-28 14:30:47 +00:00
xigang da1e88666e bump k8s.io/utils
Kubernetes-commit: fe14689f221a968806b771b226581efb834654cd
2025-03-22 10:14:01 +08:00
Benjamin Elder 89b3b23b4b make update
Kubernetes-commit: 1c3dc397ae137cc8b1d2095ea33217a239b81b55
2025-03-21 18:19:43 -07:00
Benjamin Elder 688a8fd9d9 remove inaccurate hostNetwork doc comment
also remove from copies in example / test APIs

Kubernetes-commit: 8af1629f7aeeabeaec21f3fbcee5bc60d9ad2015
2025-03-21 18:19:29 -07:00
Kubernetes Publisher e8a77bd768 Merge pull request #130910 from googs1025/fix/datarace
flake: fix data race for func TestBackoff_Step

Kubernetes-commit: 3a14b619d5862b058f941e07f381e8965c5c7702
2025-03-19 09:28:00 +00:00
Kubernetes Publisher 7e8c77e774 Merge pull request #130906 from serathius/streaming-validation
Update kube-openapi and integrate streaming tags validation

Kubernetes-commit: 32b1819423de505da855cf7544e871a04e63d6ed
2025-03-19 05:27:58 +00:00
googs1025 27fd3963bb flake: fix data race for func TestBackoff_Step
Kubernetes-commit: 2f1f19a992fcea1aa9f81731296335d1bd4dc533
2025-03-19 09:23:21 +08:00
Marek Siarkowicz 8bcc6f18d0 Update kube-openapi and integrate streaming tags validation
Kubernetes-commit: 75a4d136abac241f728407515e3d0d8305594675
2025-03-18 21:26:22 +01:00
Kubernetes Publisher 6ce776c88d Merge pull request #130857 from thockin/kk_small_vg_diffs
Port small deltas from validation-gen dev branch to master

Kubernetes-commit: abad982cf82f83368297d20faf3f2ecedc2295bf
2025-03-16 15:49:47 -07:00
Tim Hockin f2c94d6c4b Comment on origin and JSON schema
Kubernetes-commit: b47e839e4ecc77e2caad7855c9869517559dfbaf
2025-03-16 14:32:49 -07:00
Tim Hockin b63ba07f71 Use origin in validateFalse's own test
Kubernetes-commit: 4c0c2d21ea6fb2baf18619ac51e50f26e16d4545
2025-03-16 14:27:31 -07:00
Tim Hockin beddba44f4 Use test.Helper in helper funcs
Kubernetes-commit: d1d77cd553c7d43b14a4da453c69606c5413c530
2025-03-16 14:26:41 -07:00
Kubernetes Publisher eaf4038701 Merge pull request #130354 from siyuanfoundation/forward-api
KEP-4330: add forward compatibility for compatibility mode

Kubernetes-commit: 8b08487283d563efa0bc849ac3a3701463bc49bd
2025-03-14 05:27:48 +00:00
Kubernetes Publisher c8bf40462f Merge pull request #130019 from yongruilin/version-intro
KEP-4330: extend version information with more detailed version fields

Kubernetes-commit: 23d63770284cb30a2eb90b79ace0f1c7e32fb16f
2025-03-14 01:27:50 +00:00
Kubernetes Publisher a04ff375ce Merge pull request #122550 from danwinship/tighten-ip-validation
Tighten IP/CIDR validation

Kubernetes-commit: 2261137135631d24248e94dff3bc6375ad9308b2
2025-03-13 01:27:45 +00:00
Kubernetes Publisher 2eee037957 Merge pull request #130705 from aaron-prindle/validation-gen-add-metric-and-runtime-verification-upstream
[Declarative Validation] feat: add declarative validation metrics and associated runtime verification tests

Kubernetes-commit: 21f7eaa8e2b9c1a70b607cc42d0f038a9efc1906
2025-03-12 21:27:45 +00:00
Kubernetes Publisher 2687636770 Merge pull request #130739 from jpbetz/declarative-validation-test-infra
Introduce versioned validation test utilitizes and add fuzz tester

Kubernetes-commit: 7d6700a532d4e51b29d5df2827a17ffcc2f0f9af
2025-03-11 21:37:53 -07:00
Aaron Prindle f33bb5d4eb chore: change error_matcher.go to use test interface instead of importing testing pkg
Kubernetes-commit: cd9df2f115a95835e07cddf740861dbd8f6f3988
2025-03-11 05:24:07 +00:00
yongruilin 18f464253a refactor: detach Info from apimachinery util version
- Remove `info` field from `Version` struct
- Modify `WithInfo` and `Info` methods to be deprecated
- Update version information retrieval to use base version info
- Simplify version information generation in compatibility tests
- Remove unnecessary version info passing in build and test scenarios

Kubernetes-commit: 14934b481ef6522d6c1003ded19002ea45abe5d1
2025-03-05 23:55:08 +00:00
yongruilin a78ae8bc64 feat: extend version information with more detailed version fields
- Add new version fields to version.Info struct:
  * EmulationMajor and EmulationMinor to track emulated version
  * MinCompatibilityMajor and MinCompatibilityMinor for compatibility tracking
- Update related code to populate and use these new fields
- Improve version information documentation and OpenAPI generation
- Modify version routes and documentation to reflect new version information structure

Kubernetes-commit: a3094ccbe6f9f134da29aedf4d6d87a9a97bf463
2025-02-06 16:11:12 -08:00
Siyuan Zhang 39750cdbae Add emulation forward compatibility into api enablement and RemoveDeletedKinds.
Signed-off-by: Siyuan Zhang <sizhang@google.com>

Kubernetes-commit: 819cb8fe22fe37bf691f460bc32d0f03f53cce09
2025-02-04 10:11:56 -08:00
Tim Hockin d9e6c50b14 Introduce versioned validation test utilitizes and add fuzz tester
This makes a bold assumption: that the errors (count and basic content)
will be the same across versions.  If this turns out to be untrue, this
may need to get more sophisticated.  It should fail obviously when we
hit that edge.

Kubernetes-commit: 1d365762a5358e346b3133b369eadd9be6d4c85b
2024-12-11 14:28:15 -08:00
Kubernetes Publisher e79d000292 Merge pull request #129407 from serathius/streaming-proto-list-encoder
Implement streaming proto list encoder

Kubernetes-commit: 1b6e321e2311757a521615917f99dbe8e58f623c
2025-03-12 01:27:45 +00:00
Kubernetes Publisher 87bb4f9a9b Merge pull request #130730 from jpbetz/minimum-tag
Add +k8s:minimum validation tag

Kubernetes-commit: f3a23cfe90b060c3be26f095c4417bc57280c80e
2025-03-11 15:59:46 -07:00
Marek Siarkowicz e0ec8168b4 Implement streaming proto encoding
Kubernetes-commit: f5dd7107f7144c4f76ca6159c1eeddb48a12feaa
2024-12-19 12:30:39 +01:00
Tim Hockin a18d60b0df Add +k8s:minimum validation tag
Kubernetes-commit: d6ef05b9a826ab0cb662efedae095e6b7ae7adff
2024-11-30 14:31:42 -08:00
Kubernetes Publisher 6e3d6ca424 Merge pull request #128786 from danwinship/bad-ip-warnings
warn on bad IPs in objects

Kubernetes-commit: 3782b558a28fc5cc7d3f451fd03cbbb802d12a62
2025-03-11 09:27:30 +00:00
Kubernetes Publisher 56015c7e0a Merge pull request #130699 from thockin/master_validation-gen_odd_cases
Prevent validation-gen usage patterns we don't want to support

Kubernetes-commit: b90ff89ed62b82f21b1253a543e6603aafb1cadd
2025-03-11 01:27:23 +00:00
Kubernetes Publisher 40f26b37da Merge pull request #130695 from yongruilin/validation-gen_coveredbydeclarative
[Declarative Validation] Add CoveredByDeclarative to field error struct

Kubernetes-commit: f5f9484286cbe651f78b5f59f227f5dbd3b2e644
2025-03-10 17:15:46 -07:00
Tim Hockin af97bd6ab1 Prevent usage patterns we don't want to support
* typedefs to pointers
* pointers to pointers
* pointers to lists
* pointers to maps
* fixed-size arrays
* lists of pointers
* lists of lists
* lists of maps
* maps with non-string keys
* maps of pointers
* maps of lists
* maps of maps

Kubernetes-commit: dcbfe67b1ca9f5a9b5ebbb8ed62e09e14a1c65d2
2025-03-05 19:42:20 -08:00
yongruilin 06dde8a0df Add CoveredByDeclarative to field error struct
- Introduce CoveredByDeclarative field to Error struct
- Add MarkCoveredByDeclarative method for Error and ErrorList
- Implement ExtractDeclarative method to filter out declaratively covered errors
- Update error constructors to include the new field
- Add corresponding test cases for new declarative validation functionality

Kubernetes-commit: 8eb90fe136d74dc0a5c891cf1cdc428acdd06dec
2025-03-05 22:09:24 +00:00
Kubernetes Publisher ee322b25b6 Merge pull request #130666 from thockin/yaml_json_ambiguous_decode
Better handling of YAML that tastes like JSON

Kubernetes-commit: 0791d6ef704f2bffb9160b833fdf52bae20df036
2025-03-10 21:27:30 +00:00
Kubernetes Publisher e25aab096b Merge pull request #130555 from thockin/k_k_randfill
Use randfill in k/k

Kubernetes-commit: 0f2bde7745f3b4eadcf317bc5056dfeb96859bd3
2025-03-09 13:28:01 +00:00
Tim Hockin 9b3d085fa5 Better handling of YAML that tastes like JSON
For the most part, JSON is a subset of YAML.  This might lead one to
think that we should ALWAYS use YAML processing.  Unfortunately a JSON
"stream" (as defined by Go's encoding/json and many other places, though
not the JSON spec) is a series of JSON objects.  E.g. This:

```
{}{}{}
```

...is a valid JSON stream.

YAML does NOT accept that, insisting on `---` on a new line between YAML
documents.

Before this commit, YAMLOrJSONDecoder tries to detect if the input is
JSON by looking at the first few characters for "{".  Unfortunately,
some perfectly valid YAML also tastes like that.

After this commit, YAMLOrJSONDecoder will detect a failure to parse as
JSON and instead flip to YAML parsing.  This should handle the ambiguous
YAML.

Once we flip to YAML we never flip back, and once we detect a JSON
stream (as defined above) we lose the ability to flip to YAML.  A
multi-document is either all JSON or all YAML, even if we use the JSON
parser to decode the first object (because JSON is YAML for a single
object).

Kubernetes-commit: 770ce2d874bcb053af5c76ee07b3caeafb212bd2
2025-03-08 15:38:10 -08:00
Tim Hockin a18d7f2679 Vendor randfill
Kubernetes-commit: 0ce4268b1fe4f78d77249e329b0349b9d2dd2c65
2025-03-03 23:46:48 -08:00
Tim Hockin 78ddbb827e Use randfill, do API renames
Kubernetes-commit: e54719bb6674fac228671e0786d19c2cf27b08a3
2025-02-20 09:45:22 -08:00
Kubernetes Publisher ac04c7e419 Merge pull request #130569 from dims/update-to-latest-cadvisor-v0.52.0
Update to latest cadvisor @ v0.52.1 and new opencontainer/cgroups and drops opencontainers/runc

Kubernetes-commit: 0eaee48ecb8669dc65bfdf9a3583326ab88fc39d
2025-03-08 01:27:22 +00:00
Davanum Srinivas 7802db1890 update to v1.22.0-rc.0
Signed-off-by: Davanum Srinivas <davanum@gmail.com>

Kubernetes-commit: 97a54dc4b04b7d2938d11c5ae9a6233348e854ef
2025-03-07 13:45:34 -05:00
Kubernetes Publisher a3f7d4eded Merge pull request #130543 from thockin/error_matcher_and_origin
Fix up ErrorMatcher from feedback

Kubernetes-commit: 4696667025efa26a7b192b1cb5cf79cec276f2b4
2025-03-06 00:57:52 -08:00
Davanum Srinivas 4dfd1a614e update to latest cadvisor @ v0.52.0
Signed-off-by: Davanum Srinivas <davanum@gmail.com>

Kubernetes-commit: 5ecddb65715af7e2afc4f3cbb1abe393bfb4346a
2025-03-04 14:29:08 -05:00
Tim Hockin a70cc77ccb Fix up ErrorMatcher from feedback
a) Rename the type and drop the constructor
b) Make MatchErrors() into a Test() method

For followup:

c) Consider making ByType() assumed
d) Consider making ByField() assumed and handle nil as "don't care"
e) Consider making ByValue() assumed and handle nil as "don't care"

Kubernetes-commit: 0a9f492eedf6dd68fee12e4606d3fef4d608d88f
2025-03-03 10:23:18 -08:00
Kubernetes Publisher b5eba295a2 Merge pull request #130511 from z1cheng/issue_130395
Implement tests for encoding collections in Proto

Kubernetes-commit: c496aef96d8decb22aee5490fb752c029b98a856
2025-03-05 17:28:05 +00:00
Kubernetes Publisher e93b7f2f82 Merge pull request #130549 from jpbetz/validation-gen-pr2
KEP-5073: Add declarative validation to scheme

Kubernetes-commit: 89d0b7022a81dd8b54efcc203b0cfa4502171cae
2025-03-04 14:50:02 -08:00
Joe Betz 39f6713fb4 Add declarative validation to scheme
Kubernetes-commit: 5ff334a1589611c139a03238803ba5abf090eebd
2025-03-03 19:36:50 -05:00
Kubernetes Publisher 9dca0b5e16 Merge pull request #130349 from jpbetz/validation-gen-pr1
KEP-5073: Declarative Validation: Add validation generator

Kubernetes-commit: a5dda5d879cdae6562134ca7881ddf7f672f595d
2025-03-03 11:25:42 -08:00
Joe Betz 14ab9701c6 Add validators: immutable
Co-authored-by: Tim Hockin <thockin@google.com>
Co-authored-by: Aaron Prindle <aprindle@google.com>
Co-authored-by: Yongrui Lin <yongrlin@google.com>

Kubernetes-commit: a2f47e6586f3e7362d7354c35df8e1cfbb86af8f
2025-03-03 09:49:51 -05:00
Joe Betz 93247ca898 Add validators: optional/required/forbidden
Co-authored-by: Tim Hockin <thockin@google.com>
Co-authored-by: Aaron Prindle <aprindle@google.com>
Co-authored-by: Yongrui Lin <yongrlin@google.com>

Kubernetes-commit: 63050550c35b7e2fd2754d7d2b449873feefc203
2025-03-03 09:49:51 -05:00
Joe Betz f6058d5a5b Add validators: eachkey, eachval, subfield
Introduce a composable set of tags for validating child data.
This allows for point-of-use validation of shared types.

Co-authored-by: Tim Hockin <thockin@google.com>
Co-authored-by: Aaron Prindle <aprindle@google.com>
Co-authored-by: Yongrui Lin <yongrlin@google.com>

Kubernetes-commit: 31f463721701a35c1061935d554a81141b0b68bb
2025-03-03 09:49:51 -05:00
Joe Betz 77caaf986c Add validation-gen test infrastructure
Introduces the infrastructure for testing validation-gen tags.

Co-authored-by: Tim Hockin <thockin@google.com>
Co-authored-by: Aaron Prindle <aprindle@google.com>
Co-authored-by: Yongrui Lin <yongrlin@google.com>

Kubernetes-commit: 8c41bdf05b1539bb79eb7bb6f3c5323d526b97d3
2025-03-03 09:49:51 -05:00
Joe Betz 13b1842540 Introduce validation-gen
Adds code-generator/cmd/validation-gen. This provides the machinery
to discover `//+` tags in types.go files, register plugins to handle
the tags, and generate validation code.

Co-authored-by: Tim Hockin <thockin@google.com>
Co-authored-by: Aaron Prindle <aprindle@google.com>
Co-authored-by: Yongrui Lin <yongrlin@google.com>

Kubernetes-commit: c1f9e6b8eed30b6509a8a50f1ae9be1748f07f2e
2025-03-03 09:49:50 -05:00
Kubernetes Publisher 4e966741ac Merge pull request #128919 from dashpole/update_otel
Update go.opentelemetry.io dependencies to v1.33.0/v0.58.0

Kubernetes-commit: eea2f78e61fe91bb8fcd3c4a357ea3a10d1389db
2025-03-02 00:00:37 +00:00
David Ashpole ded50ecb1b update go.opentelemetry.io dependencies to v1.33.0/v0.58.0
Kubernetes-commit: 29c219dcebe30be99d6917623f8d8707a47194c1
2025-03-01 19:17:16 +00:00
z1cheng e8d821eb16 Implement tests for encoding collections in Proto
Signed-off-by: z1cheng <imchench@gmail.com>

Kubernetes-commit: b88f026053a84a135f4bca46ab569d7e3091c2b5
2025-03-01 13:42:32 +08:00
Kubernetes Publisher a3e3122ed9 Merge pull request #130388 from thockin/error_matcher_and_origin
Add an error matcher, convert 2 tests

Kubernetes-commit: aad87f2ee9761326850347f29c034c451117737d
2025-03-01 04:01:10 +00:00
Kubernetes Publisher 7d0dbe29a4 Merge pull request #129334 from serathius/streaming-json-list-encoder
Streaming json list encoder

Kubernetes-commit: 2fc329c857035676492aa6e6a995ef31448465f0
2025-03-01 00:01:16 +00:00
Kubernetes Publisher 609a76591d Merge pull request #130474 from dims/bump-x/crypto-and-x/oauth2
Bump x/oauth2 and x/crypto

Kubernetes-commit: 01ed8ed4ff0a0cbea99370c7a268019829d19e82
2025-02-28 20:01:12 +00:00
Kubernetes Publisher 6c5685cb7e Merge pull request #130355 from yongruilin/validation_origin
validation: Add Origin field to field.Error for more precise error tracking

Kubernetes-commit: 803e9d64952407981b3815b1d749cc96a39ba3c6
2025-02-28 12:01:10 +00:00
Kubernetes Publisher 758f86daa8 Merge pull request #129688 from cpanato/update-main-go124
[go] Bump images, dependencies and versions to go 1.24.0

Kubernetes-commit: b8c95e1954ef222988c0dfe5b45d5cc96c09bcb8
2025-02-27 20:01:16 +00:00
Davanum Srinivas f7c9d8b15c Bump x/oauth2 and x/crypto
Signed-off-by: Davanum Srinivas <davanum@gmail.com>

Kubernetes-commit: 0fede7b8a2fb4c7f120876c9ef1e826f8ef28da2
2025-02-27 10:59:37 -05:00
Jordan Liggitt 68b2a8167a Switch to private instances of rand for seeding for tests
Kubernetes-commit: 8090db5dcfdd2cae013c69c12ac49b2186d0e383
2025-02-25 09:56:21 -05:00
cpanato aac66c809b bump go.mod to set min go1.24
Signed-off-by: cpanato <ctadeu@gmail.com>

Kubernetes-commit: 88300c406b9199ed017e1bada29951fc18e66ae1
2025-02-25 13:21:52 +01:00
Kubernetes Publisher ee1e055b7c Merge pull request #130220 from serathius/streaming-json-tests
Add tests for encoding collections in JSON for KEP-5116

Kubernetes-commit: dc3021b156a6a5adc4b4898d366822529bcc74a4
2025-02-24 15:44:52 +00:00
Tim Hockin 46d8d84c42 Add an error matcher, convert 2 tests
I fixed up the TestValidateEndpointsCreate path to show the matcher
instead of manual origin checking.

I picked TestValidateTopologySpreadConstraints because it was the last
failing test on my screen when I changed on of the commonly hard-coded
error strings. I fixed exactly those validation errors that were needed
to make this test pass.  Some of the Origin values can be debated.

The `field/testing.Matcher` interface allows tests to configure the
criteria by which they want to match expected and actual errors.  The
hope is that everyone will use Origin for Invalid errors.

There's some collateral impact for tests which use exact-comparisons and
don't expect origins.  These are all candidates for using the matcher.

Kubernetes-commit: c8111709e564d171b23698b3cad0f86e9733ec43
2025-02-23 22:53:29 -08:00
Tim Hockin fa95ab3e7c Fix nits from PR 130355
Kubernetes-commit: 6b7e38f0180b26f875387a935ee814bfd426a70f
2025-02-27 22:43:41 -08:00
Kubernetes Publisher a783532045 Merge pull request #130187 from mansikulkarni96/129084
fix:  Sweep and fix stat, lstat, evalsymlink usage for go1.23 on Windows

Kubernetes-commit: ef54ac803b712137871c1a1f8d635d50e69ffa6c
2025-02-22 23:44:38 +00:00
yongruilin 0571dbf93e test: convert ValidateEndpointsCreate to use error Origin field in test
Update ValidateEndpointsCreate validation tests to use the new Origin field for more precise error comparisons. It leverage the Origin field instead of detailed error messages, improving test robustness and readability.

Co-authored-by: Tim Hockin <thockin@google.com>

Kubernetes-commit: 07477c656e7f3aa8c9d44e769bd2dfbdcc374b85
2025-02-21 22:34:00 +00:00
yongruilin da1b1a9e4c feat: Add Origin field to Error and related methods
This change introducing a new field in Error. It would be used in testing to compare the expected errors without matching the detail strings.

Co-authored-by: Tim Hockin <thockin@google.com>

Kubernetes-commit: 02f7dc55d162fcde7377b1cb0c5e2f9e8974548e
2025-02-21 20:20:47 +00:00
Marek Siarkowicz e38241d3e7 Add tests for encoding collections in JSON for KEP-5116
Used test cases from:
* Original PR https://github.com/kubernetes/kubernetes/pull/129334
* KEP https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/5116-streaming-response-encoding#unit-tests

For now testing current serializer implementation to show encoder
behavior and agree on set of tests. Having a separate PR should make review easier.
In separate PR will add the implementation for streaming that should
provide same response byte-to-byte.

Kubernetes-commit: 26fe6bc6e09d81cc6a6253393bc5c2c4bf35c077
2025-02-17 16:50:34 +01:00
Jordan Liggitt 0a4167067c Drop winsymlink go 1.23 workaround
(cherry picked from commit 3990b6324d0427eaf9ff970da2be02711567ef5f)

Kubernetes-commit: 1f642c79c3192994e76bbe8e7360fd661cd21ab4
2025-02-06 12:45:52 -05:00
Kubernetes Publisher 47e7fa9a40 Merge pull request #130151 from marosset/windows-unit-tests-externaljwt-plugin-fixes
fixing k8s.io/kubernetes/pkg/serviceaccount/externaljwt/plugin unit tests on Windows

Kubernetes-commit: 8dbc6739e0729e1a144a72d6280556ff7bfd3a9c
2025-02-14 13:44:20 -08:00
Mark Rossetti 11b535c1d8 fixing various unit tests on Windows that create abstract sockets
by now having them create file-based sockets on windows/darwin

Signed-off-by: Mark Rossetti <marosset@microsoft.com>

Kubernetes-commit: 5e6611af5594078a76694f0cf9fb7eef1299ff40
2025-02-13 13:09:19 -08:00
Kubernetes Publisher 46c230ea8d Merge pull request #130049 from aojea/avoid_ginkgo_dep
reduce dependencies in apimachinery net testing utils

Kubernetes-commit: 670b98bf9229bb1f193571949871f794f7b11ec3
2025-02-11 11:44:40 +00:00
Kubernetes Publisher a19f1f8137 Merge pull request #129792 from likakuli/fix-errshortbuffer
fix: Fix the issue of relist caused by client-side timeout

Kubernetes-commit: 15a186a888dc2e908681c876e321468b8d32a37b
2025-02-10 16:46:08 +00:00
Antonio Ojea d8c2c0ad39 reduce dependencies in apimachinery net testing utils
Consumers of the kubernetes golang API and clients must use
k8s.io/api,apimachinery,client-go. This is also require to download all
the necessary dependencies.

The apimachinery code contains a testing util for proxies that is used
in client-go and in the kubectl e2e. Since the tests on e2e require
ginkgo and we want to ensure this testing library is not used in
production, we cast the interface to match one of those libraries, but
the problem is that this forces consumers of apimachinery to also
download the ginkgo library.

Since NewHTTPProxyHandler receives a testing.TB interface, there is no
need to cast the interface, if someone wants to use it by implementing a
testing interface it is already aware of the risks.

Kubernetes-commit: af3b9e613d3b76b826369153760a069aabb4cf7f
2025-02-08 15:27:20 +00:00
Kubernetes Publisher 12352425f4 Merge pull request #129341 from pohly/log-client-go-watch
client-go watch: context support

Kubernetes-commit: a02fe24385a36f43e96844e19c438cc4097704ef
2025-02-07 12:46:01 +00:00
likakuli f7fd8b3a38 fix: Fix the issue of relist caused by client-side timeout
Signed-off-by: likakuli <1154584512@qq.com>

Kubernetes-commit: 38a21e06f5d72934a63b0c91a87bd75054c01172
2025-01-24 11:44:32 +08:00
Patrick Ohly 675c4f7a80 client-go + apimachinery watch: context support
The Lister and Watcher interfaces only supported methods without context, but
were typically implemented with client-go API calls which need a context. New
interfaces get added using the same approach as in
https://github.com/kubernetes/kubernetes/pull/129109.

Kubernetes-commit: 6688adae142e37114d9dfa8d94cd1d8a91fbcc13
2024-12-20 13:55:47 +01:00
Marek Siarkowicz 1e89b89bc5 Streaming JSON encoder for List
Kubernetes-commit: e7c743b2ebfaed1e3132027c0369ac25b14b6f47
2024-12-19 10:38:30 +01:00
Dan Winship fe80b851d8 Add utilvalidation.GetWarningsForIP and .GetWarningsForCIDR
(And port the existing Service warnings to use them.)

Kubernetes-commit: 610adebdb75082c8d643354ca96e66f3b2bc3773
2024-02-16 12:07:56 -05:00
Dan Winship 718670137b Fix IP/CIDR validation to allow updates to existing invalid objects
Ignore pre-existing bad IP/CIDR values in:
  - pod.spec.podIP(s)
  - pod.spec.hostIP(s)
  - service.spec.externalIPs
  - service.spec.clusterIP(s)
  - service.spec.loadBalancerSourceRanges (and corresponding annotation)
  - service.status.loadBalancer.ingress[].ip
  - endpoints.subsets
  - endpointslice.endpoints
  - networkpolicy.spec.{ingress[].from[],egress[].to[]}.ipBlock
  - ingress.status.loadBalancer.ingress[].ip

In the Endpoints and EndpointSlice case, if *any* endpoint IP is
changed, then the entire object must be valid; invalid IPs are only
allowed to remain in place for updates that don't change any IPs.
(e.g., changing the labels or annotations).

In most of the other cases, when the invalid IP is part of an array,
it can be moved around within the array without triggering
revalidation.

Kubernetes-commit: ad22c0d4954a760c0d0c1ffebb3de6c05c66ea4f
2023-12-28 11:30:45 -05:00
Dan Winship 72340d2cf7 Add legacy versions of IsValidIP/IsValidCIDR
Add validation.IsValidIPForLegacyField and
validation.IsValidCIDRForLegacyField, which validate "legacy" IP/CIDR
fields correctly. Use them for all such fields (indirectly, via a
wrapper in pkg/apis/core/validation that handles the
StrictIPCIDRValidation feature gate correctly).

Change IsValidIP and IsValidCIDR to require strict parsing and
canonical form, and update the IPAddr, ServiceCIDR, and
NetworkDeviceData validation to make use of them.

Kubernetes-commit: 692785d25b688f83db14c20f26d0b99e2e99adb8
2025-02-28 17:41:10 -05:00
Dan Winship 8aa42c4d2a Slightly improve EndpointSlice address validation
Because it used both IsValidIPv4Address and ValidateEndpointIP,
EndpointSlice validation produced duplicate error messages when given
an invalid IP. Fix this by calling IsValidIP first, and only doing the
other checks if that one fails.

Also, since no one else was using the IsValidIPv4Address and
IsValidIPv6Address methods anyway, just inline them into the
EndpointSlice validation, so we don't have to worry about "should they
do legacy or strict validation" later.

Kubernetes-commit: ba189de78ff13f6882e753138a5df07d6ab49433
2025-02-28 18:14:44 -05:00
Dan Winship 5b974f202d Add validation.IsValidInterfaceAddress
Split "ifaddr"-style ("192.168.1.5/24") validation out of IsValidCIDR.
Since there is currently only one field that uses this format, and it
already requires canonical form, IsValidInterfaceAddress requires
canonical form unconditionally.

Kubernetes-commit: fc4bb4fdb906d3120f048ae7ee422a130bf05e94
2025-02-28 16:16:51 -05:00
Dan Winship ba7db196ed Split out IP validation functions into their own file
(No code changes.)

Kubernetes-commit: 34717000dae84fb1c4df608370ad9bd572901f7b
2023-12-26 19:26:24 -05:00
179 changed files with 12215 additions and 1938 deletions

49
go.mod
View File

@ -2,36 +2,34 @@
module k8s.io/apimachinery
go 1.23.0
go 1.24.0
godebug default=go1.23
godebug winsymlink=0
godebug default=go1.24
require (
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/davecgh/go-spew v1.1.1
github.com/fxamacker/cbor/v2 v2.7.0
github.com/fxamacker/cbor/v2 v2.9.0
github.com/gogo/protobuf v1.3.2
github.com/google/gnostic-models v0.6.9
github.com/google/go-cmp v0.6.0
github.com/google/gofuzz v1.2.0
github.com/google/gnostic-models v0.7.0
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/moby/spdystream v0.5.0
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f
github.com/onsi/ginkgo/v2 v2.21.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.9.0
golang.org/x/net v0.33.0
golang.org/x/time v0.7.0
github.com/pmezard/go-difflib v1.0.0
github.com/spf13/pflag v1.0.6
github.com/stretchr/testify v1.10.0
golang.org/x/net v0.38.0
golang.org/x/time v0.9.0
gopkg.in/evanphx/json-patch.v4 v4.12.0
gopkg.in/inf.v0 v0.9.1
k8s.io/klog/v2 v2.130.1
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3
sigs.k8s.io/structured-merge-diff/v4 v4.4.2
sigs.k8s.io/yaml v1.4.0
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8
sigs.k8s.io/randfill v1.0.0
sigs.k8s.io/structured-merge-diff/v6 v6.3.0
sigs.k8s.io/yaml v1.6.0
)
require (
@ -39,20 +37,19 @@ require (
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/onsi/ginkgo/v2 v2.21.0 // indirect
github.com/onsi/gomega v1.35.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/tools v0.26.0 // indirect
google.golang.org/protobuf v1.35.1 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

76
go.sum
View File

@ -4,8 +4,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
@ -20,14 +20,11 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@ -52,8 +49,9 @@ github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVO
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
@ -64,10 +62,10 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@ -75,12 +73,16 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@ -90,22 +92,22 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@ -116,8 +118,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@ -130,13 +132,15 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg=
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7/go.mod h1:GewRfANuJ70iYzvn+i4lezLDAFzvjxZYK1gn1lWcfas=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

View File

@ -0,0 +1,54 @@
/*
Copyright 2025 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 apitesting
import (
"io"
"testing"
)
// Close and fail the test if it returns an error.
func Close(t TestingT, c io.Closer) {
t.Helper()
assertNoError(t, c.Close())
}
// CloseNoOp does nothing. Use as a replacement for Close when you
// need to disable a defer.
func CloseNoOp(TestingT, io.Closer) {}
// TestingT simulates assert.TestingT and assert.tHelper without adding
// testify as a non-test dependency.
type TestingT interface {
Errorf(format string, args ...interface{})
Helper()
}
// Ensure that testing T & B satisfy the TestingT interface
var _ TestingT = &testing.T{}
var _ TestingT = &testing.B{}
// assertNoError simulates assert.NoError without adding testify as a
// non-test dependency.
//
// In test files, use github.com/stretchr/testify/assert instead.
func assertNoError(t TestingT, err error) {
t.Helper()
if err != nil {
t.Errorf("Received unexpected error:\n%+v", err)
}
}

View File

@ -21,19 +21,19 @@ import (
"fmt"
"math/rand"
"github.com/google/gofuzz"
"sigs.k8s.io/randfill"
"k8s.io/apimachinery/pkg/runtime"
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
kjson "k8s.io/apimachinery/pkg/util/json"
)
// FuzzerFuncs returns a list of func(*SomeType, c fuzz.Continue) functions.
// FuzzerFuncs returns a list of func(*SomeType, c randfill.Continue) functions.
type FuzzerFuncs func(codecs runtimeserializer.CodecFactory) []interface{}
// FuzzerFor can randomly populate api objects that are destined for version.
func FuzzerFor(funcs FuzzerFuncs, src rand.Source, codecs runtimeserializer.CodecFactory) *fuzz.Fuzzer {
f := fuzz.New().NilChance(.5).NumElements(0, 1)
func FuzzerFor(funcs FuzzerFuncs, src rand.Source, codecs runtimeserializer.CodecFactory) *randfill.Filler {
f := randfill.New().NilChance(.5).NumElements(0, 1)
if src != nil {
f.RandSource(src)
}

View File

@ -20,6 +20,7 @@ import (
"bytes"
gojson "encoding/json"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
@ -28,7 +29,7 @@ import (
"strings"
"testing"
"github.com/google/go-cmp/cmp" //nolint:depguard
"github.com/google/go-cmp/cmp"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/runtime"
@ -115,7 +116,7 @@ func (c *CompatibilityTestOptions) Complete(t *testing.T) *CompatibilityTestOpti
c.TestDataDir = "testdata"
}
if c.TestDataDirCurrentVersion == "" {
c.TestDataDirCurrentVersion = filepath.Join(c.TestDataDir, "HEAD")
c.TestDataDirCurrentVersion = filepath.Join(c.TestDataDir, http.MethodHead)
}
if c.TestDataDirsPreviousVersions == nil {
dirs, err := filepath.Glob(filepath.Join(c.TestDataDir, "v*"))
@ -199,7 +200,7 @@ func (c *CompatibilityTestOptions) Run(t *testing.T) {
for _, gvk := range c.Kinds {
t.Run(makeName(gvk), func(t *testing.T) {
t.Run("HEAD", func(t *testing.T) {
t.Run(http.MethodHead, func(t *testing.T) {
c.runCurrentVersionTest(t, gvk, usedHEADFixtures)
})

View File

@ -24,9 +24,9 @@ import (
"strings"
"testing"
"github.com/google/go-cmp/cmp" //nolint:depguard
fuzz "github.com/google/gofuzz"
"github.com/google/go-cmp/cmp"
flag "github.com/spf13/pflag"
"sigs.k8s.io/randfill"
apitesting "k8s.io/apimachinery/pkg/api/apitesting"
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
@ -120,15 +120,15 @@ func GlobalNonRoundTrippableTypes() sets.String {
// RoundTripTypesWithoutProtobuf applies the round-trip test to all round-trippable Kinds
// in the scheme. It will skip all the GroupVersionKinds in the skip list.
func RoundTripTypesWithoutProtobuf(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
func RoundTripTypesWithoutProtobuf(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *randfill.Filler, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
roundTripTypes(t, scheme, codecFactory, fuzzer, nonRoundTrippableTypes, true)
}
func RoundTripTypes(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
func RoundTripTypes(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *randfill.Filler, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
roundTripTypes(t, scheme, codecFactory, fuzzer, nonRoundTrippableTypes, false)
}
func roundTripTypes(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool, skipProtobuf bool) {
func roundTripTypes(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *randfill.Filler, nonRoundTrippableTypes map[schema.GroupVersionKind]bool, skipProtobuf bool) {
for _, group := range groupsFromScheme(scheme) {
t.Logf("starting group %q", group)
internalVersion := schema.GroupVersion{Group: group, Version: runtime.APIVersionInternal}
@ -149,7 +149,7 @@ func roundTripTypes(t *testing.T, scheme *runtime.Scheme, codecFactory runtimese
// RoundTripExternalTypes applies the round-trip test to all external round-trippable Kinds
// in the scheme. It will skip all the GroupVersionKinds in the nonRoundTripExternalTypes list .
func RoundTripExternalTypes(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
func RoundTripExternalTypes(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *randfill.Filler, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
kinds := scheme.AllKnownTypes()
for gvk := range kinds {
if gvk.Version == runtime.APIVersionInternal || globalNonRoundTrippableTypes.Has(gvk.Kind) {
@ -163,7 +163,7 @@ func RoundTripExternalTypes(t *testing.T, scheme *runtime.Scheme, codecFactory r
// RoundTripExternalTypesWithoutProtobuf applies the round-trip test to all external round-trippable Kinds
// in the scheme. It will skip all the GroupVersionKinds in the nonRoundTripExternalTypes list.
func RoundTripExternalTypesWithoutProtobuf(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
func RoundTripExternalTypesWithoutProtobuf(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *randfill.Filler, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
kinds := scheme.AllKnownTypes()
for gvk := range kinds {
if gvk.Version == runtime.APIVersionInternal || globalNonRoundTrippableTypes.Has(gvk.Kind) {
@ -175,15 +175,15 @@ func RoundTripExternalTypesWithoutProtobuf(t *testing.T, scheme *runtime.Scheme,
}
}
func RoundTripSpecificKindWithoutProtobuf(t *testing.T, gvk schema.GroupVersionKind, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
func RoundTripSpecificKindWithoutProtobuf(t *testing.T, gvk schema.GroupVersionKind, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *randfill.Filler, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
roundTripSpecificKind(t, gvk, scheme, codecFactory, fuzzer, nonRoundTrippableTypes, true)
}
func RoundTripSpecificKind(t *testing.T, gvk schema.GroupVersionKind, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
func RoundTripSpecificKind(t *testing.T, gvk schema.GroupVersionKind, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *randfill.Filler, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
roundTripSpecificKind(t, gvk, scheme, codecFactory, fuzzer, nonRoundTrippableTypes, false)
}
func roundTripSpecificKind(t *testing.T, gvk schema.GroupVersionKind, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool, skipProtobuf bool) {
func roundTripSpecificKind(t *testing.T, gvk schema.GroupVersionKind, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *randfill.Filler, nonRoundTrippableTypes map[schema.GroupVersionKind]bool, skipProtobuf bool) {
if nonRoundTrippableTypes[gvk] {
t.Logf("skipping %v", gvk)
return
@ -204,8 +204,8 @@ func roundTripSpecificKind(t *testing.T, gvk schema.GroupVersionKind, scheme *ru
// fuzzInternalObject fuzzes an arbitrary runtime object using the appropriate
// fuzzer registered with the apitesting package.
func fuzzInternalObject(t *testing.T, fuzzer *fuzz.Fuzzer, object runtime.Object) runtime.Object {
fuzzer.Fuzz(object)
func fuzzInternalObject(t *testing.T, fuzzer *randfill.Filler, object runtime.Object) runtime.Object {
fuzzer.Fill(object)
j, err := apimeta.TypeAccessor(object)
if err != nil {
@ -225,7 +225,7 @@ func groupsFromScheme(scheme *runtime.Scheme) []string {
return ret.List()
}
func roundTripToAllExternalVersions(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, internalGVK schema.GroupVersionKind, nonRoundTrippableTypes map[schema.GroupVersionKind]bool, skipProtobuf bool) {
func roundTripToAllExternalVersions(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *randfill.Filler, internalGVK schema.GroupVersionKind, nonRoundTrippableTypes map[schema.GroupVersionKind]bool, skipProtobuf bool) {
object, err := scheme.New(internalGVK)
if err != nil {
t.Fatalf("Couldn't make a %v? %v", internalGVK, err)
@ -262,7 +262,7 @@ func roundTripToAllExternalVersions(t *testing.T, scheme *runtime.Scheme, codecF
}
}
func roundTripOfExternalType(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, externalGVK schema.GroupVersionKind, skipProtobuf bool) {
func roundTripOfExternalType(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *randfill.Filler, externalGVK schema.GroupVersionKind, skipProtobuf bool) {
object, err := scheme.New(externalGVK)
if err != nil {
t.Fatalf("Couldn't make a %v? %v", externalGVK, err)

View File

@ -37,7 +37,7 @@ import (
jsonserializer "k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/apimachinery/pkg/util/sets"
"github.com/google/go-cmp/cmp" //nolint:depguard
"github.com/google/go-cmp/cmp"
)
// RoundtripToUnstructured verifies the roundtrip faithfulness of all external types in a scheme
@ -92,13 +92,13 @@ func RoundtripToUnstructured(t *testing.T, scheme *runtime.Scheme, funcs fuzzer.
}
if nointernal.Has(gvk) {
fuzzer.Fuzz(item)
fuzzer.Fill(item)
} else {
internalObj, err := scheme.New(gvk.GroupKind().WithVersion(runtime.APIVersionInternal))
if err != nil {
t.Fatalf("couldn't create internal object %v: %v", gvk.Kind, err)
}
fuzzer.Fuzz(internalObj)
fuzzer.Fill(internalObj)
if err := scheme.Convert(internalObj, item, nil); err != nil {
t.Fatalf("conversion for %v failed: %v", gvk.Kind, err)

View File

@ -438,7 +438,7 @@ func NewGenericServerResponse(code int, verb string, qualifiedResource schema.Gr
message := fmt.Sprintf("the server responded with the status code %d but did not return more information", code)
switch code {
case http.StatusConflict:
if verb == "POST" {
if verb == http.MethodPost {
reason = metav1.StatusReasonAlreadyExists
} else {
reason = metav1.StatusReasonConflict

View File

@ -110,13 +110,13 @@ func TestErrorNew(t *testing.T) {
if time, ok := SuggestsClientDelay(NewTooManyRequests("doing something", 1)); time != 1 || !ok {
t.Errorf("unexpected %d", time)
}
if time, ok := SuggestsClientDelay(NewGenericServerResponse(429, "get", resource("tests"), "test", "doing something", 10, true)); time != 10 || !ok {
if time, ok := SuggestsClientDelay(NewGenericServerResponse(429, http.MethodGet, resource("tests"), "test", "doing something", 10, true)); time != 10 || !ok {
t.Errorf("unexpected %d", time)
}
if time, ok := SuggestsClientDelay(NewGenericServerResponse(500, "get", resource("tests"), "test", "doing something", 10, true)); time != 10 || !ok {
if time, ok := SuggestsClientDelay(NewGenericServerResponse(500, http.MethodGet, resource("tests"), "test", "doing something", 10, true)); time != 10 || !ok {
t.Errorf("unexpected %d", time)
}
if time, ok := SuggestsClientDelay(NewGenericServerResponse(429, "get", resource("tests"), "test", "doing something", 0, true)); time != 0 || ok {
if time, ok := SuggestsClientDelay(NewGenericServerResponse(429, http.MethodGet, resource("tests"), "test", "doing something", 0, true)); time != 0 || ok {
t.Errorf("unexpected %d", time)
}
}

View File

@ -221,6 +221,9 @@ func extractList(obj runtime.Object, allocNew bool) ([]runtime.Object, error) {
if err != nil {
return nil, err
}
if items.IsNil() {
return nil, nil
}
list := make([]runtime.Object, items.Len())
if len(list) == 0 {
return list, nil

View File

@ -25,15 +25,15 @@ import (
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"github.com/google/go-cmp/cmp"
fuzz "github.com/google/gofuzz"
"sigs.k8s.io/randfill"
)
func TestAsPartialObjectMetadata(t *testing.T) {
f := fuzz.New().NilChance(.5).NumElements(0, 1).RandSource(rand.NewSource(1))
f := randfill.New().NilChance(.5).NumElements(0, 1).RandSource(rand.NewSource(1))
for i := 0; i < 100; i++ {
m := &metav1.ObjectMeta{}
f.Fuzz(m)
f.Fill(m)
partial := AsPartialObjectMetadata(m)
if !reflect.DeepEqual(&partial.ObjectMeta, m) {
t.Fatalf("incomplete partial object metadata: %s", cmp.Diff(&partial.ObjectMeta, m))
@ -42,7 +42,7 @@ func TestAsPartialObjectMetadata(t *testing.T) {
for i := 0; i < 100; i++ {
m := &metav1beta1.PartialObjectMetadata{}
f.Fuzz(&m.ObjectMeta)
f.Fill(&m.ObjectMeta)
partial := AsPartialObjectMetadata(m)
if !reflect.DeepEqual(&partial.ObjectMeta, &m.ObjectMeta) {
t.Fatalf("incomplete partial object metadata: %s", cmp.Diff(&partial.ObjectMeta, &m.ObjectMeta))

View File

@ -0,0 +1,102 @@
/*
Copyright 2024 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 operation
import (
"slices"
"strings"
)
// Operation provides contextual information about a validation request and the API
// operation being validated.
// This type is intended for use with generate validation code and may be enhanced
// in the future to include other information needed to validate requests.
type Operation struct {
// Type is the category of operation being validated. This does not
// differentiate between HTTP verbs like PUT and PATCH, but rather merges
// those into a single "Update" category.
Type Type
// Options declare the options enabled for validation.
//
// Options should be set according to a resource validation strategy before validation
// is performed, and must be treated as read-only during validation.
//
// Options are identified by string names. Option string names may match the name of a feature
// gate, in which case the presence of the name in the set indicates that the feature is
// considered enabled for the resource being validated. Note that a resource may have a
// feature enabled even when the feature gate is disabled. This can happen when feature is
// already in-use by a resource, often because the feature gate was enabled when the
// resource first began using the feature.
//
// Unset options are disabled/false.
Options []string
// Request provides information about the request being validated.
Request Request
}
// HasOption returns true if the given string is in the Options slice.
func (o Operation) HasOption(option string) bool {
return slices.Contains(o.Options, option)
}
// Request provides information about the request being validated.
type Request struct {
// Subresources identifies the subresource path components of the request. For
// example, Subresources for a request to `/api/v1/pods/my-pod/status` would be
// `["status"]`. For `/api/v1/widget/my-widget/x/y/z`, it would be `["x", "y",
// "z"]`. For a root resource (`/api/v1/pods/my-pod`), Subresources will be an
// empty slice.
//
// Validation logic should only consult this field if the validation rules for a
// particular field differ depending on whether the main resource or a specific
// subresource is being accessed. For example:
//
// Updates to a Pod resource (`/`) normally cannot change container resource
// requests/limits after the Pod is created (they are immutable). However, when
// accessing the Pod's "resize" subresource (`/resize`), these specific fields
// are allowed to be modified. In this scenario, the validation logic for
// `spec.container[*].resources` must check `Subresources` to permit changes only
// when the request targets the "resize" subresource.
//
// Note: This field should not be used to control which fields a subresource
// operation is allowed to write. This is the responsibility of "field wiping".
// Field wiping logic is expected to be handled in resource strategies by
// modifying the incoming object before it is validated.
Subresources []string
}
// SubresourcePath returns the path is a slash-separated list of subresource
// names. For example, `/status`, `/resize`, or `/x/y/z`.
func (r Request) SubresourcePath() string {
if len(r.Subresources) == 0 {
return "/"
}
return "/" + strings.Join(r.Subresources, "/")
}
// Code is the request operation to be validated.
type Type uint32
const (
// Create indicates the request being validated is for a resource create operation.
Create Type = iota
// Update indicates the request being validated is for a resource update operation.
Update
)

View File

@ -28,9 +28,9 @@ import (
"unicode"
"github.com/google/go-cmp/cmp"
fuzz "github.com/google/gofuzz"
"github.com/spf13/pflag"
inf "gopkg.in/inf.v0"
"sigs.k8s.io/randfill"
cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct"
)
@ -827,12 +827,12 @@ func TestQuantityParseEmit(t *testing.T) {
}
}
var fuzzer = fuzz.New().Funcs(
func(q *Quantity, c fuzz.Continue) {
var fuzzer = randfill.New().Funcs(
func(q *Quantity, c randfill.Continue) {
q.i = Zero
if c.RandBool() {
if c.Bool() {
q.Format = BinarySI
if c.RandBool() {
if c.Bool() {
dec := &inf.Dec{}
q.d = infDecAmount{Dec: dec}
dec.SetScale(0)
@ -846,12 +846,12 @@ var fuzzer = fuzz.New().Funcs(
dec.SetUnscaled(c.Int63n(1024) << uint(10*c.Intn(5)))
return
}
if c.RandBool() {
if c.Bool() {
q.Format = DecimalSI
} else {
q.Format = DecimalExponent
}
if c.RandBool() {
if c.Bool() {
dec := &inf.Dec{}
q.d = infDecAmount{Dec: dec}
dec.SetScale(inf.Scale(c.Intn(4)))
@ -897,7 +897,7 @@ func TestQuantityDeepCopy(t *testing.T) {
func TestJSON(t *testing.T) {
for i := 0; i < 500; i++ {
q := &Quantity{}
fuzzer.Fuzz(q)
fuzzer.Fill(q)
b, err := json.Marshal(q)
if err != nil {
t.Errorf("error encoding %v: %v", q, err)
@ -1801,7 +1801,7 @@ func TestQuantityUnmarshalCBOR(t *testing.T) {
func TestQuantityRoundtripCBOR(t *testing.T) {
for i := 0; i < 500; i++ {
var initial, final Quantity
fuzzer.Fuzz(&initial)
fuzzer.Fill(&initial)
b, err := cbor.Marshal(initial)
if err != nil {
t.Errorf("error encoding %v: %v", initial, err)

59
pkg/api/safe/safe.go Normal file
View File

@ -0,0 +1,59 @@
/*
Copyright 2024 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 safe
// Field takes a pointer to any value (which may or may not be nil) and a
// function that traverses to a target type R (a typical use case is to
// dereference a field), and returns the result of the traversal, or the zero
// value of the target type.
//
// This is roughly equivalent to:
//
// value != nil ? fn(value) : zero-value
//
// ...in languages that support the ternary operator.
func Field[V any, R any](value *V, fn func(*V) R) R {
if value == nil {
var zero R
return zero
}
o := fn(value)
return o
}
// Cast takes any value, attempts to cast it to T, and returns the T value if
// the cast is successful, or else the zero value of T.
func Cast[T any](value any) T {
result, _ := value.(T)
return result
}
// Value takes a pointer to any value (which may or may not be nil) and a
// function that returns a pointer to the same type. If the value is not nil,
// it is returned, otherwise the result of the function is returned.
//
// This is roughly equivalent to:
//
// value != nil ? value : fn()
//
// ...in languages that support the ternary operator.
func Value[T any](value *T, fn func() *T) *T {
if value != nil {
return value
}
return fn()
}

View File

@ -0,0 +1,64 @@
# API validation
This package holds functions which validate fields and types in the Kubernetes
API. It may be useful beyond API validation, but this is the primary goal.
Most of the public functions here have signatures which adhere to the following
pattern, which is assumed by automation and code-generation:
```
import (
"context"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
func <Name>(ctx context.Context, op operation.Operation, fldPath *field.Path, value, oldValue <ValueType>, <OtherArgs...>) field.ErrorList
```
The name of validator functions should consider that callers will generally be
spelling out the package name and the function name, and so should aim for
legibility. E.g. `validate.Concept()`.
The `ctx` argument is Go's usual Context.
The `opCtx` argument provides information about the API operation in question.
The `fldPath` argument indicates the path to the field in question, to be used
in errors.
The `value` and `oldValue` arguments are the thing(s) being validated. For
CREATE operations (`opCtx.Operation == operation.Create`), the `oldValue`
argument will be nil. Many validators functions only look at the current value
(`value`) and disregard `oldValue`.
The `value` and `oldValue` arguments are always nilable - pointers to primitive
types, slices of any type, or maps of any type. Validator functions should
avoid dereferencing nil. Callers are expected to not pass a nil `value` unless the
API field itself was nilable. `oldValue` is always nil for CREATE operations and
is also nil for UPDATE operations if the `value` is not correlated with an `oldValue`.
Simple content-validators may have no `<OtherArgs>`, but validator functions
may take additional arguments. Some validator functions will be built as
generics, e.g. to allow any integer type or to handle arbitrary slices.
Examples:
```
// NonEmpty validates that a string is not empty.
func NonEmpty(ctx context.Context, op operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList
// Even validates that a slice has an even number of items.
func Even[T any](ctx context.Context, op operation.Operation, fldPath *field.Path, value, _ []T) field.ErrorList
// KeysMaxLen validates that all of the string keys in a map are under the
// specified length.
func KeysMaxLen[T any](ctx context.Context, op operation.Operation, fldPath *field.Path, value, _ map[string]T, maxLen int) field.ErrorList
```
Validator functions always return an `ErrorList` where each item is a distinct
validation failure and a zero-length return value (not just nil) indicates
success.
Good validation failure messages follow the Kubernetes API conventions, for
example using "must" instead of "should".

View File

@ -0,0 +1,28 @@
/*
Copyright 2024 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 validate
import (
"context"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// ValidateFunc is a function that validates a value, possibly considering the
// old value (if any).
type ValidateFunc[T any] func(ctx context.Context, op operation.Operation, fldPath *field.Path, newValue, oldValue T) field.ErrorList

View File

@ -0,0 +1,32 @@
/*
Copyright 2025 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 constraints
// Signed is a constraint that permits any signed integer type.
type Signed interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
// Unsigned is a constraint that permits any unsigned integer type.
type Unsigned interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
// Integer is a constraint that permits any integer type.
type Integer interface {
Signed | Unsigned
}

View File

@ -0,0 +1,39 @@
/*
Copyright 2014 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 content
import (
"fmt"
"reflect"
"k8s.io/apimachinery/pkg/api/validate/constraints"
)
// MinError returns a string explanation of a "must be greater than or equal"
// validation failure.
func MinError[T constraints.Integer](min T) string {
return fmt.Sprintf("must be greater than or equal to %d", min)
}
// NEQError returns a string explanation of a "must not be equal to" validation failure.
func NEQError[T any](disallowed T) string {
format := "%v"
if reflect.ValueOf(disallowed).Kind() == reflect.String {
format = "%q"
}
return fmt.Sprintf("must not be equal to "+format, disallowed)
}

50
pkg/api/validate/doc.go Normal file
View File

@ -0,0 +1,50 @@
/*
Copyright 2024 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 validate holds API validation functions which are designed for use
// with the k8s.io/code-generator/cmd/validation-gen tool. Each validation
// function has a similar fingerprint:
//
// func <Name>(ctx context.Context,
// op operation.Operation,
// fldPath *field.Path,
// value, oldValue <nilable type>,
// <other args...>) field.ErrorList
//
// The value and oldValue arguments will always be a nilable type. If the
// original value was a string, these will be a *string. If the original value
// was a slice or map, these will be the same slice or map type.
//
// For a CREATE operation, the oldValue will always be nil. For an UPDATE
// operation, either value or oldValue may be nil, e.g. when adding or removing
// a value in a list-map. Validators which care about UPDATE operations should
// look at the opCtx argument to know which operation is being executed.
//
// Tightened validation (also known as ratcheting validation) is supported by
// defining a new validation function. For example:
//
// func TightenedMaxLength(ctx context.Context, op operation.Operation, fldPath *field.Path, value, oldValue *string) field.ErrorList {
// if oldValue != nil && len(MaxLength(ctx, op, fldPath, oldValue, nil)) > 0 {
// // old value is not valid, so this value skips the tightened validation
// return nil
// }
// return MaxLength(ctx, op, fldPath, value, nil)
// }
//
// In general, we cannot distinguish a non-specified slice or map from one that
// is specified but empty. Validators should not rely on nil values, but use
// len() instead.
package validate

171
pkg/api/validate/each.go Normal file
View File

@ -0,0 +1,171 @@
/*
Copyright 2024 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 validate
import (
"context"
"sort"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// MatchFunc is a function that compares two values of the same type,
// according to some criteria, and returns true if they match.
type MatchFunc[T any] func(T, T) bool
// EachSliceVal performs validation on each element of newSlice using the provided validation function.
//
// For update operations, the match function finds corresponding values in oldSlice for each
// value in newSlice. This comparison can be either full or partial (e.g., matching only
// specific struct fields that serve as a unique identifier). If match is nil, validation
// proceeds without considering old values, and the equiv function is not used.
//
// For update operations, the equiv function checks if a new value is equivalent to its
// corresponding old value, enabling validation ratcheting. If equiv is nil but match is
// provided, the match function is assumed to perform full value comparison.
//
// Note: The slice element type must be non-nilable.
func EachSliceVal[T any](ctx context.Context, op operation.Operation, fldPath *field.Path, newSlice, oldSlice []T,
match, equiv MatchFunc[T], validator ValidateFunc[*T]) field.ErrorList {
var errs field.ErrorList
for i, val := range newSlice {
var old *T
if match != nil && len(oldSlice) > 0 {
old = lookup(oldSlice, val, match)
}
// If the operation is an update, for validation ratcheting, skip re-validating if the old
// value exists and either:
// 1. The match function provides full comparison (equiv is nil)
// 2. The equiv function confirms the values are equivalent (either directly or semantically)
//
// The equiv function provides equality comparison when match uses partial comparison.
if op.Type == operation.Update && old != nil && (equiv == nil || equiv(val, *old)) {
continue
}
errs = append(errs, validator(ctx, op, fldPath.Index(i), &val, old)...)
}
return errs
}
// lookup returns a pointer to the first element in the list that matches the
// target, according to the provided comparison function, or else nil.
func lookup[T any](list []T, target T, match MatchFunc[T]) *T {
for i := range list {
if match(list[i], target) {
return &list[i]
}
}
return nil
}
// EachMapVal validates each value in newMap using the specified validation
// function, passing the corresponding old value from oldMap if the key exists in oldMap.
// For update operations, it implements validation ratcheting by skipping validation
// when the old value exists and the equiv function confirms the values are equivalent.
// The value-type of the map is assumed to not be nilable.
// If equiv is nil, value-based ratcheting is disabled and all values will be validated.
func EachMapVal[K ~string, V any](ctx context.Context, op operation.Operation, fldPath *field.Path, newMap, oldMap map[K]V,
equiv MatchFunc[V], validator ValidateFunc[*V]) field.ErrorList {
var errs field.ErrorList
for key, val := range newMap {
var old *V
if o, found := oldMap[key]; found {
old = &o
}
// If the operation is an update, for validation ratcheting, skip re-validating if the old
// value is found and the equiv function confirms the values are equivalent.
if op.Type == operation.Update && old != nil && equiv != nil && equiv(val, *old) {
continue
}
errs = append(errs, validator(ctx, op, fldPath.Key(string(key)), &val, old)...)
}
return errs
}
// EachMapKey validates each element of newMap with the specified
// validation function.
func EachMapKey[K ~string, T any](ctx context.Context, op operation.Operation, fldPath *field.Path, newMap, oldMap map[K]T,
validator ValidateFunc[*K]) field.ErrorList {
var errs field.ErrorList
for key := range newMap {
var old *K
if _, found := oldMap[key]; found {
old = &key
}
// If the operation is an update, for validation ratcheting, skip re-validating if
// the key is found in oldMap.
if op.Type == operation.Update && old != nil {
continue
}
// Note: the field path is the field, not the key.
errs = append(errs, validator(ctx, op, fldPath, &key, nil)...)
}
return errs
}
// Unique verifies that each element of newSlice is unique, according to the
// match function. It compares every element of the slice with every other
// element and returns errors for non-unique items.
func Unique[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, newSlice, _ []T, match MatchFunc[T]) field.ErrorList {
var dups []int
for i, val := range newSlice {
for j := i + 1; j < len(newSlice); j++ {
other := newSlice[j]
if match(val, other) {
if dups == nil {
dups = make([]int, 0, len(newSlice))
}
if lookup(dups, j, func(a, b int) bool { return a == b }) == nil {
dups = append(dups, j)
}
}
}
}
var errs field.ErrorList
sort.Ints(dups)
for _, i := range dups {
var val any = newSlice[i]
// TODO: we don't want the whole item to be logged in the error, just
// the key(s). Unfortunately, the way errors are rendered, it comes out
// as something like "map[string]any{...}" which is not very nice. Once
// that is fixed, we can consider adding a way for this function to
// specify that just the keys should be rendered in the error.
errs = append(errs, field.Duplicate(fldPath.Index(i), val))
}
return errs
}
// SemanticDeepEqual is a MatchFunc that uses equality.Semantic.DeepEqual to
// compare two values.
// This wrapper is needed because MatchFunc requires a function that takes two
// arguments of specific type T, while equality.Semantic.DeepEqual takes
// arguments of type interface{}/any. The wrapper satisfies the type
// constraints of MatchFunc while leveraging the underlying semantic equality
// logic. It can be used by any other function that needs to call DeepEqual.
func SemanticDeepEqual[T any](a, b T) bool {
return equality.Semantic.DeepEqual(a, b)
}
// DirectEqual is a MatchFunc that uses the == operator to compare two values.
// It can be used by any other function that needs to compare two values
// directly.
func DirectEqual[T comparable](a, b T) bool {
return a == b
}

View File

@ -0,0 +1,492 @@
/*
Copyright 2025 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 validate
import (
"context"
"fmt"
"reflect"
"slices"
"testing"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/utils/ptr"
)
type TestStruct struct {
I int
D string
}
type TestStructWithKey struct {
Key string
I int
D string
}
type NonComparableKey struct {
I *int
}
type NonComparableStruct struct {
I int
S []string
}
type NonComparableStructWithKey struct {
Key string
I int
S []string
}
type NonComparableStructWithPtr struct {
I int
P *int
}
func TestEachSliceVal(t *testing.T) {
testEachSliceVal(t, "valid", []int{11, 12, 13})
testEachSliceVal(t, "valid", []string{"a", "b", "c"})
testEachSliceVal(t, "valid", []TestStruct{{11, "a"}, {12, "b"}, {13, "c"}})
testEachSliceVal(t, "empty", []int{})
testEachSliceVal(t, "empty", []string{})
testEachSliceVal(t, "empty", []TestStruct{})
testEachSliceVal[int](t, "nil", nil)
testEachSliceVal[string](t, "nil", nil)
testEachSliceVal[TestStruct](t, "nil", nil)
testEachSliceValUpdate(t, "valid", []int{11, 12, 13})
testEachSliceValUpdate(t, "valid", []string{"a", "b", "c"})
testEachSliceValUpdate(t, "valid", []TestStruct{{11, "a"}, {12, "b"}, {13, "c"}})
testEachSliceValUpdate(t, "empty", []int{})
testEachSliceValUpdate(t, "empty", []string{})
testEachSliceValUpdate(t, "empty", []TestStruct{})
testEachSliceValUpdate[int](t, "nil", nil)
testEachSliceValUpdate[string](t, "nil", nil)
testEachSliceValUpdate[TestStruct](t, "nil", nil)
}
func testEachSliceVal[T any](t *testing.T, name string, input []T) {
t.Helper()
var zero T
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
calls := 0
vfn := func(ctx context.Context, op operation.Operation, fldPath *field.Path, newVal, oldVal *T) field.ErrorList {
if oldVal != nil {
t.Errorf("expected nil oldVal, got %v", *oldVal)
}
calls++
return nil
}
_ = EachSliceVal(context.Background(), operation.Operation{}, field.NewPath("test"), input, nil, nil, nil, vfn)
if calls != len(input) {
t.Errorf("expected %d calls, got %d", len(input), calls)
}
})
}
func testEachSliceValUpdate[T any](t *testing.T, name string, input []T) {
t.Helper()
var zero T
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
calls := 0
vfn := func(ctx context.Context, op operation.Operation, fldPath *field.Path, newVal, oldVal *T) field.ErrorList {
if oldVal == nil {
t.Fatalf("expected non-nil oldVal")
}
if !reflect.DeepEqual(*newVal, *oldVal) {
t.Errorf("expected oldVal == newVal, got %v, %v", *oldVal, *newVal)
}
calls++
return nil
}
old := make([]T, len(input))
copy(old, input)
slices.Reverse(old)
match := func(a, b T) bool { return reflect.DeepEqual(a, b) }
_ = EachSliceVal(context.Background(), operation.Operation{}, field.NewPath("test"), input, old, match, match, vfn)
if calls != len(input) {
t.Errorf("expected %d calls, got %d", len(input), calls)
}
})
}
func TestEachSliceValRatcheting(t *testing.T) {
testEachSliceValRatcheting(t, "ComparableStruct same data different order",
[]TestStruct{
{11, "a"}, {12, "b"}, {13, "c"},
},
[]TestStruct{
{11, "a"}, {13, "c"}, {12, "b"},
},
SemanticDeepEqual,
nil,
)
testEachSliceValRatcheting(t, "ComparableStruct less data in new, exist in old",
[]TestStruct{
{11, "a"}, {12, "b"}, {13, "c"},
},
[]TestStruct{
{11, "a"}, {13, "c"},
},
DirectEqual,
nil,
)
testEachSliceValRatcheting(t, "Comparable struct with key same data different order",
[]TestStructWithKey{
{Key: "a", I: 11, D: "a"}, {Key: "b", I: 12, D: "b"}, {Key: "c", I: 13, D: "c"},
},
[]TestStructWithKey{
{Key: "a", I: 11, D: "a"}, {Key: "c", I: 13, D: "c"}, {Key: "b", I: 12, D: "b"},
},
MatchFunc[TestStructWithKey](func(a, b TestStructWithKey) bool {
return a.Key == b.Key
}),
DirectEqual,
)
testEachSliceValRatcheting(t, "Comparable struct with key less data in new, exist in old",
[]TestStructWithKey{
{Key: "a", I: 11, D: "a"}, {Key: "b", I: 12, D: "b"}, {Key: "c", I: 13, D: "c"},
},
[]TestStructWithKey{
{Key: "a", I: 11, D: "a"}, {Key: "c", I: 13, D: "c"},
},
MatchFunc[TestStructWithKey](func(a, b TestStructWithKey) bool {
return a.Key == b.Key
}),
DirectEqual,
)
testEachSliceValRatcheting(t, "NonComparableStruct same data different order",
[]NonComparableStruct{
{I: 11, S: []string{"a"}}, {I: 12, S: []string{"b"}}, {I: 13, S: []string{"c"}},
},
[]NonComparableStruct{
{I: 11, S: []string{"a"}}, {I: 13, S: []string{"c"}}, {I: 12, S: []string{"b"}},
},
SemanticDeepEqual,
nil,
)
testEachSliceValRatcheting(t, "NonComparableStructWithKey same data different order",
[]NonComparableStructWithKey{
{Key: "a", I: 11, S: []string{"a"}}, {Key: "b", I: 12, S: []string{"b"}}, {Key: "c", I: 13, S: []string{"c"}},
},
[]NonComparableStructWithKey{
{Key: "a", I: 11, S: []string{"a"}}, {Key: "b", I: 12, S: []string{"b"}}, {Key: "c", I: 13, S: []string{"c"}},
},
MatchFunc[NonComparableStructWithKey](func(a, b NonComparableStructWithKey) bool {
return a.Key == b.Key
}),
SemanticDeepEqual,
)
}
func testEachSliceValRatcheting[T any](t *testing.T, name string, old, new []T, match, equiv MatchFunc[T]) {
t.Helper()
var zero T
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
vfn := func(ctx context.Context, op operation.Operation, fldPath *field.Path, newVal, oldVal *T) field.ErrorList {
return field.ErrorList{field.Invalid(fldPath, *newVal, "expected no calls")}
}
errs := EachSliceVal(context.Background(), operation.Operation{Type: operation.Update}, field.NewPath("test"), new, old, match, equiv, vfn)
if len(errs) > 0 {
t.Errorf("expected no errors, got %d: %s", len(errs), fmtErrs(errs))
}
})
}
func TestEachMapVal(t *testing.T) {
testEachMapVal(t, "valid", map[string]int{"one": 11, "two": 12, "three": 13})
testEachMapVal(t, "valid", map[string]string{"A": "a", "B": "b", "C": "c"})
testEachMapVal(t, "valid", map[string]TestStruct{"one": {11, "a"}, "two": {12, "b"}, "three": {13, "c"}})
testEachMapVal(t, "empty", map[string]int{})
testEachMapVal(t, "empty", map[string]string{})
testEachMapVal(t, "empty", map[string]TestStruct{})
testEachMapVal[int](t, "nil", nil)
testEachMapVal[string](t, "nil", nil)
testEachMapVal[TestStruct](t, "nil", nil)
}
func testEachMapVal[T any](t *testing.T, name string, input map[string]T) {
t.Helper()
var zero T
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
calls := 0
vfn := func(ctx context.Context, op operation.Operation, fldPath *field.Path, newVal, oldVal *T) field.ErrorList {
if oldVal != nil {
t.Errorf("expected nil oldVal, got %v", *oldVal)
}
calls++
return nil
}
_ = EachMapVal(context.Background(), operation.Operation{}, field.NewPath("test"), input, nil, nil, vfn)
if calls != len(input) {
t.Errorf("expected %d calls, got %d", len(input), calls)
}
})
}
func TestEachMapValRatcheting(t *testing.T) {
testEachMapValRatcheting(t, "primitive same data",
map[string]int{"one": 11, "two": 12, "three": 13},
map[string]int{"one": 11, "three": 13, "two": 12},
DirectEqual,
0,
)
testEachMapValRatcheting(t, "primitive less data in new, exist in old",
map[string]int{"one": 11, "two": 12, "three": 13},
map[string]int{"one": 11, "three": 13},
DirectEqual,
0,
)
testEachMapValRatcheting(t, "primitive new data, not exist in old",
map[string]int{"one": 11, "two": 12, "three": 13},
map[string]int{"one": 11, "three": 13, "two": 12, "four": 14},
DirectEqual,
1,
)
testEachMapValRatcheting(t, "non comparable value, same data",
map[string]NonComparableStruct{
"one": {I: 11, S: []string{"a"}},
"two": {I: 12, S: []string{"b"}},
"three": {I: 13, S: []string{"c"}},
},
map[string]NonComparableStruct{
"one": {I: 11, S: []string{"a"}},
"three": {I: 13, S: []string{"c"}},
"two": {I: 12, S: []string{"b"}},
},
SemanticDeepEqual,
0,
)
testEachMapValRatcheting(t, "non comparable value, less data in new, exist in old",
map[string]NonComparableStruct{
"one": {I: 11, S: []string{"a"}},
"two": {I: 12, S: []string{"b"}},
"three": {I: 13, S: []string{"c"}},
},
map[string]NonComparableStruct{
"one": {I: 11, S: []string{"a"}},
"three": {I: 13, S: []string{"c"}},
},
SemanticDeepEqual,
0,
)
testEachMapValRatcheting(t, "non comparable value, new data, not exist in old",
map[string]NonComparableStruct{
"one": {I: 11, S: []string{"a"}},
"two": {I: 12, S: []string{"b"}},
"three": {I: 13, S: []string{"c"}},
},
map[string]NonComparableStruct{
"one": {I: 11, S: []string{"a"}},
"three": {I: 13, S: []string{"c"}},
"two": {I: 12, S: []string{"b"}},
"four": {I: 14, S: []string{"d"}},
},
SemanticDeepEqual,
1,
)
testEachMapValRatcheting(t, "struct with pointer field, same value different pointer",
map[string]NonComparableStructWithPtr{
"one": {I: 11, P: ptr.To(1)},
"two": {I: 12, P: ptr.To(2)},
},
map[string]NonComparableStructWithPtr{
"one": {I: 11, P: ptr.To(1)},
"two": {I: 12, P: ptr.To(2)},
},
SemanticDeepEqual,
0,
)
testEachMapValRatcheting(t, "nil map to empty map",
nil,
map[string]int{},
DirectEqual,
0,
)
testEachMapValRatcheting(t, "nil map to non-empty map",
nil,
map[string]int{"one": 1},
DirectEqual,
1, // Expect validation for new entry
)
testEachMapValRatcheting(t, "empty map to nil map",
map[string]int{},
nil,
DirectEqual,
0,
)
testEachMapValRatcheting(t, "non-empty map to nil map",
map[string]int{"one": 1},
nil,
DirectEqual,
0,
)
}
func testEachMapValRatcheting[K ~string, V any](t *testing.T, name string, old, new map[K]V, equiv MatchFunc[V], wantCalls int) {
t.Helper()
var zero V
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
calls := 0
vfn := func(ctx context.Context, op operation.Operation, fldPath *field.Path, newVal, oldVal *V) field.ErrorList {
calls++
return nil
}
_ = EachMapVal(context.Background(), operation.Operation{Type: operation.Update}, field.NewPath("test"), new, old, equiv, vfn)
if calls != wantCalls {
t.Errorf("expected %d calls, got %d", wantCalls, calls)
}
})
}
type StringType string
func TestEachMapKey(t *testing.T) {
testEachMapKey(t, "valid", map[string]int{"one": 11, "two": 12, "three": 13})
testEachMapKey(t, "valid", map[StringType]string{"A": "a", "B": "b", "C": "c"})
}
func testEachMapKey[K ~string, V any](t *testing.T, name string, input map[K]V) {
t.Helper()
var zero K
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
calls := 0
vfn := func(ctx context.Context, op operation.Operation, fldPath *field.Path, newVal, oldVal *K) field.ErrorList {
if oldVal != nil {
t.Errorf("expected nil oldVal, got %v", *oldVal)
}
calls++
return nil
}
_ = EachMapKey(context.Background(), operation.Operation{}, field.NewPath("test"), input, nil, vfn)
if calls != len(input) {
t.Errorf("expected %d calls, got %d", len(input), calls)
}
})
}
func TestEachMapKeyRatcheting(t *testing.T) {
testEachMapKeyRatcheting(t, "same data, 0 validation calls",
map[string]int{"one": 11, "two": 12, "three": 13},
map[string]int{"one": 11, "three": 13, "two": 12},
0,
)
testEachMapKeyRatcheting(t, "less data in new, exist in old, 0 validation calls",
map[string]int{"one": 11, "two": 12, "three": 13},
map[string]int{"one": 11, "three": 13},
0,
)
testEachMapKeyRatcheting(t, "new data, not exist in old, 1 validation call",
map[string]int{"one": 11, "two": 12, "three": 13},
map[string]int{"one": 11, "three": 13, "two": 12, "four": 14},
1,
)
}
func testEachMapKeyRatcheting[K ~string, V any](t *testing.T, name string, old, new map[K]V, wantCalls int) {
t.Helper()
var zero V
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
calls := 0
vfn := func(ctx context.Context, op operation.Operation, fldPath *field.Path, newVal, oldVal *K) field.ErrorList {
calls++
return nil
}
_ = EachMapKey(context.Background(), operation.Operation{Type: operation.Update}, field.NewPath("test"), new, old, vfn)
if calls != wantCalls {
t.Errorf("expected %d calls, got %d", wantCalls, calls)
}
})
}
func TestUniqueComparableValues(t *testing.T) {
testUnique(t, "int_nil", []int(nil), 0)
testUnique(t, "int_empty", []int{}, 0)
testUnique(t, "int_uniq", []int{1, 2, 3}, 0)
testUnique(t, "int_dup", []int{1, 2, 3, 2, 1}, 2)
testUnique(t, "string_nil", []string(nil), 0)
testUnique(t, "string_empty", []string{}, 0)
testUnique(t, "string_uniq", []string{"a", "b", "c"}, 0)
testUnique(t, "string_dup", []string{"a", "a", "c", "b", "a"}, 2)
type isComparable struct {
I int
S string
}
testUnique(t, "struct_nil", []isComparable(nil), 0)
testUnique(t, "struct_empty", []isComparable{}, 0)
testUnique(t, "struct_uniq", []isComparable{{1, "a"}, {2, "b"}, {3, "c"}}, 0)
testUnique(t, "struct_dup", []isComparable{{1, "a"}, {2, "b"}, {3, "c"}, {2, "b"}, {1, "a"}}, 2)
}
func testUnique[T comparable](t *testing.T, name string, input []T, wantErrs int) {
t.Helper()
t.Run(fmt.Sprintf("%s(direct)", name), func(t *testing.T) {
errs := Unique(context.Background(), operation.Operation{}, field.NewPath("test"), input, nil, DirectEqual)
if len(errs) != wantErrs {
t.Errorf("expected %d errors, got %d: %s", wantErrs, len(errs), fmtErrs(errs))
}
})
t.Run(fmt.Sprintf("%s(reflect)", name), func(t *testing.T) {
errs := Unique(context.Background(), operation.Operation{}, field.NewPath("test"), input, nil, SemanticDeepEqual)
if len(errs) != wantErrs {
t.Errorf("expected %d errors, got %d: %s", wantErrs, len(errs), fmtErrs(errs))
}
})
}
func TestUniqueNonComparableValues(t *testing.T) {
type nonComparable struct {
I int
S []string
}
testUniqueByReflect(t, "noncomp_nil", []nonComparable(nil), 0)
testUniqueByReflect(t, "noncomp_empty", []nonComparable{}, 0)
testUniqueByReflect(t, "noncomp_uniq", []nonComparable{{1, []string{"a"}}, {2, []string{"b"}}, {3, []string{"c"}}}, 0)
testUniqueByReflect(t, "noncomp_dup", []nonComparable{
{1, []string{"a"}},
{2, []string{"b"}},
{3, []string{"c"}},
{2, []string{"b"}},
{1, []string{"a"}}}, 2)
}
func testUniqueByReflect[T any](t *testing.T, name string, input []T, wantErrs int) {
t.Helper()
var zero T
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
errs := Unique(context.Background(), operation.Operation{}, field.NewPath("test"), input, nil, SemanticDeepEqual)
if len(errs) != wantErrs {
t.Errorf("expected %d errors, got %d: %s", wantErrs, len(errs), fmtErrs(errs))
}
})
}

40
pkg/api/validate/enum.go Normal file
View File

@ -0,0 +1,40 @@
/*
Copyright 2024 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 validate
import (
"context"
"slices"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// Enum verifies that the specified value is one of the valid symbols.
// This is for string enums only.
func Enum[T ~string](_ context.Context, op operation.Operation, fldPath *field.Path, value, _ *T, symbols sets.Set[T]) field.ErrorList {
if value == nil {
return nil
}
if !symbols.Has(*value) {
symbolList := symbols.UnsortedList()
slices.Sort(symbolList)
return field.ErrorList{field.NotSupported[T](fldPath, *value, symbolList)}
}
return nil
}

View File

@ -0,0 +1,107 @@
/*
Copyright 2024 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 validate
import (
"context"
"testing"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
)
func TestEnum(t *testing.T) {
cases := []struct {
value string
valid sets.Set[string]
err bool
}{{
value: "a",
valid: sets.New("a", "b", "c"),
err: false,
}, {
value: "x",
valid: sets.New("c", "a", "b"),
err: true,
}}
for i, tc := range cases {
result := Enum(context.Background(), operation.Operation{}, field.NewPath("fldpath"), &tc.value, nil, tc.valid)
if len(result) > 0 && !tc.err {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err {
t.Errorf("case %d: unexpected success", i)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if want, got := `supported values: "a", "b", "c"`, result[0].Detail; got != want {
t.Errorf("case %d: wrong error, expected: %q, got: %q", i, want, got)
}
}
}
}
func TestEnumTypedef(t *testing.T) {
type StringType string
const (
NotStringFoo StringType = "foo"
NotStringBar StringType = "bar"
NotStringQux StringType = "qux"
)
cases := []struct {
value StringType
valid sets.Set[StringType]
err bool
}{{
value: "foo",
valid: sets.New(NotStringFoo, NotStringBar, NotStringQux),
err: false,
}, {
value: "x",
valid: sets.New(NotStringFoo, NotStringBar, NotStringQux),
err: true,
}}
for i, tc := range cases {
result := Enum(context.Background(), operation.Operation{}, field.NewPath("fldpath"), &tc.value, nil, tc.valid)
if len(result) > 0 && !tc.err {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err {
t.Errorf("case %d: unexpected success", i)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if want, got := `supported values: "bar", "foo", "qux"`, result[0].Detail; got != want {
t.Errorf("case %d: wrong error, expected: %q, got: %q", i, want, got)
}
}
}
}

View File

@ -0,0 +1,38 @@
/*
Copyright 2025 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 validate
import (
"context"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/api/validate/content"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// NEQ validates that the specified comparable value is not equal to the disallowed value.
func NEQ[T comparable](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T, disallowed T) field.ErrorList {
if value == nil {
return nil
}
if *value == disallowed {
return field.ErrorList{
field.Invalid(fldPath, *value, content.NEQError(disallowed)).WithOrigin("neq"),
}
}
return nil
}

View File

@ -0,0 +1,64 @@
/*
Copyright 2025 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 validate
import (
"context"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// ImmutableByCompare verifies that the specified value has not changed in the
// course of an update operation. It does nothing if the old value is not
// provided. If the caller needs to compare types that are not trivially
// comparable, they should use ImmutableByReflect instead.
//
// Caution: structs with pointer fields satisfy comparable, but this function
// will only compare pointer values. It does not compare the pointed-to
// values.
func ImmutableByCompare[T comparable](_ context.Context, op operation.Operation, fldPath *field.Path, value, oldValue *T) field.ErrorList {
if op.Type != operation.Update {
return nil
}
if value == nil && oldValue == nil {
return nil
}
if value == nil || oldValue == nil || *value != *oldValue {
return field.ErrorList{
field.Forbidden(fldPath, "field is immutable"),
}
}
return nil
}
// ImmutableByReflect verifies that the specified value has not changed in
// the course of an update operation. It does nothing if the old value is not
// provided. Unlike ImmutableByCompare, this function can be used with types that are
// not directly comparable, at the cost of performance.
func ImmutableByReflect[T any](_ context.Context, op operation.Operation, fldPath *field.Path, value, oldValue T) field.ErrorList {
if op.Type != operation.Update {
return nil
}
if !equality.Semantic.DeepEqual(value, oldValue) {
return field.ErrorList{
field.Forbidden(fldPath, "field is immutable"),
}
}
return nil
}

View File

@ -0,0 +1,250 @@
/*
Copyright 2025 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 validate
import (
"context"
"testing"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/utils/ptr"
)
type StructComparable struct {
S string
I int
B bool
}
func TestImmutableByCompare(t *testing.T) {
structA := StructComparable{"abc", 123, true}
structA2 := structA
structB := StructComparable{"xyz", 456, false}
for _, tc := range []struct {
name string
fn func(operation.Operation, *field.Path) field.ErrorList
fail bool
}{{
name: "nil both values",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByCompare[int](context.Background(), op, fld, nil, nil)
},
}, {
name: "nil value",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByCompare(context.Background(), op, fld, nil, ptr.To(123))
},
fail: true,
}, {
name: "nil oldValue",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByCompare(context.Background(), op, fld, ptr.To(123), nil)
},
fail: true,
}, {
name: "int",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByCompare(context.Background(), op, fld, ptr.To(123), ptr.To(123))
},
}, {
name: "int fail",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByCompare(context.Background(), op, fld, ptr.To(123), ptr.To(456))
},
fail: true,
}, {
name: "string",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByCompare(context.Background(), op, fld, ptr.To("abc"), ptr.To("abc"))
},
}, {
name: "string fail",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByCompare(context.Background(), op, fld, ptr.To("abc"), ptr.To("xyz"))
},
fail: true,
}, {
name: "bool",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByCompare(context.Background(), op, fld, ptr.To(true), ptr.To(true))
},
}, {
name: "bool fail",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByCompare(context.Background(), op, fld, ptr.To(true), ptr.To(false))
},
fail: true,
}, {
name: "same struct",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByCompare(context.Background(), op, fld, ptr.To(structA), ptr.To(structA))
},
}, {
name: "equal struct",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByCompare(context.Background(), op, fld, ptr.To(structA), ptr.To(structA2))
},
}, {
name: "struct fail",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByCompare(context.Background(), op, fld, ptr.To(structA), ptr.To(structB))
},
fail: true,
}} {
t.Run(tc.name, func(t *testing.T) {
errs := tc.fn(operation.Operation{Type: operation.Create}, field.NewPath(""))
if len(errs) != 0 { // Create should always succeed
t.Errorf("case %q (create): expected success: %v", tc.name, errs)
}
errs = tc.fn(operation.Operation{Type: operation.Update}, field.NewPath(""))
if tc.fail && len(errs) == 0 {
t.Errorf("case %q (update): expected failure", tc.name)
} else if !tc.fail && len(errs) != 0 {
t.Errorf("case %q (update): expected success: %v", tc.name, errs)
}
})
}
}
type StructNonComparable struct {
S string
SP *string
I int
IP *int
B bool
BP *bool
SS []string
MSS map[string]string
}
func TestImmutableByReflect(t *testing.T) {
structA := StructNonComparable{
S: "abc",
SP: ptr.To("abc"),
I: 123,
IP: ptr.To(123),
B: true,
BP: ptr.To(true),
SS: []string{"a", "b", "c"},
MSS: map[string]string{"a": "b", "c": "d"},
}
structA2 := structA
structA2.SP = ptr.To("abc")
structA2.IP = ptr.To(123)
structA2.BP = ptr.To(true)
structA2.SS = []string{"a", "b", "c"}
structA2.MSS = map[string]string{"a": "b", "c": "d"}
structB := StructNonComparable{
S: "xyz",
SP: ptr.To("xyz"),
I: 456,
IP: ptr.To(456),
B: false,
BP: ptr.To(false),
SS: []string{"x", "y", "z"},
MSS: map[string]string{"x": "X", "y": "Y"},
}
for _, tc := range []struct {
name string
fn func(operation.Operation, *field.Path) field.ErrorList
fail bool
}{{
name: "nil both values",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByReflect[*int](context.Background(), op, fld, nil, nil)
},
}, {
name: "nil value",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByReflect(context.Background(), op, fld, nil, ptr.To(123))
},
fail: true,
}, {
name: "nil oldValue",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByReflect(context.Background(), op, fld, ptr.To(123), nil)
},
fail: true,
}, {
name: "int",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByReflect(context.Background(), op, fld, ptr.To(123), ptr.To(123))
},
}, {
name: "int fail",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByReflect(context.Background(), op, fld, ptr.To(123), ptr.To(456))
},
fail: true,
}, {
name: "string",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByReflect(context.Background(), op, fld, ptr.To("abc"), ptr.To("abc"))
},
}, {
name: "string fail",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByReflect(context.Background(), op, fld, ptr.To("abc"), ptr.To("xyz"))
},
fail: true,
}, {
name: "bool",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByReflect(context.Background(), op, fld, ptr.To(true), ptr.To(true))
},
}, {
name: "bool fail",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByReflect(context.Background(), op, fld, ptr.To(true), ptr.To(false))
},
fail: true,
}, {
name: "same struct",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByReflect(context.Background(), op, fld, ptr.To(structA), ptr.To(structA))
},
}, {
name: "equal struct",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByReflect(context.Background(), op, fld, ptr.To(structA), ptr.To(structA2))
},
}, {
name: "struct fail",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return ImmutableByReflect(context.Background(), op, fld, ptr.To(structA), ptr.To(structB))
},
fail: true,
}} {
t.Run(tc.name, func(t *testing.T) {
errs := tc.fn(operation.Operation{Type: operation.Create}, field.NewPath(""))
if len(errs) != 0 { // Create should always succeed
t.Errorf("case %q (create): expected success: %v", tc.name, errs)
}
errs = tc.fn(operation.Operation{Type: operation.Update}, field.NewPath(""))
if tc.fail && len(errs) == 0 {
t.Errorf("case %q (update): expected failure", tc.name)
} else if !tc.fail && len(errs) != 0 {
t.Errorf("case %q (update): expected success: %v", tc.name, errs)
}
})
}
}

72
pkg/api/validate/item.go Normal file
View File

@ -0,0 +1,72 @@
/*
Copyright 2024 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 validate
import (
"context"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// MatchItemFn takes a pointer to an item and returns true if it matches the criteria.
type MatchItemFn[T any] func(*T) bool
// SliceItem finds the first item in newList that satisfies the 'matches' predicate,
// and if found, also looks for a matching item in oldList. It then invokes
// 'itemValidator' on these items.
// The fldPath passed to itemValidator is indexed to the matched item's position in newList.
// This function processes only the *first* matching item found in newList.
// It assumes that the 'matches' predicate targets a unique identifier (primary key) and
// will match at most one element per list.
// If this assumption is violated, changes in list order can lead this function
// to have inconsistent behavior.
// This function does not validate items that were removed (present in oldList but not in newList).
func SliceItem[TList ~[]TItem, TItem any](
ctx context.Context, op operation.Operation, fldPath *field.Path,
newList, oldList TList,
matches MatchItemFn[TItem],
equiv MatchFunc[TItem],
itemValidator func(ctx context.Context, op operation.Operation, fldPath *field.Path, newObj, oldObj *TItem) field.ErrorList,
) field.ErrorList {
var matchedNew, matchedOld *TItem
var newIndex int
for i := range newList {
if matches(&newList[i]) {
matchedNew = &newList[i]
newIndex = i
break
}
}
if matchedNew == nil {
return nil
}
for i := range oldList {
if matches(&oldList[i]) {
matchedOld = &oldList[i]
break
}
}
if op.Type == operation.Update && matchedOld != nil && equiv(*matchedNew, *matchedOld) {
return nil
}
return itemValidator(ctx, op, fldPath.Index(newIndex), matchedNew, matchedOld)
}

View File

@ -0,0 +1,167 @@
/*
Copyright 2025 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 validate
import (
"context"
"reflect"
"testing"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
type multiKeyItem struct {
K1 string `json:"k1"`
K2 string `json:"k2"`
V int `json:"v"`
}
func TestSliceItem(t *testing.T) {
testCases := []struct {
name string
new []multiKeyItem
old []multiKeyItem
match MatchItemFn[multiKeyItem]
validator func(context.Context, operation.Operation, *field.Path, *multiKeyItem, *multiKeyItem) field.ErrorList
expected field.ErrorList
}{
{
name: "no match",
new: []multiKeyItem{
{K1: "a", K2: "1", V: 1},
},
match: func(i *multiKeyItem) bool {
return i.K1 == "target"
},
validator: func(_ context.Context, _ operation.Operation, fp *field.Path, _, _ *multiKeyItem) field.ErrorList {
return field.ErrorList{field.Invalid(fp, nil, "err")}
},
expected: nil,
},
{
name: "new item with matching keys",
new: []multiKeyItem{
{K1: "a", K2: "1", V: 1},
{K1: "target", K2: "target2", V: 2},
},
match: func(i *multiKeyItem) bool {
return i.K1 == "target" && i.K2 == "target2"
},
validator: func(_ context.Context, _ operation.Operation, fp *field.Path, n, o *multiKeyItem) field.ErrorList {
if n != nil && o == nil {
return field.ErrorList{field.Invalid(fp, n.K1, "added")}
}
return nil
},
expected: field.ErrorList{field.Invalid(field.NewPath("").Index(1), "target", "added")},
},
{
name: "updated item - same keys different values",
new: []multiKeyItem{
{K1: "a", K2: "1", V: 1},
{K1: "update", K2: "target2", V: 20},
},
old: []multiKeyItem{
{K1: "a", K2: "1", V: 1},
{K1: "update", K2: "target2", V: 2},
},
match: func(i *multiKeyItem) bool {
return i.K1 == "update" && i.K2 == "target2"
},
validator: func(_ context.Context, _ operation.Operation, fp *field.Path, n, o *multiKeyItem) field.ErrorList {
if n != nil && o != nil && n.V != o.V {
return field.ErrorList{field.Invalid(fp.Child("v"), n.V, "changed")}
}
return nil
},
expected: field.ErrorList{field.Invalid(field.NewPath("").Index(1).Child("v"), 20, "changed")},
},
{
// For completeness as listType=map && listKey=... required tags prevents dupes.
name: "first match only - multiple items with same keys",
new: []multiKeyItem{
{K1: "dup", K2: "target2", V: 1},
{K1: "dup", K2: "target2", V: 2},
},
old: []multiKeyItem{
{K1: "dup", K2: "target2", V: 10},
},
match: func(i *multiKeyItem) bool {
return i.K1 == "dup" && i.K2 == "target2"
},
validator: func(_ context.Context, _ operation.Operation, fp *field.Path, n, o *multiKeyItem) field.ErrorList {
if n != nil && o != nil {
return field.ErrorList{field.Invalid(fp, n.V, "value")}
}
return nil
},
expected: field.ErrorList{field.Invalid(field.NewPath("").Index(0), 1, "value")},
},
{
name: "nil new list",
new: nil,
old: []multiKeyItem{
{K1: "exists", K2: "target2", V: 1},
},
match: func(i *multiKeyItem) bool {
return i.K1 == "exists" && i.K2 == "target2"
},
validator: func(_ context.Context, _ operation.Operation, fp *field.Path, n, o *multiKeyItem) field.ErrorList {
if n == nil && o != nil {
return field.ErrorList{field.Invalid(fp, nil, "deleted")}
}
return nil
},
expected: nil,
},
{
name: "empty lists",
new: []multiKeyItem{},
old: []multiKeyItem{},
match: func(i *multiKeyItem) bool { return true },
validator: func(_ context.Context, _ operation.Operation, fp *field.Path, _, _ *multiKeyItem) field.ErrorList {
return field.ErrorList{field.Invalid(fp, nil, "err")}
},
expected: nil,
},
{
name: "nil lists",
new: nil,
old: nil,
match: func(i *multiKeyItem) bool { return true },
validator: func(_ context.Context, _ operation.Operation, fp *field.Path, _, _ *multiKeyItem) field.ErrorList {
return field.ErrorList{field.Invalid(fp, nil, "err")}
},
expected: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
op := operation.Operation{Type: operation.Update}
fp := field.NewPath("")
got := SliceItem(ctx, op, fp, tc.new, tc.old, tc.match, SemanticDeepEqual, tc.validator)
if !reflect.DeepEqual(got, tc.expected) {
t.Errorf("got %v want %v", got, tc.expected)
}
})
}
}

View File

@ -0,0 +1,37 @@
/*
Copyright 2024 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 validate
import (
"context"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/api/validate/constraints"
"k8s.io/apimachinery/pkg/api/validate/content"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// Minimum verifies that the specified value is greater than or equal to min.
func Minimum[T constraints.Integer](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T, min T) field.ErrorList {
if value == nil {
return nil
}
if *value < min {
return field.ErrorList{field.Invalid(fldPath, *value, content.MinError(min)).WithOrigin("minimum")}
}
return nil
}

View File

@ -0,0 +1,111 @@
/*
Copyright 2024 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 validate
import (
"context"
"regexp"
"testing"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/api/validate/constraints"
"k8s.io/apimachinery/pkg/util/validation/field"
)
func TestMinimum(t *testing.T) {
testMinimumPositive[int](t)
testMinimumNegative[int](t)
testMinimumPositive[int8](t)
testMinimumNegative[int8](t)
testMinimumPositive[int16](t)
testMinimumNegative[int16](t)
testMinimumPositive[int32](t)
testMinimumNegative[int32](t)
testMinimumPositive[int64](t)
testMinimumNegative[int64](t)
testMinimumPositive[uint](t)
testMinimumPositive[uint8](t)
testMinimumPositive[uint16](t)
testMinimumPositive[uint32](t)
testMinimumPositive[uint64](t)
}
type minimumTestCase[T constraints.Integer] struct {
value T
min T
err string // regex
}
func testMinimumPositive[T constraints.Integer](t *testing.T) {
t.Helper()
cases := []minimumTestCase[T]{{
value: 0,
min: 0,
}, {
value: 0,
min: 1,
err: "fldpath: Invalid value.*must be greater than or equal to",
}, {
value: 1,
min: 1,
}, {
value: 1,
min: 2,
err: "fldpath: Invalid value.*must be greater than or equal to",
}}
doTestMinimum[T](t, cases)
}
func testMinimumNegative[T constraints.Signed](t *testing.T) {
t.Helper()
cases := []minimumTestCase[T]{{
value: -1,
min: -1,
}, {
value: -2,
min: -1,
err: "fldpath: Invalid value.*must be greater than or equal to",
}}
doTestMinimum[T](t, cases)
}
func doTestMinimum[T constraints.Integer](t *testing.T, cases []minimumTestCase[T]) {
t.Helper()
for i, tc := range cases {
v := tc.value
result := Minimum(context.Background(), operation.Operation{}, field.NewPath("fldpath"), &v, nil, tc.min)
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}

View File

@ -0,0 +1,133 @@
/*
Copyright 2024 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 validate
import (
"context"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// RequiredValue verifies that the specified value is not the zero-value for
// its type.
func RequiredValue[T comparable](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
var zero T
if *value != zero {
return nil
}
return field.ErrorList{field.Required(fldPath, "")}
}
// RequiredPointer verifies that the specified pointer is not nil.
func RequiredPointer[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
if value != nil {
return nil
}
return field.ErrorList{field.Required(fldPath, "")}
}
// RequiredSlice verifies that the specified slice is not empty.
func RequiredSlice[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []T) field.ErrorList {
if len(value) > 0 {
return nil
}
return field.ErrorList{field.Required(fldPath, "")}
}
// RequiredMap verifies that the specified map is not empty.
func RequiredMap[K comparable, T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[K]T) field.ErrorList {
if len(value) > 0 {
return nil
}
return field.ErrorList{field.Required(fldPath, "")}
}
// ForbiddenValue verifies that the specified value is the zero-value for its
// type.
func ForbiddenValue[T comparable](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
var zero T
if *value == zero {
return nil
}
return field.ErrorList{field.Forbidden(fldPath, "")}
}
// ForbiddenPointer verifies that the specified pointer is nil.
func ForbiddenPointer[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
if value == nil {
return nil
}
return field.ErrorList{field.Forbidden(fldPath, "")}
}
// ForbiddenSlice verifies that the specified slice is empty.
func ForbiddenSlice[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []T) field.ErrorList {
if len(value) == 0 {
return nil
}
return field.ErrorList{field.Forbidden(fldPath, "")}
}
// ForbiddenMap verifies that the specified map is empty.
func ForbiddenMap[K comparable, T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[K]T) field.ErrorList {
if len(value) == 0 {
return nil
}
return field.ErrorList{field.Forbidden(fldPath, "")}
}
// OptionalValue verifies that the specified value is not the zero-value for
// its type. This is identical to RequiredValue, but the caller should treat an
// error here as an indication that the optional value was not specified.
func OptionalValue[T comparable](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
var zero T
if *value != zero {
return nil
}
return field.ErrorList{field.Required(fldPath, "optional value was not specified")}
}
// OptionalPointer verifies that the specified pointer is not nil. This is
// identical to RequiredPointer, but the caller should treat an error here as an
// indication that the optional value was not specified.
func OptionalPointer[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
if value != nil {
return nil
}
return field.ErrorList{field.Required(fldPath, "optional value was not specified")}
}
// OptionalSlice verifies that the specified slice is not empty. This is
// identical to RequiredSlice, but the caller should treat an error here as an
// indication that the optional value was not specified.
func OptionalSlice[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []T) field.ErrorList {
if len(value) > 0 {
return nil
}
return field.ErrorList{field.Required(fldPath, "optional value was not specified")}
}
// OptionalMap verifies that the specified map is not empty. This is identical
// to RequiredMap, but the caller should treat an error here as an indication that
// the optional value was not specified.
func OptionalMap[K comparable, T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[K]T) field.ErrorList {
if len(value) > 0 {
return nil
}
return field.ErrorList{field.Required(fldPath, "optional value was not specified")}
}

View File

@ -0,0 +1,924 @@
/*
Copyright 2024 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 validate
import (
"context"
"regexp"
"testing"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/utils/ptr"
)
func TestRequiredValue(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := "value"
return RequiredValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := "" // zero-value
return RequiredValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := 123
return RequiredValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := 0 // zero-value
return RequiredValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := true
return RequiredValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := false // zero-value
return RequiredValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := struct{ S string }{"value"}
return RequiredValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := struct{ S string }{} // zero-value
return RequiredValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := ptr.To("")
return RequiredValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := (*string)(nil) // zero-value
return RequiredValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Required value",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}
func TestRequiredPointer(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := ""
return RequiredPointer(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*string)(nil)
return RequiredPointer(context.Background(), op, fp, pointer, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := 0
return RequiredPointer(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*int)(nil)
return RequiredPointer(context.Background(), op, fp, pointer, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := false
return RequiredPointer(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*bool)(nil)
return RequiredPointer(context.Background(), op, fp, pointer, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := struct{ S string }{}
return RequiredPointer(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*struct{ S string })(nil)
return RequiredPointer(context.Background(), op, fp, pointer, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := (*string)(nil)
return RequiredPointer(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (**string)(nil)
return RequiredPointer(context.Background(), op, fp, pointer, nil)
},
err: "fldpath: Required value",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}
func TestRequiredSlice(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []string{""}
return RequiredSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []string{}
return RequiredSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []int{0}
return RequiredSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []int{}
return RequiredSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []bool{false}
return RequiredSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []bool{}
return RequiredSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []*string{nil}
return RequiredSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []*string{}
return RequiredSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath: Required value",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}
func TestRequiredMap(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[string]string{"": ""}
return RequiredMap(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[string]string{}
return RequiredMap(context.Background(), op, fp, value, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[int]int{0: 0}
return RequiredMap(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[int]int{}
return RequiredMap(context.Background(), op, fp, value, nil)
},
err: "fldpath: Required value",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[bool]bool{false: false}
return RequiredMap(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[string]bool{}
return RequiredMap(context.Background(), op, fp, value, nil)
},
err: "fldpath: Required value",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}
func TestOptionalValue(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := "value"
return OptionalValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := "" // zero-value
return OptionalValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := 123
return OptionalValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := 0 // zero-value
return OptionalValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := true
return OptionalValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := false // zero-value
return OptionalValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := struct{ S string }{"value"}
return OptionalValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := struct{ S string }{} // zero-value
return OptionalValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := ptr.To("")
return OptionalValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := (*string)(nil) // zero-value
return OptionalValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath:.*optional value was not specified",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}
func TestOptionalPointer(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := ""
return OptionalPointer(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*string)(nil)
return OptionalPointer(context.Background(), op, fp, pointer, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := 0
return OptionalPointer(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*int)(nil)
return OptionalPointer(context.Background(), op, fp, pointer, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := false
return OptionalPointer(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*bool)(nil)
return OptionalPointer(context.Background(), op, fp, pointer, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := struct{ S string }{}
return OptionalPointer(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*struct{ S string })(nil)
return OptionalPointer(context.Background(), op, fp, pointer, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := (*string)(nil)
return OptionalPointer(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (**string)(nil)
return OptionalPointer(context.Background(), op, fp, pointer, nil)
},
err: "fldpath:.*optional value was not specified",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}
func TestOptionalSlice(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []string{""}
return OptionalSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []string{}
return OptionalSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []int{0}
return OptionalSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []int{}
return OptionalSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []bool{false}
return OptionalSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []bool{}
return OptionalSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []*string{nil}
return OptionalSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []*string{}
return OptionalSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath:.*optional value was not specified",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}
func TestOptionalMap(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[string]string{"": ""}
return OptionalMap(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[string]string{}
return OptionalMap(context.Background(), op, fp, value, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[int]int{0: 0}
return OptionalMap(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[int]int{}
return OptionalMap(context.Background(), op, fp, value, nil)
},
err: "fldpath:.*optional value was not specified",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[bool]bool{false: false}
return OptionalMap(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[string]bool{}
return OptionalMap(context.Background(), op, fp, value, nil)
},
err: "fldpath:.*optional value was not specified",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}
func TestForbiddenValue(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := ""
return ForbiddenValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := "value"
return ForbiddenValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := 0
return ForbiddenValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := 123
return ForbiddenValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := false
return ForbiddenValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := true
return ForbiddenValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := struct{ S string }{}
return ForbiddenValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := struct{ S string }{"value"}
return ForbiddenValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := (*string)(nil)
return ForbiddenValue(context.Background(), op, fp, &value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := ptr.To("")
return ForbiddenValue(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Forbidden",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}
func TestForbiddenPointer(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*string)(nil)
return ForbiddenPointer(context.Background(), op, fp, pointer, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := ""
return ForbiddenPointer(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*int)(nil)
return ForbiddenPointer(context.Background(), op, fp, pointer, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := 0
return ForbiddenPointer(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*bool)(nil)
return ForbiddenPointer(context.Background(), op, fp, pointer, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := false
return ForbiddenPointer(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (*struct{ S string })(nil)
return ForbiddenPointer(context.Background(), op, fp, pointer, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := struct{ S string }{}
return ForbiddenPointer(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
pointer := (**string)(nil)
return ForbiddenPointer(context.Background(), op, fp, pointer, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := (*string)(nil)
return ForbiddenPointer(context.Background(), op, fp, &value, nil)
},
err: "fldpath: Forbidden",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}
func TestForbiddenSlice(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []string{}
return ForbiddenSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []string{""}
return ForbiddenSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []int{}
return ForbiddenSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []int{0}
return ForbiddenSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []bool{}
return ForbiddenSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []bool{false}
return ForbiddenSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []*string{}
return ForbiddenSlice(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := []*string{nil}
return ForbiddenSlice(context.Background(), op, fp, value, nil)
},
err: "fldpath: Forbidden",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}
func TestForbiddenMap(t *testing.T) {
cases := []struct {
fn func(op operation.Operation, fp *field.Path) field.ErrorList
err string // regex
}{{
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[string]string{}
return ForbiddenMap(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[string]string{"": ""}
return ForbiddenMap(context.Background(), op, fp, value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[int]int{}
return ForbiddenMap(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[int]int{0: 0}
return ForbiddenMap(context.Background(), op, fp, value, nil)
},
err: "fldpath: Forbidden",
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[string]bool{}
return ForbiddenMap(context.Background(), op, fp, value, nil)
},
}, {
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
value := map[bool]bool{false: false}
return ForbiddenMap(context.Background(), op, fp, value, nil)
},
err: "fldpath: Forbidden",
}}
for i, tc := range cases {
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
if len(result) > 0 && tc.err == "" {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && tc.err != "" {
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
}
}
}
}

View File

@ -0,0 +1,46 @@
/*
Copyright 2025 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 validate
import (
"context"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// GetFieldFunc is a function that extracts a field from a type and returns a
// nilable value.
type GetFieldFunc[Tstruct any, Tfield any] func(*Tstruct) Tfield
// Subfield validates a subfield of a struct against a validator function.
func Subfield[Tstruct any, Tfield any](ctx context.Context, op operation.Operation, fldPath *field.Path, newStruct, oldStruct *Tstruct,
fldName string, getField GetFieldFunc[Tstruct, Tfield], validator ValidateFunc[Tfield]) field.ErrorList {
var errs field.ErrorList
newVal := getField(newStruct)
var oldVal Tfield
if oldStruct != nil {
oldVal = getField(oldStruct)
}
// TODO: passing an equiv function to Subfield for direct comparison instead of
// SemanticDeepEqual if fields can be compared directly, to improve performance.
if op.Type == operation.Update && SemanticDeepEqual(newVal, oldVal) {
return nil
}
errs = append(errs, validator(ctx, op, fldPath.Child(fldName), newVal, oldVal)...)
return errs
}

View File

@ -0,0 +1,35 @@
/*
Copyright 2014 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 validate
import (
"context"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// FixedResult asserts a fixed boolean result. This is mostly useful for
// testing.
func FixedResult[T any](_ context.Context, op operation.Operation, fldPath *field.Path, value, _ T, result bool, arg string) field.ErrorList {
if result {
return nil
}
return field.ErrorList{
field.Invalid(fldPath, value, "forced failure: "+arg).WithOrigin("validateFalse"),
}
}

View File

@ -0,0 +1,146 @@
/*
Copyright 2024 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 validate
import (
"context"
"testing"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/utils/ptr"
)
func TestFixedResult(t *testing.T) {
cases := []struct {
value any
pass bool
}{{
value: "",
pass: false,
}, {
value: "",
pass: true,
}, {
value: "nonempty",
pass: false,
}, {
value: "nonempty",
pass: true,
}, {
value: 0,
pass: false,
}, {
value: 0,
pass: true,
}, {
value: 1,
pass: false,
}, {
value: 1,
pass: true,
}, {
value: false,
pass: false,
}, {
value: false,
pass: true,
}, {
value: true,
pass: false,
}, {
value: true,
pass: true,
}, {
value: nil,
pass: false,
}, {
value: nil,
pass: true,
}, {
value: ptr.To(""),
pass: false,
}, {
value: ptr.To(""),
pass: true,
}, {
value: ptr.To("nonempty"),
pass: false,
}, {
value: ptr.To("nonempty"),
pass: true,
}, {
value: []string(nil),
pass: false,
}, {
value: []string(nil),
pass: true,
}, {
value: []string{},
pass: false,
}, {
value: []string{},
pass: true,
}, {
value: []string{"s"},
pass: false,
}, {
value: []string{"s"},
pass: true,
}, {
value: map[string]string(nil),
pass: false,
}, {
value: map[string]string(nil),
pass: true,
}, {
value: map[string]string{},
pass: false,
}, {
value: map[string]string{},
pass: true,
}, {
value: map[string]string{"k": "v"},
pass: false,
}, {
value: map[string]string{"k": "v"},
pass: true,
}}
matcher := field.ErrorMatcher{}.ByOrigin().ByDetailExact()
for i, tc := range cases {
result := FixedResult(context.Background(), operation.Operation{}, field.NewPath("fldpath"), tc.value, nil, tc.pass, "detail string")
if len(result) != 0 && tc.pass {
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
continue
}
if len(result) == 0 && !tc.pass {
t.Errorf("case %d: unexpected success", i)
continue
}
if len(result) > 0 {
if len(result) > 1 {
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
continue
}
wantErrorList := field.ErrorList{
field.Invalid(field.NewPath("fldpath"), tc.value, "forced failure: detail string").WithOrigin("validateFalse"),
}
matcher.Test(t, wantErrorList, result)
}
}
}

212
pkg/api/validate/union.go Normal file
View File

@ -0,0 +1,212 @@
/*
Copyright 2024 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 validate
import (
"context"
"fmt"
"strings"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// ExtractorFn extracts a value from a parent object. Depending on the context,
// that could be the value of a field or just whether that field was set or
// not.
// Note: obj is not guaranteed to be non-nil, need to handle nil obj in the
// extractor.
type ExtractorFn[T, V any] func(obj T) V
// UnionValidationOptions configures how union validation behaves
type UnionValidationOptions struct {
// ErrorForEmpty returns error when no fields are set (nil means no error)
ErrorForEmpty func(fldPath *field.Path, allFields []string) *field.Error
// ErrorForMultiple returns error when multiple fields are set (nil means no error)
ErrorForMultiple func(fldPath *field.Path, specifiedFields []string, allFields []string) *field.Error
}
// Union verifies that exactly one member of a union is specified.
//
// UnionMembership must define all the members of the union.
//
// For example:
//
// var UnionMembershipForABC := validate.NewUnionMembership([2]string{"a", "A"}, [2]string{"b", "B"}, [2]string{"c", "C"})
// func ValidateABC(ctx context.Context, op operation.Operation, fldPath *field.Path, in *ABC) (errs fields.ErrorList) {
// errs = append(errs, Union(ctx, op, fldPath, in, oldIn, UnionMembershipForABC,
// func(in *ABC) bool { return in.A != nil },
// func(in *ABC) bool { return in.B != ""},
// func(in *ABC) bool { return in.C != 0 },
// )...)
// return errs
// }
func Union[T any](_ context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj T, union *UnionMembership, isSetFns ...ExtractorFn[T, bool]) field.ErrorList {
options := UnionValidationOptions{
ErrorForEmpty: func(fldPath *field.Path, allFields []string) *field.Error {
return field.Invalid(fldPath, "",
fmt.Sprintf("must specify one of: %s", strings.Join(allFields, ", ")))
},
ErrorForMultiple: func(fldPath *field.Path, specifiedFields []string, allFields []string) *field.Error {
return field.Invalid(fldPath, fmt.Sprintf("{%s}", strings.Join(specifiedFields, ", ")),
fmt.Sprintf("must specify exactly one of: %s", strings.Join(allFields, ", ")))
},
}
return unionValidate(op, fldPath, obj, oldObj, union, options, isSetFns...)
}
// DiscriminatedUnion verifies specified union member matches the discriminator.
//
// UnionMembership must define all the members of the union and the discriminator.
//
// For example:
//
// var UnionMembershipForABC = validate.NewDiscriminatedUnionMembership("type", [2]string{"a", "A"}, [2]string{"b", "B"}, [2]string{"c", "C"})
// func ValidateABC(ctx context.Context, op operation.Operation, fldPath *field.Path, in *ABC) (errs field.ErrorList) {
// errs = append(errs, DiscriminatedUnion(ctx, op, fldPath, in, oldIn, UnionMembershipForABC,
// func(in *ABC) string { return string(in.Type) },
// func(in *ABC) bool { return in.A != nil },
// func(in *ABC) bool { return in.B != ""},
// func(in *ABC) bool { return in.C != 0 },
// )...)
// return errs
// }
//
// It is not an error for the discriminatorValue to be unknown. That must be
// validated on its own.
func DiscriminatedUnion[T any, D ~string](_ context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj T, union *UnionMembership, discriminatorExtractor ExtractorFn[T, D], isSetFns ...ExtractorFn[T, bool]) (errs field.ErrorList) {
if len(union.members) != len(isSetFns) {
return field.ErrorList{
field.InternalError(fldPath,
fmt.Errorf("number of extractors (%d) does not match number of union members (%d)",
len(isSetFns), len(union.members))),
}
}
var changed bool
discriminatorValue := discriminatorExtractor(obj)
if op.Type == operation.Update {
oldDiscriminatorValue := discriminatorExtractor(oldObj)
changed = discriminatorValue != oldDiscriminatorValue
}
for i, fieldIsSet := range isSetFns {
member := union.members[i]
isDiscriminatedMember := string(discriminatorValue) == member.discriminatorValue
newIsSet := fieldIsSet(obj)
if op.Type == operation.Update && !changed {
oldIsSet := fieldIsSet(oldObj)
changed = changed || newIsSet != oldIsSet
}
if newIsSet && !isDiscriminatedMember {
errs = append(errs, field.Invalid(fldPath.Child(member.fieldName), "",
fmt.Sprintf("may only be specified when `%s` is %q", union.discriminatorName, member.discriminatorValue)))
} else if !newIsSet && isDiscriminatedMember {
errs = append(errs, field.Invalid(fldPath.Child(member.fieldName), "",
fmt.Sprintf("must be specified when `%s` is %q", union.discriminatorName, discriminatorValue)))
}
}
// If the union discriminator and membership is unchanged, we don't need to
// re-validate.
if op.Type == operation.Update && !changed {
return nil
}
return errs
}
type member struct {
fieldName, discriminatorValue string
}
// UnionMembership represents an ordered list of field union memberships.
type UnionMembership struct {
discriminatorName string
members []member
}
// NewUnionMembership returns a new UnionMembership for the given list of members.
//
// Each member is a [2]string to provide a fieldName and discriminatorValue pair, where
// [0] identifies the field name and [1] identifies the union member Name.
//
// Field names must be unique.
func NewUnionMembership(member ...[2]string) *UnionMembership {
return NewDiscriminatedUnionMembership("", member...)
}
// NewDiscriminatedUnionMembership returns a new UnionMembership for the given discriminator field and list of members.
// members are provided in the same way as for NewUnionMembership.
func NewDiscriminatedUnionMembership(discriminatorFieldName string, members ...[2]string) *UnionMembership {
u := &UnionMembership{}
u.discriminatorName = discriminatorFieldName
for _, fieldName := range members {
u.members = append(u.members, member{fieldName: fieldName[0], discriminatorValue: fieldName[1]})
}
return u
}
// allFields returns a string listing all the field names of the member of a union for use in error reporting.
func (u UnionMembership) allFields() []string {
memberNames := make([]string, 0, len(u.members))
for _, f := range u.members {
memberNames = append(memberNames, fmt.Sprintf("`%s`", f.fieldName))
}
return memberNames
}
func unionValidate[T any](op operation.Operation, fldPath *field.Path,
obj, oldObj T, union *UnionMembership, options UnionValidationOptions, isSetFns ...ExtractorFn[T, bool],
) field.ErrorList {
if len(union.members) != len(isSetFns) {
return field.ErrorList{
field.InternalError(fldPath,
fmt.Errorf("number of extractors (%d) does not match number of union members (%d)",
len(isSetFns), len(union.members))),
}
}
var specifiedFields []string
var changed bool
for i, fieldIsSet := range isSetFns {
newIsSet := fieldIsSet(obj)
if op.Type == operation.Update && !changed {
oldIsSet := fieldIsSet(oldObj)
changed = changed || newIsSet != oldIsSet
}
if newIsSet {
specifiedFields = append(specifiedFields, union.members[i].fieldName)
}
}
// If the union membership is unchanged, we don't need to re-validate.
if op.Type == operation.Update && !changed {
return nil
}
var errs field.ErrorList
if len(specifiedFields) > 1 && options.ErrorForMultiple != nil {
errs = append(errs, options.ErrorForMultiple(fldPath, specifiedFields, union.allFields()))
}
if len(specifiedFields) == 0 && options.ErrorForEmpty != nil {
errs = append(errs, options.ErrorForEmpty(fldPath, union.allFields()))
}
return errs
}

View File

@ -0,0 +1,328 @@
/*
Copyright 2024 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 validate
import (
"context"
"reflect"
"testing"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
type testMember struct{}
func TestUnion(t *testing.T) {
testCases := []struct {
name string
fields [][2]string
fieldValues []bool
expected field.ErrorList
}{
{
name: "one member set",
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
fieldValues: []bool{false, false, false, true},
expected: nil,
},
{
name: "two members set",
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
fieldValues: []bool{false, true, false, true},
expected: field.ErrorList{field.Invalid(nil, "{b, d}", "must specify exactly one of: `a`, `b`, `c`, `d`")},
},
{
name: "all members set",
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
fieldValues: []bool{true, true, true, true},
expected: field.ErrorList{field.Invalid(nil, "{a, b, c, d}", "must specify exactly one of: `a`, `b`, `c`, `d`")},
},
{
name: "no member set",
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
fieldValues: []bool{false, false, false, false},
expected: field.ErrorList{field.Invalid(nil, "", "must specify one of: `a`, `b`, `c`, `d`")},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create mock extractors that return predefined values instead of
// actually extracting from the object.
extractors := make([]ExtractorFn[*testMember, bool], len(tc.fieldValues))
for i, val := range tc.fieldValues {
extractors[i] = func(_ *testMember) bool { return val }
}
got := Union(context.Background(), operation.Operation{}, nil, &testMember{}, nil, NewUnionMembership(tc.fields...), extractors...)
if !reflect.DeepEqual(got, tc.expected) {
t.Errorf("got %v want %v", got, tc.expected)
}
})
}
}
func TestDiscriminatedUnion(t *testing.T) {
testCases := []struct {
name string
discriminatorField string
fields [][2]string
discriminatorValue string
fieldValues []bool
expected field.ErrorList
}{
{
name: "valid discriminated union A",
discriminatorField: "d",
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
discriminatorValue: "A",
fieldValues: []bool{true, false, false, false},
},
{
name: "valid discriminated union C",
discriminatorField: "d",
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
discriminatorValue: "C",
fieldValues: []bool{false, false, true, false},
},
{
name: "invalid, discriminator not set to member that is specified",
discriminatorField: "type",
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
discriminatorValue: "C",
fieldValues: []bool{false, true, false, false},
expected: field.ErrorList{
field.Invalid(field.NewPath("b"), "", "may only be specified when `type` is \"B\""),
field.Invalid(field.NewPath("c"), "", "must be specified when `type` is \"C\""),
},
},
{
name: "invalid, discriminator correct, multiple members set",
discriminatorField: "type",
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
discriminatorValue: "C",
fieldValues: []bool{false, true, true, true},
expected: field.ErrorList{
field.Invalid(field.NewPath("b"), "", "may only be specified when `type` is \"B\""),
field.Invalid(field.NewPath("d"), "", "may only be specified when `type` is \"D\""),
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
discriminatorExtractor := func(_ *testMember) string { return tc.discriminatorValue }
// Create mock extractors that return predefined values instead of
// actually extracting from the object.
extractors := make([]ExtractorFn[*testMember, bool], len(tc.fieldValues))
for i, val := range tc.fieldValues {
extractors[i] = func(_ *testMember) bool { return val }
}
got := DiscriminatedUnion(context.Background(), operation.Operation{}, nil, &testMember{}, nil, NewDiscriminatedUnionMembership(tc.discriminatorField, tc.fields...), discriminatorExtractor, extractors...)
if !reflect.DeepEqual(got, tc.expected) {
t.Errorf("got %v want %v", got.ToAggregate(), tc.expected.ToAggregate())
}
})
}
}
type testStruct struct {
M1 *m1 `json:"m1"`
M2 *m2 `json:"m2"`
}
type m1 struct{}
type m2 struct{}
var extractors = []ExtractorFn[*testStruct, bool]{
func(s *testStruct) bool {
if s == nil {
return false
}
return s.M1 != nil
},
func(s *testStruct) bool {
if s == nil {
return false
}
return s.M2 != nil
},
}
func TestUnionRatcheting(t *testing.T) {
testCases := []struct {
name string
oldStruct *testStruct
newStruct *testStruct
expected field.ErrorList
}{
{
name: "both nil",
oldStruct: nil,
newStruct: nil,
},
{
name: "both empty struct",
oldStruct: &testStruct{},
newStruct: &testStruct{},
},
{
name: "both have more than one member",
oldStruct: &testStruct{
M1: &m1{},
M2: &m2{},
},
newStruct: &testStruct{
M1: &m1{},
M2: &m2{},
},
},
{
name: "change to invalid",
oldStruct: &testStruct{
M1: &m1{},
},
newStruct: &testStruct{
M1: &m1{},
M2: &m2{},
},
expected: field.ErrorList{
field.Invalid(nil, "{m1, m2}", "must specify exactly one of: `m1`, `m2`"),
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := Union(context.Background(), operation.Operation{Type: operation.Update}, nil, tc.newStruct, tc.oldStruct, NewUnionMembership([][2]string{{"m1", "m1"}, {"m2", "m2"}}...), extractors...)
if !reflect.DeepEqual(got, tc.expected) {
t.Errorf("got %v want %v", got, tc.expected)
}
})
}
}
type testDiscriminatedStruct struct {
D string `json:"d"`
M1 *m1 `json:"m1"`
M2 *m2 `json:"m2"`
}
var testDiscriminatorExtractor = func(s *testDiscriminatedStruct) string {
if s != nil {
return s.D
}
return ""
}
var testDiscriminatedExtractors = []ExtractorFn[*testDiscriminatedStruct, bool]{
func(s *testDiscriminatedStruct) bool {
if s == nil {
return false
}
return s.M1 != nil
},
func(s *testDiscriminatedStruct) bool {
if s == nil {
return false
}
return s.M2 != nil
},
}
func TestDiscriminatedUnionRatcheting(t *testing.T) {
testCases := []struct {
name string
oldStruct *testDiscriminatedStruct
newStruct *testDiscriminatedStruct
expected field.ErrorList
}{
{
name: "pass with both nil",
},
{
name: "pass with both empty struct",
oldStruct: &testDiscriminatedStruct{},
newStruct: &testDiscriminatedStruct{},
},
{
name: "pass with both not set to member that is specified",
oldStruct: &testDiscriminatedStruct{
D: "m1",
M2: &m2{},
},
newStruct: &testDiscriminatedStruct{
D: "m1",
M2: &m2{},
},
},
{
name: "pass with both set to more than one member",
oldStruct: &testDiscriminatedStruct{
D: "m1",
M1: &m1{},
M2: &m2{},
},
newStruct: &testDiscriminatedStruct{
D: "m1",
M1: &m1{},
M2: &m2{},
},
},
{
name: "fail on changing to invalid with both set",
oldStruct: &testDiscriminatedStruct{
D: "m1",
M1: &m1{},
},
newStruct: &testDiscriminatedStruct{
D: "m1",
M1: &m1{},
M2: &m2{},
},
expected: field.ErrorList{
field.Invalid(field.NewPath("m2"), "", "may only be specified when `d` is \"m2\""),
},
},
{
name: "fail on changing the discriminator",
oldStruct: &testDiscriminatedStruct{
D: "m1",
M1: &m1{},
},
newStruct: &testDiscriminatedStruct{
D: "m2",
M1: &m1{},
},
expected: field.ErrorList{
field.Invalid(field.NewPath("m1"), "", "may only be specified when `d` is \"m1\""),
field.Invalid(field.NewPath("m2"), "", "must be specified when `d` is \"m2\""),
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := DiscriminatedUnion(context.Background(), operation.Operation{Type: operation.Update}, nil, tc.newStruct, tc.oldStruct, NewDiscriminatedUnionMembership("d", [][2]string{{"m1", "m1"}, {"m2", "m2"}}...), testDiscriminatorExtractor, testDiscriminatedExtractors...)
if !reflect.DeepEqual(got, tc.expected) {
t.Errorf("got %v want %v", got, tc.expected)
}
})
}
}

View File

@ -0,0 +1,41 @@
/*
Copyright 2024 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 validate
import (
"bytes"
"strconv"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// fmtErrs is a helper for nicer test output. It will use multiple lines if
// errs has more than 1 item.
func fmtErrs(errs field.ErrorList) string {
if len(errs) == 0 {
return "<no errors>"
}
if len(errs) == 1 {
return strconv.Quote(errs[0].Error())
}
buf := bytes.Buffer{}
for _, e := range errs {
buf.WriteString("\n")
buf.WriteString(strconv.Quote(e.Error()))
}
return buf.String()
}

View File

@ -0,0 +1,54 @@
/*
Copyright 2024 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 validate
import (
"context"
"fmt"
"strings"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// ZeroOrOneOfUnion verifies that at most one member of a union is specified.
//
// ZeroOrOneOfMembership must define all the members of the union.
//
// For example:
//
// var ZeroOrOneOfMembershipForABC = validate.NewUnionMembership([2]string{"a", "A"}, [2]string{"b", "B"}, [2]string{"c", "C"})
// func ValidateABC(ctx context.Context, op operation.Operation, fldPath *field.Path, in *ABC) (errs field.ErrorList) {
// errs = append(errs, ZeroOrOneOfUnion(ctx, op, fldPath, in, oldIn, UnionMembershipForABC,
// func(in *ABC) bool { return in.A != nil },
// func(in *ABC) bool { return in.B != ""},
// func(in *ABC) bool { return in.C != 0 },
// )...)
// return errs
// }
func ZeroOrOneOfUnion[T any](_ context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj T, union *UnionMembership, isSetFns ...ExtractorFn[T, bool]) field.ErrorList {
options := UnionValidationOptions{
ErrorForEmpty: nil,
ErrorForMultiple: func(fldPath *field.Path, specifiedFields []string, allFields []string) *field.Error {
return field.Invalid(fldPath, fmt.Sprintf("{%s}", strings.Join(specifiedFields, ", ")),
fmt.Sprintf("must specify at most one of: %s", strings.Join(allFields, ", "))).WithOrigin("zeroOrOneOf")
},
}
errs := unionValidate(op, fldPath, obj, oldObj, union, options, isSetFns...)
return errs
}

View File

@ -0,0 +1,145 @@
/*
Copyright 2024 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 validate
import (
"context"
"reflect"
"testing"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
func TestZeroOrOneOfUnion(t *testing.T) {
testCases := []struct {
name string
fields [][2]string
fieldValues []bool
expected field.ErrorList
}{
{
name: "one member set",
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
fieldValues: []bool{false, false, false, true},
expected: nil,
},
{
name: "two members set",
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
fieldValues: []bool{false, true, false, true},
expected: field.ErrorList{field.Invalid(nil, "{b, d}", "must specify at most one of: `a`, `b`, `c`, `d`").WithOrigin("zeroOrOneOf")},
},
{
name: "all members set",
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
fieldValues: []bool{true, true, true, true},
expected: field.ErrorList{field.Invalid(nil, "{a, b, c, d}", "must specify at most one of: `a`, `b`, `c`, `d`").WithOrigin("zeroOrOneOf")},
},
{
name: "no member set - allowed for ZeroOrOneOf",
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
fieldValues: []bool{false, false, false, false},
expected: nil, // This is the key difference from Union
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create mock extractors that return predefined values instead of
// actually extracting from the object.
extractors := make([]ExtractorFn[*testMember, bool], len(tc.fieldValues))
for i, val := range tc.fieldValues {
extractors[i] = func(_ *testMember) bool { return val }
}
got := ZeroOrOneOfUnion(context.Background(), operation.Operation{}, nil, &testMember{}, nil, NewUnionMembership(tc.fields...), extractors...)
if !reflect.DeepEqual(got, tc.expected) {
t.Errorf("got %v want %v", got, tc.expected)
}
})
}
}
func TestZeroOrOneOfUnionRatcheting(t *testing.T) {
testCases := []struct {
name string
oldStruct *testStruct
newStruct *testStruct
expected field.ErrorList
}{
{
name: "both nil",
oldStruct: nil,
newStruct: nil,
},
{
name: "both empty struct - allowed for ZeroOrOneOf",
oldStruct: &testStruct{},
newStruct: &testStruct{},
},
{
name: "both have more than one member",
oldStruct: &testStruct{
M1: &m1{},
M2: &m2{},
},
newStruct: &testStruct{
M1: &m1{},
M2: &m2{},
},
},
{
name: "change to invalid",
oldStruct: &testStruct{
M1: &m1{},
},
newStruct: &testStruct{
M1: &m1{},
M2: &m2{},
},
expected: field.ErrorList{
field.Invalid(nil, "{m1, m2}", "must specify at most one of: `m1`, `m2`").WithOrigin("zeroOrOneOf"),
},
},
{
name: "change from empty to one member - allowed",
oldStruct: &testStruct{},
newStruct: &testStruct{
M1: &m1{},
},
expected: nil,
},
{
name: "change from one member to empty - allowed",
oldStruct: &testStruct{
M1: &m1{},
},
newStruct: &testStruct{},
expected: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := ZeroOrOneOfUnion(context.Background(), operation.Operation{Type: operation.Update}, nil, tc.newStruct, tc.oldStruct, NewUnionMembership([][2]string{{"m1", "m1"}, {"m2", "m2"}}...), extractors...)
if !reflect.DeepEqual(got, tc.expected) {
t.Errorf("got %v want %v", got, tc.expected)
}
})
}
}

View File

@ -82,7 +82,7 @@ func maskTrailingDash(name string) string {
func ValidateNonnegativeField(value int64, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if value < 0 {
allErrs = append(allErrs, field.Invalid(fldPath, value, IsNegativeErrorMsg))
allErrs = append(allErrs, field.Invalid(fldPath, value, IsNegativeErrorMsg).WithOrigin("minimum"))
}
return allErrs
}

View File

@ -74,13 +74,13 @@ func validateOwnerReference(ownerReference metav1.OwnerReference, fldPath *field
allErrs = append(allErrs, field.Invalid(fldPath.Child("apiVersion"), ownerReference.APIVersion, "version must not be empty"))
}
if len(gvk.Kind) == 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("kind"), ownerReference.Kind, "kind must not be empty"))
allErrs = append(allErrs, field.Invalid(fldPath.Child("kind"), ownerReference.Kind, "must not be empty"))
}
if len(ownerReference.Name) == 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("name"), ownerReference.Name, "name must not be empty"))
allErrs = append(allErrs, field.Invalid(fldPath.Child("name"), ownerReference.Name, "must not be empty"))
}
if len(ownerReference.UID) == 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("uid"), ownerReference.UID, "uid must not be empty"))
allErrs = append(allErrs, field.Invalid(fldPath.Child("uid"), ownerReference.UID, "must not be empty"))
}
if _, ok := BannedOwners[gvk]; ok {
allErrs = append(allErrs, field.Invalid(fldPath, ownerReference, fmt.Sprintf("%s is disallowed from being an owner", gvk)))

View File

@ -328,19 +328,19 @@ func TestValidateObjectMetaUpdatePreventsDeletionFieldMutation(t *testing.T) {
Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1"},
New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now},
ExpectedNew: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now},
ExpectedErrs: []string{"field.deletionTimestamp: Invalid value: 1970-01-01 00:16:40 +0000 UTC: field is immutable"},
ExpectedErrs: []string{"field.deletionTimestamp: Invalid value: \"1970-01-01T00:16:40Z\": field is immutable"},
},
"invalid clear deletionTimestamp": {
Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now},
New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1"},
ExpectedNew: metav1.ObjectMeta{Name: "test", ResourceVersion: "1"},
ExpectedErrs: []string{"field.deletionTimestamp: Invalid value: \"null\": field is immutable"},
ExpectedErrs: []string{"field.deletionTimestamp: Invalid value: null: field is immutable"},
},
"invalid change deletionTimestamp": {
Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now},
New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &later},
ExpectedNew: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &later},
ExpectedErrs: []string{"field.deletionTimestamp: Invalid value: 1970-01-01 00:33:20 +0000 UTC: field is immutable"},
ExpectedErrs: []string{"field.deletionTimestamp: Invalid value: \"1970-01-01T00:33:20Z\": field is immutable"},
},
"invalid set deletionGracePeriodSeconds": {
@ -353,7 +353,7 @@ func TestValidateObjectMetaUpdatePreventsDeletionFieldMutation(t *testing.T) {
Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionGracePeriodSeconds: &gracePeriodShort},
New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1"},
ExpectedNew: metav1.ObjectMeta{Name: "test", ResourceVersion: "1"},
ExpectedErrs: []string{"field.deletionGracePeriodSeconds: Invalid value: \"null\": field is immutable"},
ExpectedErrs: []string{"field.deletionGracePeriodSeconds: Invalid value: null: field is immutable"},
},
"invalid change deletionGracePeriodSeconds": {
Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionGracePeriodSeconds: &gracePeriodShort},
@ -373,7 +373,7 @@ func TestValidateObjectMetaUpdatePreventsDeletionFieldMutation(t *testing.T) {
}
for i := range errs {
if errs[i].Error() != tc.ExpectedErrs[i] {
t.Errorf("%s: error #%d: expected %q, got %q", k, i, tc.ExpectedErrs[i], errs[i].Error())
t.Errorf("%s: error #%d:\n expected: %q\n got: %q", k, i, tc.ExpectedErrs[i], errs[i].Error())
}
}
if !reflect.DeepEqual(tc.New, tc.ExpectedNew) {
@ -419,7 +419,7 @@ func TestObjectMetaGenerationUpdate(t *testing.T) {
}
for i := range errList {
if errList[i] != tc.ExpectedErrs[i] {
t.Errorf("%s: error #%d: expected %q, got %q", k, i, tc.ExpectedErrs[i], errList[i])
t.Errorf("%s: error #%d:\n expected: %q\n got: %q", k, i, tc.ExpectedErrs[i], errs[i].Error())
}
}
}

View File

@ -23,7 +23,7 @@ import (
"strconv"
"strings"
fuzz "github.com/google/gofuzz"
"sigs.k8s.io/randfill"
apitesting "k8s.io/apimachinery/pkg/api/apitesting"
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
@ -38,29 +38,29 @@ import (
func genericFuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
return []interface{}{
func(q *resource.Quantity, c fuzz.Continue) {
func(q *resource.Quantity, c randfill.Continue) {
*q = *resource.NewQuantity(c.Int63n(1000), resource.DecimalExponent)
},
func(j *int, c fuzz.Continue) {
func(j *int, c randfill.Continue) {
*j = int(c.Int31())
},
func(j **int, c fuzz.Continue) {
if c.RandBool() {
func(j **int, c randfill.Continue) {
if c.Bool() {
i := int(c.Int31())
*j = &i
} else {
*j = nil
}
},
func(j *runtime.TypeMeta, c fuzz.Continue) {
func(j *runtime.TypeMeta, c randfill.Continue) {
// We have to customize the randomization of TypeMetas because their
// APIVersion and Kind must remain blank in memory.
j.APIVersion = ""
j.Kind = ""
},
func(j *runtime.Object, c fuzz.Continue) {
func(j *runtime.Object, c randfill.Continue) {
// TODO: uncomment when round trip starts from a versioned object
if true { //c.RandBool() {
if true { // c.Bool() {
*j = &runtime.Unknown{
// We do not set TypeMeta here because it is not carried through a round trip
Raw: []byte(`{"apiVersion":"unknown.group/unknown","kind":"Something","someKey":"someValue"}`),
@ -69,15 +69,15 @@ func genericFuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
} else {
types := []runtime.Object{&metav1.Status{}, &metav1.APIGroup{}}
t := types[c.Rand.Intn(len(types))]
c.Fuzz(t)
c.Fill(t)
*j = t
}
},
func(r *runtime.RawExtension, c fuzz.Continue) {
func(r *runtime.RawExtension, c randfill.Continue) {
// Pick an arbitrary type and fuzz it
types := []runtime.Object{&metav1.Status{}, &metav1.APIGroup{}}
obj := types[c.Rand.Intn(len(types))]
c.Fuzz(obj)
c.Fill(obj)
// Find a codec for converting the object to raw bytes. This is necessary for the
// api version and kind to be correctly set be serialization.
@ -100,7 +100,7 @@ func genericFuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
}
}
// taken from gofuzz internals for RandString
// taken from randfill (nee gofuzz) internals for RandString
type charRange struct {
first, last rune
}
@ -114,7 +114,7 @@ func (c *charRange) choose(r *rand.Rand) rune {
// randomLabelPart produces a valid random label value or name-part
// of a label key.
func randomLabelPart(c fuzz.Continue, canBeEmpty bool) string {
func randomLabelPart(c randfill.Continue, canBeEmpty bool) string {
validStartEnd := []charRange{{'0', '9'}, {'a', 'z'}, {'A', 'Z'}}
validMiddle := []charRange{{'0', '9'}, {'a', 'z'}, {'A', 'Z'},
{'.', '.'}, {'-', '-'}, {'_', '_'}}
@ -138,7 +138,7 @@ func randomLabelPart(c fuzz.Continue, canBeEmpty bool) string {
return string(runes)
}
func randomDNSLabel(c fuzz.Continue) string {
func randomDNSLabel(c randfill.Continue) string {
validStartEnd := []charRange{{'0', '9'}, {'a', 'z'}}
validMiddle := []charRange{{'0', '9'}, {'a', 'z'}, {'-', '-'}}
@ -154,11 +154,11 @@ func randomDNSLabel(c fuzz.Continue) string {
return string(runes)
}
func randomLabelKey(c fuzz.Continue) string {
func randomLabelKey(c randfill.Continue) string {
namePart := randomLabelPart(c, false)
prefixPart := ""
usePrefix := c.RandBool()
usePrefix := c.Bool()
if usePrefix {
// we can fit, with dots, at most 3 labels in the 253 allotted characters
prefixPartsLen := c.Rand.Intn(2) + 1
@ -175,28 +175,28 @@ func randomLabelKey(c fuzz.Continue) string {
func v1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
return []interface{}{
func(j *metav1.TypeMeta, c fuzz.Continue) {
func(j *metav1.TypeMeta, c randfill.Continue) {
// We have to customize the randomization of TypeMetas because their
// APIVersion and Kind must remain blank in memory.
j.APIVersion = ""
j.Kind = ""
},
func(j *metav1.ObjectMeta, c fuzz.Continue) {
c.FuzzNoCustom(j)
func(j *metav1.ObjectMeta, c randfill.Continue) {
c.FillNoCustom(j)
j.ResourceVersion = strconv.FormatUint(c.RandUint64(), 10)
j.UID = types.UID(c.RandString())
j.ResourceVersion = strconv.FormatUint(c.Uint64(), 10)
j.UID = types.UID(c.String(0))
// Fuzzing sec and nsec in a smaller range (uint32 instead of int64),
// so that the result Unix time is a valid date and can be parsed into RFC3339 format.
var sec, nsec uint32
c.Fuzz(&sec)
c.Fuzz(&nsec)
c.Fill(&sec)
c.Fill(&nsec)
j.CreationTimestamp = metav1.Unix(int64(sec), int64(nsec)).Rfc3339Copy()
if j.DeletionTimestamp != nil {
c.Fuzz(&sec)
c.Fuzz(&nsec)
c.Fill(&sec)
c.Fill(&nsec)
t := metav1.Unix(int64(sec), int64(nsec)).Rfc3339Copy()
j.DeletionTimestamp = &t
}
@ -218,16 +218,16 @@ func v1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
j.Finalizers = nil
}
},
func(j *metav1.ResourceVersionMatch, c fuzz.Continue) {
func(j *metav1.ResourceVersionMatch, c randfill.Continue) {
matches := []metav1.ResourceVersionMatch{"", metav1.ResourceVersionMatchExact, metav1.ResourceVersionMatchNotOlderThan}
*j = matches[c.Rand.Intn(len(matches))]
},
func(j *metav1.ListMeta, c fuzz.Continue) {
j.ResourceVersion = strconv.FormatUint(c.RandUint64(), 10)
j.SelfLink = c.RandString()
func(j *metav1.ListMeta, c randfill.Continue) {
j.ResourceVersion = strconv.FormatUint(c.Uint64(), 10)
j.SelfLink = c.String(0) //nolint:staticcheck // SA1019 backwards compatibility
},
func(j *metav1.LabelSelector, c fuzz.Continue) {
c.FuzzNoCustom(j)
func(j *metav1.LabelSelector, c randfill.Continue) {
c.FillNoCustom(j)
// we can't have an entirely empty selector, so force
// use of MatchExpression if necessary
if len(j.MatchLabels) == 0 && len(j.MatchExpressions) == 0 {
@ -257,7 +257,7 @@ func v1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
for i := range j.MatchExpressions {
req := metav1.LabelSelectorRequirement{}
c.Fuzz(&req)
c.Fill(&req)
req.Key = randomLabelKey(c)
req.Operator = validOperators[c.Rand.Intn(len(validOperators))]
if req.Operator == metav1.LabelSelectorOpIn || req.Operator == metav1.LabelSelectorOpNotIn {
@ -278,8 +278,8 @@ func v1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
sort.Slice(j.MatchExpressions, func(a, b int) bool { return j.MatchExpressions[a].Key < j.MatchExpressions[b].Key })
}
},
func(j *metav1.ManagedFieldsEntry, c fuzz.Continue) {
c.FuzzNoCustom(j)
func(j *metav1.ManagedFieldsEntry, c randfill.Continue) {
c.FillNoCustom(j)
j.FieldsV1 = nil
},
}
@ -287,15 +287,15 @@ func v1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
func v1beta1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
return []interface{}{
func(r *metav1beta1.TableOptions, c fuzz.Continue) {
c.FuzzNoCustom(r)
func(r *metav1beta1.TableOptions, c randfill.Continue) {
c.FillNoCustom(r)
// NoHeaders is not serialized to the wire but is allowed within the versioned
// type because we don't use meta internal types in the client and API server.
r.NoHeaders = false
},
func(r *metav1beta1.TableRow, c fuzz.Continue) {
c.Fuzz(&r.Object)
c.Fuzz(&r.Conditions)
func(r *metav1beta1.TableRow, c randfill.Continue) {
c.Fill(&r.Object)
c.Fill(&r.Conditions)
if len(r.Conditions) == 0 {
r.Conditions = nil
}
@ -307,15 +307,15 @@ func v1beta1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
t := c.Intn(6)
switch t {
case 0:
r.Cells[i] = c.RandString()
r.Cells[i] = c.String(0)
case 1:
r.Cells[i] = c.Int63()
case 2:
r.Cells[i] = c.RandBool()
r.Cells[i] = c.Bool()
case 3:
x := map[string]interface{}{}
for j := c.Intn(10) + 1; j >= 0; j-- {
x[c.RandString()] = c.RandString()
x[c.String(0)] = c.String(0)
}
r.Cells[i] = x
case 4:

View File

@ -25,13 +25,10 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/utils/ptr"
)
func TestSetListOptionsDefaults(t *testing.T) {
boolPtrFn := func(b bool) *bool {
return &b
}
scenarios := []struct {
name string
watchListFeatureEnabled bool
@ -47,8 +44,8 @@ func TestSetListOptionsDefaults(t *testing.T) {
{
name: "no-op, SendInitialEvents set",
watchListFeatureEnabled: true,
targetObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: boolPtrFn(true)},
expectedObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: boolPtrFn(true)},
targetObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: ptr.To(true)},
expectedObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: ptr.To(true)},
},
{
name: "no-op, ResourceVersionMatch set",
@ -66,13 +63,13 @@ func TestSetListOptionsDefaults(t *testing.T) {
name: "defaults applied, match on empty RV",
watchListFeatureEnabled: true,
targetObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true},
expectedObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: boolPtrFn(true), ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan},
expectedObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: ptr.To(true), ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan},
},
{
name: "defaults applied, match on RV=0",
watchListFeatureEnabled: true,
targetObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, ResourceVersion: "0"},
expectedObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, ResourceVersion: "0", SendInitialEvents: boolPtrFn(true), ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan},
expectedObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, ResourceVersion: "0", SendInitialEvents: ptr.To(true), ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan},
},
{
name: "no-op, match on empty RV but watch-list fg is off",
@ -82,14 +79,14 @@ func TestSetListOptionsDefaults(t *testing.T) {
{
name: "no-op, match on empty RV but SendInitialEvents is on",
watchListFeatureEnabled: true,
targetObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: boolPtrFn(true)},
expectedObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: boolPtrFn(true)},
targetObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: ptr.To(true)},
expectedObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: ptr.To(true)},
},
{
name: "no-op, match on empty RV but SendInitialEvents is off",
watchListFeatureEnabled: true,
targetObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: boolPtrFn(false)},
expectedObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: boolPtrFn(false)},
targetObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: ptr.To(false)},
expectedObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: ptr.To(false)},
},
{
name: "no-op, match on empty RV but ResourceVersionMatch set",

View File

@ -21,13 +21,10 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
)
func TestValidateListOptions(t *testing.T) {
boolPtrFn := func(b bool) *bool {
return &b
}
cases := []struct {
name string
opts internalversion.ListOptions
@ -65,7 +62,7 @@ func TestValidateListOptions(t *testing.T) {
}, {
name: "list-sendInitialEvents-forbidden",
opts: internalversion.ListOptions{
SendInitialEvents: boolPtrFn(true),
SendInitialEvents: ptr.To(true),
},
expectErrors: []string{"sendInitialEvents: Forbidden: sendInitialEvents is forbidden for list"},
}, {
@ -77,7 +74,7 @@ func TestValidateListOptions(t *testing.T) {
name: "valid-watch-sendInitialEvents-on",
opts: internalversion.ListOptions{
Watch: true,
SendInitialEvents: boolPtrFn(true),
SendInitialEvents: ptr.To(true),
ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
AllowWatchBookmarks: true,
},
@ -86,7 +83,7 @@ func TestValidateListOptions(t *testing.T) {
name: "valid-watch-sendInitialEvents-off",
opts: internalversion.ListOptions{
Watch: true,
SendInitialEvents: boolPtrFn(false),
SendInitialEvents: ptr.To(false),
ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
AllowWatchBookmarks: true,
},
@ -102,14 +99,14 @@ func TestValidateListOptions(t *testing.T) {
name: "watch-sendInitialEvents-without-resourceversionmatch-forbidden",
opts: internalversion.ListOptions{
Watch: true,
SendInitialEvents: boolPtrFn(true),
SendInitialEvents: ptr.To(true),
},
expectErrors: []string{"resourceVersionMatch: Forbidden: sendInitialEvents requires setting resourceVersionMatch to NotOlderThan", "sendInitialEvents: Forbidden: sendInitialEvents is forbidden for watch unless the WatchList feature gate is enabled"},
}, {
name: "watch-sendInitialEvents-with-exact-resourceversionmatch-forbidden",
opts: internalversion.ListOptions{
Watch: true,
SendInitialEvents: boolPtrFn(true),
SendInitialEvents: ptr.To(true),
ResourceVersionMatch: metav1.ResourceVersionMatchExact,
AllowWatchBookmarks: true,
},
@ -119,7 +116,7 @@ func TestValidateListOptions(t *testing.T) {
name: "watch-sendInitialEvents-on-with-empty-resourceversionmatch-forbidden",
opts: internalversion.ListOptions{
Watch: true,
SendInitialEvents: boolPtrFn(true),
SendInitialEvents: ptr.To(true),
ResourceVersionMatch: "",
},
expectErrors: []string{"resourceVersionMatch: Forbidden: sendInitialEvents requires setting resourceVersionMatch to NotOlderThan", "sendInitialEvents: Forbidden: sendInitialEvents is forbidden for watch unless the WatchList feature gate is enabled"},
@ -127,7 +124,7 @@ func TestValidateListOptions(t *testing.T) {
name: "watch-sendInitialEvents-off-with-empty-resourceversionmatch-forbidden",
opts: internalversion.ListOptions{
Watch: true,
SendInitialEvents: boolPtrFn(false),
SendInitialEvents: ptr.To(false),
ResourceVersionMatch: "",
},
expectErrors: []string{"resourceVersionMatch: Forbidden: sendInitialEvents requires setting resourceVersionMatch to NotOlderThan", "sendInitialEvents: Forbidden: sendInitialEvents is forbidden for watch unless the WatchList feature gate is enabled"},
@ -135,7 +132,7 @@ func TestValidateListOptions(t *testing.T) {
name: "watch-sendInitialEvents-with-incorrect-resourceversionmatch-forbidden",
opts: internalversion.ListOptions{
Watch: true,
SendInitialEvents: boolPtrFn(true),
SendInitialEvents: ptr.To(true),
ResourceVersionMatch: "incorrect",
AllowWatchBookmarks: true,
},
@ -147,7 +144,7 @@ func TestValidateListOptions(t *testing.T) {
name: "watch-sendInitialEvents-no-allowWatchBookmark",
opts: internalversion.ListOptions{
Watch: true,
SendInitialEvents: boolPtrFn(true),
SendInitialEvents: ptr.To(true),
ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
},
watchListFeatureEnabled: true,
@ -155,7 +152,7 @@ func TestValidateListOptions(t *testing.T) {
name: "watch-sendInitialEvents-no-watchlist-fg-disabled",
opts: internalversion.ListOptions{
Watch: true,
SendInitialEvents: boolPtrFn(true),
SendInitialEvents: ptr.To(true),
ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
AllowWatchBookmarks: true,
},
@ -164,7 +161,7 @@ func TestValidateListOptions(t *testing.T) {
name: "watch-sendInitialEvents-no-watchlist-fg-disabled",
opts: internalversion.ListOptions{
Watch: true,
SendInitialEvents: boolPtrFn(true),
SendInitialEvents: ptr.To(true),
ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
AllowWatchBookmarks: true,
Continue: "123",

View File

@ -23,9 +23,9 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
fuzz "github.com/google/gofuzz"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/randfill"
)
func TestLabelSelectorAsSelector(t *testing.T) {
@ -188,8 +188,8 @@ func TestResetObjectMetaForStatus(t *testing.T) {
existingMeta := &ObjectMeta{}
// fuzz the existingMeta to set every field, no nils
f := fuzz.New().NilChance(0).NumElements(1, 1).MaxDepth(10)
f.Fuzz(existingMeta)
f := randfill.New().NilChance(0).NumElements(1, 1).MaxDepth(10)
f.Fill(existingMeta)
ResetObjectMetaForStatus(meta, existingMeta)
// not all fields are stomped during the reset. These fields should not have been set. False

View File

@ -20,21 +20,22 @@ limitations under the License.
package v1
import (
"math/rand"
"time"
fuzz "github.com/google/gofuzz"
"sigs.k8s.io/randfill"
)
// Fuzz satisfies fuzz.Interface.
func (t *MicroTime) Fuzz(c fuzz.Continue) {
// Fuzz satisfies randfill.SimpleSelfFiller.
func (t *MicroTime) RandFill(r *rand.Rand) {
if t == nil {
return
}
// Allow for about 1000 years of randomness. Accurate to a tenth of
// micro second. Leave off nanoseconds because JSON doesn't
// represent them so they can't round-trip properly.
t.Time = time.Unix(c.Rand.Int63n(1000*365*24*60*60), 1000*c.Rand.Int63n(1000000))
t.Time = time.Unix(r.Int63n(1000*365*24*60*60), 1000*r.Int63n(1000000))
}
// ensure MicroTime implements fuzz.Interface
var _ fuzz.Interface = &MicroTime{}
// ensure MicroTime implements randfill.Interface
var _ randfill.SimpleSelfFiller = &MicroTime{}

View File

@ -27,7 +27,7 @@ import (
"sigs.k8s.io/yaml"
"github.com/google/go-cmp/cmp"
fuzz "github.com/google/gofuzz"
"sigs.k8s.io/randfill"
)
type MicroTimeHolder struct {
@ -378,10 +378,10 @@ func TestMicroTimeProtoUnmarshalRaw(t *testing.T) {
}
func TestMicroTimeRoundtripCBOR(t *testing.T) {
fuzzer := fuzz.New()
fuzzer := randfill.New()
for i := 0; i < 500; i++ {
var initial, final MicroTime
fuzzer.Fuzz(&initial)
fuzzer.Fill(&initial)
b, err := cbor.Marshal(initial)
if err != nil {
t.Errorf("error encoding %v: %v", initial, err)

View File

@ -22,15 +22,15 @@ import (
"reflect"
"testing"
fuzz "github.com/google/gofuzz"
"sigs.k8s.io/randfill"
)
func TestPatchOptionsIsSuperSetOfUpdateOptions(t *testing.T) {
f := fuzz.New()
f := randfill.New()
for i := 0; i < 1000; i++ {
t.Run(fmt.Sprintf("Run %d/1000", i), func(t *testing.T) {
update := UpdateOptions{}
f.Fuzz(&update)
f.Fill(&update)
b, err := json.Marshal(update)
if err != nil {

View File

@ -20,21 +20,22 @@ limitations under the License.
package v1
import (
"math/rand"
"time"
fuzz "github.com/google/gofuzz"
"sigs.k8s.io/randfill"
)
// Fuzz satisfies fuzz.Interface.
func (t *Time) Fuzz(c fuzz.Continue) {
// Fuzz satisfies randfill.SimpleSelfFiller.
func (t *Time) RandFill(r *rand.Rand) {
if t == nil {
return
}
// Allow for about 1000 years of randomness. Leave off nanoseconds
// because JSON doesn't represent them so they can't round-trip
// properly.
t.Time = time.Unix(c.Rand.Int63n(1000*365*24*60*60), 0)
t.Time = time.Unix(r.Int63n(1000*365*24*60*60), 0)
}
// ensure Time implements fuzz.Interface
var _ fuzz.Interface = &Time{}
// ensure Time implements randfill.SimpleSelfFiller
var _ randfill.SimpleSelfFiller = &Time{}

View File

@ -27,7 +27,7 @@ import (
"sigs.k8s.io/yaml"
"github.com/google/go-cmp/cmp"
fuzz "github.com/google/gofuzz"
"sigs.k8s.io/randfill"
)
type TimeHolder struct {
@ -303,10 +303,10 @@ func TestTimeIsZero(t *testing.T) {
}
func TestTimeRoundtripCBOR(t *testing.T) {
fuzzer := fuzz.New()
fuzzer := randfill.New()
for i := 0; i < 500; i++ {
var initial, final Time
fuzzer.Fuzz(&initial)
fuzzer.Fill(&initial)
b, err := cbor.Marshal(initial)
if err != nil {
t.Errorf("error encoding %v: %v", initial, err)

View File

@ -185,7 +185,7 @@ type ObjectMeta struct {
// Null for lists.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
// +optional
CreationTimestamp Time `json:"creationTimestamp,omitempty" protobuf:"bytes,8,opt,name=creationTimestamp"`
CreationTimestamp Time `json:"creationTimestamp,omitempty,omitzero" protobuf:"bytes,8,opt,name=creationTimestamp"`
// DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This
// field is set by the server when a graceful deletion is requested by the user, and is not
@ -439,20 +439,6 @@ const (
//
// The annotation is added to a "Bookmark" event.
InitialEventsAnnotationKey = "k8s.io/initial-events-end"
// InitialEventsListBlueprintAnnotationKey is the name of the key
// where an empty, versioned list is encoded in the requested format
// (e.g., protobuf, JSON, CBOR), then base64-encoded and stored as a string.
//
// This encoding matches the request encoding format, which may be
// protobuf, JSON, CBOR, or others, depending on what the client requested.
// This ensures that the reconstructed list can be processed through the
// same decoder chain that would handle a standard LIST call response.
//
// The annotation is added to a "Bookmark" event and is used by clients
// to guarantee the format consistency when reconstructing
// the list during WatchList processing.
InitialEventsListBlueprintAnnotationKey = "kubernetes.io/initial-events-list-blueprint"
)
// resourceVersionMatch specifies how the resourceVersion parameter is applied. resourceVersionMatch

View File

@ -55,11 +55,9 @@ func TestObjectToUnstructuredConversion(t *testing.T) {
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Carp",
"metadata": map[string]interface{}{
"creationTimestamp": nil,
},
"spec": map[string]interface{}{},
"status": map[string]interface{}{},
"metadata": map[string]interface{}{},
"spec": map[string]interface{}{},
"status": map[string]interface{}{},
},
},
},
@ -78,8 +76,7 @@ func TestObjectToUnstructuredConversion(t *testing.T) {
"apiVersion": "v1",
"kind": "Carp",
"metadata": map[string]interface{}{
"creationTimestamp": nil,
"name": "noxu",
"name": "noxu",
},
"spec": map[string]interface{}{
"hostname": "example.com",
@ -155,8 +152,7 @@ func TestUnstructuredToObjectConversion(t *testing.T) {
"apiVersion": "v1",
"kind": "Carp",
"metadata": map[string]interface{}{
"creationTimestamp": nil,
"name": "noxu",
"name": "noxu",
},
"spec": map[string]interface{}{
"hostname": "example.com",
@ -181,8 +177,7 @@ func TestUnstructuredToObjectConversion(t *testing.T) {
"apiVersion": "v1",
"kind": "Carp",
"metadata": map[string]interface{}{
"creationTimestamp": nil,
"name": "noxu",
"name": "noxu",
},
"spec": map[string]interface{}{
"hostname": "example.com",
@ -207,8 +202,7 @@ func TestUnstructuredToObjectConversion(t *testing.T) {
"apiVersion": "v9",
"kind": "Carp",
"metadata": map[string]interface{}{
"creationTimestamp": nil,
"name": "noxu",
"name": "noxu",
},
"spec": map[string]interface{}{
"hostname": "example.com",
@ -232,8 +226,7 @@ func TestUnstructuredToObjectConversion(t *testing.T) {
"apiVersion": "v1",
"kind": "Carp",
"metadata": map[string]interface{}{
"creationTimestamp": nil,
"name": "noxu",
"name": "noxu",
},
"spec": map[string]interface{}{
"hostname": "example.com",
@ -358,8 +351,7 @@ func TestUnstructuredToGVConversion(t *testing.T) {
"apiVersion": "v1",
"kind": "Carp",
"metadata": map[string]interface{}{
"creationTimestamp": nil,
"name": "noxu",
"name": "noxu",
},
"spec": map[string]interface{}{
"hostname": "example.com",
@ -388,8 +380,7 @@ func TestUnstructuredToGVConversion(t *testing.T) {
"apiVersion": "v1",
"kind": "Carp",
"metadata": map[string]interface{}{
"creationTimestamp": nil,
"name": "noxu",
"name": "noxu",
},
"spec": map[string]interface{}{
"hostname": "example.com",
@ -414,8 +405,7 @@ func TestUnstructuredToGVConversion(t *testing.T) {
"apiVersion": "v1",
"kind": "Carp",
"metadata": map[string]interface{}{
"creationTimestamp": nil,
"name": "noxu",
"name": "noxu",
},
"spec": map[string]interface{}{
"hostname": "example.com",

View File

@ -28,8 +28,8 @@ import (
"time"
"github.com/google/go-cmp/cmp"
fuzz "github.com/google/gofuzz"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/randfill"
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
"k8s.io/apimachinery/pkg/api/equality"
@ -67,7 +67,7 @@ func TestUnstructuredMetadataRoundTrip(t *testing.T) {
u := &unstructured.Unstructured{Object: map[string]interface{}{}}
uCopy := u.DeepCopy()
metadata := &metav1.ObjectMeta{}
fuzzer.Fuzz(metadata)
fuzzer.Fill(metadata)
if err := setObjectMeta(u, metadata); err != nil {
t.Fatalf("unexpected error setting fuzzed ObjectMeta: %v", err)
@ -94,7 +94,7 @@ func TestUnstructuredMetadataOmitempty(t *testing.T) {
// fuzz to make sure we don't miss any function calls below
u := &unstructured.Unstructured{Object: map[string]interface{}{}}
metadata := &metav1.ObjectMeta{}
fuzzer.Fuzz(metadata)
fuzzer.Fill(metadata)
if err := setObjectMeta(u, metadata); err != nil {
t.Fatalf("unexpected error setting fuzzed ObjectMeta: %v", err)
}
@ -184,7 +184,7 @@ func roundtripType[U runtime.Unstructured](t *testing.T) {
for i := 0; i < 50; i++ {
original := reflect.New(reflect.TypeFor[U]().Elem()).Interface().(runtime.Unstructured)
fuzzer.Fuzz(original)
fuzzer.Fill(original)
// unstructured -> JSON > unstructured > CBOR -> unstructured -> JSON -> unstructured
roundtrip(t, original, jS, cS)
// unstructured -> CBOR > unstructured > JSON -> unstructured -> CBOR -> unstructured
@ -285,25 +285,25 @@ const (
func unstructuredFuzzerFuncs(codecs serializer.CodecFactory) []interface{} {
return []interface{}{
func(u *unstructured.Unstructured, c fuzz.Continue) {
func(u *unstructured.Unstructured, c randfill.Continue) {
obj := make(map[string]interface{})
obj["apiVersion"] = generateValidAPIVersionString(c)
obj["kind"] = generateNonEmptyString(c)
for j := c.Intn(maxUnstructuredFanOut); j >= 0; j-- {
obj[c.RandString()] = generateRandomTypeValue(maxUnstructuredDepth, c)
obj[c.String(0)] = generateRandomTypeValue(maxUnstructuredDepth, c)
}
u.Object = obj
},
func(ul *unstructured.UnstructuredList, c fuzz.Continue) {
func(ul *unstructured.UnstructuredList, c randfill.Continue) {
obj := make(map[string]interface{})
obj["apiVersion"] = generateValidAPIVersionString(c)
obj["kind"] = generateNonEmptyString(c)
for j := c.Intn(maxUnstructuredFanOut); j >= 0; j-- {
obj[c.RandString()] = generateRandomTypeValue(maxUnstructuredDepth, c)
obj[c.String(0)] = generateRandomTypeValue(maxUnstructuredDepth, c)
}
for j := c.Intn(maxUnstructuredFanOut); j >= 0; j-- {
var item = unstructured.Unstructured{}
c.Fuzz(&item)
c.Fill(&item)
ul.Items = append(ul.Items, item)
}
ul.Object = obj
@ -311,16 +311,16 @@ func unstructuredFuzzerFuncs(codecs serializer.CodecFactory) []interface{} {
}
}
func generateNonEmptyString(c fuzz.Continue) string {
temp := c.RandString()
func generateNonEmptyString(c randfill.Continue) string {
temp := c.String(0)
for len(temp) == 0 {
temp = c.RandString()
temp = c.String(0)
}
return temp
}
// generateNonEmptyNoSlashString generates a non-empty string without any slashes
func generateNonEmptyNoSlashString(c fuzz.Continue) string {
func generateNonEmptyNoSlashString(c randfill.Continue) string {
temp := strings.ReplaceAll(generateNonEmptyString(c), "/", "")
for len(temp) == 0 {
temp = strings.ReplaceAll(generateNonEmptyString(c), "/", "")
@ -330,8 +330,8 @@ func generateNonEmptyNoSlashString(c fuzz.Continue) string {
// generateValidAPIVersionString generates valid apiVersion string with formats:
// <string>/<string> or <string>
func generateValidAPIVersionString(c fuzz.Continue) string {
if c.RandBool() {
func generateValidAPIVersionString(c randfill.Continue) string {
if c.Bool() {
return generateNonEmptyNoSlashString(c) + "/" + generateNonEmptyNoSlashString(c)
} else {
return generateNonEmptyNoSlashString(c)
@ -355,7 +355,7 @@ func generateValidAPIVersionString(c fuzz.Continue) string {
// All external-versioned builtin types are exercised through RoundtripToUnstructured
// in apitesting package. Types like metav1.Time are implicitly being exercised
// because they appear as fields in those types.
func generateRandomTypeValue(depth int, c fuzz.Continue) interface{} {
func generateRandomTypeValue(depth int, c randfill.Continue) interface{} {
t := c.Rand.Intn(120)
// If the max depth for unstructured is reached, only add non-recursive types
// which is 20+ in range
@ -373,21 +373,21 @@ func generateRandomTypeValue(depth int, c fuzz.Continue) interface{} {
case t < 20:
item := map[string]interface{}{}
for j := c.Intn(maxUnstructuredFanOut); j >= 0; j-- {
item[c.RandString()] = generateRandomTypeValue(depth-1, c)
item[c.String(0)] = generateRandomTypeValue(depth-1, c)
}
return item
case t < 40:
// Only valid UTF-8 encodings
var item string
c.Fuzz(&item)
c.Fill(&item)
return item
case t < 60:
var item int64
c.Fuzz(&item)
c.Fill(&item)
return item
case t < 80:
var item bool
c.Fuzz(&item)
c.Fill(&item)
return item
case t < 100:
return c.Rand.NormFloat64()

View File

@ -104,7 +104,7 @@ func ValidateLabelSelectorRequirement(sr metav1.LabelSelectorRequirement, opts L
func ValidateLabelName(labelName string, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
for _, msg := range validation.IsQualifiedName(labelName) {
allErrs = append(allErrs, field.Invalid(fldPath, labelName, msg))
allErrs = append(allErrs, field.Invalid(fldPath, labelName, msg).WithOrigin("labelKey"))
}
return allErrs
}
@ -328,11 +328,11 @@ func ValidateCondition(condition metav1.Condition, fldPath *field.Path) field.Er
}
if condition.LastTransitionTime.IsZero() {
allErrs = append(allErrs, field.Required(fldPath.Child("lastTransitionTime"), "must be set"))
allErrs = append(allErrs, field.Required(fldPath.Child("lastTransitionTime"), ""))
}
if len(condition.Reason) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("reason"), "must be set"))
allErrs = append(allErrs, field.Required(fldPath.Child("reason"), ""))
} else {
for _, currErr := range isValidConditionReason(condition.Reason) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("reason"), condition.Reason, currErr))

View File

@ -132,10 +132,6 @@ func TestInvalidDryRun(t *testing.T) {
}
func boolPtr(b bool) *bool {
return &b
}
func TestValidateDeleteOptionsWithIgnoreStoreReadError(t *testing.T) {
fieldPath := field.NewPath("ignoreStoreReadErrorWithClusterBreakingPotential")
tests := []struct {
@ -232,7 +228,7 @@ func TestValidPatchOptions(t *testing.T) {
patchType types.PatchType
}{{
opts: metav1.PatchOptions{
Force: boolPtr(true),
Force: ptr.To(true),
FieldManager: "kubectl",
},
patchType: types.ApplyYAMLPatchType,
@ -243,7 +239,7 @@ func TestValidPatchOptions(t *testing.T) {
patchType: types.ApplyYAMLPatchType,
}, {
opts: metav1.PatchOptions{
Force: boolPtr(true),
Force: ptr.To(true),
FieldManager: "kubectl",
},
patchType: types.ApplyCBORPatchType,
@ -290,7 +286,7 @@ func TestInvalidPatchOptions(t *testing.T) {
// force on non-apply
{
opts: metav1.PatchOptions{
Force: boolPtr(true),
Force: ptr.To(true),
},
patchType: types.MergePatchType,
},
@ -298,7 +294,7 @@ func TestInvalidPatchOptions(t *testing.T) {
{
opts: metav1.PatchOptions{
FieldManager: "kubectl",
Force: boolPtr(false),
Force: ptr.To(false),
},
patchType: types.MergePatchType,
},
@ -442,7 +438,7 @@ func TestValidateConditions(t *testing.T) {
if !hasError(errs, needle) {
t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
}
needle = `status.conditions[0].lastTransitionTime: Required value: must be set`
needle = `status.conditions[0].lastTransitionTime: Required value`
if !hasError(errs, needle) {
t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
}

View File

@ -19,12 +19,12 @@ package fuzzer
import (
"fmt"
"github.com/google/gofuzz"
"sigs.k8s.io/randfill"
apitesting "k8s.io/apimachinery/pkg/api/apitesting"
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
"k8s.io/apimachinery/pkg/apis/testapigroup"
"k8s.io/apimachinery/pkg/apis/testapigroup/v1"
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
"k8s.io/apimachinery/pkg/runtime"
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
)
@ -33,9 +33,9 @@ import (
// values in a Kubernetes context.
func overrideMetaFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
return []interface{}{
func(j *runtime.Object, c fuzz.Continue) {
func(j *runtime.Object, c randfill.Continue) {
// TODO: uncomment when round trip starts from a versioned object
if true { //c.RandBool() {
if true { // c.Bool() {
*j = &runtime.Unknown{
// We do not set TypeMeta here because it is not carried through a round trip
Raw: []byte(`{"apiVersion":"unknown.group/unknown","kind":"Something","someKey":"someValue"}`),
@ -44,15 +44,15 @@ func overrideMetaFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
} else {
types := []runtime.Object{&testapigroup.Carp{}}
t := types[c.Rand.Intn(len(types))]
c.Fuzz(t)
c.Fill(t)
*j = t
}
},
func(r *runtime.RawExtension, c fuzz.Continue) {
func(r *runtime.RawExtension, c randfill.Continue) {
// Pick an arbitrary type and fuzz it
types := []runtime.Object{&testapigroup.Carp{}}
obj := types[c.Rand.Intn(len(types))]
c.Fuzz(obj)
c.Fill(obj)
// Convert the object to raw bytes
bytes, err := runtime.Encode(apitesting.TestCodec(codecs, v1.SchemeGroupVersion), obj)
@ -68,11 +68,11 @@ func overrideMetaFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
func testapigroupFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
return []interface{}{
func(s *testapigroup.CarpSpec, c fuzz.Continue) {
c.FuzzNoCustom(s)
func(s *testapigroup.CarpSpec, c randfill.Continue) {
c.FillNoCustom(s)
// has a default value
ttl := int64(30)
if c.RandBool() {
if c.Bool() {
ttl = int64(c.Uint32())
}
s.TerminationGracePeriodSeconds = &ttl
@ -81,11 +81,11 @@ func testapigroupFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
s.SchedulerName = "default-scheduler"
}
},
func(j *testapigroup.CarpPhase, c fuzz.Continue) {
func(j *testapigroup.CarpPhase, c randfill.Continue) {
statuses := []testapigroup.CarpPhase{"Pending", "Running", "Succeeded", "Failed", "Unknown"}
*j = statuses[c.Rand.Intn(len(statuses))]
},
func(rp *testapigroup.RestartPolicy, c fuzz.Continue) {
func(rp *testapigroup.RestartPolicy, c randfill.Continue) {
policies := []testapigroup.RestartPolicy{"Always", "Never", "OnFailure"}
*rp = policies[c.Rand.Intn(len(policies))]
},

View File

@ -46,6 +46,7 @@ func Resource(resource string) schema.GroupResource {
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Carp{},
&CarpList{},
)
return nil
}

View File

@ -68,6 +68,14 @@ type CarpStatus struct {
// This is before the Kubelet pulled the container image(s) for the carp.
// +optional
StartTime *metav1.Time
// Carp infos are provided by different clients, hence the map type.
//
// +listType=map
// +listKey=a
// +listKey=b
// +listKey=c
Infos []CarpInfo
}
type CarpCondition struct {
@ -83,6 +91,21 @@ type CarpCondition struct {
Message string
}
type CarpInfo struct {
// A is the first map key.
// +required
A int64
// B is the second map key.
// +required
B string
// C is the third, optional map key
// +optional
C *string
// Some data for each pair of A and B.
Data string
}
// CarpSpec is a description of a carp
type CarpSpec struct {
// +optional

View File

@ -14,11 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// +k8s:openapi-gen=true
// +k8s:deepcopy-gen=package
// +k8s:conversion-gen=k8s.io/apimachinery/pkg/apis/testapigroup
// +k8s:openapi-gen=false
// +k8s:defaulter-gen=TypeMeta
// +k8s:prerelease-lifecycle-gen=true
// +groupName=testapigroup.apimachinery.k8s.io
package v1

View File

@ -101,10 +101,38 @@ func (m *CarpCondition) XXX_DiscardUnknown() {
var xxx_messageInfo_CarpCondition proto.InternalMessageInfo
func (m *CarpInfo) Reset() { *m = CarpInfo{} }
func (*CarpInfo) ProtoMessage() {}
func (*CarpInfo) Descriptor() ([]byte, []int) {
return fileDescriptor_83e19b543dd132db, []int{2}
}
func (m *CarpInfo) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *CarpInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
b = b[:cap(b)]
n, err := m.MarshalToSizedBuffer(b)
if err != nil {
return nil, err
}
return b[:n], nil
}
func (m *CarpInfo) XXX_Merge(src proto.Message) {
xxx_messageInfo_CarpInfo.Merge(m, src)
}
func (m *CarpInfo) XXX_Size() int {
return m.Size()
}
func (m *CarpInfo) XXX_DiscardUnknown() {
xxx_messageInfo_CarpInfo.DiscardUnknown(m)
}
var xxx_messageInfo_CarpInfo proto.InternalMessageInfo
func (m *CarpList) Reset() { *m = CarpList{} }
func (*CarpList) ProtoMessage() {}
func (*CarpList) Descriptor() ([]byte, []int) {
return fileDescriptor_83e19b543dd132db, []int{2}
return fileDescriptor_83e19b543dd132db, []int{3}
}
func (m *CarpList) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@ -132,7 +160,7 @@ var xxx_messageInfo_CarpList proto.InternalMessageInfo
func (m *CarpSpec) Reset() { *m = CarpSpec{} }
func (*CarpSpec) ProtoMessage() {}
func (*CarpSpec) Descriptor() ([]byte, []int) {
return fileDescriptor_83e19b543dd132db, []int{3}
return fileDescriptor_83e19b543dd132db, []int{4}
}
func (m *CarpSpec) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@ -160,7 +188,7 @@ var xxx_messageInfo_CarpSpec proto.InternalMessageInfo
func (m *CarpStatus) Reset() { *m = CarpStatus{} }
func (*CarpStatus) ProtoMessage() {}
func (*CarpStatus) Descriptor() ([]byte, []int) {
return fileDescriptor_83e19b543dd132db, []int{4}
return fileDescriptor_83e19b543dd132db, []int{5}
}
func (m *CarpStatus) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@ -188,6 +216,7 @@ var xxx_messageInfo_CarpStatus proto.InternalMessageInfo
func init() {
proto.RegisterType((*Carp)(nil), "k8s.io.apimachinery.pkg.apis.testapigroup.v1.Carp")
proto.RegisterType((*CarpCondition)(nil), "k8s.io.apimachinery.pkg.apis.testapigroup.v1.CarpCondition")
proto.RegisterType((*CarpInfo)(nil), "k8s.io.apimachinery.pkg.apis.testapigroup.v1.CarpInfo")
proto.RegisterType((*CarpList)(nil), "k8s.io.apimachinery.pkg.apis.testapigroup.v1.CarpList")
proto.RegisterType((*CarpSpec)(nil), "k8s.io.apimachinery.pkg.apis.testapigroup.v1.CarpSpec")
proto.RegisterMapType((map[string]string)(nil), "k8s.io.apimachinery.pkg.apis.testapigroup.v1.CarpSpec.NodeSelectorEntry")
@ -199,72 +228,77 @@ func init() {
}
var fileDescriptor_83e19b543dd132db = []byte{
// 1037 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x56, 0xc1, 0x6e, 0xdb, 0x46,
0x13, 0x36, 0x2d, 0xc9, 0x96, 0xd6, 0x56, 0x62, 0x6f, 0x62, 0x80, 0xbf, 0x81, 0x48, 0x8e, 0x0f,
0x86, 0xff, 0xc2, 0xa5, 0x62, 0xa3, 0x09, 0xdc, 0xa6, 0x40, 0x11, 0xda, 0x45, 0xa5, 0xc2, 0x71,
0x84, 0x95, 0x81, 0x14, 0x45, 0x0f, 0x59, 0x51, 0x5b, 0x8a, 0x95, 0xc8, 0x25, 0x76, 0x57, 0x2a,
0x74, 0x2b, 0xfa, 0x04, 0x7d, 0x88, 0xde, 0x7a, 0xee, 0x03, 0xf4, 0x50, 0xc0, 0xc7, 0x1c, 0x73,
0x12, 0x6a, 0xf5, 0x2d, 0x7c, 0x2a, 0x76, 0xb9, 0xa4, 0x48, 0x4b, 0x55, 0xa3, 0xdc, 0xb8, 0x33,
0xdf, 0xf7, 0xcd, 0xec, 0xce, 0x68, 0x46, 0xe0, 0xf3, 0xde, 0x29, 0xb7, 0x3c, 0x5a, 0xc3, 0xa1,
0xe7, 0x63, 0xa7, 0xeb, 0x05, 0x84, 0x8d, 0x6a, 0x61, 0xcf, 0x95, 0x06, 0x5e, 0x13, 0x84, 0x0b,
0x1c, 0x7a, 0x2e, 0xa3, 0x83, 0xb0, 0x36, 0x3c, 0xae, 0xb9, 0x24, 0x20, 0x0c, 0x0b, 0xd2, 0xb1,
0x42, 0x46, 0x05, 0x85, 0x47, 0x11, 0xdb, 0x4a, 0xb3, 0xad, 0xb0, 0xe7, 0x4a, 0x03, 0xb7, 0xd2,
0x6c, 0x6b, 0x78, 0xbc, 0xfb, 0xb1, 0xeb, 0x89, 0xee, 0xa0, 0x6d, 0x39, 0xd4, 0xaf, 0xb9, 0xd4,
0xa5, 0x35, 0x25, 0xd2, 0x1e, 0x7c, 0xaf, 0x4e, 0xea, 0xa0, 0xbe, 0x22, 0xf1, 0xdd, 0x4f, 0x16,
0xa6, 0xe6, 0x13, 0x81, 0xe7, 0xa4, 0xb4, 0x5b, 0xfb, 0x37, 0x16, 0x1b, 0x04, 0xc2, 0xf3, 0xc9,
0x0c, 0xe1, 0xd9, 0x7f, 0x11, 0xb8, 0xd3, 0x25, 0x3e, 0xbe, 0xcb, 0xdb, 0xff, 0x75, 0x15, 0xe4,
0xcf, 0x30, 0x0b, 0xe1, 0x1b, 0x50, 0x94, 0xc9, 0x74, 0xb0, 0xc0, 0xa6, 0xb1, 0x67, 0x1c, 0x6e,
0x9c, 0x3c, 0xb1, 0x16, 0xbe, 0x8b, 0x44, 0x5b, 0xc3, 0x63, 0xeb, 0x55, 0xfb, 0x07, 0xe2, 0x88,
0x97, 0x44, 0x60, 0x1b, 0x5e, 0x8f, 0xab, 0x2b, 0x93, 0x71, 0x15, 0x4c, 0x6d, 0x28, 0x51, 0x85,
0xdf, 0x80, 0x3c, 0x0f, 0x89, 0x63, 0xae, 0x2a, 0xf5, 0x67, 0xd6, 0x32, 0xaf, 0x6e, 0xc9, 0x1c,
0x5b, 0x21, 0x71, 0xec, 0x4d, 0x1d, 0x23, 0x2f, 0x4f, 0x48, 0x29, 0xc2, 0x37, 0x60, 0x8d, 0x0b,
0x2c, 0x06, 0xdc, 0xcc, 0x29, 0xed, 0xd3, 0x0f, 0xd0, 0x56, 0x7c, 0xfb, 0x9e, 0x56, 0x5f, 0x8b,
0xce, 0x48, 0xeb, 0xee, 0xff, 0x9e, 0x03, 0x65, 0x09, 0x3b, 0xa3, 0x41, 0xc7, 0x13, 0x1e, 0x0d,
0xe0, 0x53, 0x90, 0x17, 0xa3, 0x90, 0xa8, 0xb7, 0x2a, 0xd9, 0x8f, 0xe3, 0xac, 0xae, 0x46, 0x21,
0xb9, 0x1d, 0x57, 0xb7, 0x33, 0x60, 0x69, 0x44, 0x0a, 0x0e, 0x3f, 0x4d, 0x52, 0x5d, 0xcd, 0x10,
0x75, 0xc0, 0xdb, 0x71, 0xf5, 0x7e, 0x42, 0xcb, 0xe6, 0x00, 0x5d, 0x50, 0xee, 0x63, 0x2e, 0x9a,
0x8c, 0xb6, 0xc9, 0x95, 0xe7, 0x13, 0x7d, 0xd9, 0x8f, 0xde, 0xaf, 0x4c, 0x92, 0x61, 0xef, 0xe8,
0x68, 0xe5, 0x8b, 0xb4, 0x10, 0xca, 0xea, 0xc2, 0x21, 0x80, 0xd2, 0x70, 0xc5, 0x70, 0xc0, 0xa3,
0xfc, 0x65, 0xb4, 0xfc, 0xd2, 0xd1, 0x76, 0x75, 0x34, 0x78, 0x31, 0xa3, 0x86, 0xe6, 0x44, 0x80,
0x07, 0x60, 0x8d, 0x11, 0xcc, 0x69, 0x60, 0x16, 0xd4, 0xdb, 0x24, 0xc5, 0x40, 0xca, 0x8a, 0xb4,
0x17, 0xfe, 0x1f, 0xac, 0xfb, 0x84, 0x73, 0xec, 0x12, 0x73, 0x4d, 0x01, 0xef, 0x6b, 0xe0, 0xfa,
0xcb, 0xc8, 0x8c, 0x62, 0xff, 0xfe, 0x1f, 0x06, 0x28, 0xca, 0x52, 0x5c, 0x78, 0x5c, 0xc0, 0xef,
0x66, 0x5a, 0xdc, 0x7a, 0xbf, 0xdb, 0x48, 0xb6, 0x6a, 0xf0, 0x2d, 0x1d, 0xa8, 0x18, 0x5b, 0x52,
0xed, 0xfd, 0x1a, 0x14, 0x3c, 0x41, 0x7c, 0x59, 0xd8, 0xdc, 0xe1, 0xc6, 0xc9, 0xc9, 0xf2, 0x3d,
0x68, 0x97, 0xb5, 0x7c, 0xa1, 0x21, 0x85, 0x50, 0xa4, 0xb7, 0xff, 0xe7, 0x7a, 0x74, 0x07, 0xd9,
0xf0, 0xf0, 0x02, 0x94, 0x99, 0xa4, 0x32, 0xd1, 0xa4, 0x7d, 0xcf, 0x19, 0xa9, 0x26, 0x28, 0xd9,
0x07, 0x71, 0x61, 0x51, 0xda, 0x79, 0x7b, 0xd7, 0x80, 0xb2, 0x64, 0xe8, 0x82, 0x47, 0x82, 0x30,
0xdf, 0x0b, 0xb0, 0x2c, 0xc2, 0x57, 0x0c, 0x3b, 0xa4, 0x49, 0x98, 0x47, 0x3b, 0x2d, 0xe2, 0xd0,
0xa0, 0xc3, 0x55, 0xd1, 0x73, 0xf6, 0xe3, 0xc9, 0xb8, 0xfa, 0xe8, 0x6a, 0x11, 0x10, 0x2d, 0xd6,
0x81, 0xaf, 0xc0, 0x0e, 0x76, 0x84, 0x37, 0x24, 0xe7, 0x04, 0x77, 0xfa, 0x5e, 0x40, 0xe2, 0x00,
0x05, 0x15, 0xe0, 0x7f, 0x93, 0x71, 0x75, 0xe7, 0xc5, 0x3c, 0x00, 0x9a, 0xcf, 0x83, 0x3f, 0x1b,
0x60, 0x33, 0xa0, 0x1d, 0xd2, 0x22, 0x7d, 0xe2, 0x08, 0xca, 0xcc, 0x75, 0xf5, 0xea, 0xf5, 0x0f,
0x9b, 0x2a, 0xd6, 0x65, 0x4a, 0xea, 0xcb, 0x40, 0xb0, 0x91, 0xfd, 0x50, 0xbf, 0xe8, 0x66, 0xda,
0x85, 0x32, 0x31, 0xe1, 0xd7, 0x00, 0x72, 0xc2, 0x86, 0x9e, 0x43, 0x5e, 0x38, 0x0e, 0x1d, 0x04,
0xe2, 0x12, 0xfb, 0xc4, 0x2c, 0xaa, 0x8a, 0x24, 0xcd, 0xdf, 0x9a, 0x41, 0xa0, 0x39, 0x2c, 0x58,
0x07, 0xf7, 0xb2, 0x56, 0xb3, 0xa4, 0x74, 0xf6, 0xb4, 0x8e, 0x79, 0x4e, 0x42, 0x46, 0x1c, 0x39,
0xba, 0xb3, 0x8a, 0xe8, 0x0e, 0x0f, 0x1e, 0x81, 0xa2, 0xcc, 0x52, 0xe5, 0x02, 0x94, 0x46, 0xd2,
0xb6, 0x97, 0xda, 0x8e, 0x12, 0x04, 0x7c, 0x0a, 0x36, 0xba, 0x94, 0x8b, 0x4b, 0x22, 0x7e, 0xa4,
0xac, 0x67, 0x6e, 0xec, 0x19, 0x87, 0x45, 0xfb, 0x81, 0x26, 0x6c, 0xd4, 0xa7, 0x2e, 0x94, 0xc6,
0xc9, 0xdf, 0xa0, 0x3c, 0x36, 0x1b, 0xe7, 0xe6, 0xa6, 0xa2, 0x24, 0xbf, 0xc1, 0x7a, 0x64, 0x46,
0xb1, 0x3f, 0x86, 0x36, 0x9a, 0x67, 0x66, 0x79, 0x16, 0xda, 0x68, 0x9e, 0xa1, 0xd8, 0x2f, 0x53,
0x97, 0x9f, 0x81, 0x4c, 0x7d, 0x2b, 0x9b, 0x7a, 0x5d, 0xdb, 0x51, 0x82, 0x80, 0x35, 0x50, 0xe2,
0x83, 0x76, 0x87, 0xfa, 0xd8, 0x0b, 0xcc, 0x6d, 0x05, 0xdf, 0xd6, 0xf0, 0x52, 0x2b, 0x76, 0xa0,
0x29, 0x06, 0x3e, 0x07, 0x65, 0xb9, 0x06, 0x3b, 0x83, 0x3e, 0x61, 0x2a, 0xc6, 0x03, 0x45, 0x4a,
0xa6, 0x62, 0x2b, 0x76, 0xaa, 0x37, 0xca, 0x62, 0x77, 0xbf, 0x00, 0xdb, 0x33, 0x5d, 0x02, 0xb7,
0x40, 0xae, 0x47, 0x46, 0xd1, 0x12, 0x40, 0xf2, 0x13, 0x3e, 0x04, 0x85, 0x21, 0xee, 0x0f, 0x48,
0x34, 0xdf, 0x51, 0x74, 0xf8, 0x6c, 0xf5, 0xd4, 0xd8, 0xff, 0x2d, 0x07, 0xc0, 0x74, 0xd5, 0xc0,
0x27, 0xa0, 0x10, 0x76, 0x31, 0x8f, 0x37, 0x48, 0xdc, 0x2f, 0x85, 0xa6, 0x34, 0xde, 0x8e, 0xab,
0x25, 0x89, 0x55, 0x07, 0x14, 0x01, 0x21, 0x05, 0xc0, 0x89, 0x77, 0x43, 0x3c, 0x66, 0x9e, 0x2f,
0xdf, 0xf0, 0xc9, 0x7e, 0x99, 0xee, 0xeb, 0xc4, 0xc4, 0x51, 0x2a, 0x44, 0x7a, 0xd0, 0xe6, 0x16,
0x0f, 0xda, 0xd4, 0xec, 0xce, 0x2f, 0x9c, 0xdd, 0x07, 0x60, 0x2d, 0x2a, 0xf6, 0xdd, 0x19, 0x1f,
0xf5, 0x02, 0xd2, 0x5e, 0x89, 0x73, 0x30, 0x0b, 0x1b, 0x4d, 0x3d, 0xe2, 0x13, 0xdc, 0x99, 0xb2,
0x22, 0xed, 0x85, 0xaf, 0x41, 0x49, 0x0d, 0x34, 0xb5, 0xa2, 0xd6, 0x97, 0x5e, 0x51, 0x65, 0xd5,
0x2b, 0xb1, 0x00, 0x9a, 0x6a, 0xd9, 0xe8, 0xfa, 0xa6, 0xb2, 0xf2, 0xf6, 0xa6, 0xb2, 0xf2, 0xee,
0xa6, 0xb2, 0xf2, 0xd3, 0xa4, 0x62, 0x5c, 0x4f, 0x2a, 0xc6, 0xdb, 0x49, 0xc5, 0x78, 0x37, 0xa9,
0x18, 0x7f, 0x4d, 0x2a, 0xc6, 0x2f, 0x7f, 0x57, 0x56, 0xbe, 0x3d, 0x5a, 0xe6, 0x8f, 0xe7, 0x3f,
0x01, 0x00, 0x00, 0xff, 0xff, 0x9e, 0xd1, 0x13, 0x90, 0xa7, 0x0a, 0x00, 0x00,
// 1115 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x56, 0x41, 0x4f, 0x1b, 0x47,
0x14, 0x66, 0xb1, 0x0d, 0xf6, 0x80, 0x1b, 0x18, 0x82, 0xba, 0x45, 0x8a, 0x4d, 0x7c, 0x40, 0xb4,
0xa2, 0xeb, 0x80, 0x9a, 0x88, 0x36, 0x95, 0x2a, 0x16, 0xaa, 0x42, 0x45, 0x88, 0x35, 0x46, 0x4a,
0xd5, 0xe6, 0x90, 0xf1, 0xee, 0xb0, 0x6c, 0xf1, 0xee, 0xac, 0x66, 0xc6, 0xae, 0x7c, 0xab, 0x7a,
0xea, 0xb1, 0x3f, 0xa2, 0x7f, 0xa1, 0x3f, 0xa0, 0x37, 0x8e, 0x39, 0xa6, 0x17, 0xab, 0xb8, 0xff,
0x82, 0x53, 0x35, 0xb3, 0xb3, 0xeb, 0x35, 0x06, 0x27, 0xe6, 0xe6, 0x79, 0xef, 0xfb, 0xbe, 0xf7,
0xf6, 0xcd, 0x9b, 0xf7, 0x0c, 0xbe, 0xbe, 0xd8, 0xe5, 0x96, 0x4f, 0xeb, 0x38, 0xf2, 0x03, 0xec,
0x9c, 0xfb, 0x21, 0x61, 0xbd, 0x7a, 0x74, 0xe1, 0x49, 0x03, 0xaf, 0x0b, 0xc2, 0x05, 0x8e, 0x7c,
0x8f, 0xd1, 0x4e, 0x54, 0xef, 0x6e, 0xd7, 0x3d, 0x12, 0x12, 0x86, 0x05, 0x71, 0xad, 0x88, 0x51,
0x41, 0xe1, 0x56, 0xcc, 0xb6, 0xb2, 0x6c, 0x2b, 0xba, 0xf0, 0xa4, 0x81, 0x5b, 0x59, 0xb6, 0xd5,
0xdd, 0x5e, 0xfb, 0xdc, 0xf3, 0xc5, 0x79, 0xa7, 0x65, 0x39, 0x34, 0xa8, 0x7b, 0xd4, 0xa3, 0x75,
0x25, 0xd2, 0xea, 0x9c, 0xa9, 0x93, 0x3a, 0xa8, 0x5f, 0xb1, 0xf8, 0xda, 0x17, 0x13, 0x53, 0x0b,
0x88, 0xc0, 0xb7, 0xa4, 0xb4, 0x56, 0xbf, 0x8b, 0xc5, 0x3a, 0xa1, 0xf0, 0x03, 0x32, 0x46, 0x78,
0xf6, 0x3e, 0x02, 0x77, 0xce, 0x49, 0x80, 0x6f, 0xf2, 0x6a, 0x7f, 0xce, 0x82, 0xfc, 0x3e, 0x66,
0x11, 0x7c, 0x03, 0x8a, 0x32, 0x19, 0x17, 0x0b, 0x6c, 0x1a, 0xeb, 0xc6, 0xe6, 0xc2, 0xce, 0x13,
0x6b, 0x62, 0x5d, 0x24, 0xda, 0xea, 0x6e, 0x5b, 0x2f, 0x5b, 0x3f, 0x13, 0x47, 0xbc, 0x20, 0x02,
0xdb, 0xf0, 0xb2, 0x5f, 0x9d, 0x19, 0xf4, 0xab, 0x60, 0x68, 0x43, 0xa9, 0x2a, 0xfc, 0x01, 0xe4,
0x79, 0x44, 0x1c, 0x73, 0x56, 0xa9, 0x3f, 0xb3, 0xa6, 0xa9, 0xba, 0x25, 0x73, 0x6c, 0x46, 0xc4,
0xb1, 0x17, 0x75, 0x8c, 0xbc, 0x3c, 0x21, 0xa5, 0x08, 0xdf, 0x80, 0x39, 0x2e, 0xb0, 0xe8, 0x70,
0x33, 0xa7, 0xb4, 0x77, 0xef, 0xa1, 0xad, 0xf8, 0xf6, 0x47, 0x5a, 0x7d, 0x2e, 0x3e, 0x23, 0xad,
0x5b, 0xfb, 0x2b, 0x07, 0xca, 0x12, 0xb6, 0x4f, 0x43, 0xd7, 0x17, 0x3e, 0x0d, 0xe1, 0x53, 0x90,
0x17, 0xbd, 0x88, 0xa8, 0x5a, 0x95, 0xec, 0xc7, 0x49, 0x56, 0xa7, 0xbd, 0x88, 0x5c, 0xf7, 0xab,
0xcb, 0x23, 0x60, 0x69, 0x44, 0x0a, 0x0e, 0xbf, 0x4c, 0x53, 0x9d, 0x1d, 0x21, 0xea, 0x80, 0xd7,
0xfd, 0xea, 0x83, 0x94, 0x36, 0x9a, 0x03, 0xf4, 0x40, 0xb9, 0x8d, 0xb9, 0x68, 0x30, 0xda, 0x22,
0xa7, 0x7e, 0x40, 0xf4, 0xc7, 0x7e, 0xf6, 0x61, 0xd7, 0x24, 0x19, 0xf6, 0xaa, 0x8e, 0x56, 0x3e,
0xce, 0x0a, 0xa1, 0x51, 0x5d, 0xd8, 0x05, 0x50, 0x1a, 0x4e, 0x19, 0x0e, 0x79, 0x9c, 0xbf, 0x8c,
0x96, 0x9f, 0x3a, 0xda, 0x9a, 0x8e, 0x06, 0x8f, 0xc7, 0xd4, 0xd0, 0x2d, 0x11, 0xe0, 0x06, 0x98,
0x63, 0x04, 0x73, 0x1a, 0x9a, 0x05, 0x55, 0x9b, 0xf4, 0x32, 0x90, 0xb2, 0x22, 0xed, 0x85, 0x9f,
0x82, 0xf9, 0x80, 0x70, 0x8e, 0x3d, 0x62, 0xce, 0x29, 0xe0, 0x03, 0x0d, 0x9c, 0x7f, 0x11, 0x9b,
0x51, 0xe2, 0xaf, 0x71, 0x50, 0x94, 0x37, 0x71, 0x14, 0x9e, 0x51, 0xf8, 0x31, 0x30, 0xe2, 0xd6,
0xce, 0xd9, 0x25, 0x4d, 0x30, 0xf6, 0x90, 0x81, 0xa5, 0xa3, 0xa5, 0xaf, 0x23, 0x75, 0xd8, 0xc8,
0x68, 0xc1, 0x15, 0x60, 0x38, 0xea, 0xbb, 0x4b, 0x76, 0x41, 0x1a, 0xf7, 0x91, 0xe1, 0xc0, 0x75,
0x90, 0x57, 0x8f, 0x24, 0xa7, 0xec, 0x69, 0x3b, 0x1e, 0x60, 0x81, 0x91, 0xf2, 0xd4, 0xfe, 0x36,
0xe2, 0xa8, 0xc7, 0x3e, 0x17, 0xf0, 0xf5, 0xd8, 0xbb, 0xb2, 0x3e, 0xac, 0x84, 0x92, 0xad, 0x5e,
0xd5, 0x92, 0x0e, 0x51, 0x4c, 0x2c, 0x99, 0x37, 0xf5, 0x0a, 0x14, 0x7c, 0x41, 0x02, 0xd9, 0x4d,
0xb9, 0xcd, 0x85, 0x9d, 0x9d, 0xe9, 0x1b, 0xdf, 0x2e, 0x6b, 0xf9, 0xc2, 0x91, 0x14, 0x42, 0xb1,
0x5e, 0xed, 0x9f, 0xf9, 0xf8, 0x1b, 0xe4, 0x2b, 0x83, 0xc7, 0xa0, 0xcc, 0x24, 0x95, 0x89, 0x06,
0x6d, 0xfb, 0x4e, 0x4f, 0x7f, 0xfb, 0x46, 0xd2, 0x4d, 0x28, 0xeb, 0xbc, 0xbe, 0x69, 0x40, 0xa3,
0x64, 0xe8, 0x81, 0x47, 0x82, 0xb0, 0xc0, 0x0f, 0xb1, 0xbc, 0xf9, 0xef, 0x18, 0x76, 0x48, 0x83,
0x30, 0x9f, 0xba, 0x4d, 0xe2, 0xd0, 0xd0, 0xe5, 0xaa, 0xe2, 0x39, 0xfb, 0xf1, 0xa0, 0x5f, 0x7d,
0x74, 0x3a, 0x09, 0x88, 0x26, 0xeb, 0xc0, 0x97, 0x60, 0x15, 0x3b, 0xc2, 0xef, 0x92, 0x03, 0x82,
0xdd, 0xb6, 0x1f, 0x92, 0x24, 0x40, 0x41, 0x05, 0xf8, 0x64, 0xd0, 0xaf, 0xae, 0xee, 0xdd, 0x06,
0x40, 0xb7, 0xf3, 0xe0, 0x6f, 0x06, 0x58, 0x0c, 0xa9, 0x4b, 0x9a, 0xa4, 0x4d, 0x1c, 0x41, 0x99,
0x39, 0xaf, 0xaa, 0x7e, 0x78, 0xbf, 0x51, 0x66, 0x9d, 0x64, 0xa4, 0xbe, 0x0d, 0x05, 0xeb, 0xd9,
0x0f, 0x75, 0x45, 0x17, 0xb3, 0x2e, 0x34, 0x12, 0x13, 0x7e, 0x0f, 0x20, 0x27, 0xac, 0xeb, 0x3b,
0x64, 0xcf, 0x71, 0x68, 0x27, 0x14, 0x27, 0x38, 0x20, 0x66, 0x51, 0xdd, 0x48, 0xfa, 0xe2, 0x9a,
0x63, 0x08, 0x74, 0x0b, 0x0b, 0xbe, 0x06, 0xa6, 0x4b, 0x22, 0x46, 0x1c, 0xb9, 0x11, 0x46, 0x39,
0x66, 0x49, 0x29, 0xae, 0x6b, 0x45, 0xf3, 0xe0, 0x0e, 0x1c, 0xba, 0x53, 0x01, 0x6e, 0x81, 0xa2,
0xcc, 0x5c, 0xe5, 0x07, 0x94, 0x5a, 0xda, 0xca, 0x27, 0xda, 0x8e, 0x52, 0x04, 0x7c, 0x0a, 0x16,
0xce, 0x29, 0x17, 0x27, 0x44, 0xfc, 0x42, 0xd9, 0x85, 0xb9, 0xb0, 0x6e, 0x6c, 0x16, 0xed, 0x15,
0x4d, 0x58, 0x38, 0x1c, 0xba, 0x50, 0x16, 0x27, 0x87, 0x81, 0x3c, 0x36, 0x8e, 0x0e, 0xcc, 0x45,
0x45, 0x49, 0x87, 0xc1, 0x61, 0x6c, 0x46, 0x89, 0x3f, 0x81, 0x1e, 0x35, 0xf6, 0xcd, 0xf2, 0x38,
0xf4, 0xa8, 0xb1, 0x8f, 0x12, 0xbf, 0x4c, 0x5d, 0xfe, 0x0c, 0x65, 0xea, 0x4b, 0xa3, 0xa9, 0x1f,
0x6a, 0x3b, 0x4a, 0x11, 0xb0, 0x0e, 0x4a, 0xbc, 0xd3, 0x72, 0x69, 0x80, 0xfd, 0xd0, 0x5c, 0x56,
0xf0, 0x65, 0x0d, 0x2f, 0x35, 0x13, 0x07, 0x1a, 0x62, 0xe0, 0x73, 0x50, 0x96, 0xfb, 0xd8, 0xed,
0xb4, 0x09, 0x53, 0xe5, 0x59, 0x51, 0xa4, 0x74, 0x3c, 0x37, 0xb3, 0x4e, 0x34, 0x8a, 0x5d, 0xfb,
0x06, 0x2c, 0x8f, 0x75, 0x0e, 0x5c, 0x02, 0xb9, 0x0b, 0xd2, 0x8b, 0xb7, 0x11, 0x92, 0x3f, 0xe1,
0x43, 0x50, 0xe8, 0xe2, 0x76, 0x87, 0xc4, 0x93, 0x0d, 0xc5, 0x87, 0xaf, 0x66, 0x77, 0x8d, 0xda,
0xef, 0x79, 0x00, 0x86, 0x3b, 0x0f, 0x3e, 0x01, 0x85, 0xe8, 0x1c, 0xf3, 0x64, 0x95, 0x25, 0x3d,
0x54, 0x68, 0x48, 0xe3, 0x75, 0xbf, 0x5a, 0x92, 0x58, 0x75, 0x40, 0x31, 0x10, 0x52, 0x00, 0x9c,
0x64, 0x49, 0x25, 0xa3, 0xe7, 0xf9, 0xf4, 0x8f, 0x20, 0x5d, 0x74, 0xc3, 0x3f, 0x0e, 0xa9, 0x89,
0xa3, 0x4c, 0x88, 0xec, 0xc4, 0xcf, 0x4d, 0x9e, 0xf8, 0x99, 0x25, 0x92, 0x9f, 0xb8, 0x44, 0x36,
0xc0, 0x5c, 0x7c, 0xd9, 0x37, 0x97, 0x4d, 0xdc, 0x0b, 0x48, 0x7b, 0x25, 0xce, 0x91, 0x1b, 0xa4,
0xa1, 0x77, 0x4d, 0x8a, 0x53, 0x7b, 0xa5, 0x81, 0xb4, 0x17, 0xbe, 0x02, 0x25, 0x35, 0xe4, 0xd4,
0xae, 0x9c, 0x9f, 0x7a, 0x57, 0x96, 0x55, 0xaf, 0x24, 0x02, 0x68, 0xa8, 0x05, 0x7f, 0x02, 0x05,
0x3f, 0x3c, 0xa3, 0xdc, 0x2c, 0xaa, 0x3a, 0xdf, 0xe3, 0x7f, 0x93, 0xdc, 0x7e, 0x99, 0x31, 0x2f,
0xc5, 0x50, 0xac, 0x69, 0xa3, 0xcb, 0xab, 0xca, 0xcc, 0xdb, 0xab, 0xca, 0xcc, 0xbb, 0xab, 0xca,
0xcc, 0xaf, 0x83, 0x8a, 0x71, 0x39, 0xa8, 0x18, 0x6f, 0x07, 0x15, 0xe3, 0xdd, 0xa0, 0x62, 0xfc,
0x3b, 0xa8, 0x18, 0x7f, 0xfc, 0x57, 0x99, 0xf9, 0x71, 0x6b, 0x9a, 0xbf, 0xd7, 0xff, 0x07, 0x00,
0x00, 0xff, 0xff, 0xa8, 0x1e, 0x19, 0x20, 0x8d, 0x0b, 0x00, 0x00,
}
func (m *Carp) Marshal() (dAtA []byte, err error) {
@ -383,6 +417,49 @@ func (m *CarpCondition) MarshalToSizedBuffer(dAtA []byte) (int, error) {
return len(dAtA) - i, nil
}
func (m *CarpInfo) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalToSizedBuffer(dAtA[:size])
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *CarpInfo) MarshalTo(dAtA []byte) (int, error) {
size := m.Size()
return m.MarshalToSizedBuffer(dAtA[:size])
}
func (m *CarpInfo) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i := len(dAtA)
_ = i
var l int
_ = l
if m.C != nil {
i -= len(*m.C)
copy(dAtA[i:], *m.C)
i = encodeVarintGenerated(dAtA, i, uint64(len(*m.C)))
i--
dAtA[i] = 0x22
}
i -= len(m.Data)
copy(dAtA[i:], m.Data)
i = encodeVarintGenerated(dAtA, i, uint64(len(m.Data)))
i--
dAtA[i] = 0x1a
i -= len(m.B)
copy(dAtA[i:], m.B)
i = encodeVarintGenerated(dAtA, i, uint64(len(m.B)))
i--
dAtA[i] = 0x12
i = encodeVarintGenerated(dAtA, i, uint64(m.A))
i--
dAtA[i] = 0x8
return len(dAtA) - i, nil
}
func (m *CarpList) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
@ -572,6 +649,20 @@ func (m *CarpStatus) MarshalToSizedBuffer(dAtA []byte) (int, error) {
_ = i
var l int
_ = l
if len(m.Infos) > 0 {
for iNdEx := len(m.Infos) - 1; iNdEx >= 0; iNdEx-- {
{
size, err := m.Infos[iNdEx].MarshalToSizedBuffer(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = encodeVarintGenerated(dAtA, i, uint64(size))
}
i--
dAtA[i] = 0x42
}
}
if m.StartTime != nil {
{
size, err := m.StartTime.MarshalToSizedBuffer(dAtA[:i])
@ -673,6 +764,24 @@ func (m *CarpCondition) Size() (n int) {
return n
}
func (m *CarpInfo) Size() (n int) {
if m == nil {
return 0
}
var l int
_ = l
n += 1 + sovGenerated(uint64(m.A))
l = len(m.B)
n += 1 + l + sovGenerated(uint64(l))
l = len(m.Data)
n += 1 + l + sovGenerated(uint64(l))
if m.C != nil {
l = len(*m.C)
n += 1 + l + sovGenerated(uint64(l))
}
return n
}
func (m *CarpList) Size() (n int) {
if m == nil {
return 0
@ -756,6 +865,12 @@ func (m *CarpStatus) Size() (n int) {
l = m.StartTime.Size()
n += 1 + l + sovGenerated(uint64(l))
}
if len(m.Infos) > 0 {
for _, e := range m.Infos {
l = e.Size()
n += 1 + l + sovGenerated(uint64(l))
}
}
return n
}
@ -792,6 +907,19 @@ func (this *CarpCondition) String() string {
}, "")
return s
}
func (this *CarpInfo) String() string {
if this == nil {
return "nil"
}
s := strings.Join([]string{`&CarpInfo{`,
`A:` + fmt.Sprintf("%v", this.A) + `,`,
`B:` + fmt.Sprintf("%v", this.B) + `,`,
`Data:` + fmt.Sprintf("%v", this.Data) + `,`,
`C:` + valueToStringGenerated(this.C) + `,`,
`}`,
}, "")
return s
}
func (this *CarpList) String() string {
if this == nil {
return "nil"
@ -849,6 +977,11 @@ func (this *CarpStatus) String() string {
repeatedStringForConditions += strings.Replace(strings.Replace(f.String(), "CarpCondition", "CarpCondition", 1), `&`, ``, 1) + ","
}
repeatedStringForConditions += "}"
repeatedStringForInfos := "[]CarpInfo{"
for _, f := range this.Infos {
repeatedStringForInfos += strings.Replace(strings.Replace(f.String(), "CarpInfo", "CarpInfo", 1), `&`, ``, 1) + ","
}
repeatedStringForInfos += "}"
s := strings.Join([]string{`&CarpStatus{`,
`Phase:` + fmt.Sprintf("%v", this.Phase) + `,`,
`Conditions:` + repeatedStringForConditions + `,`,
@ -857,6 +990,7 @@ func (this *CarpStatus) String() string {
`HostIP:` + fmt.Sprintf("%v", this.HostIP) + `,`,
`CarpIP:` + fmt.Sprintf("%v", this.CarpIP) + `,`,
`StartTime:` + strings.Replace(fmt.Sprintf("%v", this.StartTime), "Time", "v1.Time", 1) + `,`,
`Infos:` + repeatedStringForInfos + `,`,
`}`,
}, "")
return s
@ -1262,6 +1396,172 @@ func (m *CarpCondition) Unmarshal(dAtA []byte) error {
}
return nil
}
func (m *CarpInfo) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: CarpInfo: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: CarpInfo: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field A", wireType)
}
m.A = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.A |= int64(b&0x7F) << shift
if b < 0x80 {
break
}
}
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field B", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthGenerated
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthGenerated
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.B = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 3:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Data", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthGenerated
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthGenerated
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Data = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 4:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field C", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthGenerated
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthGenerated
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
s := string(dAtA[iNdEx:postIndex])
m.C = &s
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipGenerated(dAtA[iNdEx:])
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
return ErrInvalidLengthGenerated
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *CarpList) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
@ -2139,6 +2439,40 @@ func (m *CarpStatus) Unmarshal(dAtA []byte) error {
return err
}
iNdEx = postIndex
case 8:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Infos", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthGenerated
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthGenerated
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Infos = append(m.Infos, CarpInfo{})
if err := m.Infos[len(m.Infos)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipGenerated(dAtA[iNdEx:])

View File

@ -77,6 +77,23 @@ message CarpCondition {
optional string message = 6;
}
message CarpInfo {
// A is the first map key.
// +required
optional int64 a = 1;
// B is the second map key.
// +required
optional string b = 2;
// C is the third, optional map key
// +optional
optional string c = 4;
// Some data for each pair of A and B.
optional string data = 3;
}
// CarpList is a list of Carps.
message CarpList {
// Standard list metadata.
@ -129,7 +146,7 @@ message CarpSpec {
// Deprecated: Use serviceAccountName instead.
// +k8s:conversion-gen=false
// +optional
optional string serviceAccount = 9;
optional string deprecatedServiceAccount = 9;
// NodeName is a request to schedule this carp onto a specific node. If it is non-empty,
// the scheduler simply schedules this carp onto that node, assuming that it fits resource
@ -138,7 +155,6 @@ message CarpSpec {
optional string nodeName = 10;
// Host networking requested for this carp. Use the host's network namespace.
// If this option is set, the ports that will be used must be specified.
// Default to false.
// +k8s:conversion-gen=false
// +optional
@ -169,7 +185,7 @@ message CarpSpec {
// If specified, the carp will be dispatched by specified scheduler.
// If not specified, the carp will be dispatched by default scheduler.
// +optional
optional string schedulername = 19;
optional string schedulerName = 19;
}
// CarpStatus represents information about the status of a carp. Status may trail the actual
@ -182,6 +198,10 @@ message CarpStatus {
// Current service state of carp.
// More info: http://kubernetes.io/docs/user-guide/carp-states#carp-conditions
// +patchStrategy=merge
// +patchMergeKey=type
// +listType=map
// +listMapKey=type
// +optional
repeated CarpCondition conditions = 2;
@ -207,5 +227,13 @@ message CarpStatus {
// This is before the Kubelet pulled the container image(s) for the carp.
// +optional
optional .k8s.io.apimachinery.pkg.apis.meta.v1.Time startTime = 7;
// Carp infos are provided by different clients, hence the map type.
//
// +listType=map
// +listMapKey=a
// +listMapKey=b
// +listMapKey=c
repeated CarpInfo infos = 8;
}

View File

@ -57,6 +57,7 @@ func init() {
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Carp{},
&CarpList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil

View File

@ -28,6 +28,7 @@ type (
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.1
// Carp is a collection of containers, used as either input (create, update) or as output (list, get).
type Carp struct {
@ -60,8 +61,12 @@ type CarpStatus struct {
Phase CarpPhase `json:"phase,omitempty" protobuf:"bytes,1,opt,name=phase,casttype=CarpPhase"`
// Current service state of carp.
// More info: http://kubernetes.io/docs/user-guide/carp-states#carp-conditions
// +patchStrategy=merge
// +patchMergeKey=type
// +listType=map
// +listMapKey=type
// +optional
Conditions []CarpCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,2,rep,name=conditions"`
Conditions []CarpCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,2,opt,name=conditions"`
// A human readable message indicating details about why the carp is in this condition.
// +optional
Message string `json:"message,omitempty" protobuf:"bytes,3,opt,name=message"`
@ -82,6 +87,14 @@ type CarpStatus struct {
// This is before the Kubelet pulled the container image(s) for the carp.
// +optional
StartTime *metav1.Time `json:"startTime,omitempty" protobuf:"bytes,7,opt,name=startTime"`
// Carp infos are provided by different clients, hence the map type.
//
// +listType=map
// +listMapKey=a
// +listMapKey=b
// +listMapKey=c
Infos []CarpInfo `json:"infos,omitempty" protobuf:"bytes,8,rep,name=infos"`
}
type CarpCondition struct {
@ -107,6 +120,21 @@ type CarpCondition struct {
Message string `json:"message,omitempty" protobuf:"bytes,6,opt,name=message"`
}
type CarpInfo struct {
// A is the first map key.
// +required
A int64 `json:"a" protobuf:"bytes,1,name=a"`
// B is the second map key.
// +required
B string `json:"b" protobuf:"bytes,2,name=b"`
// C is the third, optional map key
// +optional
C *string `json:"c,omitempty" protobuf:"bytes,4,opt,name=c"`
// Some data for each pair of A and B.
Data string `json:"data" protobuf:"bytes,3,name=data"`
}
// CarpSpec is a description of a carp
type CarpSpec struct {
// Restart policy for all containers within the carp.
@ -143,7 +171,7 @@ type CarpSpec struct {
// Deprecated: Use serviceAccountName instead.
// +k8s:conversion-gen=false
// +optional
DeprecatedServiceAccount string `json:"serviceAccount,omitempty" protobuf:"bytes,9,opt,name=serviceAccount"`
DeprecatedServiceAccount string `json:"deprecatedServiceAccount,omitempty" protobuf:"bytes,9,opt,name=deprecatedServiceAccount"`
// NodeName is a request to schedule this carp onto a specific node. If it is non-empty,
// the scheduler simply schedules this carp onto that node, assuming that it fits resource
@ -151,7 +179,6 @@ type CarpSpec struct {
// +optional
NodeName string `json:"nodeName,omitempty" protobuf:"bytes,10,opt,name=nodeName"`
// Host networking requested for this carp. Use the host's network namespace.
// If this option is set, the ports that will be used must be specified.
// Default to false.
// +k8s:conversion-gen=false
// +optional
@ -177,10 +204,11 @@ type CarpSpec struct {
// If specified, the carp will be dispatched by specified scheduler.
// If not specified, the carp will be dispatched by default scheduler.
// +optional
SchedulerName string `json:"schedulername,omitempty" protobuf:"bytes,19,opt,name=schedulername"`
SchedulerName string `json:"schedulerName,omitempty" protobuf:"bytes,19,opt,name=schedulerName"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.1
// CarpList is a list of Carps.
type CarpList struct {

View File

@ -57,6 +57,16 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*CarpInfo)(nil), (*testapigroup.CarpInfo)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1_CarpInfo_To_testapigroup_CarpInfo(a.(*CarpInfo), b.(*testapigroup.CarpInfo), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*testapigroup.CarpInfo)(nil), (*CarpInfo)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_testapigroup_CarpInfo_To_v1_CarpInfo(a.(*testapigroup.CarpInfo), b.(*CarpInfo), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*CarpList)(nil), (*testapigroup.CarpList)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1_CarpList_To_testapigroup_CarpList(a.(*CarpList), b.(*testapigroup.CarpList), scope)
}); err != nil {
@ -152,6 +162,32 @@ func Convert_testapigroup_CarpCondition_To_v1_CarpCondition(in *testapigroup.Car
return autoConvert_testapigroup_CarpCondition_To_v1_CarpCondition(in, out, s)
}
func autoConvert_v1_CarpInfo_To_testapigroup_CarpInfo(in *CarpInfo, out *testapigroup.CarpInfo, s conversion.Scope) error {
out.A = in.A
out.B = in.B
out.C = (*string)(unsafe.Pointer(in.C))
out.Data = in.Data
return nil
}
// Convert_v1_CarpInfo_To_testapigroup_CarpInfo is an autogenerated conversion function.
func Convert_v1_CarpInfo_To_testapigroup_CarpInfo(in *CarpInfo, out *testapigroup.CarpInfo, s conversion.Scope) error {
return autoConvert_v1_CarpInfo_To_testapigroup_CarpInfo(in, out, s)
}
func autoConvert_testapigroup_CarpInfo_To_v1_CarpInfo(in *testapigroup.CarpInfo, out *CarpInfo, s conversion.Scope) error {
out.A = in.A
out.B = in.B
out.C = (*string)(unsafe.Pointer(in.C))
out.Data = in.Data
return nil
}
// Convert_testapigroup_CarpInfo_To_v1_CarpInfo is an autogenerated conversion function.
func Convert_testapigroup_CarpInfo_To_v1_CarpInfo(in *testapigroup.CarpInfo, out *CarpInfo, s conversion.Scope) error {
return autoConvert_testapigroup_CarpInfo_To_v1_CarpInfo(in, out, s)
}
func autoConvert_v1_CarpList_To_testapigroup_CarpList(in *CarpList, out *testapigroup.CarpList, s conversion.Scope) error {
out.ListMeta = in.ListMeta
if in.Items != nil {
@ -242,6 +278,7 @@ func autoConvert_v1_CarpStatus_To_testapigroup_CarpStatus(in *CarpStatus, out *t
out.HostIP = in.HostIP
out.CarpIP = in.CarpIP
out.StartTime = (*metav1.Time)(unsafe.Pointer(in.StartTime))
out.Infos = *(*[]testapigroup.CarpInfo)(unsafe.Pointer(&in.Infos))
return nil
}
@ -258,6 +295,7 @@ func autoConvert_testapigroup_CarpStatus_To_v1_CarpStatus(in *testapigroup.CarpS
out.HostIP = in.HostIP
out.CarpIP = in.CarpIP
out.StartTime = (*metav1.Time)(unsafe.Pointer(in.StartTime))
out.Infos = *(*[]CarpInfo)(unsafe.Pointer(&in.Infos))
return nil
}

View File

@ -71,6 +71,27 @@ func (in *CarpCondition) DeepCopy() *CarpCondition {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CarpInfo) DeepCopyInto(out *CarpInfo) {
*out = *in
if in.C != nil {
in, out := &in.C, &out.C
*out = new(string)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CarpInfo.
func (in *CarpInfo) DeepCopy() *CarpInfo {
if in == nil {
return nil
}
out := new(CarpInfo)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CarpList) DeepCopyInto(out *CarpList) {
*out = *in
@ -151,6 +172,13 @@ func (in *CarpStatus) DeepCopyInto(out *CarpStatus) {
in, out := &in.StartTime, &out.StartTime
*out = (*in).DeepCopy()
}
if in.Infos != nil {
in, out := &in.Infos, &out.Infos
*out = make([]CarpInfo, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}

View File

@ -0,0 +1,34 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
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.
*/
// Code generated by prerelease-lifecycle-gen. DO NOT EDIT.
package v1
// APILifecycleIntroduced is an autogenerated function, returning the release in which the API struct was introduced as int versions of major and minor for comparison.
// It is controlled by "k8s:prerelease-lifecycle-gen:introduced" tags in types.go.
func (in *Carp) APILifecycleIntroduced() (major, minor int) {
return 1, 1
}
// APILifecycleIntroduced is an autogenerated function, returning the release in which the API struct was introduced as int versions of major and minor for comparison.
// It is controlled by "k8s:prerelease-lifecycle-gen:introduced" tags in types.go.
func (in *CarpList) APILifecycleIntroduced() (major, minor int) {
return 1, 1
}

View File

@ -71,6 +71,27 @@ func (in *CarpCondition) DeepCopy() *CarpCondition {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CarpInfo) DeepCopyInto(out *CarpInfo) {
*out = *in
if in.C != nil {
in, out := &in.C, &out.C
*out = new(string)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CarpInfo.
func (in *CarpInfo) DeepCopy() *CarpInfo {
if in == nil {
return nil
}
out := new(CarpInfo)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CarpList) DeepCopyInto(out *CarpList) {
*out = *in
@ -151,6 +172,13 @@ func (in *CarpStatus) DeepCopyInto(out *CarpStatus) {
in, out := &in.StartTime, &out.StartTime
*out = (*in).DeepCopy()
}
if in.Infos != nil {
in, out := &in.Infos, &out.Infos
*out = make([]CarpInfo, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}

View File

@ -25,6 +25,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/conversion/queryparams"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/utils/ptr"
)
type namedString string
@ -161,20 +162,20 @@ func TestConvert(t *testing.T) {
},
{
input: &baz{
Ptr: intp(5),
Bptr: boolp(true),
Ptr: ptr.To(5),
Bptr: ptr.To(true),
},
expected: url.Values{"ptr": {"5"}, "bptr": {"true"}},
},
{
input: &baz{
Bptr: boolp(true),
Bptr: ptr.To(true),
},
expected: url.Values{"ptr": {""}, "bptr": {"true"}},
},
{
input: &baz{
Ptr: intp(5),
Ptr: ptr.To(5),
},
expected: url.Values{"ptr": {"5"}},
},
@ -213,7 +214,3 @@ func TestConvert(t *testing.T) {
validateResult(t, test.input, result, test.expected)
}
}
func intp(n int) *int { return &n }
func boolp(b bool) *bool { return &b }

View File

@ -31,6 +31,9 @@ type Labels interface {
// Get returns the value for the provided label.
Get(label string) (value string)
// Lookup returns the value for the provided label if it exists and whether the provided label exist
Lookup(label string) (value string, exists bool)
}
// Set is a map of label:value. It implements Labels.
@ -59,6 +62,12 @@ func (ls Set) Get(label string) string {
return ls[label]
}
// Lookup returns the value for the provided label if it exists and whether the provided label exist
func (ls Set) Lookup(label string) (string, bool) {
val, exists := ls[label]
return val, exists
}
// AsSelector converts labels into a selectors. It does not
// perform any validation, which means the server will reject
// the request if the Set contains invalid values.

View File

@ -23,11 +23,12 @@ import (
"strconv"
"strings"
"k8s.io/klog/v2"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/klog/v2"
)
var (
@ -115,6 +116,15 @@ func Nothing() Selector {
return sharedNothingSelector
}
// MatchesNothing only returns true for selectors which are definitively determined to match no objects.
// This currently only detects the `labels.Nothing()` selector, but may change over time to detect more selectors that match no objects.
//
// Note: The current implementation does not check for selector conflict scenarios (e.g., a=a,a!=a).
// Support for detecting such cases can be added in the future.
func MatchesNothing(selector Selector) bool {
return selector == sharedNothingSelector
}
// NewSelector returns a nil selector
func NewSelector() Selector {
return internalSelector(nil)
@ -236,26 +246,29 @@ func (r *Requirement) hasValue(value string) bool {
func (r *Requirement) Matches(ls Labels) bool {
switch r.operator {
case selection.In, selection.Equals, selection.DoubleEquals:
if !ls.Has(r.key) {
val, exists := ls.Lookup(r.key)
if !exists {
return false
}
return r.hasValue(ls.Get(r.key))
return r.hasValue(val)
case selection.NotIn, selection.NotEquals:
if !ls.Has(r.key) {
val, exists := ls.Lookup(r.key)
if !exists {
return true
}
return !r.hasValue(ls.Get(r.key))
return !r.hasValue(val)
case selection.Exists:
return ls.Has(r.key)
case selection.DoesNotExist:
return !ls.Has(r.key)
case selection.GreaterThan, selection.LessThan:
if !ls.Has(r.key) {
val, exists := ls.Lookup(r.key)
if !exists {
return false
}
lsValue, err := strconv.ParseInt(ls.Get(r.key), 10, 64)
lsValue, err := strconv.ParseInt(val, 10, 64)
if err != nil {
klog.V(10).Infof("ParseInt failed for value %+v in label %+v, %+v", ls.Get(r.key), ls, err)
klog.V(10).Infof("ParseInt failed for value %+v in label %+v, %+v", val, ls, err)
return false
}
@ -996,7 +1009,8 @@ type ValidatedSetSelector Set
func (s ValidatedSetSelector) Matches(labels Labels) bool {
for k, v := range s {
if !labels.Has(k) || v != labels.Get(k) {
val, exists := labels.Lookup(k)
if !exists || v != val {
return false
}
}

View File

@ -1008,6 +1008,26 @@ func BenchmarkRequirementString(b *testing.B) {
}
}
func BenchmarkRequirementMatches(b *testing.B) {
r := Requirement{
key: "environment",
operator: selection.NotIn,
strValues: []string{
"dev",
},
}
labels := Set(map[string]string{
"key": "value",
"environment": "dev",
})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
r.Matches(labels)
}
}
func TestRequirementEqual(t *testing.T) {
tests := []struct {
name string
@ -1079,3 +1099,114 @@ func TestRequirementEqual(t *testing.T) {
})
}
}
func TestMatchesNothing(t *testing.T) {
tests := []struct {
name string
selector string
set map[string]string
labelSelector Selector
want bool
}{
{
name: "MatchNothing should match Nothing()",
labelSelector: Nothing(),
want: true,
},
{
name: "MatchNothing should match sharedNothingSelector",
labelSelector: sharedNothingSelector,
want: true,
},
{
name: "MatchNothing should not match Everything()",
labelSelector: Everything(),
want: false,
},
{
name: "MatchNothing should not match sharedEverythingSelector",
labelSelector: sharedEverythingSelector,
want: false,
},
{
name: "MatchNothing should not match empty set",
set: map[string]string{},
want: false,
},
{
name: "MatchNothing should not match non-empty set",
set: map[string]string{"key": "value"},
want: false,
},
{
name: "MatchNothing should not match empty selector",
selector: "",
want: false,
},
{
name: "MatchNothing should not match non-empty selector - exists",
selector: "a",
want: false,
},
{
name: "MatchNothing should not match non-empty selector - not exists",
selector: "!a",
want: false,
},
{
name: "MatchNothing should not match non-empty selector - equals",
selector: "a=b",
want: false,
},
{
name: "MatchNothing should not match non-empty selector - not equals",
selector: "a!=b",
want: false,
},
{
name: "MatchNothing should not match non-empty selector - in",
selector: "a in (b)",
want: false,
},
{
name: "MatchNothing should not match non-empty selector - notin",
selector: "a notin (b)",
want: false,
},
{
name: "MatchNothing should not match non-empty selector - conflict exists and not exists",
selector: "a,!a",
want: false,
},
{
name: "MatchNothing should not match non-empty selector - conflict equals and not equals",
selector: "a=b,a!=b",
want: false,
},
{
name: "MatchNothing should not match non-empty selector - conflict in and notin",
selector: "a in (b),a notin (b)",
want: false,
},
}
for i := 0; i < len(tests); i++ {
if tests[i].labelSelector != nil {
expectMatchNothing(t, tests[i].labelSelector, tests[i].want)
} else if tests[i].set != nil {
expectMatchNothing(t, SelectorFromSet(tests[i].set), tests[i].want)
} else {
selector, err := Parse(tests[i].selector)
if err != nil {
t.Errorf("Unable to parse %v as a selector.\n", selector)
}
expectMatchNothing(t, selector, tests[i].want)
}
}
}
func expectMatchNothing(t *testing.T, selector Selector, want bool) {
if MatchesNothing(selector) != want {
t.Errorf("Wanted %s to MatchNothing '%t', but it did not.\n", selector, want)
}
}

View File

@ -29,10 +29,11 @@ import (
"sync/atomic"
"time"
"sigs.k8s.io/structured-merge-diff/v6/value"
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/apimachinery/pkg/util/json"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"sigs.k8s.io/structured-merge-diff/v4/value"
"k8s.io/klog/v2"
)
@ -53,6 +54,7 @@ type fieldInfo struct {
name string
nameValue reflect.Value
omitempty bool
omitzero func(dv reflect.Value) bool
}
type fieldsCacheMap map[structField]*fieldInfo
@ -376,19 +378,24 @@ func fieldInfoFromField(structType reflect.Type, field int) *fieldInfo {
typeField := structType.Field(field)
jsonTag := typeField.Tag.Get("json")
if len(jsonTag) == 0 {
// Make the first character lowercase.
if typeField.Name == "" {
if !typeField.Anonymous {
// match stdlib behavior for naming fields that don't specify a json tag name
info.name = typeField.Name
} else {
info.name = strings.ToLower(typeField.Name[:1]) + typeField.Name[1:]
}
} else {
items := strings.Split(jsonTag, ",")
info.name = items[0]
if len(info.name) == 0 && !typeField.Anonymous {
// match stdlib behavior for naming fields that don't specify a json tag name
info.name = typeField.Name
}
for i := range items {
if items[i] == "omitempty" {
if i > 0 && items[i] == "omitempty" {
info.omitempty = true
break
}
if i > 0 && items[i] == "omitzero" {
info.omitzero = value.OmitZeroFunc(typeField.Type)
}
}
}
@ -775,7 +782,7 @@ func pointerToUnstructured(sv, dv reflect.Value) error {
return toUnstructured(sv.Elem(), dv)
}
func isZero(v reflect.Value) bool {
func isEmpty(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.String:
return v.Len() == 0
@ -816,10 +823,14 @@ func structToUnstructured(sv, dv reflect.Value) error {
// This field should be skipped.
continue
}
if fieldInfo.omitempty && isZero(fv) {
if fieldInfo.omitempty && isEmpty(fv) {
// omitempty fields should be ignored.
continue
}
if fieldInfo.omitzero != nil && fieldInfo.omitzero(fv) {
// omitzero fields should be ignored
continue
}
if len(fieldInfo.name) == 0 {
// This field is inlined.
if err := toUnstructured(fv, dv); err != nil {

View File

@ -36,9 +36,9 @@ import (
"k8s.io/apimachinery/pkg/util/json"
"github.com/google/go-cmp/cmp"
fuzz "github.com/google/gofuzz"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sigs.k8s.io/randfill"
)
var simpleEquality = conversion.EqualitiesOrDie(
@ -640,9 +640,9 @@ func TestUnknownFields(t *testing.T) {
// with the various validation directives (Ignore, Warn, Strict)
func BenchmarkFromUnstructuredWithValidation(b *testing.B) {
re := regexp.MustCompile("^I$")
f := fuzz.NewWithSeed(1).NilChance(0.1).SkipFieldsWithPattern(re)
f := randfill.NewWithSeed(1).NilChance(0.1).SkipFieldsWithPattern(re)
iObj := &I{}
f.Fuzz(&iObj)
f.Fill(&iObj)
unstr, err := runtime.DefaultUnstructuredConverter.ToUnstructured(iObj)
if err != nil {
@ -998,3 +998,179 @@ func TestCustomToUnstructuredTopLevel(t *testing.T) {
})
}
}
type OmitemptyNameField struct {
I int `json:"omitempty"`
}
func TestOmitempty(t *testing.T) {
expected := `{"omitempty":0}`
o := &OmitemptyNameField{}
jsonData, err := json.Marshal(o)
if err != nil {
t.Fatal(err)
}
if e, a := expected, string(jsonData); e != a {
t.Fatalf("expected\n%s\ngot\n%s", e, a)
}
unstr, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&o)
if err != nil {
t.Fatal(err)
}
jsonUnstrData, err := json.Marshal(unstr)
if err != nil {
t.Fatal(err)
}
if e, a := expected, string(jsonUnstrData); e != a {
t.Fatalf("expected\n%s\ngot\n%s", e, a)
}
}
type InlineTestPrimitive struct {
NoNameTagPrimitive int64 `json:""`
NoNameTagInlinePrimitive int64 `json:",inline"`
NoNameTagOmitemptyPrimitive int64 `json:",omitempty"`
}
type InlineTestAnonymous struct {
NoTag
NoNameTag `json:""`
NameTag `json:"nameTagEmbedded"`
NoNameTagInline `json:",inline"`
NoNameTagOmitempty `json:",omitempty"`
}
type InlineTestNamed struct {
NoTag NoTag
NoNameTag NoNameTag `json:""`
NameTag NameTag `json:"nameTagEmbedded"`
NoNameTagInline NoNameTagInline `json:",inline"`
NoNameTagOmitempty NoNameTagOmitempty `json:",omitempty"`
}
type NoTag struct {
Data0 int `json:"data0"`
}
type NameTag struct {
Data1 int `json:"data1"`
}
type NoNameTag struct {
Data2 int `json:"data2"`
}
type NoNameTagInline struct {
Data3 int `json:"data3"`
}
type NoNameTagOmitempty struct {
Data4 int `json:"data4"`
}
func TestInline(t *testing.T) {
testcases := []struct {
name string
obj any
expect map[string]any
}{
{
name: "primitive-zero",
obj: &InlineTestPrimitive{},
expect: map[string]any{
"NoNameTagPrimitive": int64(0),
"NoNameTagInlinePrimitive": int64(0),
},
},
{
name: "primitive-set",
obj: &InlineTestPrimitive{
NoNameTagPrimitive: 1,
NoNameTagInlinePrimitive: 2,
NoNameTagOmitemptyPrimitive: 3,
},
expect: map[string]any{
"NoNameTagPrimitive": int64(1),
"NoNameTagInlinePrimitive": int64(2),
"NoNameTagOmitemptyPrimitive": int64(3),
},
},
{
name: "anonymous-zero",
obj: &InlineTestAnonymous{},
expect: map[string]any{
"data0": int64(0),
"data2": int64(0),
"data3": int64(0),
"data4": int64(0),
"nameTagEmbedded": map[string]any{"data1": int64(0)},
},
},
{
name: "anonymous-set",
obj: &InlineTestAnonymous{},
expect: map[string]any{
"data0": int64(0),
"data2": int64(0),
"data3": int64(0),
"data4": int64(0),
"nameTagEmbedded": map[string]any{"data1": int64(0)},
},
},
{
name: "named-zero",
obj: &InlineTestNamed{},
expect: map[string]any{
"NoTag": map[string]any{"data0": int64(0)},
"nameTagEmbedded": map[string]any{"data1": int64(0)},
"NoNameTag": map[string]any{"data2": int64(0)},
"NoNameTagInline": map[string]any{"data3": int64(0)},
"NoNameTagOmitempty": map[string]any{"data4": int64(0)},
},
},
{
name: "named-set",
obj: &InlineTestNamed{
NoTag: NoTag{Data0: 10},
NameTag: NameTag{Data1: 11},
NoNameTag: NoNameTag{Data2: 12},
NoNameTagInline: NoNameTagInline{Data3: 13},
NoNameTagOmitempty: NoNameTagOmitempty{Data4: 14},
},
expect: map[string]any{
"NoTag": map[string]any{"data0": int64(10)},
"nameTagEmbedded": map[string]any{"data1": int64(11)},
"NoNameTag": map[string]any{"data2": int64(12)},
"NoNameTagInline": map[string]any{"data3": int64(13)},
"NoNameTagOmitempty": map[string]any{"data4": int64(14)},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
defer func() {
// handle panics
if err := recover(); err != nil {
t.Fatal(err)
}
}()
// Check the expectation against stdlib
jsonData, err := json.Marshal(tc.obj)
if err != nil {
t.Fatal(err)
}
jsonUnstr := map[string]any{}
if err := json.Unmarshal(jsonData, &jsonUnstr); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(tc.expect, jsonUnstr) {
t.Fatal(cmp.Diff(tc.expect, jsonUnstr))
}
// Check the expectation against DefaultUnstructuredConverter.ToUnstructured
unstr, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.obj)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(tc.expect, unstr) {
t.Fatal(cmp.Diff(tc.expect, unstr))
}
})
}
}

View File

@ -0,0 +1,359 @@
/*
Copyright 2025 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.
*/
// These tests are in a separate package to break cyclic dependency in tests.
// Unstructured type depends on unstructured converter package but we want to test how the converter handles
// the Unstructured type so we need to import both.
package runtime
import (
encodingjson "encoding/json"
"fmt"
"reflect"
"testing"
"time"
"k8s.io/apimachinery/pkg/util/json"
"github.com/google/go-cmp/cmp"
)
type ZeroParent struct {
Int int `json:"int,omitzero"`
IntP *int `json:"intP,omitzero"`
String string `json:"string,omitzero"`
StringP *string `json:"stringP,omitzero"`
Bool bool `json:"bool,omitzero"`
BoolP bool `json:"boolP,omitzero"`
Slice []int `json:"slice,omitzero"`
SliceP *[]int `json:"sliceP,omitzero"`
Map map[string]int `json:"map,omitzero"`
MapP *map[string]int `json:"mapP,omitzero"`
Struct ZeroChild `json:"struct,omitzero"`
StructP *ZeroChild `json:"structP,omitzero"`
CustomPrimitive ZeroCustomPrimitive `json:"customPrimitive,omitzero"`
CustomPrimitiveP *ZeroCustomPrimitiveP `json:"customPrimitiveP,omitzero"`
CustomStruct ZeroCustomStruct `json:"customStruct,omitzero"`
CustomStructP *ZeroCustomStructP `json:"customStructP,omitzero"`
CustomPPrimitive ZeroCustomPPrimitive `json:"customPPrimitive,omitzero"`
CustomPPrimitiveP *ZeroCustomPPrimitiveP `json:"customPPrimitiveP,omitzero"`
CustomPStruct ZeroCustomPStruct `json:"customPStruct,omitzero"`
CustomPStructP *ZeroCustomPStructP `json:"customPStructP,omitzero"`
}
type ZeroChild struct {
Data int `json:"data"`
}
type ZeroCustomPrimitive int
func (z ZeroCustomPrimitive) IsZero() bool {
return z == 42
}
type ZeroCustomPrimitiveP int
func (z ZeroCustomPrimitiveP) IsZero() bool {
return z == 42
}
type ZeroCustomStruct struct {
Data int `json:"data"`
}
func (z ZeroCustomStruct) IsZero() bool {
return z.Data == 42
}
type ZeroCustomStructP struct {
Data int `json:"data"`
}
func (z ZeroCustomStructP) IsZero() bool {
return z.Data == 42
}
type ZeroCustomPPrimitive int
func (z *ZeroCustomPPrimitive) IsZero() bool {
return *z == 42
}
type ZeroCustomPPrimitiveP int
func (z *ZeroCustomPPrimitiveP) IsZero() bool {
return *z == 42
}
type ZeroCustomPStruct struct {
Data int `json:"data"`
}
func (z *ZeroCustomPStruct) IsZero() bool {
return z.Data == 42
}
type ZeroCustomPStructP struct {
Data int `json:"data"`
}
func (z *ZeroCustomPStructP) IsZero() bool {
return z.Data == 42
}
func TestOmitZero2(t *testing.T) {
testcases := []struct {
name string
obj any
expect map[string]any
}{
{
name: "emptyzero",
obj: &ZeroParent{},
expect: map[string]any{
"customPPrimitive": int64(0),
"customPStruct": map[string]any{"data": int64(0)},
"customPrimitive": int64(0),
"customStruct": map[string]any{"data": int64(0)},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
jsonData, err := json.Marshal(tc.obj)
if err != nil {
t.Fatal(err)
}
jsonUnstructured := map[string]any{}
if err := json.Unmarshal(jsonData, &jsonUnstructured); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(jsonUnstructured, tc.expect) {
t.Fatal(cmp.Diff(tc.expect, jsonUnstructured))
}
unstr, err := DefaultUnstructuredConverter.ToUnstructured(tc.obj)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(unstr, tc.expect) {
t.Fatal(cmp.Diff(tc.expect, unstr))
}
})
}
}
type NonZeroStruct struct{}
func (nzs NonZeroStruct) IsZero() bool {
return false
}
type NoPanicStruct struct {
Int int `json:"int,omitzero"`
}
func (nps *NoPanicStruct) IsZero() bool {
return nps.Int != 0
}
type isZeroer interface {
IsZero() bool
}
type OptionalsZero struct {
Sr string `json:"sr"`
So string `json:"so,omitzero"`
Sw string `json:"-"`
Ir int `json:"omitzero"` // actually named omitzero, not an option
Io int `json:"io,omitzero"`
Slr []string `json:"slr,random"` //nolint:staticcheck // SA5008
Slo []string `json:"slo,omitzero"`
SloNonNil []string `json:"slononnil,omitzero"`
Mr map[string]any `json:"mr"`
Mo map[string]any `json:",omitzero"`
Moo map[string]any `json:"moo,omitzero"`
Fr float64 `json:"fr"`
Fo float64 `json:"fo,omitzero"`
Foo float64 `json:"foo,omitzero"`
Foo2 [2]float64 `json:"foo2,omitzero"`
Br bool `json:"br"`
Bo bool `json:"bo,omitzero"`
Ur uint `json:"ur"`
Uo uint `json:"uo,omitzero"`
Str struct{} `json:"str"`
Sto struct{} `json:"sto,omitzero"`
Time time.Time `json:"time,omitzero"`
TimeLocal time.Time `json:"timelocal,omitzero"`
Nzs NonZeroStruct `json:"nzs,omitzero"`
NilIsZeroer isZeroer `json:"niliszeroer,omitzero"` // nil interface
NonNilIsZeroer isZeroer `json:"nonniliszeroer,omitzero"` // non-nil interface
NoPanicStruct0 isZeroer `json:"nps0,omitzero"` // non-nil interface with nil pointer
NoPanicStruct1 isZeroer `json:"nps1,omitzero"` // non-nil interface with non-nil pointer
NoPanicStruct2 *NoPanicStruct `json:"nps2,omitzero"` // nil pointer
NoPanicStruct3 *NoPanicStruct `json:"nps3,omitzero"` // non-nil pointer
NoPanicStruct4 NoPanicStruct `json:"nps4,omitzero"` // concrete type
}
func TestOmitZero(t *testing.T) {
const want = `{
"Mo": {},
"br": false,
"fr": 0,
"mr": {},
"nps1": {},
"nps3": {},
"nps4": {},
"nzs": {},
"omitzero": 0,
"slononnil": [],
"slr": null,
"sr": "",
"str": {},
"ur": 0
}`
var o OptionalsZero
o.Sw = "something"
o.SloNonNil = make([]string, 0)
o.Mr = map[string]any{}
o.Mo = map[string]any{}
o.Foo = -0
o.Foo2 = [2]float64{+0, -0}
o.TimeLocal = time.Time{}.Local()
o.NonNilIsZeroer = time.Time{}
o.NoPanicStruct0 = (*NoPanicStruct)(nil)
o.NoPanicStruct1 = &NoPanicStruct{}
o.NoPanicStruct3 = &NoPanicStruct{}
unstr, err := DefaultUnstructuredConverter.ToUnstructured(&o)
if err != nil {
t.Fatal(err)
}
got, err := encodingjson.MarshalIndent(unstr, "", " ")
if err != nil {
t.Fatalf("MarshalIndent error: %v", err)
}
if got := string(got); got != want {
t.Errorf("MarshalIndent:\n\tgot: %s\n\twant: %s\n", got, want)
}
}
func TestOmitZeroMap(t *testing.T) {
const want = `{
"foo": {
"br": false,
"fr": 0,
"mr": null,
"nps4": {},
"nzs": {},
"omitzero": 0,
"slr": null,
"sr": "",
"str": {},
"ur": 0
}
}`
m := map[string]OptionalsZero{"foo": {}}
unstr, err := DefaultUnstructuredConverter.ToUnstructured(&m)
if err != nil {
t.Fatal(err)
}
got, err := encodingjson.MarshalIndent(unstr, "", " ")
if err != nil {
t.Fatalf("MarshalIndent error: %v", err)
}
if got := string(got); got != want {
fmt.Println(got)
t.Errorf("MarshalIndent:\n\tgot: %s\n\twant: %s\n", got, want)
}
}
type OptionalsEmptyZero struct {
Sr string `json:"sr"`
So string `json:"so,omitempty,omitzero"`
Sw string `json:"-"`
Io int `json:"io,omitempty,omitzero"`
Slr []string `json:"slr,random"` //nolint:staticcheck // SA5008
Slo []string `json:"slo,omitempty,omitzero"`
SloNonNil []string `json:"slononnil,omitempty,omitzero"`
Mr map[string]any `json:"mr"`
Mo map[string]any `json:",omitempty,omitzero"`
Fr float64 `json:"fr"`
Fo float64 `json:"fo,omitempty,omitzero"`
Br bool `json:"br"`
Bo bool `json:"bo,omitempty,omitzero"`
Ur uint `json:"ur"`
Uo uint `json:"uo,omitempty,omitzero"`
Str struct{} `json:"str"`
Sto struct{} `json:"sto,omitempty,omitzero"`
Time time.Time `json:"time,omitempty,omitzero"`
Nzs NonZeroStruct `json:"nzs,omitempty,omitzero"`
}
func TestOmitEmptyZero(t *testing.T) {
const want = `{
"br": false,
"fr": 0,
"mr": {},
"nzs": {},
"slr": null,
"sr": "",
"str": {},
"ur": 0
}`
var o OptionalsEmptyZero
o.Sw = "something"
o.SloNonNil = make([]string, 0)
o.Mr = map[string]any{}
o.Mo = map[string]any{}
unstr, err := DefaultUnstructuredConverter.ToUnstructured(&o)
if err != nil {
t.Fatal(err)
}
got, err := encodingjson.MarshalIndent(unstr, "", " ")
if err != nil {
t.Fatalf("MarshalIndent error: %v", err)
}
if got := string(got); got != want {
t.Errorf("MarshalIndent:\n\tgot: %s\n\twant: %s\n", got, want)
}
}

View File

@ -259,6 +259,7 @@ type ObjectDefaulter interface {
type ObjectVersioner interface {
ConvertToVersion(in Object, gv GroupVersioner) (out Object, err error)
PrioritizedVersionsForGroup(group string) []schema.GroupVersion
}
// ObjectConvertor converts an object to a different version.
@ -384,3 +385,9 @@ type Unstructured interface {
// If the items passed to fn are not retained, or are retained for the same duration, use EachListItem instead for memory efficiency.
EachListItemWithAlloc(func(Object) error) error
}
// ApplyConfiguration is an interface that root apply configuration types implement.
type ApplyConfiguration interface {
// IsApplyConfiguration is implemented if the object is the root of an apply configuration.
IsApplyConfiguration()
}

View File

@ -17,15 +17,18 @@ limitations under the License.
package runtime
import (
"context"
"fmt"
"reflect"
"strings"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/naming"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// Scheme defines methods for serializing and deserializing API objects, a type
@ -68,6 +71,12 @@ type Scheme struct {
// the provided object must be a pointer.
defaulterFuncs map[reflect.Type]func(interface{})
// validationFuncs is a map to funcs to be called with an object to perform validation.
// The provided object must be a pointer.
// If oldObject is non-nil, update validation is performed and may perform additional
// validation such as transition rules and immutability checks.
validationFuncs map[reflect.Type]func(ctx context.Context, op operation.Operation, object, oldObject interface{}) field.ErrorList
// converter stores all registered conversion functions. It also has
// default converting behavior.
converter *conversion.Converter
@ -96,6 +105,7 @@ func NewScheme() *Scheme {
unversionedKinds: map[string]reflect.Type{},
fieldLabelConversionFuncs: map[schema.GroupVersionKind]FieldLabelConversionFunc{},
defaulterFuncs: map[reflect.Type]func(interface{}){},
validationFuncs: map[reflect.Type]func(ctx context.Context, op operation.Operation, object, oldObject interface{}) field.ErrorList{},
versionPriority: map[string][]string{},
schemeName: naming.GetNameFromCallsite(internalPackages...),
}
@ -347,6 +357,35 @@ func (s *Scheme) Default(src Object) {
}
}
// AddValidationFunc registered a function that can validate the object, and
// oldObject. These functions will be invoked when Validate() or ValidateUpdate()
// is called. The function will never be called unless the validated object
// matches srcType. If this function is invoked twice with the same srcType, the
// fn passed to the later call will be used instead.
func (s *Scheme) AddValidationFunc(srcType Object, fn func(ctx context.Context, op operation.Operation, object, oldObject interface{}) field.ErrorList) {
s.validationFuncs[reflect.TypeOf(srcType)] = fn
}
// Validate validates the provided Object according to the generated declarative validation code.
// WARNING: This does not validate all objects! The handwritten validation code in validation.go
// is not run when this is called. Only the generated zz_generated.validations.go validation code is run.
func (s *Scheme) Validate(ctx context.Context, options []string, object Object, subresources ...string) field.ErrorList {
if fn, ok := s.validationFuncs[reflect.TypeOf(object)]; ok {
return fn(ctx, operation.Operation{Type: operation.Create, Request: operation.Request{Subresources: subresources}, Options: options}, object, nil)
}
return nil
}
// ValidateUpdate validates the provided object and oldObject according to the generated declarative validation code.
// WARNING: This does not validate all objects! The handwritten validation code in validation.go
// is not run when this is called. Only the generated zz_generated.validations.go validation code is run.
func (s *Scheme) ValidateUpdate(ctx context.Context, options []string, object, oldObject Object, subresources ...string) field.ErrorList {
if fn, ok := s.validationFuncs[reflect.TypeOf(object)]; ok {
return fn(ctx, operation.Operation{Type: operation.Update, Request: operation.Request{Subresources: subresources}, Options: options}, object, oldObject)
}
return nil
}
// Convert will attempt to convert in into out. Both must be pointers. For easy
// testing of conversion functions. Returns an error if the conversion isn't
// possible. You can call this with types that haven't been registered (for example,
@ -704,3 +743,67 @@ func (s *Scheme) Name() string {
// internalPackages are packages that ignored when creating a default reflector name. These packages are in the common
// call chains to NewReflector, so they'd be low entropy names for reflectors
var internalPackages = []string{"k8s.io/apimachinery/pkg/runtime/scheme.go"}
// ToOpenAPIDefinitionName returns the REST-friendly OpenAPI definition name known type identified by groupVersionKind.
// If the groupVersionKind does not identify a known type, an error is returned.
// The Version field of groupVersionKind is required, and the Group and Kind fields are required for unstructured.Unstructured
// types. If a required field is empty, an error is returned.
//
// The OpenAPI definition name is the canonical name of the type, with the group and version removed.
// For example, the OpenAPI definition name of Pod is `io.k8s.api.core.v1.Pod`.
//
// A known type that is registered as an unstructured.Unstructured type is treated as a custom resource and
// which has an OpenAPI definition name of the form `<reversed-group>.<version.<kind>`.
// For example, the OpenAPI definition name of `group: stable.example.com, version: v1, kind: Pod` is
// `com.example.stable.v1.Pod`.
func (s *Scheme) ToOpenAPIDefinitionName(groupVersionKind schema.GroupVersionKind) (string, error) {
if groupVersionKind.Version == "" { // Empty version is not allowed by New() so check it first to avoid a panic.
return "", fmt.Errorf("version is required on all types: %v", groupVersionKind)
}
example, err := s.New(groupVersionKind)
if err != nil {
return "", err
}
if _, ok := example.(Unstructured); ok {
if groupVersionKind.Group == "" || groupVersionKind.Kind == "" {
return "", fmt.Errorf("unable to convert GroupVersionKind with empty fields to unstructured type to an OpenAPI definition name: %v", groupVersionKind)
}
return reverseParts(groupVersionKind.Group) + "." + groupVersionKind.Version + "." + groupVersionKind.Kind, nil
}
rtype := reflect.TypeOf(example).Elem()
name := toOpenAPIDefinitionName(rtype.PkgPath() + "." + rtype.Name())
return name, nil
}
// toOpenAPIDefinitionName converts Golang package/type canonical name into REST friendly OpenAPI name.
// Input is expected to be `PkgPath + "." TypeName.
//
// Examples of REST friendly OpenAPI name:
//
// Input: k8s.io/api/core/v1.Pod
// Output: io.k8s.api.core.v1.Pod
//
// Input: k8s.io/api/core/v1
// Output: io.k8s.api.core.v1
//
// Input: csi.storage.k8s.io/v1alpha1.CSINodeInfo
// Output: io.k8s.storage.csi.v1alpha1.CSINodeInfo
//
// Note that this is a copy of ToRESTFriendlyName from k8s.io/kube-openapi/pkg/util. It is duplicated here to avoid
// a dependency on kube-openapi.
func toOpenAPIDefinitionName(name string) string {
nameParts := strings.Split(name, "/")
// Reverse first part. e.g., io.k8s... instead of k8s.io...
if len(nameParts) > 0 && strings.Contains(nameParts[0], ".") {
nameParts[0] = reverseParts(nameParts[0])
}
return strings.Join(nameParts, ".")
}
func reverseParts(dotSeparatedName string) string {
parts := strings.Split(dotSeparatedName, ".")
for i, j := 0, len(parts)-1; i < j; i, j = i+1, j-1 {
parts[i], parts[j] = parts[j], parts[i]
}
return strings.Join(parts, ".")
}

View File

@ -17,12 +17,17 @@ limitations under the License.
package runtime_test
import (
"context"
"fmt"
"reflect"
"slices"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@ -30,6 +35,7 @@ import (
runtimetesting "k8s.io/apimachinery/pkg/runtime/testing"
"k8s.io/apimachinery/pkg/util/diff"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
)
type testConversions struct {
@ -1009,3 +1015,165 @@ func TestMetaValuesUnregisteredConvert(t *testing.T) {
t.Errorf("Expected %v, got %v", e, a)
}
}
func TestRegisterValidate(t *testing.T) {
invalidValue := field.Invalid(field.NewPath("testString"), "", "Invalid value").WithOrigin("invalid-value")
invalidLength := field.Invalid(field.NewPath("testString"), "", "Invalid length").WithOrigin("invalid-length")
invalidStatusErr := field.Invalid(field.NewPath("testString"), "", "Invalid condition").WithOrigin("invalid-condition")
invalidIfOptionErr := field.Invalid(field.NewPath("testString"), "", "Invalid when option is set").WithOrigin("invalid-when-option-set")
testCases := []struct {
name string
object runtime.Object
oldObject runtime.Object
subresource []string
options []string
expected field.ErrorList
}{
{
name: "single error",
object: &TestType1{},
expected: field.ErrorList{invalidValue},
},
{
name: "multiple errors",
object: &TestType2{},
expected: field.ErrorList{invalidValue, invalidLength},
},
{
name: "update error",
object: &TestType2{},
oldObject: &TestType2{},
expected: field.ErrorList{invalidLength},
},
{
name: "options error",
object: &TestType1{},
options: []string{"option1"},
expected: field.ErrorList{invalidIfOptionErr},
},
{
name: "subresource error",
object: &TestType1{},
subresource: []string{"status"},
expected: field.ErrorList{invalidStatusErr},
},
}
s := runtime.NewScheme()
ctx := context.Background()
// register multiple types for testing to ensure registration is working as expected
s.AddValidationFunc(&TestType1{}, func(ctx context.Context, op operation.Operation, object, oldObject interface{}) field.ErrorList {
if op.HasOption("option1") {
return field.ErrorList{invalidIfOptionErr}
}
if slices.Equal(op.Request.Subresources, []string{"status"}) {
return field.ErrorList{invalidStatusErr}
}
return field.ErrorList{invalidValue}
})
s.AddValidationFunc(&TestType2{}, func(ctx context.Context, op operation.Operation, object, oldObject interface{}) field.ErrorList {
if oldObject != nil {
return field.ErrorList{invalidLength}
}
return field.ErrorList{invalidValue, invalidLength}
})
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var results field.ErrorList
if tc.oldObject == nil {
results = s.Validate(ctx, tc.options, tc.object, tc.subresource...)
} else {
results = s.ValidateUpdate(ctx, tc.options, tc.object, tc.oldObject, tc.subresource...)
}
matcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin()
matcher.Test(t, tc.expected, results)
})
}
}
type TestType1 struct {
Version string `json:"apiVersion,omitempty"`
Kind string `json:"kind,omitempty"`
TestString string `json:"testString"`
}
func (TestType1) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind }
func (TestType1) DeepCopyObject() runtime.Object { return nil }
type TestType2 struct {
Version string `json:"apiVersion,omitempty"`
Kind string `json:"kind,omitempty"`
TestString string `json:"testString"`
}
func (TestType2) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind }
func (TestType2) DeepCopyObject() runtime.Object { return nil }
func TestToOpenAPIDefinitionName(t *testing.T) {
testCases := []struct {
name string
registerGvk *schema.GroupVersionKind // defaults to gvk unless set
registerObj runtime.Object
gvk schema.GroupVersionKind
out string
wantErr error
}{
{
name: "unstructured type",
registerObj: &unstructured.Unstructured{},
gvk: schema.GroupVersionKind{Group: "stable.example.com", Version: "v1", Kind: "CronTab"},
out: "com.example.stable.v1.CronTab",
},
{
name: "unregistered type: empty group",
registerObj: &unstructured.Unstructured{},
gvk: schema.GroupVersionKind{Version: "v1", Kind: "CronTab"},
wantErr: fmt.Errorf("unable to convert GroupVersionKind with empty fields to unstructured type to an OpenAPI definition name: %v", schema.GroupVersionKind{Version: "v1", Kind: "CronTab"}),
},
{
name: "unregistered type: empty version",
registerObj: &unstructured.Unstructured{},
registerGvk: &schema.GroupVersionKind{Group: "stable.example.com", Version: "v1", Kind: "CronTab"},
gvk: schema.GroupVersionKind{Group: "stable.example.com", Kind: "CronTab"},
wantErr: fmt.Errorf("version is required on all types: %v", schema.GroupVersionKind{Group: "stable.example.com", Kind: "CronTab"}),
},
{
name: "unregistered type: empty kind",
registerObj: &unstructured.Unstructured{},
gvk: schema.GroupVersionKind{Group: "stable.example.com", Version: "v1"},
wantErr: fmt.Errorf("unable to convert GroupVersionKind with empty fields to unstructured type to an OpenAPI definition name: %v", schema.GroupVersionKind{Group: "stable.example.com", Version: "v1"}),
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
if test.registerGvk == nil {
test.registerGvk = &test.gvk
}
scheme := runtime.NewScheme()
scheme.AddKnownTypeWithName(*test.registerGvk, test.registerObj)
utilruntime.Must(runtimetesting.RegisterConversions(scheme))
out, err := scheme.ToOpenAPIDefinitionName(test.gvk)
if test.wantErr != nil {
if err == nil || err.Error() != test.wantErr.Error() {
t.Errorf("expected error: %v but got %v", test.wantErr, err)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if out != test.out {
t.Errorf("expected %s, got %s", test.out, out)
}
})
}
}

View File

@ -152,10 +152,6 @@ func (s *serializer) encode(mode modes.EncMode, obj runtime.Object, w io.Writer)
v = u.UnstructuredContent()
}
if err := modes.RejectCustomMarshalers(v); err != nil {
return err
}
if _, err := w.Write(selfDescribedCBOR); err != nil {
return err
}
@ -270,8 +266,6 @@ func (s *serializer) unmarshal(data []byte, into interface{}) (strict, lax error
}
}()
into = &content
} else if err := modes.RejectCustomMarshalers(into); err != nil {
return nil, err
}
if !s.options.strict {

View File

@ -209,38 +209,38 @@ func TestEncode(t *testing.T) {
},
},
{
name: "unsupported marshaler",
name: "text marshaler",
in: &textMarshalerObject{},
assertOnWriter: func() (io.Writer, func(*testing.T)) {
var b bytes.Buffer
return &b, func(t *testing.T) {
if b.Len() != 0 {
t.Errorf("expected no bytes to be written, got %d", b.Len())
if diff := cmp.Diff(b.Bytes(), []byte{0xd9, 0xd9, 0xf7, 0x64, 't', 'e', 's', 't'}); diff != "" {
t.Errorf("unexpected diff:\n%s", diff)
}
}
},
assertOnError: func(t *testing.T, err error) {
if want := "unable to serialize *cbor.textMarshalerObject: *cbor.textMarshalerObject implements encoding.TextMarshaler without corresponding cbor interface"; err == nil || err.Error() != want {
t.Errorf("expected error %q, got: %v", want, err)
if err != nil {
t.Errorf("expected nil error, got: %v", err)
}
},
},
{
name: "unsupported marshaler within unstructured content",
name: "text marshaler within unstructured content",
in: &unstructured.Unstructured{
Object: map[string]interface{}{"": textMarshalerObject{}},
},
assertOnWriter: func() (io.Writer, func(*testing.T)) {
var b bytes.Buffer
return &b, func(t *testing.T) {
if b.Len() != 0 {
t.Errorf("expected no bytes to be written, got %d", b.Len())
if diff := cmp.Diff(b.Bytes(), []byte{0xd9, 0xd9, 0xf7, 0xa1, 0x40, 0x64, 't', 'e', 's', 't'}); diff != "" {
t.Errorf("unexpected diff:\n%s", diff)
}
}
},
assertOnError: func(t *testing.T, err error) {
if want := "unable to serialize map[string]interface {}: cbor.textMarshalerObject implements encoding.TextMarshaler without corresponding cbor interface"; err == nil || err.Error() != want {
t.Errorf("expected error %q, got: %v", want, err)
if err != nil {
t.Errorf("expected nil error, got: %v", err)
}
},
},
@ -689,19 +689,6 @@ func TestDecode(t *testing.T) {
}
},
},
{
name: "into unsupported marshaler",
data: []byte("\xa0"),
into: &textMarshalerObject{},
metaFactory: stubMetaFactory{gvk: &schema.GroupVersionKind{}},
typer: stubTyper{gvks: []schema.GroupVersionKind{{Version: "v", Kind: "k"}}},
expectedGVK: &schema.GroupVersionKind{Version: "v", Kind: "k"},
assertOnError: func(t *testing.T, err error) {
if want := "unable to serialize *cbor.textMarshalerObject: *cbor.textMarshalerObject implements encoding.TextMarshaler without corresponding cbor interface"; err == nil || err.Error() != want {
t.Errorf("expected error %q, got: %v", want, err)
}
},
},
} {
t.Run(tc.name, func(t *testing.T) {
s := newSerializer(tc.metaFactory, tc.creater, tc.typer, tc.options...)
@ -731,7 +718,7 @@ func (textMarshalerObject) DeepCopyObject() runtime.Object {
}
func (textMarshalerObject) MarshalText() ([]byte, error) {
return nil, nil
return []byte("test"), nil
}
func TestMetaFactoryInterpret(t *testing.T) {

View File

@ -25,31 +25,13 @@ import (
// Marshal serializes a value to CBOR. If there is more than one way to encode the value, it will
// make the same choice as the CBOR implementation of runtime.Serializer.
//
// Note: Support for CBOR is at an alpha stage. If the value (or, for composite types, any of its
// nested values) implement any of the interfaces encoding.TextMarshaler, encoding.TextUnmarshaler,
// encoding/json.Marshaler, or encoding/json.Unmarshaler, a non-nil error will be returned unless
// the value also implements the corresponding CBOR interfaces. This limitation will ultimately be
// removed in favor of automatic transcoding to CBOR.
func Marshal(src interface{}) ([]byte, error) {
if err := modes.RejectCustomMarshalers(src); err != nil {
return nil, err
}
func Marshal(src any) ([]byte, error) {
return modes.Encode.Marshal(src)
}
// Unmarshal deserializes from CBOR into an addressable value. If there is more than one way to
// unmarshal a value, it will make the same choice as the CBOR implementation of runtime.Serializer.
//
// Note: Support for CBOR is at an alpha stage. If the value (or, for composite types, any of its
// nested values) implement any of the interfaces encoding.TextMarshaler, encoding.TextUnmarshaler,
// encoding/json.Marshaler, or encoding/json.Unmarshaler, a non-nil error will be returned unless
// the value also implements the corresponding CBOR interfaces. This limitation will ultimately be
// removed in favor of automatic transcoding to CBOR.
func Unmarshal(src []byte, dst interface{}) error {
if err := modes.RejectCustomMarshalers(dst); err != nil {
return err
}
func Unmarshal(src []byte, dst any) error {
return modes.Decode.Unmarshal(src, dst)
}

View File

@ -1,81 +0,0 @@
/*
Copyright 2024 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 direct_test
import (
"encoding"
"encoding/json"
"fmt"
"reflect"
"testing"
"k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct"
)
var _ json.Marshaler = CustomJSONMarshaler{}
type CustomJSONMarshaler struct{}
func (CustomJSONMarshaler) MarshalJSON() ([]byte, error) {
panic("unimplemented")
}
var _ json.Unmarshaler = CustomJSONUnmarshaler{}
type CustomJSONUnmarshaler struct{}
func (CustomJSONUnmarshaler) UnmarshalJSON([]byte) error {
panic("unimplemented")
}
var _ encoding.TextMarshaler = CustomTextMarshaler{}
type CustomTextMarshaler struct{}
func (CustomTextMarshaler) MarshalText() ([]byte, error) {
panic("unimplemented")
}
var _ encoding.TextUnmarshaler = CustomTextUnmarshaler{}
type CustomTextUnmarshaler struct{}
func (CustomTextUnmarshaler) UnmarshalText([]byte) error {
panic("unimplemented")
}
func TestRejectsCustom(t *testing.T) {
for _, tc := range []struct {
value interface{}
iface reflect.Type
}{
{value: CustomJSONMarshaler{}, iface: reflect.TypeFor[json.Marshaler]()},
{value: CustomJSONUnmarshaler{}, iface: reflect.TypeFor[json.Unmarshaler]()},
{value: CustomTextMarshaler{}, iface: reflect.TypeFor[encoding.TextMarshaler]()},
{value: CustomTextUnmarshaler{}, iface: reflect.TypeFor[encoding.TextUnmarshaler]()},
} {
t.Run(fmt.Sprintf("%T", tc.value), func(t *testing.T) {
want := fmt.Sprintf("unable to serialize %T: %T implements %s without corresponding cbor interface", tc.value, tc.value, tc.iface.String())
if _, err := direct.Marshal(tc.value); err == nil || err.Error() != want {
t.Errorf("want error: %q, got: %v", want, err)
}
if err := direct.Unmarshal(nil, tc.value); err == nil || err.Error() != want {
t.Errorf("want error: %q, got: %v", want, err)
}
})
}
}

View File

@ -1,422 +0,0 @@
/*
Copyright 2024 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 modes
import (
"encoding"
"encoding/json"
"errors"
"fmt"
"reflect"
"sync"
"github.com/fxamacker/cbor/v2"
)
// Returns a non-nil error if and only if the argument's type (or one of its component types, for
// composite types) implements json.Marshaler or encoding.TextMarshaler without also implementing
// cbor.Marshaler and likewise for the respective Unmarshaler interfaces.
//
// This is a temporary, graduation-blocking restriction and will be removed in favor of automatic
// transcoding between CBOR and JSON/text for these types. This restriction allows CBOR to be
// exercised for in-tree and unstructured types while mitigating the risk of mangling out-of-tree
// types in client programs.
func RejectCustomMarshalers(v interface{}) error {
if v == nil {
return nil
}
rv := reflect.ValueOf(v)
if err := marshalerCache.getChecker(rv.Type()).check(rv, maxDepth); err != nil {
return fmt.Errorf("unable to serialize %T: %w", v, err)
}
if err := unmarshalerCache.getChecker(rv.Type()).check(rv, maxDepth); err != nil {
return fmt.Errorf("unable to serialize %T: %w", v, err)
}
return nil
}
// Recursion depth is limited as a basic mitigation against cyclic objects. Objects created by the
// decoder shouldn't be able to contain cycles, but practically any object can be passed to the
// encoder.
var errMaxDepthExceeded = errors.New("object depth exceeds limit (possible cycle?)")
// The JSON encoder begins detecting cycles after depth 1000. Use a generous limit here, knowing
// that it can might deeply nested acyclic objects. The limit will be removed along with the rest of
// this mechanism.
const maxDepth = 2048
var marshalerCache = checkers{
cborInterface: reflect.TypeFor[cbor.Marshaler](),
nonCBORInterfaces: []reflect.Type{
reflect.TypeFor[json.Marshaler](),
reflect.TypeFor[encoding.TextMarshaler](),
},
}
var unmarshalerCache = checkers{
cborInterface: reflect.TypeFor[cbor.Unmarshaler](),
nonCBORInterfaces: []reflect.Type{
reflect.TypeFor[json.Unmarshaler](),
reflect.TypeFor[encoding.TextUnmarshaler](),
},
assumeAddressableValues: true,
}
// checker wraps a function for dynamically checking a value of a specific type for custom JSON
// behaviors not matched by a custom CBOR behavior.
type checker struct {
// check returns a non-nil error if the given value might be marshalled to or from CBOR
// using the default behavior for its kind, but marshalled to or from JSON using custom
// behavior.
check func(rv reflect.Value, depth int) error
// safe returns true if all values of this type are safe from mismatched custom marshalers.
safe func() bool
}
// TODO: stale
// Having a single addressable checker for comparisons lets us prune and collapse parts of the
// object traversal that are statically known to be safe. Depending on the type, it may be
// unnecessary to inspect each value of that type. For example, no value of the built-in type bool
// can implement json.Marshaler (a named type whose underlying type is bool could, but it is a
// distinct type from bool).
var noop = checker{
safe: func() bool {
return true
},
check: func(rv reflect.Value, depth int) error {
return nil
},
}
type checkers struct {
m sync.Map // reflect.Type => *checker
cborInterface reflect.Type
nonCBORInterfaces []reflect.Type
assumeAddressableValues bool
}
func (cache *checkers) getChecker(rt reflect.Type) checker {
if ptr, ok := cache.m.Load(rt); ok {
return *ptr.(*checker)
}
return cache.getCheckerInternal(rt, nil)
}
// linked list node representing the path from a composite type to an element type
type path struct {
Type reflect.Type
Parent *path
}
func (p path) cyclic(rt reflect.Type) bool {
for ancestor := &p; ancestor != nil; ancestor = ancestor.Parent {
if ancestor.Type == rt {
return true
}
}
return false
}
func (cache *checkers) getCheckerInternal(rt reflect.Type, parent *path) (c checker) {
// Store a placeholder cache entry first to handle cyclic types.
var wg sync.WaitGroup
wg.Add(1)
defer wg.Done()
placeholder := checker{
safe: func() bool {
wg.Wait()
return c.safe()
},
check: func(rv reflect.Value, depth int) error {
wg.Wait()
return c.check(rv, depth)
},
}
if actual, loaded := cache.m.LoadOrStore(rt, &placeholder); loaded {
// Someone else stored an entry for this type, use it.
return *actual.(*checker)
}
// Take a nonreflective path for the unstructured container types. They're common and
// usually nested inside one another.
switch rt {
case reflect.TypeFor[map[string]interface{}](), reflect.TypeFor[[]interface{}]():
return checker{
safe: func() bool {
return false
},
check: func(rv reflect.Value, depth int) error {
return checkUnstructuredValue(cache, rv.Interface(), depth)
},
}
}
// It's possible that one of the relevant interfaces is implemented on a type with a pointer
// receiver, but that a particular value of that type is not addressable. For example:
//
// func (Foo) MarshalText() ([]byte, error) { ... }
// func (*Foo) MarshalCBOR() ([]byte, error) { ... }
//
// Both methods are in the method set of *Foo, but the method set of Foo contains only
// MarshalText.
//
// Both the unmarshaler and marshaler checks assume that methods implementing a JSON or text
// interface with a pointer receiver are always accessible. Only the unmarshaler check
// assumes that CBOR methods with pointer receivers are accessible.
if rt.Implements(cache.cborInterface) {
return noop
}
for _, unsafe := range cache.nonCBORInterfaces {
if rt.Implements(unsafe) {
err := fmt.Errorf("%v implements %v without corresponding cbor interface", rt, unsafe)
return checker{
safe: func() bool {
return false
},
check: func(reflect.Value, int) error {
return err
},
}
}
}
if cache.assumeAddressableValues && reflect.PointerTo(rt).Implements(cache.cborInterface) {
return noop
}
for _, unsafe := range cache.nonCBORInterfaces {
if reflect.PointerTo(rt).Implements(unsafe) {
err := fmt.Errorf("%v implements %v without corresponding cbor interface", reflect.PointerTo(rt), unsafe)
return checker{
safe: func() bool {
return false
},
check: func(reflect.Value, int) error {
return err
},
}
}
}
self := &path{Type: rt, Parent: parent}
switch rt.Kind() {
case reflect.Array:
ce := cache.getCheckerInternal(rt.Elem(), self)
rtlen := rt.Len()
if rtlen == 0 || (!self.cyclic(rt.Elem()) && ce.safe()) {
return noop
}
return checker{
safe: func() bool {
return false
},
check: func(rv reflect.Value, depth int) error {
if depth <= 0 {
return errMaxDepthExceeded
}
for i := 0; i < rtlen; i++ {
if err := ce.check(rv.Index(i), depth-1); err != nil {
return err
}
}
return nil
},
}
case reflect.Interface:
// All interface values have to be checked because their dynamic type might
// implement one of the interesting interfaces or be composed of another type that
// does.
return checker{
safe: func() bool {
return false
},
check: func(rv reflect.Value, depth int) error {
if rv.IsNil() {
return nil
}
// Unpacking interfaces must count against recursion depth,
// consider this cycle:
// > var i interface{}
// > var p *interface{} = &i
// > i = p
// > rv := reflect.ValueOf(i)
// > for {
// > rv = rv.Elem()
// > }
if depth <= 0 {
return errMaxDepthExceeded
}
rv = rv.Elem()
return cache.getChecker(rv.Type()).check(rv, depth-1)
},
}
case reflect.Map:
rtk := rt.Key()
ck := cache.getCheckerInternal(rtk, self)
rte := rt.Elem()
ce := cache.getCheckerInternal(rte, self)
if !self.cyclic(rtk) && !self.cyclic(rte) && ck.safe() && ce.safe() {
return noop
}
return checker{
safe: func() bool {
return false
},
check: func(rv reflect.Value, depth int) error {
if depth <= 0 {
return errMaxDepthExceeded
}
iter := rv.MapRange()
rvk := reflect.New(rtk).Elem()
rve := reflect.New(rte).Elem()
for iter.Next() {
rvk.SetIterKey(iter)
if err := ck.check(rvk, depth-1); err != nil {
return err
}
rve.SetIterValue(iter)
if err := ce.check(rve, depth-1); err != nil {
return err
}
}
return nil
},
}
case reflect.Pointer:
ce := cache.getCheckerInternal(rt.Elem(), self)
if !self.cyclic(rt.Elem()) && ce.safe() {
return noop
}
return checker{
safe: func() bool {
return false
},
check: func(rv reflect.Value, depth int) error {
if rv.IsNil() {
return nil
}
if depth <= 0 {
return errMaxDepthExceeded
}
return ce.check(rv.Elem(), depth-1)
},
}
case reflect.Slice:
ce := cache.getCheckerInternal(rt.Elem(), self)
if !self.cyclic(rt.Elem()) && ce.safe() {
return noop
}
return checker{
safe: func() bool {
return false
},
check: func(rv reflect.Value, depth int) error {
if depth <= 0 {
return errMaxDepthExceeded
}
for i := 0; i < rv.Len(); i++ {
if err := ce.check(rv.Index(i), depth-1); err != nil {
return err
}
}
return nil
},
}
case reflect.Struct:
type field struct {
Index int
Checker checker
}
var fields []field
for i := 0; i < rt.NumField(); i++ {
f := rt.Field(i)
cf := cache.getCheckerInternal(f.Type, self)
if !self.cyclic(f.Type) && cf.safe() {
continue
}
fields = append(fields, field{Index: i, Checker: cf})
}
if len(fields) == 0 {
return noop
}
return checker{
safe: func() bool {
return false
},
check: func(rv reflect.Value, depth int) error {
if depth <= 0 {
return errMaxDepthExceeded
}
for _, fi := range fields {
if err := fi.Checker.check(rv.Field(fi.Index), depth-1); err != nil {
return err
}
}
return nil
},
}
default:
// Not a serializable composite type (funcs and channels are composite types but are
// rejected by JSON and CBOR serialization).
return noop
}
}
func checkUnstructuredValue(cache *checkers, v interface{}, depth int) error {
switch v := v.(type) {
case nil, bool, int64, float64, string:
return nil
case []interface{}:
if depth <= 0 {
return errMaxDepthExceeded
}
for _, element := range v {
if err := checkUnstructuredValue(cache, element, depth-1); err != nil {
return err
}
}
return nil
case map[string]interface{}:
if depth <= 0 {
return errMaxDepthExceeded
}
for _, element := range v {
if err := checkUnstructuredValue(cache, element, depth-1); err != nil {
return err
}
}
return nil
default:
// Unmarshaling an unstructured doesn't use other dynamic types, but nothing
// prevents inserting values with arbitrary dynamic types into unstructured content,
// as long as they can be marshalled.
rv := reflect.ValueOf(v)
return cache.getChecker(rv.Type()).check(rv, depth)
}
}

View File

@ -1,198 +0,0 @@
/*
Copyright 2024 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 modes
import (
"encoding"
"encoding/json"
"reflect"
"sync"
"testing"
"github.com/fxamacker/cbor/v2"
)
type TextUnmarshalerPointer struct{}
func (*TextUnmarshalerPointer) UnmarshalText([]byte) error { return nil }
type Struct[F any] struct {
Field F
}
type StructCBORMarshalerValue[F any] struct {
Field F
}
func (StructCBORMarshalerValue[F]) MarshalCBOR() ([]byte, error) { return nil, nil }
type StructCBORMarshalerPointer[F any] struct {
Field F
}
func (*StructCBORMarshalerPointer[F]) MarshalCBOR() ([]byte, error) { return nil, nil }
type StructCBORUnmarshalerPointer[F any] struct {
Field F
}
func (*StructCBORUnmarshalerPointer[F]) UnmarshalCBOR([]byte) error { return nil }
type StructCBORUnmarshalerValueWithEmbeddedJSONMarshaler struct {
json.Marshaler
}
func (StructCBORUnmarshalerValueWithEmbeddedJSONMarshaler) MarshalCBOR() ([]byte, error) {
return nil, nil
}
type SafeCyclicTypeA struct {
Bs []SafeCyclicTypeB
}
type SafeCyclicTypeB struct {
As []SafeCyclicTypeA
}
type UnsafeCyclicTypeA struct {
Bs []UnsafeCyclicTypeB
}
type UnsafeCyclicTypeB struct {
As []UnsafeCyclicTypeA
json.Marshaler
}
func TestCheckUnsupportedMarshalers(t *testing.T) {
t.Run("accepted", func(t *testing.T) {
for _, v := range []interface{}{
// Unstructured types.
nil,
false,
0,
0.0,
"",
[]interface{}{nil, false, 0, 0.0, "", []interface{}{}, map[string]interface{}{}},
map[string]interface{}{
"nil": nil,
"false": false,
"0": 0,
"0.0": 0.0,
`""`: "",
"[]interface{}{}": []interface{}{},
"map[string]interface{}{}": map[string]interface{}{},
},
// Zero-length array is statically OK, even though its element type is
// json.Marshaler.
[0]json.Marshaler{},
[3]bool{},
// Has to be dynamically checked.
[1]interface{}{},
map[int]interface{}{0: nil},
Struct[string]{},
StructCBORMarshalerValue[json.Marshaler]{},
&StructCBORMarshalerValue[json.Marshaler]{},
&StructCBORMarshalerPointer[json.Marshaler]{},
new(string),
map[cbor.Marshaler]cbor.Marshaler{},
make([]string, 10),
[]Struct[interface{}]{{Field: true}},
&Struct[StructCBORUnmarshalerPointer[json.Unmarshaler]]{},
// Checked dynamically because the dynamic type of an interface, even
// encoding.TextMarshaler or json.Marshaler, might also implement
// cbor.Marshaler. Accepted because there are no map entries.
map[encoding.TextMarshaler]struct{}{},
map[string]json.Marshaler{},
// Methods of json.Marshaler and cbor.Marshaler are both promoted to the
// struct's method set.
struct {
json.Marshaler
StructCBORMarshalerValue[int]
}{},
// Embedded field's json.Marshaler implementation is promoted, but the
// containing struct implements cbor.Marshaler.
StructCBORUnmarshalerValueWithEmbeddedJSONMarshaler{},
SafeCyclicTypeA{},
SafeCyclicTypeB{},
} {
if err := RejectCustomMarshalers(v); err != nil {
t.Errorf("%#v: unexpected non-nil error: %v", v, err)
}
}
})
t.Run("rejected", func(t *testing.T) {
for _, v := range []interface{}{
Struct[json.Marshaler]{},
StructCBORMarshalerValue[json.Unmarshaler]{},
Struct[interface{}]{Field: [1]json.Unmarshaler{}},
// cbor.Marshaler implemented with pointer receiver on non-pointer type.
StructCBORMarshalerPointer[json.Marshaler]{},
[1]json.Marshaler{},
[1]interface{}{[1]json.Marshaler{}},
map[int]interface{}{0: [1]json.Marshaler{}},
[]interface{}{[1]json.Marshaler{}},
map[string]interface{}{"": [1]json.Marshaler{}},
[]Struct[interface{}]{{Field: [1]json.Marshaler{}}},
map[encoding.TextMarshaler]struct{}{[1]encoding.TextMarshaler{}[0]: {}},
map[string]json.Marshaler{"": [1]json.Marshaler{}[0]},
&Struct[StructCBORMarshalerPointer[json.Marshaler]]{},
Struct[TextUnmarshalerPointer]{},
struct {
json.Marshaler
}{},
UnsafeCyclicTypeA{Bs: []UnsafeCyclicTypeB{{}}},
UnsafeCyclicTypeB{},
} {
if err := RejectCustomMarshalers(v); err == nil {
t.Errorf("%#v: unexpected nil error", v)
}
}
})
}
// With -test.race, retrieves a custom marshaler checker for a cyclic type concurrently from many
// goroutines to root out data races in checker lazy initialization.
func TestLazyCheckerInitializationDataRace(t *testing.T) {
cache := checkers{
cborInterface: reflect.TypeFor[cbor.Marshaler](),
nonCBORInterfaces: []reflect.Type{
reflect.TypeFor[json.Marshaler](),
reflect.TypeFor[encoding.TextMarshaler](),
},
}
rt := reflect.TypeFor[SafeCyclicTypeA]()
begin := make(chan struct{})
var wg sync.WaitGroup
for range 32 {
wg.Add(1)
go func() {
defer wg.Done()
<-begin
cache.getChecker(rt)
}()
}
close(begin)
wg.Wait()
}

View File

@ -44,7 +44,10 @@ var simpleValues *cbor.SimpleValueRegistry = func() *cbor.SimpleValueRegistry {
return simpleValues
}()
var Decode cbor.DecMode = func() cbor.DecMode {
// decode is the basis for the Decode mode, with no JSONUnmarshalerTranscoder
// configured. TranscodeToJSON uses this directly rather than Decode to avoid an initialization
// cycle between the two. Everything else should use one of the exported DecModes.
var decode cbor.DecMode = func() cbor.DecMode {
decode, err := cbor.DecOptions{
// Maps with duplicate keys are well-formed but invalid according to the CBOR spec
// and never acceptable. Unlike the JSON serializer, inputs containing duplicate map
@ -139,6 +142,10 @@ var Decode cbor.DecMode = func() cbor.DecMode {
// Disable default recognition of types implementing encoding.BinaryUnmarshaler,
// which is not recognized for JSON decoding.
BinaryUnmarshaler: cbor.BinaryUnmarshalerNone,
// Marshal types that implement encoding.TextMarshaler by calling their MarshalText
// method and encoding the result to a CBOR text string.
TextUnmarshaler: cbor.TextUnmarshalerTextString,
}.DecMode()
if err != nil {
panic(err)
@ -146,6 +153,19 @@ var Decode cbor.DecMode = func() cbor.DecMode {
return decode
}()
var Decode cbor.DecMode = func() cbor.DecMode {
opts := decode.DecOptions()
// When decoding into a value of a type that implements json.Unmarshaler (and does not
// implement cbor.Unmarshaler), transcode the input to JSON and pass it to the value's
// UnmarshalJSON method.
opts.JSONUnmarshalerTranscoder = TranscodeFunc(TranscodeToJSON)
dm, err := opts.DecMode()
if err != nil {
panic(err)
}
return dm
}()
// DecodeLax is derived from Decode, but does not complain about unknown fields in the input.
var DecodeLax cbor.DecMode = func() cbor.DecMode {
opts := Decode.DecOptions()

View File

@ -211,6 +211,20 @@ func TestDecode(t *testing.T) {
}
}),
},
{
name: "text unmarshaler",
in: hex("4161"),
into: &RoundtrippableText{},
want: &RoundtrippableText{Text: "a"},
assertOnError: assertNilError,
},
{
name: "json unmarshaler",
in: hex("4161"),
into: &RoundtrippableJSON{},
want: &RoundtrippableJSON{Raw: `"a"`},
assertOnError: assertNilError,
},
})
group(t, "text string", []test{
@ -236,6 +250,20 @@ func TestDecode(t *testing.T) {
want: "",
assertOnError: assertNilError,
},
{
name: "text unmarshaler",
in: hex("6161"),
into: &RoundtrippableText{},
want: &RoundtrippableText{Text: "a"},
assertOnError: assertNilError,
},
{
name: "json unmarshaler",
in: hex("6161"),
into: &RoundtrippableJSON{},
want: &RoundtrippableJSON{Raw: `"a"`},
assertOnError: assertNilError,
},
})
group(t, "array", []test{

View File

@ -22,7 +22,10 @@ import (
"github.com/fxamacker/cbor/v2"
)
var Encode = EncMode{
// encode is the basis for the Encode mode, with no JSONMarshalerTranscoder
// configured. TranscodeFromJSON uses this directly rather than Encode to avoid an initialization
// cycle between the two. Everything else should use one of the exported EncModes.
var encode = EncMode{
delegate: func() cbor.UserBufferEncMode {
encode, err := cbor.EncOptions{
// Map keys need to be sorted to have deterministic output, and this is the order
@ -94,6 +97,10 @@ var Encode = EncMode{
// Disable default recognition of types implementing encoding.BinaryMarshaler, which
// is not recognized for JSON encoding.
BinaryMarshaler: cbor.BinaryMarshalerNone,
// Unmarshal into types that implement encoding.TextUnmarshaler by passing
// the contents of a CBOR string to their UnmarshalText method.
TextMarshaler: cbor.TextMarshalerTextString,
}.UserBufferEncMode()
if err != nil {
panic(err)
@ -102,6 +109,21 @@ var Encode = EncMode{
}(),
}
var Encode = EncMode{
delegate: func() cbor.UserBufferEncMode {
opts := encode.options()
// To encode a value of a type that implements json.Marshaler (and does not
// implement cbor.Marshaler), transcode the result of calling its MarshalJSON method
// directly to CBOR.
opts.JSONMarshalerTranscoder = TranscodeFunc(TranscodeFromJSON)
em, err := opts.UserBufferEncMode()
if err != nil {
panic(err)
}
return em
}(),
}
var EncodeNondeterministic = EncMode{
delegate: func() cbor.UserBufferEncMode {
opts := Encode.options()

View File

@ -96,6 +96,18 @@ func TestEncode(t *testing.T) {
want: []byte{0xd6, 0x45, 'h', 'e', 'l', 'l', 'o'},
assertOnError: assertNilError,
},
{
name: "text marshaler",
in: &RoundtrippableText{Text: "a"},
want: []byte{0x61, 0x61},
assertOnError: assertNilError,
},
{
name: "json marshaler",
in: &RoundtrippableJSON{Raw: `"a"`},
want: []byte{0x41, 0x61},
assertOnError: assertNilError,
},
} {
encModes := tc.modes
if len(encModes) == 0 {

View File

@ -34,6 +34,28 @@ func nilPointerFor[T interface{}]() *T {
return nil
}
type RoundtrippableText struct{ Text string }
func (rt RoundtrippableText) MarshalText() ([]byte, error) {
return []byte(rt.Text), nil
}
func (rt *RoundtrippableText) UnmarshalText(text []byte) error {
rt.Text = string(text)
return nil
}
type RoundtrippableJSON struct{ Raw string }
func (rj RoundtrippableJSON) MarshalJSON() ([]byte, error) {
return []byte(rj.Raw), nil
}
func (rj *RoundtrippableJSON) UnmarshalJSON(raw []byte) error {
rj.Raw = string(raw)
return nil
}
// TestRoundtrip roundtrips object serialization to interface{} and back via CBOR.
func TestRoundtrip(t *testing.T) {
type modePair struct {
@ -264,6 +286,14 @@ func TestRoundtrip(t *testing.T) {
V *map[string]interface{} `json:"v,omitempty"`
}{},
},
{
name: "textmarshaler and textunmarshaler",
obj: RoundtrippableText{Text: "foo"},
},
{
name: "json marshaler and unmarshaler",
obj: RoundtrippableJSON{Raw: `{"foo":[42,3.1,true,false,null]}`},
},
} {
modePairs := tc.modePairs
if len(modePairs) == 0 {

View File

@ -0,0 +1,108 @@
/*
Copyright 2025 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 modes
import (
"encoding/json"
"errors"
"io"
kjson "sigs.k8s.io/json"
)
type TranscodeFunc func(dst io.Writer, src io.Reader) error
func (f TranscodeFunc) Transcode(dst io.Writer, src io.Reader) error {
return f(dst, src)
}
func TranscodeFromJSON(dst io.Writer, src io.Reader) error {
var tmp any
dec := kjson.NewDecoderCaseSensitivePreserveInts(src)
if err := dec.Decode(&tmp); err != nil {
return err
}
if err := dec.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
return errors.New("extraneous data")
}
return encode.MarshalTo(tmp, dst)
}
func TranscodeToJSON(dst io.Writer, src io.Reader) error {
var tmp any
dec := decode.NewDecoder(src)
if err := dec.Decode(&tmp); err != nil {
return err
}
if err := dec.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
return errors.New("extraneous data")
}
// Use an Encoder to avoid the extra []byte allocated by Marshal. Encode, unlike Marshal,
// appends a trailing newline to separate consecutive encodings of JSON values that aren't
// self-delimiting, like numbers. Strip the newline to avoid the assumption that every
// json.Unmarshaler implementation will accept trailing whitespace.
enc := json.NewEncoder(&trailingLinefeedSuppressor{delegate: dst})
enc.SetIndent("", "")
return enc.Encode(tmp)
}
// trailingLinefeedSuppressor is an io.Writer that wraps another io.Writer, suppressing a single
// trailing linefeed if it is the last byte written by the latest call to Write.
type trailingLinefeedSuppressor struct {
lf bool
delegate io.Writer
}
func (w *trailingLinefeedSuppressor) Write(p []byte) (int, error) {
if len(p) == 0 {
// Avoid flushing a buffered linefeeds on an empty write.
return 0, nil
}
if w.lf {
// The previous write had a trailing linefeed that was buffered. That wasn't the
// last Write call, so flush the buffered linefeed before continuing.
n, err := w.delegate.Write([]byte{'\n'})
if n > 0 {
w.lf = false
}
if err != nil {
return 0, err
}
}
if p[len(p)-1] != '\n' {
return w.delegate.Write(p)
}
p = p[:len(p)-1]
if len(p) == 0 { // []byte{'\n'}
w.lf = true
return 1, nil
}
n, err := w.delegate.Write(p)
if n == len(p) {
// Everything up to the trailing linefeed has been flushed. Eat the linefeed.
w.lf = true
n++
}
return n, err
}

View File

@ -0,0 +1,291 @@
/*
Copyright 2025 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 modes
import (
"errors"
"strings"
"testing"
)
func TestTranscodeFromJSON(t *testing.T) {
for _, tc := range []struct {
name string
json string
cbor string
err error
}{
{
name: "whole number",
json: "42",
cbor: "\x18\x2a",
},
{
name: "decimal number",
json: "1.5",
cbor: "\xf9\x3e\x00",
},
{
name: "false",
json: "false",
cbor: "\xf4",
},
{
name: "true",
json: "true",
cbor: "\xf5",
},
{
name: "null",
json: "null",
cbor: "\xf6",
},
{
name: "string",
json: `"foo"`,
cbor: "\x43foo",
},
{
name: "array",
json: `[]`,
cbor: "\x80",
},
{
name: "object",
json: `{"foo":"bar"}`,
cbor: "\xa1\x43foo\x43bar",
},
{
name: "extraneous data",
json: "{}{}",
err: errors.New("extraneous data"),
},
{
name: "eof",
json: "",
err: errors.New("EOF"),
},
{
name: "unexpected eof",
json: "{",
err: errors.New("unexpected EOF"),
},
{
name: "malformed json",
json: "}",
err: errors.New("invalid character '}' looking for beginning of value"),
},
} {
t.Run(tc.name, func(t *testing.T) {
var out strings.Builder
err := TranscodeFromJSON(&out, strings.NewReader(tc.json))
if (err == nil) != (tc.err == nil) || tc.err != nil && tc.err.Error() != err.Error() {
t.Fatalf("unexpected error: want %v got %v", tc.err, err)
}
if got, want := out.String(), tc.cbor; got != want {
t.Errorf("unexpected transcoding: want 0x%x got 0x%x", want, got)
}
})
}
}
func TestTranscodeToJSON(t *testing.T) {
for _, tc := range []struct {
name string
cbor string
json string
err error
}{
{
name: "whole number",
cbor: "\x18\x2a",
json: "42",
},
{
name: "decimal number",
cbor: "\xf9\x3e\x00",
json: "1.5",
},
{
name: "false",
cbor: "\xf4",
json: "false",
},
{
name: "true",
cbor: "\xf5",
json: "true",
},
{
name: "null",
cbor: "\xf6",
json: "null",
},
{
name: "string",
cbor: "\x43foo",
json: `"foo"`,
},
{
name: "array",
cbor: "\x80",
json: `[]`,
},
{
name: "object",
cbor: "\xa1\x43foo\x43bar",
json: `{"foo":"bar"}`,
},
{
name: "extraneous data",
cbor: "\xa0\xa0",
err: errors.New("extraneous data"),
},
{
name: "unexpected eof",
cbor: "\xa1",
err: errors.New("unexpected EOF"),
},
{
name: "malformed cbor",
cbor: "\xff",
err: errors.New(`cbor: unexpected "break" code`),
},
} {
t.Run(tc.name, func(t *testing.T) {
var out strings.Builder
err := TranscodeToJSON(&out, strings.NewReader(tc.cbor))
if (err == nil) != (tc.err == nil) || tc.err != nil && tc.err.Error() != err.Error() {
t.Fatalf("unexpected error: want %v got %v", tc.err, err)
}
if got, want := out.String(), tc.json; got != want {
t.Errorf("unexpected transcoding: want %q got %q", want, got)
}
})
}
}
type write struct {
p string
n int
err error
}
type mockWriter struct {
t testing.TB
calls []write
}
func (m *mockWriter) Write(p []byte) (int, error) {
if len(m.calls) == 0 {
m.t.Fatalf("unexpected call (p=%q)", string(p))
}
if got, want := string(p), m.calls[0].p; got != want {
m.t.Errorf("unexpected argument: want %q, got %q", want, got)
}
n, err := m.calls[0].n, m.calls[0].err
m.calls = m.calls[1:]
return n, err
}
func TestTrailingLinefeedSuppressor(t *testing.T) {
for _, tc := range []struct {
name string
calls []write
delegated []write
}{
{
name: "one write without newline",
calls: []write{{"foo", 3, nil}},
delegated: []write{{"foo", 3, nil}},
},
{
name: "one write with newline",
calls: []write{{"foo\n", 4, nil}},
delegated: []write{{"foo", 3, nil}},
},
{
name: "one write with only newline",
calls: []write{{"\n", 1, nil}},
delegated: nil,
},
{
name: "one empty write",
calls: []write{{"", 0, nil}},
delegated: nil,
},
{
name: "three writes, all with only newline",
calls: []write{{"\n", 1, nil}, {"\n", 1, nil}, {"\n", 1, nil}},
delegated: []write{{"\n", 1, nil}, {"\n", 1, nil}},
},
{
name: "buffered linefeed not flushed on empty write",
calls: []write{{"\n", 1, nil}, {"", 0, nil}},
delegated: nil,
},
{
name: "two writes, last with trailing newline",
calls: []write{{"foo", 3, nil}, {"bar\n", 4, nil}},
delegated: []write{{"foo", 3, nil}, {"bar", 3, nil}},
},
{
name: "two writes, first with trailing newline",
calls: []write{{"foo\n", 4, nil}, {"bar", 3, nil}},
delegated: []write{{"foo", 3, nil}, {"\n", 1, nil}, {"bar", 3, nil}},
},
{
name: "two writes, both with trailing newlines",
calls: []write{{"foo\n", 4, nil}, {"bar\n", 4, nil}},
delegated: []write{{"foo", 3, nil}, {"\n", 1, nil}, {"bar", 3, nil}},
},
{
name: "two writes, neither with newlines",
calls: []write{{"foo", 3, nil}, {"bar", 3, nil}},
delegated: []write{{"foo", 3, nil}, {"bar", 3, nil}},
},
{
name: "delegate error before reaching newline",
calls: []write{{"foo\n", 1, errors.New("test")}, {"oo\n", 3, nil}},
delegated: []write{{"foo", 1, errors.New("test")}, {"oo", 2, nil}},
},
{
name: "delegate error after reaching newline",
calls: []write{{"foo\n", 4, errors.New("test")}},
delegated: []write{{"foo", 3, errors.New("test")}},
},
{
name: "delegate error flushing newline",
calls: []write{{"foo\n", 4, nil}, {"\n", 0, errors.New("test")}, {"\n", 1, nil}},
delegated: []write{{"foo", 3, nil}, {"\n", 0, errors.New("test")}, {"\n", 1, nil}},
},
} {
t.Run(tc.name, func(t *testing.T) {
w := &trailingLinefeedSuppressor{delegate: &mockWriter{t: t, calls: tc.delegated}}
for _, call := range tc.calls {
n, err := w.Write([]byte(call.p))
if n != call.n {
t.Errorf("unexpected n: want %d got %d", call.n, n)
}
if (err == nil) != (call.err == nil) || call.err != nil && call.err.Error() != err.Error() {
t.Errorf("unexpected error: want %v got %v", call.err, err)
}
}
})
}
}

View File

@ -28,7 +28,7 @@ import (
func newSerializersForScheme(scheme *runtime.Scheme, mf json.MetaFactory, options CodecFactoryOptions) []runtime.SerializerInfo {
jsonSerializer := json.NewSerializerWithOptions(
mf, scheme, scheme,
json.SerializerOptions{Yaml: false, Pretty: false, Strict: options.Strict},
json.SerializerOptions{Yaml: false, Pretty: false, Strict: options.Strict, StreamingCollectionsEncoding: options.StreamingCollectionsEncodingToJSON},
)
jsonSerializerType := runtime.SerializerInfo{
MediaType: runtime.ContentTypeJSON,
@ -38,7 +38,7 @@ func newSerializersForScheme(scheme *runtime.Scheme, mf json.MetaFactory, option
Serializer: jsonSerializer,
StrictSerializer: json.NewSerializerWithOptions(
mf, scheme, scheme,
json.SerializerOptions{Yaml: false, Pretty: false, Strict: true},
json.SerializerOptions{Yaml: false, Pretty: false, Strict: true, StreamingCollectionsEncoding: options.StreamingCollectionsEncodingToJSON},
),
StreamSerializer: &runtime.StreamSerializerInfo{
EncodesAsText: true,
@ -61,7 +61,9 @@ func newSerializersForScheme(scheme *runtime.Scheme, mf json.MetaFactory, option
mf, scheme, scheme,
json.SerializerOptions{Yaml: true, Pretty: false, Strict: true},
)
protoSerializer := protobuf.NewSerializer(scheme, scheme)
protoSerializer := protobuf.NewSerializerWithOptions(scheme, scheme, protobuf.SerializerOptions{
StreamingCollectionsEncoding: options.StreamingCollectionsEncodingToProtobuf,
})
protoRawSerializer := protobuf.NewRawSerializer(scheme, scheme)
serializers := []runtime.SerializerInfo{
@ -113,6 +115,9 @@ type CodecFactoryOptions struct {
// Pretty includes a pretty serializer along with the non-pretty one
Pretty bool
StreamingCollectionsEncodingToJSON bool
StreamingCollectionsEncodingToProtobuf bool
serializers []func(runtime.ObjectCreater, runtime.ObjectTyper) runtime.SerializerInfo
}
@ -147,6 +152,18 @@ func WithSerializer(f func(runtime.ObjectCreater, runtime.ObjectTyper) runtime.S
}
}
func WithStreamingCollectionEncodingToJSON() CodecFactoryOptionsMutator {
return func(options *CodecFactoryOptions) {
options.StreamingCollectionsEncodingToJSON = true
}
}
func WithStreamingCollectionEncodingToProtobuf() CodecFactoryOptionsMutator {
return func(options *CodecFactoryOptions) {
options.StreamingCollectionsEncodingToProtobuf = true
}
}
// NewCodecFactory provides methods for retrieving serializers for the supported wire formats
// and conversion wrappers to define preferred internal and external versions. In the future,
// as the internal version is used less, callers may instead use a defaulting serializer and

View File

@ -34,8 +34,8 @@ import (
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"github.com/google/go-cmp/cmp"
fuzz "github.com/google/gofuzz"
flag "github.com/spf13/pflag"
"sigs.k8s.io/randfill"
"sigs.k8s.io/yaml"
)
@ -61,9 +61,9 @@ func (testMetaFactory) Interpret(data []byte) (*schema.GroupVersionKind, error)
}
// TestObjectFuzzer can randomly populate all the above objects.
var TestObjectFuzzer = fuzz.New().NilChance(.5).NumElements(1, 100).Funcs(
func(j *runtimetesting.MyWeirdCustomEmbeddedVersionKindField, c fuzz.Continue) {
c.FuzzNoCustom(j)
var TestObjectFuzzer = randfill.New().NilChance(.5).NumElements(1, 100).Funcs(
func(j *runtimetesting.MyWeirdCustomEmbeddedVersionKindField, c randfill.Continue) {
c.FillNoCustom(j)
j.APIVersion = ""
j.ObjectKind = ""
},
@ -106,7 +106,7 @@ var semantic = conversion.EqualitiesOrDie(
func runTest(t *testing.T, source interface{}) {
name := reflect.TypeOf(source).Elem().Name()
TestObjectFuzzer.Fuzz(source)
TestObjectFuzzer.Fill(source)
_, codec := GetTestScheme()
data, err := runtime.Encode(codec, source.(runtime.Object))

View File

@ -0,0 +1,230 @@
/*
Copyright 2025 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 json
import (
"encoding/json"
"fmt"
"io"
"maps"
"slices"
"sort"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/conversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
func streamEncodeCollections(obj runtime.Object, w io.Writer) (bool, error) {
list, ok := obj.(*unstructured.UnstructuredList)
if ok {
return true, streamingEncodeUnstructuredList(w, list)
}
if _, ok := obj.(json.Marshaler); ok {
return false, nil
}
typeMeta, listMeta, items, err := getListMeta(obj)
if err == nil {
return true, streamingEncodeList(w, typeMeta, listMeta, items)
}
return false, nil
}
// getListMeta implements list extraction logic for json stream serialization.
//
// Reason for a custom logic instead of reusing accessors from meta package:
// * Validate json tags to prevent incompatibility with json standard package.
// * ListMetaAccessor doesn't distinguish empty from nil value.
// * TypeAccessort reparsing "apiVersion" and serializing it with "{group}/{version}"
func getListMeta(list runtime.Object) (metav1.TypeMeta, metav1.ListMeta, []runtime.Object, error) {
listValue, err := conversion.EnforcePtr(list)
if err != nil {
return metav1.TypeMeta{}, metav1.ListMeta{}, nil, err
}
listType := listValue.Type()
if listType.NumField() != 3 {
return metav1.TypeMeta{}, metav1.ListMeta{}, nil, fmt.Errorf("expected ListType to have 3 fields")
}
// TypeMeta
typeMeta, ok := listValue.Field(0).Interface().(metav1.TypeMeta)
if !ok {
return metav1.TypeMeta{}, metav1.ListMeta{}, nil, fmt.Errorf("expected TypeMeta field to have TypeMeta type")
}
if listType.Field(0).Tag.Get("json") != ",inline" {
return metav1.TypeMeta{}, metav1.ListMeta{}, nil, fmt.Errorf(`expected TypeMeta json field tag to be ",inline"`)
}
// ListMeta
listMeta, ok := listValue.Field(1).Interface().(metav1.ListMeta)
if !ok {
return metav1.TypeMeta{}, metav1.ListMeta{}, nil, fmt.Errorf("expected ListMeta field to have ListMeta type")
}
if listType.Field(1).Tag.Get("json") != "metadata,omitempty" {
return metav1.TypeMeta{}, metav1.ListMeta{}, nil, fmt.Errorf(`expected ListMeta json field tag to be "metadata,omitempty"`)
}
// Items
items, err := meta.ExtractList(list)
if err != nil {
return metav1.TypeMeta{}, metav1.ListMeta{}, nil, err
}
if listType.Field(2).Tag.Get("json") != "items" {
return metav1.TypeMeta{}, metav1.ListMeta{}, nil, fmt.Errorf(`expected Items json field tag to be "items"`)
}
return typeMeta, listMeta, items, nil
}
func streamingEncodeList(w io.Writer, typeMeta metav1.TypeMeta, listMeta metav1.ListMeta, items []runtime.Object) error {
// Start
if _, err := w.Write([]byte(`{`)); err != nil {
return err
}
// TypeMeta
if typeMeta.Kind != "" {
if err := encodeKeyValuePair(w, "kind", typeMeta.Kind, []byte(",")); err != nil {
return err
}
}
if typeMeta.APIVersion != "" {
if err := encodeKeyValuePair(w, "apiVersion", typeMeta.APIVersion, []byte(",")); err != nil {
return err
}
}
// ListMeta
if err := encodeKeyValuePair(w, "metadata", listMeta, []byte(",")); err != nil {
return err
}
// Items
if err := encodeItemsObjectSlice(w, items); err != nil {
return err
}
// End
_, err := w.Write([]byte("}\n"))
return err
}
func encodeItemsObjectSlice(w io.Writer, items []runtime.Object) (err error) {
if items == nil {
err := encodeKeyValuePair(w, "items", nil, nil)
return err
}
_, err = w.Write([]byte(`"items":[`))
if err != nil {
return err
}
suffix := []byte(",")
for i, item := range items {
if i == len(items)-1 {
suffix = nil
}
err := encodeValue(w, item, suffix)
if err != nil {
return err
}
}
_, err = w.Write([]byte("]"))
if err != nil {
return err
}
return err
}
func streamingEncodeUnstructuredList(w io.Writer, list *unstructured.UnstructuredList) error {
_, err := w.Write([]byte(`{`))
if err != nil {
return err
}
keys := slices.Collect(maps.Keys(list.Object))
if _, exists := list.Object["items"]; !exists {
keys = append(keys, "items")
}
sort.Strings(keys)
suffix := []byte(",")
for i, key := range keys {
if i == len(keys)-1 {
suffix = nil
}
if key == "items" {
err = encodeItemsUnstructuredSlice(w, list.Items, suffix)
} else {
err = encodeKeyValuePair(w, key, list.Object[key], suffix)
}
if err != nil {
return err
}
}
_, err = w.Write([]byte("}\n"))
return err
}
func encodeItemsUnstructuredSlice(w io.Writer, items []unstructured.Unstructured, suffix []byte) (err error) {
_, err = w.Write([]byte(`"items":[`))
if err != nil {
return err
}
comma := []byte(",")
for i, item := range items {
if i == len(items)-1 {
comma = nil
}
err := encodeValue(w, item.Object, comma)
if err != nil {
return err
}
}
_, err = w.Write([]byte("]"))
if err != nil {
return err
}
if len(suffix) > 0 {
_, err = w.Write(suffix)
}
return err
}
func encodeKeyValuePair(w io.Writer, key string, value any, suffix []byte) (err error) {
err = encodeValue(w, key, []byte(":"))
if err != nil {
return err
}
err = encodeValue(w, value, suffix)
if err != nil {
return err
}
return err
}
func encodeValue(w io.Writer, value any, suffix []byte) error {
data, err := json.Marshal(value)
if err != nil {
return err
}
_, err = w.Write(data)
if err != nil {
return err
}
if len(suffix) > 0 {
_, err = w.Write(suffix)
}
return err
}

View File

@ -0,0 +1,795 @@
/*
Copyright 2025 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 json
import (
"bytes"
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"sigs.k8s.io/randfill"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
testapigroupv1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func TestCollectionsEncoding(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
testCollectionsEncoding(t, NewSerializerWithOptions(DefaultMetaFactory, nil, nil, SerializerOptions{}), false)
})
t.Run("Streaming", func(t *testing.T) {
testCollectionsEncoding(t, NewSerializerWithOptions(DefaultMetaFactory, nil, nil, SerializerOptions{StreamingCollectionsEncoding: true}), true)
})
}
// testCollectionsEncoding should provide comprehensive tests to validate streaming implementation of encoder.
func testCollectionsEncoding(t *testing.T, s *Serializer, streamingEnabled bool) {
var buf writeCountingBuffer
var remainingItems int64 = 1
// As defined in KEP-5116 we it should include the following scenarios:
// Context: https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/5116-streaming-response-encoding#unit-tests
for _, tc := range []struct {
name string
in runtime.Object
cannotStream bool
expect string
}{
// Preserving the distinction between integers and floating-point numbers
{
name: "Struct with floats",
in: &StructWithFloatsList{
Items: []StructWithFloats{
{
Int: 1,
Float32: float32(1),
Float64: 1.1,
},
},
},
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{},\"Int\":1,\"Float32\":1,\"Float64\":1.1}]}\n",
},
{
name: "Unstructured object float",
in: &unstructured.UnstructuredList{
Object: map[string]interface{}{
"int": 1,
"float32": float32(1),
"float64": 1.1,
},
},
expect: "{\"float32\":1,\"float64\":1.1,\"int\":1,\"items\":[]}\n",
},
{
name: "Unstructured items float",
in: &unstructured.UnstructuredList{
Items: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"int": 1,
"float32": float32(1),
"float64": 1.1,
},
},
},
},
expect: "{\"items\":[{\"float32\":1,\"float64\":1.1,\"int\":1}]}\n",
},
// Handling structs with duplicate field names (JSON tag names) without producing duplicate keys in the encoded output
{
name: "StructWithDuplicatedTags",
in: &StructWithDuplicatedTagsList{
Items: []StructWithDuplicatedTags{
{
Key1: "key1",
Key2: "key2",
},
},
},
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{}}]}\n",
},
// Encoding Go strings containing invalid UTF-8 sequences without error
{
name: "UnstructuredList object invalid UTF-8 ",
in: &unstructured.UnstructuredList{
Object: map[string]interface{}{
"key": "\x80", // first byte is a continuation byte
},
},
expect: "{\"items\":[],\"key\":\"\\ufffd\"}\n",
},
{
name: "UnstructuredList items invalid UTF-8 ",
in: &unstructured.UnstructuredList{
Items: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"key": "\x80", // first byte is a continuation byte
},
},
},
},
expect: "{\"items\":[{\"key\":\"\\ufffd\"}]}\n",
},
// Preserving the distinction between absent, present-but-null, and present-and-empty states for slices and maps
{
name: "CarpList items nil",
in: &testapigroupv1.CarpList{
Items: nil,
},
expect: "{\"metadata\":{},\"items\":null}\n",
},
{
name: "CarpList slice nil",
in: &testapigroupv1.CarpList{
Items: []testapigroupv1.Carp{
{
Status: testapigroupv1.CarpStatus{
Conditions: nil,
},
},
},
},
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{},\"spec\":{},\"status\":{}}]}\n",
},
{
name: "CarpList map nil",
in: &testapigroupv1.CarpList{
Items: []testapigroupv1.Carp{
{
Spec: testapigroupv1.CarpSpec{
NodeSelector: nil,
},
},
},
},
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{},\"spec\":{},\"status\":{}}]}\n",
},
{
name: "UnstructuredList items nil",
in: &unstructured.UnstructuredList{
Items: nil,
},
expect: "{\"items\":[]}\n",
},
{
name: "UnstructuredList items slice nil",
in: &unstructured.UnstructuredList{
Items: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"slice": ([]string)(nil),
},
},
},
},
expect: "{\"items\":[{\"slice\":null}]}\n",
},
{
name: "UnstructuredList items map nil",
in: &unstructured.UnstructuredList{
Items: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"map": (map[string]string)(nil),
},
},
},
},
expect: "{\"items\":[{\"map\":null}]}\n",
},
{
name: "UnstructuredList object nil",
in: &unstructured.UnstructuredList{
Object: nil,
},
expect: "{\"items\":[]}\n",
},
{
name: "UnstructuredList object slice nil",
in: &unstructured.UnstructuredList{
Object: map[string]interface{}{
"slice": ([]string)(nil),
},
},
expect: "{\"items\":[],\"slice\":null}\n",
},
{
name: "UnstructuredList object map nil",
in: &unstructured.UnstructuredList{
Object: map[string]interface{}{
"map": (map[string]string)(nil),
},
},
expect: "{\"items\":[],\"map\":null}\n",
},
{
name: "CarpList items empty",
in: &testapigroupv1.CarpList{
Items: []testapigroupv1.Carp{},
},
expect: "{\"metadata\":{},\"items\":[]}\n",
},
{
name: "CarpList slice empty",
in: &testapigroupv1.CarpList{
Items: []testapigroupv1.Carp{
{
Status: testapigroupv1.CarpStatus{
Conditions: []testapigroupv1.CarpCondition{},
},
},
},
},
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{},\"spec\":{},\"status\":{}}]}\n",
},
{
name: "CarpList map empty",
in: &testapigroupv1.CarpList{
Items: []testapigroupv1.Carp{
{
Spec: testapigroupv1.CarpSpec{
NodeSelector: map[string]string{},
},
},
},
},
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{},\"spec\":{},\"status\":{}}]}\n",
},
{
name: "UnstructuredList items empty",
in: &unstructured.UnstructuredList{
Items: []unstructured.Unstructured{},
},
expect: "{\"items\":[]}\n",
},
{
name: "UnstructuredList items slice empty",
in: &unstructured.UnstructuredList{
Items: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"slice": []string{},
},
},
},
},
expect: "{\"items\":[{\"slice\":[]}]}\n",
},
{
name: "UnstructuredList items map empty",
in: &unstructured.UnstructuredList{
Items: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"map": map[string]string{},
},
},
},
},
expect: "{\"items\":[{\"map\":{}}]}\n",
},
{
name: "UnstructuredList object empty",
in: &unstructured.UnstructuredList{
Object: map[string]interface{}{},
},
expect: "{\"items\":[]}\n",
},
{
name: "UnstructuredList object slice empty",
in: &unstructured.UnstructuredList{
Object: map[string]interface{}{
"slice": []string{},
},
},
expect: "{\"items\":[],\"slice\":[]}\n",
},
{
name: "UnstructuredList object map empty",
in: &unstructured.UnstructuredList{
Object: map[string]interface{}{
"map": map[string]string{},
},
},
expect: "{\"items\":[],\"map\":{}}\n",
},
// Handling structs implementing MarshallJSON method, especially built-in collection types.
{
name: "List with MarshallJSON cannot be streamed",
in: &ListWithMarshalJSONList{},
expect: "\"marshallJSON\"\n",
cannotStream: true,
},
{
name: "Struct with MarshallJSON",
in: &StructWithMarshalJSONList{
Items: []StructWithMarshalJSON{
{},
},
},
expect: "{\"metadata\":{},\"items\":[\"marshallJSON\"]}\n",
},
// Handling raw bytes.
{
name: "Struct with raw bytes",
in: &StructWithRawBytesList{
Items: []StructWithRawBytes{
{
Slice: []byte{0x01, 0x02, 0x03},
Array: [3]byte{0x01, 0x02, 0x03},
},
},
},
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{},\"Slice\":\"AQID\",\"Array\":[1,2,3]}]}\n",
},
{
name: "UnstructuredList object raw bytes",
in: &unstructured.UnstructuredList{
Object: map[string]interface{}{
"slice": []byte{0x01, 0x02, 0x03},
"array": [3]byte{0x01, 0x02, 0x03},
},
},
expect: "{\"array\":[1,2,3],\"items\":[],\"slice\":\"AQID\"}\n",
},
{
name: "UnstructuredList items raw bytes",
in: &unstructured.UnstructuredList{
Items: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"slice": []byte{0x01, 0x02, 0x03},
"array": [3]byte{0x01, 0x02, 0x03},
},
},
},
},
expect: "{\"items\":[{\"array\":[1,2,3],\"slice\":\"AQID\"}]}\n",
},
// Other scenarios:
{
name: "List just kind",
in: &testapigroupv1.CarpList{
TypeMeta: metav1.TypeMeta{
Kind: "List",
},
},
expect: "{\"kind\":\"List\",\"metadata\":{},\"items\":null}\n",
},
{
name: "List just apiVersion",
in: &testapigroupv1.CarpList{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
},
},
expect: "{\"apiVersion\":\"v1\",\"metadata\":{},\"items\":null}\n",
},
{
name: "List no elements",
in: &testapigroupv1.CarpList{
TypeMeta: metav1.TypeMeta{
Kind: "List",
APIVersion: "v1",
},
ListMeta: metav1.ListMeta{
ResourceVersion: "2345",
},
Items: []testapigroupv1.Carp{},
},
expect: "{\"kind\":\"List\",\"apiVersion\":\"v1\",\"metadata\":{\"resourceVersion\":\"2345\"},\"items\":[]}\n",
},
{
name: "List one element with continue",
in: &testapigroupv1.CarpList{
TypeMeta: metav1.TypeMeta{
Kind: "List",
APIVersion: "v1",
},
ListMeta: metav1.ListMeta{
ResourceVersion: "2345",
Continue: "abc",
RemainingItemCount: &remainingItems,
},
Items: []testapigroupv1.Carp{
{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Carp"}, ObjectMeta: metav1.ObjectMeta{
Name: "pod",
Namespace: "default",
}},
},
},
expect: "{\"kind\":\"List\",\"apiVersion\":\"v1\",\"metadata\":{\"resourceVersion\":\"2345\",\"continue\":\"abc\",\"remainingItemCount\":1},\"items\":[{\"kind\":\"Carp\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"pod\",\"namespace\":\"default\"},\"spec\":{},\"status\":{}}]}\n",
},
{
name: "List two elements",
in: &testapigroupv1.CarpList{
TypeMeta: metav1.TypeMeta{
Kind: "List",
APIVersion: "v1",
},
ListMeta: metav1.ListMeta{
ResourceVersion: "2345",
},
Items: []testapigroupv1.Carp{
{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Carp"}, ObjectMeta: metav1.ObjectMeta{
Name: "pod",
Namespace: "default",
}},
{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Carp"}, ObjectMeta: metav1.ObjectMeta{
Name: "pod2",
Namespace: "default2",
}},
},
},
expect: `{"kind":"List","apiVersion":"v1","metadata":{"resourceVersion":"2345"},"items":[{"kind":"Carp","apiVersion":"v1","metadata":{"name":"pod","namespace":"default"},"spec":{},"status":{}},{"kind":"Carp","apiVersion":"v1","metadata":{"name":"pod2","namespace":"default2"},"spec":{},"status":{}}]}
`,
},
{
name: "List with extra field cannot be streamed",
in: &ListWithAdditionalFields{
TypeMeta: metav1.TypeMeta{
Kind: "List",
APIVersion: "v1",
},
ListMeta: metav1.ListMeta{
ResourceVersion: "2345",
},
Items: []testapigroupv1.Carp{},
},
cannotStream: true,
expect: "{\"kind\":\"List\",\"apiVersion\":\"v1\",\"metadata\":{\"resourceVersion\":\"2345\"},\"items\":[],\"AdditionalField\":0}\n",
},
{
name: "Not a collection cannot be streamed",
in: &testapigroupv1.Carp{
TypeMeta: metav1.TypeMeta{
Kind: "List",
APIVersion: "v1",
},
},
cannotStream: true,
expect: "{\"kind\":\"List\",\"apiVersion\":\"v1\",\"metadata\":{},\"spec\":{},\"status\":{}}\n",
},
{
name: "UnstructuredList empty",
in: &unstructured.UnstructuredList{},
expect: "{\"items\":[]}\n",
},
{
name: "UnstructuredList just kind",
in: &unstructured.UnstructuredList{
Object: map[string]interface{}{"kind": "List"},
},
expect: "{\"items\":[],\"kind\":\"List\"}\n",
},
{
name: "UnstructuredList just apiVersion",
in: &unstructured.UnstructuredList{
Object: map[string]interface{}{"apiVersion": "v1"},
},
expect: "{\"apiVersion\":\"v1\",\"items\":[]}\n",
},
{
name: "UnstructuredList no elements",
in: &unstructured.UnstructuredList{
Object: map[string]interface{}{"kind": "List", "apiVersion": "v1", "metadata": map[string]interface{}{"resourceVersion": "2345"}},
Items: []unstructured.Unstructured{},
},
expect: "{\"apiVersion\":\"v1\",\"items\":[],\"kind\":\"List\",\"metadata\":{\"resourceVersion\":\"2345\"}}\n",
},
{
name: "UnstructuredList one element with continue",
in: &unstructured.UnstructuredList{
Object: map[string]interface{}{"kind": "List", "apiVersion": "v1", "metadata": map[string]interface{}{
"resourceVersion": "2345",
"continue": "abc",
"remainingItemCount": "1",
}},
Items: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Carp",
"metadata": map[string]interface{}{
"name": "pod",
"namespace": "default",
},
},
},
},
},
expect: "{\"apiVersion\":\"v1\",\"items\":[{\"apiVersion\":\"v1\",\"kind\":\"Carp\",\"metadata\":{\"name\":\"pod\",\"namespace\":\"default\"}}],\"kind\":\"List\",\"metadata\":{\"continue\":\"abc\",\"remainingItemCount\":\"1\",\"resourceVersion\":\"2345\"}}\n",
},
{
name: "UnstructuredList two elements",
in: &unstructured.UnstructuredList{
Object: map[string]interface{}{"kind": "List", "apiVersion": "v1", "metadata": map[string]interface{}{
"resourceVersion": "2345",
}},
Items: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Carp",
"metadata": map[string]interface{}{
"name": "pod",
"namespace": "default",
},
},
},
{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Carp",
"metadata": map[string]interface{}{
"name": "pod2",
"namespace": "default",
},
},
},
},
},
expect: "{\"apiVersion\":\"v1\",\"items\":[{\"apiVersion\":\"v1\",\"kind\":\"Carp\",\"metadata\":{\"name\":\"pod\",\"namespace\":\"default\"}},{\"apiVersion\":\"v1\",\"kind\":\"Carp\",\"metadata\":{\"name\":\"pod2\",\"namespace\":\"default\"}}],\"kind\":\"List\",\"metadata\":{\"resourceVersion\":\"2345\"}}\n",
},
{
name: "UnstructuredList conflict on items",
in: &unstructured.UnstructuredList{
Object: map[string]interface{}{"items": []unstructured.Unstructured{
{
Object: map[string]interface{}{
"name": "pod",
},
},
},
},
Items: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"name": "pod2",
},
},
},
},
expect: "{\"items\":[{\"name\":\"pod2\"}]}\n",
},
} {
t.Run(tc.name, func(t *testing.T) {
buf.Reset()
if err := s.Encode(tc.in, &buf); err != nil {
t.Fatalf("unexpected error: %v", err)
}
t.Logf("encoded: %s", buf.String())
if diff := cmp.Diff(buf.String(), tc.expect); diff != "" {
t.Errorf("not matching:\n%s", diff)
}
expectStreaming := !tc.cannotStream && streamingEnabled
if expectStreaming && buf.writeCount <= 1 {
t.Errorf("expected streaming but Write was called only: %d", buf.writeCount)
}
if !expectStreaming && buf.writeCount > 1 {
t.Errorf("expected non-streaming but Write was called more than once: %d", buf.writeCount)
}
})
}
}
type StructWithFloatsList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
Items []StructWithFloats `json:"items" protobuf:"bytes,2,rep,name=items"`
}
func (l *StructWithFloatsList) DeepCopyObject() runtime.Object {
return nil
}
type StructWithFloats struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
Int int
Float32 float32
Float64 float64
}
func (s *StructWithFloats) DeepCopyObject() runtime.Object {
return nil
}
type StructWithDuplicatedTagsList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
Items []StructWithDuplicatedTags `json:"items" protobuf:"bytes,2,rep,name=items"`
}
func (l *StructWithDuplicatedTagsList) DeepCopyObject() runtime.Object {
return nil
}
type StructWithDuplicatedTags struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
Key1 string `json:"key"`
Key2 string `json:"key"` //nolint:govet
}
func (s *StructWithDuplicatedTags) DeepCopyObject() runtime.Object {
return nil
}
type ListWithMarshalJSONList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
Items []string `json:"items" protobuf:"bytes,2,rep,name=items"`
}
func (l *ListWithMarshalJSONList) DeepCopyObject() runtime.Object {
return nil
}
func (l *ListWithMarshalJSONList) MarshalJSON() ([]byte, error) {
return []byte(`"marshallJSON"`), nil
}
type StructWithMarshalJSONList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
Items []StructWithMarshalJSON `json:"items" protobuf:"bytes,2,rep,name=items"`
}
func (s *StructWithMarshalJSONList) DeepCopyObject() runtime.Object {
return nil
}
type StructWithMarshalJSON struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
}
func (l *StructWithMarshalJSON) DeepCopyObject() runtime.Object {
return nil
}
func (l *StructWithMarshalJSON) MarshalJSON() ([]byte, error) {
return []byte(`"marshallJSON"`), nil
}
type StructWithRawBytesList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
Items []StructWithRawBytes `json:"items" protobuf:"bytes,2,rep,name=items"`
}
func (s *StructWithRawBytesList) DeepCopyObject() runtime.Object {
return nil
}
type StructWithRawBytes struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
Slice []byte
Array [3]byte
}
func (s *StructWithRawBytes) DeepCopyObject() runtime.Object {
return nil
}
type ListWithAdditionalFields struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
Items []testapigroupv1.Carp `json:"items" protobuf:"bytes,2,rep,name=items"`
AdditionalField int
}
func (s *ListWithAdditionalFields) DeepCopyObject() runtime.Object {
return nil
}
type writeCountingBuffer struct {
writeCount int
bytes.Buffer
}
func (b *writeCountingBuffer) Write(data []byte) (int, error) {
b.writeCount++
return b.Buffer.Write(data)
}
func (b *writeCountingBuffer) Reset() {
b.writeCount = 0
b.Buffer.Reset()
}
func TestFuzzCollectionsEncoding(t *testing.T) {
disableFuzzFieldsV1 := func(field *metav1.FieldsV1, c randfill.Continue) {}
fuzzUnstructuredList := func(list *unstructured.UnstructuredList, c randfill.Continue) {
list.Object = map[string]interface{}{
"kind": "List",
"apiVersion": "v1",
c.String(0): c.String(0),
c.String(0): c.Uint64(),
c.String(0): c.Bool(),
"metadata": map[string]interface{}{
"resourceVersion": fmt.Sprintf("%d", c.Uint64()),
"continue": c.String(0),
"remainingItemCount": fmt.Sprintf("%d", c.Uint64()),
c.String(0): c.String(0),
}}
c.Fill(&list.Items)
}
fuzzMap := func(kvs map[string]interface{}, c randfill.Continue) {
kvs[c.String(0)] = c.Bool()
kvs[c.String(0)] = c.Uint64()
kvs[c.String(0)] = c.String(0)
}
f := randfill.New().Funcs(disableFuzzFieldsV1, fuzzUnstructuredList, fuzzMap)
streamingBuffer := &bytes.Buffer{}
normalSerializer := NewSerializerWithOptions(DefaultMetaFactory, nil, nil, SerializerOptions{StreamingCollectionsEncoding: false})
normalBuffer := &bytes.Buffer{}
t.Run("CarpList", func(t *testing.T) {
for i := 0; i < 1000; i++ {
list := &testapigroupv1.CarpList{}
f.Fill(list)
streamingBuffer.Reset()
normalBuffer.Reset()
ok, err := streamEncodeCollections(list, streamingBuffer)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Fatalf("expected streaming encoder to encode %T", list)
}
if err := normalSerializer.Encode(list, normalBuffer); err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(normalBuffer.String(), streamingBuffer.String()); diff != "" {
t.Logf("normal: %s", normalBuffer.String())
t.Logf("streaming: %s", streamingBuffer.String())
t.Errorf("not matching:\n%s", diff)
}
}
})
t.Run("UnstructuredList", func(t *testing.T) {
for i := 0; i < 1000; i++ {
list := &unstructured.UnstructuredList{}
f.Fill(list)
streamingBuffer.Reset()
normalBuffer.Reset()
ok, err := streamEncodeCollections(list, streamingBuffer)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Fatalf("expected streaming encoder to encode %T", list)
}
if err := normalSerializer.Encode(list, normalBuffer); err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(normalBuffer.String(), streamingBuffer.String()); diff != "" {
t.Logf("normal: %s", normalBuffer.String())
t.Logf("streaming: %s", streamingBuffer.String())
t.Errorf("not matching:\n%s", diff)
}
}
})
}

View File

@ -36,7 +36,7 @@ import (
// is not nil, the object has the group, version, and kind fields set.
// Deprecated: use NewSerializerWithOptions instead.
func NewSerializer(meta MetaFactory, creater runtime.ObjectCreater, typer runtime.ObjectTyper, pretty bool) *Serializer {
return NewSerializerWithOptions(meta, creater, typer, SerializerOptions{false, pretty, false})
return NewSerializerWithOptions(meta, creater, typer, SerializerOptions{false, pretty, false, false})
}
// NewYAMLSerializer creates a YAML serializer that handles encoding versioned objects into the proper YAML form. If typer
@ -44,7 +44,7 @@ func NewSerializer(meta MetaFactory, creater runtime.ObjectCreater, typer runtim
// matches JSON, and will error if constructs are used that do not serialize to JSON.
// Deprecated: use NewSerializerWithOptions instead.
func NewYAMLSerializer(meta MetaFactory, creater runtime.ObjectCreater, typer runtime.ObjectTyper) *Serializer {
return NewSerializerWithOptions(meta, creater, typer, SerializerOptions{true, false, false})
return NewSerializerWithOptions(meta, creater, typer, SerializerOptions{true, false, false, false})
}
// NewSerializerWithOptions creates a JSON/YAML serializer that handles encoding versioned objects into the proper JSON/YAML
@ -93,6 +93,9 @@ type SerializerOptions struct {
// Strict: configures the Serializer to return strictDecodingError's when duplicate fields are present decoding JSON or YAML.
// Note that enabling this option is not as performant as the non-strict variant, and should not be used in fast paths.
Strict bool
// StreamingCollectionsEncoding enables encoding collection, one item at the time, drastically reducing memory needed.
StreamingCollectionsEncoding bool
}
// Serializer handles encoding versioned objects into the proper JSON form
@ -242,6 +245,15 @@ func (s *Serializer) doEncode(obj runtime.Object, w io.Writer) error {
_, err = w.Write(data)
return err
}
if s.options.StreamingCollectionsEncoding {
ok, err := streamEncodeCollections(obj, w)
if err != nil {
return err
}
if ok {
return nil
}
}
encoder := json.NewEncoder(w)
return encoder.Encode(obj)
}

View File

@ -0,0 +1,174 @@
/*
Copyright 2025 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 protobuf
import (
"errors"
"io"
"math/bits"
"github.com/gogo/protobuf/proto"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/apimachinery/pkg/runtime"
)
var (
errFieldCount = errors.New("expected ListType to have 3 fields")
errTypeMetaField = errors.New("expected TypeMeta field to have TypeMeta type")
errTypeMetaProtobufTag = errors.New(`expected TypeMeta protobuf field tag to be ""`)
errListMetaField = errors.New("expected ListMeta field to have ListMeta type")
errListMetaProtobufTag = errors.New(`expected ListMeta protobuf field tag to be "bytes,1,opt,name=metadata"`)
errItemsProtobufTag = errors.New(`expected Items protobuf field tag to be "bytes,2,rep,name=items"`)
errItemsSizer = errors.New(`expected Items elements to implement proto.Sizer`)
)
// getStreamingListData implements list extraction logic for protobuf stream serialization.
//
// Reason for a custom logic instead of reusing accessors from meta package:
// * Validate proto tags to prevent incompatibility with proto standard package.
// * ListMetaAccessor doesn't distinguish empty from nil value.
// * TypeAccessor reparsing "apiVersion" and serializing it with "{group}/{version}"
func getStreamingListData(list runtime.Object) (data streamingListData, err error) {
listValue, err := conversion.EnforcePtr(list)
if err != nil {
return data, err
}
listType := listValue.Type()
if listType.NumField() != 3 {
return data, errFieldCount
}
// TypeMeta: validated, but not returned as is not serialized.
_, ok := listValue.Field(0).Interface().(metav1.TypeMeta)
if !ok {
return data, errTypeMetaField
}
if listType.Field(0).Tag.Get("protobuf") != "" {
return data, errTypeMetaProtobufTag
}
// ListMeta
listMeta, ok := listValue.Field(1).Interface().(metav1.ListMeta)
if !ok {
return data, errListMetaField
}
// if we were ever to relax the protobuf tag check we should update the hardcoded `0xa` below when writing ListMeta.
if listType.Field(1).Tag.Get("protobuf") != "bytes,1,opt,name=metadata" {
return data, errListMetaProtobufTag
}
data.listMeta = listMeta
// Items; if we were ever to relax the protobuf tag check we should update the hardcoded `0x12` below when writing Items.
if listType.Field(2).Tag.Get("protobuf") != "bytes,2,rep,name=items" {
return data, errItemsProtobufTag
}
items, err := meta.ExtractList(list)
if err != nil {
return data, err
}
data.items = items
data.totalSize, data.listMetaSize, data.itemsSizes, err = listSize(listMeta, items)
return data, err
}
type streamingListData struct {
// totalSize is the total size of the serialized List object, including their proto headers/size bytes
totalSize int
// listMetaSize caches results from .Size() call to listMeta, doesn't include header bytes (field identifier, size)
listMetaSize int
listMeta metav1.ListMeta
// itemsSizes caches results from .Size() call to items, doesn't include header bytes (field identifier, size)
itemsSizes []int
items []runtime.Object
}
// listSize return size of ListMeta and items to be later used for preallocations.
// listMetaSize and itemSizes do not include header bytes (field identifier, size).
func listSize(listMeta metav1.ListMeta, items []runtime.Object) (totalSize, listMetaSize int, itemSizes []int, err error) {
// ListMeta
listMetaSize = listMeta.Size()
totalSize += 1 + sovGenerated(uint64(listMetaSize)) + listMetaSize
// Items
itemSizes = make([]int, len(items))
for i, item := range items {
sizer, ok := item.(proto.Sizer)
if !ok {
return totalSize, listMetaSize, nil, errItemsSizer
}
n := sizer.Size()
itemSizes[i] = n
totalSize += 1 + sovGenerated(uint64(n)) + n
}
return totalSize, listMetaSize, itemSizes, nil
}
func streamingEncodeUnknownList(w io.Writer, unk runtime.Unknown, listData streamingListData, memAlloc runtime.MemoryAllocator) error {
_, err := w.Write(protoEncodingPrefix)
if err != nil {
return err
}
// encodeList is responsible for encoding the List into the unknown Raw.
encodeList := func(writer io.Writer) (int, error) {
return streamingEncodeList(writer, listData, memAlloc)
}
_, err = unk.MarshalToWriter(w, listData.totalSize, encodeList)
return err
}
func streamingEncodeList(w io.Writer, listData streamingListData, memAlloc runtime.MemoryAllocator) (size int, err error) {
// ListMeta; 0xa = (1 << 3) | 2; field number: 1, type: 2 (LEN). https://protobuf.dev/programming-guides/encoding/#structure
n, err := doEncodeWithHeader(&listData.listMeta, w, 0xa, listData.listMetaSize, memAlloc)
size += n
if err != nil {
return size, err
}
// Items; 0x12 = (2 << 3) | 2; field number: 2, type: 2 (LEN). https://protobuf.dev/programming-guides/encoding/#structure
for i, item := range listData.items {
n, err := doEncodeWithHeader(item, w, 0x12, listData.itemsSizes[i], memAlloc)
size += n
if err != nil {
return size, err
}
}
return size, nil
}
func writeVarintGenerated(w io.Writer, v int) (int, error) {
buf := make([]byte, sovGenerated(uint64(v)))
encodeVarintGenerated(buf, len(buf), uint64(v))
return w.Write(buf)
}
// sovGenerated is copied from `generated.pb.go` returns size of varint.
func sovGenerated(v uint64) int {
return (bits.Len64(v|1) + 6) / 7
}
// encodeVarintGenerated is copied from `generated.pb.go` encodes varint.
func encodeVarintGenerated(dAtA []byte, offset int, v uint64) int {
offset -= sovGenerated(v)
base := offset
for v >= 1<<7 {
dAtA[offset] = uint8(v&0x7f | 0x80)
v >>= 7
offset++
}
dAtA[offset] = uint8(v)
return base
}

View File

@ -0,0 +1,333 @@
/*
Copyright 2025 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 protobuf
import (
"bytes"
"encoding/base64"
"io"
"os/exec"
"testing"
"github.com/gogo/protobuf/proto"
"github.com/google/go-cmp/cmp"
"sigs.k8s.io/randfill"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
testapigroupv1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
func TestCollectionsEncoding(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
testCollectionsEncoding(t, NewSerializer(nil, nil), false)
})
t.Run("Streaming", func(t *testing.T) {
testCollectionsEncoding(t, NewSerializerWithOptions(nil, nil, SerializerOptions{StreamingCollectionsEncoding: true}), true)
})
}
func testCollectionsEncoding(t *testing.T, s *Serializer, streamingEnabled bool) {
var remainingItems int64 = 1
testCases := []struct {
name string
in runtime.Object
// expect is base64 encoded protobuf bytes
expect string
}{
{
name: "CarpList items nil",
in: &testapigroupv1.CarpList{
Items: nil,
},
expect: "azhzAAoECgASABIICgYKABIAGgAaACIA",
},
{
name: "CarpList slice nil",
in: &testapigroupv1.CarpList{
Items: []testapigroupv1.Carp{
{
Status: testapigroupv1.CarpStatus{
Conditions: nil,
},
},
},
},
expect: "azhzAAoECgASABJBCgYKABIAGgASNwoQCgASABoAIgAqADIAOABCABIXGgBCAEoAUgBYAGAAaACCAQCKAQCaAQAaCgoAGgAiACoAMgAaACIA",
},
{
name: "CarpList map nil",
in: &testapigroupv1.CarpList{
Items: []testapigroupv1.Carp{
{
Spec: testapigroupv1.CarpSpec{
NodeSelector: nil,
},
},
},
},
expect: "azhzAAoECgASABJBCgYKABIAGgASNwoQCgASABoAIgAqADIAOABCABIXGgBCAEoAUgBYAGAAaACCAQCKAQCaAQAaCgoAGgAiACoAMgAaACIA",
},
{
name: "CarpList items empty",
in: &testapigroupv1.CarpList{
Items: []testapigroupv1.Carp{},
},
expect: "azhzAAoECgASABIICgYKABIAGgAaACIA",
},
{
name: "CarpList slice empty",
in: &testapigroupv1.CarpList{
Items: []testapigroupv1.Carp{
{
Status: testapigroupv1.CarpStatus{
Conditions: []testapigroupv1.CarpCondition{},
},
},
},
},
expect: "azhzAAoECgASABJBCgYKABIAGgASNwoQCgASABoAIgAqADIAOABCABIXGgBCAEoAUgBYAGAAaACCAQCKAQCaAQAaCgoAGgAiACoAMgAaACIA",
},
{
name: "CarpList map empty",
in: &testapigroupv1.CarpList{
Items: []testapigroupv1.Carp{
{
Spec: testapigroupv1.CarpSpec{
NodeSelector: map[string]string{},
},
},
},
},
expect: "azhzAAoECgASABJBCgYKABIAGgASNwoQCgASABoAIgAqADIAOABCABIXGgBCAEoAUgBYAGAAaACCAQCKAQCaAQAaCgoAGgAiACoAMgAaACIA",
},
{
name: "List just kind",
in: &testapigroupv1.CarpList{
TypeMeta: metav1.TypeMeta{
Kind: "List",
},
},
expect: "azhzAAoICgASBExpc3QSCAoGCgASABoAGgAiAA==",
},
{
name: "List just apiVersion",
in: &testapigroupv1.CarpList{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
},
},
expect: "azhzAAoGCgJ2MRIAEggKBgoAEgAaABoAIgA=",
},
{
name: "List no elements",
in: &testapigroupv1.CarpList{
TypeMeta: metav1.TypeMeta{
Kind: "List",
APIVersion: "v1",
},
ListMeta: metav1.ListMeta{
ResourceVersion: "2345",
},
Items: []testapigroupv1.Carp{},
},
expect: "azhzAAoKCgJ2MRIETGlzdBIMCgoKABIEMjM0NRoAGgAiAA==",
},
{
name: "List one element with continue",
in: &testapigroupv1.CarpList{
TypeMeta: metav1.TypeMeta{
Kind: "List",
APIVersion: "v1",
},
ListMeta: metav1.ListMeta{
ResourceVersion: "2345",
Continue: "abc",
RemainingItemCount: &remainingItems,
},
Items: []testapigroupv1.Carp{
{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Carp"}, ObjectMeta: metav1.ObjectMeta{
Name: "pod",
Namespace: "default",
}},
},
},
expect: "azhzAAoKCgJ2MRIETGlzdBJUCg8KABIEMjM0NRoDYWJjIAESQQoaCgNwb2QSABoHZGVmYXVsdCIAKgAyADgAQgASFxoAQgBKAFIAWABgAGgAggEAigEAmgEAGgoKABoAIgAqADIAGgAiAA==",
},
{
name: "List two elements",
in: &testapigroupv1.CarpList{
TypeMeta: metav1.TypeMeta{
Kind: "List",
APIVersion: "v1",
},
ListMeta: metav1.ListMeta{
ResourceVersion: "2345",
},
Items: []testapigroupv1.Carp{
{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Carp"}, ObjectMeta: metav1.ObjectMeta{
Name: "pod",
Namespace: "default",
}},
{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Carp"}, ObjectMeta: metav1.ObjectMeta{
Name: "pod2",
Namespace: "default2",
}},
},
},
expect: "azhzAAoKCgJ2MRIETGlzdBKUAQoKCgASBDIzNDUaABJBChoKA3BvZBIAGgdkZWZhdWx0IgAqADIAOABCABIXGgBCAEoAUgBYAGAAaACCAQCKAQCaAQAaCgoAGgAiACoAMgASQwocCgRwb2QyEgAaCGRlZmF1bHQyIgAqADIAOABCABIXGgBCAEoAUgBYAGAAaACCAQCKAQCaAQAaCgoAGgAiACoAMgAaACIA",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var buf writeCountingBuffer
if err := s.Encode(tc.in, &buf); err != nil {
t.Fatalf("unexpected error: %v", err)
}
actualBytes := buf.Bytes()
expectBytes, err := io.ReadAll(base64.NewDecoder(base64.StdEncoding, bytes.NewBufferString(tc.expect)))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(expectBytes, actualBytes) {
expectedBytes, err := base64.StdEncoding.DecodeString(tc.expect)
if err == nil {
t.Errorf("expected:\n%v\ngot:\n%v", expectedBytes, actualBytes)
} else {
t.Errorf("expected:\n%v\ngot:\n%v", tc.expect, base64.StdEncoding.EncodeToString(actualBytes))
}
actualProto := dumpProto(t, actualBytes[4:])
expectedProto := dumpProto(t, expectBytes[4:])
if actualProto != "" && expectedProto != "" {
t.Log(cmp.Diff(actualProto, expectedProto))
} else {
t.Log(cmp.Diff(actualBytes, expectBytes))
}
}
if streamingEnabled && buf.writeCount <= 1 {
t.Errorf("expected streaming but Write was called only: %d", buf.writeCount)
}
if !streamingEnabled && buf.writeCount > 1 {
t.Errorf("expected non-streaming but Write was called more than once: %d", buf.writeCount)
}
})
}
}
// dumpProto does a best-effort dump of the given proto bytes using protoc if it can be found in the path.
// This is only used when the test has already failed, to try to give more visibility into the diff of the failure.
func dumpProto(t *testing.T, data []byte) string {
t.Helper()
protoc, err := exec.LookPath("protoc")
if err != nil {
t.Logf("cannot find protoc in path to dump proto contents: %v", err)
return ""
}
cmd := exec.Command(protoc, "--decode_raw")
cmd.Stdin = bytes.NewBuffer(data)
d, err := cmd.CombinedOutput()
if err != nil {
t.Logf("protoc invocation failed: %v", err)
return ""
}
return string(d)
}
type writeCountingBuffer struct {
writeCount int
bytes.Buffer
}
func (b *writeCountingBuffer) Write(data []byte) (int, error) {
b.writeCount++
return b.Buffer.Write(data)
}
func (b *writeCountingBuffer) Reset() {
b.writeCount = 0
b.Buffer.Reset()
}
func TestFuzzCollection(t *testing.T) {
f := randfill.New()
streamingEncoder := NewSerializerWithOptions(nil, nil, SerializerOptions{StreamingCollectionsEncoding: true})
streamingBuffer := &bytes.Buffer{}
normalEncoder := NewSerializerWithOptions(nil, nil, SerializerOptions{StreamingCollectionsEncoding: false})
normalBuffer := &bytes.Buffer{}
for i := 0; i < 1000; i++ {
list := &testapigroupv1.CarpList{}
f.FillNoCustom(list)
streamingBuffer.Reset()
normalBuffer.Reset()
if err := streamingEncoder.Encode(list, streamingBuffer); err != nil {
t.Fatal(err)
}
if err := normalEncoder.Encode(list, normalBuffer); err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(streamingBuffer.String(), normalBuffer.String()); diff != "" {
t.Logf("normal: %s", normalBuffer.String())
t.Logf("streaming: %s", streamingBuffer.String())
t.Fatalf("unexpected output:\n%s", diff)
}
}
}
func TestCallsToSize(t *testing.T) {
counter := &countingSizer{data: []byte("abba")}
listMeta := metav1.ListMeta{}
listData := streamingListData{
totalSize: 14,
listMeta: listMeta,
listMetaSize: listMeta.Size(),
itemsSizes: []int{counter.Size()},
items: []runtime.Object{counter},
}
err := streamingEncodeUnknownList(io.Discard, runtime.Unknown{}, listData, &runtime.Allocator{})
if err != nil {
t.Fatal(err)
}
if counter.count != 1 {
t.Errorf("Expected only 1 call to sizer, got %d", counter.count)
}
}
type countingSizer struct {
data []byte
count int
}
var _ proto.Sizer = (*countingSizer)(nil)
var _ runtime.ProtobufMarshaller = (*countingSizer)(nil)
func (s *countingSizer) MarshalTo(data []byte) (int, error) {
return copy(data, s.data), nil
}
func (s *countingSizer) Size() int {
s.count++
return len(s.data)
}
func (s *countingSizer) DeepCopyObject() runtime.Object {
return nil
}
func (s *countingSizer) GetObjectKind() schema.ObjectKind {
return nil
}

View File

@ -72,10 +72,18 @@ func IsNotMarshalable(err error) bool {
// is passed, the encoded object will have group, version, and kind fields set. If typer is nil, the objects will be written
// as-is (any type info passed with the object will be used).
func NewSerializer(creater runtime.ObjectCreater, typer runtime.ObjectTyper) *Serializer {
return NewSerializerWithOptions(creater, typer, SerializerOptions{})
}
// NewSerializerWithOptions creates a Protobuf serializer that handles encoding versioned objects into the proper wire form. If a typer
// is passed, the encoded object will have group, version, and kind fields set. If typer is nil, the objects will be written
// as-is (any type info passed with the object will be used).
func NewSerializerWithOptions(creater runtime.ObjectCreater, typer runtime.ObjectTyper, opts SerializerOptions) *Serializer {
return &Serializer{
prefix: protoEncodingPrefix,
creater: creater,
typer: typer,
options: opts,
}
}
@ -84,6 +92,14 @@ type Serializer struct {
prefix []byte
creater runtime.ObjectCreater
typer runtime.ObjectTyper
options SerializerOptions
}
// SerializerOptions holds the options which are used to configure a Proto serializer.
type SerializerOptions struct {
// StreamingCollectionsEncoding enables encoding collection, one item at the time, drastically reducing memory needed.
StreamingCollectionsEncoding bool
}
var _ runtime.Serializer = &Serializer{}
@ -209,6 +225,13 @@ func (s *Serializer) doEncode(obj runtime.Object, w io.Writer, memAlloc runtime.
},
}
}
if s.options.StreamingCollectionsEncoding {
listData, err := getStreamingListData(obj)
if err == nil {
// Doesn't honor custom proto marshaling methods (like json streaming), because all proto objects implement proto methods.
return streamingEncodeUnknownList(w, unk, listData, memAlloc)
}
}
switch t := obj.(type) {
case bufferedMarshaller:
@ -428,6 +451,39 @@ func (s *RawSerializer) encode(obj runtime.Object, w io.Writer, memAlloc runtime
}
func (s *RawSerializer) doEncode(obj runtime.Object, w io.Writer, memAlloc runtime.MemoryAllocator) error {
_, err := doEncode(obj, w, nil, memAlloc)
return err
}
func doEncodeWithHeader(obj any, w io.Writer, field byte, precomputedSize int, memAlloc runtime.MemoryAllocator) (size int, err error) {
// Field identifier
n, err := w.Write([]byte{field})
size += n
if err != nil {
return size, err
}
// Size
n, err = writeVarintGenerated(w, precomputedSize)
size += n
if err != nil {
return size, err
}
// Obj
n, err = doEncode(obj, w, &precomputedSize, memAlloc)
size += n
if err != nil {
return size, err
}
if n != precomputedSize {
return size, fmt.Errorf("the size value was %d, but doEncode wrote %d bytes to data", precomputedSize, n)
}
return size, nil
}
// doEncode encodes provided object into writer using a allocator if possible.
// Avoids call by object Size if precomputedObjSize is provided.
// precomputedObjSize should not include header bytes (field identifier, size).
func doEncode(obj any, w io.Writer, precomputedObjSize *int, memAlloc runtime.MemoryAllocator) (int, error) {
if memAlloc == nil {
klog.Error("a mandatory memory allocator wasn't provided, this might have a negative impact on performance, check invocations of EncodeWithAllocator method, falling back on runtime.SimpleAllocator")
memAlloc = &runtime.SimpleAllocator{}
@ -436,40 +492,43 @@ func (s *RawSerializer) doEncode(obj runtime.Object, w io.Writer, memAlloc runti
case bufferedReverseMarshaller:
// this path performs a single allocation during write only when the Allocator wasn't provided
// it also requires the caller to implement the more efficient Size and MarshalToSizedBuffer methods
encodedSize := uint64(t.Size())
data := memAlloc.Allocate(encodedSize)
if precomputedObjSize == nil {
s := t.Size()
precomputedObjSize = &s
}
data := memAlloc.Allocate(uint64(*precomputedObjSize))
n, err := t.MarshalToSizedBuffer(data)
if err != nil {
return err
return 0, err
}
_, err = w.Write(data[:n])
return err
return w.Write(data[:n])
case bufferedMarshaller:
// this path performs a single allocation during write only when the Allocator wasn't provided
// it also requires the caller to implement the more efficient Size and MarshalTo methods
encodedSize := uint64(t.Size())
data := memAlloc.Allocate(encodedSize)
if precomputedObjSize == nil {
s := t.Size()
precomputedObjSize = &s
}
data := memAlloc.Allocate(uint64(*precomputedObjSize))
n, err := t.MarshalTo(data)
if err != nil {
return err
return 0, err
}
_, err = w.Write(data[:n])
return err
return w.Write(data[:n])
case proto.Marshaler:
// this path performs extra allocations
data, err := t.Marshal()
if err != nil {
return err
return 0, err
}
_, err = w.Write(data)
return err
return w.Write(data)
default:
return errNotMarshalable{reflect.TypeOf(obj)}
return 0, errNotMarshalable{reflect.TypeOf(obj)}
}
}

Some files were not shown because too many files have changed in this diff Show More