@@ -45,7 +45,6 @@ import (
45
45
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
46
46
"k8s.io/apimachinery/pkg/runtime"
47
47
"k8s.io/apimachinery/pkg/runtime/schema"
48
- utilerrors "k8s.io/apimachinery/pkg/util/errors"
49
48
"k8s.io/apimachinery/pkg/util/uuid"
50
49
"k8s.io/apimachinery/pkg/util/wait"
51
50
utilyaml "k8s.io/apimachinery/pkg/util/yaml"
@@ -82,7 +81,7 @@ type ratchetingTestContext struct {
82
81
* testing.T
83
82
DynamicClient dynamic.Interface
84
83
APIExtensionsClient clientset.Interface
85
- SkipStatus bool
84
+ StatusSubresource bool
86
85
}
87
86
88
87
type ratchetingTestOperation interface {
@@ -97,12 +96,6 @@ type expectError struct {
97
96
func (e expectError ) Do (ctx * ratchetingTestContext ) error {
98
97
err := e .op .Do (ctx )
99
98
if err != nil {
100
- // If there is an error, it should happen when updating the CR
101
- // and also when updating the CR status subresource.
102
- if agg , ok := err .(utilerrors.Aggregate ); ok && len (agg .Errors ()) != 2 {
103
- return fmt .Errorf ("expected 2 errors, got %v" , len (agg .Errors ()))
104
- }
105
-
106
99
return nil
107
100
}
108
101
return errors .New ("expected error" )
@@ -172,7 +165,12 @@ func (a applyPatchOperation) Do(ctx *ratchetingTestContext) error {
172
165
173
166
patch := & unstructured.Unstructured {}
174
167
if obj , ok := a .patch .(map [string ]interface {}); ok {
175
- patch .Object = obj
168
+ patch .Object = map [string ]interface {}{}
169
+
170
+ // Copy the map at the top level to avoid modifying the original.
171
+ for k , v := range obj {
172
+ patch .Object [k ] = v
173
+ }
176
174
} else if str , ok := a .patch .(string ); ok {
177
175
str = FixTabsOrDie (str )
178
176
if err := utilyaml .NewYAMLOrJSONDecoder (strings .NewReader (str ), len (str )).Decode (& patch .Object ); err != nil {
@@ -182,67 +180,31 @@ func (a applyPatchOperation) Do(ctx *ratchetingTestContext) error {
182
180
return fmt .Errorf ("invalid patch type: %T" , a .patch )
183
181
}
184
182
183
+ if ctx .StatusSubresource {
184
+ patch .Object = map [string ]interface {}{"status" : patch .Object }
185
+ }
186
+
185
187
patch .SetKind (kind )
186
188
patch .SetAPIVersion (a .gvr .GroupVersion ().String ())
187
189
patch .SetName (a .name )
188
190
patch .SetNamespace ("default" )
189
191
190
- _ , err := ctx .DynamicClient .
191
- Resource ( a . gvr ).
192
- Namespace ( patch .GetNamespace ()).
193
- Apply (
194
- context . TODO (),
195
- patch . GetName (),
196
- patch ,
197
- metav1. ApplyOptions {
198
- FieldManager : "manager" ,
199
- })
192
+ c := ctx .DynamicClient .Resource ( a . gvr ). Namespace ( patch . GetNamespace ())
193
+ if ctx . StatusSubresource {
194
+ if _ , err := c . Get ( context . TODO (), patch .GetName (), metav1. GetOptions {}); apierrors . IsNotFound ( err ) {
195
+ // ApplyStatus will not automatically create an object, we must make sure it exists before we can
196
+ // apply the status to it.
197
+ _ , err := c . Create ( context . TODO (), patch , metav1. CreateOptions {})
198
+ if err != nil {
199
+ return err
200
+ }
201
+ }
200
202
201
- if ctx . SkipStatus {
203
+ _ , err := c . ApplyStatus ( context . TODO (), patch . GetName (), patch , metav1. ApplyOptions { FieldManager : "manager" })
202
204
return err
203
205
}
204
-
205
- errs := []error {}
206
-
207
- if err != nil {
208
- errs = append (errs , err )
209
- }
210
-
211
- statusPatch := & unstructured.Unstructured {
212
- Object : map [string ]interface {}{
213
- "status" : patch .Object ,
214
- },
215
- }
216
-
217
- statusPatch .SetKind (kind )
218
- statusPatch .SetAPIVersion (a .gvr .GroupVersion ().String ())
219
- statusPatch .SetName (a .name )
220
- statusPatch .SetNamespace ("default" )
221
-
222
- delete (patch .Object , "metadata" )
223
- delete (patch .Object , "apiVersion" )
224
- delete (patch .Object , "kind" )
225
-
226
- _ , err = ctx .DynamicClient .
227
- Resource (a .gvr ).
228
- Namespace (statusPatch .GetNamespace ()).
229
- ApplyStatus (
230
- context .TODO (),
231
- statusPatch .GetName (),
232
- statusPatch ,
233
- metav1.ApplyOptions {
234
- FieldManager : "manager" ,
235
- })
236
-
237
- if err != nil {
238
- errs = append (errs , err )
239
- }
240
-
241
- if len (errs ) > 0 {
242
- return utilerrors .NewAggregate (errs )
243
- }
244
-
245
- return nil
206
+ _ , err := c .Apply (context .TODO (), patch .GetName (), patch , metav1.ApplyOptions {FieldManager : "manager" })
207
+ return err
246
208
}
247
209
248
210
func (a applyPatchOperation ) Description () string {
@@ -270,6 +232,17 @@ func (u updateMyCRDV1Beta1Schema) Do(ctx *ratchetingTestContext) error {
270
232
sch .Properties = map [string ]apiextensionsv1.JSONSchemaProps {}
271
233
}
272
234
235
+ if ctx .StatusSubresource {
236
+ sch = & apiextensionsv1.JSONSchemaProps {
237
+ Type : "object" ,
238
+ Properties : map [string ]apiextensionsv1.JSONSchemaProps {
239
+ "status" : * sch ,
240
+ },
241
+ }
242
+ }
243
+
244
+ // sentinel must be in the root level of the schema.
245
+ // Do not include this in the status schema.
273
246
uuidString := string (uuid .NewUUID ())
274
247
sentinelName := "__ratcheting_sentinel_field__"
275
248
sch .Properties [sentinelName ] = apiextensionsv1.JSONSchemaProps {
@@ -279,21 +252,11 @@ func (u updateMyCRDV1Beta1Schema) Do(ctx *ratchetingTestContext) error {
279
252
}},
280
253
}
281
254
282
- if ! ctx .SkipStatus {
283
- // Duplicate the schema as a status schema
284
- statusSchema := sch .DeepCopy ()
285
- sch .Properties ["status" ] = * statusSchema
286
- }
287
-
288
255
for _ , v := range myCRD .Spec .Versions {
289
256
if v .Name != myCRDV1Beta1 .Version {
290
257
continue
291
258
}
292
259
293
- * v .Subresources = apiextensionsv1.CustomResourceSubresources {
294
- Status : & apiextensionsv1.CustomResourceSubresourceStatus {},
295
- }
296
-
297
260
v .Schema .OpenAPIV3Schema = sch
298
261
}
299
262
@@ -315,7 +278,12 @@ func (u updateMyCRDV1Beta1Schema) Do(ctx *ratchetingTestContext) error {
315
278
name : "sentinel-resource" ,
316
279
patch : map [string ]interface {}{
317
280
sentinelName : fmt .Sprintf ("invalid-%d" , counter ),
318
- }}.Do (ctx )
281
+ }}.Do (& ratchetingTestContext {
282
+ T : ctx .T ,
283
+ DynamicClient : ctx .DynamicClient ,
284
+ APIExtensionsClient : ctx .APIExtensionsClient ,
285
+ StatusSubresource : false , // Do not carry this over, sentinel check is in the root level.
286
+ })
319
287
320
288
if err == nil {
321
289
return false , errors .New ("expected error when creating sentinel resource" )
@@ -344,18 +312,17 @@ type patchMyCRDV1Beta1Schema struct {
344
312
}
345
313
346
314
func (p patchMyCRDV1Beta1Schema ) Do (ctx * ratchetingTestContext ) error {
347
- var err error
348
- patchJSON , err := json .Marshal (p .patch )
349
- if err != nil {
350
- return err
315
+ patch := p .patch
316
+ if ctx .StatusSubresource {
317
+ patch = map [string ]interface {}{
318
+ "properties" : map [string ]interface {}{
319
+ "status" : patch ,
320
+ },
321
+ }
351
322
}
352
323
353
- statusPatch := map [string ]interface {}{
354
- "properties" : map [string ]interface {}{
355
- "status" : p .patch ,
356
- },
357
- }
358
- statusPatchJSON , err := json .Marshal (statusPatch )
324
+ var err error
325
+ patchJSON , err := json .Marshal (patch )
359
326
if err != nil {
360
327
return err
361
328
}
@@ -380,19 +347,19 @@ func (p patchMyCRDV1Beta1Schema) Do(ctx *ratchetingTestContext) error {
380
347
return err
381
348
}
382
349
383
- mergedStatus , err := jsonpatch .MergePatch (merged , statusPatchJSON )
384
- if err != nil {
385
- return err
386
- }
387
-
388
350
var parsed apiextensionsv1.JSONSchemaProps
389
- if err := json .Unmarshal (mergedStatus , & parsed ); err != nil {
351
+ if err := json .Unmarshal (merged , & parsed ); err != nil {
390
352
return err
391
353
}
392
354
393
355
return updateMyCRDV1Beta1Schema {
394
356
newSchema : & parsed ,
395
- }.Do (ctx )
357
+ }.Do (& ratchetingTestContext {
358
+ T : ctx .T ,
359
+ DynamicClient : ctx .DynamicClient ,
360
+ APIExtensionsClient : ctx .APIExtensionsClient ,
361
+ StatusSubresource : false , // We have already handled the status subresource.
362
+ })
396
363
}
397
364
398
365
return fmt .Errorf ("could not find version %v in CRD %v" , myCRDV1Beta1 .Version , myCRD .Name )
@@ -478,14 +445,7 @@ func runTests(t *testing.T, cases []ratchetingTestCase) {
478
445
continue
479
446
}
480
447
481
- t .Run (c .Name , func (t * testing.T ) {
482
- ctx := & ratchetingTestContext {
483
- T : t ,
484
- DynamicClient : dynamicClient ,
485
- APIExtensionsClient : apiExtensionClient ,
486
- SkipStatus : c .SkipStatus ,
487
- }
488
-
448
+ run := func (t * testing.T , ctx * ratchetingTestContext ) {
489
449
for i , op := range c .Operations {
490
450
t .Logf ("Performing Operation: %v" , op .Description ())
491
451
if err := op .Do (ctx ); err != nil {
@@ -498,7 +458,26 @@ func runTests(t *testing.T, cases []ratchetingTestCase) {
498
458
if err != nil {
499
459
t .Fatal (err )
500
460
}
461
+ }
462
+
463
+ t .Run (c .Name , func (t * testing.T ) {
464
+ run (t , & ratchetingTestContext {
465
+ T : t ,
466
+ DynamicClient : dynamicClient ,
467
+ APIExtensionsClient : apiExtensionClient ,
468
+ })
501
469
})
470
+
471
+ if ! c .SkipStatus {
472
+ t .Run ("Status: " + c .Name , func (t * testing.T ) {
473
+ run (t , & ratchetingTestContext {
474
+ T : t ,
475
+ DynamicClient : dynamicClient ,
476
+ APIExtensionsClient : apiExtensionClient ,
477
+ StatusSubresource : true ,
478
+ })
479
+ })
480
+ }
502
481
}
503
482
}
504
483
@@ -1473,8 +1452,7 @@ func TestRatchetingFunctionality(t *testing.T) {
1473
1452
Name : "Not_should_not_ratchet" ,
1474
1453
},
1475
1454
{
1476
- Name : "CEL_transition_rules_should_not_ratchet" ,
1477
- SkipStatus : true , // Adding a broken status transition rule prevents the object from being updated.
1455
+ Name : "CEL_transition_rules_should_not_ratchet" ,
1478
1456
Operations : []ratchetingTestOperation {
1479
1457
updateMyCRDV1Beta1Schema {& apiextensionsv1.JSONSchemaProps {
1480
1458
Type : "object" ,
0 commit comments