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  }