k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/volume/git_repo/git_repo_test.go (about)

     1  /*
     2  Copyright 2014 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 git_repo
    18  
    19  import (
    20  	"fmt"
    21  	"io/ioutil"
    22  	"os"
    23  	"path"
    24  	"path/filepath"
    25  	"reflect"
    26  	"strings"
    27  	"testing"
    28  
    29  	v1 "k8s.io/api/core/v1"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/types"
    32  	"k8s.io/kubernetes/pkg/volume"
    33  	"k8s.io/kubernetes/pkg/volume/emptydir"
    34  	volumetest "k8s.io/kubernetes/pkg/volume/testing"
    35  	"k8s.io/utils/exec"
    36  	fakeexec "k8s.io/utils/exec/testing"
    37  )
    38  
    39  func newTestHost(t *testing.T) (string, volume.VolumeHost) {
    40  	tempDir, err := ioutil.TempDir("", "git_repo_test.")
    41  	if err != nil {
    42  		t.Fatalf("can't make a temp rootdir: %v", err)
    43  	}
    44  	return tempDir, volumetest.NewFakeVolumeHost(t, tempDir, nil, emptydir.ProbeVolumePlugins())
    45  }
    46  
    47  func TestCanSupport(t *testing.T) {
    48  	plugMgr := volume.VolumePluginMgr{}
    49  	tempDir, host := newTestHost(t)
    50  	defer os.RemoveAll(tempDir)
    51  	plugMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, host)
    52  
    53  	plug, err := plugMgr.FindPluginByName("kubernetes.io/git-repo")
    54  	if err != nil {
    55  		t.Fatal("Can't find the plugin by name")
    56  	}
    57  	if plug.GetPluginName() != "kubernetes.io/git-repo" {
    58  		t.Errorf("Wrong name: %s", plug.GetPluginName())
    59  	}
    60  	if !plug.CanSupport(&volume.Spec{Volume: &v1.Volume{VolumeSource: v1.VolumeSource{GitRepo: &v1.GitRepoVolumeSource{}}}}) {
    61  		t.Errorf("Expected true")
    62  	}
    63  }
    64  
    65  // Expected command
    66  type expectedCommand struct {
    67  	// The git command
    68  	cmd []string
    69  	// The dir of git command is executed
    70  	dir string
    71  }
    72  
    73  func TestPlugin(t *testing.T) {
    74  	gitURL := "https://github.com/kubernetes/kubernetes.git"
    75  	revision := "2a30ce65c5ab586b98916d83385c5983edd353a1"
    76  
    77  	scenarios := []struct {
    78  		name              string
    79  		vol               *v1.Volume
    80  		expecteds         []expectedCommand
    81  		isExpectedFailure bool
    82  	}{
    83  		{
    84  			name: "target-dir",
    85  			vol: &v1.Volume{
    86  				Name: "vol1",
    87  				VolumeSource: v1.VolumeSource{
    88  					GitRepo: &v1.GitRepoVolumeSource{
    89  						Repository: gitURL,
    90  						Revision:   revision,
    91  						Directory:  "target_dir",
    92  					},
    93  				},
    94  			},
    95  			expecteds: []expectedCommand{
    96  				{
    97  					cmd: []string{"git", "clone", "--", gitURL, "target_dir"},
    98  					dir: "",
    99  				},
   100  				{
   101  					cmd: []string{"git", "checkout", revision},
   102  					dir: "/target_dir",
   103  				},
   104  				{
   105  					cmd: []string{"git", "reset", "--hard"},
   106  					dir: "/target_dir",
   107  				},
   108  			},
   109  			isExpectedFailure: false,
   110  		},
   111  		{
   112  			name: "target-dir-no-revision",
   113  			vol: &v1.Volume{
   114  				Name: "vol1",
   115  				VolumeSource: v1.VolumeSource{
   116  					GitRepo: &v1.GitRepoVolumeSource{
   117  						Repository: gitURL,
   118  						Directory:  "target_dir",
   119  					},
   120  				},
   121  			},
   122  			expecteds: []expectedCommand{
   123  				{
   124  					cmd: []string{"git", "clone", "--", gitURL, "target_dir"},
   125  					dir: "",
   126  				},
   127  			},
   128  			isExpectedFailure: false,
   129  		},
   130  		{
   131  			name: "only-git-clone",
   132  			vol: &v1.Volume{
   133  				Name: "vol1",
   134  				VolumeSource: v1.VolumeSource{
   135  					GitRepo: &v1.GitRepoVolumeSource{
   136  						Repository: gitURL,
   137  					},
   138  				},
   139  			},
   140  			expecteds: []expectedCommand{
   141  				{
   142  					cmd: []string{"git", "clone", "--", gitURL},
   143  					dir: "",
   144  				},
   145  			},
   146  			isExpectedFailure: false,
   147  		},
   148  		{
   149  			name: "no-target-dir",
   150  			vol: &v1.Volume{
   151  				Name: "vol1",
   152  				VolumeSource: v1.VolumeSource{
   153  					GitRepo: &v1.GitRepoVolumeSource{
   154  						Repository: gitURL,
   155  						Revision:   revision,
   156  						Directory:  "",
   157  					},
   158  				},
   159  			},
   160  			expecteds: []expectedCommand{
   161  				{
   162  					cmd: []string{"git", "clone", "--", gitURL},
   163  					dir: "",
   164  				},
   165  				{
   166  					cmd: []string{"git", "checkout", revision},
   167  					dir: "/kubernetes",
   168  				},
   169  				{
   170  					cmd: []string{"git", "reset", "--hard"},
   171  					dir: "/kubernetes",
   172  				},
   173  			},
   174  			isExpectedFailure: false,
   175  		},
   176  		{
   177  			name: "current-dir",
   178  			vol: &v1.Volume{
   179  				Name: "vol1",
   180  				VolumeSource: v1.VolumeSource{
   181  					GitRepo: &v1.GitRepoVolumeSource{
   182  						Repository: gitURL,
   183  						Revision:   revision,
   184  						Directory:  ".",
   185  					},
   186  				},
   187  			},
   188  			expecteds: []expectedCommand{
   189  				{
   190  					cmd: []string{"git", "clone", "--", gitURL, "."},
   191  					dir: "",
   192  				},
   193  				{
   194  					cmd: []string{"git", "checkout", revision},
   195  					dir: "",
   196  				},
   197  				{
   198  					cmd: []string{"git", "reset", "--hard"},
   199  					dir: "",
   200  				},
   201  			},
   202  			isExpectedFailure: false,
   203  		},
   204  		{
   205  			name: "current-dir-mess",
   206  			vol: &v1.Volume{
   207  				Name: "vol1",
   208  				VolumeSource: v1.VolumeSource{
   209  					GitRepo: &v1.GitRepoVolumeSource{
   210  						Repository: gitURL,
   211  						Revision:   revision,
   212  						Directory:  "./.",
   213  					},
   214  				},
   215  			},
   216  			expecteds: []expectedCommand{
   217  				{
   218  					cmd: []string{"git", "clone", "--", gitURL, "./."},
   219  					dir: "",
   220  				},
   221  				{
   222  					cmd: []string{"git", "checkout", revision},
   223  					dir: "",
   224  				},
   225  				{
   226  					cmd: []string{"git", "reset", "--hard"},
   227  					dir: "",
   228  				},
   229  			},
   230  			isExpectedFailure: false,
   231  		},
   232  		{
   233  			name: "invalid-repository",
   234  			vol: &v1.Volume{
   235  				Name: "vol1",
   236  				VolumeSource: v1.VolumeSource{
   237  					GitRepo: &v1.GitRepoVolumeSource{
   238  						Repository: "--foo",
   239  					},
   240  				},
   241  			},
   242  			isExpectedFailure: true,
   243  		},
   244  		{
   245  			name: "invalid-revision",
   246  			vol: &v1.Volume{
   247  				Name: "vol1",
   248  				VolumeSource: v1.VolumeSource{
   249  					GitRepo: &v1.GitRepoVolumeSource{
   250  						Repository: gitURL,
   251  						Revision:   "--bar",
   252  					},
   253  				},
   254  			},
   255  			isExpectedFailure: true,
   256  		},
   257  		{
   258  			name: "invalid-directory",
   259  			vol: &v1.Volume{
   260  				Name: "vol1",
   261  				VolumeSource: v1.VolumeSource{
   262  					GitRepo: &v1.GitRepoVolumeSource{
   263  						Repository: gitURL,
   264  						Directory:  "-b",
   265  					},
   266  				},
   267  			},
   268  			isExpectedFailure: true,
   269  		},
   270  		{
   271  			name: "invalid-revision-directory-combo",
   272  			vol: &v1.Volume{
   273  				Name: "vol1",
   274  				VolumeSource: v1.VolumeSource{
   275  					GitRepo: &v1.GitRepoVolumeSource{
   276  						Repository: gitURL,
   277  						Revision:   "main",
   278  						Directory:  "foo/bar",
   279  					},
   280  				},
   281  			},
   282  			isExpectedFailure: true,
   283  		},
   284  	}
   285  
   286  	for _, scenario := range scenarios {
   287  		allErrs := doTestPlugin(scenario, t)
   288  		if len(allErrs) == 0 && scenario.isExpectedFailure {
   289  			t.Errorf("Unexpected success for scenario: %s", scenario.name)
   290  		}
   291  		if len(allErrs) > 0 && !scenario.isExpectedFailure {
   292  			t.Errorf("Unexpected failure for scenario: %s - %+v", scenario.name, allErrs)
   293  		}
   294  	}
   295  
   296  }
   297  
   298  func doTestPlugin(scenario struct {
   299  	name              string
   300  	vol               *v1.Volume
   301  	expecteds         []expectedCommand
   302  	isExpectedFailure bool
   303  }, t *testing.T) []error {
   304  	allErrs := []error{}
   305  
   306  	plugMgr := volume.VolumePluginMgr{}
   307  	rootDir, host := newTestHost(t)
   308  	defer os.RemoveAll(rootDir)
   309  	plugMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, host)
   310  
   311  	plug, err := plugMgr.FindPluginByName("kubernetes.io/git-repo")
   312  	if err != nil {
   313  		allErrs = append(allErrs,
   314  			fmt.Errorf("can't find the plugin by name"))
   315  		return allErrs
   316  	}
   317  	pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{UID: types.UID("poduid")}}
   318  	mounter, err := plug.NewMounter(volume.NewSpecFromVolume(scenario.vol), pod, volume.VolumeOptions{})
   319  
   320  	if err != nil {
   321  		allErrs = append(allErrs,
   322  			fmt.Errorf("failed to make a new Mounter: %w", err))
   323  		return allErrs
   324  	}
   325  	if mounter == nil {
   326  		allErrs = append(allErrs,
   327  			fmt.Errorf("got a nil Mounter"))
   328  		return allErrs
   329  	}
   330  
   331  	path := mounter.GetPath()
   332  	suffix := filepath.Join("pods/poduid/volumes/kubernetes.io~git-repo", scenario.vol.Name)
   333  	if !strings.HasSuffix(path, suffix) {
   334  		allErrs = append(allErrs,
   335  			fmt.Errorf("got unexpected path: %s", path))
   336  		return allErrs
   337  	}
   338  
   339  	// Test setUp()
   340  	setUpErrs := doTestSetUp(scenario, mounter)
   341  	allErrs = append(allErrs, setUpErrs...)
   342  
   343  	if _, err := os.Stat(path); err != nil {
   344  		if os.IsNotExist(err) {
   345  			allErrs = append(allErrs,
   346  				fmt.Errorf("SetUp() failed, volume path not created: %s", path))
   347  			return allErrs
   348  		}
   349  		allErrs = append(allErrs,
   350  			fmt.Errorf("SetUp() failed: %v", err))
   351  		return allErrs
   352  
   353  	}
   354  
   355  	// gitRepo volume should create its own empty wrapper path
   356  	podWrapperMetadataDir := fmt.Sprintf("%v/pods/poduid/plugins/kubernetes.io~empty-dir/wrapped_%v", rootDir, scenario.vol.Name)
   357  
   358  	if _, err := os.Stat(podWrapperMetadataDir); err != nil {
   359  		if os.IsNotExist(err) {
   360  			allErrs = append(allErrs,
   361  				fmt.Errorf("SetUp() failed, empty-dir wrapper path is not created: %s", podWrapperMetadataDir))
   362  		} else {
   363  			allErrs = append(allErrs,
   364  				fmt.Errorf("SetUp() failed: %v", err))
   365  		}
   366  	}
   367  
   368  	unmounter, err := plug.NewUnmounter("vol1", types.UID("poduid"))
   369  	if err != nil {
   370  		allErrs = append(allErrs,
   371  			fmt.Errorf("failed to make a new Unmounter: %w", err))
   372  		return allErrs
   373  	}
   374  	if unmounter == nil {
   375  		allErrs = append(allErrs,
   376  			fmt.Errorf("got a nil Unmounter"))
   377  		return allErrs
   378  	}
   379  
   380  	if err := unmounter.TearDown(); err != nil {
   381  		allErrs = append(allErrs,
   382  			fmt.Errorf("expected success, got: %w", err))
   383  		return allErrs
   384  	}
   385  	if _, err := os.Stat(path); err == nil {
   386  		allErrs = append(allErrs,
   387  			fmt.Errorf("TearDown() failed, volume path still exists: %s", path))
   388  	} else if !os.IsNotExist(err) {
   389  		allErrs = append(allErrs,
   390  			fmt.Errorf("TearDown() failed: %w", err))
   391  	}
   392  	return allErrs
   393  }
   394  
   395  func doTestSetUp(scenario struct {
   396  	name              string
   397  	vol               *v1.Volume
   398  	expecteds         []expectedCommand
   399  	isExpectedFailure bool
   400  }, mounter volume.Mounter) []error {
   401  	expecteds := scenario.expecteds
   402  	allErrs := []error{}
   403  
   404  	// Construct combined outputs from expected commands
   405  	var fakeOutputs []fakeexec.FakeAction
   406  	var fcmd fakeexec.FakeCmd
   407  	for _, expected := range expecteds {
   408  		expected := expected
   409  		if expected.cmd[1] == "clone" {
   410  			// Calculate the subdirectory clone would create (if any)
   411  			// git clone -- https://github.com/kubernetes/kubernetes.git target_dir --> target_dir
   412  			// git clone -- https://github.com/kubernetes/kubernetes.git            --> kubernetes
   413  			// git clone -- https://github.com/kubernetes/kubernetes.git .          --> .
   414  			// git clone -- https://github.com/kubernetes/kubernetes.git ./.        --> .
   415  			cloneSubdir := path.Base(expected.cmd[len(expected.cmd)-1])
   416  			if cloneSubdir == "kubernetes.git" {
   417  				cloneSubdir = "kubernetes"
   418  			}
   419  			fakeOutputs = append(fakeOutputs, func() ([]byte, []byte, error) {
   420  				// git clone, it creates new dir/files
   421  				os.MkdirAll(filepath.Join(fcmd.Dirs[0], expected.dir, cloneSubdir), 0750)
   422  				return []byte{}, nil, nil
   423  			})
   424  		} else {
   425  			// git checkout || git reset, they create nothing
   426  			fakeOutputs = append(fakeOutputs, func() ([]byte, []byte, error) {
   427  				return []byte{}, nil, nil
   428  			})
   429  		}
   430  	}
   431  	fcmd = fakeexec.FakeCmd{
   432  		CombinedOutputScript: fakeOutputs,
   433  	}
   434  
   435  	// Construct fake exec outputs from fcmd
   436  	var fakeAction []fakeexec.FakeCommandAction
   437  	for i := 0; i < len(expecteds); i++ {
   438  		fakeAction = append(fakeAction, func(cmd string, args ...string) exec.Cmd {
   439  			return fakeexec.InitFakeCmd(&fcmd, cmd, args...)
   440  		})
   441  
   442  	}
   443  	fake := &fakeexec.FakeExec{
   444  		CommandScript: fakeAction,
   445  	}
   446  
   447  	g := mounter.(*gitRepoVolumeMounter)
   448  	g.exec = fake
   449  
   450  	err := g.SetUp(volume.MounterArgs{})
   451  	if err != nil {
   452  		allErrs = append(allErrs, err)
   453  	}
   454  
   455  	if fake.CommandCalls != len(expecteds) {
   456  		allErrs = append(allErrs,
   457  			fmt.Errorf("unexpected command calls in scenario: expected %d, saw: %d", len(expecteds), fake.CommandCalls))
   458  	}
   459  	var expectedCmds [][]string
   460  	for _, expected := range expecteds {
   461  		expectedCmds = append(expectedCmds, expected.cmd)
   462  	}
   463  	if !reflect.DeepEqual(expectedCmds, fcmd.CombinedOutputLog) {
   464  		allErrs = append(allErrs,
   465  			fmt.Errorf("unexpected commands: %v, expected: %v", fcmd.CombinedOutputLog, expectedCmds))
   466  	}
   467  
   468  	var expectedPaths []string
   469  	for _, expected := range expecteds {
   470  		expectedPaths = append(expectedPaths, filepath.Join(g.GetPath(), expected.dir))
   471  	}
   472  	if len(fcmd.Dirs) != len(expectedPaths) || !reflect.DeepEqual(expectedPaths, fcmd.Dirs) {
   473  		allErrs = append(allErrs,
   474  			fmt.Errorf("unexpected directories: %v, expected: %v", fcmd.Dirs, expectedPaths))
   475  	}
   476  
   477  	return allErrs
   478  }