istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/k8s/configutil_test.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package k8s 16 17 import ( 18 "fmt" 19 "reflect" 20 "testing" 21 22 "github.com/google/go-cmp/cmp" 23 v1 "k8s.io/api/core/v1" 24 "k8s.io/apimachinery/pkg/api/errors" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/runtime" 27 "k8s.io/apimachinery/pkg/runtime/schema" 28 "k8s.io/client-go/kubernetes/fake" 29 ktesting "k8s.io/client-go/testing" 30 31 "istio.io/istio/pkg/config/constants" 32 "istio.io/istio/pkg/kube" 33 "istio.io/istio/pkg/kube/kclient" 34 "istio.io/istio/pkg/test" 35 ) 36 37 const ( 38 configMapName = "test-configmap-name" 39 namespaceName = "test-ns" 40 dataName = "test-data-name" 41 ) 42 43 func TestUpdateDataInConfigMap(t *testing.T) { 44 gvr := schema.GroupVersionResource{ 45 Resource: "configmaps", 46 Version: "v1", 47 } 48 caBundle := "test-data" 49 testData := map[string]string{ 50 constants.CACertNamespaceConfigMapDataName: "test-data", 51 } 52 testCases := []struct { 53 name string 54 existingConfigMap *v1.ConfigMap 55 expectedActions []ktesting.Action 56 expectedErr string 57 }{ 58 { 59 name: "non-existing ConfigMap", 60 expectedErr: "cannot update nil configmap", 61 }, 62 { 63 name: "existing empty ConfigMap", 64 existingConfigMap: createConfigMap(namespaceName, configMapName, map[string]string{}), 65 expectedActions: []ktesting.Action{ 66 ktesting.NewUpdateAction(gvr, namespaceName, createConfigMap(namespaceName, configMapName, testData)), 67 }, 68 expectedErr: "", 69 }, 70 { 71 name: "existing nop ConfigMap", 72 existingConfigMap: createConfigMap(namespaceName, configMapName, testData), 73 expectedActions: []ktesting.Action{}, 74 expectedErr: "", 75 }, 76 { 77 name: "existing with other keys", 78 existingConfigMap: createConfigMap(namespaceName, configMapName, map[string]string{"foo": "bar"}), 79 expectedActions: []ktesting.Action{ 80 ktesting.NewUpdateAction(gvr, namespaceName, createConfigMap(namespaceName, configMapName, 81 map[string]string{"test-key": "test-data", "foo": "bar"})), 82 }, 83 }, 84 } 85 86 for _, tc := range testCases { 87 t.Run(tc.name, func(t *testing.T) { 88 kc := kube.NewFakeClient() 89 fake := kc.Kube().(*fake.Clientset) 90 configmaps := kclient.New[*v1.ConfigMap](kc) 91 if tc.existingConfigMap != nil { 92 if _, err := configmaps.Create(tc.existingConfigMap); err != nil { 93 t.Errorf("failed to create configmap %v", err) 94 } 95 } 96 fake.ClearActions() 97 err := updateDataInConfigMap(configmaps, tc.existingConfigMap, []byte(caBundle)) 98 if err != nil && err.Error() != tc.expectedErr { 99 t.Errorf("actual error (%s) different from expected error (%s).", err.Error(), tc.expectedErr) 100 } 101 if err == nil { 102 if tc.expectedErr != "" { 103 t.Errorf("expecting error %s but got no error", tc.expectedErr) 104 } else if err := checkActions(fake.Actions(), tc.expectedActions); err != nil { 105 t.Error(err) 106 } 107 } 108 }) 109 } 110 } 111 112 func TestInsertDataToConfigMap(t *testing.T) { 113 gvr := schema.GroupVersionResource{ 114 Resource: "configmaps", 115 Version: "v1", 116 } 117 caBundle := []byte("test-data") 118 testData := map[string]string{ 119 constants.CACertNamespaceConfigMapDataName: "test-data", 120 } 121 testCases := []struct { 122 name string 123 meta metav1.ObjectMeta 124 existingConfigMap *v1.ConfigMap 125 caBundle []byte 126 expectedActions []ktesting.Action 127 expectedErr string 128 clientMod func(*fake.Clientset) 129 }{ 130 { 131 name: "non-existing ConfigMap", 132 existingConfigMap: nil, 133 caBundle: caBundle, 134 meta: metav1.ObjectMeta{Namespace: namespaceName, Name: configMapName}, 135 expectedActions: []ktesting.Action{ 136 ktesting.NewCreateAction(gvr, namespaceName, createConfigMap(namespaceName, 137 configMapName, testData)), 138 }, 139 expectedErr: "", 140 }, 141 { 142 name: "existing ConfigMap", 143 meta: metav1.ObjectMeta{Namespace: namespaceName, Name: configMapName}, 144 existingConfigMap: createConfigMap(namespaceName, configMapName, map[string]string{}), 145 caBundle: caBundle, 146 expectedActions: []ktesting.Action{ 147 ktesting.NewUpdateAction(gvr, namespaceName, createConfigMap(namespaceName, configMapName, testData)), 148 }, 149 expectedErr: "", 150 }, 151 { 152 name: "creation failure for ConfigMap", 153 existingConfigMap: nil, 154 caBundle: caBundle, 155 meta: metav1.ObjectMeta{Namespace: namespaceName, Name: configMapName}, 156 expectedActions: []ktesting.Action{ 157 ktesting.NewGetAction(gvr, namespaceName, configMapName), 158 ktesting.NewGetAction(gvr, namespaceName, configMapName), 159 ktesting.NewCreateAction(gvr, namespaceName, createConfigMap(namespaceName, configMapName, 160 map[string]string{dataName: "test-data"})), 161 }, 162 expectedErr: fmt.Sprintf("error when creating configmap %v: no permission to create configmap", 163 configMapName), 164 clientMod: createConfigMapDisabledClient, 165 }, 166 { 167 name: "creation: concurrently created by other client", 168 existingConfigMap: nil, 169 caBundle: caBundle, 170 meta: metav1.ObjectMeta{Namespace: namespaceName, Name: configMapName}, 171 expectedActions: []ktesting.Action{ 172 ktesting.NewCreateAction(gvr, namespaceName, createConfigMap(namespaceName, configMapName, 173 map[string]string{dataName: "test-data"})), 174 }, 175 expectedErr: "", 176 clientMod: createConfigMapAlreadyExistClient, 177 }, 178 { 179 name: "creation: namespace is deleting", 180 existingConfigMap: nil, 181 caBundle: caBundle, 182 meta: metav1.ObjectMeta{Namespace: namespaceName, Name: configMapName}, 183 expectedActions: []ktesting.Action{ 184 ktesting.NewCreateAction(gvr, namespaceName, createConfigMap(namespaceName, configMapName, 185 map[string]string{dataName: "test-data"})), 186 }, 187 expectedErr: "", 188 clientMod: createConfigMapNamespaceDeletingClient, 189 }, 190 { 191 name: "creation: namespace is forbidden", 192 existingConfigMap: nil, 193 caBundle: caBundle, 194 meta: metav1.ObjectMeta{Namespace: constants.KubeSystemNamespace, Name: configMapName}, 195 expectedActions: []ktesting.Action{ 196 ktesting.NewCreateAction(gvr, constants.KubeSystemNamespace, createConfigMap(constants.KubeSystemNamespace, configMapName, testData)), 197 }, 198 expectedErr: "", 199 clientMod: createConfigMapNamespaceForbidden, 200 }, 201 } 202 203 for _, tc := range testCases { 204 t.Run(tc.name, func(t *testing.T) { 205 var objs []runtime.Object 206 if tc.existingConfigMap != nil { 207 objs = []runtime.Object{tc.existingConfigMap} 208 } 209 kc := kube.NewFakeClient(objs...) 210 fake := kc.Kube().(*fake.Clientset) 211 configmaps := kclient.New[*v1.ConfigMap](kc) 212 if tc.clientMod != nil { 213 tc.clientMod(fake) 214 } 215 kc.RunAndWait(test.NewStop(t)) 216 fake.ClearActions() 217 err := InsertDataToConfigMap(configmaps, tc.meta, tc.caBundle) 218 if err != nil && err.Error() != tc.expectedErr { 219 t.Errorf("actual error (%s) different from expected error (%s).", err.Error(), tc.expectedErr) 220 } 221 if err == nil { 222 if tc.expectedErr != "" { 223 t.Errorf("expecting error %s but got no error; actions: %+v", tc.expectedErr, fake.Actions()) 224 } else if err := checkActions(fake.Actions(), tc.expectedActions); err != nil { 225 t.Error(err) 226 } 227 } 228 }) 229 } 230 } 231 232 func createConfigMapDisabledClient(client *fake.Clientset) { 233 client.PrependReactor("get", "configmaps", func(action ktesting.Action) (bool, runtime.Object, error) { 234 return true, &v1.ConfigMap{}, errors.NewNotFound(v1.Resource("configmaps"), configMapName) 235 }) 236 client.PrependReactor("create", "configmaps", func(action ktesting.Action) (bool, runtime.Object, error) { 237 return true, &v1.ConfigMap{}, errors.NewUnauthorized("no permission to create configmap") 238 }) 239 } 240 241 func createConfigMapAlreadyExistClient(client *fake.Clientset) { 242 client.PrependReactor("get", "configmaps", func(action ktesting.Action) (bool, runtime.Object, error) { 243 return true, &v1.ConfigMap{}, errors.NewNotFound(v1.Resource("configmaps"), configMapName) 244 }) 245 client.PrependReactor("create", "configmaps", func(action ktesting.Action) (bool, runtime.Object, error) { 246 return true, &v1.ConfigMap{}, errors.NewAlreadyExists(v1.Resource("configmaps"), configMapName) 247 }) 248 } 249 250 func createConfigMapNamespaceDeletingClient(client *fake.Clientset) { 251 client.PrependReactor("get", "configmaps", func(action ktesting.Action) (bool, runtime.Object, error) { 252 return true, &v1.ConfigMap{}, errors.NewNotFound(v1.Resource("configmaps"), configMapName) 253 }) 254 255 err := errors.NewForbidden(v1.Resource("configmaps"), configMapName, 256 fmt.Errorf("unable to create new content in namespace %s because it is being terminated", namespaceName)) 257 err.ErrStatus.Details.Causes = append(err.ErrStatus.Details.Causes, metav1.StatusCause{ 258 Type: v1.NamespaceTerminatingCause, 259 Message: fmt.Sprintf("namespace %s is being terminated", namespaceName), 260 Field: "metadata.namespace", 261 }) 262 client.PrependReactor("create", "configmaps", func(action ktesting.Action) (bool, runtime.Object, error) { 263 return true, &v1.ConfigMap{}, err 264 }) 265 } 266 267 func createConfigMapNamespaceForbidden(client *fake.Clientset) { 268 client.PrependReactor("get", "configmaps", func(action ktesting.Action) (bool, runtime.Object, error) { 269 return true, &v1.ConfigMap{}, errors.NewNotFound(v1.Resource("configmaps"), configMapName) 270 }) 271 client.PrependReactor("create", "configmaps", func(action ktesting.Action) (bool, runtime.Object, error) { 272 return true, &v1.ConfigMap{}, errors.NewForbidden(v1.Resource("configmaps"), configMapName, fmt.Errorf( 273 "User \"system:serviceaccount:istio-system:istiod\" cannot create resource \"configmaps\" in API group \"\" in the namespace \"kube-system\"")) 274 }) 275 } 276 277 // nolint: unparam 278 func createConfigMap(namespace, configName string, data map[string]string) *v1.ConfigMap { 279 return &v1.ConfigMap{ 280 ObjectMeta: metav1.ObjectMeta{ 281 Name: configName, 282 Namespace: namespace, 283 }, 284 Data: data, 285 } 286 } 287 288 func checkActions(actual, expected []ktesting.Action) error { 289 if len(actual) != len(expected) { 290 return fmt.Errorf("unexpected number of actions, want %d but got %d, %v", len(expected), len(actual), actual) 291 } 292 293 for i, action := range actual { 294 expectedAction := expected[i] 295 verb := expectedAction.GetVerb() 296 resource := expectedAction.GetResource().Resource 297 if !action.Matches(verb, resource) { 298 return fmt.Errorf("unexpected %dth action, want \n%+v but got \n%+v\n%v", i, expectedAction, action, cmp.Diff(expectedAction, action)) 299 } 300 } 301 302 return nil 303 } 304 305 func Test_insertData(t *testing.T) { 306 type args struct { 307 cm *v1.ConfigMap 308 data map[string]string 309 } 310 tests := []struct { 311 name string 312 args args 313 want bool 314 expectedCM *v1.ConfigMap 315 }{ 316 { 317 name: "unchanged", 318 args: args{ 319 cm: createConfigMap(namespaceName, configMapName, map[string]string{"foo": "bar"}), 320 data: nil, 321 }, 322 want: false, 323 expectedCM: createConfigMap(namespaceName, configMapName, map[string]string{"foo": "bar"}), 324 }, 325 { 326 name: "unchanged", 327 args: args{ 328 cm: createConfigMap(namespaceName, configMapName, map[string]string{"foo": "bar"}), 329 data: map[string]string{"foo": "bar"}, 330 }, 331 want: false, 332 expectedCM: createConfigMap(namespaceName, configMapName, map[string]string{"foo": "bar"}), 333 }, 334 { 335 name: "changed", 336 args: args{ 337 cm: createConfigMap(namespaceName, configMapName, map[string]string{"foo": "bar"}), 338 data: map[string]string{"bar": "foo"}, 339 }, 340 want: true, 341 expectedCM: createConfigMap(namespaceName, configMapName, map[string]string{"foo": "bar", "bar": "foo"}), 342 }, 343 { 344 name: "changed", 345 args: args{ 346 cm: createConfigMap(namespaceName, configMapName, map[string]string{"foo": "bar"}), 347 data: map[string]string{"foo": "foo"}, 348 }, 349 want: true, 350 expectedCM: createConfigMap(namespaceName, configMapName, map[string]string{"foo": "foo"}), 351 }, 352 { 353 name: "changed", 354 args: args{ 355 cm: createConfigMap(namespaceName, configMapName, nil), 356 data: map[string]string{"bar": "foo"}, 357 }, 358 want: true, 359 expectedCM: createConfigMap(namespaceName, configMapName, map[string]string{"bar": "foo"}), 360 }, 361 { 362 name: "changed", 363 args: args{ 364 cm: createConfigMap(namespaceName, configMapName, nil), 365 data: nil, 366 }, 367 want: true, 368 expectedCM: createConfigMap(namespaceName, configMapName, nil), 369 }, 370 } 371 for _, tt := range tests { 372 t.Run(tt.name, func(t *testing.T) { 373 if got := insertData(tt.args.cm, tt.args.data); got != tt.want { 374 t.Errorf("insertData() = %v, want %v", got, tt.want) 375 } 376 if !reflect.DeepEqual(tt.args.cm.Data, tt.expectedCM.Data) { 377 t.Errorf("configmap data: %v, want %v", tt.args.cm.Data, tt.expectedCM) 378 } 379 }) 380 } 381 }