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 }