Compare commits

...

25 Commits

Author SHA1 Message Date
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
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
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
12 changed files with 668 additions and 128 deletions

10
go.mod
View File

@ -11,7 +11,7 @@ require (
github.com/davecgh/go-spew v1.1.1
github.com/fxamacker/cbor/v2 v2.8.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
@ -24,12 +24,12 @@ require (
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-20250610211856-8b98d1ed966a
k8s.io/kube-openapi v0.0.0-20250628140032-d90c4fd18f59
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8
sigs.k8s.io/randfill v1.0.0
sigs.k8s.io/structured-merge-diff/v4 v4.7.0
sigs.k8s.io/yaml v1.4.0
sigs.k8s.io/yaml v1.5.0
)
require (
@ -47,6 +47,8 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/x448/float16 v0.8.4 // 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

19
go.sum
View File

@ -20,8 +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/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.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
@ -80,6 +80,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=
@ -129,16 +133,17 @@ 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-20250610211856-8b98d1ed966a h1:ZV3Zr+/7s7aVbjNGICQt+ppKWsF1tehxggNfbM7XnG8=
k8s.io/kube-openapi v0.0.0-20250610211856-8b98d1ed966a/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
k8s.io/kube-openapi v0.0.0-20250628140032-d90c4fd18f59 h1:Jc4GiFTK2HHOpfQFoQEGXTBTs2pETwHukmoD4yoTqwo=
k8s.io/kube-openapi v0.0.0-20250628140032-d90c4fd18f59/go.mod h1:GLOk5B+hDbRROvt0X2+hqX64v/zO3vXN7J78OUmBSKw=
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-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
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 v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
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.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI=
sigs.k8s.io/structured-merge-diff/v4 v4.7.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/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=

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

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

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

@ -33,7 +33,6 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
runtimetesting "k8s.io/apimachinery/pkg/runtime/testing"
runtimetestingv1 "k8s.io/apimachinery/pkg/runtime/testing/v1"
"k8s.io/apimachinery/pkg/util/diff"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
@ -1125,12 +1124,6 @@ func TestToOpenAPIDefinitionName(t *testing.T) {
out string
wantErr error
}{
{
name: "built-registerObj type",
registerObj: &runtimetestingv1.ExternalSimple{},
gvk: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Simple"},
out: "io.k8s.apimachinery.pkg.runtime.testing.v1.ExternalSimple",
},
{
name: "unstructured type",
registerObj: &unstructured.Unstructured{},

View File

@ -52,6 +52,10 @@ func runValidation(t *testing.T, scheme *runtime.Scheme, options []string, unver
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,6 +82,10 @@ func runUpdateValidation(t *testing.T, scheme *runtime.Scheme, options []string,
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

@ -17,8 +17,8 @@ limitations under the License.
package field
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
@ -72,45 +72,43 @@ var omitValue = OmitValueType{}
// for building nice-looking higher-level error reporting.
func (e *Error) ErrorBody() string {
var s string
switch {
case e.Type == ErrorTypeRequired:
switch e.Type {
case ErrorTypeRequired, ErrorTypeForbidden, ErrorTypeTooLong, ErrorTypeInternal:
s = e.Type.String()
case e.Type == ErrorTypeForbidden:
s = e.Type.String()
case e.Type == ErrorTypeTooLong:
s = e.Type.String()
case e.Type == ErrorTypeInternal:
s = e.Type.String()
case e.BadValue == omitValue:
s = e.Type.String()
default:
value := e.BadValue
valueType := reflect.TypeOf(value)
if value == nil || valueType == nil {
value = "null"
} else if valueType.Kind() == reflect.Pointer {
if reflectValue := reflect.ValueOf(value); reflectValue.IsNil() {
value = "null"
} else {
value = reflectValue.Elem().Interface()
}
case ErrorTypeInvalid, ErrorTypeTypeInvalid, ErrorTypeNotSupported,
ErrorTypeNotFound, ErrorTypeDuplicate, ErrorTypeTooMany:
if e.BadValue == omitValue {
s = e.Type.String()
break
}
switch t := value.(type) {
switch t := e.BadValue.(type) {
case int64, int32, float64, float32, bool:
// use simple printer for simple types
s = fmt.Sprintf("%s: %v", e.Type, value)
s = fmt.Sprintf("%s: %v", e.Type, t)
case string:
s = fmt.Sprintf("%s: %q", e.Type, t)
case fmt.Stringer:
// anything that defines String() is better than raw struct
s = fmt.Sprintf("%s: %s", e.Type, t.String())
default:
// fallback to raw struct
// TODO: internal types have panic guards against json.Marshalling to prevent
// accidental use of internal types in external serialized form. For now, use
// %#v, although it would be better to show a more expressive output in the future
s = fmt.Sprintf("%s: %#v", e.Type, value)
// use more complex techniques to render more complex types
valstr := ""
jb, err := json.Marshal(e.BadValue)
if err == nil {
// best case
valstr = string(jb)
} else if stringer, ok := e.BadValue.(fmt.Stringer); ok {
// anything that defines String() is better than raw struct
valstr = stringer.String()
} else {
// worst case - fallback to raw struct
// TODO: internal types have panic guards against json.Marshalling to prevent
// accidental use of internal types in external serialized form. For now, use
// %#v, although it would be better to show a more expressive output in the future
valstr = fmt.Sprintf("%#v", e.BadValue)
}
s = fmt.Sprintf("%s: %s", e.Type, valstr)
}
default:
internal := InternalError(nil, fmt.Errorf("unhandled error code: %s: please report this", e.Type))
s = internal.ErrorBody()
}
if len(e.Detail) != 0 {
s += fmt.Sprintf(": %s", e.Detail)
@ -197,7 +195,7 @@ func (t ErrorType) String() string {
case ErrorTypeTypeInvalid:
return "Invalid value"
default:
panic(fmt.Sprintf("unrecognized validation error: %q", string(t)))
return fmt.Sprintf("<unknown error %q>", string(t))
}
}
@ -258,10 +256,14 @@ func Forbidden(field *Path, detail string) *Error {
// the given value is too long. This is similar to Invalid, but the returned
// error will not include the too-long value. If maxLength is negative, it will
// be included in the message. The value argument is not used.
func TooLong(field *Path, value interface{}, maxLength int) *Error {
func TooLong(field *Path, _ interface{}, maxLength int) *Error {
var msg string
if maxLength >= 0 {
msg = fmt.Sprintf("may not be more than %d bytes", maxLength)
bs := "bytes"
if maxLength == 1 {
bs = "byte"
}
msg = fmt.Sprintf("may not be more than %d %s", maxLength, bs)
} else {
msg = "value is too long"
}
@ -281,7 +283,11 @@ func TooMany(field *Path, actualQuantity, maxQuantity int) *Error {
var msg string
if maxQuantity >= 0 {
msg = fmt.Sprintf("must have at most %d items", maxQuantity)
is := "items"
if maxQuantity == 1 {
is = "item"
}
msg = fmt.Sprintf("must have at most %d %s", maxQuantity, is)
} else {
msg = "has too many items"
}

View File

@ -19,8 +19,10 @@ package field
import (
"fmt"
"reflect"
"strings"
"testing"
"time"
"k8s.io/utils/ptr"
)
func TestMakeFuncs(t *testing.T) {
@ -62,52 +64,6 @@ func TestMakeFuncs(t *testing.T) {
}
}
func TestErrorUsefulMessage(t *testing.T) {
{
s := Invalid(nil, nil, "").Error()
t.Logf("message: %v", s)
if !strings.Contains(s, "null") {
t.Errorf("error message did not contain 'null': %s", s)
}
}
s := Invalid(NewPath("foo"), "bar", "deet").Error()
t.Logf("message: %v", s)
for _, part := range []string{"foo", "bar", "deet", ErrorTypeInvalid.String()} {
if !strings.Contains(s, part) {
t.Errorf("error message did not contain expected part '%v'", part)
}
}
type complicated struct {
Baz int
Qux string
Inner interface{}
KV map[string]int
}
s = Invalid(
NewPath("foo"),
&complicated{
Baz: 1,
Qux: "aoeu",
Inner: &complicated{Qux: "asdf"},
KV: map[string]int{"Billy": 2},
},
"detail",
).Error()
t.Logf("message: %v", s)
for _, part := range []string{
"foo", ErrorTypeInvalid.String(),
"Baz", "Qux", "Inner", "KV", "detail",
"1", "aoeu", "Billy", "2",
// "asdf", TODO: re-enable once we have a better nested printer
} {
if !strings.Contains(s, part) {
t.Errorf("error message did not contain expected part '%v'", part)
}
}
}
func TestToAggregate(t *testing.T) {
testCases := struct {
ErrList []ErrorList
@ -167,14 +123,6 @@ func TestErrListFilter(t *testing.T) {
}
}
func TestNotSupported(t *testing.T) {
notSupported := NotSupported(NewPath("f"), "v", []string{"a", "b", "c"})
expected := `Unsupported value: "v": supported values: "a", "b", "c"`
if notSupported.ErrorBody() != expected {
t.Errorf("Expected: %s\n, but got: %s\n", expected, notSupported.ErrorBody())
}
}
func TestErrorOrigin(t *testing.T) {
err := Invalid(NewPath("field"), "value", "detail")
@ -301,3 +249,438 @@ func TestErrorListRemoveCoveredByDeclarative(t *testing.T) {
}
}
}
func TestErrorFormatting(t *testing.T) {
cases := []struct {
name string
input *Error
expect string
}{{
name: "required",
input: &Error{
Type: ErrorTypeRequired,
Field: "path.to.field",
BadValue: "the value",
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Required value: the details`,
}, {
name: "required func",
input: Required(NewPath("path.to.field"), "the details"),
expect: `path.to.field: Required value: the details`,
}, {
name: "forbidden",
input: &Error{
Type: ErrorTypeForbidden,
Field: "path.to.field",
BadValue: "the value",
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Forbidden: the details`,
}, {
name: "forbidden func",
input: Forbidden(NewPath("path.to.field"), "the details"),
expect: `path.to.field: Forbidden: the details`,
}, {
name: "too long",
input: &Error{
Type: ErrorTypeTooLong,
Field: "path.to.field",
BadValue: "the value",
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Too long: the details`,
}, {
name: "too long func(1)",
input: TooLong(NewPath("path.to.field"), "the value", 1),
expect: `path.to.field: Too long: may not be more than 1 byte`,
}, {
name: "too long func(2)",
input: TooLong(NewPath("path.to.field"), "the value", 2),
expect: `path.to.field: Too long: may not be more than 2 bytes`,
}, {
name: "too long func(-1)",
input: TooLong(NewPath("path.to.field"), "the value", -1),
expect: `path.to.field: Too long: value is too long`,
}, {
name: "too many",
input: &Error{
Type: ErrorTypeTooMany,
Field: "path.to.field",
BadValue: "the value",
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Too many: "the value": the details`,
}, {
name: "too many func(2, 1)",
input: TooMany(NewPath("path.to.field"), 2, 1),
expect: `path.to.field: Too many: 2: must have at most 1 item`,
}, {
name: "too many func(3, 2)",
input: TooMany(NewPath("path.to.field"), 3, 2),
expect: `path.to.field: Too many: 3: must have at most 2 items`,
}, {
name: "too many func(2, -1)",
input: TooMany(NewPath("path.to.field"), 2, -1),
expect: `path.to.field: Too many: 2: has too many items`,
}, {
name: "too many func(-1, 1)",
input: TooMany(NewPath("path.to.field"), -1, 1),
expect: `path.to.field: Too many: must have at most 1 item`,
}, {
name: "internal error",
input: &Error{
Type: ErrorTypeInternal,
Field: "path.to.field",
BadValue: "the value",
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Internal error: the details`,
}, {
name: "internal error func",
input: InternalError(NewPath("path.to.field"), fmt.Errorf("the error")),
expect: `path.to.field: Internal error: the error`,
}, {
name: "invalid string",
input: &Error{
Type: ErrorTypeInvalid,
Field: "path.to.field",
BadValue: "the value",
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Invalid value: "the value": the details`,
}, {
name: "invalid string type",
input: &Error{
Type: ErrorTypeInvalid,
Field: "path.to.field",
BadValue: StringType("the value"),
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Invalid value: "the value": the details`,
}, {
name: "invalid int",
input: &Error{
Type: ErrorTypeInvalid,
Field: "path.to.field",
BadValue: -42,
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Invalid value: -42: the details`,
}, {
name: "invalid bool",
input: &Error{
Type: ErrorTypeInvalid,
Field: "path.to.field",
BadValue: true,
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Invalid value: true: the details`,
}, {
name: "invalid struct",
input: &Error{
Type: ErrorTypeInvalid,
Field: "path.to.field",
BadValue: mkTinyStruct(),
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Invalid value: {"stringField":"stringval","intField":9376,"boolField":true}: the details`,
}, {
name: "invalid list",
input: &Error{
Type: ErrorTypeInvalid,
Field: "path.to.field",
BadValue: []string{"one", "two", "three"},
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Invalid value: ["one","two","three"]: the details`,
}, {
name: "invalid map",
input: &Error{
Type: ErrorTypeInvalid,
Field: "path.to.field",
BadValue: map[string]int{"one": 1, "two": 2, "three": 3},
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Invalid value: {"one":1,"three":3,"two":2}: the details`,
}, {
name: "invalid time",
input: &Error{
Type: ErrorTypeInvalid,
Field: "path.to.field",
BadValue: time.Time{},
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Invalid value: "0001-01-01T00:00:00Z": the details`,
}, {
name: "invalid omitValue",
input: &Error{
Type: ErrorTypeInvalid,
Field: "path.to.field",
BadValue: omitValue,
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Invalid value: the details`,
}, {
name: "invalid untyped nil",
input: &Error{
Type: ErrorTypeInvalid,
Field: "path.to.field",
BadValue: nil,
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Invalid value: null: the details`,
}, {
name: "invalid typed nil",
input: &Error{
Type: ErrorTypeInvalid,
Field: "path.to.field",
BadValue: (*string)(nil),
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Invalid value: null: the details`,
}, {
name: "invalid string ptr",
input: &Error{
Type: ErrorTypeInvalid,
Field: "path.to.field",
BadValue: ptr.To("the value"),
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Invalid value: "the value": the details`,
}, {
name: "invalid string type ptr",
input: &Error{
Type: ErrorTypeInvalid,
Field: "path.to.field",
BadValue: ptr.To(StringType("the value")),
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Invalid value: "the value": the details`,
}, {
name: "invalid int ptr",
input: &Error{
Type: ErrorTypeInvalid,
Field: "path.to.field",
BadValue: ptr.To(-42),
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Invalid value: -42: the details`,
}, {
name: "invalid bool ptr",
input: &Error{
Type: ErrorTypeInvalid,
Field: "path.to.field",
BadValue: ptr.To(true),
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Invalid value: true: the details`,
}, {
name: "invalid struct ptr",
input: &Error{
Type: ErrorTypeInvalid,
Field: "path.to.field",
BadValue: ptr.To(mkTinyStruct()),
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Invalid value: {"stringField":"stringval","intField":9376,"boolField":true}: the details`,
}, {
name: "invalid list ptr",
input: &Error{
Type: ErrorTypeInvalid,
Field: "path.to.field",
BadValue: ptr.To([]string{"one", "two", "three"}),
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Invalid value: ["one","two","three"]: the details`,
}, {
name: "invalid map ptr",
input: &Error{
Type: ErrorTypeInvalid,
Field: "path.to.field",
BadValue: ptr.To(map[string]int{"one": 1, "two": 2, "three": 3}),
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Invalid value: {"one":1,"three":3,"two":2}: the details`,
}, {
name: "invalid func",
input: Invalid(NewPath("path.to.field"), "the value", "the details"),
expect: `path.to.field: Invalid value: "the value": the details`,
}, {
name: "not found",
input: &Error{
Type: ErrorTypeNotFound,
Field: "path.to.field",
BadValue: "the value",
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Not found: "the value": the details`,
}, {
name: "not supported",
input: &Error{
Type: ErrorTypeNotSupported,
Field: "path.to.field",
BadValue: "the value",
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Unsupported value: "the value": the details`,
}, {
name: "not supported func",
input: NotSupported(NewPath("path.to.field"), "the value", []string{"val1", "val2"}),
expect: `path.to.field: Unsupported value: "the value": supported values: "val1", "val2"`,
}, {
name: "duplicate",
input: &Error{
Type: ErrorTypeDuplicate,
Field: "path.to.field",
BadValue: "the value",
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Duplicate value: "the value": the details`,
}, {
name: "duplicate func",
input: Duplicate(NewPath("path.to.field"), "the value"),
expect: `path.to.field: Duplicate value: "the value"`,
}, {
name: "type invalid",
input: &Error{
Type: ErrorTypeTypeInvalid,
Field: "path.to.field",
BadValue: "the value",
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Invalid value: "the value": the details`,
}, {
name: "type invalid func",
input: TypeInvalid(NewPath("path.to.field"), "the value", "the details"),
expect: `path.to.field: Invalid value: "the value": the details`,
}, {
name: "failed marshal stringer",
input: &Error{
Type: ErrorTypeTypeInvalid,
Field: "path.to.field",
BadValue: SelfMarshalerStringer{"invisible"},
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Invalid value: magic: the details`,
}, {
name: "failed marshal non-stringer",
input: &Error{
Type: ErrorTypeTypeInvalid,
Field: "path.to.field",
BadValue: SelfMarshalerNonStringer{"visible"},
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Invalid value: field.SelfMarshalerNonStringer{S:"visible"}: the details`,
}, {
name: "unknown error type",
input: &Error{
Type: "not real",
Field: "path.to.field",
BadValue: SelfMarshalerNonStringer{"visible"},
Detail: "the details",
Origin: "theOrigin",
CoveredByDeclarative: true,
},
expect: `path.to.field: Internal error: unhandled error code: <unknown error "not real">: please report this: the details`,
}}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result := tc.input.Error()
if want := tc.expect; result != want {
t.Errorf("wrong error string:\n expected: %q\n got: %q", want, result)
}
})
}
}
type StringType string
type TinyStruct struct {
StringField string `json:"stringField"`
IntField int `json:"intField"`
BoolField bool `json:"boolField"`
}
func mkTinyStruct() TinyStruct {
return TinyStruct{
StringField: "stringval",
IntField: 9376,
BoolField: true,
}
}
type SelfMarshalerStringer struct{ S string }
func (SelfMarshalerStringer) MarshalJSON() ([]byte, error) {
return nil, fmt.Errorf("this always fails")
}
func (SelfMarshalerStringer) String() string {
return "magic"
}
type SelfMarshalerNonStringer struct{ S string }
func (SelfMarshalerNonStringer) MarshalJSON() ([]byte, error) {
return nil, fmt.Errorf("this always fails")
}

View File

@ -61,7 +61,7 @@ func Test_parseKubeVersion(t *testing.T) {
wantOk bool
}{
{
name: "invaild version for ga",
name: "invalid version for ga",
v: "v1.1",
wantMajorVersion: 0,
wantVType: 0,
@ -69,7 +69,7 @@ func Test_parseKubeVersion(t *testing.T) {
wantOk: false,
},
{
name: "invaild version for alpha",
name: "invalid version for alpha",
v: "v1alpha1.1",
wantMajorVersion: 0,
wantVType: 0,