Compare commits

...

132 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
126 changed files with 5500 additions and 1380 deletions

27
go.mod
View File

@ -9,26 +9,27 @@ 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/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/spf13/pflag v1.0.5
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.33.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-20250318190949-c8a335a9a2ff
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3
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/v4 v4.6.0
sigs.k8s.io/yaml v1.4.0
sigs.k8s.io/structured-merge-diff/v6 v6.3.0
sigs.k8s.io/yaml v1.6.0
)
require (
@ -40,15 +41,15 @@ require (
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.30.0 // indirect
golang.org/x/text v0.22.0 // 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
)

53
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,9 +20,8 @@ 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/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=
@ -50,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,8 +64,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
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=
@ -79,6 +79,10 @@ 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=
@ -88,20 +92,20 @@ 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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.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.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
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=
@ -128,16 +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-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
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/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
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/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
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

@ -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,7 +24,7 @@ import (
"strings"
"testing"
"github.com/google/go-cmp/cmp" //nolint:depguard
"github.com/google/go-cmp/cmp"
flag "github.com/spf13/pflag"
"sigs.k8s.io/randfill"

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

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

@ -16,7 +16,10 @@ limitations under the License.
package operation
import "k8s.io/apimachinery/pkg/util/sets"
import (
"slices"
"strings"
)
// Operation provides contextual information about a validation request and the API
// operation being validated.
@ -41,7 +44,50 @@ type Operation struct {
// resource first began using the feature.
//
// Unset options are disabled/false.
Options sets.Set[string]
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.

View File

@ -16,10 +16,16 @@ 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.
// 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
@ -35,3 +41,19 @@ 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

@ -18,6 +18,7 @@ package content
import (
"fmt"
"reflect"
"k8s.io/apimachinery/pkg/api/validate/constraints"
)
@ -27,3 +28,12 @@ import (
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)
}

View File

@ -18,25 +18,45 @@ package validate
import (
"context"
"sort"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// CompareFunc is a function that compares two values of the same type.
type CompareFunc[T any] func(T, T) bool
// 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 validates each element of newSlice with the specified
// validation function. The comparison function is used to find the
// corresponding value in oldSlice. The value-type of the slices is assumed to
// not be nilable.
// 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,
cmp CompareFunc[T], validator ValidateFunc[*T]) field.ErrorList {
match, equiv MatchFunc[T], validator ValidateFunc[*T]) field.ErrorList {
var errs field.ErrorList
for i, val := range newSlice {
var old *T
if cmp != nil && len(oldSlice) > 0 {
old = lookup(oldSlice, val, cmp)
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)...)
}
@ -45,39 +65,107 @@ func EachSliceVal[T any](ctx context.Context, op operation.Operation, fldPath *f
// 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, cmp func(T, T) bool) *T {
func lookup[T any](list []T, target T, match MatchFunc[T]) *T {
for i := range list {
if cmp(list[i], target) {
if match(list[i], target) {
return &list[i]
}
}
return nil
}
// EachMapVal validates each element of newMap with the specified validation
// function and, if the corresponding key is found in oldMap, the old value.
// The value-type of the slices is assumed to not be nilable.
// 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,
validator ValidateFunc[*V]) field.ErrorList {
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. The oldMap argument is not used.
// 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

@ -25,6 +25,7 @@ import (
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/utils/ptr"
)
type TestStruct struct {
@ -32,6 +33,32 @@ type TestStruct struct {
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"})
@ -70,7 +97,7 @@ func testEachSliceVal[T any](t *testing.T, name string, input []T) {
calls++
return nil
}
_ = EachSliceVal(context.Background(), operation.Operation{}, field.NewPath("test"), input, nil, nil, vfn)
_ = 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)
}
@ -95,14 +122,98 @@ func testEachSliceValUpdate[T any](t *testing.T, name string, input []T) {
old := make([]T, len(input))
copy(old, input)
slices.Reverse(old)
cmp := func(a, b T) bool { return reflect.DeepEqual(a, b) }
_ = EachSliceVal(context.Background(), operation.Operation{}, field.NewPath("test"), input, old, cmp, vfn)
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"})
@ -129,13 +240,131 @@ func testEachMapVal[T any](t *testing.T, name string, input map[string]T) {
calls++
return nil
}
_ = EachMapVal(context.Background(), operation.Operation{}, field.NewPath("test"), input, nil, vfn)
_ = 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) {
@ -161,3 +390,103 @@ func testEachMapKey[K ~string, V any](t *testing.T, name string, input map[K]V)
}
})
}
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

@ -24,11 +24,15 @@ import (
"k8s.io/apimachinery/pkg/util/validation/field"
)
// Immutable 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 ImmutableNonComparable instead.
func Immutable[T comparable](_ context.Context, op operation.Operation, fldPath *field.Path, value, oldValue *T) field.ErrorList {
// 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
}
@ -43,11 +47,11 @@ func Immutable[T comparable](_ context.Context, op operation.Operation, fldPath
return nil
}
// ImmutableNonComparable verifies that the specified value has not changed in
// 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 Immutable, this function can be used with types that are
// provided. Unlike ImmutableByCompare, this function can be used with types that are
// not directly comparable, at the cost of performance.
func ImmutableNonComparable[T any](_ context.Context, op operation.Operation, fldPath *field.Path, value, oldValue T) field.ErrorList {
func ImmutableByReflect[T any](_ context.Context, op operation.Operation, fldPath *field.Path, value, oldValue T) field.ErrorList {
if op.Type != operation.Update {
return nil
}

View File

@ -25,15 +25,16 @@ import (
"k8s.io/utils/ptr"
)
type Struct struct {
type StructComparable struct {
S string
I int
B bool
}
func TestImmutable(t *testing.T) {
structA := Struct{"abc", 123, true}
structB := Struct{"xyz", 456, false}
func TestImmutableByCompare(t *testing.T) {
structA := StructComparable{"abc", 123, true}
structA2 := structA
structB := StructComparable{"xyz", 456, false}
for _, tc := range []struct {
name string
@ -42,62 +43,194 @@ func TestImmutable(t *testing.T) {
}{{
name: "nil both values",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return Immutable[int](context.Background(), op, fld, nil, nil)
return ImmutableByCompare[int](context.Background(), op, fld, nil, nil)
},
}, {
name: "nil value",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return Immutable(context.Background(), op, fld, nil, ptr.To(123))
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 Immutable(context.Background(), op, fld, ptr.To(123), nil)
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 Immutable(context.Background(), op, fld, ptr.To(123), ptr.To(123))
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 Immutable(context.Background(), op, fld, ptr.To(123), ptr.To(456))
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 Immutable(context.Background(), op, fld, ptr.To("abc"), ptr.To("abc"))
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 Immutable(context.Background(), op, fld, ptr.To("abc"), ptr.To("xyz"))
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 Immutable(context.Background(), op, fld, ptr.To(true), ptr.To(true))
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 Immutable(context.Background(), op, fld, ptr.To(true), ptr.To(false))
return ImmutableByCompare(context.Background(), op, fld, ptr.To(true), ptr.To(false))
},
fail: true,
}, {
name: "struct",
name: "same struct",
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
return Immutable(context.Background(), op, fld, ptr.To(structA), ptr.To(structA))
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 Immutable(context.Background(), op, fld, ptr.To(structA), ptr.To(structB))
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,
}} {

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

@ -83,7 +83,7 @@ func ForbiddenSlice[T any](_ context.Context, _ operation.Operation, fldPath *fi
return field.ErrorList{field.Forbidden(fldPath, "")}
}
// RequiredMap verifies that the specified map is empty.
// 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

View File

@ -36,6 +36,11 @@ func Subfield[Tstruct any, Tfield any](ctx context.Context, op operation.Operati
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
}

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,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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -385,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

@ -75,7 +75,7 @@ type Scheme struct {
// 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{}, subresources ...string) field.ErrorList
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.
@ -105,7 +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{}, subresource ...string) field.ErrorList{},
validationFuncs: map[reflect.Type]func(ctx context.Context, op operation.Operation, object, oldObject interface{}) field.ErrorList{},
versionPriority: map[string][]string{},
schemeName: naming.GetNameFromCallsite(internalPackages...),
}
@ -362,16 +362,16 @@ func (s *Scheme) Default(src Object) {
// 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{}, subresources ...string) field.ErrorList) {
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 sets.Set[string], object Object, subresources ...string) field.ErrorList {
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, Options: options}, object, nil, subresources...)
return fn(ctx, operation.Operation{Type: operation.Create, Request: operation.Request{Subresources: subresources}, Options: options}, object, nil)
}
return nil
}
@ -379,9 +379,9 @@ func (s *Scheme) Validate(ctx context.Context, options sets.Set[string], object
// 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 sets.Set[string], object, oldObject Object, subresources ...string) field.ErrorList {
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, Options: options}, object, oldObject, subresources...)
return fn(ctx, operation.Operation{Type: operation.Update, Request: operation.Request{Subresources: subresources}, Options: options}, object, oldObject)
}
return nil
}
@ -743,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

