k8s.io/kubernetes@v1.29.3/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  
   272  	for _, scenario := range scenarios {
   273  		allErrs := doTestPlugin(scenario, t)
   274  		if len(allErrs) == 0 && scenario.isExpectedFailure {
   275  			t.Errorf("Unexpected success for scenario: %s", scenario.name)
   276  		}
   277  		if len(allErrs) > 0 && !scenario.isExpectedFailure {
   278  			t.Errorf("Unexpected failure for scenario: %s - %+v", scenario.name, allErrs)
   279  		}
   280  	}
   281  
   282  }
   283  
   284  func doTestPlugin(scenario struct {
   285  	name              string
   286  	vol               *v1.Volume
   287  	expecteds         []expectedCommand
   288  	isExpectedFailure bool
   289  }, t *testing.T) []error {
   290  	allErrs := []error{}
   291  
   292  	plugMgr := volume.VolumePluginMgr{}
   293  	rootDir, host := newTestHost(t)
   294  	defer os.RemoveAll(rootDir)
   295  	plugMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, host)
   296  
   297  	plug, err := plugMgr.FindPluginByName("kubernetes.io/git-repo")
   298  	if err != nil {
   299  		allErrs = append(allErrs,
   300  			fmt.Errorf("can't find the plugin by name"))
   301  		return allErrs
   302  	}
   303  	pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{UID: types.UID("poduid")}}
   304  	mounter, err := plug.NewMounter(volume.NewSpecFromVolume(scenario.vol), pod, volume.VolumeOptions{})
   305  
   306  	if err != nil {
   307  		allErrs = append(allErrs,
   308  			fmt.Errorf("failed to make a new Mounter: %w", err))
   309  		return allErrs
   310  	}
   311  	if mounter == nil {
   312  		allErrs = append(allErrs,
   313  			fmt.Errorf("got a nil Mounter"))
   314  		return allErrs
   315  	}
   316  
   317  	path := mounter.GetPath()
   318  	suffix := filepath.Join("pods/poduid/volumes/kubernetes.io~git-repo", scenario.vol.Name)
   319  	if !strings.HasSuffix(path, suffix) {
   320  		allErrs = append(allErrs,
   321  			fmt.Errorf("got unexpected path: %s", path))
   322  		return allErrs
   323  	}
   324  
   325  	// Test setUp()
   326  	setUpErrs := doTestSetUp(scenario, mounter)
   327  	allErrs = append(allErrs, setUpErrs...)
   328  
   329  	if _, err := os.Stat(path); err != nil {
   330  		if os.IsNotExist(err) {
   331  			allErrs = append(allErrs,
   332  				fmt.Errorf("SetUp() failed, volume path not created: %s", path))
   333  			return allErrs
   334  		}
   335  		allErrs = append(allErrs,
   336  			fmt.Errorf("SetUp() failed: %v", err))
   337  		return allErrs
   338  
   339  	}
   340  
   341  	// gitRepo volume should create its own empty wrapper path
   342  	podWrapperMetadataDir := fmt.Sprintf("%v/pods/poduid/plugins/kubernetes.io~empty-dir/wrapped_%v", rootDir, scenario.vol.Name)
   343  
   344  	if _, err := os.Stat(podWrapperMetadataDir); err != nil {
   345  		if os.IsNotExist(err) {
   346  			allErrs = append(allErrs,
   347  				fmt.Errorf("SetUp() failed, empty-dir wrapper path is not created: %s", podWrapperMetadataDir))
   348  		} else {
   349  			allErrs = append(allErrs,
   350  				fmt.Errorf("SetUp() failed: %v", err))
   351  		}
   352  	}
   353  
   354  	unmounter, err := plug.NewUnmounter("vol1", types.UID("poduid"))
   355  	if err != nil {
   356  		allErrs = append(allErrs,
   357  			fmt.Errorf("failed to make a new Unmounter: %w", err))
   358  		return allErrs
   359  	}
   360  	if unmounter == nil {
   361  		allErrs = append(allErrs,
   362  			fmt.Errorf("got a nil Unmounter"))
   363  		return allErrs
   364  	}
   365  
   366  	if err := unmounter.TearDown(); err != nil {
   367  		allErrs = append(allErrs,
   368  			fmt.Errorf("expected success, got: %w", err))
   369  		return allErrs
   370  	}
   371  	if _, err := os.Stat(path); err == nil {
   372  		allErrs = append(allErrs,
   373  			fmt.Errorf("TearDown() failed, volume path still exists: %s", path))
   374  	} else if !os.IsNotExist(err) {
   375  		allErrs = append(allErrs,
   376  			fmt.Errorf("TearDown() failed: %w", err))
   377  	}
   378  	return allErrs
   379  }
   380  
   381  func doTestSetUp(scenario struct {
   382  	name              string
   383  	vol               *v1.Volume
   384  	expecteds         []expectedCommand
   385  	isExpectedFailure bool
   386  }, mounter volume.Mounter) []error {
   387  	expecteds := scenario.expecteds
   388  	allErrs := []error{}
   389  
   390  	// Construct combined outputs from expected commands
   391  	var fakeOutputs []fakeexec.FakeAction
   392  	var fcmd fakeexec.FakeCmd
   393  	for _, expected := range expecteds {
   394  		expected := expected
   395  		if expected.cmd[1] == "clone" {
   396  			// Calculate the subdirectory clone would create (if any)
   397  			// git clone -- https://github.com/kubernetes/kubernetes.git target_dir --> target_dir
   398  			// git clone -- https://github.com/kubernetes/kubernetes.git            --> kubernetes
   399  			// git clone -- https://github.com/kubernetes/kubernetes.git .          --> .
   400  			// git clone -- https://github.com/kubernetes/kubernetes.git ./.        --> .
   401  			cloneSubdir := path.Base(expected.cmd[len(expected.cmd)-1])
   402  			if cloneSubdir == "kubernetes.git" {
   403  				cloneSubdir = "kubernetes"
   404  			}
   405  			fakeOutputs = append(fakeOutputs, func() ([]byte, []byte, error) {
   406  				// git clone, it creates new dir/files
   407  				os.MkdirAll(filepath.Join(fcmd.Dirs[0], expected.dir, cloneSubdir), 0750)
   408  				return []byte{}, nil, nil
   409  			})
   410  		} else {
   411  			// git checkout || git reset, they create nothing
   412  			fakeOutputs = append(fakeOutputs, func() ([]byte, []byte, error) {
   413  				return []byte{}, nil, nil
   414  			})
   415  		}
   416  	}
   417  	fcmd = fakeexec.FakeCmd{
   418  		CombinedOutputScript: fakeOutputs,
   419  	}
   420  
   421  	// Construct fake exec outputs from fcmd
   422  	var fakeAction []fakeexec.FakeCommandAction
   423  	for i := 0; i < len(expecteds); i++ {
   424  		fakeAction = append(fakeAction, func(cmd string, args ...string) exec.Cmd {
   425  			return fakeexec.InitFakeCmd(&fcmd, cmd, args...)
   426  		})
   427  
   428  	}
   429  	fake := &fakeexec.FakeExec{
   430  		CommandScript: fakeAction,
   431  	}
   432  
   433  	g := mounter.(*gitRepoVolumeMounter)
   434  	g.exec = fake
   435  
   436  	err := g.SetUp(volume.MounterArgs{})
   437  	if err != nil {
   438  		allErrs = append(allErrs, err)
   439  	}
   440  
   441  	if fake.CommandCalls != len(expecteds) {
   442  		allErrs = append(allErrs,
   443  			fmt.Errorf("unexpected command calls in scenario: expected %d, saw: %d", len(expecteds), fake.CommandCalls))
   444  	}
   445  	var expectedCmds [][]string
   446  	for _, expected := range expecteds {
   447  		expectedCmds = append(expectedCmds, expected.cmd)
   448  	}
   449  	if !reflect.DeepEqual(expectedCmds, fcmd.CombinedOutputLog) {
   450  		allErrs = append(allErrs,
   451  			fmt.Errorf("unexpected commands: %v, expected: %v", fcmd.CombinedOutputLog, expectedCmds))
   452  	}
   453  
   454  	var expectedPaths []string
   455  	for _, expected := range expecteds {
   456  		expectedPaths = append(expectedPaths, filepath.Join(g.GetPath(), expected.dir))
   457  	}
   458  	if len(fcmd.Dirs) != len(expectedPaths) || !reflect.DeepEqual(expectedPaths, fcmd.Dirs) {
   459  		allErrs = append(allErrs,
   460  			fmt.Errorf("unexpected directories: %v, expected: %v", fcmd.Dirs, expectedPaths))
   461  	}
   462  
   463  	return allErrs
   464  }