k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/volume/git_repo/git_repo.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  	"path/filepath"
    23  	"strings"
    24  
    25  	v1 "k8s.io/api/core/v1"
    26  	"k8s.io/apimachinery/pkg/types"
    27  	"k8s.io/kubernetes/pkg/volume"
    28  	volumeutil "k8s.io/kubernetes/pkg/volume/util"
    29  	"k8s.io/utils/exec"
    30  	utilstrings "k8s.io/utils/strings"
    31  )
    32  
    33  // This is the primary entrypoint for volume plugins.
    34  func ProbeVolumePlugins() []volume.VolumePlugin {
    35  	return []volume.VolumePlugin{&gitRepoPlugin{nil}}
    36  }
    37  
    38  type gitRepoPlugin struct {
    39  	host volume.VolumeHost
    40  }
    41  
    42  var _ volume.VolumePlugin = &gitRepoPlugin{}
    43  
    44  func wrappedVolumeSpec() volume.Spec {
    45  	return volume.Spec{
    46  		Volume: &v1.Volume{VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}}},
    47  	}
    48  }
    49  
    50  const (
    51  	gitRepoPluginName = "kubernetes.io/git-repo"
    52  )
    53  
    54  func (plugin *gitRepoPlugin) Init(host volume.VolumeHost) error {
    55  	plugin.host = host
    56  	return nil
    57  }
    58  
    59  func (plugin *gitRepoPlugin) GetPluginName() string {
    60  	return gitRepoPluginName
    61  }
    62  
    63  func (plugin *gitRepoPlugin) GetVolumeName(spec *volume.Spec) (string, error) {
    64  	volumeSource, _ := getVolumeSource(spec)
    65  	if volumeSource == nil {
    66  		return "", fmt.Errorf("Spec does not reference a Git repo volume type")
    67  	}
    68  
    69  	return fmt.Sprintf(
    70  		"%v:%v:%v",
    71  		volumeSource.Repository,
    72  		volumeSource.Revision,
    73  		volumeSource.Directory), nil
    74  }
    75  
    76  func (plugin *gitRepoPlugin) CanSupport(spec *volume.Spec) bool {
    77  	return spec.Volume != nil && spec.Volume.GitRepo != nil
    78  }
    79  
    80  func (plugin *gitRepoPlugin) RequiresRemount(spec *volume.Spec) bool {
    81  	return false
    82  }
    83  
    84  func (plugin *gitRepoPlugin) SupportsMountOption() bool {
    85  	return false
    86  }
    87  
    88  func (plugin *gitRepoPlugin) SupportsSELinuxContextMount(spec *volume.Spec) (bool, error) {
    89  	return false, nil
    90  }
    91  
    92  func (plugin *gitRepoPlugin) NewMounter(spec *volume.Spec, pod *v1.Pod, opts volume.VolumeOptions) (volume.Mounter, error) {
    93  	if err := validateVolume(spec.Volume.GitRepo); err != nil {
    94  		return nil, err
    95  	}
    96  
    97  	return &gitRepoVolumeMounter{
    98  		gitRepoVolume: &gitRepoVolume{
    99  			volName: spec.Name(),
   100  			podUID:  pod.UID,
   101  			plugin:  plugin,
   102  		},
   103  		pod:      *pod,
   104  		source:   spec.Volume.GitRepo.Repository,
   105  		revision: spec.Volume.GitRepo.Revision,
   106  		target:   spec.Volume.GitRepo.Directory,
   107  		exec:     exec.New(),
   108  		opts:     opts,
   109  	}, nil
   110  }
   111  
   112  func (plugin *gitRepoPlugin) NewUnmounter(volName string, podUID types.UID) (volume.Unmounter, error) {
   113  	return &gitRepoVolumeUnmounter{
   114  		&gitRepoVolume{
   115  			volName: volName,
   116  			podUID:  podUID,
   117  			plugin:  plugin,
   118  		},
   119  	}, nil
   120  }
   121  
   122  func (plugin *gitRepoPlugin) ConstructVolumeSpec(volumeName, mountPath string) (volume.ReconstructedVolume, error) {
   123  	gitVolume := &v1.Volume{
   124  		Name: volumeName,
   125  		VolumeSource: v1.VolumeSource{
   126  			GitRepo: &v1.GitRepoVolumeSource{},
   127  		},
   128  	}
   129  	return volume.ReconstructedVolume{
   130  		Spec: volume.NewSpecFromVolume(gitVolume),
   131  	}, nil
   132  }
   133  
   134  // gitRepo volumes are directories which are pre-filled from a git repository.
   135  // These do not persist beyond the lifetime of a pod.
   136  type gitRepoVolume struct {
   137  	volName string
   138  	podUID  types.UID
   139  	plugin  *gitRepoPlugin
   140  	volume.MetricsNil
   141  }
   142  
   143  var _ volume.Volume = &gitRepoVolume{}
   144  
   145  func (gr *gitRepoVolume) GetPath() string {
   146  	name := gitRepoPluginName
   147  	return gr.plugin.host.GetPodVolumeDir(gr.podUID, utilstrings.EscapeQualifiedName(name), gr.volName)
   148  }
   149  
   150  // gitRepoVolumeMounter builds git repo volumes.
   151  type gitRepoVolumeMounter struct {
   152  	*gitRepoVolume
   153  
   154  	pod      v1.Pod
   155  	source   string
   156  	revision string
   157  	target   string
   158  	exec     exec.Interface
   159  	opts     volume.VolumeOptions
   160  }
   161  
   162  var _ volume.Mounter = &gitRepoVolumeMounter{}
   163  
   164  func (b *gitRepoVolumeMounter) GetAttributes() volume.Attributes {
   165  	return volume.Attributes{
   166  		ReadOnly:       false,
   167  		Managed:        true,
   168  		SELinuxRelabel: true, // xattr change should be okay, TODO: double check
   169  	}
   170  }
   171  
   172  // SetUp creates new directory and clones a git repo.
   173  func (b *gitRepoVolumeMounter) SetUp(mounterArgs volume.MounterArgs) error {
   174  	return b.SetUpAt(b.GetPath(), mounterArgs)
   175  }
   176  
   177  // SetUpAt creates new directory and clones a git repo.
   178  func (b *gitRepoVolumeMounter) SetUpAt(dir string, mounterArgs volume.MounterArgs) error {
   179  	if volumeutil.IsReady(b.getMetaDir()) {
   180  		return nil
   181  	}
   182  
   183  	// Wrap EmptyDir, let it do the setup.
   184  	wrapped, err := b.plugin.host.NewWrapperMounter(b.volName, wrappedVolumeSpec(), &b.pod, b.opts)
   185  	if err != nil {
   186  		return err
   187  	}
   188  	if err := wrapped.SetUpAt(dir, mounterArgs); err != nil {
   189  		return err
   190  	}
   191  
   192  	args := []string{"clone", "--", b.source}
   193  
   194  	if len(b.target) != 0 {
   195  		args = append(args, b.target)
   196  	}
   197  	if output, err := b.execCommand("git", args, dir); err != nil {
   198  		return fmt.Errorf("failed to exec 'git %s': %s: %v",
   199  			strings.Join(args, " "), output, err)
   200  	}
   201  
   202  	files, err := ioutil.ReadDir(dir)
   203  	if err != nil {
   204  		return err
   205  	}
   206  
   207  	if len(b.revision) == 0 {
   208  		// Done!
   209  		volumeutil.SetReady(b.getMetaDir())
   210  		return nil
   211  	}
   212  
   213  	var subdir string
   214  
   215  	switch {
   216  	case len(b.target) != 0 && filepath.Clean(b.target) == ".":
   217  		// if target dir is '.', use the current dir
   218  		subdir = filepath.Join(dir)
   219  	case len(files) == 1:
   220  		// if target is not '.', use the generated folder
   221  		subdir = filepath.Join(dir, files[0].Name())
   222  	default:
   223  		// if target is not '.', but generated many files, it's wrong
   224  		return fmt.Errorf("unexpected directory contents: %v", files)
   225  	}
   226  
   227  	if output, err := b.execCommand("git", []string{"checkout", b.revision}, subdir); err != nil {
   228  		return fmt.Errorf("failed to exec 'git checkout %s': %s: %v", b.revision, output, err)
   229  	}
   230  	if output, err := b.execCommand("git", []string{"reset", "--hard"}, subdir); err != nil {
   231  		return fmt.Errorf("failed to exec 'git reset --hard': %s: %v", output, err)
   232  	}
   233  
   234  	volume.SetVolumeOwnership(b, dir, mounterArgs.FsGroup, nil /*fsGroupChangePolicy*/, volumeutil.FSGroupCompleteHook(b.plugin, nil))
   235  
   236  	volumeutil.SetReady(b.getMetaDir())
   237  	return nil
   238  }
   239  
   240  func (b *gitRepoVolumeMounter) getMetaDir() string {
   241  	return filepath.Join(b.plugin.host.GetPodPluginDir(b.podUID, utilstrings.EscapeQualifiedName(gitRepoPluginName)), b.volName)
   242  }
   243  
   244  func (b *gitRepoVolumeMounter) execCommand(command string, args []string, dir string) ([]byte, error) {
   245  	cmd := b.exec.Command(command, args...)
   246  	cmd.SetDir(dir)
   247  	return cmd.CombinedOutput()
   248  }
   249  
   250  func validateVolume(src *v1.GitRepoVolumeSource) error {
   251  	if err := validateNonFlagArgument(src.Repository, "repository"); err != nil {
   252  		return err
   253  	}
   254  	if err := validateNonFlagArgument(src.Revision, "revision"); err != nil {
   255  		return err
   256  	}
   257  	if err := validateNonFlagArgument(src.Directory, "directory"); err != nil {
   258  		return err
   259  	}
   260  	if (src.Revision != "") && (src.Directory != "") {
   261  		cleanedDir := filepath.Clean(src.Directory)
   262  		if strings.Contains(cleanedDir, "/") || (strings.Contains(cleanedDir, "\\")) {
   263  			return fmt.Errorf("%q is not a valid directory, it must not contain a directory separator", src.Directory)
   264  		}
   265  	}
   266  	return nil
   267  }
   268  
   269  // gitRepoVolumeUnmounter cleans git repo volumes.
   270  type gitRepoVolumeUnmounter struct {
   271  	*gitRepoVolume
   272  }
   273  
   274  var _ volume.Unmounter = &gitRepoVolumeUnmounter{}
   275  
   276  // TearDown simply deletes everything in the directory.
   277  func (c *gitRepoVolumeUnmounter) TearDown() error {
   278  	return c.TearDownAt(c.GetPath())
   279  }
   280  
   281  // TearDownAt simply deletes everything in the directory.
   282  func (c *gitRepoVolumeUnmounter) TearDownAt(dir string) error {
   283  	return volumeutil.UnmountViaEmptyDir(dir, c.plugin.host, c.volName, wrappedVolumeSpec(), c.podUID)
   284  }
   285  
   286  func getVolumeSource(spec *volume.Spec) (*v1.GitRepoVolumeSource, bool) {
   287  	var readOnly bool
   288  	var volumeSource *v1.GitRepoVolumeSource
   289  
   290  	if spec.Volume != nil && spec.Volume.GitRepo != nil {
   291  		volumeSource = spec.Volume.GitRepo
   292  		readOnly = spec.ReadOnly
   293  	}
   294  
   295  	return volumeSource, readOnly
   296  }
   297  
   298  func validateNonFlagArgument(arg, argName string) error {
   299  	if len(arg) > 0 && arg[0] == '-' {
   300  		return fmt.Errorf("%q is an invalid value for %s", arg, argName)
   301  	}
   302  	return nil
   303  }