Skip to content

Commit fb744ce

Browse files
committed
Merge pull request #186 from arschles/app-deletion
fix(boot.go,pkg/*): implement a deleted app cleaner
2 parents 096fd61 + ab7a313 commit fb744ce

8 files changed

Lines changed: 356 additions & 23 deletions

File tree

boot.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
cookoolog "github.com/Masterminds/cookoo/log"
99
"github.com/codegangsta/cli"
1010
"github.com/deis/builder/pkg"
11+
"github.com/deis/builder/pkg/cleaner"
1112
"github.com/deis/builder/pkg/conf"
1213
"github.com/deis/builder/pkg/gitreceive"
1314
"github.com/deis/builder/pkg/gitreceive/storage"
@@ -20,6 +21,7 @@ import (
2021
const (
2122
serverConfAppName = "deis-builder-server"
2223
gitReceiveConfAppName = "deis-builder-git-receive"
24+
gitHomeDir = "/home/git"
2325
)
2426

2527
func init() {
@@ -46,6 +48,8 @@ func main() {
4648
pkglog.Err("getting config for %s [%s]", serverConfAppName, err)
4749
os.Exit(1)
4850
}
51+
pushLock := sshd.NewInMemoryRepositoryLock()
52+
cleanerRef := cleaner.NewRef()
4953
circ := sshd.NewCircuit()
5054

5155
s3Client, err := storage.GetClient(cnf.HealthSrvTestStorageRegion)
@@ -65,11 +69,18 @@ func main() {
6569
healthSrvCh <- err
6670
}
6771
}()
72+
log.Printf("Starting deleted app cleaner")
73+
cleanerErrCh := make(chan error)
74+
go func() {
75+
if err := cleanerRef.Run(gitHomeDir, kubeClient.Namespaces(), cleanerRef, cnf.CleanerPollSleepDuration()); err != nil {
76+
cleanerErrCh <- err
77+
}
78+
}()
6879

6980
log.Printf("Starting SSH server on %s:%d", cnf.SSHHostIP, cnf.SSHHostPort)
7081
sshCh := make(chan int)
7182
go func() {
72-
sshCh <- pkg.RunBuilder(cnf.SSHHostIP, cnf.SSHHostPort, circ)
83+
sshCh <- pkg.RunBuilder(cnf.SSHHostIP, cnf.SSHHostPort, gitHomeDir, circ, pushLock, cleanerRef)
7384
}()
7485

7586
select {
@@ -79,6 +90,9 @@ func main() {
7990
case i := <-sshCh:
8091
log.Printf("Unexpected SSH server stop with code %d", i)
8192
os.Exit(i)
93+
case err := <-cleanerErrCh:
94+
log.Printf("Error running the deleted app cleaner (%s)", err)
95+
os.Exit(1)
8296
}
8397
},
8498
},

pkg/builder.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/Masterminds/cookoo"
1212
clog "github.com/Masterminds/cookoo/log"
13+
"github.com/deis/builder/pkg/cleaner"
1314
"github.com/deis/builder/pkg/sshd"
1415
)
1516

@@ -27,7 +28,7 @@ const (
2728
// Git.
2829
//
2930
// Run returns on of the Status* status code constants.
30-
func RunBuilder(sshHostIP string, sshHostPort int, sshServerCircuit *sshd.Circuit) int {
31+
func RunBuilder(sshHostIP string, sshHostPort int, gitHomeDir string, sshServerCircuit *sshd.Circuit, pushLock sshd.RepositoryLock, cleanerRef cleaner.Ref) int {
3132
reg, router, ocxt := cookoo.Cookoo()
3233
log.SetFlags(0) // Time is captured elsewhere.
3334

@@ -58,7 +59,7 @@ func RunBuilder(sshHostIP string, sshHostPort int, sshServerCircuit *sshd.Circui
5859
// Start the SSH service.
5960
// TODO: We could refactor Serve to be a command, and then run this as
6061
// a route.
61-
if err := sshd.Serve(reg, router, sshServerCircuit, cxt); err != nil {
62+
if err := sshd.Serve(reg, router, sshServerCircuit, gitHomeDir, pushLock, cleanerRef, cxt); err != nil {
6263
clog.Errf(cxt, "SSH server failed: %s", err)
6364
return StatusLocalError
6465
}

pkg/cleaner/cleaner.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Package cleaner is a background process that compares the kubernetes namespace list with the folders in the local git home directory, deleting what's not in the namespace list
2+
package cleaner
3+
4+
import (
5+
"io/ioutil"
6+
"log"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
"sync"
11+
"time"
12+
13+
"github.com/deis/builder/pkg/k8s"
14+
"k8s.io/kubernetes/pkg/api"
15+
"k8s.io/kubernetes/pkg/fields"
16+
"k8s.io/kubernetes/pkg/labels"
17+
)
18+
19+
const (
20+
dotGitSuffix = ".git"
21+
)
22+
23+
type Ref struct {
24+
mut *sync.Mutex
25+
}
26+
27+
func NewRef() Ref {
28+
return Ref{mut: new(sync.Mutex)}
29+
}
30+
31+
func (c Ref) Lock() {
32+
c.mut.Lock()
33+
}
34+
35+
func (c Ref) Unlock() {
36+
c.mut.Unlock()
37+
}
38+
39+
// localDirs returns all of the local directories immediately under gitHome that filter returns true for. filter will receive only the names of each of the top level directories (not their fully qualified paths), and should return true if it should be included in the output
40+
func localDirs(gitHome string, filter func(string) bool) ([]string, error) {
41+
fileInfos, err := ioutil.ReadDir(gitHome)
42+
if err != nil {
43+
return nil, err
44+
}
45+
var ret []string
46+
for _, fileInfo := range fileInfos {
47+
nm := fileInfo.Name()
48+
if len(nm) <= 0 || nm == "." || !fileInfo.IsDir() {
49+
continue
50+
}
51+
if filter(nm) {
52+
ret = append(ret, filepath.Join(gitHome, nm))
53+
}
54+
}
55+
return ret, nil
56+
}
57+
58+
// getDiff gets the directories that are not in namespaceList
59+
func getDiff(namespaceList []api.Namespace, dirs []string) []string {
60+
var ret []string
61+
62+
// create a set of lowercase namespace names
63+
namespacesSet := make(map[string]struct{})
64+
for _, ns := range namespaceList {
65+
lowerName := strings.ToLower(ns.Name)
66+
namespacesSet[lowerName] = struct{}{}
67+
}
68+
69+
// get dirs not in the namespaces set
70+
for _, dir := range dirs {
71+
lowerName := strings.ToLower(dir)
72+
if _, ok := namespacesSet[lowerName]; !ok {
73+
ret = append(ret, lowerName)
74+
}
75+
}
76+
77+
return ret
78+
}
79+
80+
func stripSuffixes(strs []string, suffix string) []string {
81+
ret := make([]string, len(strs))
82+
for i, str := range strs {
83+
idx := strings.LastIndex(str, suffix)
84+
if idx >= 0 {
85+
ret[i] = str[:idx]
86+
} else {
87+
ret[i] = str
88+
}
89+
}
90+
return ret
91+
}
92+
93+
func dirHasGitSuffix(dir string) bool {
94+
return strings.HasSuffix(dir, dotGitSuffix)
95+
}
96+
97+
// Run starts the deleted app cleaner. Every pollSleepDuration, it compares the result of nsLister.List with the directories in the top level of gitHome on the local file system. On any error, it uses log messages to output a human readable description of what happened.
98+
func (c Ref) Run(gitHome string, nsLister k8s.NamespaceLister, ref Ref, pollSleepDuration time.Duration) error {
99+
for {
100+
nsList, err := nsLister.List(labels.Everything(), fields.Everything())
101+
if err != nil {
102+
log.Printf("Cleaner error listing namespaces (%s)", err)
103+
continue
104+
} else {
105+
lst := make([]string, len(nsList.Items))
106+
for i, ns := range nsList.Items {
107+
lst[i] = strings.ToLower(ns.Name)
108+
}
109+
}
110+
111+
gitDirs, err := localDirs(gitHome, dirHasGitSuffix)
112+
if err != nil {
113+
log.Printf("Cleaner error listing local git directories (%s)", err)
114+
continue
115+
}
116+
117+
gitDirs = stripSuffixes(gitDirs, dotGitSuffix)
118+
119+
appsToDelete := getDiff(nsList.Items, gitDirs)
120+
121+
for _, appToDelete := range appsToDelete {
122+
dirToDelete := appToDelete + dotGitSuffix
123+
ref.Lock()
124+
if err := os.RemoveAll(dirToDelete); err != nil {
125+
log.Printf("Cleaner error removing deleted app %s (%s)", dirToDelete, err)
126+
}
127+
ref.Unlock()
128+
}
129+
130+
time.Sleep(pollSleepDuration)
131+
}
132+
}

pkg/cleaner/cleaner_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package cleaner
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
9+
"github.com/arschles/assert"
10+
"k8s.io/kubernetes/pkg/api"
11+
)
12+
13+
func TestGetDiff(t *testing.T) {
14+
nsList := []api.Namespace{
15+
api.Namespace{ObjectMeta: api.ObjectMeta{Name: "app1"}},
16+
api.Namespace{ObjectMeta: api.ObjectMeta{Name: "app2"}},
17+
}
18+
dirList := []string{"app1", "app3"}
19+
diff := getDiff(nsList, dirList)
20+
assert.Equal(t, len(diff), 1, "number of items in the disjunction")
21+
}
22+
23+
func TestDirHasGitSuffix(t *testing.T) {
24+
assert.True(t, dirHasGitSuffix("a.git"), "'a.git' reported no git suffix")
25+
assert.False(t, dirHasGitSuffix("abc"), "'a' reported git suffix")
26+
}
27+
28+
func TestLocalDirs(t *testing.T) {
29+
wd, err := os.Getwd()
30+
assert.NoErr(t, err)
31+
pkgDir, err := filepath.Abs(wd + "/..")
32+
assert.NoErr(t, err)
33+
lDirs, err := localDirs(pkgDir, func(dir string) bool {
34+
// no directories with any dots in them
35+
return len(strings.Split(dir, ".")) == 1
36+
})
37+
assert.NoErr(t, err)
38+
39+
expectedPackages := map[string]int{
40+
pkgDir + "/cleaner": 1,
41+
pkgDir + "/conf": 1,
42+
pkgDir + "/controller": 1,
43+
pkgDir + "/env": 1,
44+
pkgDir + "/git": 1,
45+
pkgDir + "/gitreceive": 1,
46+
pkgDir + "/healthsrv": 1,
47+
pkgDir + "/k8s": 1,
48+
pkgDir + "/sshd": 1,
49+
}
50+
51+
actualPackages := map[string]int{}
52+
for _, lDir := range lDirs {
53+
actualPackages[lDir]++
54+
}
55+
assert.Equal(t, len(actualPackages), len(expectedPackages), "number of packages")
56+
for actualPackageName, actualNum := range actualPackages {
57+
if actualNum != 1 {
58+
t.Errorf("found %d %s packages", actualNum, actualPackageName)
59+
continue
60+
}
61+
expectedNum, ok := expectedPackages[actualPackageName]
62+
if !ok {
63+
t.Errorf("found unexpected package %s", actualPackageName)
64+
continue
65+
}
66+
if actualNum != expectedNum {
67+
t.Errorf("found %d %s packages, expected %d", actualNum, actualPackageName, expectedNum)
68+
continue
69+
}
70+
}
71+
}
72+
73+
func TestStripSuffixes(t *testing.T) {
74+
strs := []string{"a.git", "b.git", "c.git", "d"}
75+
newStrs := stripSuffixes(strs, dotGitSuffix)
76+
assert.Equal(t, len(newStrs), len(strs), "number of strings")
77+
for _, str := range newStrs {
78+
assert.False(t, strings.HasSuffix(str, dotGitSuffix), "string %s has suffix %s", str, dotGitSuffix)
79+
}
80+
}

pkg/k8s/namespace.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package k8s
2+
3+
import (
4+
"k8s.io/kubernetes/pkg/api"
5+
"k8s.io/kubernetes/pkg/fields"
6+
"k8s.io/kubernetes/pkg/labels"
7+
)
8+
9+
// NamespaceLister is a (k8s.io/kubernetes/pkg/client/unversioned).NamespaceInterface compatible interface which only has the List function. It's used in places that only need List to make them easier to test and more easily swappable with other implementations.
10+
//
11+
// Example usage:
12+
//
13+
// var nsl NamespaceLister
14+
// nsl = kubeClient.Namespaces()
15+
type NamespaceLister interface {
16+
List(labels.Selector, fields.Selector) (*api.NamespaceList, error)
17+
}

pkg/sshd/config.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
package sshd
22

3+
import (
4+
"time"
5+
)
6+
37
// Config represents the required SSH server configuration
48
type Config struct {
5-
SSHHostIP string `envconfig:"SSH_HOST_IP" default:"0.0.0.0" required:"true"`
6-
SSHHostPort int `envconfig:"SSH_HOST_PORT" default:"2223" required:"true"`
7-
HealthSrvPort int `envconfig:"HEALTH_SERVER_PORT" default:"8092"`
8-
HealthSrvTestStorageRegion string `envconfig:"HELATH_SERVER_TEST_STORAGE_REGION" default:"us-east-1"`
9+
SSHHostIP string `envconfig:"SSH_HOST_IP" default:"0.0.0.0" required:"true"`
10+
SSHHostPort int `envconfig:"SSH_HOST_PORT" default:"2223" required:"true"`
11+
HealthSrvPort int `envconfig:"HEALTH_SERVER_PORT" default:"8092"`
12+
HealthSrvTestStorageRegion string `envconfig:"HEALTH_SERVER_TEST_STORAGE_REGION" default:"us-east-1"`
13+
CleanerPollSleepDurationSec int `envconfig:"CLEANER_POLL_SLEEP_DURATION_SEC" default:"1"`
14+
}
15+
16+
func (c Config) CleanerPollSleepDuration() time.Duration {
17+
return time.Duration(c.CleanerPollSleepDurationSec) * time.Second
918
}

0 commit comments

Comments
 (0)