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