Skip to content

Commit cb13ac5

Browse files
authored
Merge pull request #619 from Porges/floating-point-validation
🐛 Allow floating-point values in validations
2 parents a0e33b1 + 4b99330 commit cb13ac5

File tree

6 files changed

+155
-25
lines changed

6 files changed

+155
-25
lines changed

pkg/crd/markers/validation.go

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ limitations under the License.
1717
package markers
1818

1919
import (
20-
"fmt"
21-
2220
"encoding/json"
21+
"fmt"
22+
"math"
2323

2424
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2525

@@ -37,7 +37,7 @@ const (
3737
// reusable and writing complex validations on slice items.
3838
var ValidationMarkers = mustMakeAllWithPrefix("kubebuilder:validation", markers.DescribesField,
3939

40-
// integer markers
40+
// numeric markers
4141

4242
Maximum(0),
4343
Minimum(0),
@@ -123,11 +123,19 @@ func init() {
123123

124124
// +controllertools:marker:generateHelp:category="CRD validation"
125125
// Maximum specifies the maximum numeric value that this field can have.
126-
type Maximum int
126+
type Maximum float64
127+
128+
func (m Maximum) Value() float64 {
129+
return float64(m)
130+
}
127131

128132
// +controllertools:marker:generateHelp:category="CRD validation"
129-
// Minimum specifies the minimum numeric value that this field can have. Negative integers are supported.
130-
type Minimum int
133+
// Minimum specifies the minimum numeric value that this field can have. Negative numbers are supported.
134+
type Minimum float64
135+
136+
func (m Minimum) Value() float64 {
137+
return float64(m)
138+
}
131139

132140
// +controllertools:marker:generateHelp:category="CRD validation"
133141
// ExclusiveMinimum indicates that the minimum is "up to" but not including that value.
@@ -139,7 +147,11 @@ type ExclusiveMaximum bool
139147

140148
// +controllertools:marker:generateHelp:category="CRD validation"
141149
// MultipleOf specifies that this field must have a numeric value that's a multiple of this one.
142-
type MultipleOf int
150+
type MultipleOf float64
151+
152+
func (m MultipleOf) Value() float64 {
153+
return float64(m)
154+
}
143155

144156
// +controllertools:marker:generateHelp:category="CRD validation"
145157
// MaxLength specifies the maximum length for this string.
@@ -252,6 +264,14 @@ type XIntOrString struct{}
252264
// to be used only as a last resort.
253265
type Schemaless struct{}
254266

267+
func hasNumericType(schema *apiext.JSONSchemaProps) bool {
268+
return schema.Type == "integer" || schema.Type == "number"
269+
}
270+
271+
func isIntegral(value float64) bool {
272+
return value == math.Trunc(value) && !math.IsNaN(value) && !math.IsInf(value, 0)
273+
}
274+
255275
// +controllertools:marker:generateHelp:category="CRD validation"
256276
// XValidation marks a field as requiring a value for which a given
257277
// expression evaluates to true.
@@ -264,40 +284,60 @@ type XValidation struct {
264284
}
265285

266286
func (m Maximum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
267-
if schema.Type != "integer" {
268-
return fmt.Errorf("must apply maximum to an integer")
287+
if !hasNumericType(schema) {
288+
return fmt.Errorf("must apply maximum to a numeric value, found %s", schema.Type)
289+
}
290+
291+
if schema.Type == "integer" && !isIntegral(m.Value()) {
292+
return fmt.Errorf("cannot apply non-integral maximum validation (%v) to integer value", m.Value())
269293
}
270-
val := float64(m)
294+
295+
val := m.Value()
271296
schema.Maximum = &val
272297
return nil
273298
}
299+
274300
func (m Minimum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
275-
if schema.Type != "integer" {
276-
return fmt.Errorf("must apply minimum to an integer")
301+
if !hasNumericType(schema) {
302+
return fmt.Errorf("must apply minimum to a numeric value, found %s", schema.Type)
303+
}
304+
305+
if schema.Type == "integer" && !isIntegral(m.Value()) {
306+
return fmt.Errorf("cannot apply non-integral minimum validation (%v) to integer value", m.Value())
277307
}
278-
val := float64(m)
308+
309+
val := m.Value()
279310
schema.Minimum = &val
280311
return nil
281312
}
313+
282314
func (m ExclusiveMaximum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
283-
if schema.Type != "integer" {
284-
return fmt.Errorf("must apply exclusivemaximum to an integer")
315+
if !hasNumericType(schema) {
316+
return fmt.Errorf("must apply exclusivemaximum to a numeric value, found %s", schema.Type)
285317
}
286318
schema.ExclusiveMaximum = bool(m)
287319
return nil
288320
}
321+
289322
func (m ExclusiveMinimum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
290-
if schema.Type != "integer" {
291-
return fmt.Errorf("must apply exclusiveminimum to an integer")
323+
if !hasNumericType(schema) {
324+
return fmt.Errorf("must apply exclusiveminimum to a numeric value, found %s", schema.Type)
292325
}
326+
293327
schema.ExclusiveMinimum = bool(m)
294328
return nil
295329
}
330+
296331
func (m MultipleOf) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
297-
if schema.Type != "integer" {
298-
return fmt.Errorf("must apply multipleof to an integer")
332+
if !hasNumericType(schema) {
333+
return fmt.Errorf("must apply multipleof to a numeric value, found %s", schema.Type)
299334
}
300-
val := float64(m)
335+
336+
if schema.Type == "integer" && !isIntegral(m.Value()) {
337+
return fmt.Errorf("cannot apply non-integral multipleof validation (%v) to integer value", m.Value())
338+
}
339+
340+
val := m.Value()
301341
schema.MultipleOf = &val
302342
return nil
303343
}
@@ -310,6 +350,7 @@ func (m MaxLength) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
310350
schema.MaxLength = &val
311351
return nil
312352
}
353+
313354
func (m MinLength) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
314355
if schema.Type != "string" {
315356
return fmt.Errorf("must apply minlength to a string")
@@ -318,6 +359,7 @@ func (m MinLength) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
318359
schema.MinLength = &val
319360
return nil
320361
}
362+
321363
func (m Pattern) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
322364
// Allow string types or IntOrStrings. An IntOrString will still
323365
// apply the pattern validation when a string is detected, the pattern
@@ -337,6 +379,7 @@ func (m MaxItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
337379
schema.MaxItems = &val
338380
return nil
339381
}
382+
340383
func (m MinItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
341384
if schema.Type != "array" {
342385
return fmt.Errorf("must apply minitems to an array")
@@ -345,6 +388,7 @@ func (m MinItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
345388
schema.MinItems = &val
346389
return nil
347390
}
391+
348392
func (m UniqueItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
349393
if schema.Type != "array" {
350394
return fmt.Errorf("must apply uniqueitems to an array")
@@ -388,6 +432,7 @@ func (m Enum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
388432
schema.Enum = vals
389433
return nil
390434
}
435+
391436
func (m Format) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
392437
schema.Format = string(m)
393438
return nil

pkg/crd/markers/zz_generated.markerhelp.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/crd/parser_integration_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ var _ = Describe("CRD Generation From Parsing to CustomResourceDefinition", func
7575
Collector: &markers.Collector{Registry: reg},
7676
Checker: &loader.TypeChecker{},
7777
IgnoreUnexportedFields: true,
78+
AllowDangerousTypes: true, // need to allow “dangerous types” in this file for testing
7879
}
7980
crd.AddKnownTypes(parser)
8081

@@ -189,6 +190,5 @@ var _ = Describe("CRD Generation From Parsing to CustomResourceDefinition", func
189190

190191
By("checking that no errors occurred along the way (expect for type errors)")
191192
Expect(packageErrors(cronJobPkg, packages.TypeError)).NotTo(HaveOccurred())
192-
193193
})
194194
})

pkg/crd/testdata/cronjob_types.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ limitations under the License.
1616
// TODO(directxman12): test this across both versions (right now we're just
1717
// trusting k/k conversion, which is probably fine though)
1818

19-
//go:generate ../../../.run-controller-gen.sh crd:ignoreUnexportedFields=true paths=./;./deprecated;./unserved output:dir=.
19+
//go:generate ../../../.run-controller-gen.sh crd:ignoreUnexportedFields=true,allowDangerousTypes=true paths=./;./deprecated;./unserved output:dir=.
2020

2121
// +groupName=testdata.kubebuilder.io
2222
// +versionName=v1
@@ -182,6 +182,26 @@ type CronJobSpec struct {
182182
// Maps of arrays of things-that-aren’t-strings are permitted
183183
MapOfArraysOfFloats map[string][]bool `json:"mapOfArraysOfFloats,omitempty"`
184184

185+
// +kubebuilder:validation:Minimum=-0.5
186+
// +kubebuilder:validation:Maximum=1.5
187+
// +kubebuilder:validation:MultipleOf=0.5
188+
FloatWithValidations float64 `json:"floatWithValidations"`
189+
190+
// +kubebuilder:validation:Minimum=-0.5
191+
// +kubebuilder:validation:Maximum=1.5
192+
// +kubebuilder:validation:MultipleOf=0.5
193+
Float64WithValidations float64 `json:"float64WithValidations"`
194+
195+
// +kubebuilder:validation:Minimum=-2
196+
// +kubebuilder:validation:Maximum=2
197+
// +kubebuilder:validation:MultipleOf=2
198+
IntWithValidations int `json:"intWithValidations"`
199+
200+
// +kubebuilder:validation:Minimum=-2
201+
// +kubebuilder:validation:Maximum=2
202+
// +kubebuilder:validation:MultipleOf=2
203+
Int32WithValidations int32 `json:"int32WithValidations"`
204+
185205
// This tests that unexported fields are skipped in the schema generation
186206
unexportedField string
187207

pkg/crd/testdata/testdata.kubebuilder.io_cronjobs.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,26 @@ spec:
139139
a pointer to distinguish between explicit zero and not specified.
140140
format: int32
141141
type: integer
142+
float64WithValidations:
143+
maximum: 1.5
144+
minimum: -0.5
145+
multipleOf: 0.5
146+
type: number
147+
floatWithValidations:
148+
maximum: 1.5
149+
minimum: -0.5
150+
multipleOf: 0.5
151+
type: number
142152
foo:
143153
description: This tests that exported fields are not skipped in the
144154
schema generation
145155
type: string
156+
int32WithValidations:
157+
format: int32
158+
maximum: 2
159+
minimum: -2
160+
multipleOf: 2
161+
type: integer
146162
intOrStringWithAPattern:
147163
anyOf:
148164
- type: integer
@@ -153,6 +169,11 @@ spec:
153169
for having a pattern on this type.
154170
pattern: ^((100|[0-9]{1,2})%|[0-9]+)$
155171
x-kubernetes-int-or-string: true
172+
intWithValidations:
173+
maximum: 2
174+
minimum: -2
175+
multipleOf: 2
176+
type: integer
156177
jobTemplate:
157178
description: Specifies the job that will be created when executing
158179
a CronJob.
@@ -7346,7 +7367,11 @@ spec:
73467367
- defaultedSlice
73477368
- defaultedString
73487369
- embeddedResource
7370+
- float64WithValidations
7371+
- floatWithValidations
73497372
- foo
7373+
- int32WithValidations
7374+
- intWithValidations
73507375
- jobTemplate
73517376
- mapOfInfo
73527377
- patternObject

0 commit comments

Comments
 (0)