...

Source file src/k8s.io/kubernetes/plugin/pkg/admission/storage/persistentvolume/label/admission_test.go

Documentation: k8s.io/kubernetes/plugin/pkg/admission/storage/persistentvolume/label

     1  /*
     2  Copyright 2015 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 label
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"reflect"
    23  	"sort"
    24  	"testing"
    25  
    26  	v1 "k8s.io/api/core/v1"
    27  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/runtime/schema"
    30  	"k8s.io/apiserver/pkg/admission"
    31  	admissiontesting "k8s.io/apiserver/pkg/admission/testing"
    32  	cloudprovider "k8s.io/cloud-provider"
    33  	persistentvolume "k8s.io/component-helpers/storage/volume"
    34  	api "k8s.io/kubernetes/pkg/apis/core"
    35  )
    36  
    37  type mockVolumes struct {
    38  	volumeLabels      map[string]string
    39  	volumeLabelsError error
    40  }
    41  
    42  var _ cloudprovider.PVLabeler = &mockVolumes{}
    43  
    44  func (v *mockVolumes) GetLabelsForVolume(ctx context.Context, pv *v1.PersistentVolume) (map[string]string, error) {
    45  	return v.volumeLabels, v.volumeLabelsError
    46  }
    47  
    48  func mockVolumeFailure(err error) *mockVolumes {
    49  	return &mockVolumes{volumeLabelsError: err}
    50  }
    51  
    52  func mockVolumeLabels(labels map[string]string) *mockVolumes {
    53  	return &mockVolumes{volumeLabels: labels}
    54  }
    55  
    56  func Test_PVLAdmission(t *testing.T) {
    57  	testcases := []struct {
    58  		name            string
    59  		handler         *persistentVolumeLabel
    60  		pvlabeler       cloudprovider.PVLabeler
    61  		preAdmissionPV  *api.PersistentVolume
    62  		postAdmissionPV *api.PersistentVolume
    63  		err             error
    64  	}{
    65  		{
    66  			name:    "non-cloud PV ignored",
    67  			handler: newPersistentVolumeLabel(),
    68  			pvlabeler: mockVolumeLabels(map[string]string{
    69  				"a":                  "1",
    70  				"b":                  "2",
    71  				v1.LabelTopologyZone: "1__2__3",
    72  			}),
    73  			preAdmissionPV: &api.PersistentVolume{
    74  				ObjectMeta: metav1.ObjectMeta{Name: "noncloud", Namespace: "myns"},
    75  				Spec: api.PersistentVolumeSpec{
    76  					PersistentVolumeSource: api.PersistentVolumeSource{
    77  						HostPath: &api.HostPathVolumeSource{
    78  							Path: "/",
    79  						},
    80  					},
    81  				},
    82  			},
    83  			postAdmissionPV: &api.PersistentVolume{
    84  				ObjectMeta: metav1.ObjectMeta{Name: "noncloud", Namespace: "myns"},
    85  				Spec: api.PersistentVolumeSpec{
    86  					PersistentVolumeSource: api.PersistentVolumeSource{
    87  						HostPath: &api.HostPathVolumeSource{
    88  							Path: "/",
    89  						},
    90  					},
    91  				},
    92  			},
    93  			err: nil,
    94  		},
    95  		{
    96  			name:      "cloud provider error blocks creation of volume",
    97  			handler:   newPersistentVolumeLabel(),
    98  			pvlabeler: mockVolumeFailure(errors.New("invalid volume")),
    99  			preAdmissionPV: &api.PersistentVolume{
   100  				ObjectMeta: metav1.ObjectMeta{Name: "gcepd", Namespace: "myns"},
   101  				Spec: api.PersistentVolumeSpec{
   102  					PersistentVolumeSource: api.PersistentVolumeSource{
   103  						GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{
   104  							PDName: "123",
   105  						},
   106  					},
   107  				},
   108  			},
   109  			postAdmissionPV: &api.PersistentVolume{
   110  				ObjectMeta: metav1.ObjectMeta{Name: "gcepd", Namespace: "myns"},
   111  				Spec: api.PersistentVolumeSpec{
   112  					PersistentVolumeSource: api.PersistentVolumeSource{
   113  						GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{
   114  							PDName: "123",
   115  						},
   116  					},
   117  				},
   118  			},
   119  			err: apierrors.NewForbidden(schema.ParseGroupResource("persistentvolumes"), "gcepd", errors.New("error querying GCE PD volume 123: invalid volume")),
   120  		},
   121  		{
   122  			name:      "cloud provider returns no labels",
   123  			handler:   newPersistentVolumeLabel(),
   124  			pvlabeler: mockVolumeLabels(map[string]string{}),
   125  			preAdmissionPV: &api.PersistentVolume{
   126  				ObjectMeta: metav1.ObjectMeta{Name: "awsebs", Namespace: "myns"},
   127  				Spec: api.PersistentVolumeSpec{
   128  					PersistentVolumeSource: api.PersistentVolumeSource{
   129  						AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
   130  							VolumeID: "123",
   131  						},
   132  					},
   133  				},
   134  			},
   135  			postAdmissionPV: &api.PersistentVolume{
   136  				ObjectMeta: metav1.ObjectMeta{Name: "awsebs", Namespace: "myns"},
   137  				Spec: api.PersistentVolumeSpec{
   138  					PersistentVolumeSource: api.PersistentVolumeSource{
   139  						AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
   140  							VolumeID: "123",
   141  						},
   142  					},
   143  				},
   144  			},
   145  			err: nil,
   146  		},
   147  		{
   148  			name:      "cloud provider returns nil, nil",
   149  			handler:   newPersistentVolumeLabel(),
   150  			pvlabeler: mockVolumeFailure(nil),
   151  			preAdmissionPV: &api.PersistentVolume{
   152  				ObjectMeta: metav1.ObjectMeta{Name: "awsebs", Namespace: "myns"},
   153  				Spec: api.PersistentVolumeSpec{
   154  					PersistentVolumeSource: api.PersistentVolumeSource{
   155  						AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
   156  							VolumeID: "123",
   157  						},
   158  					},
   159  				},
   160  			},
   161  			postAdmissionPV: &api.PersistentVolume{
   162  				ObjectMeta: metav1.ObjectMeta{Name: "awsebs", Namespace: "myns"},
   163  				Spec: api.PersistentVolumeSpec{
   164  					PersistentVolumeSource: api.PersistentVolumeSource{
   165  						AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
   166  							VolumeID: "123",
   167  						},
   168  					},
   169  				},
   170  			},
   171  			err: nil,
   172  		},
   173  		{
   174  			name:    "existing Beta labels from dynamic provisioning are not changed",
   175  			handler: newPersistentVolumeLabel(),
   176  			pvlabeler: mockVolumeLabels(map[string]string{
   177  				v1.LabelFailureDomainBetaZone:   "domain1",
   178  				v1.LabelFailureDomainBetaRegion: "region1",
   179  			}),
   180  			preAdmissionPV: &api.PersistentVolume{
   181  				ObjectMeta: metav1.ObjectMeta{
   182  					Name: "awsebs", Namespace: "myns",
   183  					Labels: map[string]string{
   184  						v1.LabelFailureDomainBetaZone:   "existingDomain",
   185  						v1.LabelFailureDomainBetaRegion: "existingRegion",
   186  					},
   187  					Annotations: map[string]string{
   188  						persistentvolume.AnnDynamicallyProvisioned: "kubernetes.io/aws-ebs",
   189  					},
   190  				},
   191  				Spec: api.PersistentVolumeSpec{
   192  					PersistentVolumeSource: api.PersistentVolumeSource{
   193  						AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
   194  							VolumeID: "123",
   195  						},
   196  					},
   197  				},
   198  			},
   199  			postAdmissionPV: &api.PersistentVolume{
   200  				ObjectMeta: metav1.ObjectMeta{
   201  					Name:      "awsebs",
   202  					Namespace: "myns",
   203  					Labels: map[string]string{
   204  						v1.LabelFailureDomainBetaZone:   "existingDomain",
   205  						v1.LabelFailureDomainBetaRegion: "existingRegion",
   206  					},
   207  					Annotations: map[string]string{
   208  						persistentvolume.AnnDynamicallyProvisioned: "kubernetes.io/aws-ebs",
   209  					},
   210  				},
   211  				Spec: api.PersistentVolumeSpec{
   212  					PersistentVolumeSource: api.PersistentVolumeSource{
   213  						AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
   214  							VolumeID: "123",
   215  						},
   216  					},
   217  					NodeAffinity: &api.VolumeNodeAffinity{
   218  						Required: &api.NodeSelector{
   219  							NodeSelectorTerms: []api.NodeSelectorTerm{
   220  								{
   221  									MatchExpressions: []api.NodeSelectorRequirement{
   222  										{
   223  											Key:      v1.LabelFailureDomainBetaRegion,
   224  											Operator: api.NodeSelectorOpIn,
   225  											Values:   []string{"existingRegion"},
   226  										},
   227  										{
   228  											Key:      v1.LabelFailureDomainBetaZone,
   229  											Operator: api.NodeSelectorOpIn,
   230  											Values:   []string{"existingDomain"},
   231  										},
   232  									},
   233  								},
   234  							},
   235  						},
   236  					},
   237  				},
   238  			},
   239  			err: nil,
   240  		},
   241  		{
   242  			name:    "existing GA labels from dynamic provisioning are not changed",
   243  			handler: newPersistentVolumeLabel(),
   244  			pvlabeler: mockVolumeLabels(map[string]string{
   245  				v1.LabelTopologyZone:   "domain1",
   246  				v1.LabelTopologyRegion: "region1",
   247  			}),
   248  			preAdmissionPV: &api.PersistentVolume{
   249  				ObjectMeta: metav1.ObjectMeta{
   250  					Name: "awsebs", Namespace: "myns",
   251  					Labels: map[string]string{
   252  						v1.LabelTopologyZone:   "existingDomain",
   253  						v1.LabelTopologyRegion: "existingRegion",
   254  					},
   255  					Annotations: map[string]string{
   256  						persistentvolume.AnnDynamicallyProvisioned: "kubernetes.io/aws-ebs",
   257  					},
   258  				},
   259  				Spec: api.PersistentVolumeSpec{
   260  					PersistentVolumeSource: api.PersistentVolumeSource{
   261  						AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
   262  							VolumeID: "123",
   263  						},
   264  					},
   265  				},
   266  			},
   267  			postAdmissionPV: &api.PersistentVolume{
   268  				ObjectMeta: metav1.ObjectMeta{
   269  					Name:      "awsebs",
   270  					Namespace: "myns",
   271  					Labels: map[string]string{
   272  						v1.LabelTopologyZone:   "existingDomain",
   273  						v1.LabelTopologyRegion: "existingRegion",
   274  					},
   275  					Annotations: map[string]string{
   276  						persistentvolume.AnnDynamicallyProvisioned: "kubernetes.io/aws-ebs",
   277  					},
   278  				},
   279  				Spec: api.PersistentVolumeSpec{
   280  					PersistentVolumeSource: api.PersistentVolumeSource{
   281  						AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
   282  							VolumeID: "123",
   283  						},
   284  					},
   285  					NodeAffinity: &api.VolumeNodeAffinity{
   286  						Required: &api.NodeSelector{
   287  							NodeSelectorTerms: []api.NodeSelectorTerm{
   288  								{
   289  									MatchExpressions: []api.NodeSelectorRequirement{
   290  										{
   291  											Key:      v1.LabelTopologyRegion,
   292  											Operator: api.NodeSelectorOpIn,
   293  											Values:   []string{"existingRegion"},
   294  										},
   295  										{
   296  											Key:      v1.LabelTopologyZone,
   297  											Operator: api.NodeSelectorOpIn,
   298  											Values:   []string{"existingDomain"},
   299  										},
   300  									},
   301  								},
   302  							},
   303  						},
   304  					},
   305  				},
   306  			},
   307  			err: nil,
   308  		},
   309  		{
   310  			name:    "existing labels from user are changed",
   311  			handler: newPersistentVolumeLabel(),
   312  			pvlabeler: mockVolumeLabels(map[string]string{
   313  				v1.LabelTopologyZone:   "domain1",
   314  				v1.LabelTopologyRegion: "region1",
   315  			}),
   316  			preAdmissionPV: &api.PersistentVolume{
   317  				ObjectMeta: metav1.ObjectMeta{
   318  					Name: "gcePV", Namespace: "myns",
   319  					Labels: map[string]string{
   320  						v1.LabelTopologyZone:   "existingDomain",
   321  						v1.LabelTopologyRegion: "existingRegion",
   322  					},
   323  				},
   324  				Spec: api.PersistentVolumeSpec{
   325  					PersistentVolumeSource: api.PersistentVolumeSource{
   326  						GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{
   327  							PDName: "123",
   328  						},
   329  					},
   330  				},
   331  			},
   332  			postAdmissionPV: &api.PersistentVolume{
   333  				ObjectMeta: metav1.ObjectMeta{
   334  					Name:      "gcePV",
   335  					Namespace: "myns",
   336  					Labels: map[string]string{
   337  						v1.LabelTopologyZone:   "domain1",
   338  						v1.LabelTopologyRegion: "region1",
   339  					},
   340  				},
   341  				Spec: api.PersistentVolumeSpec{
   342  					PersistentVolumeSource: api.PersistentVolumeSource{
   343  						GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{
   344  							PDName: "123",
   345  						},
   346  					},
   347  					NodeAffinity: &api.VolumeNodeAffinity{
   348  						Required: &api.NodeSelector{
   349  							NodeSelectorTerms: []api.NodeSelectorTerm{
   350  								{
   351  									MatchExpressions: []api.NodeSelectorRequirement{
   352  										{
   353  											Key:      v1.LabelTopologyRegion,
   354  											Operator: api.NodeSelectorOpIn,
   355  											Values:   []string{"region1"},
   356  										},
   357  										{
   358  											Key:      v1.LabelTopologyZone,
   359  											Operator: api.NodeSelectorOpIn,
   360  											Values:   []string{"domain1"},
   361  										},
   362  									},
   363  								},
   364  							},
   365  						},
   366  					},
   367  				},
   368  			},
   369  			err: nil,
   370  		},
   371  		{
   372  			name:    "GCE PD PV labeled correctly",
   373  			handler: newPersistentVolumeLabel(),
   374  			pvlabeler: mockVolumeLabels(map[string]string{
   375  				"a":                  "1",
   376  				"b":                  "2",
   377  				v1.LabelTopologyZone: "1__2__3",
   378  			}),
   379  			preAdmissionPV: &api.PersistentVolume{
   380  				ObjectMeta: metav1.ObjectMeta{Name: "gcepd", Namespace: "myns"},
   381  				Spec: api.PersistentVolumeSpec{
   382  					PersistentVolumeSource: api.PersistentVolumeSource{
   383  						GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{
   384  							PDName: "123",
   385  						},
   386  					},
   387  				},
   388  			},
   389  			postAdmissionPV: &api.PersistentVolume{
   390  				ObjectMeta: metav1.ObjectMeta{
   391  					Name:      "gcepd",
   392  					Namespace: "myns",
   393  					Labels: map[string]string{
   394  						"a":                  "1",
   395  						"b":                  "2",
   396  						v1.LabelTopologyZone: "1__2__3",
   397  					},
   398  				},
   399  				Spec: api.PersistentVolumeSpec{
   400  					PersistentVolumeSource: api.PersistentVolumeSource{
   401  						GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{
   402  							PDName: "123",
   403  						},
   404  					},
   405  					NodeAffinity: &api.VolumeNodeAffinity{
   406  						Required: &api.NodeSelector{
   407  							NodeSelectorTerms: []api.NodeSelectorTerm{
   408  								{
   409  									MatchExpressions: []api.NodeSelectorRequirement{
   410  										{
   411  											Key:      "a",
   412  											Operator: api.NodeSelectorOpIn,
   413  											Values:   []string{"1"},
   414  										},
   415  										{
   416  											Key:      "b",
   417  											Operator: api.NodeSelectorOpIn,
   418  											Values:   []string{"2"},
   419  										},
   420  										{
   421  											Key:      v1.LabelTopologyZone,
   422  											Operator: api.NodeSelectorOpIn,
   423  											Values:   []string{"1", "2", "3"},
   424  										},
   425  									},
   426  								},
   427  							},
   428  						},
   429  					},
   430  				},
   431  			},
   432  			err: nil,
   433  		},
   434  	}
   435  
   436  	for _, testcase := range testcases {
   437  		t.Run(testcase.name, func(t *testing.T) {
   438  			setPVLabeler(testcase.handler, testcase.pvlabeler)
   439  			handler := admissiontesting.WithReinvocationTesting(t, admission.NewChainHandler(testcase.handler))
   440  
   441  			err := handler.Admit(context.TODO(), admission.NewAttributesRecord(testcase.preAdmissionPV, nil, api.Kind("PersistentVolume").WithVersion("version"), testcase.preAdmissionPV.Namespace, testcase.preAdmissionPV.Name, api.Resource("persistentvolumes").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
   442  			if !reflect.DeepEqual(err, testcase.err) {
   443  				t.Logf("expected error: %q", testcase.err)
   444  				t.Logf("actual error: %q", err)
   445  				t.Error("unexpected error when admitting PV")
   446  			}
   447  
   448  			// sort node selector match expression by key because they are added out of order in the admission controller
   449  			sortMatchExpressions(testcase.preAdmissionPV)
   450  			if !reflect.DeepEqual(testcase.preAdmissionPV, testcase.postAdmissionPV) {
   451  				t.Logf("expected PV: %+v", testcase.postAdmissionPV)
   452  				t.Logf("actual PV: %+v", testcase.preAdmissionPV)
   453  				t.Error("unexpected PV")
   454  			}
   455  
   456  		})
   457  	}
   458  }
   459  
   460  // setPVLabler applies the given mock pvlabeler to implement PV labeling for all cloud providers.
   461  // Given we mock out the values of the labels anyways, assigning the same mock labeler for every
   462  // provider does not reduce test coverage but it does simplify/clean up the tests here because
   463  // the provider is then decided based on the type of PV (EBS, GCEPD, Azure Disk, etc)
   464  func setPVLabeler(handler *persistentVolumeLabel, pvlabeler cloudprovider.PVLabeler) {
   465  	handler.gcePVLabeler = pvlabeler
   466  }
   467  
   468  // sortMatchExpressions sorts a PV's node selector match expressions by key name if it is not nil
   469  func sortMatchExpressions(pv *api.PersistentVolume) {
   470  	if pv.Spec.NodeAffinity == nil ||
   471  		pv.Spec.NodeAffinity.Required == nil ||
   472  		pv.Spec.NodeAffinity.Required.NodeSelectorTerms == nil {
   473  		return
   474  	}
   475  
   476  	match := pv.Spec.NodeAffinity.Required.NodeSelectorTerms[0].MatchExpressions
   477  	sort.Slice(match, func(i, j int) bool {
   478  		return match[i].Key < match[j].Key
   479  	})
   480  
   481  	pv.Spec.NodeAffinity.Required.NodeSelectorTerms[0].MatchExpressions = match
   482  }
   483  

View as plain text