Browse code

Update recycler image to use binary

Jordan Liggitt authored on 2015/11/12 03:21:06
Showing 13 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,25 @@
0
+package main
1
+
2
+import (
3
+	"fmt"
4
+	"os"
5
+
6
+	"github.com/openshift/origin/pkg/cmd/recycle"
7
+)
8
+
9
+func main() {
10
+	basename := os.Args[0]
11
+	args := os.Args[1:]
12
+	if len(args) == 0 || len(args[0]) == 0 {
13
+		fmt.Printf("Usage: %s DIR\n", basename)
14
+		os.Exit(1)
15
+	}
16
+
17
+	if err := recycle.Recycle(args[0]); err != nil {
18
+		fmt.Printf("Scrub failed: %v\n", err)
19
+		os.Exit(1)
20
+	}
21
+
22
+	fmt.Println("Scrub OK")
23
+	os.Exit(0)
24
+}
... ...
@@ -49,6 +49,7 @@ cp -pf "${imagedir}/hello-openshift" examples/hello-openshift/bin
49 49
 cp -pf "${imagedir}/deployment"      examples/deployment/bin
50 50
 cp -pf "${imagedir}/gitserver"       examples/gitserver/bin
51 51
 cp -pf "${imagedir}/dockerregistry"  images/dockerregistry/bin
52
+cp -pf "${imagedir}/recycle"         images/recycler/bin
52 53
 
53 54
 # Copy SDN scripts into images/node
54 55
 os::util::install-sdn "${OS_ROOT}" "${OS_ROOT}/images/node"
... ...
@@ -31,6 +31,7 @@ readonly OS_IMAGE_COMPILE_TARGETS=(
31 31
   images/pod
32 32
   cmd/dockerregistry
33 33
   cmd/gitserver
34
+  cmd/recycle
34 35
 )
