@@ -16,11 +16,15 @@ limitations under the License.
1616package cronjob
1717
1818import (
19+ "context"
1920 "os"
21+ "time"
2022
2123 . "github.com/onsi/ginkgo/v2"
2224 . "github.com/onsi/gomega"
2325 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
26+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
27+ "k8s.io/apimachinery/pkg/runtime/schema"
2428 "sigs.k8s.io/yaml"
2529)
2630
@@ -36,4 +40,98 @@ var _ = Describe("CronJob CRD", func() {
3640 err = k8sClient .Create (ctx , crd )
3741 Expect (err ).NotTo (HaveOccurred ())
3842 })
43+
44+ Context ("validating opaque markers" , func () {
45+ applyCronJob := func (ctx context.Context , name , opaqueVal , nonOpaqueVal string ) error {
46+ obj := & unstructured.Unstructured {}
47+ obj .SetGroupVersionKind (schema.GroupVersionKind {
48+ Group : "testdata.kubebuilder.io" ,
49+ Version : "v1" ,
50+ Kind : "CronJob" ,
51+ })
52+ obj .SetName (name )
53+ obj .SetNamespace ("default" )
54+
55+ spec := map [string ]interface {}{
56+ "schedule" : "*/5 * * * *" , // required field
57+ "foo" : "bar" ,
58+ "baz" : "baz" ,
59+ "binaryName" : "YmluYXJ5" , // base64 for "binary"
60+ "canBeNull" : "ok" ,
61+ "defaultedEmptyMap" : map [string ]interface {}{},
62+ "defaultedEmptyObject" : map [string ]interface {}{},
63+ "defaultedEmptySlice" : []interface {}{},
64+ "defaultedObject" : []interface {}{},
65+ "defaultedSlice" : []interface {}{},
66+ "defaultedString" : "some string" ,
67+ "doubleDefaultedString" : "some string" ,
68+ "embeddedResource" : map [string ]interface {}{"kind" : "Pod" , "apiVersion" : "v1" },
69+ "explicitlyRequiredK8s" : "required" ,
70+ "explicitlyRequiredKubebuilder" : "required" ,
71+ "explicitlyRequiredKubernetes" : "required" ,
72+ "float64WithValidations" : 1.5 ,
73+ "floatWithValidations" : 1.5 ,
74+ "int32WithValidations" : 2 ,
75+ "intWithValidations" : 2 ,
76+ "jobTemplate" : map [string ]interface {}{
77+ "template" : map [string ]interface {}{},
78+ },
79+ "kubernetesDefaultedEmptyMap" : map [string ]interface {}{},
80+ "kubernetesDefaultedEmptyObject" : map [string ]interface {}{},
81+ "kubernetesDefaultedEmptySlice" : []interface {}{},
82+ "kubernetesDefaultedObject" : []interface {}{},
83+ "kubernetesDefaultedSlice" : []interface {}{},
84+ "kubernetesDefaultedString" : "string" ,
85+ "mapOfInfo" : map [string ]interface {}{},
86+ "nestedMapOfInfo" : map [string ]interface {}{},
87+ "nestedStructWithSeveralFields" : map [string ]interface {}{"foo" : "str" , "bar" : true },
88+ "nestedStructWithSeveralFieldsDoubleMarked" : map [string ]interface {}{"foo" : "str" , "bar" : true },
89+ "nestedassociativeList" : []interface {}{},
90+ "patternObject" : "https://example.com" ,
91+ "stringPair" : []interface {}{"a" , "b" },
92+ "structWithSeveralFields" : map [string ]interface {}{"foo" : "str" , "bar" : true },
93+ "twoOfAKindPart0" : "longenough" ,
94+ "twoOfAKindPart1" : "longenough" ,
95+ "unprunedEmbeddedResource" : map [string ]interface {}{"kind" : "Pod" , "apiVersion" : "v1" },
96+ "unprunedFomType" : map [string ]interface {}{},
97+ "unprunedFomTypeAndField" : map [string ]interface {}{},
98+ "unprunedJSON" : map [string ]interface {}{"foo" : "str" , "bar" : true },
99+ "associativeList" : []interface {}{},
100+ }
101+ if opaqueVal != "" {
102+ spec ["opaqueField" ] = opaqueVal
103+ }
104+ if nonOpaqueVal != "" {
105+ spec ["nonOpaqueField" ] = nonOpaqueVal
106+ }
107+ obj .Object ["spec" ] = spec
108+
109+ return k8sClient .Create (ctx , obj )
110+ }
111+
112+ It ("should suppress type-level validation for fields with +k8s:opaque" , func (ctx SpecContext ) {
113+ // type-level validation is MinLength=4
114+ // field-level validation is MaxLength=5
115+
116+ By ("allowing opaqueField with length 3 (suppresses type-level MinLength=4)" )
117+ Eventually (func () error {
118+ return applyCronJob (ctx , "test-opaque-short" , "abc" , "" )
119+ }, 5 * time .Second , 1 * time .Second ).Should (Succeed ())
120+
121+ By ("rejecting nonOpaqueField with length 3 (inherits type-level MinLength=4)" )
122+ err := applyCronJob (ctx , "test-non-opaque-short" , "" , "abc" )
123+ Expect (err ).To (HaveOccurred ())
124+ Expect (err .Error ()).To (ContainSubstring ("should be at least 4 chars long" ))
125+
126+ By ("rejecting opaqueField with length 6 (applies field-level MaxLength=5)" )
127+ err = applyCronJob (ctx , "test-opaque-long" , "abcdef" , "" )
128+ Expect (err ).To (HaveOccurred ())
129+ Expect (err .Error ()).To (ContainSubstring ("Too long" ))
130+
131+ By ("rejecting nonOpaqueField with length 6 (applies field-level MaxLength=5)" )
132+ err = applyCronJob (ctx , "test-non-opaque-long" , "" , "abcdef" )
133+ Expect (err ).To (HaveOccurred ())
134+ Expect (err .Error ()).To (ContainSubstring ("Too long" ))
135+ })
136+ })
39137})
0 commit comments