1/* 2Copyright 2014 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package git_repo 18 19import ( 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. 34func ProbeVolumePlugins() []volume.VolumePlugin { 35 return []volume.VolumePlugin{&gitRepoPlugin{nil}} 36} 37 38type gitRepoPlugin struct { 39 host volume.VolumeHost 40} 41 42var _ volume.VolumePlugin = &gitRepoPlugin{} 43 44func wrappedVolumeSpec() volume.Spec { 45 return volume.Spec{ 46 Volume: &v1.Volume{VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}}}, 47 } 48} 49 50const ( 51 gitRepoPluginName = "kubernetes.io/git-repo" 52) 53 54func (plugin *gitRepoPlugin) Init(host volume.VolumeHost) error { 55 plugin.host = host 56 return nil 57} 58 59func (plugin *gitRepoPlugin) GetPluginName() string { 60 return gitRepoPluginName 61} 62 63func (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 76func (plugin *gitRepoPlugin) CanSupport(spec *volume.Spec) bool { 77 return spec.Volume != nil && spec.Volume.GitRepo != nil 78} 79 80func (plugin *gitRepoPlugin) RequiresRemount(spec *volume.Spec) bool { 81 return false 82} 83 84func (plugin *gitRepoPlugin) SupportsMountOption() bool { 85 return false 86} 87 88func (plugin *gitRepoPlugin) SupportsBulkVolumeVerification() bool { 89 return false 90} 91 92func (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 112func (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 122func (plugin *gitRepoPlugin) ConstructVolumeSpec(volumeName, mountPath string) (*volume.Spec, error) { 123 gitVolume := &v1.Volume{ 124 Name: volumeName, 125 VolumeSource: v1.VolumeSource{ 126 GitRepo: &v1.GitRepoVolumeSource{}, 127 }, 128 } 129 return volume.NewSpecFromVolume(gitVolume), nil 130} 131 132// gitRepo volumes are directories which are pre-filled from a git repository. 133// These do not persist beyond the lifetime of a pod. 134type gitRepoVolume struct { 135 volName string 136 podUID types.UID 137 plugin *gitRepoPlugin 138 volume.MetricsNil 139} 140 141var _ volume.Volume = &gitRepoVolume{} 142 143func (gr *gitRepoVolume) GetPath() string { 144 name := gitRepoPluginName 145 return gr.plugin.host.GetPodVolumeDir(gr.podUID, utilstrings.EscapeQualifiedName(name), gr.volName) 146} 147 148// gitRepoVolumeMounter builds git repo volumes. 149type gitRepoVolumeMounter struct { 150 *gitRepoVolume 151 152 pod v1.Pod 153 source string 154 revision string 155 target string 156 exec exec.Interface 157 opts volume.VolumeOptions 158} 159 160var _ volume.Mounter = &gitRepoVolumeMounter{} 161 162func (b *gitRepoVolumeMounter) GetAttributes() volume.Attributes { 163 return volume.Attributes{ 164 ReadOnly: false, 165 Managed: true, 166 SupportsSELinux: true, // xattr change should be okay, TODO: double check 167 } 168} 169 170// Checks prior to mount operations to verify that the required components (binaries, etc.) 171// to mount the volume are available on the underlying node. 172// If not, it returns an error 173func (b *gitRepoVolumeMounter) CanMount() error { 174 return nil 175} 176 177// SetUp creates new directory and clones a git repo. 178func (b *gitRepoVolumeMounter) SetUp(mounterArgs volume.MounterArgs) error { 179 return b.SetUpAt(b.GetPath(), mounterArgs) 180} 181 182// SetUpAt creates new directory and clones a git repo. 183func (b *gitRepoVolumeMounter) SetUpAt(dir string, mounterArgs volume.MounterArgs) error { 184 if volumeutil.IsReady(b.getMetaDir()) { 185 return nil 186 } 187 188 // Wrap EmptyDir, let it do the setup. 189 wrapped, err := b.plugin.host.NewWrapperMounter(b.volName, wrappedVolumeSpec(), &b.pod, b.opts) 190 if err != nil { 191 return err 192 } 193 if err := wrapped.SetUpAt(dir, mounterArgs); err != nil { 194 return err 195 } 196 197 args := []string{"clone", "--", b.source} 198 199 if len(b.target) != 0 { 200 args = append(args, b.target) 201 } 202 if output, err := b.execCommand("git", args, dir); err != nil { 203 return fmt.Errorf("failed to exec 'git %s': %s: %v", 204 strings.Join(args, " "), output, err) 205 } 206 207 files, err := ioutil.ReadDir(dir) 208 if err != nil { 209 return err 210 } 211 212 if len(b.revision) == 0 { 213 // Done! 214 volumeutil.SetReady(b.getMetaDir()) 215 return nil 216 } 217 218 var subdir string 219 220 switch { 221 case len(b.target) != 0 && filepath.Clean(b.target) == ".": 222 // if target dir is '.', use the current dir 223 subdir = filepath.Join(dir) 224 case len(files) == 1: 225 // if target is not '.', use the generated folder 226 subdir = filepath.Join(dir, files[0].Name()) 227 default: 228 // if target is not '.', but generated many files, it's wrong 229 return fmt.Errorf("unexpected directory contents: %v", files) 230 } 231 232 if output, err := b.execCommand("git", []string{"checkout", b.revision}, subdir); err != nil { 233 return fmt.Errorf("failed to exec 'git checkout %s': %s: %v", b.revision, output, err) 234 } 235 if output, err := b.execCommand("git", []string{"reset", "--hard"}, subdir); err != nil { 236 return fmt.Errorf("failed to exec 'git reset --hard': %s: %v", output, err) 237 } 238 239 volume.SetVolumeOwnership(b, mounterArgs.FsGroup, nil /*fsGroupChangePolicy*/, volumeutil.FSGroupCompleteHook(b.plugin, nil)) 240 241 volumeutil.SetReady(b.getMetaDir()) 242 return nil 243} 244 245func (b *gitRepoVolumeMounter) getMetaDir() string { 246 return filepath.Join(b.plugin.host.GetPodPluginDir(b.podUID, utilstrings.EscapeQualifiedName(gitRepoPluginName)), b.volName) 247} 248 249func (b *gitRepoVolumeMounter) execCommand(command string, args []string, dir string) ([]byte, error) { 250 cmd := b.exec.Command(command, args...) 251 cmd.SetDir(dir) 252 return cmd.CombinedOutput() 253} 254 255func validateVolume(src *v1.GitRepoVolumeSource) error { 256 if err := validateNonFlagArgument(src.Repository, "repository"); err != nil { 257 return err 258 } 259 if err := validateNonFlagArgument(src.Revision, "revision"); err != nil { 260 return err 261 } 262 if err := validateNonFlagArgument(src.Directory, "directory"); err != nil { 263 return err 264 } 265 return nil 266} 267 268// gitRepoVolumeUnmounter cleans git repo volumes. 269type gitRepoVolumeUnmounter struct { 270 *gitRepoVolume 271} 272 273var _ volume.Unmounter = &gitRepoVolumeUnmounter{} 274 275// TearDown simply deletes everything in the directory. 276func (c *gitRepoVolumeUnmounter) TearDown() error { 277 return c.TearDownAt(c.GetPath()) 278} 279 280// TearDownAt simply deletes everything in the directory. 281func (c *gitRepoVolumeUnmounter) TearDownAt(dir string) error { 282 return volumeutil.UnmountViaEmptyDir(dir, c.plugin.host, c.volName, wrappedVolumeSpec(), c.podUID) 283} 284 285func getVolumeSource(spec *volume.Spec) (*v1.GitRepoVolumeSource, bool) { 286 var readOnly bool 287 var volumeSource *v1.GitRepoVolumeSource 288 289 if spec.Volume != nil && spec.Volume.GitRepo != nil { 290 volumeSource = spec.Volume.GitRepo 291 readOnly = spec.ReadOnly 292 } 293 294 return volumeSource, readOnly 295} 296 297func validateNonFlagArgument(arg, argName string) error { 298 if len(arg) > 0 && arg[0] == '-' { 299 return fmt.Errorf("%q is an invalid value for %s", arg, argName) 300 } 301 return nil 302} 303