35 36
 readonly OS_SCRATCH_IMAGE_COMPILE_TARGETS=(
36 37
   examples/hello-openshift
... ...
@@ -59,7 +60,6 @@ readonly OS_ALL_BINARIES=("${OS_ALL_TARGETS[@]##*/}")
59 59
 readonly OPENSHIFT_BINARY_SYMLINKS=(
60 60
   openshift-router
61 61
   openshift-deploy
62
-  openshift-recycle
63 62
   openshift-sti-build
64 63
   openshift-docker-build
65 64
   origin
... ...
@@ -1,17 +1,10 @@
1 1
 #
2
-# This is the default deployment strategy image for OpenShift Origin. It expects a set of
3
-# environment variables to parameterize the deploy:
4
-#
5
-#   KUBERNETES_MASTER - the address of the OpenShift master
6
-#   KUBERNETES_DEPLOYMENT_ID - the deployment identifier that is running this build
2
+# This is the default OpenShift Origin persistent volume recycler image.
7 3
 #
8 4
 # The standard name for this image is openshift/origin-recycler
9 5
 #
10
-FROM openshift/origin
11
-
12
-RUN yum install -y sudo && \
13
-    sed -i -e "s/Defaults    requiretty.*/ #Defaults    requiretty/g" /etc/sudoers
6
+FROM scratch
14 7
 
15
-ADD scripts/recycler.sh /usr/share/openshift/scripts/volumes/recycler.sh
8
+ADD bin/recycle /usr/bin/recycle
16 9
 
17
-ENTRYPOINT ["/usr/share/openshift/scripts/volumes/recycler.sh"]
10
+ENTRYPOINT ["/usr/bin/recycle"]
18 11
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+recycle
0 1
deleted file mode 100755
... ...
@@ -1,49 +0,0 @@
1
-#!/bin/bash
2
-
3
-# 'recycler' performs an 'rm -rf' on a volume to scrub it clean before it's
4
-# reused as a cluster resource. This script is intended to be used in a pod that
5
-# performs the scrub. The container in the pod should succeed or fail based on
6
-# the exit status of this script.
7
-
8
-set -o errexit
9
-set -o noglob
10
-set -o nounset
11
-set -o pipefail
12
-
13
-if [[ $# -ne 1 ]]; then
14
-    echo >&2 "Usage: $0 some/path/to/scrub"
15
-    exit 1
16
-fi
17
-
18
-# first and only arg is the directory to scrub
19
-dir="${1}"
20
-
21
-if [[ ! -d "${dir}" ]]; then
22
-    echo >&2 "Error: scrub directory '${dir}' does not exist"
23
-    exit 1
24
-fi
25
-
26
-# shred regular files
27
-function recycle_file() {
28
-    filename="${1}"
29
-    uid=$(stat -c "#%u" "${filename}")
30
-    sudo -u "${uid}" shred "${filename}"
31
-}
32
-export -f recycle_file
33
-
34
-find "${dir}" -type f -print0 | xargs -r -n 1 -0 bash -c 'recycle_file "$@"' {}
35
-
36
-# rm all
37
-function rm_all() {
38
-    filename="${1}"
39
-    uid=$(stat -c "#%u" "${filename}")
40
-    sudo -u "${uid}" rm -rf "${filename}"
41
-}
42
-export -f rm_all
43
-
44
-find "${dir}" ! -type d -print0 | xargs -r -n 1 -0 bash -c 'rm_all "$@"' {}
45
-
46
-find "${dir}" -mindepth 1 -type d -print0 | sort -zrg | xargs -r -n 1 -0 bash -c 'rm_all "$@"' {}
47
-
48
-echo "Scrub OK"
49
-exit 0
50 1
new file mode 100644
... ...
@@ -0,0 +1,16 @@
0
+package recycle
1
+
2
+import "os"
3
+
4
+// Recycle recursively deletes files and folders within the given path. It does not delete the path itself.
5
+func Recycle(dir string) error {
6
+	return newWalker(func(path string, info os.FileInfo) error {
7
+		// Leave the root dir alone
8
+		if path == dir {
9
+			return nil
10
+		}
11
+
12
+		// Delete all subfiles/subdirs
13
+		return os.Remove(path)
14
+	}).Walk(dir)
15
+}
0 16
new file mode 100644
... ...
@@ -0,0 +1,89 @@
0
+package recycle
1
+
2
+import (
3
+	"io/ioutil"
4
+	"os"
5
+	"path"
6
+	"path/filepath"
7
+	"testing"
8
+)
9
+
10
+func TestRecycle(t *testing.T) {
11
+	root, err := ioutil.TempDir("", "recycler-test-")
12
+	if err != nil {
13
+		t.Fatal(err)
14
+	}
15
+	defer func() {
16
+		if err := os.RemoveAll(root); err != nil {
17
+			t.Fatal(err)
18
+		}
19
+	}()
20
+
21
+	filenames := []string{
22
+		string([]byte{3}), // Ctrl+C
23
+		string([]byte{4}), // Ctrl+D
24
+
25
+		`white space`,
26
+		`new
27
+	line`,
28
+
29
+		`*`,
30
+		`~`,
31
+		`\`,
32
+		`\\`,
33
+
34
+		` && touch and-escape`,
35
+		` || touch or-escape`,
36
+		` ; touch semi-escape`,
37
+		` " touch quote-escape`,
38
+		` ' touch apos-escape`,
39
+		` }"; touch brace-escape`,
40
+
41
+		`env x='() { :;}; echo vulnerable'`, // shellshock
42
+
43
+		`$USER`,
44
+
45
+		`...`,
46
+		`.file`,
47
+
48
+		`中文`,                   // utf-8
49
+		`κόσμε`,                // utf-8
50
+		`Iñtërnâtiônàlizætiøn`, // utf-8
51
+	}
52
+
53
+	for _, dir := range filenames {
54
+		dirpath := path.Join(root, dir)
55
+		if err := os.Mkdir(dirpath, os.FileMode(0755)); err != nil {
56
+			t.Errorf("Error writing dir %s\n%v", dirpath, err)
57
+			continue
58
+		}
59
+
60
+		for _, file := range filenames {
61
+			filepath := path.Join(dirpath, file)
62
+			if err := ioutil.WriteFile(filepath, []byte(filepath), os.FileMode(0755)); err != nil {
63
+				t.Errorf("Error writing file %s\n%v", filepath, err)
64
+			}
65
+
66
+			if _, err := os.Stat(filepath); err != nil {
67
+				t.Errorf("Error verifying file %s\n%v", filepath, err)
68
+			}
69
+		}
70
+	}
71
+
72
+	err = Recycle(root)
73
+	if err != nil {
74
+		t.Error(err)
75
+	}
76
+
77
+	remaining := []string{}
78
+	err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
79
+		remaining = append(remaining, path)
80
+		return err
81
+	})
82
+	if err != nil {
83
+		t.Errorf("Unexpected error: %v", err)
84
+	}
85
+	if len(remaining) != 1 || remaining[0] != root {
86
+		t.Errorf("Unexpected files left after recycling: %#v", remaining)
87
+	}
88
+}
0 89
new file mode 100644
... ...
@@ -0,0 +1,179 @@
0
+package recycle
1
+
2
+import (
3
+	"fmt"
4
+	"os"
5
+	"path/filepath"
6
+	"runtime"
7
+	"sort"
8
+)
9
+
10
+type walkFunc func(path string, info os.FileInfo) error
11
+
12
+// Walker visits a directory tree, depth-first, calling walkFn for every file/directory.
13
+// It calls setfsuid with the owning UID of each directory before calling walkFn for the direct children of that directory.
14
+type walker struct {
15
+	walkFn walkFunc
16
+
17
+	// fsuid holds our current fsuid
18
+	fsuid int64
19
+
20
+	// lstat is for testing, defaults to os.Lstat
21
+	lstat func(path string) (os.FileInfo, error)
22
+
23
+	// getuid is for testing, defaults to fileinfo.Sys().(*syscall.Stat_t).Uid
24
+	getuid func(info os.FileInfo) (int64, error)
25
+
26
+	// setfsuid is for testing, defaults to syscall.Setfsuid
27
+	setfsuid func(uid int) error
28
+
29
+	// readDirNames is for testing, defaults to readDirNames
30
+	readDirNames func(dirname string) ([]string, error)
31
+}
32
+
33
+type walkError struct {
34
+	path      string
35
+	info      os.FileInfo
36
+	operation string
37
+	err       error
38
+}
39
+
40
+func (w walkError) Error() string {
41
+	var mode interface{} = "unknown"
42
+	if w.info != nil {
43
+		mode = w.info.Mode()
44
+	}
45
+	return fmt.Sprintf("%s (%s), %s: %s", w.path, mode, w.operation, w.err)
46
+}
47
+
48
+func makeWalkError(path string, info os.FileInfo, err error, operation string) error {
49
+	if _, isWalkError := err.(walkError); isWalkError {
50
+		return err
51
+	}
52
+	return walkError{path, info, operation, err}
53
+}
54
+
55
+func newWalker(walkFn walkFunc) *walker {
56
+	return &walker{
57
+		walkFn:       walkFn,
58
+		fsuid:        int64(os.Getuid()), // default to the uid of the process
59
+		lstat:        os.Lstat,
60
+		getuid:       getuid,
61
+		setfsuid:     setfsuid,
62
+		readDirNames: readDirNames,
63
+	}
64
+}
65
+
66
+func (w *walker) Walk(root string) error {
67
+	// Lock threads, so our Setfsuid calls always apply to the same thread
68
+	runtime.LockOSThread()
69
+	defer runtime.UnlockOSThread()
70
+
71
+	// The launching process must have the ability to stat the root dir
72
+	info, err := w.lstat(root)
73
+	if err != nil {
74
+		return makeWalkError(root, info, err, "lstat root dir")
75
+	}
76
+
77
+	// become the root dir's owner to begin
78
+	err = w.becomeOwner(info)
79
+	if err != nil {
80
+		return makeWalkError(root, info, err, "becoming root dir owner")
81
+	}
82
+
83
+	return w.walk(root, info)
84
+}
85
+
86
+func (w *walker) walk(path string, info os.FileInfo) error {
87
+	var err error
88
+
89
+	// Descend first
90
+	if info.IsDir() {
91
+		// Remember our current fsuid
92
+		previousFSuid := w.fsuid
93
+
94
+		// become the dir's owner, in order to list/rmdir/unlink child files
95
+		err = w.becomeOwner(info)
96
+		if err != nil {
97
+			return makeWalkError(path, info, err, "becoming dir owner")
98
+		}
99
+
100
+		// read dir info
101
+		names, err := w.readDirNames(path)
102
+		if err != nil {
103
+			return makeWalkError(path, info, err, fmt.Sprintf("reading dir names as %d", w.fsuid))
104
+		}
105
+
106
+		// visit files
107
+		for _, name := range names {
108
+			filename := filepath.Join(path, name)
109
+
110
+			fileInfo, err := w.lstat(filename)
111
+			if err != nil {
112
+				return makeWalkError(path, info, err, fmt.Sprintf("lstat child as %d", w.fsuid))
113
+			}
114
+
115
+			err = w.walk(filename, fileInfo)
116
+			if err != nil {
117
+				return err
118
+			}
119
+		}
120
+
121
+		// Return to our previous fsuid, in order to rmdir the current directory
122
+		err = w.becomeUid(previousFSuid)
123
+		if err != nil {
124
+			return makeWalkError(path, info, err, "returning to previous uid")
125
+		}
126
+	}
127
+
128
+	// visit the current file
129
+	err = w.walkFn(path, info)
130
+	if err != nil {
131
+		return makeWalkError(path, info, err, "calling walkFn")
132
+	}
133
+
134
+	return nil
135
+}
136
+
137
+func (w *walker) becomeOwner(info os.FileInfo) error {
138
+	// get the UID
139
+	uid, err := w.getuid(info)
140
+	if err != nil {
141
+		return err
142
+	}
143
+
144
+	return w.becomeUid(uid)
145
+}
146
+
147
+func (w *walker) becomeUid(uid int64) error {
148
+	// if we already were the UID, no-op
149
+	if w.fsuid == uid {
150
+		return nil
151
+	}
152
+
153
+	// become the UID
154
+	if err := w.setfsuid(int(uid)); err != nil {
155
+		return err
156
+	}
157
+
158
+	// remember the last UID we became
159
+	w.fsuid = uid
160
+
161
+	return nil
162
+}
163
+
164
+// readDirNames reads the directory named by dirname and returns a sorted list of directory entries.
165
+func readDirNames(dirname string) ([]string, error) {
166
+	f, err := os.Open(dirname)
167
+	if err != nil {
168
+		return nil, err
169
+	}
170
+	defer f.Close()
171
+
172
+	names, err := f.Readdirnames(-1)
173
+	if err != nil {
174
+		return nil, err
175
+	}
176
+	sort.Strings(names)
177
+	return names, nil
178
+}
0 179
new file mode 100644
... ...
@@ -0,0 +1,23 @@
0
+// +build linux
1
+
2
+package recycle
3
+
4
+import (
5
+	"errors"
6
+	"os"
7
+	"syscall"
8
+)
9
+
10
+var StatError = errors.New("fileinfo.Sys() is not *syscall.Stat_t")
11
+
12
+func getuid(info os.FileInfo) (int64, error) {
13
+	stat_t, ok := info.Sys().(*syscall.Stat_t)
14
+	if !ok {
15
+		return 0, StatError
16
+	}
17
+	return int64(stat_t.Uid), nil
18
+}
19
+
20
+func setfsuid(uid int) (err error) {
21
+	return syscall.Setfsuid(uid)
22
+}
0 23
new file mode 100644
... ...
@@ -0,0 +1,15 @@
0
+// +build !linux
1
+
2
+package recycle
3
+
4
+import "os"
5
+
6
+func getuid(info os.FileInfo) (int64, error) {
7
+	// no-op on non-linux platforms
8
+	return 0, nil
9
+}
10
+
11
+func setfsuid(uid int) (err error) {
12
+	// no-op on non-linux platforms
13
+	return nil
14
+}
0 15
new file mode 100644
... ...
@@ -0,0 +1,154 @@
0
+package recycle
1
+
2
+import (
3
+	"io/ioutil"
4
+	"os"
5
+	"path/filepath"
6
+	"reflect"
7
+	"testing"
8
+)
9
+
10
+func TestStatUID(t *testing.T) {
11
+	root, err := ioutil.TempDir("", "walker-test-")
12
+	if err != nil {
13
+		t.Fatal(err)
14
+	}
15
+	defer func() {
16
+		if err := os.RemoveAll(root); err != nil {
17
+			t.Fatal(err)
18
+		}
19
+	}()
20
+
21
+	rootName := filepath.Base(root)
22
+
23
+	files := map[string]testFile{
24
+		filepath.Join(root):                  {uid: 0, dir: true},
25
+		filepath.Join(root, "dir1"):          {uid: 1, dir: true},
26
+		filepath.Join(root, "dir2"):          {uid: 2, dir: true},
27
+		filepath.Join(root, "dir2/subdir"):   {uid: 2, dir: true},
28
+		filepath.Join(root, "dir2/subfile1"): {uid: 123},
29
+		filepath.Join(root, "dir2/subfile2"): {uid: 234},
30
+		filepath.Join(root, "file1"):         {uid: 345},
31
+		filepath.Join(root, "file2"):         {uid: 456},
32
+	}
33
+	for path, fileinfo := range files {
34
+		if fileinfo.dir {
35
+			if err := os.MkdirAll(path, os.FileMode(0755)); err != nil {
36
+				t.Fatalf("Error writing dir %s\n%v", path, err)
37
+				continue
38
+			}
39
+		} else {
40
+			if err := os.MkdirAll(filepath.Dir(path), os.FileMode(0755)); err != nil {
41
+				t.Fatalf("Error writing dir %s\n%v", path, err)
42
+				continue
43
+			}
44
+			if err := ioutil.WriteFile(path, []byte(path), os.FileMode(0755)); err != nil {
45
+				t.Fatalf("Error writing file %s\n%v", path, err)
46
+			}
47
+		}
48
+	}
49
+
50
+	expectedActions := []testAction{
51
+		// get root info, list files
52
+		// no setfsuid call, because we are already uid 0
53
+		{"lstat", rootName},
54
+		{"readDirNames", rootName},
55
+
56
+		// get dir1 info, become owner, list files, change back to root owner, walk dir1 (as owner of root dir, so we can rmdir dir1)
57
+		{"lstat", "dir1"},
58
+		{"setfsuid", 1},
59
+		{"readDirNames", "dir1"},
60
+		{"setfsuid", 0},
61
+		{"walk", "dir1"},
62
+
63
+		// get dir2 info, become owner, list files
64
+		{"lstat", "dir2"},
65
+		{"setfsuid", 2},
66
+		{"readDirNames", "dir2"},
67
+		// get subdir info, list files, walk
68
+		// no setfsuid calls to subdir owner or back to dir2 owner, because we are already uid 2
69
+		{"lstat", "subdir"},
70
+		{"readDirNames", "subdir"},
71
+		{"walk", "subdir"},
72
+		// stat and walk subfiles, no fsuid changes needed for files
73
+		{"lstat", "subfile1"},
74
+		{"walk", "subfile1"},
75
+		{"lstat", "subfile2"},
76
+		{"walk", "subfile2"},
77
+		// change back to root owner, walk dir2 (as owner of root dir, so we can rmdir dir2)
78
+		{"setfsuid", 0},
79
+		{"walk", "dir2"},
80
+
81
+		// stat and walk files, no fsuid changes needed for files
82
+		{"lstat", "file1"},
83
+		{"walk", "file1"},
84
+		{"lstat", "file2"},
85
+		{"walk", "file2"},
86
+
87
+		// finally, walk the root
88
+		{"walk", rootName},
89
+	}
90
+	actions := []testAction{}
91
+
92
+	w := &walker{
93
+		fsuid: 0, // mock starting as uid 0
94
+		walkFn: func(path string, info os.FileInfo) error {
95
+			actions = append(actions, testAction{"walk", filepath.Base(path)})
96
+			return nil
97
+		},
98
+		lstat: func(path string) (os.FileInfo, error) {
99
+			actions = append(actions, testAction{"lstat", filepath.Base(path)})
100
+			l, err := os.Lstat(path)
101
+			return testFileInfoWrapper{l, files[path]}, err
102
+		},
103
+		getuid: func(info os.FileInfo) (int64, error) {
104
+			return info.(testFileInfoWrapper).testData.uid, nil
105
+		},
106
+		setfsuid: func(uid int) error {
107
+			actions = append(actions, testAction{"setfsuid", uid})
108
+			return nil
109
+		},
110
+		readDirNames: func(path string) ([]string, error) {
111
+			actions = append(actions, testAction{"readDirNames", filepath.Base(path)})
112
+			return readDirNames(path)
113
+		},
114
+	}
115
+
116
+	err = w.Walk(root)
117
+	if err != nil {
118
+		t.Fatalf("Unexpected error: %v", err)
119
+	}
120
+
121
+	for i, action := range actions {
122
+		if len(expectedActions) < i+1 {
123
+			t.Errorf("%d unexpected actions: %+v", len(actions)-len(expectedActions), actions[i:])
124
+			break
125
+		}
126
+
127
+		expectedAction := expectedActions[i]
128
+		if !reflect.DeepEqual(expectedAction, action) {
129
+			t.Errorf("%d: expected %#v\ngot                      %#v", i, expectedAction, action)
130
+			continue
131
+		}
132
+	}
133
+
134
+	if len(expectedActions) > len(actions) {
135
+		t.Errorf("%d additional expected actions:%+v", len(expectedActions)-len(actions), expectedActions[len(actions):])
136
+	}
137
+
138
+}
139
+
140
+type testFile struct {
141
+	uid int64
142
+	dir bool
143
+}
144
+
145
+type testAction struct {
146
+	Action string
147
+	Data   interface{}
148
+}
149
+
150
+type testFileInfoWrapper struct {
151
+	os.FileInfo
152
+	testData testFile
153
+}
... ...
@@ -80,9 +80,10 @@ func (c *MasterConfig) RunPersistentVolumeClaimRecycler(recyclerImageName string
80 80
 	uid := int64(0)
81 81
 	defaultScrubPod := volume.NewPersistentVolumeRecyclerPodTemplate()
82 82
 	defaultScrubPod.Spec.Containers[0].Image = recyclerImageName
83
-	defaultScrubPod.Spec.Containers[0].Command = []string{"/usr/share/openshift/scripts/volumes/recycler.sh"}
83
+	defaultScrubPod.Spec.Containers[0].Command = []string{"/usr/bin/recycle"}
84 84
 	defaultScrubPod.Spec.Containers[0].Args = []string{"/scrub"}
85 85
 	defaultScrubPod.Spec.Containers[0].SecurityContext = &kapi.SecurityContext{RunAsUser: &uid}
86
+	defaultScrubPod.Spec.Containers[0].ImagePullPolicy = kapi.PullIfNotPresent
86 87
 
87 88
 	hostPathConfig := volume.VolumeConfig{
88 89
 		RecyclerMinimumTimeout:   30,