sigs.k8s.io/cluster-api@v1.7.1/exp/runtime/topologymutation/walker_test.go (about) 1 /* 2 Copyright 2022 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package topologymutation 18 19 import ( 20 "bytes" 21 "context" 22 "encoding/json" 23 "fmt" 24 "testing" 25 26 . "github.com/onsi/gomega" 27 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/apimachinery/pkg/runtime" 30 "k8s.io/apimachinery/pkg/runtime/serializer" 31 "k8s.io/apimachinery/pkg/types" 32 33 bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1" 34 controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1" 35 runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1" 36 ) 37 38 var ( 39 testScheme = runtime.NewScheme() 40 ) 41 42 func init() { 43 _ = controlplanev1.AddToScheme(testScheme) 44 _ = bootstrapv1.AddToScheme(testScheme) 45 } 46 47 func Test_WalkTemplates(t *testing.T) { 48 g := NewWithT(t) 49 50 decoder := serializer.NewCodecFactory(testScheme).UniversalDecoder( 51 controlplanev1.GroupVersion, 52 bootstrapv1.GroupVersion, 53 ) 54 mutatingFunc := func(_ context.Context, obj runtime.Object, _ map[string]apiextensionsv1.JSON, _ runtimehooksv1.HolderReference) error { 55 switch obj := obj.(type) { 56 case *controlplanev1.KubeadmControlPlaneTemplate: 57 obj.Annotations = map[string]string{"a": "a"} 58 case *bootstrapv1.KubeadmConfigTemplate: 59 obj.Annotations = map[string]string{"b": "b"} 60 } 61 return nil 62 } 63 kubeadmControlPlaneTemplate := controlplanev1.KubeadmControlPlaneTemplate{ 64 TypeMeta: metav1.TypeMeta{ 65 Kind: "KubeadmControlPlaneTemplate", 66 APIVersion: controlplanev1.GroupVersion.String(), 67 }, 68 } 69 kubeadmConfigTemplate := bootstrapv1.KubeadmConfigTemplate{ 70 TypeMeta: metav1.TypeMeta{ 71 Kind: "KubeadmConfigTemplate", 72 APIVersion: bootstrapv1.GroupVersion.String(), 73 }, 74 } 75 tests := []struct { 76 name string 77 globalVariables []runtimehooksv1.Variable 78 requestItems []runtimehooksv1.GeneratePatchesRequestItem 79 expectedResponse *runtimehooksv1.GeneratePatchesResponse 80 options []WalkTemplatesOption 81 }{ 82 { 83 name: "Fails for invalid builtin variables", 84 globalVariables: []runtimehooksv1.Variable{ 85 newVariable(runtimehooksv1.BuiltinsName, runtimehooksv1.Builtins{ 86 Cluster: &runtimehooksv1.ClusterBuiltins{ 87 Name: "test", 88 }, 89 }), 90 }, 91 requestItems: []runtimehooksv1.GeneratePatchesRequestItem{ 92 requestItem("1", kubeadmControlPlaneTemplate, []runtimehooksv1.Variable{ 93 newVariable(runtimehooksv1.BuiltinsName, "{invalid-builtin-value}"), 94 }), 95 }, 96 expectedResponse: &runtimehooksv1.GeneratePatchesResponse{ 97 CommonResponse: runtimehooksv1.CommonResponse{ 98 Status: runtimehooksv1.ResponseStatusFailure, 99 Message: fmt.Sprintf("failed to merge builtin variables: failed to unmarshal builtin variable: json: cannot unmarshal string into Go value of type %s.Builtins", runtimehooksv1.GroupVersion.Version), 100 }, 101 }, 102 }, 103 { 104 name: "Silently ignore unknown types if FailForUnknownTypes option is not set", 105 requestItems: []runtimehooksv1.GeneratePatchesRequestItem{ 106 { 107 UID: types.UID("1"), 108 Object: runtime.RawExtension{ 109 Raw: []byte("{\"kind\":\"Unknown\",\"apiVersion\":\"controlplane.cluster.x-k8s.io/v1beta1\"}"), 110 }, 111 }, 112 }, 113 expectedResponse: &runtimehooksv1.GeneratePatchesResponse{ 114 CommonResponse: runtimehooksv1.CommonResponse{ 115 Status: runtimehooksv1.ResponseStatusSuccess, 116 }, 117 }, 118 }, 119 { 120 name: "Fails for unknown types if FailForUnknownTypes option is set", 121 requestItems: []runtimehooksv1.GeneratePatchesRequestItem{ 122 { 123 UID: types.UID("1"), 124 Object: runtime.RawExtension{ 125 Raw: []byte("{\"kind\":\"Unknown\",\"apiVersion\":\"controlplane.cluster.x-k8s.io/v1beta1\"}"), 126 }, 127 }, 128 }, 129 expectedResponse: &runtimehooksv1.GeneratePatchesResponse{ 130 CommonResponse: runtimehooksv1.CommonResponse{ 131 Status: runtimehooksv1.ResponseStatusFailure, 132 Message: "no kind \"Unknown\" is registered for version \"controlplane.cluster.x-k8s." + 133 "io/v1beta1\" in scheme", 134 }, 135 }, 136 options: []WalkTemplatesOption{ 137 FailForUnknownTypes{}, 138 }, 139 }, 140 { 141 name: "Fails for invalid holder ref", 142 requestItems: []runtimehooksv1.GeneratePatchesRequestItem{ 143 { 144 UID: types.UID("1"), 145 Object: runtime.RawExtension{ 146 Raw: toJSON(kubeadmConfigTemplate), 147 }, 148 HolderReference: runtimehooksv1.HolderReference{ 149 APIVersion: "invalid/cluster.x-k8s.io/v1beta1", 150 }, 151 }, 152 }, 153 expectedResponse: &runtimehooksv1.GeneratePatchesResponse{ 154 CommonResponse: runtimehooksv1.CommonResponse{ 155 Status: runtimehooksv1.ResponseStatusFailure, 156 Message: "error generating patches - HolderReference apiVersion \"invalid/cluster.x-k8s." + 157 "io/v1beta1\" is not in valid format: unexpected GroupVersion string: invalid/cluster.x-k8s." + 158 "io/v1beta1", 159 }, 160 }, 161 options: []WalkTemplatesOption{ 162 FailForUnknownTypes{}, 163 }, 164 }, 165 { 166 name: "Creates Json patches by default", 167 requestItems: []runtimehooksv1.GeneratePatchesRequestItem{ 168 requestItem("1", kubeadmControlPlaneTemplate, nil), 169 requestItem("2", kubeadmConfigTemplate, nil), 170 }, 171 expectedResponse: &runtimehooksv1.GeneratePatchesResponse{ 172 CommonResponse: runtimehooksv1.CommonResponse{ 173 Status: runtimehooksv1.ResponseStatusSuccess, 174 }, 175 Items: []runtimehooksv1.GeneratePatchesResponseItem{ 176 responseItem("1", "[{\"op\":\"add\",\"path\":\"/metadata/annotations\",\"value\":{\"a\":\"a\"}}]", runtimehooksv1.JSONPatchType), 177 responseItem("2", "[{\"op\":\"add\",\"path\":\"/metadata/annotations\",\"value\":{\"b\":\"b\"}}]", runtimehooksv1.JSONPatchType), 178 }, 179 }, 180 }, 181 { 182 name: "Creates Json patches if option PatchFormat is set to JSONPatchType", 183 requestItems: []runtimehooksv1.GeneratePatchesRequestItem{ 184 requestItem("1", kubeadmControlPlaneTemplate, nil), 185 requestItem("2", kubeadmConfigTemplate, nil), 186 }, 187 expectedResponse: &runtimehooksv1.GeneratePatchesResponse{ 188 CommonResponse: runtimehooksv1.CommonResponse{ 189 Status: runtimehooksv1.ResponseStatusSuccess, 190 }, 191 Items: []runtimehooksv1.GeneratePatchesResponseItem{ 192 responseItem("1", "[{\"op\":\"add\",\"path\":\"/metadata/annotations\",\"value\":{\"a\":\"a\"}}]", runtimehooksv1.JSONPatchType), 193 responseItem("2", "[{\"op\":\"add\",\"path\":\"/metadata/annotations\",\"value\":{\"b\":\"b\"}}]", runtimehooksv1.JSONPatchType), 194 }, 195 }, 196 options: []WalkTemplatesOption{ 197 PatchFormat{Format: runtimehooksv1.JSONPatchType}, 198 }, 199 }, 200 { 201 name: "Creates Json Merge patches if option PatchFormat is set to JSONMergePatchType", 202 requestItems: []runtimehooksv1.GeneratePatchesRequestItem{ 203 requestItem("1", kubeadmControlPlaneTemplate, nil), 204 requestItem("2", kubeadmConfigTemplate, nil), 205 }, 206 expectedResponse: &runtimehooksv1.GeneratePatchesResponse{ 207 CommonResponse: runtimehooksv1.CommonResponse{ 208 Status: runtimehooksv1.ResponseStatusSuccess, 209 }, 210 Items: []runtimehooksv1.GeneratePatchesResponseItem{ 211 responseItem("1", "{\"metadata\":{\"annotations\":{\"a\":\"a\"}}}", runtimehooksv1.JSONMergePatchType), 212 responseItem("2", "{\"metadata\":{\"annotations\":{\"b\":\"b\"}}}", runtimehooksv1.JSONMergePatchType), 213 }, 214 }, 215 options: []WalkTemplatesOption{ 216 PatchFormat{Format: runtimehooksv1.JSONMergePatchType}, 217 }, 218 }, 219 } 220 for _, tt := range tests { 221 t.Run(tt.name, func(*testing.T) { 222 response := &runtimehooksv1.GeneratePatchesResponse{} 223 request := &runtimehooksv1.GeneratePatchesRequest{Variables: tt.globalVariables, Items: tt.requestItems} 224 225 WalkTemplates(context.Background(), decoder, request, response, mutatingFunc, tt.options...) 226 227 g.Expect(response.Status).To(Equal(tt.expectedResponse.Status)) 228 g.Expect(response.Message).To(ContainSubstring(tt.expectedResponse.Message)) 229 for i, item := range response.Items { 230 expectedItem := tt.expectedResponse.Items[i] 231 g.Expect(item.PatchType).To(Equal(expectedItem.PatchType)) 232 g.Expect(item.UID).To(Equal(expectedItem.UID)) 233 234 switch item.PatchType { 235 case runtimehooksv1.JSONPatchType: 236 // Note; the order of the individual patch operations in Items[].Patch is not deterministic so we unmarshal 237 // to an array and check that the arrays hold equivalent items. 238 var actualPatchOps []map[string]interface{} 239 var expectedPatchOps []map[string]interface{} 240 g.Expect(json.Unmarshal(item.Patch, &actualPatchOps)).To(Succeed()) 241 g.Expect(json.Unmarshal(expectedItem.Patch, &expectedPatchOps)).To(Succeed()) 242 g.Expect(actualPatchOps).To(ConsistOf(expectedPatchOps)) 243 case runtimehooksv1.JSONMergePatchType: 244 g.Expect(item.Patch).To(Equal(expectedItem.Patch)) 245 } 246 } 247 }) 248 } 249 } 250 251 // toJSONCompact is used to be able to write JSON values in a readable manner. 252 func toJSONCompact(value string) []byte { 253 var compactValue bytes.Buffer 254 if err := json.Compact(&compactValue, []byte(value)); err != nil { 255 panic(err) 256 } 257 return compactValue.Bytes() 258 } 259 260 // toJSON marshals the object and returns a byte array. This function panics on any error. 261 func toJSON(val interface{}) []byte { 262 jsonStr, err := json.Marshal(val) 263 if err != nil { 264 panic(err) 265 } 266 return jsonStr 267 } 268 269 // requestItem returns a GeneratePatchesRequestItem with the given uid, variables and object. 270 func requestItem(uid string, object interface{}, variables []runtimehooksv1.Variable) runtimehooksv1.GeneratePatchesRequestItem { 271 return runtimehooksv1.GeneratePatchesRequestItem{ 272 UID: types.UID(uid), 273 Variables: variables, 274 Object: runtime.RawExtension{ 275 Raw: toJSON(object), 276 }, 277 } 278 } 279 280 // responseItem returns a GeneratePatchesResponseItem of PatchType JSONPatch with the passed uid and patch. 281 func responseItem(uid, patch string, format runtimehooksv1.PatchType) runtimehooksv1.GeneratePatchesResponseItem { 282 return runtimehooksv1.GeneratePatchesResponseItem{ 283 UID: types.UID(uid), 284 PatchType: format, 285 Patch: toJSONCompact(patch), 286 } 287 } 288 289 // newVariable returns a runtimehooksv1.Variable with the passed name and value. 290 func newVariable(name string, value interface{}) runtimehooksv1.Variable { 291 return runtimehooksv1.Variable{ 292 Name: name, 293 Value: apiextensionsv1.JSON{Raw: toJSON(value)}, 294 } 295 }