@ -20,12 +20,14 @@ 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"
@ -33,7 +35,6 @@ 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/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
)
@ -1026,7 +1027,7 @@ func TestRegisterValidate(t *testing.T) {
object runtime.Object
oldObject runtime.Object
subresource []string
options sets.Set[string]
options []string
expected field.ErrorList
}{
{
@ -1048,7 +1049,7 @@ func TestRegisterValidate(t *testing.T) {
{
name: "options error",
object: &TestType1{},
options: sets.New("option1"),
options: []string{"option1"},
expected: field.ErrorList{invalidIfOptionErr},
},
{
@ -1063,17 +1064,17 @@ func TestRegisterValidate(t *testing.T) {
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{}, subresources ...string) field.ErrorList {
if op.Options.Has("option1") {
s.AddValidationFunc(&TestType1{}, func(ctx context.Context, op operation.Operation, object, oldObject interface{}) field.ErrorList {
if op.HasOption("option1") {
return field.ErrorList{invalidIfOptionErr}
}
if len(subresources) == 1 && subresources[0] == "status" {
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{}, subresources ...string) field.ErrorList {
s.AddValidationFunc(&TestType2{}, func(ctx context.Context, op operation.Operation, object, oldObject interface{}) field.ErrorList {
if oldObject != nil {
return field.ErrorList{invalidLength}
}
@ -1113,3 +1114,66 @@ type TestType2 struct {
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

@ -23,11 +23,12 @@ import (
"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"
"sigs.k8s.io/randfill"
)
func TestCollectionsEncoding(t *testing.T) {
@ -63,7 +64,7 @@ func testCollectionsEncoding(t *testing.T, s *Serializer, streamingEnabled bool)
},
},
},
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{\"creationTimestamp\":null},\"Int\":1,\"Float32\":1,\"Float64\":1.1}]}\n",
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{},\"Int\":1,\"Float32\":1,\"Float64\":1.1}]}\n",
},
{
name: "Unstructured object float",
@ -102,7 +103,7 @@ func testCollectionsEncoding(t *testing.T, s *Serializer, streamingEnabled bool)
},
},
},
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{\"creationTimestamp\":null}}]}\n",
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{}}]}\n",
},
// Encoding Go strings containing invalid UTF-8 sequences without error
{
@ -146,7 +147,7 @@ func testCollectionsEncoding(t *testing.T, s *Serializer, streamingEnabled bool)
},
},
},
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{\"creationTimestamp\":null},\"spec\":{},\"status\":{}}]}\n",
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{},\"spec\":{},\"status\":{}}]}\n",
},
{
name: "CarpList map nil",
@ -159,7 +160,7 @@ func testCollectionsEncoding(t *testing.T, s *Serializer, streamingEnabled bool)
},
},
},
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{\"creationTimestamp\":null},\"spec\":{},\"status\":{}}]}\n",
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{},\"spec\":{},\"status\":{}}]}\n",
},
{
name: "UnstructuredList items nil",
@ -237,7 +238,7 @@ func testCollectionsEncoding(t *testing.T, s *Serializer, streamingEnabled bool)
},
},
},
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{\"creationTimestamp\":null},\"spec\":{},\"status\":{}}]}\n",
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{},\"spec\":{},\"status\":{}}]}\n",
},
{
name: "CarpList map empty",
@ -250,7 +251,7 @@ func testCollectionsEncoding(t *testing.T, s *Serializer, streamingEnabled bool)
},
},
},
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{\"creationTimestamp\":null},\"spec\":{},\"status\":{}}]}\n",
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{},\"spec\":{},\"status\":{}}]}\n",
},
{
name: "UnstructuredList items empty",
@ -337,7 +338,7 @@ func testCollectionsEncoding(t *testing.T, s *Serializer, streamingEnabled bool)
},
},
},
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{\"creationTimestamp\":null},\"Slice\":\"AQID\",\"Array\":[1,2,3]}]}\n",
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{},\"Slice\":\"AQID\",\"Array\":[1,2,3]}]}\n",
},
{
name: "UnstructuredList object raw bytes",
@ -415,7 +416,7 @@ func testCollectionsEncoding(t *testing.T, s *Serializer, streamingEnabled bool)
}},
},
},
expect: "{\"kind\":\"List\",\"apiVersion\":\"v1\",\"metadata\":{\"resourceVersion\":\"2345\",\"continue\":\"abc\",\"remainingItemCount\":1},\"items\":[{\"kind\":\"Carp\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"pod\",\"namespace\":\"default\",\"creationTimestamp\":null},\"spec\":{},\"status\":{}}]}\n",
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",
@ -438,7 +439,7 @@ func testCollectionsEncoding(t *testing.T, s *Serializer, streamingEnabled bool)
}},
},
},
expect: `{"kind":"List","apiVersion":"v1","metadata":{"resourceVersion":"2345"},"items":[{"kind":"Carp","apiVersion":"v1","metadata":{"name":"pod","namespace":"default","creationTimestamp":null},"spec":{},"status":{}},{"kind":"Carp","apiVersion":"v1","metadata":{"name":"pod2","namespace":"default2","creationTimestamp":null},"spec":{},"status":{}}]}
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":{}}]}
`,
},
{
@ -465,7 +466,7 @@ func testCollectionsEncoding(t *testing.T, s *Serializer, streamingEnabled bool)
},
},
cannotStream: true,
expect: "{\"kind\":\"List\",\"apiVersion\":\"v1\",\"metadata\":{\"creationTimestamp\":null},\"spec\":{},\"status\":{}}\n",
expect: "{\"kind\":\"List\",\"apiVersion\":\"v1\",\"metadata\":{},\"spec\":{},\"status\":{}}\n",
},
{
name: "UnstructuredList empty",

View File

@ -0,0 +1,19 @@
/*
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.
*/
// +k8s:deepcopy-gen=package
package v1

View File

@ -0,0 +1,27 @@
/*
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 v1
import (
"k8s.io/apimachinery/pkg/runtime"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type ExternalSimple struct {
runtime.TypeMeta `json:",inline"`
TestString string `json:"testString"`
}

View File

@ -0,0 +1,51 @@
//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 deepcopy-gen. DO NOT EDIT.
package v1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ExternalSimple) DeepCopyInto(out *ExternalSimple) {
*out = *in
out.TypeMeta = in.TypeMeta
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSimple.
func (in *ExternalSimple) DeepCopy() *ExternalSimple {
if in == nil {
return nil
}
out := new(ExternalSimple)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ExternalSimple) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}

View File

@ -21,7 +21,6 @@ import (
"testing"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
)
@ -38,26 +37,25 @@ type VersionValidationRunner func(t *testing.T, gv string, versionValidationErro
// errs = append(errs, versionValidationErrors...) // generated declarative validation
// // Validate that the errors are what was expected for this test case.
// })
func RunValidationForEachVersion(t *testing.T, scheme *runtime.Scheme, options sets.Set[string], unversioned runtime.Object, fn VersionValidationRunner) {
runValidation(t, scheme, options, unversioned, fn)
func RunValidationForEachVersion(t *testing.T, scheme *runtime.Scheme, options []string, unversioned runtime.Object, fn VersionValidationRunner, subresources ...string) {
runValidation(t, scheme, options, unversioned, fn, subresources...)
}
// RunUpdateValidationForEachVersion is like RunValidationForEachVersion but for update validation.
func RunUpdateValidationForEachVersion(t *testing.T, scheme *runtime.Scheme, options sets.Set[string], unversioned, unversionedOld runtime.Object, fn VersionValidationRunner) {
runUpdateValidation(t, scheme, options, unversioned, unversionedOld, fn)
func RunUpdateValidationForEachVersion(t *testing.T, scheme *runtime.Scheme, options []string, unversioned, unversionedOld runtime.Object, fn VersionValidationRunner, subresources ...string) {
runUpdateValidation(t, scheme, options, unversioned, unversionedOld, fn, subresources...)
}
// RunStatusValidationForEachVersion is like RunUpdateValidationForEachVersion but for status validation.
func RunStatusValidationForEachVersion(t *testing.T, scheme *runtime.Scheme, options sets.Set[string], unversioned, unversionedOld runtime.Object, fn VersionValidationRunner) {
runUpdateValidation(t, scheme, options, unversioned, unversionedOld, fn, "status")
}
func runValidation(t *testing.T, scheme *runtime.Scheme, options sets.Set[string], unversioned runtime.Object, fn VersionValidationRunner, subresources ...string) {
func runValidation(t *testing.T, scheme *runtime.Scheme, options []string, unversioned runtime.Object, fn VersionValidationRunner, subresources ...string) {
unversionedGVKs, _, err := scheme.ObjectKinds(unversioned)
if err != nil {
t.Fatal(err)
}
for _, unversionedGVK := range unversionedGVKs {
// skip if passed in unversioned object is not internal.
if unversionedGVK.Version != runtime.APIVersionInternal {
continue
}
gvs := scheme.VersionsForGroupKind(unversionedGVK.GroupKind())
for _, gv := range gvs {
gvk := gv.WithKind(unversionedGVK.Kind)
@ -78,12 +76,16 @@ func runValidation(t *testing.T, scheme *runtime.Scheme, options sets.Set[string
}
}
func runUpdateValidation(t *testing.T, scheme *runtime.Scheme, options sets.Set[string], unversionedNew, unversionedOld runtime.Object, fn VersionValidationRunner, subresources ...string) {
func runUpdateValidation(t *testing.T, scheme *runtime.Scheme, options []string, unversionedNew, unversionedOld runtime.Object, fn VersionValidationRunner, subresources ...string) {
unversionedGVKs, _, err := scheme.ObjectKinds(unversionedNew)
if err != nil {
t.Fatal(err)
}
for _, unversionedGVK := range unversionedGVKs {
// skip if passed in unversioned object is not internal.
if unversionedGVK.Version != runtime.APIVersionInternal {
continue
}
gvs := scheme.VersionsForGroupKind(unversionedGVK.GroupKind())
for _, gv := range gvs {
gvk := gv.WithKind(unversionedGVK.Kind)

View File

@ -147,7 +147,7 @@ func TestGenericTypeMeta(t *testing.T) {
Name string `json:"name,omitempty"`
GenerateName string `json:"generateName,omitempty"`
UID string `json:"uid,omitempty"`
CreationTimestamp metav1.Time `json:"creationTimestamp,omitempty"`
CreationTimestamp metav1.Time `json:"creationTimestamp,omitempty,omitzero"`
SelfLink string `json:"selfLink,omitempty"`
ResourceVersion string `json:"resourceVersion,omitempty"`
APIVersion string `json:"apiVersion,omitempty"`

31
pkg/util/diff/cmp.go Normal file
View File

@ -0,0 +1,31 @@
//go:build usegocmp
// +build usegocmp
/*
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 diff
import (
"github.com/google/go-cmp/cmp" //nolint:depguard
)
// Diff returns a string representation of the difference between two objects.
// When built with the usegocmp tag, it uses go-cmp/cmp to generate a diff
// between the objects.
func Diff(a, b any) string {
return cmp.Diff(a, b)
}

View File

@ -0,0 +1,356 @@
//go:build !usegocmp
// +build !usegocmp
/*
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 diff
import (
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
// TestDiffWithRealGoCmp is a comprehensive test that compares our Diff with the actual go-cmp Diff
// across a variety of data structures and types to ensure compatibility.
func TestDiffWithRealGoCmp(t *testing.T) {
// Test with simple types
t.Run("SimpleTypes", func(t *testing.T) {
testCases := []struct {
name string
a interface{}
b interface{}
}{
{name: "Integers", a: 42, b: 43},
{name: "Strings", a: "hello", b: "world"},
{name: "Booleans", a: true, b: false},
{name: "Floats", a: 3.14, b: 2.71},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ourDiff := Diff(tc.a, tc.b)
goCmpDiff := cmp.Diff(tc.a, tc.b)
t.Logf("Our diff:\n%s\n\nGo-cmp diff:\n%s", ourDiff, goCmpDiff)
// Verify both diffs are non-empty
if ourDiff == "" || goCmpDiff == "" {
t.Errorf("Expected non-empty diffs, got ourDiff: %v, goCmpDiff: %v",
ourDiff == "", goCmpDiff == "")
}
})
}
})
// Test with a simple struct
t.Run("SimpleStruct", func(t *testing.T) {
type TestStruct struct {
Name string
Age int
Tags []string
Map map[string]int
}
a := TestStruct{
Name: "Alice",
Age: 30,
Tags: []string{"tag1", "tag2"},
Map: map[string]int{"a": 1, "b": 2},
}
b := TestStruct{
Name: "Bob",
Age: 25,
Tags: []string{"tag1", "tag3"},
Map: map[string]int{"a": 1, "c": 3},
}
ourDiff := Diff(a, b)
goCmpDiff := cmp.Diff(a, b)
t.Logf("Our diff:\n%s\n\nGo-cmp diff:\n%s", ourDiff, goCmpDiff)
// Check that our diff contains key differences
keyDifferences := []string{
"Name", "Alice", "Bob",
"Age", "30", "25",
"Tags", "tag2", "tag3",
"Map", "b", "c",
}
for _, key := range keyDifferences {
if !strings.Contains(ourDiff, key) {
t.Errorf("Our diff doesn't contain expected key difference: %q", key)
}
}
})
// Test with a complex nested struct
t.Run("ComplexNestedStruct", func(t *testing.T) {
type Address struct {
Street string
City string
State string
PostalCode string
Country string
}
type Contact struct {
Type string
Value string
}
type Person struct {
ID int
FirstName string
LastName string
Age int
Addresses map[string]Address
Contacts []Contact
Metadata map[string]interface{}
CreatedAt time.Time
}
now := time.Now()
later := now.Add(24 * time.Hour)
person1 := Person{
ID: 1,
FirstName: "John",
LastName: "Doe",
Age: 30,
Addresses: map[string]Address{
"home": {
Street: "123 Main St",
City: "Anytown",
State: "CA",
PostalCode: "12345",
Country: "USA",
},
"work": {
Street: "456 Market St",
City: "Worktown",
State: "CA",
PostalCode: "54321",
Country: "USA",
},
},
Contacts: []Contact{
{Type: "email", Value: "john.doe@example.com"},
{Type: "phone", Value: "555-1234"},
},
Metadata: map[string]interface{}{
"created": "2023-01-01",
"loginCount": 42,
"settings": map[string]bool{
"notifications": true,
"darkMode": false,
},
},
CreatedAt: now,
}
person2 := Person{
ID: 1,
FirstName: "John",
LastName: "Smith", // Different
Age: 31, // Different
Addresses: map[string]Address{
"home": {
Street: "123 Main St",
City: "Anytown",
State: "CA",
PostalCode: "12345",
Country: "USA",
},
// "work" address is missing
},
Contacts: []Contact{
{Type: "email", Value: "john.smith@example.com"}, // Different
{Type: "phone", Value: "555-1234"},
{Type: "fax", Value: "555-5678"}, // Additional
},
Metadata: map[string]interface{}{
"created": "2023-01-01",
"loginCount": 43, // Different
"settings": map[string]bool{
"notifications": false, // Different
"darkMode": true, // Different
},
"newField": "new value", // New field
},
CreatedAt: later, // Different
}
ourDiff := Diff(person1, person2)
goCmpDiff := cmp.Diff(person1, person2)
t.Logf("Our diff:\n%s\n\nGo-cmp diff:\n%s", ourDiff, goCmpDiff)
// Check that our diff contains key differences
keyDifferences := []string{
"LastName", "Smith",
"Age",
"Addresses", "work",
"Contacts", "john.smith@example.com", "fax",
"Metadata", "loginCount", "settings", "notifications", "darkMode", "newField",
}
for _, key := range keyDifferences {
if !strings.Contains(ourDiff, key) {
t.Errorf("Our diff doesn't contain expected key difference: %q", key)
}
}
// Check that both diffs are non-empty
if ourDiff == "" || goCmpDiff == "" {
t.Errorf("Expected non-empty diffs, got ourDiff: %v, goCmpDiff: %v",
ourDiff == "", goCmpDiff == "")
}
})
// Test with slices and maps
t.Run("SlicesAndMaps", func(t *testing.T) {
// Test with slices
a1 := []int{1, 2, 3, 4, 5}
b1 := []int{1, 2, 6, 4, 7}
ourDiff1 := Diff(a1, b1)
goCmpDiff1 := cmp.Diff(a1, b1)
t.Logf("Our diff (slices):\n%s\n\nGo-cmp diff (slices):\n%s", ourDiff1, goCmpDiff1)
// Check that our diff contains the differences
if !strings.Contains(ourDiff1, "3") || !strings.Contains(ourDiff1, "6") ||
!strings.Contains(ourDiff1, "5") || !strings.Contains(ourDiff1, "7") {
t.Errorf("Our diff doesn't contain all expected differences for slices")
}
// Test with maps
a2 := map[string]int{"a": 1, "b": 2, "c": 3}
b2 := map[string]int{"a": 1, "b": 5, "d": 4}
ourDiff2 := Diff(a2, b2)
goCmpDiff2 := cmp.Diff(a2, b2)
t.Logf("Our diff (maps):\n%s\n\nGo-cmp diff (maps):\n%s", ourDiff2, goCmpDiff2)
// Check that our diff contains the differences
if !strings.Contains(ourDiff2, "b") || !strings.Contains(ourDiff2, "5") ||
!strings.Contains(ourDiff2, "c") || !strings.Contains(ourDiff2, "d") {
t.Errorf("Our diff doesn't contain all expected differences for maps")
}
})
// Test with unexported fields
t.Run("UnexportedFields", func(t *testing.T) {
type WithUnexported struct {
Exported int
unexported int
}
a := WithUnexported{Exported: 1, unexported: 2}
b := WithUnexported{Exported: 3, unexported: 4}
ourDiff := Diff(a, b)
// Use cmpopts.IgnoreUnexported to ignore unexported fields in go-cmp
goCmpDiff := cmp.Diff(a, b, cmpopts.IgnoreUnexported(WithUnexported{}))
t.Logf("Our diff (unexported):\n%s\n\nGo-cmp diff (unexported):\n%s", ourDiff, goCmpDiff)
// Check that our diff contains only the exported field difference
if !strings.Contains(ourDiff, "Exported") || !strings.Contains(ourDiff, "1") || !strings.Contains(ourDiff, "3") {
t.Errorf("Our diff doesn't contain the exported field difference")
}
})
// Test with embedded structs
t.Run("EmbeddedStructs", func(t *testing.T) {
type Embedded struct {
Value int
}
type Container struct {
Embedded
Extra string
}
a := Container{Embedded: Embedded{Value: 1}, Extra: "a"}
b := Container{Embedded: Embedded{Value: 2}, Extra: "b"}
ourDiff := Diff(a, b)
goCmpDiff := cmp.Diff(a, b)
t.Logf("Our diff (embedded):\n%s\n\nGo-cmp diff (embedded):\n%s", ourDiff, goCmpDiff)
// Check that our diff contains the container field difference
if !strings.Contains(ourDiff, "Extra") || !strings.Contains(ourDiff, "a") || !strings.Contains(ourDiff, "b") {
t.Errorf("Our diff doesn't contain the container field difference")
}
})
// Test with interface values of same type
t.Run("InterfaceValues", func(t *testing.T) {
type Container struct {
Value interface{}
}
// Test with same type in interface
c := Container{Value: 42}
d := Container{Value: 43}
ourDiff := Diff(c, d)
goCmpDiff := cmp.Diff(c, d)
t.Logf("Our diff (interface same type):\n%s\n\nGo-cmp diff (interface same type):\n%s", ourDiff, goCmpDiff)
// Check that our diff contains the value difference
if !strings.Contains(ourDiff, "42") || !strings.Contains(ourDiff, "43") {
t.Errorf("Our diff doesn't contain the value difference for interface values of same type")
}
})
// Test with objects that cannot be marshaled to JSON
t.Run("UnmarshalableObjects", func(t *testing.T) {
// Test with a circular reference, which cannot be marshaled to JSON
type Node struct {
Value int
Next *Node
}
// Create a circular reference
nodeA := &Node{Value: 1}
nodeA.Next = nodeA // Points to itself
nodeB := &Node{Value: 2}
nodeB.Next = nodeB // Points to itself
// This should fall back to using dump.Pretty
circularDiff := Diff(nodeA, nodeB)
t.Logf("Diff for circular references:\n%s", circularDiff)
// Verify the diff contains the values
if !strings.Contains(circularDiff, "1") || !strings.Contains(circularDiff, "2") {
t.Errorf("Diff doesn't contain expected value differences for circular references")
}
})
}

View File

@ -1,5 +1,8 @@
//go:build !usegocmp
// +build !usegocmp
/*
Copyright 2014 The Kubernetes Authors.
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.
@ -17,122 +20,43 @@ limitations under the License.
package diff
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"strings"
"text/tabwriter"
"github.com/google/go-cmp/cmp" //nolint:depguard
"github.com/pmezard/go-difflib/difflib"
"k8s.io/apimachinery/pkg/util/dump"
)
func legacyDiff(a, b interface{}) string {
return cmp.Diff(a, b)
}
// Diff returns a string representation of the difference between two objects.
// When built without the usegocmp tag, it uses go-difflib/difflib to generate a
// unified diff of the objects. It attempts to use JSON serialization first,
// falling back to an object dump via the dump package if JSON marshaling fails.
func Diff(a, b any) string {
// StringDiff diffs a and b and returns a human readable diff.
// DEPRECATED: use github.com/google/go-cmp/cmp.Diff
func StringDiff(a, b string) string {
return legacyDiff(a, b)
}
// ObjectDiff prints the diff of two go objects and fails if the objects
// contain unhandled unexported fields.
// DEPRECATED: use github.com/google/go-cmp/cmp.Diff
func ObjectDiff(a, b interface{}) string {
return legacyDiff(a, b)
}
// ObjectGoPrintDiff prints the diff of two go objects and fails if the objects
// contain unhandled unexported fields.
// DEPRECATED: use github.com/google/go-cmp/cmp.Diff
func ObjectGoPrintDiff(a, b interface{}) string {
return legacyDiff(a, b)
}
// ObjectReflectDiff prints the diff of two go objects and fails if the objects
// contain unhandled unexported fields.
// DEPRECATED: use github.com/google/go-cmp/cmp.Diff
func ObjectReflectDiff(a, b interface{}) string {
return legacyDiff(a, b)
}
// ObjectGoPrintSideBySide prints a and b as textual dumps side by side,
// enabling easy visual scanning for mismatches.
func ObjectGoPrintSideBySide(a, b interface{}) string {
sA := dump.Pretty(a)
sB := dump.Pretty(b)
linesA := strings.Split(sA, "\n")
linesB := strings.Split(sB, "\n")
width := 0
for _, s := range linesA {
l := len(s)
if l > width {
width = l
}
aStr, aErr := toPrettyJSON(a)
bStr, bErr := toPrettyJSON(b)
if aErr != nil || bErr != nil {
aStr = dump.Pretty(a)
bStr = dump.Pretty(b)
}
for _, s := range linesB {
l := len(s)
if l > width {
width = l
}
diff := difflib.UnifiedDiff{
A: difflib.SplitLines(aStr),
B: difflib.SplitLines(bStr),
Context: 3,
}
buf := &bytes.Buffer{}
w := tabwriter.NewWriter(buf, width, 0, 1, ' ', 0)
max := len(linesA)
if len(linesB) > max {
max = len(linesB)
diffstr, err := difflib.GetUnifiedDiffString(diff)
if err != nil {
return fmt.Sprintf("error generating diff: %v", err)
}
for i := 0; i < max; i++ {
var a, b string
if i < len(linesA) {
a = linesA[i]
}
if i < len(linesB) {
b = linesB[i]
}
fmt.Fprintf(w, "%s\t%s\n", a, b)
}
w.Flush()
return buf.String()
return diffstr
}
// IgnoreUnset is an option that ignores fields that are unset on the right
// hand side of a comparison. This is useful in testing to assert that an
// object is a derivative.
func IgnoreUnset() cmp.Option {
return cmp.Options{
// ignore unset fields in v2
cmp.FilterPath(func(path cmp.Path) bool {
_, v2 := path.Last().Values()
switch v2.Kind() {
case reflect.Slice, reflect.Map:
if v2.IsNil() || v2.Len() == 0 {
return true
}
case reflect.String:
if v2.Len() == 0 {
return true
}
case reflect.Interface, reflect.Pointer:
if v2.IsNil() {
return true
}
}
return false
}, cmp.Ignore()),
// ignore map entries that aren't set in v2
cmp.FilterPath(func(path cmp.Path) bool {
switch i := path.Last().(type) {
case cmp.MapIndex:
if _, v2 := i.Values(); !v2.IsValid() {
fmt.Println("E")
return true
}
}
return false
}, cmp.Ignore()),
}
// toPrettyJSON converts an object to a pretty-printed JSON string.
func toPrettyJSON(data any) (string, error) {
jsonData, err := json.MarshalIndent(data, "", " ")
return string(jsonData), err
}

View File

@ -0,0 +1,67 @@
/*
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 diff
import (
"bytes"
"fmt"
"strings"
"text/tabwriter"
"k8s.io/apimachinery/pkg/util/dump"
)
// ObjectGoPrintSideBySide prints a and b as textual dumps side by side,
// enabling easy visual scanning for mismatches.
func ObjectGoPrintSideBySide(a, b interface{}) string {
sA := dump.Pretty(a)
sB := dump.Pretty(b)
linesA := strings.Split(sA, "\n")
linesB := strings.Split(sB, "\n")
width := 0
for _, s := range linesA {
l := len(s)
if l > width {
width = l
}
}
for _, s := range linesB {
l := len(s)
if l > width {
width = l
}
}
buf := &bytes.Buffer{}
w := tabwriter.NewWriter(buf, width, 0, 1, ' ', 0)
max := len(linesA)
if len(linesB) > max {
max = len(linesB)
}
for i := 0; i < max; i++ {
var a, b string
if i < len(linesA) {
a = linesA[i]
}
if i < len(linesB) {
b = linesB[i]
}
_, _ = fmt.Fprintf(w, "%s\t%s\n", a, b)
}
_ = w.Flush()
return buf.String()
}

View File

@ -19,16 +19,10 @@ package dump
import (
"fmt"
"testing"
"k8s.io/utils/ptr"
)
func ptrint(i int) *int {
return &i
}
func ptrstr(s string) *string {
return &s
}
// custom type to test Stringer interface on non-pointer receiver.
type customString string
@ -101,7 +95,7 @@ func TestPretty(t *testing.T) {
{uint32(93), "(uint32) 93\n"},
{uint64(93), "(uint64) 93\n"},
{uintptr(93), "(uintptr) 0x5d\n"},
{ptrint(93), "(*int)(93)\n"},
{ptr.To(93), "(*int)(93)\n"},
{float32(93.76), "(float32) 93.76\n"},
{float64(93.76), "(float64) 93.76\n"},
{complex64(93i), "(complex64) (0+93i)\n"},
@ -109,7 +103,7 @@ func TestPretty(t *testing.T) {
{bool(true), "(bool) true\n"},
{bool(false), "(bool) false\n"},
{string("test"), "(string) (len=4) \"test\"\n"},
{ptrstr("test"), "(*string)((len=4) \"test\")\n"},
{ptr.To("test"), "(*string)((len=4) \"test\")\n"},
{[1]string{"arr"}, "([1]string) (len=1) {\n (string) (len=3) \"arr\"\n}\n"},
{[]string{"slice"}, "([]string) (len=1) {\n (string) (len=5) \"slice\"\n}\n"},
{tcs, "(dump.customString) (len=4) \"test\"\n"},
@ -180,7 +174,7 @@ func TestForHash(t *testing.T) {
{uint32(93), "(uint32)93"},
{uint64(93), "(uint64)93"},
{uintptr(93), "(uintptr)0x5d"},
{ptrint(93), "(*int)93"},
{ptr.To(93), "(*int)93"},
{float32(93.76), "(float32)93.76"},
{float64(93.76), "(float64)93.76"},
{complex64(93i), "(complex64)(0+93i)"},
@ -188,7 +182,7 @@ func TestForHash(t *testing.T) {
{bool(true), "(bool)true"},
{bool(false), "(bool)false"},
{string("test"), "(string)test"},
{ptrstr("test"), "(*string)test"},
{ptr.To("test"), "(*string)test"},
{[1]string{"arr"}, "([1]string)[arr]"},
{[]string{"slice"}, "([]string)[slice]"},
{tcs, "(dump.customString)test"},
@ -259,7 +253,7 @@ func TestOneLine(t *testing.T) {
{uint32(93), "(uint32)93"},
{uint64(93), "(uint64)93"},
{uintptr(93), "(uintptr)0x5d"},
{ptrint(93), "(*int)93"},
{ptr.To(93), "(*int)93"},
{float32(93.76), "(float32)93.76"},
{float64(93.76), "(float64)93.76"},
{complex64(93i), "(complex64)(0+93i)"},
@ -267,7 +261,7 @@ func TestOneLine(t *testing.T) {
{bool(true), "(bool)true"},
{bool(false), "(bool)false"},
{string("test"), "(string)test"},
{ptrstr("test"), "(*string)test"},
{ptr.To("test"), "(*string)test"},
{[1]string{"arr"}, "([1]string)[arr]"},
{[]string{"slice"}, "([]string)[slice]"},
{tcs, "(dump.customString)test"},

View File

@ -24,6 +24,7 @@ import (
)
// MessageCountMap contains occurrence for each error message.
// Deprecated: Not used anymore in the k8s.io codebase, use `errors.Join` instead.
type MessageCountMap map[string]int
// Aggregate represents an object that contains multiple errors, but does not
@ -199,6 +200,7 @@ func Flatten(agg Aggregate) Aggregate {
}
// CreateAggregateFromMessageCountMap converts MessageCountMap Aggregate
// Deprecated: Not used anymore in the k8s.io codebase, use `errors.Join` instead.
func CreateAggregateFromMessageCountMap(m MessageCountMap) Aggregate {
if m == nil {
return nil

View File

@ -79,7 +79,7 @@ func TestHandshake(t *testing.T) {
}
for name, test := range tests {
req, err := http.NewRequest("GET", "http://www.example.com/", nil)
req, err := http.NewRequest(http.MethodGet, "http://www.example.com/", nil)
if err != nil {
t.Fatalf("%s: error creating request: %v", name, err)
}

View File

@ -194,7 +194,7 @@ func (s *SpdyRoundTripper) dialWithHttpProxy(req *http.Request, proxyURL *url.UR
// proxying logic adapted from http://blog.h6t.eu/post/74098062923/golang-websocket-with-http-proxy-support
proxyReq := http.Request{
Method: "CONNECT",
Method: http.MethodConnect,
URL: &url.URL{},
Host: targetHost,
}

View File

@ -321,7 +321,7 @@ func TestRoundTripAndNewConnection(t *testing.T) {
if err != nil {
t.Fatalf("error creating request: %s", err)
}
req, err := http.NewRequest("GET", server.URL, nil)
req, err := http.NewRequest(http.MethodGet, server.URL, nil)
if err != nil {
t.Fatalf("error creating request: %s", err)
}
@ -612,7 +612,7 @@ func TestRoundTripSocks5AndNewConnection(t *testing.T) {
))
defer server.Close()
req, err := http.NewRequest("GET", server.URL, nil)
req, err := http.NewRequest(http.MethodGet, server.URL, nil)
if err != nil {
t.Fatalf("error creating request: %s", err)
}
@ -778,7 +778,7 @@ func TestRoundTripPassesContextToDialer(t *testing.T) {
t.Run(u, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
require.NoError(t, err)
spdyTransport, err := NewRoundTripper(&tls.Config{})
if err != nil {

View File

@ -68,7 +68,7 @@ func TestUpgradeResponse(t *testing.T) {
}))
defer server.Close()
req, err := http.NewRequest("GET", server.URL, nil)
req, err := http.NewRequest(http.MethodGet, server.URL, nil)
if err != nil {
t.Fatalf("%d: error creating request: %s", i, err)
}

View File

@ -369,7 +369,7 @@ func TestIsWebSocketRequestWithStreamCloseProtocol(t *testing.T) {
}
for name, test := range tests {
req, err := http.NewRequest("GET", "http://www.example.com/", nil)
req, err := http.NewRequest(http.MethodGet, "http://www.example.com/", nil)
require.NoError(t, err)
for key, value := range test.headers {
req.Header.Add(key, value)

View File

@ -20,8 +20,8 @@ import (
"bytes"
"fmt"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/typed"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
"sigs.k8s.io/structured-merge-diff/v6/typed"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

View File

@ -20,7 +20,7 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
"sigs.k8s.io/structured-merge-diff/v4/typed"
"sigs.k8s.io/structured-merge-diff/v6/typed"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"

View File

@ -19,7 +19,7 @@ package managedfields
import (
"fmt"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"

View File

@ -22,8 +22,8 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kube-openapi/pkg/schemaconv"
"k8s.io/kube-openapi/pkg/util/proto"
smdschema "sigs.k8s.io/structured-merge-diff/v4/schema"
"sigs.k8s.io/structured-merge-diff/v4/typed"
smdschema "sigs.k8s.io/structured-merge-diff/v6/schema"
"sigs.k8s.io/structured-merge-diff/v6/typed"
)
// groupVersionKindExtensionKey is the key used to lookup the

View File

@ -22,7 +22,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
)
type capManagersManager struct {

View File

@ -32,7 +32,7 @@ import (
"k8s.io/apimachinery/pkg/util/managedfields/internal"
internaltesting "k8s.io/apimachinery/pkg/util/managedfields/internal/testing"
"k8s.io/apimachinery/pkg/util/managedfields/managedfieldstest"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
)
type fakeManager struct{}

View File

@ -25,8 +25,8 @@ import (
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/merge"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
"sigs.k8s.io/structured-merge-diff/v6/merge"
)
// NewConflictError returns an error including details on the requests apply conflicts

View File

@ -24,8 +24,8 @@ import (
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/managedfields/internal"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/merge"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
"sigs.k8s.io/structured-merge-diff/v6/merge"
)
// TestNewConflictError tests that NewConflictError creates the correct StatusError for a given smd Conflicts

View File

@ -26,7 +26,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/klog/v2"
"sigs.k8s.io/structured-merge-diff/v4/merge"
"sigs.k8s.io/structured-merge-diff/v6/merge"
)
// DefaultMaxUpdateManagers defines the default maximum retained number of managedFields entries from updates

View File

@ -21,7 +21,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
)
// EmptyFields represents a set with no paths

View File

@ -23,7 +23,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
)
// TestFieldsRoundTrip tests that a fields trie can be round tripped as a path set

View File

@ -24,8 +24,8 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/merge"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
"sigs.k8s.io/structured-merge-diff/v6/merge"
)
type lastAppliedManager struct {

View File

@ -29,8 +29,8 @@ import (
"k8s.io/apimachinery/pkg/util/managedfields/internal"
"k8s.io/apimachinery/pkg/util/managedfields/managedfieldstest"
yamlutil "k8s.io/apimachinery/pkg/util/yaml"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/merge"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
"sigs.k8s.io/structured-merge-diff/v6/merge"
"sigs.k8s.io/yaml"
)

View File

@ -24,7 +24,7 @@ import (
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
)
// ManagedInterface groups a fieldpath.ManagedFields together with the timestamps associated with each operation.

View File

@ -21,7 +21,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
)
type managedFieldsUpdater struct {

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