k8s.io/kubernetes@v1.29.3/pkg/volume/downwardapi/downwardapi_test.go (about)

     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 downwardapi
    18  
    19  import (
    20  	"fmt"
    21  	"io/ioutil"
    22  	"os"
    23  	"path/filepath"
    24  	"runtime"
    25  	"testing"
    26  
    27  	"k8s.io/api/core/v1"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/types"
    30  	clientset "k8s.io/client-go/kubernetes"
    31  	"k8s.io/client-go/kubernetes/fake"
    32  	utiltesting "k8s.io/client-go/util/testing"
    33  	"k8s.io/kubernetes/pkg/fieldpath"
    34  	"k8s.io/kubernetes/pkg/volume"
    35  	"k8s.io/kubernetes/pkg/volume/emptydir"
    36  	volumetest "k8s.io/kubernetes/pkg/volume/testing"
    37  )
    38  
    39  const (
    40  	downwardAPIDir = "..data"
    41  	testPodUID     = types.UID("test_pod_uid")
    42  	testNamespace  = "test_metadata_namespace"
    43  	testName       = "test_metadata_name"
    44  )
    45  
    46  func newTestHost(t *testing.T, clientset clientset.Interface) (string, volume.VolumeHost) {
    47  	tempDir, err := utiltesting.MkTmpdir("downwardApi_volume_test.")
    48  	if err != nil {
    49  		t.Fatalf("can't make a temp rootdir: %v", err)
    50  	}
    51  	return tempDir, volumetest.NewFakeVolumeHost(t, tempDir, clientset, emptydir.ProbeVolumePlugins())
    52  }
    53  
    54  func TestCanSupport(t *testing.T) {
    55  	pluginMgr := volume.VolumePluginMgr{}
    56  	tmpDir, host := newTestHost(t, nil)
    57  	defer os.RemoveAll(tmpDir)
    58  	pluginMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, host)
    59  
    60  	plugin, err := pluginMgr.FindPluginByName(downwardAPIPluginName)
    61  	if err != nil {
    62  		t.Fatal("Can't find the plugin by name")
    63  	}
    64  	if plugin.GetPluginName() != downwardAPIPluginName {
    65  		t.Errorf("Wrong name: %s", plugin.GetPluginName())
    66  	}
    67  	if !plugin.CanSupport(&volume.Spec{Volume: &v1.Volume{VolumeSource: v1.VolumeSource{DownwardAPI: &v1.DownwardAPIVolumeSource{}}}}) {
    68  		t.Errorf("Expected true")
    69  	}
    70  	if plugin.CanSupport(&volume.Spec{Volume: &v1.Volume{VolumeSource: v1.VolumeSource{}}}) {
    71  		t.Errorf("Expected false")
    72  	}
    73  }
    74  
    75  func TestDownwardAPI(t *testing.T) {
    76  	// Skip tests that fail on Windows, as discussed during the SIG Testing meeting from January 10, 2023
    77  	if runtime.GOOS == "windows" {
    78  		t.Skip("Skipping test that fails on Windows")
    79  	}
    80  
    81  	labels1 := map[string]string{
    82  		"key1": "value1",
    83  		"key2": "value2",
    84  	}
    85  	labels2 := map[string]string{
    86  		"key1": "value1",
    87  		"key2": "value2",
    88  		"key3": "value3",
    89  	}
    90  	annotations := map[string]string{
    91  		"a1":        "value1",
    92  		"a2":        "value2",
    93  		"multiline": "c\nb\na",
    94  	}
    95  	testCases := []struct {
    96  		name           string
    97  		files          map[string]string
    98  		modes          map[string]int32
    99  		podLabels      map[string]string
   100  		podAnnotations map[string]string
   101  		steps          []testStep
   102  	}{
   103  		{
   104  			name:      "test_labels",
   105  			files:     map[string]string{"labels": "metadata.labels"},
   106  			podLabels: labels1,
   107  			steps: []testStep{
   108  				// for steps that involve files, stepName is also
   109  				// used as the name of the file to verify
   110  				verifyMapInFile{stepName{"labels"}, labels1},
   111  			},
   112  		},
   113  		{
   114  			name:           "test_annotations",
   115  			files:          map[string]string{"annotations": "metadata.annotations"},
   116  			podAnnotations: annotations,
   117  			steps: []testStep{
   118  				verifyMapInFile{stepName{"annotations"}, annotations},
   119  			},
   120  		},
   121  		{
   122  			name:  "test_name",
   123  			files: map[string]string{"name_file_name": "metadata.name"},
   124  			steps: []testStep{
   125  				verifyLinesInFile{stepName{"name_file_name"}, testName},
   126  			},
   127  		},
   128  		{
   129  			name:  "test_namespace",
   130  			files: map[string]string{"namespace_file_name": "metadata.namespace"},
   131  			steps: []testStep{
   132  				verifyLinesInFile{stepName{"namespace_file_name"}, testNamespace},
   133  			},
   134  		},
   135  		{
   136  			name:      "test_write_twice_no_update",
   137  			files:     map[string]string{"labels": "metadata.labels"},
   138  			podLabels: labels1,
   139  			steps: []testStep{
   140  				reSetUp{stepName{"resetup"}, false, nil},
   141  				verifyMapInFile{stepName{"labels"}, labels1},
   142  			},
   143  		},
   144  		{
   145  			name:      "test_write_twice_with_update",
   146  			files:     map[string]string{"labels": "metadata.labels"},
   147  			podLabels: labels1,
   148  			steps: []testStep{
   149  				reSetUp{stepName{"resetup"}, true, labels2},
   150  				verifyMapInFile{stepName{"labels"}, labels2},
   151  			},
   152  		},
   153  		{
   154  			name: "test_write_with_unix_path",
   155  			files: map[string]string{
   156  				"these/are/my/labels":        "metadata.labels",
   157  				"these/are/your/annotations": "metadata.annotations",
   158  			},
   159  			podLabels:      labels1,
   160  			podAnnotations: annotations,
   161  			steps: []testStep{
   162  				verifyMapInFile{stepName{"these/are/my/labels"}, labels1},
   163  				verifyMapInFile{stepName{"these/are/your/annotations"}, annotations},
   164  			},
   165  		},
   166  		{
   167  			name:      "test_write_with_two_consecutive_slashes_in_the_path",
   168  			files:     map[string]string{"this//labels": "metadata.labels"},
   169  			podLabels: labels1,
   170  			steps: []testStep{
   171  				verifyMapInFile{stepName{"this/labels"}, labels1},
   172  			},
   173  		},
   174  		{
   175  			name:  "test_default_mode",
   176  			files: map[string]string{"name_file_name": "metadata.name"},
   177  			steps: []testStep{
   178  				verifyMode{stepName{"name_file_name"}, 0644},
   179  			},
   180  		},
   181  		{
   182  			name:  "test_item_mode",
   183  			files: map[string]string{"name_file_name": "metadata.name"},
   184  			modes: map[string]int32{"name_file_name": 0400},
   185  			steps: []testStep{
   186  				verifyMode{stepName{"name_file_name"}, 0400},
   187  			},
   188  		},
   189  	}
   190  	for _, testCase := range testCases {
   191  		test := newDownwardAPITest(t, testCase.name, testCase.files, testCase.podLabels, testCase.podAnnotations, testCase.modes)
   192  		for _, step := range testCase.steps {
   193  			test.t.Logf("Test case: %q Step: %q", testCase.name, step.getName())
   194  			step.run(test)
   195  		}
   196  		test.tearDown()
   197  	}
   198  }
   199  
   200  type downwardAPITest struct {
   201  	t          *testing.T
   202  	name       string
   203  	plugin     volume.VolumePlugin
   204  	pod        *v1.Pod
   205  	mounter    volume.Mounter
   206  	volumePath string
   207  	rootDir    string
   208  }
   209  
   210  func newDownwardAPITest(t *testing.T, name string, volumeFiles, podLabels, podAnnotations map[string]string, modes map[string]int32) *downwardAPITest {
   211  	defaultMode := int32(0644)
   212  	var files []v1.DownwardAPIVolumeFile
   213  	for path, fieldPath := range volumeFiles {
   214  		file := v1.DownwardAPIVolumeFile{
   215  			Path: path,
   216  			FieldRef: &v1.ObjectFieldSelector{
   217  				FieldPath: fieldPath,
   218  			},
   219  		}
   220  		if mode, found := modes[path]; found {
   221  			file.Mode = &mode
   222  		}
   223  		files = append(files, file)
   224  	}
   225  	podMeta := metav1.ObjectMeta{
   226  		Name:        testName,
   227  		Namespace:   testNamespace,
   228  		Labels:      podLabels,
   229  		Annotations: podAnnotations,
   230  	}
   231  	clientset := fake.NewSimpleClientset(&v1.Pod{ObjectMeta: podMeta})
   232  
   233  	pluginMgr := volume.VolumePluginMgr{}
   234  	rootDir, host := newTestHost(t, clientset)
   235  	pluginMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, host)
   236  	plugin, err := pluginMgr.FindPluginByName(downwardAPIPluginName)
   237  	if err != nil {
   238  		t.Fatal("Can't find the plugin by name")
   239  	}
   240  
   241  	volumeSpec := &v1.Volume{
   242  		Name: name,
   243  		VolumeSource: v1.VolumeSource{
   244  			DownwardAPI: &v1.DownwardAPIVolumeSource{
   245  				DefaultMode: &defaultMode,
   246  				Items:       files,
   247  			},
   248  		},
   249  	}
   250  	podMeta.UID = testPodUID
   251  	pod := &v1.Pod{ObjectMeta: podMeta}
   252  	mounter, err := plugin.NewMounter(volume.NewSpecFromVolume(volumeSpec), pod, volume.VolumeOptions{})
   253  	if err != nil {
   254  		t.Errorf("Failed to make a new Mounter: %v", err)
   255  	}
   256  	if mounter == nil {
   257  		t.Fatalf("Got a nil Mounter")
   258  	}
   259  
   260  	volumePath := mounter.GetPath()
   261  
   262  	err = mounter.SetUp(volume.MounterArgs{})
   263  	if err != nil {
   264  		t.Errorf("Failed to setup volume: %v", err)
   265  	}
   266  
   267  	// downwardAPI volume should create its own empty wrapper path
   268  	podWrapperMetadataDir := fmt.Sprintf("%v/pods/%v/plugins/kubernetes.io~empty-dir/wrapped_%v", rootDir, testPodUID, name)
   269  
   270  	if _, err := os.Stat(podWrapperMetadataDir); err != nil {
   271  		if os.IsNotExist(err) {
   272  			t.Errorf("SetUp() failed, empty-dir wrapper path was not created: %s", podWrapperMetadataDir)
   273  		} else {
   274  			t.Errorf("SetUp() failed: %v", err)
   275  		}
   276  	}
   277  
   278  	return &downwardAPITest{
   279  		t:          t,
   280  		plugin:     plugin,
   281  		pod:        pod,
   282  		mounter:    mounter,
   283  		volumePath: volumePath,
   284  		rootDir:    rootDir,
   285  	}
   286  }
   287  
   288  func (test *downwardAPITest) tearDown() {
   289  	unmounter, err := test.plugin.NewUnmounter(test.name, testPodUID)
   290  	if err != nil {
   291  		test.t.Errorf("Failed to make a new Unmounter: %v", err)
   292  	}
   293  	if unmounter == nil {
   294  		test.t.Fatalf("Got a nil Unmounter")
   295  	}
   296  
   297  	if err := unmounter.TearDown(); err != nil {
   298  		test.t.Errorf("Expected success, got: %v", err)
   299  	}
   300  	if _, err := os.Stat(test.volumePath); err == nil {
   301  		test.t.Errorf("TearDown() failed, volume path still exists: %s", test.volumePath)
   302  	} else if !os.IsNotExist(err) {
   303  		test.t.Errorf("TearDown() failed: %v", err)
   304  	}
   305  	os.RemoveAll(test.rootDir)
   306  }
   307  
   308  // testStep represents a named step of downwardAPITest.
   309  // For steps that deal with files, step name also serves
   310  // as the name of the file that's used by the step.
   311  type testStep interface {
   312  	getName() string
   313  	run(*downwardAPITest)
   314  }
   315  
   316  type stepName struct {
   317  	name string
   318  }
   319  
   320  func (step stepName) getName() string { return step.name }
   321  
   322  func doVerifyLinesInFile(t *testing.T, volumePath, filename string, expected string) {
   323  	data, err := ioutil.ReadFile(filepath.Join(volumePath, filename))
   324  	if err != nil {
   325  		t.Errorf(err.Error())
   326  		return
   327  	}
   328  	actualStr := string(data)
   329  	expectedStr := expected
   330  	if actualStr != expectedStr {
   331  		t.Errorf("Found `%s`, expected `%s`", actualStr, expectedStr)
   332  	}
   333  }
   334  
   335  type verifyLinesInFile struct {
   336  	stepName
   337  	expected string
   338  }
   339  
   340  func (step verifyLinesInFile) run(test *downwardAPITest) {
   341  	doVerifyLinesInFile(test.t, test.volumePath, step.name, step.expected)
   342  }
   343  
   344  type verifyMapInFile struct {
   345  	stepName
   346  	expected map[string]string
   347  }
   348  
   349  func (step verifyMapInFile) run(test *downwardAPITest) {
   350  	doVerifyLinesInFile(test.t, test.volumePath, step.name, fieldpath.FormatMap(step.expected))
   351  }
   352  
   353  type verifyMode struct {
   354  	stepName
   355  	expectedMode int32
   356  }
   357  
   358  func (step verifyMode) run(test *downwardAPITest) {
   359  	fileInfo, err := os.Stat(filepath.Join(test.volumePath, step.name))
   360  	if err != nil {
   361  		test.t.Errorf(err.Error())
   362  		return
   363  	}
   364  
   365  	actualMode := fileInfo.Mode()
   366  	expectedMode := os.FileMode(step.expectedMode)
   367  	if actualMode != expectedMode {
   368  		test.t.Errorf("Found mode `%v` expected %v", actualMode, expectedMode)
   369  	}
   370  }
   371  
   372  type reSetUp struct {
   373  	stepName
   374  	linkShouldChange bool
   375  	newLabels        map[string]string
   376  }
   377  
   378  func (step reSetUp) run(test *downwardAPITest) {
   379  	if step.newLabels != nil {
   380  		test.pod.ObjectMeta.Labels = step.newLabels
   381  	}
   382  
   383  	currentTarget, err := os.Readlink(filepath.Join(test.volumePath, downwardAPIDir))
   384  	if err != nil {
   385  		test.t.Errorf("labels file should be a link... %s\n", err.Error())
   386  	}
   387  
   388  	// now re-run Setup
   389  	if err = test.mounter.SetUp(volume.MounterArgs{}); err != nil {
   390  		test.t.Errorf("Failed to re-setup volume: %v", err)
   391  	}
   392  
   393  	// get the link of the link
   394  	currentTarget2, err := os.Readlink(filepath.Join(test.volumePath, downwardAPIDir))
   395  	if err != nil {
   396  		test.t.Errorf(".current should be a link... %s\n", err.Error())
   397  	}
   398  
   399  	switch {
   400  	case step.linkShouldChange && currentTarget2 == currentTarget:
   401  		test.t.Errorf("Got and update between the two Setup... Target link should NOT be the same\n")
   402  	case !step.linkShouldChange && currentTarget2 != currentTarget:
   403  		test.t.Errorf("No update between the two Setup... Target link should be the same %s %s\n",
   404  			currentTarget, currentTarget2)
   405  	}
   406  }