├── .gitignore ├── .travis.yml ├── GLOCKFILE ├── LICENSE ├── Makefile ├── README.md ├── backup.go ├── cmd └── commander │ ├── dump.go │ └── main.go ├── commander ├── app.go ├── config.go ├── hosts.go ├── pool.go ├── runtime.go ├── scheduler.go └── scheduler_test.go ├── config ├── app_config.go ├── app_config_test.go ├── backend.go ├── config.go ├── consul.go ├── dedupe_test.go ├── memory.go ├── notify.go ├── redis.go ├── redis_test.go ├── registration.go ├── store.go └── store_test.go ├── discovery ├── discovery.go └── shuttle.go ├── galaxy.go ├── log └── log.go ├── logo.jpg ├── runtime ├── aws.go └── runtime.go └── utils ├── ssh.go ├── utils.go ├── utils_test.go ├── vmap.go └── vmap_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | .vagrant 24 | dist 25 | *.tar.gz 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.5 4 | install: 5 | - go get github.com/robfig/glock 6 | - make deps 7 | script: 8 | - make all fmt test 9 | -------------------------------------------------------------------------------- /GLOCKFILE: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml f87ce853111478914f0bcffa34d43a93643e6eda 2 | github.com/codegangsta/cli 50c77ecec0068c9aef9d90ae0fd0fdf410041da3 3 | github.com/fatih/color 95b468b5f34882796c597b718955603a584a9bd4 4 | github.com/fsouza/go-dockerclient 64c100a0b566fb3569431b9416eb5a794a322981 5 | github.com/garyburd/redigo 535138d7bcd717d6531c701ef5933d98b1866257 6 | github.com/hashicorp/consul a02ba028156e7b4db52a1e090394568aa4a3def8 7 | github.com/litl/shuttle 2f96e5ace416402767cb59dca49e788f983fe35e 8 | github.com/ryanuber/columnize 44cb4788b2ec3c3d158dd3d1b50aba7d66f4b59a 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Litl, LLC. All rights reserved. 4 | Copyright (c) 2015 The Galaxy Authors. All rights reserved. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .SILENT : 2 | .PHONY : commander galaxy clean fmt test upload-release 3 | 4 | TAG:=`git describe --abbrev=0 --tags` 5 | LDFLAGS:=-X main.buildVersion=`git describe --long --tags` 6 | 7 | all: commander galaxy 8 | 9 | deps: 10 | glock sync github.com/litl/galaxy 11 | 12 | commander: 13 | echo "Building commander" 14 | go install -ldflags "$(LDFLAGS)" github.com/litl/galaxy/cmd/commander 15 | 16 | galaxy: 17 | echo "Building galaxy" 18 | go install -ldflags "$(LDFLAGS)" github.com/litl/galaxy 19 | 20 | clean: dist-clean 21 | rm -f $(GOPATH)/bin/commander 22 | rm -f $(GOPATH)/bin/galaxy 23 | 24 | fmt: 25 | go fmt github.com/litl/galaxy/... 26 | 27 | test: 28 | go test -v github.com/litl/galaxy/... 29 | 30 | dist-clean: 31 | rm -rf dist 32 | rm -f galaxy-*.tar.gz 33 | 34 | dist-init: 35 | mkdir -p dist/$$GOOS/$$GOARCH 36 | 37 | dist-build: dist-init 38 | echo "Compiling $$GOOS/$$GOARCH" 39 | go build -ldflags "$(LDFLAGS)" -o dist/$$GOOS/$$GOARCH/galaxy github.com/litl/galaxy 40 | go build -ldflags "$(LDFLAGS)" -o dist/$$GOOS/$$GOARCH/commander github.com/litl/galaxy/cmd/commander 41 | 42 | dist-linux-amd64: 43 | export GOOS="linux"; \ 44 | export GOARCH="amd64"; \ 45 | $(MAKE) dist-build 46 | 47 | dist-linux-386: 48 | export GOOS="linux"; \ 49 | export GOARCH="386"; \ 50 | $(MAKE) dist-build 51 | 52 | dist-darwin-amd64: 53 | export GOOS="darwin"; \ 54 | export GOARCH="amd64"; \ 55 | $(MAKE) dist-build 56 | 57 | dist: dist-clean dist-init dist-linux-amd64 dist-linux-386 dist-darwin-amd64 58 | 59 | release-tarball: 60 | echo "Building $$GOOS-$$GOARCH-$(TAG).tar.gz" 61 | GZIP=-9 tar -cvzf galaxy-$$GOOS-$$GOARCH-$(TAG).tar.gz -C dist/$$GOOS/$$GOARCH galaxy commander >/dev/null 2>&1 62 | 63 | release-linux-amd64: 64 | export GOOS="linux"; \ 65 | export GOARCH="amd64"; \ 66 | $(MAKE) release-tarball 67 | 68 | release-linux-386: 69 | export GOOS="linux"; \ 70 | export GOARCH="386"; \ 71 | $(MAKE) release-tarball 72 | 73 | release-darwin-amd64: 74 | export GOOS="darwin"; \ 75 | export GOARCH="amd64"; \ 76 | $(MAKE) release-tarball 77 | 78 | release: deps dist release-linux-amd64 release-linux-386 release-darwin-amd64 79 | 80 | upload-release: 81 | aws s3 cp galaxy-darwin-amd64-$(TAG).tar.gz s3://litl-package-repo/galaxy/galaxy-darwin-amd64-$(TAG).tar.gz --acl public-read 82 | aws s3 cp galaxy-linux-amd64-$(TAG).tar.gz s3://litl-package-repo/galaxy/galaxy-linux-amd64-$(TAG).tar.gz --acl public-read 83 | aws s3 cp galaxy-linux-386-$(TAG).tar.gz s3://litl-package-repo/galaxy/galaxy-linux-386-$(TAG).tar.gz --acl public-read 84 | echo https://s3.amazonaws.com/litl-package-repo/galaxy/galaxy-darwin-amd64-$(TAG).tar.gz 85 | echo https://s3.amazonaws.com/litl-package-repo/galaxy/galaxy-linux-amd64-$(TAG).tar.gz 86 | echo https://s3.amazonaws.com/litl-package-repo/galaxy/galaxy-linux-386-$(TAG).tar.gz 87 | 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | galaxy 2 | ====== 3 | 4 | *Docker Micro-PaaS* 5 | 6 | ![galaxy](logo.jpg) 7 | 8 | === 9 | 10 | galaxy is a micro-paas designed for running 12-factor style, stateless, microservices 11 | within Docker containers while being lightweight and simple to operate. It handles the deployment, 12 | configuration and orchestration of Docker containers across multiple hosts. 13 | 14 | It is ideally suited for running Docker containers: 15 | * Alongside existing applications while transitioning to containers 16 | * On clusters of 10's-100's of hosts 17 | * On existing or new infrastructure you are already using 18 | * For HTTP based micro-services 19 | 20 | ### Features: 21 | 22 | * Minimal dependencies (two binaries and redis) 23 | * Automatic service registration, discovery and proxying of registered services. 24 | * Virtual Host HTTP(S) proxying 25 | * Container scheduling and scaling across hosts 26 | * Heroku style config variable interface 27 | * Container contraints (CPU/Mem) 28 | 29 | There are two sub-projects: commander and shuttle. 30 | 31 | * Commander - Container deployment and service discovery. 32 | * [Shuttle](https://github.com/litl/shuttle) - An HTTP/TCP proxy that can be configured through a HTTP based API. 33 | 34 | ## Getting Started 35 | 36 | (These assume that Docker is running on your local host. If you're 37 | running it remotely, or via something like boot2docker, you'll need to 38 | adjust the `GALAXY_REGISTRY_URL` below and provide the `-host-ip` 39 | argument to `commander agent`.) 40 | 41 | To setup a single host environment, run the following: 42 | 43 | ``` 44 | $ docker run -d --name redis -p 6379:6379 redis 45 | $ export GALAXY_REGISTRY_URL=redis://127.0.0.1:6379 46 | $ export GALAXY_ENV=local 47 | $ export GALAXY_POOL=web 48 | $ commander agent 49 | ``` 50 | 51 | To create a new app for _nginx_: 52 | 53 | ``` 54 | $ commander app:create nginx 55 | ``` 56 | 57 | To deploy a latest official nginx image to our _nginx_ app: 58 | 59 | ``` 60 | $ commander app:deploy nginx nginx 61 | ``` 62 | 63 | Finally, we need to assign this app to our default `web` pool: 64 | 65 | ``` 66 | $ commander app:assign nginx 67 | ``` 68 | 69 | You should see nginx started by the `commander agent` process. 70 | 71 | ## Exposing Services 72 | 73 | To expose the nginx app, we need to run shuttle to handle request routing: 74 | 75 | Start shuttle: 76 | 77 | ``` 78 | $ shuttle -http 0.0.0.0:8080 79 | ``` 80 | 81 | This starts a shuttle with an admin server on 127.0.0.1:9090 (the 82 | default) and an HTTP server on 0.0.0.0:8080. 83 | 84 | Next, stop your old commander agent and restart it, pointing to the 85 | shuttle admin address: 86 | 87 | ``` 88 | $ commander -shuttle-addr 127.0.0.1:9090 agent 89 | ``` 90 | 91 | Assign a service port to nginx: 92 | 93 | ``` 94 | $ commander runtime:set -port 8888 nginx 95 | ``` 96 | 97 | You should now be able to access the nginx app on host port 8888: 98 | ``` 99 | $ curl localhost:8888 100 | ``` 101 | 102 | Add a virtual host: 103 | ``` 104 | $ commander runtime:set -vhost my.domain nginx 105 | $ curl -v my.domain:8080 106 | ``` 107 | 108 | ## Dev Setup 109 | 110 | You need to have a docker 1.4.1+ and golang 1.4. 111 | 112 | 1. Install [glock](https://github.com/robfig/glock) 113 | 2. make deps 114 | 3. make 115 | 116 | ## License 117 | 118 | MIT 119 | 120 | -------------------------------------------------------------------------------- /backup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "time" 9 | 10 | "github.com/codegangsta/cli" 11 | gconfig "github.com/litl/galaxy/config" 12 | "github.com/litl/galaxy/log" 13 | "github.com/litl/galaxy/utils" 14 | ) 15 | 16 | type backupData struct { 17 | Time time.Time 18 | Apps []*appCfg 19 | } 20 | 21 | // Serialized backup format 22 | type appCfg struct { 23 | Name string 24 | Version string 25 | Env map[string]string 26 | Ports map[string]string 27 | } 28 | 29 | // Backup app config to a file or STDOUT 30 | func appBackup(c *cli.Context) { 31 | initStore(c) 32 | 33 | env := utils.GalaxyEnv(c) 34 | if env == "" { 35 | log.Fatal("ERROR: env is required. Pass --env or set GALAXY_ENV") 36 | } 37 | 38 | backup := &backupData{ 39 | Time: time.Now(), 40 | } 41 | 42 | toBackup := c.Args() 43 | 44 | if len(toBackup) == 0 { 45 | appList, err := configStore.ListApps(env) 46 | if err != nil { 47 | log.Fatalf("ERROR: %s\n", err) 48 | } 49 | 50 | for _, app := range appList { 51 | toBackup = append(toBackup, app.Name()) 52 | } 53 | } 54 | 55 | errCount := 0 56 | for _, app := range toBackup { 57 | data, err := getAppBackup(app, env) 58 | if err != nil { 59 | // log errors and continue 60 | log.Errorf("ERROR: %s [%s]", err, app) 61 | errCount++ 62 | continue 63 | } 64 | backup.Apps = append(backup.Apps, data) 65 | } 66 | 67 | if errCount > 0 { 68 | fmt.Printf("WARNING: backup completed with %d errors\n", errCount) 69 | defer os.Exit(errCount) 70 | } 71 | 72 | j, err := json.MarshalIndent(backup, "", " ") 73 | if err != nil { 74 | log.Fatal(err) 75 | } 76 | 77 | fileName := c.String("file") 78 | if fileName != "" { 79 | if err := ioutil.WriteFile(fileName, j, 0666); err != nil { 80 | log.Fatal(err) 81 | } 82 | return 83 | } 84 | 85 | os.Stdout.Write(j) 86 | } 87 | 88 | func getAppBackup(app, env string) (*appCfg, error) { 89 | svcCfg, err := configStore.GetApp(app, env) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | if svcCfg == nil { 95 | return nil, fmt.Errorf("app not found") 96 | } 97 | 98 | backup := &appCfg{ 99 | Name: app, 100 | Version: svcCfg.Version(), 101 | Env: svcCfg.Env(), 102 | } 103 | return backup, nil 104 | } 105 | 106 | // restore an app's config from backup 107 | func appRestore(c *cli.Context) { 108 | initStore(c) 109 | 110 | var err error 111 | var rawBackup []byte 112 | 113 | fileName := c.String("file") 114 | if fileName != "" { 115 | rawBackup, err = ioutil.ReadFile(fileName) 116 | if err != nil { 117 | log.Fatal(err) 118 | } 119 | } else { 120 | log.Println("Reading backup from STDIN") 121 | rawBackup, err = ioutil.ReadAll(os.Stdin) 122 | if err != nil { 123 | log.Fatal(err) 124 | } 125 | } 126 | 127 | backup := &backupData{} 128 | if err := json.Unmarshal(rawBackup, backup); err != nil { 129 | log.Fatal(err) 130 | } 131 | 132 | fmt.Println("Found backup from ", backup.Time) 133 | 134 | var toRestore []*appCfg 135 | 136 | if apps := c.Args(); len(apps) > 0 { 137 | for _, app := range apps { 138 | found := false 139 | for _, bkup := range backup.Apps { 140 | if bkup.Name == app { 141 | toRestore = append(toRestore, bkup) 142 | found = true 143 | break 144 | } 145 | } 146 | if !found { 147 | log.Fatalf("no backup found for '%s'\n", app) 148 | } 149 | 150 | } 151 | } else { 152 | toRestore = backup.Apps 153 | } 154 | 155 | // check for conflicts 156 | // NOTE: there is still a race here if an app is created after this check 157 | if !c.Bool("force") { 158 | needForce := false 159 | for _, bkup := range toRestore { 160 | exists, err := configStore.AppExists(bkup.Name, utils.GalaxyEnv(c)) 161 | if err != nil { 162 | log.Fatal(err) 163 | } 164 | if exists { 165 | log.Warnf("Cannot restore over existing app '%s'", bkup.Name) 166 | needForce = true 167 | } 168 | } 169 | if needForce { 170 | log.Fatal("Use -force to overwrite") 171 | } 172 | } 173 | 174 | loggedErr := false 175 | for _, bkup := range toRestore { 176 | if err := restoreApp(bkup, utils.GalaxyEnv(c)); err != nil { 177 | log.Errorf("%s", err) 178 | loggedErr = true 179 | } 180 | } 181 | 182 | if loggedErr { 183 | // This is mostly to give a non-zero exit status 184 | log.Fatal("Error occured during restore") 185 | } 186 | } 187 | 188 | func restoreApp(bkup *appCfg, env string) error { 189 | fmt.Println("restoring", bkup.Name) 190 | 191 | var svcCfg gconfig.App 192 | 193 | exists, err := configStore.AppExists(bkup.Name, env) 194 | if err != nil { 195 | return err 196 | } 197 | 198 | if exists { 199 | svcCfg, err = configStore.GetApp(bkup.Name, env) 200 | if err != nil { 201 | return err 202 | } 203 | } 204 | 205 | if svcCfg == nil { 206 | svcCfg = configStore.NewAppConfig(bkup.Name, bkup.Version) 207 | } 208 | 209 | for k, v := range bkup.Env { 210 | svcCfg.EnvSet(k, v) 211 | } 212 | 213 | _, err = configStore.UpdateApp(svcCfg, env) 214 | return err 215 | } 216 | -------------------------------------------------------------------------------- /cmd/commander/dump.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | 9 | "github.com/litl/galaxy/config" 10 | ) 11 | 12 | type dumpConfig struct { 13 | Pools []string 14 | Hosts []config.HostInfo 15 | Configs []config.AppDefinition 16 | Regs []config.ServiceRegistration 17 | } 18 | 19 | // Dump everything related to a single environment from galaxy to stdout, 20 | // including current runtime config, hosts, IPs etc. 21 | // This isn't really useful other than to sync between config backends, but we 22 | // can probably convert this to a better backup once we stabilize the code some 23 | // more. 24 | func dump(env string) { 25 | envDump := &dumpConfig{ 26 | Configs: []config.AppDefinition{}, 27 | Regs: []config.ServiceRegistration{}, 28 | } 29 | 30 | pools, err := configStore.ListPools(env) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | envDump.Pools = pools 36 | 37 | for _, pool := range pools { 38 | hosts, err := configStore.ListHosts(env, pool) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | for _, host := range hosts { 43 | host.Pool = pool 44 | envDump.Hosts = append(envDump.Hosts, host) 45 | } 46 | } 47 | 48 | apps, err := configStore.ListApps(env) 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | 53 | for _, app := range apps { 54 | // AppDefinition is intended to be serializable itself 55 | if ad, ok := app.(*config.AppDefinition); ok { 56 | envDump.Configs = append(envDump.Configs, *ad) 57 | continue 58 | } 59 | 60 | // otherwise, manually convert the App to an AppDefinition 61 | ad := config.AppDefinition{ 62 | AppName: app.Name(), 63 | Image: app.Version(), 64 | ImageID: app.VersionID(), 65 | Environment: app.Env(), 66 | } 67 | 68 | for _, pool := range app.RuntimePools() { 69 | ad.SetProcesses(pool, app.GetProcesses(pool)) 70 | ad.SetMemory(pool, app.GetMemory(pool)) 71 | ad.SetCPUShares(pool, app.GetCPUShares(pool)) 72 | } 73 | 74 | envDump.Configs = append(envDump.Configs, ad) 75 | } 76 | 77 | // The registrations are temporary, but dump them anyway, so we can try and 78 | // convert an environment by keeping the runtime config in sync. 79 | regs, err := configStore.ListRegistrations(env) 80 | if err != nil { 81 | log.Fatal(err) 82 | } 83 | envDump.Regs = append(envDump.Regs, regs...) 84 | 85 | js, err := json.MarshalIndent(envDump, "", " ") 86 | if err != nil { 87 | log.Fatal(err) 88 | } 89 | 90 | os.Stdout.Write(js) 91 | } 92 | 93 | // Restore everything we can from a Galaxy dump on stdin. 94 | // This probably will panic if not using consul 95 | func restore(env string) { 96 | js, err := ioutil.ReadAll(os.Stdin) 97 | if err != nil { 98 | log.Fatal(err) 99 | } 100 | 101 | envDump := dumpConfig{} 102 | err = json.Unmarshal(js, &envDump) 103 | if err != nil { 104 | log.Fatal(err) 105 | } 106 | 107 | for _, pool := range envDump.Pools { 108 | _, err := configStore.CreatePool(env, pool) 109 | if err != nil { 110 | log.Println(err) 111 | } 112 | } 113 | 114 | for _, appDef := range envDump.Configs { 115 | _, err := configStore.UpdateApp(&appDef, env) 116 | if err != nil { 117 | log.Println(err) 118 | } 119 | } 120 | 121 | for _, hostInfo := range envDump.Hosts { 122 | err := configStore.UpdateHost(env, pool, hostInfo) 123 | if err != nil { 124 | log.Println(err) 125 | } 126 | } 127 | 128 | for _, reg := range envDump.Regs { 129 | err := configStore.Backend.RegisterService(env, reg.Pool, ®) 130 | if err != nil { 131 | log.Println(err) 132 | } 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /commander/app.go: -------------------------------------------------------------------------------- 1 | package commander 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/litl/galaxy/config" 9 | "github.com/litl/galaxy/log" 10 | "github.com/litl/galaxy/runtime" 11 | "github.com/litl/galaxy/utils" 12 | "github.com/ryanuber/columnize" 13 | ) 14 | 15 | func AppList(configStore *config.Store, env string) error { 16 | 17 | envs := []string{env} 18 | 19 | if env == "" { 20 | var err error 21 | envs, err = configStore.ListEnvs() 22 | if err != nil { 23 | return err 24 | } 25 | } 26 | 27 | columns := []string{"NAME | ENV | VERSION | IMAGE ID | CONFIG | POOLS "} 28 | 29 | for _, env := range envs { 30 | 31 | appList, err := configStore.ListApps(env) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | pools, err := configStore.ListPools(env) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | for _, app := range appList { 42 | name := app.Name() 43 | versionDeployed := app.Version() 44 | versionID := app.VersionID() 45 | if len(versionID) > 12 { 46 | versionID = versionID[:12] 47 | } 48 | 49 | assignments := []string{} 50 | for _, pool := range pools { 51 | aa, err := configStore.ListAssignments(env, pool) 52 | if err != nil { 53 | return err 54 | } 55 | if utils.StringInSlice(app.Name(), aa) { 56 | assignments = append(assignments, pool) 57 | } 58 | } 59 | 60 | columns = append(columns, strings.Join([]string{ 61 | name, 62 | env, 63 | versionDeployed, 64 | versionID, 65 | strconv.FormatInt(app.ID(), 10), 66 | strings.Join(assignments, ","), 67 | }, " | ")) 68 | } 69 | } 70 | output := columnize.SimpleFormat(columns) 71 | fmt.Println(output) 72 | return nil 73 | } 74 | 75 | func AppCreate(configStore *config.Store, app, env string) error { 76 | // Don't allow creating runtime hosts entries 77 | if app == "hosts" { 78 | return fmt.Errorf("could not create app: %s", app) 79 | } 80 | 81 | created, err := configStore.CreateApp(app, env) 82 | 83 | if err != nil { 84 | return fmt.Errorf("could not create app: %s", err) 85 | } 86 | 87 | if created { 88 | log.Printf("Created %s in env %s.\n", app, env) 89 | } else { 90 | log.Printf("%s already exists in in env %s.", app, env) 91 | } 92 | return nil 93 | } 94 | 95 | func AppDelete(configStore *config.Store, app, env string) error { 96 | 97 | // Don't allow deleting runtime hosts entries 98 | if app == "hosts" || app == "pools" { 99 | return fmt.Errorf("could not delete app: %s", app) 100 | } 101 | 102 | deleted, err := configStore.DeleteApp(app, env) 103 | if err != nil { 104 | return fmt.Errorf("could not delete app: %s", err) 105 | } 106 | 107 | if deleted { 108 | log.Printf("Deleted %s from env %s.\n", app, env) 109 | } else { 110 | log.Printf("%s does not exists in env %s.\n", app, env) 111 | } 112 | return nil 113 | } 114 | 115 | func AppDeploy(configStore *config.Store, serviceRuntime *runtime.ServiceRuntime, app, env, version string) error { 116 | log.Printf("Pulling image %s...", version) 117 | 118 | image, err := serviceRuntime.PullImage(version, "") 119 | if image == nil || err != nil { 120 | return fmt.Errorf("unable to pull %s. Has it been released yet?", version) 121 | } 122 | 123 | svcCfg, err := configStore.GetApp(app, env) 124 | if err != nil { 125 | return fmt.Errorf("unable to deploy app: %s.", err) 126 | } 127 | 128 | if svcCfg == nil { 129 | return fmt.Errorf("app %s does not exist. Create it first.", app) 130 | } 131 | 132 | svcCfg.SetVersion(version) 133 | svcCfg.SetVersionID(utils.StripSHA(image.ID)) 134 | 135 | updated, err := configStore.UpdateApp(svcCfg, env) 136 | if err != nil { 137 | return fmt.Errorf("could not store version: %s", err) 138 | } 139 | if !updated { 140 | return fmt.Errorf("%s NOT deployed.", version) 141 | } 142 | log.Printf("Deployed %s.\n", version) 143 | return nil 144 | } 145 | 146 | func AppRestart(Store *config.Store, app, env string) error { 147 | err := Store.NotifyRestart(app, env) 148 | if err != nil { 149 | return fmt.Errorf("could not restart %s: %s", app, err) 150 | } 151 | return nil 152 | } 153 | 154 | func AppRun(configStore *config.Store, serviceRuntime *runtime.ServiceRuntime, app, env string, args []string) error { 155 | appCfg, err := configStore.GetApp(app, env) 156 | if err != nil { 157 | return fmt.Errorf("unable to run command: %s.", err) 158 | 159 | } 160 | 161 | _, err = serviceRuntime.RunCommand(env, appCfg, args) 162 | if err != nil { 163 | return fmt.Errorf("could not start container: %s", err) 164 | } 165 | return nil 166 | } 167 | 168 | func AppShell(configStore *config.Store, serviceRuntime *runtime.ServiceRuntime, app, env, pool string) error { 169 | appCfg, err := configStore.GetApp(app, env) 170 | if err != nil { 171 | return fmt.Errorf("unable to run command: %s.", err) 172 | } 173 | 174 | err = serviceRuntime.StartInteractive(env, pool, appCfg) 175 | if err != nil { 176 | return fmt.Errorf("could not start container: %s", err) 177 | } 178 | return nil 179 | } 180 | 181 | func AppAssign(configStore *config.Store, app, env, pool string) error { 182 | // Don't allow deleting runtime hosts entries 183 | if app == "hosts" || app == "pools" { 184 | return fmt.Errorf("invalid app name: %s", app) 185 | } 186 | 187 | exists, err := configStore.PoolExists(env, pool) 188 | if err != nil { 189 | return err 190 | } 191 | 192 | if !exists { 193 | log.Warnf("WARN: Pool %s does not exist.", pool) 194 | } 195 | 196 | created, err := configStore.AssignApp(app, env, pool) 197 | 198 | if err != nil { 199 | return err 200 | } 201 | 202 | if created { 203 | log.Printf("Assigned %s in env %s to pool %s.\n", app, env, pool) 204 | } else { 205 | log.Printf("%s already assigned to pool %s in env %s.\n", app, pool, env) 206 | } 207 | return nil 208 | } 209 | 210 | func AppUnassign(configStore *config.Store, app, env, pool string) error { 211 | // Don't allow deleting runtime hosts entries 212 | if app == "hosts" || app == "pools" { 213 | return fmt.Errorf("invalid app name: %s", app) 214 | } 215 | 216 | deleted, err := configStore.UnassignApp(app, env, pool) 217 | if err != nil { 218 | return err 219 | } 220 | 221 | if deleted { 222 | log.Printf("Unassigned %s in env %s from pool %s\n", app, env, pool) 223 | } else { 224 | log.Printf("%s could not be unassigned.\n", pool) 225 | } 226 | return nil 227 | } 228 | -------------------------------------------------------------------------------- /commander/config.go: -------------------------------------------------------------------------------- 1 | package commander 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "sort" 8 | "strings" 9 | 10 | "github.com/litl/galaxy/config" 11 | "github.com/litl/galaxy/log" 12 | ) 13 | 14 | func ConfigList(configStore *config.Store, app, env string) error { 15 | 16 | cfg, err := configStore.GetApp(app, env) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | if cfg == nil { 22 | return fmt.Errorf("unable to list config for %s.", app) 23 | } 24 | 25 | keys := sort.StringSlice{"ENV"} 26 | for k, _ := range cfg.Env() { 27 | keys = append(keys, k) 28 | } 29 | 30 | keys.Sort() 31 | 32 | for _, k := range keys { 33 | if k == "ENV" { 34 | log.Printf("%s=%s\n", k, env) 35 | continue 36 | } 37 | fmt.Printf("%s=%s\n", k, cfg.Env()[k]) 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func ConfigSet(configStore *config.Store, app, env string, envVars []string) error { 44 | 45 | if len(envVars) == 0 { 46 | bytes, err := ioutil.ReadAll(os.Stdin) 47 | if err != nil { 48 | return err 49 | 50 | } 51 | envVars = strings.Split(string(bytes), "\n") 52 | } 53 | 54 | if len(envVars) == 0 { 55 | return fmt.Errorf("no config values specified.") 56 | } 57 | 58 | svcCfg, err := configStore.GetApp(app, env) 59 | if err != nil { 60 | return fmt.Errorf("unable to set config: %s.", err) 61 | } 62 | 63 | if svcCfg == nil { 64 | svcCfg = configStore.NewAppConfig(app, "") 65 | } 66 | 67 | updated := false 68 | for _, arg := range envVars { 69 | 70 | if strings.TrimSpace(arg) == "" { 71 | continue 72 | } 73 | 74 | if !strings.Contains(arg, "=") { 75 | return fmt.Errorf("bad config variable format: %s", arg) 76 | } 77 | 78 | sep := strings.Index(arg, "=") 79 | k := strings.ToUpper(strings.TrimSpace(arg[0:sep])) 80 | v := strings.TrimSpace(arg[sep+1:]) 81 | if k == "ENV" { 82 | log.Warnf("%s cannot be updated.", k) 83 | continue 84 | } 85 | 86 | log.Printf("%s=%s\n", k, v) 87 | svcCfg.EnvSet(k, v) 88 | updated = true 89 | } 90 | 91 | if !updated { 92 | return fmt.Errorf("configuration NOT changed for %s", app) 93 | } 94 | 95 | updated, err = configStore.UpdateApp(svcCfg, env) 96 | if err != nil { 97 | return fmt.Errorf("unable to set config: %s.", err) 98 | } 99 | 100 | if !updated { 101 | return fmt.Errorf("configuration NOT changed for %s", app) 102 | } 103 | log.Printf("Configuration changed for %s. v%d\n", app, svcCfg.ID()) 104 | return nil 105 | } 106 | 107 | func ConfigGet(configStore *config.Store, app, env string, envVars []string) error { 108 | 109 | cfg, err := configStore.GetApp(app, env) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | for _, arg := range envVars { 115 | fmt.Printf("%s=%s\n", strings.ToUpper(arg), cfg.Env()[strings.ToUpper(arg)]) 116 | } 117 | return nil 118 | } 119 | 120 | func ConfigUnset(configStore *config.Store, app, env string, envVars []string) error { 121 | 122 | if len(envVars) == 0 { 123 | return fmt.Errorf("no config values specified.") 124 | } 125 | 126 | svcCfg, err := configStore.GetApp(app, env) 127 | if err != nil { 128 | return fmt.Errorf("unable to unset config: %s.", err) 129 | } 130 | 131 | updated := false 132 | for _, arg := range envVars { 133 | k := strings.ToUpper(strings.TrimSpace(arg)) 134 | if k == "ENV" || svcCfg.EnvGet(k) == "" { 135 | log.Warnf("%s cannot be unset.", k) 136 | continue 137 | } 138 | 139 | log.Printf("%s\n", k) 140 | svcCfg.EnvSet(strings.ToUpper(arg), "") 141 | updated = true 142 | } 143 | 144 | if !updated { 145 | return fmt.Errorf("Configuration NOT changed for %s", app) 146 | } 147 | 148 | updated, err = configStore.UpdateApp(svcCfg, env) 149 | if err != nil { 150 | return fmt.Errorf("ERROR: Unable to unset config: %s.", err) 151 | 152 | } 153 | 154 | if !updated { 155 | return fmt.Errorf("Configuration NOT changed for %s", app) 156 | 157 | } 158 | log.Printf("Configuration changed for %s. v%d.\n", app, svcCfg.ID()) 159 | return nil 160 | } 161 | -------------------------------------------------------------------------------- /commander/hosts.go: -------------------------------------------------------------------------------- 1 | package commander 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/litl/galaxy/config" 8 | "github.com/ryanuber/columnize" 9 | ) 10 | 11 | func HostsList(configStore *config.Store, env, pool string) error { 12 | 13 | envs := []string{env} 14 | 15 | if env == "" { 16 | var err error 17 | envs, err = configStore.ListEnvs() 18 | if err != nil { 19 | return err 20 | } 21 | } 22 | 23 | columns := []string{"ENV | POOL | HOST IP "} 24 | 25 | for _, env := range envs { 26 | 27 | var err error 28 | pools := []string{pool} 29 | if pool == "" { 30 | pools, err = configStore.ListPools(env) 31 | if err != nil { 32 | return err 33 | } 34 | } 35 | 36 | for _, pool := range pools { 37 | 38 | hosts, err := configStore.ListHosts(env, pool) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | if len(hosts) == 0 { 44 | columns = append(columns, strings.Join([]string{ 45 | env, 46 | pool, 47 | "", 48 | }, " | ")) 49 | continue 50 | } 51 | for _, p := range hosts { 52 | columns = append(columns, strings.Join([]string{ 53 | env, 54 | pool, 55 | p.HostIP, 56 | }, " | ")) 57 | } 58 | } 59 | } 60 | output := columnize.SimpleFormat(columns) 61 | fmt.Println(output) 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /commander/pool.go: -------------------------------------------------------------------------------- 1 | package commander 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/litl/galaxy/config" 8 | "github.com/ryanuber/columnize" 9 | ) 10 | 11 | // TODO: shouldn't the command cmd be printing the output, and not the package? 12 | // The app, config, host, and runtime sections all do this too. (otherwise we 13 | // should just combine the two packages). And why do we print the output here, 14 | // but print the error in main??? 15 | func ListPools(configStore *config.Store, env string) error { 16 | var envs []string 17 | var err error 18 | 19 | if env != "" { 20 | envs = []string{env} 21 | } else { 22 | envs, err = configStore.ListEnvs() 23 | if err != nil { 24 | return err 25 | } 26 | 27 | } 28 | 29 | columns := []string{"ENV | POOL | APPS "} 30 | 31 | for _, env := range envs { 32 | 33 | pools, err := configStore.ListPools(env) 34 | if err != nil { 35 | return fmt.Errorf("ERROR: cannot list pools: %s", err) 36 | } 37 | 38 | if len(pools) == 0 { 39 | columns = append(columns, strings.Join([]string{ 40 | env, 41 | "", 42 | ""}, " | ")) 43 | continue 44 | } 45 | 46 | for _, pool := range pools { 47 | 48 | assigments, err := configStore.ListAssignments(env, pool) 49 | if err != nil { 50 | fmt.Printf("ERROR: cannot list pool assignments for %s/%s: %s", env, pool, err) 51 | } 52 | 53 | columns = append(columns, strings.Join([]string{ 54 | env, 55 | pool, 56 | strings.Join(assigments, ",")}, " | ")) 57 | 58 | } 59 | } 60 | fmt.Println(columnize.SimpleFormat(columns)) 61 | return nil 62 | } 63 | 64 | // Create a pool for an environment 65 | func PoolCreate(configStore *config.Store, env, pool string) error { 66 | exists, err := configStore.PoolExists(env, pool) 67 | if err != nil { 68 | return err 69 | } else if exists { 70 | return fmt.Errorf("pool '%s' exists", pool) 71 | } 72 | 73 | _, err = configStore.CreatePool(pool, env) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | return nil 79 | } 80 | 81 | func PoolDelete(configStore *config.Store, env, pool string) error { 82 | exists, err := configStore.PoolExists(env, pool) 83 | if err != nil { 84 | return err 85 | } else if !exists { 86 | return fmt.Errorf("pool '%s' does not exist", pool) 87 | } 88 | 89 | empty, err := configStore.DeletePool(pool, env) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | if !empty { 95 | return fmt.Errorf("pool '%s' is not epmty", pool) 96 | } 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /commander/runtime.go: -------------------------------------------------------------------------------- 1 | package commander 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/litl/galaxy/config" 9 | "github.com/litl/galaxy/utils" 10 | "github.com/ryanuber/columnize" 11 | ) 12 | 13 | type RuntimeOptions struct { 14 | Ps int 15 | Memory string 16 | CPUShares string 17 | VirtualHost string 18 | Port string 19 | MaintenanceMode string 20 | } 21 | 22 | func RuntimeList(configStore *config.Store, app, env, pool string) error { 23 | 24 | envs := []string{env} 25 | 26 | if env == "" { 27 | var err error 28 | envs, err = configStore.ListEnvs() 29 | if err != nil { 30 | return err 31 | } 32 | } 33 | 34 | columns := []string{"ENV | NAME | POOL | PS | MEM | VHOSTS | PORT | MAINT"} 35 | 36 | for _, env := range envs { 37 | 38 | appList, err := configStore.ListApps(env) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | for _, appCfg := range appList { 44 | 45 | if app != "" && appCfg.Name() != app { 46 | continue 47 | } 48 | 49 | for _, p := range appCfg.RuntimePools() { 50 | 51 | if pool != "" && p != pool { 52 | continue 53 | } 54 | 55 | name := appCfg.Name() 56 | ps := appCfg.GetProcesses(p) 57 | mem := appCfg.GetMemory(p) 58 | 59 | columns = append(columns, strings.Join([]string{ 60 | env, 61 | name, 62 | p, 63 | strconv.FormatInt(int64(ps), 10), 64 | mem, 65 | appCfg.Env()["VIRTUAL_HOST"], 66 | appCfg.Env()["GALAXY_PORT"], 67 | fmt.Sprint(appCfg.GetMaintenanceMode(p)), 68 | }, " | ")) 69 | } 70 | } 71 | } 72 | output := columnize.SimpleFormat(columns) 73 | fmt.Println(output) 74 | return nil 75 | 76 | } 77 | 78 | func RuntimeSet(configStore *config.Store, app, env, pool string, options RuntimeOptions) (bool, error) { 79 | 80 | cfg, err := configStore.GetApp(app, env) 81 | if err != nil { 82 | return false, err 83 | } 84 | 85 | if options.Ps != 0 && options.Ps != cfg.GetProcesses(pool) { 86 | cfg.SetProcesses(pool, options.Ps) 87 | } 88 | 89 | if options.Memory != "" && options.Memory != cfg.GetMemory(pool) { 90 | cfg.SetMemory(pool, options.Memory) 91 | } 92 | 93 | vhosts := []string{} 94 | vhostsFromEnv := cfg.Env()["VIRTUAL_HOST"] 95 | if vhostsFromEnv != "" { 96 | vhosts = strings.Split(cfg.Env()["VIRTUAL_HOST"], ",") 97 | } 98 | 99 | if options.VirtualHost != "" && !utils.StringInSlice(options.VirtualHost, vhosts) { 100 | vhosts = append(vhosts, options.VirtualHost) 101 | cfg.EnvSet("VIRTUAL_HOST", strings.Join(vhosts, ",")) 102 | } 103 | 104 | if options.Port != "" { 105 | cfg.EnvSet("GALAXY_PORT", options.Port) 106 | } 107 | 108 | if options.MaintenanceMode != "" { 109 | b, err := strconv.ParseBool(options.MaintenanceMode) 110 | if err != nil { 111 | return false, err 112 | } 113 | 114 | cfg.SetMaintenanceMode(pool, b) 115 | } 116 | 117 | return configStore.UpdateApp(cfg, env) 118 | } 119 | 120 | func RuntimeUnset(configStore *config.Store, app, env, pool string, options RuntimeOptions) (bool, error) { 121 | 122 | cfg, err := configStore.GetApp(app, env) 123 | if err != nil { 124 | return false, err 125 | } 126 | 127 | if options.Ps != 0 { 128 | cfg.SetProcesses(pool, -1) 129 | } 130 | 131 | if options.Memory != "" { 132 | cfg.SetMemory(pool, "") 133 | } 134 | 135 | vhosts := strings.Split(cfg.Env()["VIRTUAL_HOST"], ",") 136 | if options.VirtualHost != "" && utils.StringInSlice(options.VirtualHost, vhosts) { 137 | vhosts = utils.RemoveStringInSlice(options.VirtualHost, vhosts) 138 | cfg.EnvSet("VIRTUAL_HOST", strings.Join(vhosts, ",")) 139 | } 140 | 141 | if options.Port != "" { 142 | cfg.EnvSet("GALAXY_PORT", "") 143 | } 144 | 145 | return configStore.UpdateApp(cfg, env) 146 | } 147 | -------------------------------------------------------------------------------- /commander/scheduler.go: -------------------------------------------------------------------------------- 1 | package commander 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/litl/galaxy/config" 7 | ) 8 | 9 | // Balanced returns the number of instances that should be run on the host 10 | // according to the desired state for the app in the given env and pool. The 11 | // number returned for the host represent an approximately equal distribution 12 | // across all hosts. 13 | func Balanced(configStore *config.Store, hostId, app, env, pool string) (int, error) { 14 | hosts, err := configStore.ListHosts(env, pool) 15 | if err != nil { 16 | return 0, err 17 | } 18 | 19 | cfg, err := configStore.GetApp(app, env) 20 | if err != nil { 21 | return 0, err 22 | } 23 | 24 | desired := cfg.GetProcesses(pool) 25 | if desired == 0 { 26 | return 0, nil 27 | } 28 | 29 | if desired == -1 { 30 | return 1, nil 31 | } 32 | 33 | hostIds := []string{} 34 | for _, h := range hosts { 35 | hostIds = append(hostIds, h.HostIP) 36 | } 37 | sort.Strings(hostIds) 38 | 39 | hostIdx := -1 40 | for i, v := range hostIds { 41 | if v == hostId { 42 | hostIdx = i 43 | break 44 | } 45 | } 46 | 47 | if hostIdx < 0 { 48 | return 0, nil 49 | } 50 | 51 | count := 0 52 | for i := 0; i < desired; i++ { 53 | if i%len(hosts) == hostIdx { 54 | count = count + 1 55 | } 56 | } 57 | 58 | return count, nil 59 | } 60 | -------------------------------------------------------------------------------- /commander/scheduler_test.go: -------------------------------------------------------------------------------- 1 | package commander 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/litl/galaxy/config" 7 | ) 8 | 9 | func NewTestStore() (*config.Store, *config.MemoryBackend) { 10 | r := &config.Store{} 11 | b := config.NewMemoryBackend() 12 | r.Backend = b 13 | return r, b 14 | } 15 | 16 | func setup(t *testing.T, desired int, hosts []string) *config.Store { 17 | s, b := NewTestStore() 18 | 19 | created, err := s.CreateApp("app", "dev") 20 | if !created || err != nil { 21 | t.Errorf("Failed to create app: %s", err) 22 | } 23 | 24 | ac, err := s.GetApp("app", "dev") 25 | if !created || err != nil { 26 | t.Errorf("Failed to get app: %s", err) 27 | } 28 | 29 | ac.SetProcesses("web", desired) 30 | 31 | b.ListHostsFunc = func(env, pool string) ([]config.HostInfo, error) { 32 | ret := []config.HostInfo{} 33 | for _, h := range hosts { 34 | ret = append(ret, config.HostInfo{ 35 | HostIP: h, 36 | }) 37 | } 38 | return ret, nil 39 | 40 | } 41 | return s 42 | } 43 | 44 | func TestScheduleOneBadHost(t *testing.T) { 45 | 46 | s := setup(t, 1, []string{"127.0.0.1"}) 47 | 48 | count, err := Balanced(s, "127.0.0.2", "app", "dev", "web") 49 | if err != nil { 50 | t.Errorf("Expected %d. Got %s", 0, err) 51 | } 52 | 53 | if count != 0 { 54 | t.Errorf("Expected %d. Got %d", 0, count) 55 | } 56 | } 57 | 58 | func TestScheduleOneOneHost(t *testing.T) { 59 | 60 | s := setup(t, 1, []string{"127.0.0.1"}) 61 | 62 | count, err := Balanced(s, "127.0.0.1", "app", "dev", "web") 63 | if err != nil { 64 | t.Errorf("Expected %d. Got %s", 1, err) 65 | } 66 | 67 | if count != 1 { 68 | t.Errorf("Expected %d. Got %d", 1, count) 69 | } 70 | } 71 | 72 | func TestScheduleFiveOneHost(t *testing.T) { 73 | 74 | s := setup(t, 5, []string{"127.0.0.1"}) 75 | 76 | count, err := Balanced(s, "127.0.0.1", "app", "dev", "web") 77 | if err != nil { 78 | t.Errorf("Expected %d. Got %s", 5, err) 79 | } 80 | 81 | if count != 5 { 82 | t.Errorf("Expected %d. Got %d", 5, count) 83 | } 84 | } 85 | 86 | func TestScheduleTwoOneHost(t *testing.T) { 87 | 88 | s := setup(t, 2, []string{"127.0.0.1"}) 89 | 90 | count, err := Balanced(s, "127.0.0.1", "app", "dev", "web") 91 | if err != nil { 92 | t.Errorf("Expected %d. Got %s", 2, err) 93 | } 94 | 95 | if count != 2 { 96 | t.Errorf("Expected %d. Got %d", 2, count) 97 | } 98 | } 99 | 100 | func TestScheduleOneTwoHost(t *testing.T) { 101 | 102 | s := setup(t, 1, []string{"127.0.0.1", "127.0.0.2"}) 103 | 104 | count, err := Balanced(s, "127.0.0.1", "app", "dev", "web") 105 | if err != nil { 106 | t.Errorf("Expected %d. Got %s", 1, err) 107 | } 108 | 109 | if count != 1 { 110 | t.Errorf("Expected %d. Got %d", 1, count) 111 | } 112 | 113 | count, err = Balanced(s, "127.0.0.2", "app", "dev", "web") 114 | if err != nil { 115 | t.Errorf("Expected %d. Got %s", 0, err) 116 | } 117 | 118 | if count != 0 { 119 | t.Errorf("Expected %d. Got %d", 0, count) 120 | } 121 | } 122 | 123 | func TestScheduleTwoTwoHost(t *testing.T) { 124 | 125 | s := setup(t, 2, []string{"127.0.0.1", "127.0.0.2"}) 126 | 127 | count, err := Balanced(s, "127.0.0.1", "app", "dev", "web") 128 | if err != nil { 129 | t.Errorf("Expected %d. Got %s", 1, err) 130 | } 131 | 132 | if count != 1 { 133 | t.Errorf("Expected %d. Got %d", 1, count) 134 | } 135 | 136 | count, err = Balanced(s, "127.0.0.2", "app", "dev", "web") 137 | if err != nil { 138 | t.Errorf("Expected %d. Got %s", 1, err) 139 | } 140 | 141 | if count != 1 { 142 | t.Errorf("Expected %d. Got %d", 1, count) 143 | } 144 | } 145 | 146 | func TestScheduleFiveTwoHost(t *testing.T) { 147 | 148 | s := setup(t, 5, []string{"127.0.0.1", "127.0.0.2"}) 149 | 150 | count, err := Balanced(s, "127.0.0.1", "app", "dev", "web") 151 | if err != nil { 152 | t.Errorf("Expected %d. Got %s", 1, err) 153 | } 154 | 155 | if count != 3 { 156 | t.Errorf("Expected %d. Got %d", 3, count) 157 | } 158 | 159 | count, err = Balanced(s, "127.0.0.2", "app", "dev", "web") 160 | if err != nil { 161 | t.Errorf("Expected %d. Got %s", 2, err) 162 | } 163 | 164 | if count != 2 { 165 | t.Errorf("Expected %d. Got %d", 2, count) 166 | } 167 | } 168 | 169 | func TestScheduleOneDefault(t *testing.T) { 170 | 171 | s := setup(t, -1, []string{"127.0.0.1"}) 172 | 173 | count, err := Balanced(s, "127.0.0.1", "app", "dev", "web") 174 | if err != nil { 175 | t.Errorf("Expected %d. Got %s", 1, err) 176 | } 177 | 178 | if count != 1 { 179 | t.Errorf("Expected %d. Got %d", 1, count) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /config/app_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/litl/galaxy/utils" 9 | ) 10 | 11 | // Interface to wrap AppConfig, so that it can be swapped out with a different 12 | // Backend. This should be temporary as many of these methods won't be useful. 13 | type App interface { 14 | Name() string 15 | Env() map[string]string 16 | EnvSet(key, value string) 17 | EnvGet(key string) string 18 | Version() string 19 | SetVersion(version string) 20 | VersionID() string 21 | SetVersionID(versionID string) 22 | ID() int64 23 | ContainerName() string 24 | SetProcesses(pool string, count int) 25 | GetProcesses(pool string) int 26 | RuntimePools() []string 27 | SetMemory(pool string, mem string) 28 | GetMemory(pool string) string 29 | SetCPUShares(pool string, cpu string) 30 | GetCPUShares(pool string) string 31 | SetMaintenanceMode(pool string, maint bool) 32 | GetMaintenanceMode(pool string) bool 33 | } 34 | 35 | type AppConfig struct { 36 | // ID is used for ordering and conflict resolution. 37 | // Usualy set to time.Now().UnixNano() 38 | name string `redis:"name"` 39 | versionVMap *utils.VersionedMap 40 | environmentVMap *utils.VersionedMap 41 | portsVMap *utils.VersionedMap 42 | runtimeVMap *utils.VersionedMap 43 | } 44 | 45 | func NewAppConfig(app, version string) App { 46 | svcCfg := &AppConfig{ 47 | name: app, 48 | versionVMap: utils.NewVersionedMap(), 49 | environmentVMap: utils.NewVersionedMap(), 50 | portsVMap: utils.NewVersionedMap(), 51 | runtimeVMap: utils.NewVersionedMap(), 52 | } 53 | svcCfg.SetVersion(version) 54 | 55 | return svcCfg 56 | } 57 | 58 | // TODO: this isn't used anyhere 59 | func NewAppConfigWithEnv(app, version string, env map[string]string) App { 60 | svcCfg := NewAppConfig(app, version).(*AppConfig) 61 | 62 | for k, v := range env { 63 | svcCfg.environmentVMap.Set(k, v) 64 | } 65 | 66 | return svcCfg 67 | } 68 | 69 | func (s *AppConfig) Name() string { 70 | return s.name 71 | } 72 | 73 | // Env returns a map representing the runtime environment for the container. 74 | // Changes to this map have no effect. 75 | func (s *AppConfig) Env() map[string]string { 76 | env := map[string]string{} 77 | for _, k := range s.environmentVMap.Keys() { 78 | val := s.environmentVMap.Get(k) 79 | if val != "" { 80 | env[k] = val 81 | } 82 | } 83 | return env 84 | } 85 | 86 | func (s *AppConfig) EnvSet(key, value string) { 87 | s.environmentVMap.SetVersion(key, value, s.nextID()) 88 | } 89 | 90 | func (s *AppConfig) EnvGet(key string) string { 91 | return s.environmentVMap.Get(key) 92 | } 93 | 94 | func (s *AppConfig) Version() string { 95 | return s.versionVMap.Get("version") 96 | } 97 | 98 | func (s *AppConfig) SetVersion(version string) { 99 | s.versionVMap.SetVersion("version", version, s.nextID()) 100 | } 101 | 102 | func (s *AppConfig) VersionID() string { 103 | return s.versionVMap.Get("versionID") 104 | } 105 | 106 | func (s *AppConfig) SetVersionID(versionID string) { 107 | s.versionVMap.SetVersion("versionID", versionID, s.nextID()) 108 | } 109 | 110 | func (s *AppConfig) Ports() map[string]string { 111 | ports := map[string]string{} 112 | for _, k := range s.portsVMap.Keys() { 113 | val := s.portsVMap.Get(k) 114 | if val != "" { 115 | ports[k] = val 116 | } 117 | } 118 | return ports 119 | } 120 | 121 | func (s *AppConfig) ClearPorts() { 122 | for _, k := range s.portsVMap.Keys() { 123 | s.portsVMap.SetVersion(k, "", s.nextID()) 124 | } 125 | } 126 | 127 | func (s *AppConfig) AddPort(port, portType string) { 128 | s.portsVMap.Set(port, portType) 129 | } 130 | 131 | func (s *AppConfig) ID() int64 { 132 | id := int64(0) 133 | for _, vmap := range []*utils.VersionedMap{ 134 | s.environmentVMap, 135 | s.versionVMap, 136 | s.portsVMap, 137 | s.runtimeVMap, 138 | } { 139 | if vmap.LatestVersion() > id { 140 | id = vmap.LatestVersion() 141 | } 142 | } 143 | return id 144 | } 145 | 146 | func (s *AppConfig) ContainerName() string { 147 | return s.name + "_" + strconv.FormatInt(s.ID(), 10) 148 | } 149 | 150 | func (s *AppConfig) nextID() int64 { 151 | return s.ID() + 1 152 | } 153 | 154 | func (s *AppConfig) SetProcesses(pool string, count int) { 155 | key := fmt.Sprintf("%s-ps", pool) 156 | s.runtimeVMap.SetVersion(key, strconv.FormatInt(int64(count), 10), s.nextID()) 157 | } 158 | 159 | func (s *AppConfig) GetProcesses(pool string) int { 160 | key := fmt.Sprintf("%s-ps", pool) 161 | ps := s.runtimeVMap.Get(key) 162 | if ps == "" { 163 | return -1 164 | } 165 | count, _ := strconv.ParseInt(ps, 10, 16) 166 | return int(count) 167 | } 168 | 169 | func (s *AppConfig) RuntimePools() []string { 170 | keys := s.runtimeVMap.Keys() 171 | pools := []string{} 172 | for _, k := range keys { 173 | pool := k[:strings.Index(k, "-")] 174 | if !utils.StringInSlice(pool, pools) { 175 | pools = append(pools, pool) 176 | } 177 | } 178 | return pools 179 | } 180 | 181 | func (s *AppConfig) SetMemory(pool string, mem string) { 182 | key := fmt.Sprintf("%s-mem", pool) 183 | s.runtimeVMap.SetVersion(key, mem, s.nextID()) 184 | } 185 | 186 | func (s *AppConfig) GetMemory(pool string) string { 187 | key := fmt.Sprintf("%s-mem", pool) 188 | return s.runtimeVMap.Get(key) 189 | } 190 | 191 | func (s *AppConfig) SetCPUShares(pool string, cpu string) { 192 | key := fmt.Sprintf("%s-cpu", pool) 193 | s.runtimeVMap.SetVersion(key, cpu, s.nextID()) 194 | } 195 | 196 | func (s *AppConfig) GetCPUShares(pool string) string { 197 | key := fmt.Sprintf("%s-cpu", pool) 198 | return s.runtimeVMap.Get(key) 199 | } 200 | 201 | func (s *AppConfig) SetMaintenanceMode(pool string, maint bool) { 202 | key := fmt.Sprintf("%s-maint", pool) 203 | s.runtimeVMap.SetVersion(key, fmt.Sprint(maint), s.nextID()) 204 | } 205 | 206 | func (s *AppConfig) GetMaintenanceMode(pool string) bool { 207 | key := fmt.Sprintf("%s-maint", pool) 208 | maint, _ := strconv.ParseBool(s.runtimeVMap.Get(key)) 209 | return maint 210 | } 211 | -------------------------------------------------------------------------------- /config/app_config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | ) 7 | 8 | func TestSetVersion(t *testing.T) { 9 | sc := NewAppConfig("foo", "") 10 | if sc.Version() != "" { 11 | t.Fail() 12 | } 13 | 14 | sc.SetVersion("1") 15 | if sc.Version() != "1" { 16 | t.Fail() 17 | } 18 | 19 | sc.SetVersion("2") 20 | if sc.Version() != "2" { 21 | t.Fail() 22 | } 23 | 24 | sc.SetVersion("") 25 | if sc.Version() != "" { 26 | t.Fail() 27 | } 28 | } 29 | 30 | func TestSetEnv(t *testing.T) { 31 | sc := NewAppConfig("foo", "") 32 | if len(sc.Env()) != 0 { 33 | t.Fail() 34 | } 35 | 36 | sc.EnvSet("foo", "bar") 37 | if sc.EnvGet("foo") != "bar" { 38 | t.Fail() 39 | } 40 | if sc.Env()["foo"] != "bar" { 41 | t.Fail() 42 | } 43 | 44 | sc.EnvSet("foo", "baz") 45 | if sc.EnvGet("foo") != "baz" { 46 | t.Fail() 47 | } 48 | if sc.Env()["foo"] != "baz" { 49 | t.Fail() 50 | } 51 | 52 | sc.EnvSet("bing", "bang") 53 | if len(sc.Env()) != 2 { 54 | t.Fail() 55 | } 56 | } 57 | 58 | func TestID(t *testing.T) { 59 | sc := NewAppConfig("foo", "") 60 | id := sc.ID() 61 | if id != 1 { 62 | t.Fatalf("id should be 1. Got %d", id) 63 | } 64 | 65 | sc.SetVersion("foo") 66 | if sc.ID() < id { 67 | t.Fail() 68 | } 69 | id = sc.ID() 70 | 71 | sc.EnvSet("foo", "bar") 72 | if sc.ID() < id { 73 | t.Fail() 74 | } 75 | } 76 | 77 | func TestContainerName(t *testing.T) { 78 | sc := NewAppConfig("foo", "registry.foo.com/foobar:abc234") 79 | if sc.ContainerName() != "foo_"+strconv.FormatInt(sc.ID(), 10) { 80 | } 81 | sc.EnvSet("biz", "baz") 82 | 83 | if sc.ContainerName() != "foo_"+strconv.FormatInt(sc.ID(), 10) { 84 | t.Fatalf("Expected %s. Got %s", "foo_"+strconv.FormatInt(sc.ID(), 10), sc.ContainerName()) 85 | } 86 | 87 | } 88 | 89 | func TestIDAlwaysIncrements(t *testing.T) { 90 | 91 | sc := NewAppConfig("foo", "") 92 | 93 | id := sc.ID() 94 | sc.EnvSet("k1", "v1") 95 | if sc.ID() <= id { 96 | t.Fatalf("Expected version to increment") 97 | } 98 | id = sc.ID() 99 | 100 | sc.EnvSet("k1", "v2") 101 | if sc.ID() <= id { 102 | t.Fatalf("Expected version to increment") 103 | } 104 | id = sc.ID() 105 | 106 | sc.EnvSet("k1", "v3") 107 | if sc.ID() <= id { 108 | t.Fatalf("Expected version to increment") 109 | } 110 | id = sc.ID() 111 | 112 | sc.SetVersion("blah") 113 | if sc.ID() <= id { 114 | t.Fatalf("Expected version to increment") 115 | } 116 | id = sc.ID() 117 | 118 | sc.SetVersion("bar") 119 | if sc.ID() <= id { 120 | t.Fatalf("Expected version to increment") 121 | } 122 | id = sc.ID() 123 | } 124 | -------------------------------------------------------------------------------- /config/backend.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Backend interface { 4 | // Apps 5 | AppExists(app, env string) (bool, error) 6 | CreateApp(app, env string) (bool, error) 7 | ListApps(env string) ([]App, error) 8 | GetApp(app, env string) (App, error) 9 | UpdateApp(svcCfg App, env string) (bool, error) 10 | DeleteApp(svcCfg App, env string) (bool, error) 11 | 12 | // Pools 13 | AssignApp(app, env, pool string) (bool, error) 14 | UnassignApp(app, env, pool string) (bool, error) 15 | ListAssignments(env, pool string) ([]string, error) 16 | CreatePool(env, pool string) (bool, error) 17 | DeletePool(env, pool string) (bool, error) 18 | ListPools(env string) ([]string, error) 19 | 20 | // Envs 21 | ListEnvs() ([]string, error) 22 | 23 | // Host 24 | UpdateHost(env, pool string, host HostInfo) error 25 | ListHosts(env, pool string) ([]HostInfo, error) 26 | DeleteHost(env, pool string, host HostInfo) error 27 | 28 | //Pub/Sub 29 | Subscribe(key string) chan string 30 | Notify(key, value string) (int, error) 31 | 32 | // Registration 33 | RegisterService(env, pool string, reg *ServiceRegistration) error 34 | UnregisterService(env, pool, hostIP, name, containerID string) (*ServiceRegistration, error) 35 | GetServiceRegistration(env, pool, hostIP, name, containerID string) (*ServiceRegistration, error) 36 | ListRegistrations(env string) ([]ServiceRegistration, error) 37 | 38 | connect() 39 | reconnect() 40 | } 41 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | // AppDefintiion contains all the configuration needed to run a container 9 | // in the galaxy environment. 10 | type AppDefinition struct { 11 | // Version of this structure in the config storage as of the last operation 12 | // In consul, this would correspond to `ModifyIndex`/. The value stored 13 | // would be the previous index, and over-written upon retrieval. 14 | ConfigIndex int64 15 | 16 | // ("Name" is taken by the interface getter) 17 | AppName string 18 | 19 | // Image is the specific docker image to be run. 20 | Image string 21 | 22 | // Docker Image ID 23 | // If "Image" does not contain a tag, or uses "latest", we need a way to 24 | // know what version we're running. 25 | // TODO: how can we handle this case and not have pull every image on the 26 | // host that runs app:deploy? 27 | ImageID string 28 | 29 | // PortMappings defines how ports are mapped from the host to the docker 30 | // container. 31 | PortMappings []PortMapping 32 | 33 | // Hosts entries to insert into /etc/hosts inside the container 34 | Hosts []HostsEntry 35 | 36 | // A set of custom DNS servers for the container 37 | DNS []string 38 | 39 | // Entry point arguments for the container 40 | EntryPoint []string 41 | 42 | // Command arguments for the container 43 | Command []string 44 | 45 | // The environment passed to the container 46 | Environment map[string]string 47 | 48 | // Resources are assigned per logical group, e.g. Pool 49 | // TODO: This seems awkward -- apps don't know about the env they are 50 | // assigned to, but they need to know about the pools. 51 | // This is needed while refactoring though, as all the resource 52 | // limits are assigned through the config, and rely on the pool. 53 | Assignments []AppAssignment 54 | } 55 | 56 | type HostsEntry struct { 57 | Address string 58 | Host string 59 | } 60 | 61 | type PortMapping struct { 62 | // HostPort is the port that will be bound to the host, directly or through 63 | // a proxy. 64 | HostPort string 65 | 66 | // ContainerPort is the port exposed in the docker image 67 | ContainerPort string 68 | 69 | // Network defines the transport used for this port. TCP is the default if 70 | // not set. 71 | Network string 72 | 73 | // Hostnames that can can be routed to this HostPort via a virtual host 74 | // http handler 75 | Hostnames []string 76 | 77 | // Predefined error pages to return if a backend returns an error, or is 78 | // unavailable when access through an http virtual host. 79 | ErrorPages map[int]string 80 | } 81 | 82 | // AppAssignment provides the location and resource limits for an app to run 83 | type AppAssignment struct { 84 | // We currently only assign to Pools 85 | Pool string 86 | 87 | // Name of assigned App 88 | App string 89 | 90 | // Docker CPU share constraint: 0-1024 91 | // The default is 0, meaning unconstrained. 92 | CPU int 93 | 94 | // Docker Memory limit (, where unit = b, k, m or g) 95 | Memory string 96 | 97 | // MemorySwap is the total memory limit (memory + swap, format: 98 | // , where unit = b, k, m or g) 99 | MemorySwap string 100 | 101 | // Number of instances to run across all hosts in this grouping 102 | Instances int 103 | 104 | // Minimum number of instances to keep running during a deploy or restart. 105 | // Default is 1 if Instances is > 1, else 0. 106 | MinInstances int 107 | 108 | // Whether this app is in maintenance mode 109 | MaintenanceMode bool 110 | } 111 | 112 | // 113 | // Below are all methods to make an AppDefinition implement the existing App interface 114 | 115 | func (a *AppDefinition) Name() string { 116 | return a.AppName 117 | } 118 | 119 | func (a *AppDefinition) Env() map[string]string { 120 | return a.Environment 121 | } 122 | 123 | func (a *AppDefinition) EnvSet(key, value string) { 124 | a.Environment[key] = value 125 | } 126 | 127 | func (a *AppDefinition) EnvGet(key string) string { 128 | return a.Environment[key] 129 | } 130 | 131 | func (a *AppDefinition) Version() string { 132 | return a.Image 133 | } 134 | 135 | func (a *AppDefinition) SetVersion(version string) { 136 | a.Image = version 137 | } 138 | 139 | func (a *AppDefinition) VersionID() string { 140 | return a.ImageID 141 | } 142 | 143 | func (a *AppDefinition) SetVersionID(versionID string) { 144 | a.ImageID = versionID 145 | } 146 | 147 | func (a *AppDefinition) ID() int64 { 148 | return a.ConfigIndex 149 | } 150 | 151 | func (a *AppDefinition) ContainerName() string { 152 | return fmt.Sprintf("%s_%d", a.Name(), a.ID()) 153 | } 154 | 155 | func (a *AppDefinition) SetProcesses(pool string, count int) { 156 | i := a.assignment(pool) 157 | a.Assignments[i].Instances = count 158 | } 159 | 160 | func (a *AppDefinition) GetProcesses(pool string) int { 161 | i := a.assignment(pool) 162 | return a.Assignments[i].Instances 163 | } 164 | 165 | func (a *AppDefinition) RuntimePools() []string { 166 | pools := []string{} 167 | for _, as := range a.Assignments { 168 | pools = append(pools, as.Pool) 169 | } 170 | return pools 171 | } 172 | 173 | func (a *AppDefinition) SetMemory(pool string, mem string) { 174 | i := a.assignment(pool) 175 | a.Assignments[i].Memory = mem 176 | } 177 | 178 | func (a *AppDefinition) GetMemory(pool string) string { 179 | i := a.assignment(pool) 180 | return a.Assignments[i].Memory 181 | } 182 | 183 | func (a *AppDefinition) SetCPUShares(pool string, cpu string) { 184 | i := a.assignment(pool) 185 | a.Assignments[i].CPU, _ = strconv.Atoi(cpu) 186 | } 187 | 188 | func (a *AppDefinition) GetCPUShares(pool string) string { 189 | i := a.assignment(pool) 190 | return strconv.Itoa(a.Assignments[i].CPU) 191 | } 192 | 193 | func (a *AppDefinition) SetMaintenanceMode(pool string, maint bool) { 194 | i := a.assignment(pool) 195 | a.Assignments[i].MaintenanceMode = maint 196 | } 197 | 198 | func (a *AppDefinition) GetMaintenanceMode(pool string) bool { 199 | i := a.assignment(pool) 200 | return a.Assignments[i].MaintenanceMode 201 | } 202 | 203 | // TODO: This is to make it easier to refactor in this new config. 204 | // Might want to rework this once we define what the semantics of the 205 | // Assignments are. 206 | // 207 | // assignment returns the index of the assignment we're looking for, adding a 208 | // new one if it doesn't exist. 209 | func (a *AppDefinition) assignment(pool string) int { 210 | for i := range a.Assignments { 211 | if a.Assignments[i].Pool == pool { 212 | return i 213 | } 214 | } 215 | 216 | // FIXME: Instances is hard-coded at -1 to match old behavior 217 | a.Assignments = append(a.Assignments, AppAssignment{Pool: pool, Instances: -1}) 218 | return len(a.Assignments) - 1 219 | } 220 | -------------------------------------------------------------------------------- /config/consul.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "path" 7 | "sort" 8 | "sync" 9 | "time" 10 | 11 | "github.com/litl/galaxy/log" 12 | 13 | consul "github.com/hashicorp/consul/api" 14 | ) 15 | 16 | /* 17 | TODO: logging! 18 | 19 | TODO: use CAS operations so that we don't have any races between 20 | configuration changes 21 | 22 | The consul tree looks like: 23 | galaxy/apps/env/app_name 24 | galaxy/pools/env/pool_name 25 | galaxy/hosts/env/pool/host_ip 26 | galaxy/services/env/pool/host_ip/service_name/container_id 27 | 28 | The Methods for ConsulBackend are tentatively defined here to satisfy the 29 | confing.Backend interface, and may not be appropriate 30 | */ 31 | type ConsulBackend struct { 32 | client *consul.Client 33 | 34 | // We always need a session to set the TTL for keys 35 | sessionID string 36 | 37 | // stop any backend goroutined 38 | done chan struct{} 39 | 40 | // filter events we've already seen 41 | seen *eventCache 42 | } 43 | 44 | var UnknownApp = fmt.Errorf("unkown app") 45 | 46 | func NewConsulBackend() *ConsulBackend { 47 | client, err := consul.NewClient(consul.DefaultConfig()) 48 | if err != nil { 49 | // this shouldn't ever error with the default config 50 | panic(err) 51 | } 52 | 53 | node, err := client.Agent().NodeName() 54 | if err != nil { 55 | log.Fatal(err) 56 | } 57 | 58 | // find an existing galaxy session if one exists, or create a new one 59 | sessions, _, err := client.Session().Node(node, nil) 60 | if err != nil { 61 | log.Fatal(err) 62 | } 63 | 64 | var session *consul.SessionEntry 65 | for _, s := range sessions { 66 | if s.Name == "galaxy" { 67 | session = s 68 | break 69 | } 70 | } 71 | 72 | // we have a session, now make sure we can renew it so it doesn't expire 73 | // before we start running 74 | if session != nil { 75 | session, _, err = client.Session().Renew(session.ID, nil) 76 | if err != nil { 77 | log.Debug("error renewing galaxy session:", err) 78 | } 79 | } 80 | 81 | // no existing session, so create a new one 82 | if session == nil { 83 | session = &consul.SessionEntry{ 84 | Name: "galaxy", 85 | Behavior: "delete", 86 | TTL: "15s", 87 | } 88 | 89 | session.ID, _, err = client.Session().Create(session, nil) 90 | if err != nil { 91 | // we can't continue without a session for key TTLs 92 | log.Fatal(err) 93 | } 94 | } 95 | 96 | // keep our session alive in the background 97 | done := make(chan struct{}) 98 | go client.Session().RenewPeriodic("10s", session.ID, nil, done) 99 | 100 | return &ConsulBackend{ 101 | client: client, 102 | sessionID: session.ID, 103 | done: done, 104 | seen: &eventCache{ 105 | seen: make(map[string]uint64), 106 | }, 107 | } 108 | } 109 | 110 | // Check that an app exists and has a config 111 | func (c *ConsulBackend) AppExists(app, env string) (bool, error) { 112 | appCfg, err := c.GetApp(app, env) 113 | if err != nil { 114 | if err == UnknownApp { 115 | return false, nil 116 | } 117 | return false, err 118 | } 119 | 120 | if appCfg == nil { 121 | return false, nil 122 | } 123 | return true, nil 124 | } 125 | 126 | // Create and save an empty AppDefinition for a new app 127 | func (c *ConsulBackend) CreateApp(app, env string) (bool, error) { 128 | kvp := &consul.KVPair{} 129 | kvp.Key = path.Join("galaxy", "apps", env, app) 130 | 131 | // TODO: intit this in one place 132 | emptyConfig := &AppDefinition{ 133 | AppName: app, 134 | Environment: make(map[string]string), 135 | } 136 | 137 | var err error 138 | kvp.Value, err = json.Marshal(emptyConfig) 139 | if err != nil { 140 | return false, err 141 | } 142 | 143 | _, err = c.client.KV().Put(kvp, nil) 144 | if err != nil { 145 | return false, err 146 | } 147 | 148 | return true, nil 149 | } 150 | 151 | // List all apps in an environment 152 | func (c *ConsulBackend) ListApps(env string) ([]App, error) { 153 | key := path.Join("galaxy", "apps", env) 154 | kvPairs, _, err := c.client.KV().List(key, nil) 155 | if err != nil { 156 | return nil, err 157 | } 158 | 159 | apps := make([]App, len(kvPairs)) 160 | for i, kvp := range kvPairs { 161 | ad := &AppDefinition{} 162 | err := json.Unmarshal(kvp.Value, ad) 163 | if err != nil { 164 | log.Println("error decoding AppDefinition for %s: %s", kvp.Key, err.Error()) 165 | continue 166 | } 167 | ad.ConfigIndex = int64(kvp.ModifyIndex) 168 | apps[i] = App(ad) 169 | } 170 | return apps, nil 171 | } 172 | 173 | // Retrieve the current config for an application 174 | func (c *ConsulBackend) GetApp(app, env string) (App, error) { 175 | key := path.Join("galaxy", "apps", env, app) 176 | kvp, _, err := c.client.KV().Get(key, nil) 177 | if err != nil { 178 | return nil, err 179 | } 180 | 181 | if kvp == nil { 182 | return nil, UnknownApp 183 | } 184 | 185 | ad := &AppDefinition{} 186 | err = json.Unmarshal(kvp.Value, ad) 187 | if err != nil { 188 | return nil, err 189 | } 190 | 191 | ad.ConfigIndex = int64(kvp.ModifyIndex) 192 | return ad, nil 193 | } 194 | 195 | // Update the current configuration for an app 196 | func (c *ConsulBackend) UpdateApp(app App, env string) (bool, error) { 197 | ad := app.(*AppDefinition) 198 | key := path.Join("galaxy", "apps", env, ad.Name()) 199 | kvp := &consul.KVPair{Key: key} 200 | 201 | var err error 202 | kvp.Value, err = json.Marshal(ad) 203 | if err != nil { 204 | return false, err 205 | } 206 | 207 | _, err = c.client.KV().Put(kvp, nil) 208 | if err != nil { 209 | return false, err 210 | } 211 | return true, nil 212 | } 213 | 214 | // Delete the configuration for an app 215 | // FIXME: Why does this take an App? Everything else takes a string 216 | func (c *ConsulBackend) DeleteApp(app App, env string) (bool, error) { 217 | key := path.Join("galaxy", "apps", env, app.Name()) 218 | _, err := c.client.KV().Delete(key, nil) 219 | if err != nil { 220 | return false, err 221 | } 222 | 223 | return true, nil 224 | } 225 | 226 | // Add a pool assignment for this app, and update the config. 227 | // The pool need not exist, it just won't run until there is a corresponding 228 | // pool. 229 | func (c *ConsulBackend) AssignApp(app, env, pool string) (bool, error) { 230 | appCfg, err := c.GetApp(app, env) 231 | if err != nil { 232 | return false, err 233 | } 234 | 235 | ad := appCfg.(*AppDefinition) 236 | // return early if we're already assigned to this pool 237 | for i := range ad.Assignments { 238 | if ad.Assignments[i].Pool == pool { 239 | return true, nil 240 | } 241 | } 242 | 243 | // FIXME: we have to hard-code -1 here to match old behavior 244 | a := AppAssignment{Pool: pool, Instances: -1} 245 | ad.Assignments = append(ad.Assignments, a) 246 | return c.UpdateApp(ad, env) 247 | } 248 | 249 | // Remove a pool assignment for this app, and update the config 250 | func (c *ConsulBackend) UnassignApp(app, env, pool string) (bool, error) { 251 | found := false 252 | appCfg, err := c.GetApp(app, env) 253 | if err != nil { 254 | return false, err 255 | } 256 | 257 | ad := appCfg.(*AppDefinition) 258 | for i := range ad.Assignments { 259 | if ad.Assignments[i].Pool == pool { 260 | // remove the item for the slice 261 | a := ad.Assignments 262 | a[i], a = a[len(a)-1], a[:len(a)-1] 263 | ad.Assignments = a 264 | found = true 265 | break 266 | } 267 | } 268 | 269 | if !found { 270 | return false, nil 271 | } 272 | 273 | ok, err := c.UpdateApp(ad, env) 274 | return found && ok, err 275 | } 276 | 277 | // List apps assigned to a pool 278 | func (c *ConsulBackend) ListAssignments(env, pool string) ([]string, error) { 279 | apps, err := c.ListApps(env) 280 | if err != nil { 281 | return nil, err 282 | } 283 | 284 | assigned := []string{} 285 | for _, app := range apps { 286 | for _, a := range app.(*AppDefinition).Assignments { 287 | if a.Pool == pool { 288 | assigned = append(assigned, app.Name()) 289 | } 290 | } 291 | } 292 | 293 | return assigned, nil 294 | } 295 | 296 | // Create a pool entry 297 | // Pool are just an empty Key/Value pair, to signify that this pool has been 298 | // purposely created. 299 | func (c *ConsulBackend) CreatePool(env, pool string) (bool, error) { 300 | key := path.Join("galaxy", "pools", env, pool) 301 | kvp := &consul.KVPair{Key: key} 302 | _, err := c.client.KV().Put(kvp, nil) 303 | if err != nil { 304 | return false, err 305 | } 306 | return true, nil 307 | } 308 | 309 | // Delete the pool entry 310 | func (c *ConsulBackend) DeletePool(env, pool string) (bool, error) { 311 | key := path.Join("galaxy", "pools", env, pool) 312 | _, err := c.client.KV().DeleteTree(key, nil) 313 | if err != nil { 314 | return false, err 315 | } 316 | return true, nil 317 | } 318 | 319 | // List all pools in an environment 320 | func (c *ConsulBackend) ListPools(env string) ([]string, error) { 321 | prefix := path.Join("galaxy", "pools", env) + "/" 322 | keys, _, err := c.client.KV().Keys(prefix, "/", nil) 323 | if err != nil { 324 | return nil, err 325 | } 326 | 327 | pools := make([]string, len(keys)) 328 | for i, key := range keys { 329 | pools[i] = path.Base(key) 330 | } 331 | return pools, nil 332 | } 333 | 334 | func (c *ConsulBackend) ListEnvs() ([]string, error) { 335 | prefix := path.Join("galaxy", "apps") + "/" 336 | keys, _, err := c.client.KV().Keys(prefix, "/", nil) 337 | if err != nil { 338 | return nil, err 339 | } 340 | 341 | envs := make([]string, len(keys)) 342 | for i, key := range keys { 343 | envs[i] = path.Base(key) 344 | } 345 | 346 | return envs, nil 347 | } 348 | 349 | // TODO: We need to keep the hosts entries for now, to easily correlate the 350 | // host with the env and pool, and to schedule apps on appropriate hosts. 351 | // Rename appropriately to reflect that this only adds a host, and 352 | // there's nothing to update. 353 | func (c *ConsulBackend) UpdateHost(env, pool string, host HostInfo) error { 354 | key := path.Join("galaxy", "hosts", env, pool, host.HostIP) 355 | // lookup the SessionID of this host 356 | kvp, _, err := c.client.KV().Get(key, nil) 357 | if err != nil { 358 | return err 359 | } 360 | 361 | if kvp == nil { 362 | // new host, add the key and acquire the lock for TTL 363 | kvp = &consul.KVPair{ 364 | Key: key, 365 | Session: c.sessionID, 366 | } 367 | 368 | if kvp.Value, err = json.Marshal(host); err != nil { 369 | return err 370 | } 371 | 372 | if _, err = c.client.KV().Put(kvp, nil); err != nil { 373 | return err 374 | } 375 | 376 | if _, _, err = c.client.KV().Acquire(kvp, nil); err != nil { 377 | return err 378 | } 379 | } 380 | 381 | if kvp.Session == "" { 382 | err := fmt.Errorf("Host %s has no session!", key) 383 | return err 384 | } 385 | 386 | return nil 387 | } 388 | 389 | func (c *ConsulBackend) ListHosts(env, pool string) ([]HostInfo, error) { 390 | prefix := path.Join("galaxy", "hosts", env, pool) + "/" 391 | keys, _, err := c.client.KV().Keys(prefix, "/", nil) 392 | if err != nil { 393 | return nil, err 394 | } 395 | 396 | hosts := make([]HostInfo, len(keys)) 397 | for i, key := range keys { 398 | hosts[i].HostIP = path.Base(key) 399 | } 400 | return hosts, nil 401 | } 402 | 403 | func (c *ConsulBackend) DeleteHost(env, pool string, host HostInfo) error { 404 | key := path.Join("galaxy", "hosts", env, pool, host.HostIP) 405 | _, err := c.client.KV().Delete(key, nil) 406 | return err 407 | } 408 | 409 | // FIXME: the int return value is useless here, and not used on the redis 410 | // backend either. 411 | func (c *ConsulBackend) Notify(key, value string) (int, error) { 412 | event := &consul.UserEvent{ 413 | Name: key, 414 | Payload: []byte(value), 415 | } 416 | 417 | _, _, err := c.client.Event().Fire(event, nil) 418 | if err != nil { 419 | return 0, err 420 | } 421 | return 1, nil 422 | } 423 | 424 | func (c *ConsulBackend) Subscribe(key string) chan string { 425 | msgs := make(chan string) 426 | go c.sub(key, msgs) 427 | return msgs 428 | } 429 | 430 | // FIXME: This can't be shut down. Make sure that's not a problem 431 | func (c *ConsulBackend) sub(key string, msgs chan string) { 432 | var events []*consul.UserEvent 433 | var meta *consul.QueryMeta 434 | var err error 435 | for { 436 | // No way to handle failure here, just keep trying to get our first set of events. 437 | // We need a successful query to get the last index to search from. 438 | events, meta, err = c.client.Event().List(key, nil) 439 | if err != nil { 440 | log.Println("Subscribe error:", err) 441 | time.Sleep(5 * time.Second) 442 | continue 443 | } 444 | // cache all old events 445 | c.seen.Filter(events) 446 | break 447 | } 448 | 449 | lastIndex := meta.LastIndex 450 | for { 451 | opts := &consul.QueryOptions{ 452 | WaitIndex: lastIndex, 453 | WaitTime: 30 * time.Second, 454 | } 455 | events, meta, err = c.client.Event().List(key, opts) 456 | if err != nil { 457 | log.Printf("Subscribe(%s): %s\n", key, err.Error()) 458 | continue 459 | } 460 | 461 | if meta.LastIndex == lastIndex { 462 | // no new events 463 | continue 464 | } 465 | 466 | for _, event := range c.seen.Filter(events) { 467 | msgs <- string(event.Payload) 468 | } 469 | 470 | lastIndex = meta.LastIndex 471 | } 472 | } 473 | 474 | // Marshal a ServiceRegistry in consul, and associate it with a session so it 475 | // is deleted on expiration. 476 | func (c *ConsulBackend) RegisterService(env, pool string, reg *ServiceRegistration) error { 477 | key := path.Join("galaxy", "services", env, pool, reg.ExternalIP, reg.Name, reg.ContainerID[0:12]) 478 | 479 | // check for an existing value, so we don't try to re-acquire the lock 480 | existing, _, err := c.client.KV().Get(key, nil) 481 | if err != nil { 482 | return err 483 | } 484 | 485 | if existing != nil && existing.Session == c.sessionID { 486 | // already registered 487 | return nil 488 | } 489 | 490 | kvp := &consul.KVPair{ 491 | Key: key, 492 | Session: c.sessionID, 493 | } 494 | 495 | kvp.Value, err = json.Marshal(reg) 496 | if err != nil { 497 | return err 498 | } 499 | 500 | _, err = c.client.KV().Put(kvp, nil) 501 | if err != nil { 502 | return err 503 | } 504 | 505 | // you need to acquire a lock with the key in order for the key to be 506 | // associated with the session. 507 | ok, _, err := c.client.KV().Acquire(kvp, nil) 508 | if err != nil { 509 | return err 510 | } 511 | 512 | if !ok { 513 | // TODO: is this a re-Acquire, or another Failure 514 | return fmt.Errorf("Lock failed on Key:%s Session:%s", kvp.Key, kvp.Session) 515 | } 516 | 517 | return nil 518 | } 519 | 520 | // TODO: do we need to return a *ServiceRegistration? 521 | func (c *ConsulBackend) UnregisterService(env, pool, hostIP, name, containerID string) (*ServiceRegistration, error) { 522 | key := path.Join("galaxy", "services", env, pool, hostIP, name, containerID[0:12]) 523 | 524 | registration, err := c.GetServiceRegistration(env, pool, hostIP, name, containerID) 525 | if err != nil || registration == nil { 526 | return registration, err 527 | } 528 | 529 | if registration.ContainerID != containerID { 530 | return nil, nil 531 | } 532 | 533 | _, err = c.client.KV().Delete(key, nil) 534 | if err != nil { 535 | return registration, err 536 | } 537 | 538 | return registration, nil 539 | } 540 | 541 | func (c *ConsulBackend) GetServiceRegistration(env, pool, hostIP, name, containerID string) (*ServiceRegistration, error) { 542 | key := path.Join("galaxy", "services", env, pool, hostIP, name, containerID[0:12]) 543 | 544 | existingRegistration := ServiceRegistration{ 545 | Path: key, 546 | } 547 | 548 | kvp, _, err := c.client.KV().Get(key, nil) 549 | if err != nil { 550 | return nil, err 551 | } 552 | 553 | if kvp == nil { 554 | return nil, nil 555 | } 556 | 557 | err = json.Unmarshal(kvp.Value, &existingRegistration) 558 | if err != nil { 559 | return nil, err 560 | } 561 | 562 | // FIXME: this is a fake Expires, set to the default TTL. 563 | // There's no easy way to get the session expiration, and does it really 564 | // matter, since it can't be longer than TTL? 565 | existingRegistration.Expires = time.Now().UTC().Add(time.Duration(DefaultTTL) * time.Second) 566 | return &existingRegistration, nil 567 | } 568 | 569 | func (c *ConsulBackend) ListRegistrations(env string) ([]ServiceRegistration, error) { 570 | prefix := path.Join("galaxy", "services", env) 571 | 572 | kvPairs, _, err := c.client.KV().List(prefix, nil) 573 | if err != nil { 574 | return nil, err 575 | } 576 | 577 | regList := []ServiceRegistration{} 578 | for _, kvp := range kvPairs { 579 | svcReg := ServiceRegistration{ 580 | Name: path.Base(kvp.Key), 581 | } 582 | err = json.Unmarshal(kvp.Value, &svcReg) 583 | if err != nil { 584 | log.Warnf("WARN: Unable to unmarshal JSON for %s: %s", kvp.Key, err) 585 | continue 586 | } 587 | 588 | regList = append(regList, svcReg) 589 | } 590 | 591 | return regList, nil 592 | } 593 | 594 | // Required for the interface, but not used by consul 595 | func (c *ConsulBackend) connect() {} 596 | func (c *ConsulBackend) reconnect() {} 597 | 598 | var _ Backend = &ConsulBackend{} 599 | 600 | // we need to de-dupe consul events, as up to 256 events may be returned 601 | type eventCache struct { 602 | sync.Mutex 603 | // map the ID to the LTime, that way we can check for duplicate IDs, and purge based on LTime 604 | seen map[string]uint64 605 | } 606 | 607 | // Return only events we haven't seen 608 | func (c *eventCache) Filter(events []*consul.UserEvent) []*consul.UserEvent { 609 | c.Lock() 610 | defer c.Unlock() 611 | 612 | var newEvents []*consul.UserEvent 613 | 614 | for _, e := range events { 615 | _, ok := c.seen[e.ID] 616 | if !ok { 617 | newEvents = append(newEvents, e) 618 | c.seen[e.ID] = e.LTime 619 | } 620 | } 621 | 622 | // purge the cache occasionally 623 | // the agent can't return events older what we just saw, so prune the cache 624 | // to just the most recent events. 625 | if len(c.seen) > 2*len(events) { 626 | c.prune(len(events)) 627 | } 628 | 629 | return newEvents 630 | } 631 | 632 | // Prune all but the newest `size` events. 633 | func (c *eventCache) prune(size int) { 634 | events := make([]event, 0) 635 | for id, ltime := range c.seen { 636 | events = append(events, event{id: id, ltime: ltime}) 637 | } 638 | 639 | sort.Sort(byLTime(events)) 640 | 641 | for _, event := range events[:len(events)-size] { 642 | delete(c.seen, event.id) 643 | } 644 | } 645 | 646 | type byLTime []event 647 | 648 | func (a byLTime) Len() int { return len(a) } 649 | func (a byLTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 650 | func (a byLTime) Less(i, j int) bool { return a[i].ltime < a[j].ltime } 651 | 652 | type event struct { 653 | id string 654 | ltime uint64 655 | } 656 | -------------------------------------------------------------------------------- /config/dedupe_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/consul/api" 8 | ) 9 | 10 | func TestEventDedupe(t *testing.T) { 11 | cache := &eventCache{ 12 | seen: make(map[string]uint64), 13 | } 14 | 15 | events := make([]*api.UserEvent, 0) 16 | for i := uint64(0); i < 10; i++ { 17 | events = append(events, &api.UserEvent{ID: fmt.Sprintf("%x", i), LTime: i}) 18 | } 19 | 20 | newEvents := cache.Filter(events) 21 | 22 | if len(events) != len(newEvents) { 23 | t.Fatal("missing events") 24 | } 25 | 26 | // add 11 more 27 | for i := uint64(100); i < 111; i++ { 28 | events = append(events, &api.UserEvent{ID: fmt.Sprintf("%x", i), LTime: i}) 29 | } 30 | 31 | newEvents = cache.Filter(events) 32 | if len(newEvents) != 11 { 33 | t.Fatalf("got %d events out of 11", len(newEvents)) 34 | } 35 | 36 | // remove the old events 37 | events = events[:0] 38 | // add 9 more 39 | for i := uint64(200); i < 209; i++ { 40 | events = append(events, &api.UserEvent{ID: fmt.Sprintf("%x", i), LTime: i}) 41 | } 42 | 43 | // make sure we only get 9 back 44 | newEvents = cache.Filter(events) 45 | if len(newEvents) != 9 { 46 | t.Fatal("got %d events out of 9", len(newEvents)) 47 | } 48 | 49 | // check for correct events 50 | for _, event := range newEvents { 51 | if event.LTime < 200 || event.LTime > 209 { 52 | t.Fatal("wrong event", event.ID) 53 | } 54 | } 55 | 56 | // since we only have 9 events, and the cache has 21, it should have purged 57 | // the old events 58 | if len(cache.seen) != 9 { 59 | t.Fatal("cache should have been purged to the 9 most recent events") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /config/memory.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/litl/galaxy/utils" 8 | ) 9 | 10 | type Value struct { 11 | value interface{} 12 | ttl int 13 | } 14 | 15 | type MemoryBackend struct { 16 | maps map[string]map[string]string 17 | apps map[string][]App // env -> []app 18 | assignments map[string][]string 19 | 20 | AppExistsFunc func(app, env string) (bool, error) 21 | CreateAppFunc func(app, env string) (bool, error) 22 | GetAppFunc func(app, env string) (App, error) 23 | UpdateAppFunc func(svcCfg App, env string) (bool, error) 24 | DeleteAppFunc func(svcCfg App, env string) (bool, error) 25 | ListAppFunc func(env string) ([]AppConfig, error) 26 | AssignAppFunc func(app, env, pool string) (bool, error) 27 | UnassignAppFunc func(app, env, pool string) (bool, error) 28 | ListAssignmentsFunc func(env, pool string) ([]string, error) 29 | CreatePoolFunc func(env, pool string) (bool, error) 30 | DeletePoolFunc func(env, pool string) (bool, error) 31 | ListPoolsFunc func(env string) ([]string, error) 32 | ListEnvsFunc func() ([]string, error) 33 | ListHostsFunc func(env, pool string) ([]HostInfo, error) 34 | 35 | MembersFunc func(key string) ([]string, error) 36 | KeysFunc func(key string) ([]string, error) 37 | AddMemberFunc func(key, value string) (int, error) 38 | RemoveMemberFunc func(key, value string) (int, error) 39 | NotifyFunc func(key, value string) (int, error) 40 | SetMultiFunc func(key string, values map[string]string) (string, error) 41 | } 42 | 43 | func NewMemoryBackend() *MemoryBackend { 44 | return &MemoryBackend{ 45 | maps: make(map[string]map[string]string), 46 | apps: make(map[string][]App), 47 | assignments: make(map[string][]string), 48 | } 49 | } 50 | 51 | func (r *MemoryBackend) AppExists(app, env string) (bool, error) { 52 | if r.AppExistsFunc != nil { 53 | return r.AppExistsFunc(app, env) 54 | } 55 | 56 | for _, s := range r.apps[env] { 57 | if s.Name() == app { 58 | return true, nil 59 | } 60 | } 61 | return false, nil 62 | } 63 | 64 | func (r *MemoryBackend) CreateApp(app, env string) (bool, error) { 65 | 66 | if r.CreateAppFunc != nil { 67 | return r.CreateAppFunc(app, env) 68 | } 69 | 70 | if exists, err := r.AppExists(app, env); !exists && err == nil { 71 | r.apps[env] = append(r.apps[env], NewAppConfig(app, "")) 72 | return true, nil 73 | } 74 | 75 | return false, nil 76 | } 77 | 78 | func (r *MemoryBackend) ListApps(env string) ([]App, error) { 79 | return r.apps[env], nil 80 | } 81 | 82 | func (r *MemoryBackend) GetApp(app, env string) (App, error) { 83 | if r.GetAppFunc != nil { 84 | return r.GetAppFunc(app, env) 85 | } 86 | 87 | for _, cfg := range r.apps[env] { 88 | if cfg.Name() == app { 89 | return cfg, nil 90 | } 91 | } 92 | return nil, nil 93 | } 94 | 95 | func (r *MemoryBackend) UpdateApp(svcCfg App, env string) (bool, error) { 96 | if r.UpdateAppFunc != nil { 97 | return r.UpdateAppFunc(svcCfg, env) 98 | } 99 | return false, nil 100 | } 101 | 102 | func (r *MemoryBackend) DeleteApp(svcCfg App, env string) (bool, error) { 103 | if r.DeleteAppFunc != nil { 104 | return r.DeleteAppFunc(svcCfg, env) 105 | } 106 | 107 | cfgs := []App{} 108 | for _, cfg := range r.apps[env] { 109 | if cfg.Name() != svcCfg.Name() { 110 | cfgs = append(cfgs, cfg) 111 | } 112 | } 113 | r.apps[env] = cfgs 114 | return true, nil 115 | } 116 | 117 | func (r *MemoryBackend) AssignApp(app, env, pool string) (bool, error) { 118 | if r.AssignAppFunc != nil { 119 | return r.AssignAppFunc(app, env, pool) 120 | } 121 | 122 | key := env + "/" + pool 123 | if !utils.StringInSlice(app, r.assignments[key]) { 124 | r.assignments[key] = append(r.assignments[key], app) 125 | } 126 | 127 | return true, nil 128 | } 129 | 130 | func (r *MemoryBackend) UnassignApp(app, env, pool string) (bool, error) { 131 | if r.UnassignAppFunc != nil { 132 | return r.UnassignAppFunc(app, env, pool) 133 | } 134 | 135 | key := env + "/" + pool 136 | if !utils.StringInSlice(app, r.assignments[key]) { 137 | return false, nil 138 | } 139 | r.assignments[key] = utils.RemoveStringInSlice(app, r.assignments[key]) 140 | 141 | return true, nil 142 | } 143 | 144 | func (r *MemoryBackend) ListAssignments(env, pool string) ([]string, error) { 145 | if r.ListAssignmentsFunc != nil { 146 | return r.ListAssignmentsFunc(env, pool) 147 | } 148 | 149 | key := env + "/" + pool 150 | return r.assignments[key], nil 151 | } 152 | 153 | func (r *MemoryBackend) CreatePool(env, pool string) (bool, error) { 154 | if r.CreatePoolFunc != nil { 155 | return r.CreatePoolFunc(env, pool) 156 | } 157 | 158 | key := env + "/" + pool 159 | r.assignments[key] = []string{} 160 | return true, nil 161 | } 162 | 163 | func (r *MemoryBackend) DeletePool(env, pool string) (bool, error) { 164 | if r.DeletePoolFunc != nil { 165 | return r.DeletePoolFunc(env, pool) 166 | } 167 | 168 | key := env + "/" + pool 169 | delete(r.assignments, key) 170 | return true, nil 171 | } 172 | 173 | func (r *MemoryBackend) ListPools(env string) ([]string, error) { 174 | if r.ListPoolsFunc != nil { 175 | return r.ListPools(env) 176 | } 177 | 178 | p := []string{} 179 | for k, _ := range r.assignments { 180 | parts := strings.Split(k, "/") 181 | p = append(p, parts[1]) 182 | } 183 | return p, nil 184 | } 185 | 186 | func (r *MemoryBackend) ListEnvs() ([]string, error) { 187 | if r.ListEnvsFunc != nil { 188 | return r.ListEnvsFunc() 189 | } 190 | 191 | p := []string{} 192 | for k, _ := range r.assignments { 193 | parts := strings.Split(k, "/") 194 | env := parts[0] 195 | if !utils.StringInSlice(env, p) { 196 | p = append(p, parts[0]) 197 | } 198 | } 199 | return p, nil 200 | } 201 | 202 | func (r *MemoryBackend) connect() { 203 | } 204 | 205 | func (r *MemoryBackend) reconnect() { 206 | } 207 | 208 | func (r *MemoryBackend) Keys(key string) ([]string, error) { 209 | if r.KeysFunc != nil { 210 | return r.KeysFunc(key) 211 | } 212 | 213 | keys := []string{} 214 | rp := strings.NewReplacer("*", `.*`) 215 | p := rp.Replace(key) 216 | 217 | re := regexp.MustCompile(p) 218 | for k := range r.maps { 219 | if re.MatchString(k) { 220 | keys = append(keys, k) 221 | } 222 | } 223 | 224 | return keys, nil 225 | } 226 | 227 | func (r *MemoryBackend) Expire(key string, ttl uint64) (int, error) { 228 | return 0, nil 229 | } 230 | 231 | func (r *MemoryBackend) TTL(key string) (int, error) { 232 | return 0, nil 233 | } 234 | 235 | func (r *MemoryBackend) Delete(key string) (int, error) { 236 | if _, ok := r.maps[key]; ok { 237 | delete(r.maps, key) 238 | return 1, nil 239 | } 240 | return 0, nil 241 | } 242 | 243 | func (r *MemoryBackend) AddMember(key, value string) (int, error) { 244 | if r.AddMemberFunc != nil { 245 | return r.AddMemberFunc(key, value) 246 | } 247 | 248 | set := r.maps[key] 249 | if set == nil { 250 | set = make(map[string]string) 251 | r.maps[key] = set 252 | } 253 | set[value] = "1" 254 | return 1, nil 255 | } 256 | 257 | func (r *MemoryBackend) RemoveMember(key, value string) (int, error) { 258 | if r.RemoveMemberFunc != nil { 259 | return r.RemoveMemberFunc(key, value) 260 | } 261 | 262 | set := r.maps[key] 263 | if set == nil { 264 | return 0, nil 265 | } 266 | 267 | if _, ok := set[value]; ok { 268 | delete(set, value) 269 | return 1, nil 270 | } 271 | return 0, nil 272 | 273 | } 274 | 275 | func (r *MemoryBackend) Members(key string) ([]string, error) { 276 | if r.MembersFunc != nil { 277 | return r.MembersFunc(key) 278 | } 279 | 280 | values := []string{} 281 | set := r.maps[key] 282 | for v := range set { 283 | values = append(values, v) 284 | } 285 | return values, nil 286 | } 287 | 288 | func (r *MemoryBackend) Notify(key, value string) (int, error) { 289 | if r.NotifyFunc != nil { 290 | return r.NotifyFunc(key, value) 291 | } 292 | return 0, nil 293 | } 294 | 295 | func (r *MemoryBackend) Subscribe(key string) chan string { 296 | return make(chan string) 297 | } 298 | 299 | func (r *MemoryBackend) Set(key, field string, value string) (string, error) { 300 | return "OK", nil 301 | } 302 | 303 | func (r *MemoryBackend) Get(key, field string) (string, error) { 304 | return "", nil 305 | } 306 | 307 | func (r *MemoryBackend) GetAll(key string) (map[string]string, error) { 308 | return r.maps[key], nil 309 | } 310 | 311 | func (r *MemoryBackend) SetMulti(key string, values map[string]string) (string, error) { 312 | if r.SetMultiFunc != nil { 313 | return r.SetMultiFunc(key, values) 314 | } 315 | 316 | r.maps[key] = values 317 | return "OK", nil 318 | } 319 | 320 | func (r *MemoryBackend) DeleteMulti(key string, fields ...string) (int, error) { 321 | m := r.maps[key] 322 | for _, field := range fields { 323 | delete(m, field) 324 | } 325 | return len(fields), nil 326 | } 327 | 328 | func (r *MemoryBackend) UpdateHost(env, pool string, host HostInfo) error { 329 | panic("not implemented") 330 | } 331 | 332 | func (r *MemoryBackend) ListHosts(env, pool string) ([]HostInfo, error) { 333 | if r.ListHostsFunc != nil { 334 | return r.ListHostsFunc(env, pool) 335 | } 336 | panic("not implemented") 337 | } 338 | 339 | func (r *MemoryBackend) DeleteHost(env, pool string, host HostInfo) error { 340 | panic("not implemented") 341 | } 342 | 343 | func (r *MemoryBackend) RegisterService(env, pool string, reg *ServiceRegistration) error { 344 | panic("not implemented") 345 | } 346 | 347 | func (r *MemoryBackend) UnregisterService(env, pool, hostIP, name, containerID string) (*ServiceRegistration, error) { 348 | panic("not implemented") 349 | } 350 | 351 | func (r *MemoryBackend) GetServiceRegistration(env, pool, hostIP, name, containerID string) (*ServiceRegistration, error) { 352 | panic("not implemented") 353 | } 354 | 355 | func (r *MemoryBackend) ListRegistrations(env string) ([]ServiceRegistration, error) { 356 | panic("not implemented") 357 | } 358 | -------------------------------------------------------------------------------- /config/notify.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type ConfigChange struct { 11 | AppConfig App 12 | Restart bool 13 | Error error 14 | } 15 | 16 | func (s *Store) CheckForChangesNow() { 17 | s.pollCh <- true 18 | } 19 | 20 | func (s *Store) checkForChanges(env string) { 21 | lastVersion := make(map[string]int64) 22 | for { 23 | appCfg, err := s.ListApps(env) 24 | if err != nil { 25 | s.restartChan <- &ConfigChange{ 26 | Error: err, 27 | } 28 | time.Sleep(5 * time.Second) 29 | continue 30 | } 31 | 32 | for _, config := range appCfg { 33 | lastVersion[config.Name()] = config.ID() 34 | } 35 | break 36 | 37 | } 38 | 39 | for { 40 | <-s.pollCh 41 | appCfg, err := s.ListApps(env) 42 | if err != nil { 43 | s.restartChan <- &ConfigChange{ 44 | Error: err, 45 | } 46 | continue 47 | } 48 | for _, changedConfig := range appCfg { 49 | changeCopy := changedConfig 50 | if changedConfig.ID() != lastVersion[changedConfig.Name()] { 51 | log.Printf("%s changed from %d to %d", changedConfig.Name(), 52 | lastVersion[changedConfig.Name()], changedConfig.ID()) 53 | lastVersion[changedConfig.Name()] = changedConfig.ID() 54 | s.restartChan <- &ConfigChange{ 55 | AppConfig: changeCopy, 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | func (s *Store) checkForChangePeriodically(stop chan struct{}) { 63 | // TODO: default polling interval 64 | ticker := time.NewTicker(10 * time.Second) 65 | for { 66 | select { 67 | case <-stop: 68 | ticker.Stop() 69 | return 70 | case <-ticker.C: 71 | s.CheckForChangesNow() 72 | } 73 | } 74 | } 75 | 76 | func (s *Store) restartApp(app, env string) { 77 | appCfg, err := s.GetApp(app, env) 78 | if err != nil { 79 | s.restartChan <- &ConfigChange{ 80 | Error: err, 81 | } 82 | return 83 | } 84 | 85 | s.restartChan <- &ConfigChange{ 86 | Restart: true, 87 | AppConfig: appCfg, 88 | } 89 | } 90 | 91 | func (s *Store) NotifyRestart(app, env string) error { 92 | // TODO: received count ignored, use it somehow? 93 | _, err := s.Backend.Notify(fmt.Sprintf("galaxy-%s", env), fmt.Sprintf("restart %s", app)) 94 | if err != nil { 95 | return err 96 | } 97 | return nil 98 | } 99 | 100 | func (s *Store) NotifyEnvChanged(env string) error { 101 | // TODO: received count ignored, use it somehow? 102 | _, err := s.Backend.Notify(fmt.Sprintf("galaxy-%s", env), "config") 103 | if err != nil { 104 | return err 105 | } 106 | return nil 107 | } 108 | 109 | func (s *Store) subscribeChanges(env string) { 110 | 111 | msgs := s.Backend.Subscribe(fmt.Sprintf("galaxy-%s", env)) 112 | for { 113 | 114 | msg := <-msgs 115 | if msg == "config" { 116 | s.CheckForChangesNow() 117 | } else if strings.HasPrefix(msg, "restart") { 118 | parts := strings.Split(msg, " ") 119 | app := parts[1] 120 | s.restartApp(app, env) 121 | } else { 122 | log.Printf("Ignoring notification: %s\n", msg) 123 | } 124 | } 125 | } 126 | 127 | func (s *Store) Watch(env string, stop chan struct{}) chan *ConfigChange { 128 | go s.checkForChanges(env) 129 | go s.checkForChangePeriodically(stop) 130 | go s.subscribeChanges(env) 131 | return s.restartChan 132 | } 133 | -------------------------------------------------------------------------------- /config/redis.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "path" 7 | "strings" 8 | "sync" 9 | "time" 10 | 11 | "github.com/garyburd/redigo/redis" 12 | "github.com/litl/galaxy/log" 13 | "github.com/litl/galaxy/utils" 14 | ) 15 | 16 | type RedisBackend struct { 17 | redisPool redis.Pool 18 | RedisHost string 19 | } 20 | 21 | func (r *RedisBackend) AppExists(app, env string) (bool, error) { 22 | matches, err := r.Keys(path.Join(env, app, "*")) 23 | if err != nil { 24 | return false, err 25 | } 26 | return len(matches) > 0, nil 27 | } 28 | 29 | func (r *RedisBackend) CreateApp(app, env string) (bool, error) { 30 | emptyConfig := NewAppConfig(app, "") 31 | return r.UpdateApp(emptyConfig, env) 32 | } 33 | 34 | func (r *RedisBackend) ListApps(env string) ([]App, error) { 35 | // TODO: convert to scan 36 | apps, err := r.Keys(path.Join(env, "*", "version")) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | // TODO: is it OK to error out early? 42 | var appList []App 43 | for _, app := range apps { 44 | parts := strings.Split(app, "/") 45 | 46 | // app entries should be 3 parts, /env/pool/app 47 | if len(parts) != 3 { 48 | continue 49 | } 50 | 51 | // we don't want host keys 52 | if parts[1] == "hosts" { 53 | continue 54 | } 55 | 56 | cfg, err := r.GetApp(parts[1], env) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | appList = append(appList, cfg) 62 | } 63 | 64 | return appList, nil 65 | } 66 | 67 | func (r *RedisBackend) UpdateApp(cfg App, env string) (bool, error) { 68 | svcCfg := cfg.(*AppConfig) 69 | for k, v := range svcCfg.Env() { 70 | if svcCfg.environmentVMap.Get(k) != v { 71 | svcCfg.environmentVMap.Set(k, v) 72 | } 73 | } 74 | 75 | for k, v := range svcCfg.Ports() { 76 | if svcCfg.portsVMap.Get(k) != v { 77 | svcCfg.portsVMap.Set(k, v) 78 | } 79 | } 80 | 81 | //TODO: user MULTI/EXEC 82 | err := r.SaveVMap(path.Join(env, svcCfg.name, "environment"), 83 | svcCfg.environmentVMap) 84 | 85 | if err != nil { 86 | return false, err 87 | } 88 | 89 | err = r.SaveVMap(path.Join(env, svcCfg.name, "version"), 90 | svcCfg.versionVMap) 91 | 92 | if err != nil { 93 | return false, err 94 | } 95 | 96 | err = r.SaveVMap(path.Join(env, svcCfg.name, "ports"), 97 | svcCfg.portsVMap) 98 | 99 | if err != nil { 100 | return false, err 101 | } 102 | 103 | err = r.SaveVMap(path.Join(env, svcCfg.name, "runtime"), 104 | svcCfg.runtimeVMap) 105 | 106 | if err != nil { 107 | return false, err 108 | } 109 | return true, nil 110 | } 111 | 112 | func (r *RedisBackend) GetApp(app, env string) (App, error) { 113 | svcCfg := NewAppConfig(path.Base(app), "").(*AppConfig) 114 | 115 | err := r.LoadVMap(path.Join(env, app, "environment"), svcCfg.environmentVMap) 116 | if err != nil { 117 | return nil, err 118 | } 119 | err = r.LoadVMap(path.Join(env, app, "version"), svcCfg.versionVMap) 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | err = r.LoadVMap(path.Join(env, app, "ports"), svcCfg.portsVMap) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | err = r.LoadVMap(path.Join(env, app, "runtime"), svcCfg.runtimeVMap) 130 | if err != nil { 131 | return nil, err 132 | } 133 | return svcCfg, nil 134 | } 135 | 136 | func (r *RedisBackend) DeleteApp(svcCfg App, env string) (bool, error) { 137 | deletedOne := false 138 | deleted, err := r.Delete(path.Join(env, svcCfg.Name())) 139 | if err != nil { 140 | return false, err 141 | } 142 | 143 | deletedOne = deletedOne || deleted == 1 144 | 145 | for _, k := range []string{"environment", "version", "ports", "runtime"} { 146 | deleted, err = r.Delete(path.Join(env, svcCfg.Name(), k)) 147 | if err != nil { 148 | return false, err 149 | } 150 | deletedOne = deletedOne || deleted == 1 151 | } 152 | 153 | return deletedOne, nil 154 | } 155 | 156 | func (r *RedisBackend) AssignApp(app, env, pool string) (bool, error) { 157 | added, err := r.AddMember(path.Join(env, "pools", pool), app) 158 | if err != nil { 159 | return false, err 160 | } 161 | return added == 1, err 162 | } 163 | 164 | func (r *RedisBackend) UnassignApp(app, env, pool string) (bool, error) { 165 | removed, err := r.RemoveMember(path.Join(env, "pools", pool), app) 166 | return removed == 1, err 167 | } 168 | 169 | func (r *RedisBackend) ListAssignments(env, pool string) ([]string, error) { 170 | return r.Members(path.Join(env, "pools", pool)) 171 | } 172 | 173 | func (r *RedisBackend) CreatePool(env, pool string) (bool, error) { 174 | //FIXME: Create an associated auto-scaling groups tied to the 175 | //pool 176 | 177 | added, err := r.AddMember(path.Join(env, "pools", "*"), pool) 178 | return added == 1, err 179 | } 180 | 181 | func (r *RedisBackend) DeletePool(env, pool string) (bool, error) { 182 | apps, err := r.Members(path.Join(env, "pools", pool)) 183 | if err != nil { 184 | return false, err 185 | } 186 | 187 | if len(apps) > 0 { 188 | return false, nil 189 | } 190 | 191 | _, err = r.RemoveMember(path.Join(env, "pools", "*"), pool) 192 | if err != nil { 193 | return false, err 194 | } 195 | return true, nil 196 | } 197 | 198 | func (r *RedisBackend) ListPools(env string) ([]string, error) { 199 | // This is the host entry created by commander 200 | // when it starts up. It can dynamically create 201 | // a pool 202 | key := path.Join(env, "*", "hosts", "*", "info") 203 | keys, err := r.Keys(key) 204 | if err != nil { 205 | return nil, err 206 | } 207 | 208 | pools := []string{} 209 | 210 | for _, k := range keys { 211 | parts := strings.Split(k, "/") 212 | pool := parts[1] 213 | if !utils.StringInSlice(pool, pools) { 214 | pools = append(pools, pool) 215 | } 216 | } 217 | 218 | // This is the pools that have been manually created. It's 219 | // possible to assign an app to a pool that has no running 220 | // hosts so we add these to the pools list as well. 221 | key = path.Join(env, "pools", "*") 222 | keys, err = r.Keys(key) 223 | if err != nil { 224 | return nil, err 225 | } 226 | 227 | for _, k := range keys { 228 | parts := strings.Split(k, "/") 229 | pool := parts[2] 230 | 231 | if pool != "*" { 232 | continue 233 | } 234 | 235 | members, err := r.Members(k) 236 | if err != nil { 237 | return nil, err 238 | } 239 | 240 | for _, m := range members { 241 | if !utils.StringInSlice(m, pools) { 242 | pools = append(pools, m) 243 | } 244 | } 245 | } 246 | 247 | return pools, nil 248 | } 249 | 250 | func (r *RedisBackend) ListEnvs() ([]string, error) { 251 | envs := []string{} 252 | pools, err := r.Keys(path.Join("*", "pools", "*")) 253 | if err != nil { 254 | return nil, err 255 | } 256 | 257 | for _, pool := range pools { 258 | parts := strings.Split(pool, "/") 259 | if !utils.StringInSlice(parts[0], envs) { 260 | envs = append(envs, parts[0]) 261 | } 262 | } 263 | return envs, nil 264 | } 265 | 266 | func (r *RedisBackend) LoadVMap(key string, dest *utils.VersionedMap) error { 267 | serialized, err := r.GetAll(key) 268 | if err != nil { 269 | return err 270 | } 271 | 272 | dest.UnmarshalMap(serialized) 273 | return nil 274 | } 275 | 276 | func (r *RedisBackend) SaveVMap(key string, vmap *utils.VersionedMap) error { 277 | 278 | serialized := vmap.MarshalMap() 279 | if len(serialized) == 0 { 280 | return nil 281 | } 282 | 283 | created, err := r.SetMulti(key, serialized) 284 | 285 | if err != nil { 286 | return err 287 | } 288 | 289 | if created != "OK" { 290 | return errors.New("not saved") 291 | } 292 | 293 | r.GcVMap(key, vmap) 294 | return nil 295 | } 296 | 297 | func (r *RedisBackend) GcVMap(key string, vmap *utils.VersionedMap) error { 298 | serialized := vmap.MarshalExpiredMap(5) 299 | if len(serialized) > 0 { 300 | keys := []string{} 301 | for k, _ := range serialized { 302 | keys = append(keys, k) 303 | } 304 | 305 | deleted, err := r.DeleteMulti(key, keys...) 306 | 307 | if err != nil { 308 | return err 309 | } 310 | 311 | if deleted != 1 { 312 | return errors.New("not deleted") 313 | } 314 | } 315 | return nil 316 | } 317 | 318 | func (r *RedisBackend) dialTimeout() (redis.Conn, error) { 319 | rwTimeout := 5 * time.Second 320 | return redis.DialTimeout("tcp", r.RedisHost, rwTimeout, rwTimeout, rwTimeout) 321 | } 322 | 323 | func (r *RedisBackend) testOnBorrow(c redis.Conn, t time.Time) error { 324 | _, err := c.Do("PING") 325 | if err != nil { 326 | defer c.Close() 327 | } 328 | return err 329 | } 330 | 331 | func (r *RedisBackend) connect() { 332 | r.redisPool = redis.Pool{ 333 | MaxIdle: 1, 334 | IdleTimeout: 120 * time.Second, 335 | Dial: r.dialTimeout, 336 | TestOnBorrow: r.testOnBorrow, 337 | } 338 | } 339 | 340 | // not needed with a redis.Pool 341 | func (r *RedisBackend) reconnect() {} 342 | 343 | func (r *RedisBackend) Keys(key string) ([]string, error) { 344 | conn := r.redisPool.Get() 345 | defer conn.Close() 346 | 347 | if err := conn.Err(); err != nil { 348 | return nil, err 349 | } 350 | 351 | return redis.Strings(conn.Do("KEYS", key)) 352 | } 353 | 354 | func (r *RedisBackend) Expire(key string, ttl uint64) (int, error) { 355 | conn := r.redisPool.Get() 356 | defer conn.Close() 357 | 358 | if err := conn.Err(); err != nil { 359 | return 0, err 360 | } 361 | 362 | return redis.Int(conn.Do("EXPIRE", key, ttl)) 363 | } 364 | 365 | func (r *RedisBackend) TTL(key string) (int, error) { 366 | conn := r.redisPool.Get() 367 | defer conn.Close() 368 | 369 | if err := conn.Err(); err != nil { 370 | return 0, err 371 | } 372 | 373 | return redis.Int(conn.Do("TTL", key)) 374 | } 375 | 376 | func (r *RedisBackend) Delete(key string) (int, error) { 377 | conn := r.redisPool.Get() 378 | defer conn.Close() 379 | 380 | if err := conn.Err(); err != nil { 381 | return 0, err 382 | } 383 | 384 | return redis.Int(conn.Do("DEL", key)) 385 | } 386 | 387 | func (r *RedisBackend) AddMember(key, value string) (int, error) { 388 | conn := r.redisPool.Get() 389 | defer conn.Close() 390 | 391 | if err := conn.Err(); err != nil { 392 | return 0, err 393 | } 394 | 395 | return redis.Int(conn.Do("SADD", key, value)) 396 | } 397 | 398 | func (r *RedisBackend) RemoveMember(key, value string) (int, error) { 399 | conn := r.redisPool.Get() 400 | defer conn.Close() 401 | 402 | if err := conn.Err(); err != nil { 403 | return 0, err 404 | } 405 | 406 | return redis.Int(conn.Do("SREM", key, value)) 407 | } 408 | 409 | func (r *RedisBackend) Members(key string) ([]string, error) { 410 | conn := r.redisPool.Get() 411 | defer conn.Close() 412 | 413 | if err := conn.Err(); err != nil { 414 | return nil, err 415 | } 416 | 417 | return redis.Strings(conn.Do("SMEMBERS", key)) 418 | } 419 | 420 | func (r *RedisBackend) Notify(key, value string) (int, error) { 421 | conn := r.redisPool.Get() 422 | defer conn.Close() 423 | 424 | if err := conn.Err(); err != nil { 425 | return 0, err 426 | } 427 | 428 | return redis.Int(conn.Do("PUBLISH", key, value)) 429 | } 430 | 431 | func (r *RedisBackend) subscribeChannel(key string, msgs chan string) { 432 | var wg sync.WaitGroup 433 | 434 | redisPool := redis.Pool{ 435 | MaxIdle: 1, 436 | IdleTimeout: 0, 437 | Dial: func() (redis.Conn, error) { 438 | return redis.DialTimeout("tcp", r.RedisHost, time.Second, 0, 0) 439 | }, 440 | // test every connection for now 441 | TestOnBorrow: r.testOnBorrow, 442 | } 443 | 444 | for { 445 | conn := redisPool.Get() 446 | // no defer, doesn't return 447 | if err := conn.Err(); err != nil { 448 | conn.Close() 449 | log.Printf("ERROR: %v\n", err) 450 | time.Sleep(5 * time.Second) 451 | continue 452 | } 453 | 454 | wg.Add(1) 455 | psc := redis.PubSubConn{Conn: conn} 456 | go func() { 457 | defer wg.Done() 458 | for { 459 | switch n := psc.Receive().(type) { 460 | case redis.Message: 461 | msg := string(n.Data) 462 | msgs <- msg 463 | case error: 464 | psc.Close() 465 | log.Printf("ERROR: %v\n", n) 466 | return 467 | } 468 | } 469 | }() 470 | 471 | wg.Add(1) 472 | go func() { 473 | defer wg.Done() 474 | psc.Subscribe(key) 475 | log.Printf("Monitoring for config changes on channel: %s\n", key) 476 | }() 477 | wg.Wait() 478 | conn.Close() 479 | } 480 | } 481 | 482 | func (r *RedisBackend) Subscribe(key string) chan string { 483 | msgs := make(chan string) 484 | go r.subscribeChannel(key, msgs) 485 | return msgs 486 | } 487 | 488 | func (r *RedisBackend) Set(key, field string, value string) (string, error) { 489 | conn := r.redisPool.Get() 490 | defer conn.Close() 491 | 492 | if err := conn.Err(); err != nil { 493 | return "", err 494 | } 495 | 496 | return redis.String(conn.Do("HMSET", key, field, value)) 497 | } 498 | 499 | func (r *RedisBackend) Get(key, field string) (string, error) { 500 | conn := r.redisPool.Get() 501 | defer conn.Close() 502 | 503 | if err := conn.Err(); err != nil { 504 | return "", err 505 | } 506 | 507 | ret, err := redis.String(conn.Do("HGET", key, field)) 508 | if err != nil && err == redis.ErrNil { 509 | return "", nil 510 | } 511 | 512 | return ret, err 513 | } 514 | 515 | func (r *RedisBackend) GetAll(key string) (map[string]string, error) { 516 | conn := r.redisPool.Get() 517 | defer conn.Close() 518 | 519 | if err := conn.Err(); err != nil { 520 | return nil, err 521 | } 522 | 523 | matches, err := redis.Values(conn.Do("HGETALL", key)) 524 | if err != nil { 525 | return nil, err 526 | } 527 | 528 | serialized := make(map[string]string) 529 | for i := 0; i < len(matches); i += 2 { 530 | key := string(matches[i].([]byte)) 531 | value := string(matches[i+1].([]byte)) 532 | serialized[key] = value 533 | } 534 | return serialized, nil 535 | 536 | } 537 | 538 | func (r *RedisBackend) SetMulti(key string, values map[string]string) (string, error) { 539 | conn := r.redisPool.Get() 540 | defer conn.Close() 541 | 542 | if err := conn.Err(); err != nil { 543 | return "", err 544 | } 545 | 546 | redisArgs := redis.Args{}.Add(key).AddFlat(values) 547 | return redis.String(conn.Do("HMSET", redisArgs...)) 548 | } 549 | 550 | func (r *RedisBackend) DeleteMulti(key string, fields ...string) (int, error) { 551 | conn := r.redisPool.Get() 552 | defer conn.Close() 553 | 554 | if err := conn.Err(); err != nil { 555 | return 0, err 556 | } 557 | 558 | args := []string{} 559 | for _, field := range fields { 560 | args = append(args, field) 561 | } 562 | redisArgs := redis.Args{}.Add(key).AddFlat(args) 563 | return redis.Int(conn.Do("HDEL", redisArgs...)) 564 | 565 | } 566 | 567 | func (r *RedisBackend) DeleteHost(env, pool string, host HostInfo) error { 568 | key := path.Join(env, pool, "hosts", host.HostIP, "info") 569 | _, err := r.Delete(key) 570 | return err 571 | } 572 | 573 | func (r *RedisBackend) UpdateHost(env, pool string, host HostInfo) error { 574 | key := path.Join(env, pool, "hosts", host.HostIP, "info") 575 | existing := utils.NewVersionedMap() 576 | 577 | err := r.LoadVMap(key, existing) 578 | if err != nil { 579 | return err 580 | } 581 | 582 | save := false 583 | if existing.Get("HostIP") != host.HostIP { 584 | existing.Set("HostIP", host.HostIP) 585 | save = true 586 | } 587 | 588 | if save { 589 | err = r.SaveVMap(key, existing) 590 | if err != nil { 591 | return err 592 | } 593 | } 594 | 595 | _, err = r.Expire(key, DefaultTTL) 596 | return err 597 | } 598 | 599 | func (r *RedisBackend) ListHosts(env, pool string) ([]HostInfo, error) { 600 | key := path.Join(env, pool, "hosts", "*", "info") 601 | keys, err := r.Keys(key) 602 | if err != nil { 603 | return nil, err 604 | } 605 | 606 | hosts := []HostInfo{} 607 | 608 | for _, k := range keys { 609 | existing := utils.NewVersionedMap() 610 | 611 | err := r.LoadVMap(k, existing) 612 | if err != nil { 613 | return nil, err 614 | } 615 | hosts = append(hosts, HostInfo{ 616 | HostIP: existing.Get("HostIP"), 617 | }) 618 | } 619 | return hosts, nil 620 | } 621 | 622 | func (r *RedisBackend) RegisterService(env, pool string, reg *ServiceRegistration) error { 623 | registrationPath := path.Join(env, pool, "hosts", reg.ExternalIP, reg.Name, reg.ContainerID[0:12]) 624 | 625 | jsonReg, err := json.Marshal(reg) 626 | if err != nil { 627 | return err 628 | } 629 | 630 | _, err = r.Set(registrationPath, "location", string(jsonReg)) 631 | if err != nil { 632 | return err 633 | } 634 | 635 | _, err = r.Expire(registrationPath, DefaultTTL) 636 | 637 | if err != nil { 638 | return err 639 | } 640 | return nil 641 | } 642 | 643 | func (r *RedisBackend) UnregisterService(env, pool, hostIP, name, containerID string) (*ServiceRegistration, error) { 644 | registrationPath := path.Join(env, pool, "hosts", hostIP, name, containerID[0:12]) 645 | 646 | registration, err := r.GetServiceRegistration(env, pool, hostIP, name, containerID) 647 | if err != nil || registration == nil { 648 | return registration, err 649 | } 650 | 651 | if registration.ContainerID != containerID { 652 | return nil, nil 653 | } 654 | 655 | _, err = r.Delete(registrationPath) 656 | if err != nil { 657 | return registration, err 658 | } 659 | 660 | return registration, nil 661 | } 662 | 663 | func (r *RedisBackend) GetServiceRegistration(env, pool, hostIP, name, containerID string) (*ServiceRegistration, error) { 664 | regPath := path.Join(env, pool, "hosts", hostIP, name, containerID[0:12]) 665 | 666 | existingRegistration := ServiceRegistration{ 667 | Path: regPath, 668 | } 669 | 670 | location, err := r.Get(regPath, "location") 671 | 672 | if err != nil { 673 | return nil, err 674 | } 675 | 676 | if location == "" { 677 | return nil, nil 678 | } 679 | 680 | err = json.Unmarshal([]byte(location), &existingRegistration) 681 | if err != nil { 682 | return nil, err 683 | } 684 | 685 | expires, err := r.TTL(regPath) 686 | if err != nil { 687 | return nil, err 688 | } 689 | existingRegistration.Expires = time.Now().UTC().Add(time.Duration(expires) * time.Second) 690 | return &existingRegistration, nil 691 | } 692 | 693 | func (r *RedisBackend) ListRegistrations(env string) ([]ServiceRegistration, error) { 694 | keys, err := r.Keys(path.Join(env, "*", "hosts", "*", "*", "*")) 695 | if err != nil { 696 | return nil, err 697 | } 698 | 699 | var regList []ServiceRegistration 700 | for _, key := range keys { 701 | 702 | pool := strings.Split(key, "/")[1] 703 | 704 | val, err := r.Get(key, "location") 705 | if err != nil { 706 | log.Warnf("WARN: Unable to get location for %s: %s", key, err) 707 | continue 708 | } 709 | 710 | svcReg := ServiceRegistration{ 711 | Name: path.Base(key), 712 | Pool: pool, 713 | } 714 | err = json.Unmarshal([]byte(val), &svcReg) 715 | if err != nil { 716 | log.Warnf("WARN: Unable to unmarshal JSON for %s: %s", key, err) 717 | continue 718 | } 719 | 720 | svcReg.Path = key 721 | 722 | regList = append(regList, svcReg) 723 | } 724 | 725 | return regList, nil 726 | } 727 | -------------------------------------------------------------------------------- /config/redis_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/garyburd/redigo/redis" 9 | ) 10 | 11 | type TestConn struct { 12 | History []string 13 | CloseFn func() error 14 | ErrFn func() error 15 | DoFn func(commandName string, args ...interface{}) (reply interface{}, err error) 16 | SendFn func(commandName string, args ...interface{}) error 17 | FlushFn func() error 18 | ReceiveFn func() (reply interface{}, err error) 19 | } 20 | 21 | func (t *TestConn) Record(cmd string, args ...interface{}) { 22 | sa := []string{} 23 | for _, v := range args { 24 | if v == nil { 25 | continue 26 | } 27 | sa = append(sa, v.(string)) 28 | } 29 | t.History = append(t.History, fmt.Sprintf("%s %s", cmd, strings.Join(sa, " "))) 30 | } 31 | 32 | func (t *TestConn) Close() error { 33 | t.Record("Close()", nil) 34 | if t.CloseFn != nil { 35 | return t.CloseFn() 36 | } 37 | return nil 38 | } 39 | 40 | func (t *TestConn) Err() error { 41 | t.Record("Err()", nil) 42 | if t.ErrFn != nil { 43 | return t.ErrFn() 44 | } 45 | return nil 46 | } 47 | 48 | func (t *TestConn) Do(commandName string, args ...interface{}) (reply interface{}, err error) { 49 | t.Record(commandName, args...) 50 | 51 | if t.DoFn != nil { 52 | return t.DoFn(commandName, args...) 53 | } 54 | return nil, nil 55 | } 56 | 57 | func (t *TestConn) Send(commandName string, args ...interface{}) error { 58 | t.Record(commandName, args...) 59 | 60 | if t.SendFn != nil { 61 | return t.SendFn(commandName, args...) 62 | } 63 | return nil 64 | } 65 | 66 | func (t *TestConn) Flush() error { 67 | t.Record("Flush()", nil) 68 | 69 | if t.FlushFn != nil { 70 | return t.FlushFn() 71 | } 72 | return nil 73 | } 74 | 75 | func (t *TestConn) Receive() (reply interface{}, err error) { 76 | t.Record("Receive()", nil) 77 | if t.ReceiveFn != nil { 78 | return t.ReceiveFn() 79 | } 80 | return nil, nil 81 | } 82 | 83 | func NewTestRedisBackend() (*RedisBackend, *TestConn) { 84 | c := &TestConn{} 85 | return &RedisBackend{ 86 | redisPool: redis.Pool{ 87 | Dial: func() (redis.Conn, error) { 88 | return c, nil 89 | }, 90 | }, 91 | }, c 92 | } 93 | 94 | func TestAppExistsKeyFormat(t *testing.T) { 95 | 96 | r, c := NewTestRedisBackend() 97 | r.AppExists("foo", "dev") 98 | assertInHistory(t, c.History, "KEYS dev/foo/*") 99 | } 100 | 101 | func assertInHistory(t *testing.T, history []string, cmd string) { 102 | found := false 103 | for _, v := range history { 104 | if v == cmd { 105 | found = true 106 | } 107 | } 108 | if !found { 109 | t.Fatalf("Expected %s in [%s]", cmd, strings.Join(history, ",")) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /config/registration.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "time" 7 | 8 | "github.com/fsouza/go-dockerclient" 9 | ) 10 | 11 | func newServiceRegistration(container *docker.Container, hostIP, galaxyPort string) *ServiceRegistration { 12 | //FIXME: We're using the first found port and assuming it's tcp. 13 | //How should we handle a service that exposes multiple ports 14 | //as well as tcp vs udp ports. 15 | var externalPort, internalPort string 16 | 17 | // sort the port bindings by internal port number so multiple ports are assigned deterministically 18 | // (docker.Port is a string with a Port method) 19 | cPorts := container.NetworkSettings.Ports 20 | allPorts := []string{} 21 | for p, _ := range cPorts { 22 | allPorts = append(allPorts, string(p)) 23 | } 24 | sort.Strings(allPorts) 25 | 26 | for _, k := range allPorts { 27 | v := cPorts[docker.Port(k)] 28 | if len(v) > 0 { 29 | externalPort = v[0].HostPort 30 | internalPort = docker.Port(k).Port() 31 | // Look for a match to GALAXY_PORT if we have multiple ports to 32 | // choose from. (don't require this, or we may break existing services) 33 | if len(allPorts) > 1 && internalPort == galaxyPort { 34 | break 35 | } 36 | } 37 | } 38 | 39 | serviceRegistration := ServiceRegistration{ 40 | ContainerName: container.Name, 41 | ContainerID: container.ID, 42 | StartedAt: container.Created, 43 | Image: container.Config.Image, 44 | Port: galaxyPort, 45 | } 46 | 47 | if externalPort != "" && internalPort != "" { 48 | serviceRegistration.ExternalIP = hostIP 49 | serviceRegistration.InternalIP = container.NetworkSettings.IPAddress 50 | serviceRegistration.ExternalPort = externalPort 51 | serviceRegistration.InternalPort = internalPort 52 | } 53 | return &serviceRegistration 54 | } 55 | 56 | type ServiceRegistration struct { 57 | Name string `json:"NAME,omitempty"` 58 | ExternalIP string `json:"EXTERNAL_IP,omitempty"` 59 | ExternalPort string `json:"EXTERNAL_PORT,omitempty"` 60 | InternalIP string `json:"INTERNAL_IP,omitempty"` 61 | InternalPort string `json:"INTERNAL_PORT,omitempty"` 62 | ContainerID string `json:"CONTAINER_ID"` 63 | ContainerName string `json:"CONTAINER_NAME"` 64 | Image string `json:"IMAGE,omitempty"` 65 | ImageId string `json:"IMAGE_ID,omitempty"` 66 | StartedAt time.Time `json:"STARTED_AT"` 67 | Expires time.Time `json:"-"` 68 | Path string `json:"-"` 69 | VirtualHosts []string `json:"VIRTUAL_HOSTS"` 70 | Port string `json:"PORT"` 71 | ErrorPages map[string]string `json:"ERROR_PAGES,omitempty"` 72 | // pool is inserted only for commander dump and restore 73 | Pool string 74 | } 75 | 76 | func (s *ServiceRegistration) Equals(other ServiceRegistration) bool { 77 | return s.ExternalIP == other.ExternalIP && 78 | s.ExternalPort == other.ExternalPort && 79 | s.InternalIP == other.InternalIP && 80 | s.InternalPort == other.InternalPort 81 | } 82 | 83 | func (s *ServiceRegistration) addr(ip, port string) string { 84 | if ip != "" && port != "" { 85 | return fmt.Sprint(ip, ":", port) 86 | } 87 | return "" 88 | 89 | } 90 | func (s *ServiceRegistration) ExternalAddr() string { 91 | return s.addr(s.ExternalIP, s.ExternalPort) 92 | } 93 | 94 | func (s *ServiceRegistration) InternalAddr() string { 95 | return s.addr(s.InternalIP, s.InternalPort) 96 | } 97 | -------------------------------------------------------------------------------- /config/store.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | "strings" 8 | "time" 9 | 10 | "github.com/fsouza/go-dockerclient" 11 | "github.com/litl/galaxy/log" 12 | "github.com/litl/galaxy/utils" 13 | ) 14 | 15 | const ( 16 | DefaultTTL = 60 17 | ) 18 | 19 | type HostInfo struct { 20 | HostIP string 21 | // The Pool field is currently only used for commander dump and restore 22 | Pool string 23 | } 24 | 25 | type Store struct { 26 | Backend Backend 27 | TTL uint64 28 | pollCh chan bool 29 | restartChan chan *ConfigChange 30 | } 31 | 32 | func NewStore(ttl uint64, registryURL string) *Store { 33 | s := &Store{ 34 | TTL: ttl, 35 | pollCh: make(chan bool), 36 | restartChan: make(chan *ConfigChange, 10), 37 | } 38 | 39 | u, err := url.Parse(registryURL) 40 | if err != nil { 41 | log.Fatalf("ERROR: Unable to parse %s", err) 42 | } 43 | 44 | switch strings.ToLower(u.Scheme) { 45 | case "redis": 46 | s.Backend = &RedisBackend{ 47 | RedisHost: u.Host, 48 | } 49 | s.Backend.connect() 50 | case "consul": 51 | s.Backend = NewConsulBackend() 52 | default: 53 | log.Fatalf("ERROR: Unsupported registry backend: %s", u) 54 | } 55 | 56 | return s 57 | } 58 | 59 | // FIXME: We still have a function that returns just an *AppConfig for the 60 | // RedisBackend. Unify these somehow, and preferebly decouple this from 61 | // config.Store. 62 | func (s *Store) NewAppConfig(app, version string) App { 63 | var appCfg App 64 | switch s.Backend.(type) { 65 | case *RedisBackend: 66 | appCfg = &AppConfig{ 67 | name: app, 68 | versionVMap: utils.NewVersionedMap(), 69 | environmentVMap: utils.NewVersionedMap(), 70 | portsVMap: utils.NewVersionedMap(), 71 | runtimeVMap: utils.NewVersionedMap(), 72 | } 73 | case *ConsulBackend: 74 | appCfg = &AppDefinition{ 75 | AppName: app, 76 | Environment: make(map[string]string), 77 | } 78 | default: 79 | panic("unknown backend") 80 | } 81 | 82 | appCfg.SetVersion(version) 83 | return appCfg 84 | } 85 | 86 | func (s *Store) PoolExists(env, pool string) (bool, error) { 87 | pools, err := s.ListPools(env) 88 | if err != nil { 89 | return false, err 90 | } 91 | 92 | return utils.StringInSlice(pool, pools), nil 93 | } 94 | 95 | func (s *Store) AppExists(app, env string) (bool, error) { 96 | return s.Backend.AppExists(app, env) 97 | } 98 | 99 | func (s *Store) ListAssignments(env, pool string) ([]string, error) { 100 | return s.Backend.ListAssignments(env, pool) 101 | } 102 | 103 | func (s *Store) ListAssignedPools(env, app string) ([]string, error) { 104 | pools, err := s.ListPools(env) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | assignments := []string{} 110 | for _, pool := range pools { 111 | apps, err := s.ListAssignments(env, pool) 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | if utils.StringInSlice(app, apps) && !utils.StringInSlice(pool, assignments) { 117 | assignments = append(assignments, pool) 118 | } 119 | } 120 | return assignments, nil 121 | } 122 | 123 | func (s *Store) AssignApp(app, env, pool string) (bool, error) { 124 | if exists, err := s.AppExists(app, env); !exists || err != nil { 125 | return false, err 126 | } 127 | 128 | added, err := s.Backend.AssignApp(app, env, pool) 129 | if err != nil { 130 | return false, err 131 | } 132 | 133 | err = s.NotifyRestart(app, env) 134 | if err != nil { 135 | return added, err 136 | } 137 | 138 | return added, nil 139 | } 140 | 141 | func (s *Store) UnassignApp(app, env, pool string) (bool, error) { 142 | removed, err := s.Backend.UnassignApp(app, env, pool) 143 | if !removed || err != nil { 144 | return removed, err 145 | } 146 | 147 | err = s.NotifyRestart(app, env) 148 | if err != nil { 149 | return removed, err 150 | } 151 | 152 | return removed, nil 153 | } 154 | 155 | func (s *Store) CreatePool(name, env string) (bool, error) { 156 | return s.Backend.CreatePool(env, name) 157 | } 158 | 159 | func (s *Store) DeletePool(pool, env string) (bool, error) { 160 | assignments, err := s.ListAssignments(env, pool) 161 | if err != nil { 162 | return false, err 163 | } 164 | 165 | if len(assignments) > 0 { 166 | return false, nil 167 | } 168 | 169 | return s.Backend.DeletePool(env, pool) 170 | } 171 | 172 | func (s *Store) ListPools(env string) ([]string, error) { 173 | return s.Backend.ListPools(env) 174 | } 175 | 176 | func (s *Store) CreateApp(app, env string) (bool, error) { 177 | if exists, err := s.AppExists(app, env); exists || err != nil { 178 | return false, err 179 | } 180 | 181 | return s.Backend.CreateApp(app, env) 182 | 183 | } 184 | 185 | func (s *Store) DeleteApp(app, env string) (bool, error) { 186 | 187 | pools, err := s.ListPools(env) 188 | if err != nil { 189 | return false, err 190 | } 191 | 192 | for _, pool := range pools { 193 | assignments, err := s.ListAssignments(env, pool) 194 | if err != nil { 195 | return false, err 196 | } 197 | if utils.StringInSlice(app, assignments) { 198 | return false, errors.New(fmt.Sprintf("app is assigned to pool %s", pool)) 199 | } 200 | } 201 | 202 | svcCfg, err := s.Backend.GetApp(app, env) 203 | if err != nil { 204 | return false, err 205 | } 206 | 207 | if svcCfg == nil { 208 | return true, nil 209 | } 210 | 211 | deleted, err := s.Backend.DeleteApp(svcCfg, env) 212 | if !deleted || err != nil { 213 | return deleted, err 214 | } 215 | 216 | err = s.NotifyEnvChanged(env) 217 | if err != nil { 218 | return deleted, err 219 | } 220 | 221 | return true, nil 222 | } 223 | 224 | func (s *Store) ListApps(env string) ([]App, error) { 225 | return s.Backend.ListApps(env) 226 | } 227 | 228 | func (s *Store) ListEnvs() ([]string, error) { 229 | return s.Backend.ListEnvs() 230 | } 231 | 232 | func (s *Store) GetApp(app, env string) (App, error) { 233 | exists, err := s.AppExists(app, env) 234 | if err != nil { 235 | return nil, err 236 | } 237 | 238 | if !exists { 239 | return nil, fmt.Errorf("app %s does not exist", app) 240 | } 241 | 242 | return s.Backend.GetApp(app, env) 243 | } 244 | 245 | func (s *Store) UpdateApp(svcCfg App, env string) (bool, error) { 246 | updated, err := s.Backend.UpdateApp(svcCfg, env) 247 | if !updated || err != nil { 248 | return updated, err 249 | } 250 | 251 | err = s.NotifyEnvChanged(env) 252 | if err != nil { 253 | return false, err 254 | } 255 | return true, nil 256 | } 257 | 258 | func (s *Store) UpdateHost(env, pool string, host HostInfo) error { 259 | return s.Backend.UpdateHost(env, pool, host) 260 | } 261 | 262 | func (s *Store) ListHosts(env, pool string) ([]HostInfo, error) { 263 | return s.Backend.ListHosts(env, pool) 264 | } 265 | 266 | func (s *Store) DeleteHost(env, pool string, host HostInfo) error { 267 | return s.Backend.DeleteHost(env, pool, host) 268 | } 269 | 270 | func (s *Store) RegisterService(env, pool, hostIP string, container *docker.Container) (*ServiceRegistration, error) { 271 | 272 | environment := s.EnvFor(container) 273 | 274 | name := environment["GALAXY_APP"] 275 | if name == "" { 276 | return nil, fmt.Errorf("GALAXY_APP not set on container %s", container.ID[0:12]) 277 | } 278 | 279 | serviceRegistration := newServiceRegistration(container, hostIP, environment["GALAXY_PORT"]) 280 | serviceRegistration.Name = name 281 | serviceRegistration.ImageId = container.Config.Image 282 | 283 | vhosts := environment["VIRTUAL_HOST"] 284 | if strings.TrimSpace(vhosts) != "" { 285 | serviceRegistration.VirtualHosts = strings.Split(vhosts, ",") 286 | } 287 | 288 | errorPages := make(map[string]string) 289 | 290 | // scan environment variables for the VIRTUAL_HOST_%d pattern 291 | // but save the original variable and url. 292 | for vhostCode, url := range environment { 293 | code := 0 294 | n, err := fmt.Sscanf(vhostCode, "VIRTUAL_HOST_%d", &code) 295 | if err != nil || n == 0 { 296 | continue 297 | } 298 | 299 | errorPages[vhostCode] = url 300 | } 301 | 302 | if len(errorPages) > 0 { 303 | serviceRegistration.ErrorPages = errorPages 304 | } 305 | 306 | serviceRegistration.Expires = time.Now().UTC().Add(time.Duration(s.TTL) * time.Second) 307 | 308 | err := s.Backend.RegisterService(env, pool, serviceRegistration) 309 | return serviceRegistration, err 310 | } 311 | 312 | func (s *Store) UnRegisterService(env, pool, hostIP string, container *docker.Container) (*ServiceRegistration, error) { 313 | 314 | environment := s.EnvFor(container) 315 | 316 | name := environment["GALAXY_APP"] 317 | if name == "" { 318 | return nil, fmt.Errorf("GALAXY_APP not set on container %s", container.ID[0:12]) 319 | } 320 | 321 | registration, err := s.Backend.UnregisterService(env, pool, hostIP, name, container.ID) 322 | if err != nil || registration == nil { 323 | return registration, err 324 | } 325 | 326 | return registration, nil 327 | } 328 | 329 | func (s *Store) GetServiceRegistration(env, pool, hostIP string, container *docker.Container) (*ServiceRegistration, error) { 330 | 331 | environment := s.EnvFor(container) 332 | 333 | name := environment["GALAXY_APP"] 334 | if name == "" { 335 | return nil, fmt.Errorf("GALAXY_APP not set on container %s", container.ID[0:12]) 336 | } 337 | 338 | serviceReg, err := s.Backend.GetServiceRegistration(env, pool, hostIP, name, container.ID) 339 | if err != nil { 340 | return nil, err 341 | } 342 | return serviceReg, nil 343 | } 344 | 345 | func (s *Store) IsRegistered(env, pool, hostIP string, container *docker.Container) (bool, error) { 346 | 347 | reg, err := s.GetServiceRegistration(env, pool, hostIP, container) 348 | return reg != nil, err 349 | } 350 | 351 | func (s *Store) ListRegistrations(env string) ([]ServiceRegistration, error) { 352 | return s.Backend.ListRegistrations(env) 353 | } 354 | 355 | func (s *Store) EnvFor(container *docker.Container) map[string]string { 356 | env := map[string]string{} 357 | for _, item := range container.Config.Env { 358 | sep := strings.Index(item, "=") 359 | if sep < 0 { 360 | continue 361 | } 362 | k, v := item[0:sep], item[sep+1:] 363 | env[k] = v 364 | } 365 | return env 366 | } 367 | -------------------------------------------------------------------------------- /config/store_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | func NewTestStore() (*Store, *MemoryBackend) { 9 | r := &Store{} 10 | b := NewMemoryBackend() 11 | r.Backend = b 12 | return r, b 13 | } 14 | 15 | func TestAppNotExists(t *testing.T) { 16 | r, _ := NewTestStore() 17 | 18 | if exists, err := r.AppExists("foo", "dev"); exists || err != nil { 19 | t.Errorf("AppExists(%q) = %t, %v, want %t, %v", 20 | "foo", exists, err, false, nil) 21 | } 22 | } 23 | 24 | func TestGetNonExistentApp(t *testing.T) { 25 | r, _ := NewTestStore() 26 | _, err := r.GetApp("bad", "bogus") 27 | if err == nil { 28 | t.Errorf("GetApp() should have returned an error. got nil") 29 | } 30 | } 31 | 32 | func TestAppExists(t *testing.T) { 33 | r, _ := NewTestStore() 34 | assertAppCreated(t, r, "app") 35 | assertAppExists(t, r, "app") 36 | } 37 | 38 | func TestListAssignmentKeyFormat(t *testing.T) { 39 | r, b := NewTestStore() 40 | 41 | b.MembersFunc = func(key string) ([]string, error) { 42 | if key != "dev/pools/foo" { 43 | t.Errorf("ListAssignments(%q) wrong key, want %s", key, "dev/pools/foo") 44 | } 45 | return []string{}, nil 46 | } 47 | 48 | r.ListAssignments("dev", "foo") 49 | } 50 | 51 | func TestListAssignmentsEmpty(t *testing.T) { 52 | r, _ := NewTestStore() 53 | 54 | assignments, err := r.ListAssignments("dev", "foo") 55 | if err != nil { 56 | t.Error(err) 57 | } 58 | 59 | if len(assignments) != 0 { 60 | t.Errorf("ListAssignments(%q) = %d, want %d", "foo", len(assignments), 0) 61 | } 62 | } 63 | 64 | func TestListAssignmentsNotEmpty(t *testing.T) { 65 | r, _ := NewTestStore() 66 | 67 | assertPoolCreated(t, r, "web") 68 | for _, k := range []string{"one", "two"} { 69 | assertAppCreated(t, r, k) 70 | if assigned, err := r.AssignApp(k, "dev", "web"); !assigned || err != nil { 71 | t.Fatalf("AssignApp(%q) = %t, %v, want %t, %v", k, assigned, err, true, nil) 72 | } 73 | } 74 | 75 | var assignments []string 76 | var err error 77 | if assignments, err = r.ListAssignments("dev", "web"); len(assignments) != 2 || err != nil { 78 | t.Fatalf("ListAssignments(%q) = %d, %v, want %d, %v", "web", len(assignments), err, 2, nil) 79 | } 80 | 81 | if assignments[0] != "one" { 82 | t.Fatalf("assignments[0] = %v, want %v", assignments[0], "one") 83 | } 84 | 85 | if assignments[1] != "two" { 86 | t.Fatalf("assignments[1] = %v, want %v", assignments[0], "two") 87 | } 88 | } 89 | 90 | func TestAssignAppNotExists(t *testing.T) { 91 | r, _ := NewTestStore() 92 | 93 | assigned, err := r.AssignApp("foo", "dev", "web") 94 | if assigned { 95 | t.Errorf("AssignApp(%q) = %t, want %t", "foo", assigned, false) 96 | } 97 | 98 | if err != nil { 99 | t.Error(err) 100 | } 101 | } 102 | 103 | func TestAssignAppPoolExists(t *testing.T) { 104 | r, _ := NewTestStore() 105 | 106 | assertAppCreated(t, r, "app") 107 | 108 | if assigned, err := r.AssignApp("app", "dev", "web"); !assigned || err != nil { 109 | t.Errorf("AssignApp(%q) = %t, %v, want %t, %v", "app", assigned, err, 110 | true, nil) 111 | } 112 | } 113 | 114 | func TestAssignAppAddMemberFail(t *testing.T) { 115 | r, b := NewTestStore() 116 | 117 | assertAppCreated(t, r, "app") 118 | assertPoolCreated(t, r, "web") 119 | 120 | b.AssignAppFunc = func(app, env, pool string) (bool, error) { 121 | return false, errors.New("something failed") 122 | } 123 | 124 | if assigned, err := r.AssignApp("app", "dev", "web"); assigned || err == nil { 125 | t.Errorf("AssignApp(%q) = %t, %v, want %t, %v", "app", assigned, err, false, 126 | errors.New("something failed")) 127 | } 128 | } 129 | 130 | func TestAssignAppNotifyFail(t *testing.T) { 131 | r, b := NewTestStore() 132 | 133 | assertAppCreated(t, r, "app") 134 | assertPoolCreated(t, r, "web") 135 | 136 | b.NotifyFunc = func(key, value string) (int, error) { 137 | return 0, errors.New("something failed") 138 | } 139 | 140 | if assigned, err := r.AssignApp("app", "dev", "web"); !assigned || err == nil { 141 | t.Errorf("AssignApp(%q) = %t, %v, want %t, %v", "app", assigned, err, false, 142 | errors.New("something failed")) 143 | } 144 | } 145 | 146 | func TestUnassignAppNotExists(t *testing.T) { 147 | r, _ := NewTestStore() 148 | 149 | if unassigned, err := r.UnassignApp("foo", "dev", "web"); unassigned || err != nil { 150 | t.Errorf("UnAssignApp(%q) = %t, %v, want %t, %v", "foo", unassigned, err, false, nil) 151 | } 152 | } 153 | 154 | func TestUnassignAppRemoveMemberFail(t *testing.T) { 155 | r, b := NewTestStore() 156 | 157 | assertAppCreated(t, r, "app") 158 | assertPoolCreated(t, r, "web") 159 | 160 | if assigned, err := r.AssignApp("app", "dev", "web"); !assigned || err != nil { 161 | t.Errorf("AssignApp(%q) = %t, %v, want %t, %v", "app", assigned, err, true, nil) 162 | } 163 | 164 | b.UnassignAppFunc = func(app, env, pool string) (bool, error) { 165 | return false, errors.New("something failed") 166 | } 167 | 168 | if unassigned, err := r.UnassignApp("foo", "dev", "web"); unassigned || err == nil { 169 | t.Errorf("UnAssignApp(%q) = %t, %v, want %t, %v", "foo", unassigned, err, 170 | false, errors.New("something failed")) 171 | } 172 | } 173 | 174 | func TestUnassignAppAddMemberNotifyRestart(t *testing.T) { 175 | r, b := NewTestStore() 176 | 177 | assertAppCreated(t, r, "app") 178 | assertPoolCreated(t, r, "web") 179 | 180 | if assigned, err := r.AssignApp("app", "dev", "web"); !assigned || err != nil { 181 | t.Errorf("AssignApp() = %t, %v, want %t, %v", assigned, err, true, nil) 182 | } 183 | 184 | b.NotifyFunc = func(key, value string) (int, error) { 185 | if key != "galaxy-dev" { 186 | t.Errorf("UnassignApp(%q) wrong notify key, want %s. got %s", "app", key, "galaxy-dev") 187 | } 188 | 189 | if value != "restart app" { 190 | t.Errorf("UnassignApp(%q) wrong notify value, want %s. got %s", "app", value, "restart app") 191 | } 192 | return 1, nil 193 | } 194 | if unassigned, err := r.UnassignApp("app", "dev", "web"); !unassigned || err != nil { 195 | t.Errorf("UnAssignApp(%q) = %t, %v, want %t, %v", "app", unassigned, err, true, nil) 196 | } 197 | } 198 | 199 | func TestUnassignAppNotifyFailed(t *testing.T) { 200 | r, b := NewTestStore() 201 | 202 | assertAppCreated(t, r, "app") 203 | assertPoolCreated(t, r, "web") 204 | 205 | if assigned, err := r.AssignApp("app", "dev", "web"); !assigned || err != nil { 206 | t.Errorf("AssignApp() = %t, %v, want %t, %v", assigned, err, true, nil) 207 | } 208 | 209 | b.NotifyFunc = func(key, value string) (int, error) { 210 | return 0, errors.New("something failed") 211 | } 212 | 213 | if unassigned, err := r.UnassignApp("app", "dev", "web"); !unassigned || err == nil { 214 | t.Errorf("UnAssignApp(%q) = %t, %v, want %t, %v", "app", unassigned, err, true, nil) 215 | } 216 | 217 | } 218 | 219 | func TestCreatePool(t *testing.T) { 220 | r, _ := NewTestStore() 221 | assertPoolCreated(t, r, "web") 222 | } 223 | 224 | func TestCreatePoolAddMemberFailedl(t *testing.T) { 225 | r, b := NewTestStore() 226 | b.CreatePoolFunc = func(env, pool string) (bool, error) { 227 | return false, errors.New("something failed") 228 | } 229 | 230 | if created, err := r.CreatePool("web", "dev"); created || err == nil { 231 | t.Errorf("CreatePool(%q) = %t, %v, want %t, %v", "web", created, err, true, nil) 232 | } 233 | } 234 | 235 | func TestDeletePool(t *testing.T) { 236 | r, _ := NewTestStore() 237 | 238 | assertPoolCreated(t, r, "web") 239 | 240 | if exists, err := r.PoolExists("dev", "web"); !exists || err != nil { 241 | t.Errorf("PoolExists()) = %t, %v, want %t, %v", exists, err, true, nil) 242 | } 243 | 244 | if deleted, err := r.DeletePool("web", "dev"); !deleted || err != nil { 245 | t.Errorf("DeletePool(%q) = %t, %v, want %t, %v", "web", deleted, err, true, nil) 246 | } 247 | } 248 | 249 | func TestDeletePoolHasAssignments(t *testing.T) { 250 | r, _ := NewTestStore() 251 | 252 | assertAppCreated(t, r, "app") 253 | assertPoolCreated(t, r, "web") 254 | 255 | // This is weird. AssignApp should probably take app & pool as params. 256 | if assigned, err := r.AssignApp("app", "dev", "web"); !assigned || err != nil { 257 | t.Errorf("AssignApp() = %t, %v, want %t, %v", assigned, err, true, nil) 258 | } 259 | 260 | // Should fail. Can't delete a pool if apps are assigned 261 | if deleted, err := r.DeletePool("web", "dev"); deleted || err != nil { 262 | t.Errorf("DeletePool(%q) = %t, %v, want %t, %v", "web", deleted, err, false, nil) 263 | } 264 | } 265 | 266 | func TestListPools(t *testing.T) { 267 | r, _ := NewTestStore() 268 | 269 | for _, pool := range []string{"one", "two"} { 270 | assertPoolCreated(t, r, pool) 271 | } 272 | 273 | if pools, err := r.ListPools("dev"); len(pools) == 0 || err != nil { 274 | t.Errorf("ListPools() = %d, %v, want %d, %v", len(pools), err, 2, nil) 275 | } 276 | } 277 | 278 | func TestCreateApp(t *testing.T) { 279 | r, _ := NewTestStore() 280 | 281 | assertAppCreated(t, r, "app") 282 | } 283 | 284 | func TestCreateAppAlreadyExists(t *testing.T) { 285 | r, _ := NewTestStore() 286 | 287 | assertAppCreated(t, r, "app") 288 | 289 | if created, err := r.CreateApp("app", "dev"); created || err != nil { 290 | t.Fatalf("CreateApp() = %t, %v, want %t, %v", 291 | created, err, 292 | false, nil) 293 | } 294 | } 295 | 296 | func TestCreateAppError(t *testing.T) { 297 | r, b := NewTestStore() 298 | 299 | b.CreateAppFunc = func(app, env string) (bool, error) { 300 | return false, errors.New("something failed") 301 | } 302 | 303 | if created, err := r.CreateApp("foo", "dev"); created || err == nil { 304 | t.Fatalf("CreateApp() = %t, %v, want %t, %v", 305 | created, err, 306 | false, errors.New("something failed")) 307 | } 308 | } 309 | 310 | func TestDeleteApp(t *testing.T) { 311 | r, _ := NewTestStore() 312 | 313 | assertAppCreated(t, r, "app") 314 | assertAppExists(t, r, "app") 315 | 316 | if deleted, err := r.DeleteApp("app", "dev"); !deleted || err != nil { 317 | t.Fatalf("DeleteApp(%q) = %t, %v, want %t, %v", "app", deleted, err, 318 | true, nil) 319 | } 320 | } 321 | 322 | func TestDeleteAppStillAssigned(t *testing.T) { 323 | r, _ := NewTestStore() 324 | 325 | assertAppCreated(t, r, "app") 326 | assertAppExists(t, r, "app") 327 | assertPoolCreated(t, r, "web") 328 | 329 | if assigned, err := r.AssignApp("app", "dev", "web"); !assigned || err != nil { 330 | t.Fatalf("AssignApp(%q) = %t, %v, want %t, %v", "app", assigned, err, 331 | true, nil) 332 | } 333 | 334 | if deleted, err := r.DeleteApp("app", "dev"); deleted || err == nil { 335 | t.Fatalf("DeleteApp(%q) = %t, %v, want %t, %v", "app", deleted, err, 336 | false, errors.New("app is assigned to pool web")) 337 | } 338 | } 339 | 340 | func TestListApps(t *testing.T) { 341 | r, _ := NewTestStore() 342 | 343 | if apps, err := r.ListApps("dev"); len(apps) > 0 || err != nil { 344 | t.Fatalf("ListApps() = %d, %v, want %d, %v", len(apps), err, 345 | 0, nil) 346 | } 347 | 348 | for _, k := range []string{"one", "two"} { 349 | assertAppCreated(t, r, k) 350 | } 351 | 352 | if apps, err := r.ListApps("dev"); len(apps) != 2 || err != nil { 353 | t.Fatalf("ListApps() = %d, %v, want %d, %v", len(apps), err, 354 | 2, nil) 355 | } 356 | } 357 | 358 | func TestListAppsIgnoreSpecialKeys(t *testing.T) { 359 | r, b := NewTestStore() 360 | 361 | b.maps["dev/hosts/environment"] = make(map[string]string) 362 | 363 | if apps, err := r.ListApps("dev"); len(apps) > 0 || err != nil { 364 | t.Fatalf("ListApps() = %d, %v, want %d, %v", len(apps), err, 365 | 0, nil) 366 | } 367 | } 368 | 369 | func TestListEnvs(t *testing.T) { 370 | r, b := NewTestStore() 371 | 372 | b.assignments["dev/web"] = []string{} 373 | b.assignments["prod/web"] = []string{} 374 | b.assignments["prod/batch"] = []string{} 375 | 376 | if apps, err := r.ListEnvs(); len(apps) != 2 || err != nil { 377 | t.Fatalf("ListApps() = %d, %v, want %d, %v", len(apps), err, 378 | 2, nil) 379 | } 380 | } 381 | 382 | func assertAppCreated(t *testing.T, r *Store, app string) { 383 | if created, err := r.CreateApp(app, "dev"); !created || err != nil { 384 | t.Fatalf("CreateApp(%q) = %t, %v, want %t, %v", app, 385 | created, err, 386 | true, nil) 387 | } 388 | } 389 | 390 | func assertAppExists(t *testing.T, r *Store, app string) { 391 | if exists, err := r.AppExists(app, "dev"); !exists || err != nil { 392 | t.Fatalf("AppExists(%q) = %t, %v, want %t, %v", app, exists, err, 393 | true, nil) 394 | } 395 | } 396 | 397 | func assertPoolCreated(t *testing.T, r *Store, pool string) { 398 | if created, err := r.CreatePool(pool, "dev"); !created || err != nil { 399 | t.Errorf("CreatePool(%q) = %t, %v, want %t, %v", pool, created, err, true, nil) 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /discovery/discovery.go: -------------------------------------------------------------------------------- 1 | package discovery 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "time" 7 | 8 | "github.com/litl/galaxy/config" 9 | "github.com/litl/galaxy/log" 10 | "github.com/litl/galaxy/runtime" 11 | "github.com/litl/galaxy/utils" 12 | "github.com/ryanuber/columnize" 13 | 14 | shuttle "github.com/litl/shuttle/client" 15 | ) 16 | 17 | func Status(serviceRuntime *runtime.ServiceRuntime, configStore *config.Store, env, pool, hostIP string) error { 18 | 19 | containers, err := serviceRuntime.ManagedContainers() 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | //FIXME: addresses, port, and expires missing in output 25 | columns := []string{ 26 | "APP | CONTAINER ID | IMAGE | EXTERNAL | INTERNAL | PORT | CREATED | EXPIRES"} 27 | 28 | for _, container := range containers { 29 | name := serviceRuntime.EnvFor(container)["GALAXY_APP"] 30 | registered, err := configStore.GetServiceRegistration( 31 | env, pool, hostIP, container) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | if registered != nil { 37 | columns = append(columns, 38 | strings.Join([]string{ 39 | registered.Name, 40 | registered.ContainerID[0:12], 41 | registered.Image, 42 | registered.ExternalAddr(), 43 | registered.InternalAddr(), 44 | registered.Port, 45 | utils.HumanDuration(time.Now().UTC().Sub(registered.StartedAt)) + " ago", 46 | "In " + utils.HumanDuration(registered.Expires.Sub(time.Now().UTC())), 47 | }, " | ")) 48 | 49 | } else { 50 | columns = append(columns, 51 | strings.Join([]string{ 52 | name, 53 | container.ID[0:12], 54 | container.Image, 55 | "", 56 | "", 57 | "", 58 | utils.HumanDuration(time.Now().Sub(container.Created)) + " ago", 59 | "", 60 | }, " | ")) 61 | } 62 | 63 | } 64 | 65 | result := columnize.SimpleFormat(columns) 66 | log.Println(result) 67 | return nil 68 | } 69 | 70 | func Unregister(serviceRuntime *runtime.ServiceRuntime, configStore *config.Store, 71 | env, pool, hostIP, shuttleAddr string) { 72 | unregisterShuttle(configStore, env, hostIP, shuttleAddr) 73 | serviceRuntime.UnRegisterAll(env, pool, hostIP) 74 | os.Exit(0) 75 | } 76 | 77 | func RegisterAll(serviceRuntime *runtime.ServiceRuntime, configStore *config.Store, env, pool, hostIP, shuttleAddr string, loggedOnce bool) { 78 | columns := []string{"CONTAINER ID | IMAGE | EXTERNAL | INTERNAL | CREATED | EXPIRES"} 79 | 80 | registrations, err := serviceRuntime.RegisterAll(env, pool, hostIP) 81 | if err != nil { 82 | log.Errorf("ERROR: Unable to register containers: %s", err) 83 | return 84 | } 85 | 86 | fn := log.Debugf 87 | if !loggedOnce { 88 | fn = log.Printf 89 | } 90 | 91 | for _, registration := range registrations { 92 | if !loggedOnce || time.Now().Unix()%60 < 10 { 93 | fn("Registered %s running as %s for %s%s", strings.TrimPrefix(registration.ContainerName, "/"), 94 | registration.ContainerID[0:12], registration.Name, locationAt(registration)) 95 | } 96 | 97 | columns = append(columns, strings.Join([]string{ 98 | registration.ContainerID[0:12], 99 | registration.Image, 100 | registration.ExternalAddr(), 101 | registration.InternalAddr(), 102 | utils.HumanDuration(time.Now().Sub(registration.StartedAt)) + " ago", 103 | "In " + utils.HumanDuration(registration.Expires.Sub(time.Now().UTC())), 104 | }, " | ")) 105 | 106 | } 107 | 108 | registerShuttle(configStore, env, pool, shuttleAddr) 109 | } 110 | 111 | func Register(serviceRuntime *runtime.ServiceRuntime, configStore *config.Store, env, pool, hostIP, shuttleAddr string) { 112 | if shuttleAddr != "" { 113 | client = shuttle.NewClient(shuttleAddr) 114 | } 115 | 116 | RegisterAll(serviceRuntime, configStore, env, pool, hostIP, shuttleAddr, false) 117 | 118 | containerEvents := make(chan runtime.ContainerEvent) 119 | err := serviceRuntime.RegisterEvents(env, pool, hostIP, containerEvents) 120 | if err != nil { 121 | log.Printf("ERROR: Unable to register docker event listener: %s", err) 122 | } 123 | 124 | for { 125 | 126 | select { 127 | case ce := <-containerEvents: 128 | switch ce.Status { 129 | case "start": 130 | reg, err := configStore.RegisterService(env, pool, hostIP, ce.Container) 131 | if err != nil { 132 | log.Errorf("ERROR: Unable to register container: %s", err) 133 | continue 134 | } 135 | 136 | log.Printf("Registered %s running as %s for %s%s", strings.TrimPrefix(reg.ContainerName, "/"), 137 | reg.ContainerID[0:12], reg.Name, locationAt(reg)) 138 | registerShuttle(configStore, env, pool, shuttleAddr) 139 | case "die", "stop": 140 | reg, err := configStore.UnRegisterService(env, pool, hostIP, ce.Container) 141 | if err != nil { 142 | log.Errorf("ERROR: Unable to unregister container: %s", err) 143 | continue 144 | } 145 | 146 | if reg != nil { 147 | log.Printf("Unregistered %s running as %s for %s%s", strings.TrimPrefix(reg.ContainerName, "/"), 148 | reg.ContainerID[0:12], reg.Name, locationAt(reg)) 149 | } 150 | RegisterAll(serviceRuntime, configStore, env, pool, hostIP, shuttleAddr, true) 151 | pruneShuttleBackends(configStore, env, shuttleAddr) 152 | } 153 | 154 | case <-time.After(10 * time.Second): 155 | RegisterAll(serviceRuntime, configStore, env, pool, hostIP, shuttleAddr, true) 156 | pruneShuttleBackends(configStore, env, shuttleAddr) 157 | } 158 | } 159 | } 160 | 161 | func locationAt(reg *config.ServiceRegistration) string { 162 | location := reg.ExternalAddr() 163 | if location != "" { 164 | location = " at " + location 165 | } 166 | return location 167 | } 168 | -------------------------------------------------------------------------------- /discovery/shuttle.go: -------------------------------------------------------------------------------- 1 | package discovery 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/litl/galaxy/config" 7 | "github.com/litl/galaxy/log" 8 | 9 | shuttle "github.com/litl/shuttle/client" 10 | ) 11 | 12 | var ( 13 | client *shuttle.Client 14 | ) 15 | 16 | func registerShuttle(configStore *config.Store, env, pool, shuttleAddr string) { 17 | if client == nil { 18 | return 19 | } 20 | 21 | registrations, err := configStore.ListRegistrations(env) 22 | if err != nil { 23 | log.Errorf("ERROR: Unable to list registrations: %s", err) 24 | return 25 | } 26 | 27 | backends := make(map[string]*shuttle.ServiceConfig) 28 | 29 | for _, r := range registrations { 30 | 31 | // No service ports exposed on the host, skip it. 32 | if r.ExternalAddr() == "" { 33 | continue 34 | } 35 | 36 | service := backends[r.Name] 37 | if service == nil { 38 | service = &shuttle.ServiceConfig{ 39 | Name: r.Name, 40 | VirtualHosts: r.VirtualHosts, 41 | } 42 | if r.Port != "" { 43 | service.Addr = "0.0.0.0:" + r.Port 44 | } 45 | backends[r.Name] = service 46 | } 47 | b := shuttle.BackendConfig{ 48 | Name: r.ContainerID[0:12], 49 | Addr: r.ExternalAddr(), 50 | CheckAddr: r.ExternalAddr(), 51 | } 52 | service.Backends = append(service.Backends, b) 53 | 54 | // lookup the VIRTUAL_HOST_%d environment variables and load them into the ServiceConfig 55 | errorPages := make(map[string][]int) 56 | for vhostCode, url := range r.ErrorPages { 57 | code := 0 58 | n, err := fmt.Sscanf(vhostCode, "VIRTUAL_HOST_%d", &code) 59 | if err != nil || n == 0 { 60 | continue 61 | } 62 | 63 | errorPages[url] = append(errorPages[url], code) 64 | } 65 | 66 | if len(errorPages) > 0 { 67 | service.ErrorPages = errorPages 68 | } 69 | 70 | app, err := configStore.GetApp(service.Name, env) 71 | if err != nil { 72 | log.Errorf("ERROR: Unable to get app for service %s: %s", service.Name, err) 73 | continue 74 | } 75 | 76 | service.MaintenanceMode = app.GetMaintenanceMode(pool) 77 | } 78 | 79 | for _, service := range backends { 80 | err := client.UpdateService(service) 81 | if err != nil { 82 | log.Errorf("ERROR: Unable to register shuttle service: %s", err) 83 | } 84 | } 85 | 86 | } 87 | 88 | func unregisterShuttle(configStore *config.Store, env, hostIP, shuttleAddr string) { 89 | 90 | if client == nil { 91 | return 92 | } 93 | 94 | registrations, err := configStore.ListRegistrations(env) 95 | if err != nil { 96 | log.Errorf("ERROR: Unable to list registrations: %s", err) 97 | return 98 | } 99 | 100 | backends := make(map[string]*shuttle.ServiceConfig) 101 | 102 | for _, r := range registrations { 103 | 104 | // Registration for a container on a different host? Skip it. 105 | if r.ExternalIP != hostIP { 106 | continue 107 | } 108 | 109 | // No service ports exposed on the host, skip it. 110 | if r.ExternalAddr() == "" || r.Port == "" { 111 | continue 112 | } 113 | 114 | service := backends[r.Name] 115 | if service == nil { 116 | service = &shuttle.ServiceConfig{ 117 | Name: r.Name, 118 | VirtualHosts: r.VirtualHosts, 119 | } 120 | if r.Port != "" { 121 | service.Addr = "0.0.0.0:" + r.Port 122 | } 123 | backends[r.Name] = service 124 | } 125 | b := shuttle.BackendConfig{ 126 | Name: r.ContainerID[0:12], 127 | Addr: r.ExternalAddr(), 128 | } 129 | service.Backends = append(service.Backends, b) 130 | } 131 | 132 | for _, service := range backends { 133 | 134 | err := client.RemoveService(service.Name) 135 | if err != nil { 136 | log.Errorf("ERROR: Unable to remove shuttle service: %s", err) 137 | } 138 | } 139 | 140 | } 141 | 142 | func pruneShuttleBackends(configStore *config.Store, env, shuttleAddr string) { 143 | if client == nil { 144 | return 145 | } 146 | 147 | config, err := client.GetConfig() 148 | if err != nil { 149 | log.Errorf("ERROR: Unable to get shuttle config: %s", err) 150 | return 151 | } 152 | 153 | registrations, err := configStore.ListRegistrations(env) 154 | if err != nil { 155 | log.Errorf("ERROR: Unable to list registrations: %s", err) 156 | return 157 | } 158 | 159 | // FIXME: THERE SHOULD HAVE BEEN AN ERROR IF `len(registrations) == 0` IS WRONG! 160 | if len(registrations) == 0 { 161 | // If there are no registrations, skip pruning it because we might be in a bad state and 162 | // don't want to inadvertently unregister everything. Shuttle will handle the down 163 | // nodes if they are really down. 164 | return 165 | } 166 | 167 | for _, service := range config.Services { 168 | 169 | app, err := configStore.GetApp(service.Name, env) 170 | if err != nil { 171 | log.Errorf("ERROR: Unable to load app %s: %s", app, err) 172 | continue 173 | } 174 | 175 | pools, err := configStore.ListAssignedPools(env, service.Name) 176 | if err != nil { 177 | log.Errorf("ERROR: Unable to list pool assignments for %s: %s", service.Name, err) 178 | continue 179 | } 180 | 181 | if app == nil || len(pools) == 0 { 182 | err := client.RemoveService(service.Name) 183 | if err != nil { 184 | log.Errorf("ERROR: Unable to remove service %s from shuttle: %s", service.Name, err) 185 | } 186 | log.Printf("Unregisterred shuttle service %s", service.Name) 187 | continue 188 | } 189 | 190 | for _, backend := range service.Backends { 191 | backendExists := false 192 | for _, r := range registrations { 193 | if backend.Name == r.ContainerID[0:12] { 194 | backendExists = true 195 | break 196 | } 197 | } 198 | 199 | if !backendExists { 200 | err := client.RemoveBackend(service.Name, backend.Name) 201 | if err != nil { 202 | log.Errorf("ERROR: Unable to remove backend %s from shuttle: %s", backend.Name, err) 203 | } 204 | log.Printf("Unregisterred shuttle backend %s", backend.Name) 205 | } 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /galaxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "os" 7 | "os/exec" 8 | "os/signal" 9 | "path/filepath" 10 | "strings" 11 | "sync" 12 | 13 | "github.com/litl/galaxy/commander" 14 | gconfig "github.com/litl/galaxy/config" 15 | "github.com/litl/galaxy/log" 16 | "github.com/litl/galaxy/runtime" 17 | "github.com/litl/galaxy/utils" 18 | 19 | "github.com/BurntSushi/toml" 20 | "github.com/codegangsta/cli" 21 | "github.com/ryanuber/columnize" 22 | ) 23 | 24 | var ( 25 | serviceRuntime *runtime.ServiceRuntime 26 | configStore *gconfig.Store 27 | 28 | initOnce sync.Once 29 | buildVersion string 30 | ) 31 | 32 | var config struct { 33 | Host string `toml:"host"` 34 | } 35 | 36 | func initStore(c *cli.Context) { 37 | configStore = gconfig.NewStore(uint64(c.Int("ttl")), utils.GalaxyRedisHost(c)) 38 | } 39 | 40 | // ensure the registry as a redis host, but only once 41 | func initRuntime(c *cli.Context) { 42 | serviceRuntime = runtime.NewServiceRuntime( 43 | configStore, 44 | "", 45 | "127.0.0.1", 46 | ) 47 | } 48 | 49 | func ensureAppParam(c *cli.Context, command string) string { 50 | app := c.Args().First() 51 | if app == "" { 52 | cli.ShowCommandHelp(c, command) 53 | log.Fatal("ERROR: app name missing") 54 | } 55 | 56 | exists, err := appExists(app, utils.GalaxyEnv(c)) 57 | if err != nil { 58 | log.Fatalf("ERROR: can't deteremine if %s exists: %s", app, err) 59 | } 60 | 61 | if !exists { 62 | log.Fatalf("ERROR: %s does not exist. Create it first.", app) 63 | } 64 | 65 | return app 66 | } 67 | 68 | func ensureEnvArg(c *cli.Context) { 69 | if utils.GalaxyEnv(c) == "" { 70 | log.Fatal("ERROR: env is required. Pass --env or set GALAXY_ENV") 71 | } 72 | } 73 | 74 | func ensurePoolArg(c *cli.Context) { 75 | if utils.GalaxyPool(c) == "" { 76 | log.Fatal("ERROR: pool is required. Pass --pool or set GALAXY_POOL") 77 | } 78 | } 79 | 80 | func appExists(app, env string) (bool, error) { 81 | return configStore.AppExists(app, env) 82 | } 83 | 84 | func appList(c *cli.Context) { 85 | initStore(c) 86 | err := commander.AppList(configStore, utils.GalaxyEnv(c)) 87 | if err != nil { 88 | log.Fatalf("ERROR: %s", err) 89 | } 90 | } 91 | 92 | func appCreate(c *cli.Context) { 93 | ensureEnvArg(c) 94 | initStore(c) 95 | 96 | app := c.Args().First() 97 | if app == "" { 98 | cli.ShowCommandHelp(c, "app:create") 99 | log.Fatal("ERROR: app name missing") 100 | } 101 | 102 | err := commander.AppCreate(configStore, app, utils.GalaxyEnv(c)) 103 | if err != nil { 104 | log.Fatalf("ERROR: %s", err) 105 | } 106 | } 107 | 108 | func appDelete(c *cli.Context) { 109 | ensureEnvArg(c) 110 | initStore(c) 111 | 112 | app := ensureAppParam(c, "app:delete") 113 | 114 | err := commander.AppDelete(configStore, app, utils.GalaxyEnv(c)) 115 | if err != nil { 116 | log.Fatalf("ERROR: %s", err) 117 | } 118 | } 119 | 120 | func appDeploy(c *cli.Context) { 121 | ensureEnvArg(c) 122 | initStore(c) 123 | initRuntime(c) 124 | 125 | app := ensureAppParam(c, "app:deploy") 126 | 127 | version := "" 128 | if len(c.Args().Tail()) == 1 { 129 | version = c.Args().Tail()[0] 130 | } 131 | 132 | if version == "" { 133 | log.Println("ERROR: version missing") 134 | cli.ShowCommandHelp(c, "app:deploy") 135 | return 136 | } 137 | 138 | err := commander.AppDeploy(configStore, serviceRuntime, app, utils.GalaxyEnv(c), version) 139 | if err != nil { 140 | log.Fatalf("ERROR: %s", err) 141 | } 142 | } 143 | 144 | func appRestart(c *cli.Context) { 145 | initStore(c) 146 | 147 | app := ensureAppParam(c, "app:restart") 148 | 149 | err := commander.AppRestart(configStore, app, utils.GalaxyEnv(c)) 150 | if err != nil { 151 | log.Fatalf("ERROR: %s", err) 152 | } 153 | } 154 | 155 | func appRun(c *cli.Context) { 156 | ensureEnvArg(c) 157 | initStore(c) 158 | initRuntime(c) 159 | 160 | app := ensureAppParam(c, "app:run") 161 | 162 | if len(c.Args()) < 2 { 163 | log.Fatalf("ERROR: Missing command to run.") 164 | return 165 | } 166 | 167 | err := commander.AppRun(configStore, serviceRuntime, app, utils.GalaxyEnv(c), c.Args()[1:]) 168 | if err != nil { 169 | log.Fatalf("ERROR: %s", err) 170 | } 171 | } 172 | 173 | func appShell(c *cli.Context) { 174 | ensureEnvArg(c) 175 | initStore(c) 176 | initRuntime(c) 177 | 178 | app := ensureAppParam(c, "app:shell") 179 | 180 | err := commander.AppShell(configStore, serviceRuntime, app, 181 | utils.GalaxyEnv(c), utils.GalaxyPool(c)) 182 | if err != nil { 183 | log.Fatalf("ERROR: %s", err) 184 | } 185 | } 186 | 187 | func configList(c *cli.Context) { 188 | ensureEnvArg(c) 189 | initStore(c) 190 | app := ensureAppParam(c, "config") 191 | 192 | err := commander.ConfigList(configStore, app, utils.GalaxyEnv(c)) 193 | if err != nil { 194 | log.Fatalf("ERROR: Unable to list config: %s.", err) 195 | return 196 | } 197 | } 198 | 199 | func configSet(c *cli.Context) { 200 | ensureEnvArg(c) 201 | initStore(c) 202 | app := ensureAppParam(c, "config:set") 203 | 204 | args := c.Args().Tail() 205 | err := commander.ConfigSet(configStore, app, utils.GalaxyEnv(c), args) 206 | 207 | if err != nil { 208 | log.Fatalf("ERROR: Unable to update config: %s.", err) 209 | return 210 | } 211 | } 212 | 213 | func configUnset(c *cli.Context) { 214 | ensureEnvArg(c) 215 | initStore(c) 216 | app := ensureAppParam(c, "config:unset") 217 | 218 | err := commander.ConfigUnset(configStore, app, utils.GalaxyEnv(c), c.Args().Tail()) 219 | if err != nil { 220 | log.Fatalf("ERROR: Unable to unset config: %s.", err) 221 | return 222 | } 223 | } 224 | 225 | func configGet(c *cli.Context) { 226 | ensureEnvArg(c) 227 | initStore(c) 228 | app := ensureAppParam(c, "config:get") 229 | 230 | err := commander.ConfigGet(configStore, app, utils.GalaxyEnv(c), c.Args().Tail()) 231 | 232 | if err != nil { 233 | log.Fatalf("ERROR: Unable to get config: %s.", err) 234 | return 235 | } 236 | } 237 | 238 | // Return the path for the config directory, and create it if it doesn't exist 239 | func cfgDir() string { 240 | homeDir := utils.HomeDir() 241 | if homeDir == "" { 242 | log.Fatal("ERROR: Unable to determine current home dir. Set $HOME.") 243 | } 244 | 245 | configDir := filepath.Join(homeDir, ".galaxy") 246 | _, err := os.Stat(configDir) 247 | if err != nil && os.IsNotExist(err) { 248 | err = os.Mkdir(configDir, 0700) 249 | if err != nil { 250 | log.Fatal("ERROR: cannot create config directory:", err) 251 | } 252 | } 253 | return configDir 254 | } 255 | 256 | func poolAssign(c *cli.Context) { 257 | ensureEnvArg(c) 258 | ensurePoolArg(c) 259 | initStore(c) 260 | 261 | app := ensureAppParam(c, "pool:assign") 262 | 263 | err := commander.AppAssign(configStore, app, utils.GalaxyEnv(c), utils.GalaxyPool(c)) 264 | if err != nil { 265 | log.Fatalf("ERROR: %s", err) 266 | } 267 | } 268 | 269 | func poolUnassign(c *cli.Context) { 270 | ensureEnvArg(c) 271 | ensurePoolArg(c) 272 | initStore(c) 273 | 274 | app := c.Args().First() 275 | if app == "" { 276 | cli.ShowCommandHelp(c, "pool:assign") 277 | log.Fatal("ERROR: app name missing") 278 | } 279 | 280 | err := commander.AppUnassign(configStore, app, utils.GalaxyEnv(c), utils.GalaxyPool(c)) 281 | if err != nil { 282 | log.Fatalf("ERROR: %s", err) 283 | } 284 | } 285 | 286 | func poolCreate(c *cli.Context) { 287 | ensureEnvArg(c) 288 | ensurePoolArg(c) 289 | initStore(c) 290 | created, err := configStore.CreatePool(utils.GalaxyPool(c), utils.GalaxyEnv(c)) 291 | if err != nil { 292 | log.Fatalf("ERROR: Could not create pool: %s", err) 293 | return 294 | } 295 | 296 | if created { 297 | log.Printf("Pool %s created\n", utils.GalaxyPool(c)) 298 | } else { 299 | log.Printf("Pool %s already exists\n", utils.GalaxyPool(c)) 300 | } 301 | } 302 | 303 | func poolUpdate(c *cli.Context) { 304 | ensureEnvArg(c) 305 | ensurePoolArg(c) 306 | } 307 | 308 | func poolList(c *cli.Context) { 309 | initStore(c) 310 | 311 | envs := []string{utils.GalaxyEnv(c)} 312 | if utils.GalaxyEnv(c) == "" { 313 | var err error 314 | envs, err = configStore.ListEnvs() 315 | if err != nil { 316 | log.Fatalf("ERROR: %s", err) 317 | } 318 | } 319 | 320 | columns := []string{"ENV | POOL | APPS "} 321 | 322 | for _, env := range envs { 323 | pools, err := configStore.ListPools(env) 324 | if err != nil { 325 | log.Fatalf("ERROR: cannot list pools: %s", err) 326 | return 327 | } 328 | 329 | if len(pools) == 0 { 330 | columns = append(columns, strings.Join([]string{ 331 | env, 332 | "", 333 | ""}, " | ")) 334 | continue 335 | } 336 | 337 | for _, pool := range pools { 338 | 339 | assigments, err := configStore.ListAssignments(env, pool) 340 | if err != nil { 341 | log.Fatalf("ERROR: cannot list pool assignments: %s", err) 342 | } 343 | 344 | columns = append(columns, strings.Join([]string{ 345 | env, 346 | pool, 347 | strings.Join(assigments, ",")}, " | ")) 348 | } 349 | 350 | } 351 | output := columnize.SimpleFormat(columns) 352 | log.Println(output) 353 | } 354 | 355 | func poolDelete(c *cli.Context) { 356 | ensureEnvArg(c) 357 | ensurePoolArg(c) 358 | initStore(c) 359 | empty, err := configStore.DeletePool(utils.GalaxyPool(c), utils.GalaxyEnv(c)) 360 | if err != nil { 361 | log.Fatalf("ERROR: Could not delete pool: %s", err) 362 | return 363 | } 364 | 365 | if empty { 366 | log.Printf("Pool %s deleted\n", utils.GalaxyPool(c)) 367 | 368 | } else { 369 | log.Printf("Pool %s has apps assigned. Unassign them first.\n", utils.GalaxyPool(c)) 370 | } 371 | } 372 | 373 | func loadConfig() { 374 | configFile := filepath.Join(cfgDir(), "galaxy.toml") 375 | 376 | _, err := os.Stat(configFile) 377 | if err == nil { 378 | if _, err := toml.DecodeFile(configFile, &config); err != nil { 379 | log.Fatalf("ERROR: Unable to logout: %s", err) 380 | return 381 | } 382 | } 383 | 384 | } 385 | 386 | func pgPsql(c *cli.Context) { 387 | ensureEnvArg(c) 388 | initStore(c) 389 | app := ensureAppParam(c, "pg:psql") 390 | 391 | appCfg, err := configStore.GetApp(app, utils.GalaxyEnv(c)) 392 | if err != nil { 393 | log.Fatalf("ERROR: Unable to run command: %s.", err) 394 | return 395 | } 396 | 397 | database_url := appCfg.Env()["DATABASE_URL"] 398 | if database_url == "" { 399 | log.Printf("No DATABASE_URL configured. Set one with config:set first.") 400 | return 401 | } 402 | 403 | if !strings.HasPrefix(database_url, "postgres://") { 404 | log.Printf("DATABASE_URL is not a postgres database.") 405 | return 406 | } 407 | 408 | if c.Bool("ro") { 409 | dbURL, err := url.Parse(database_url) 410 | if err != nil { 411 | log.Printf("Invalid DATABASE_URL: %s", database_url) 412 | return 413 | } 414 | 415 | qp, err := url.ParseQuery(dbURL.RawQuery) 416 | if err != nil { 417 | log.Printf("Invalid DATABASE_URL: %s", database_url) 418 | return 419 | } 420 | 421 | options := qp.Get("options") 422 | if options != "" { 423 | options += " " 424 | } 425 | options += fmt.Sprintf("-c default_transaction_read_only=true") 426 | qp.Set("options", options) 427 | 428 | dbURL.RawQuery = strings.Replace(qp.Encode(), "+", "%20", -1) 429 | 430 | database_url = dbURL.String() 431 | } 432 | 433 | cmd := exec.Command("psql", database_url) 434 | 435 | cmd.Stdin = os.Stdin 436 | cmd.Stdout = os.Stdout 437 | cmd.Stderr = os.Stderr 438 | 439 | // Ignore SIGINT while the process is running 440 | ch := make(chan os.Signal, 1) 441 | signal.Notify(ch, os.Interrupt) 442 | 443 | defer func() { 444 | signal.Stop(ch) 445 | close(ch) 446 | }() 447 | 448 | go func() { 449 | for { 450 | _, ok := <-ch 451 | if !ok { 452 | break 453 | } 454 | } 455 | }() 456 | 457 | err = cmd.Start() 458 | if err != nil { 459 | log.Fatal(err) 460 | } 461 | 462 | err = cmd.Wait() 463 | if err != nil { 464 | fmt.Printf("Command finished with error: %v\n", err) 465 | } 466 | } 467 | 468 | func main() { 469 | 470 | loadConfig() 471 | 472 | // Don't print date, etc. and print to stdout 473 | log.DefaultLogger = log.New(os.Stdout, "", log.INFO) 474 | log.DefaultLogger.SetFlags(0) 475 | 476 | app := cli.NewApp() 477 | app.Name = "galaxy" 478 | app.Usage = "galaxy cli" 479 | app.Version = buildVersion 480 | app.Flags = []cli.Flag{ 481 | cli.StringFlag{Name: "registry", Value: "", Usage: "host:port[,host:port,..]"}, 482 | cli.StringFlag{Name: "env", Value: "", Usage: "environment (dev, test, prod, etc.)"}, 483 | cli.StringFlag{Name: "pool", Value: "", Usage: "pool (web, worker, etc.)"}, 484 | } 485 | 486 | app.Commands = []cli.Command{ 487 | { 488 | Name: "app", 489 | Usage: "list the apps currently created", 490 | Action: appList, 491 | Description: "app", 492 | }, 493 | { 494 | Name: "app:backup", 495 | Usage: "backup app configs to a file or stdout", 496 | Action: appBackup, 497 | Description: "app:backup [app[,app2]]", 498 | Flags: []cli.Flag{ 499 | cli.StringFlag{Name: "file", Usage: "backup filename"}, 500 | }, 501 | }, 502 | { 503 | Name: "app:restore", 504 | Usage: "restore an app's config", 505 | Action: appRestore, 506 | Description: "app:restore [app[,app2]]", 507 | Flags: []cli.Flag{ 508 | cli.StringFlag{Name: "file", Usage: "backup filename"}, 509 | cli.BoolFlag{Name: "force", Usage: "force overwrite of existing config"}, 510 | }, 511 | }, 512 | { 513 | Name: "app:create", 514 | Usage: "create a new app", 515 | Action: appCreate, 516 | Description: "app:create", 517 | }, 518 | { 519 | Name: "app:delete", 520 | Usage: "delete a new app", 521 | Action: appDelete, 522 | Description: "app:delete", 523 | }, 524 | { 525 | Name: "app:deploy", 526 | Usage: "deploy a new version of an app", 527 | Action: appDeploy, 528 | Description: "app:deploy ", 529 | Flags: []cli.Flag{ 530 | cli.BoolFlag{Name: "force", Usage: "force pulling the image"}, 531 | }, 532 | }, 533 | { 534 | Name: "app:restart", 535 | Usage: "restart an app", 536 | Action: appRestart, 537 | Description: "app:restart ", 538 | }, 539 | { 540 | Name: "app:run", 541 | Usage: "run a command in a container", 542 | Action: appRun, 543 | Description: "app:run ", 544 | }, 545 | { 546 | Name: "app:shell", 547 | Usage: "run a bash shell in a container", 548 | Action: appShell, 549 | Description: "app:shell ", 550 | }, 551 | { 552 | Name: "config", 553 | Usage: "list the config values for an app", 554 | Action: configList, 555 | Description: "config ", 556 | }, 557 | { 558 | Name: "config:set", 559 | Usage: "set one or more configuration variables", 560 | Action: configSet, 561 | Description: "config:set KEY=VALUE [KEY=VALUE ...]", 562 | }, 563 | { 564 | Name: "config:unset", 565 | Usage: "unset one or more configuration variables", 566 | Action: configUnset, 567 | Description: "config:unset KEY [KEY ...]", 568 | }, 569 | { 570 | Name: "config:get", 571 | Usage: "display the config value for an app", 572 | Action: configGet, 573 | Description: "config:get KEY [KEY ...]", 574 | }, 575 | { 576 | Name: "pool", 577 | Usage: "list the pools", 578 | Action: poolList, 579 | Description: "pool", 580 | }, 581 | { 582 | Name: "pool:assign", 583 | Usage: "assign an app to a pool", 584 | Action: poolAssign, 585 | Description: "pool:assign", 586 | }, 587 | { 588 | Name: "pool:unassign", 589 | Usage: "unassign an app from a pool", 590 | Action: poolUnassign, 591 | Description: "pool:unassign", 592 | }, 593 | 594 | { 595 | Name: "pool:create", 596 | Usage: "create a pool", 597 | Action: poolCreate, 598 | Description: "pool:create", 599 | }, 600 | { 601 | Name: "pool:delete", 602 | Usage: "deletes a pool", 603 | Action: poolDelete, 604 | Description: "pool:delete", 605 | Flags: []cli.Flag{ 606 | cli.BoolFlag{Name: "y", Usage: "skip confirmation"}, 607 | }, 608 | }, 609 | { 610 | Name: "pg:psql", 611 | Usage: "connect to database using psql", 612 | Action: pgPsql, 613 | Description: "pg:psql ", 614 | Flags: []cli.Flag{ 615 | cli.BoolFlag{Name: "ro", Usage: "read-only connection"}, 616 | }, 617 | }, 618 | } 619 | app.Run(os.Args) 620 | } 621 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "io" 5 | golog "log" 6 | "os" 7 | 8 | "github.com/fatih/color" 9 | ) 10 | 11 | const ( 12 | ERROR = iota 13 | INFO 14 | WARN 15 | DEBUG 16 | ) 17 | 18 | type Logger struct { 19 | golog.Logger 20 | Level int 21 | Prefix string 22 | } 23 | 24 | var ( 25 | red = color.New(color.FgRed).SprintFunc() 26 | redln = color.New(color.FgRed).SprintlnFunc() 27 | redf = color.New(color.FgRed).SprintfFunc() 28 | yellow = color.New(color.FgYellow).SprintFunc() 29 | yellowln = color.New(color.FgYellow).SprintlnFunc() 30 | yellowf = color.New(color.FgYellow).SprintfFunc() 31 | ) 32 | 33 | func New(out io.Writer, prefix string, level int) *Logger { 34 | l := &Logger{ 35 | Level: level, 36 | Prefix: prefix, 37 | } 38 | l.Logger = *(golog.New(out, prefix, golog.LstdFlags)) 39 | return l 40 | } 41 | 42 | var DefaultLogger = New(os.Stderr, "", INFO) 43 | 44 | func (l *Logger) Debug(v ...interface{}) { 45 | if l.Level < DEBUG { 46 | return 47 | } 48 | l.Println(v...) 49 | } 50 | 51 | func (l *Logger) Debugf(fmt string, v ...interface{}) { 52 | if l.Level < DEBUG { 53 | return 54 | } 55 | l.Printf(fmt, v...) 56 | } 57 | 58 | func (l *Logger) Write(p []byte) (n int, err error) { 59 | if l.Level < DEBUG { 60 | return 61 | } 62 | l.Print(string(p)) 63 | return len(p), nil 64 | } 65 | 66 | func Debug(v ...interface{}) { DefaultLogger.Debug(v...) } 67 | func Debugf(format string, v ...interface{}) { DefaultLogger.Debugf(format, v...) } 68 | func Fatal(v ...interface{}) { 69 | DefaultLogger.Fatal(red(v...)) 70 | } 71 | func Fatalf(format string, v ...interface{}) { 72 | DefaultLogger.Fatal(redf(format, v...)) 73 | } 74 | func Fatalln(v ...interface{}) { 75 | DefaultLogger.Fatal(redln(v...)) 76 | } 77 | func Panic(v ...interface{}) { 78 | DefaultLogger.Panic(red(v...)) 79 | } 80 | func Panicf(format string, v ...interface{}) { 81 | DefaultLogger.Panic(redf(format, v...)) 82 | } 83 | func Panicln(v ...interface{}) { 84 | DefaultLogger.Panic(redln(v...)) 85 | } 86 | 87 | func Error(v ...interface{}) { 88 | DefaultLogger.Print(red(v...)) 89 | } 90 | func Errorf(format string, v ...interface{}) { 91 | DefaultLogger.Print(redf(format, v...)) 92 | } 93 | func Errorln(v ...interface{}) { 94 | DefaultLogger.Print(redln(v...)) 95 | } 96 | 97 | func Warn(v ...interface{}) { 98 | DefaultLogger.Print(yellow(v...)) 99 | } 100 | func Warnf(format string, v ...interface{}) { 101 | DefaultLogger.Print(yellowf(format, v...)) 102 | } 103 | func Warnln(v ...interface{}) { 104 | DefaultLogger.Print(yellowln(v...)) 105 | } 106 | 107 | func Print(v ...interface{}) { DefaultLogger.Print(v...) } 108 | func Printf(format string, v ...interface{}) { DefaultLogger.Printf(format, v...) } 109 | func Println(v ...interface{}) { DefaultLogger.Println(v...) } 110 | -------------------------------------------------------------------------------- /logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litl/galaxy/c36b08369196b157deb84149f8806d4e5f146e3b/logo.jpg -------------------------------------------------------------------------------- /runtime/aws.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "io/ioutil" 5 | "net" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | func EC2PublicHostname() (string, error) { 11 | transport := http.Transport{ 12 | Dial: func(network, addr string) (net.Conn, error) { 13 | return net.DialTimeout(network, addr, time.Duration(1*time.Second)) 14 | }, 15 | } 16 | 17 | client := http.Client{ 18 | Transport: &transport, 19 | } 20 | resp, err := client.Get("http://169.254.169.254/latest/meta-data/public-hostname") 21 | if err != nil { 22 | return "", err 23 | } 24 | defer resp.Body.Close() 25 | body, err := ioutil.ReadAll(resp.Body) 26 | if err != nil { 27 | return "", err 28 | } 29 | return string(body), nil 30 | } 31 | -------------------------------------------------------------------------------- /runtime/runtime.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "net/url" 8 | "os" 9 | "os/exec" 10 | "os/signal" 11 | "strconv" 12 | "strings" 13 | "time" 14 | 15 | docker "github.com/fsouza/go-dockerclient" 16 | "github.com/litl/galaxy/config" 17 | "github.com/litl/galaxy/log" 18 | "github.com/litl/galaxy/utils" 19 | ) 20 | 21 | var blacklistedContainerId = make(map[string]bool) 22 | 23 | // the deafult docker index server 24 | var defaultIndexServer = "https://index.docker.io/v1/" 25 | 26 | type ServiceRuntime struct { 27 | dockerClient *docker.Client 28 | dns string 29 | configStore *config.Store 30 | dockerIP string 31 | hostIP string 32 | } 33 | 34 | type ContainerEvent struct { 35 | Status string 36 | Container *docker.Container 37 | ServiceRegistration *config.ServiceRegistration 38 | } 39 | 40 | func NewServiceRuntime(configStore *config.Store, dns, hostIP string) *ServiceRuntime { 41 | var err error 42 | var client *docker.Client 43 | 44 | dockerZero, err := dockerBridgeIp() 45 | if err != nil { 46 | log.Fatalf("ERROR: Unable to find docker0 bridge: %s", err) 47 | } 48 | 49 | endpoint := GetEndpoint() 50 | 51 | if certPath := os.Getenv("DOCKER_CERT_PATH"); certPath != "" { 52 | cert := certPath + "/cert.pem" 53 | key := certPath + "/key.pem" 54 | ca := certPath + "/ca.pem" 55 | client, err = docker.NewTLSClient(endpoint, cert, key, ca) 56 | } else { 57 | client, err = docker.NewClient(endpoint) 58 | } 59 | 60 | if err != nil { 61 | log.Fatalf("ERROR: Unable to initialize docker client: %s: %s", err, endpoint) 62 | } 63 | 64 | client.HTTPClient.Timeout = 60 * time.Second 65 | 66 | return &ServiceRuntime{ 67 | dns: dns, 68 | configStore: configStore, 69 | hostIP: hostIP, 70 | dockerIP: dockerZero, 71 | dockerClient: client, 72 | } 73 | } 74 | 75 | func GetEndpoint() string { 76 | defaultEndpoint := "unix:///var/run/docker.sock" 77 | if os.Getenv("DOCKER_HOST") != "" { 78 | defaultEndpoint = os.Getenv("DOCKER_HOST") 79 | } 80 | 81 | return defaultEndpoint 82 | 83 | } 84 | 85 | // based off of https://github.com/dotcloud/docker/blob/2a711d16e05b69328f2636f88f8eac035477f7e4/utils/utils.go 86 | func parseHost(addr string) (string, string, error) { 87 | var ( 88 | proto string 89 | host string 90 | port int 91 | ) 92 | addr = strings.TrimSpace(addr) 93 | switch { 94 | case addr == "tcp://": 95 | return "", "", fmt.Errorf("Invalid bind address format: %s", addr) 96 | case strings.HasPrefix(addr, "unix://"): 97 | proto = "unix" 98 | addr = strings.TrimPrefix(addr, "unix://") 99 | if addr == "" { 100 | addr = "/var/run/docker.sock" 101 | } 102 | case strings.HasPrefix(addr, "tcp://"): 103 | proto = "tcp" 104 | addr = strings.TrimPrefix(addr, "tcp://") 105 | case strings.HasPrefix(addr, "fd://"): 106 | return "fd", addr, nil 107 | case addr == "": 108 | proto = "unix" 109 | addr = "/var/run/docker.sock" 110 | default: 111 | if strings.Contains(addr, "://") { 112 | return "", "", fmt.Errorf("Invalid bind address protocol: %s", addr) 113 | } 114 | proto = "tcp" 115 | } 116 | 117 | if proto != "unix" && strings.Contains(addr, ":") { 118 | hostParts := strings.Split(addr, ":") 119 | if len(hostParts) != 2 { 120 | return "", "", fmt.Errorf("Invalid bind address format: %s", addr) 121 | } 122 | if hostParts[0] != "" { 123 | host = hostParts[0] 124 | } else { 125 | host = "127.0.0.1" 126 | } 127 | 128 | if p, err := strconv.Atoi(hostParts[1]); err == nil && p != 0 { 129 | port = p 130 | } else { 131 | return "", "", fmt.Errorf("Invalid bind address format: %s", addr) 132 | } 133 | 134 | } else if proto == "tcp" && !strings.Contains(addr, ":") { 135 | return "", "", fmt.Errorf("Invalid bind address format: %s", addr) 136 | } else { 137 | host = addr 138 | } 139 | if proto == "unix" { 140 | return proto, host, nil 141 | 142 | } 143 | return proto, fmt.Sprintf("%s:%d", host, port), nil 144 | } 145 | 146 | func dockerBridgeIp() (string, error) { 147 | dh := os.Getenv("DOCKER_HOST") 148 | if dh != "" && strings.HasPrefix(dh, "tcp") { 149 | _, hostPort, err := parseHost(dh) 150 | return strings.Split(hostPort, ":")[0], err 151 | } 152 | 153 | dockerZero, err := net.InterfaceByName("docker0") 154 | if err != nil { 155 | return "", err 156 | } 157 | addrs, _ := dockerZero.Addrs() 158 | for _, addr := range addrs { 159 | ip, _, err := net.ParseCIDR(addr.String()) 160 | if err != nil { 161 | return "", err 162 | } 163 | if ip.DefaultMask() != nil { 164 | return ip.String(), nil 165 | } 166 | } 167 | return "", errors.New("unable to find docker0 interface") 168 | } 169 | 170 | func (s *ServiceRuntime) Ping() error { 171 | return s.dockerClient.Ping() 172 | } 173 | 174 | func (s *ServiceRuntime) InspectImage(image string) (*docker.Image, error) { 175 | return s.dockerClient.InspectImage(image) 176 | } 177 | 178 | func (s *ServiceRuntime) InspectContainer(id string) (*docker.Container, error) { 179 | return s.dockerClient.InspectContainer(id) 180 | } 181 | 182 | func (s *ServiceRuntime) StopAllMatching(name string) error { 183 | containers, err := s.ManagedContainers() 184 | if err != nil { 185 | return err 186 | } 187 | 188 | for _, container := range containers { 189 | 190 | env := s.EnvFor(container) 191 | // Container name does match one that would be started w/ this service config 192 | if env["GALAXY_APP"] != name { 193 | continue 194 | } 195 | 196 | s.stopContainer(container) 197 | } 198 | return nil 199 | 200 | } 201 | 202 | func (s *ServiceRuntime) Stop(appCfg config.App) error { 203 | containers, err := s.ManagedContainers() 204 | if err != nil { 205 | return err 206 | } 207 | 208 | for _, container := range containers { 209 | cenv := s.EnvFor(container) 210 | if cenv["GALAXY_APP"] == appCfg.Name() && cenv["GALAXY_VERSION"] == strconv.FormatInt(appCfg.ID(), 10) { 211 | return s.stopContainer(container) 212 | } 213 | } 214 | return nil 215 | } 216 | 217 | func (s *ServiceRuntime) stopContainer(container *docker.Container) error { 218 | if _, ok := blacklistedContainerId[container.ID]; ok { 219 | log.Printf("Container %s blacklisted. Won't try to stop.\n", container.ID) 220 | return nil 221 | } 222 | 223 | log.Printf("Stopping %s container %s\n", strings.TrimPrefix(container.Name, "/"), container.ID[0:12]) 224 | 225 | c := make(chan error, 1) 226 | go func() { c <- s.dockerClient.StopContainer(container.ID, 10) }() 227 | select { 228 | case err := <-c: 229 | if err != nil { 230 | log.Printf("ERROR: Unable to stop container: %s\n", container.ID) 231 | return err 232 | } 233 | case <-time.After(20 * time.Second): 234 | blacklistedContainerId[container.ID] = true 235 | log.Printf("ERROR: Timed out trying to stop container. Zombie?. Blacklisting: %s\n", container.ID) 236 | return nil 237 | } 238 | log.Printf("Stopped %s container %s\n", strings.TrimPrefix(container.Name, "/"), container.ID[0:12]) 239 | 240 | return nil 241 | // TODO: why is this commented out? 242 | // Should we verify that containers are actually removed somehow? 243 | /* return s.dockerClient.RemoveContainer(docker.RemoveContainerOptions{ 244 | ID: container.ID, 245 | RemoveVolumes: true, 246 | })*/ 247 | } 248 | 249 | func (s *ServiceRuntime) StopOldVersion(appCfg config.App, limit int) error { 250 | containers, err := s.ManagedContainers() 251 | if err != nil { 252 | return err 253 | } 254 | 255 | stopped := 0 256 | 257 | for _, container := range containers { 258 | 259 | if stopped == limit { 260 | return nil 261 | } 262 | 263 | env := s.EnvFor(container) 264 | // Container name does match one that would be started w/ this service config 265 | if env["GALAXY_APP"] != appCfg.Name() { 266 | continue 267 | } 268 | 269 | image, err := s.InspectImage(container.Image) 270 | if err != nil { 271 | log.Errorf("ERROR: Unable to inspect image %s: %s", container.Image, err) 272 | continue 273 | } 274 | 275 | if image == nil { 276 | log.Errorf("ERROR: Image for container %s does not exist!", container.ID[0:12]) 277 | continue 278 | 279 | } 280 | 281 | version := env["GALAXY_VERSION"] 282 | 283 | if version == "" { 284 | log.Printf("WARNING: %s missing GALAXY_VERSION", appCfg.ContainerName()) 285 | } 286 | 287 | if version != strconv.FormatInt(appCfg.ID(), 10) && version != "" { 288 | s.stopContainer(container) 289 | stopped = stopped + 1 290 | } 291 | } 292 | 293 | return nil 294 | } 295 | 296 | /* 297 | FIXME: using StopOldVersion(cfg, -1) for now 298 | func (s *ServiceRuntime) StopAllButCurrentVersion(appCfg config.App) error { 299 | containers, err := s.ManagedContainers() 300 | if err != nil { 301 | return err 302 | } 303 | 304 | for _, container := range containers { 305 | 306 | env := s.EnvFor(container) 307 | // Container name does match one that would be started w/ this service config 308 | if env["GALAXY_APP"] != appCfg.Name() { 309 | continue 310 | } 311 | 312 | image, err := s.InspectImage(container.Image) 313 | if err != nil { 314 | log.Errorf("ERROR: Unable to inspect image: %s", container.Image) 315 | continue 316 | } 317 | 318 | if image == nil { 319 | log.Errorf("ERROR: Image for container %s does not exist!", container.ID[0:12]) 320 | continue 321 | 322 | } 323 | 324 | version := env["GALAXY_VERSION"] 325 | 326 | imageDiffers := image.ID != appCfg.VersionID() && appCfg.VersionID() != "" 327 | versionDiffers := version != strconv.FormatInt(appCfg.ID(), 10) && version != "" 328 | 329 | if imageDiffers || versionDiffers { 330 | s.stopContainer(container) 331 | } 332 | } 333 | return nil 334 | } 335 | */ 336 | 337 | // TODO: these aren't called from anywhere. Are they useful? 338 | /* 339 | func (s *ServiceRuntime) StopAllButLatestService(name string, stopCutoff int64) error { 340 | containers, err := s.ManagedContainers() 341 | if err != nil { 342 | return err 343 | } 344 | 345 | var toStop []*docker.Container 346 | var latestContainer *docker.Container 347 | for _, container := range containers { 348 | if s.EnvFor(container)["GALAXY_APP"] == name { 349 | if latestContainer == nil || container.Created.After(latestContainer.Created) { 350 | latestContainer = container 351 | } 352 | toStop = append(toStop, container) 353 | } 354 | } 355 | 356 | for _, container := range toStop { 357 | if container.ID != latestContainer.ID && 358 | container.Created.Unix() < (time.Now().Unix()-stopCutoff) { 359 | s.stopContainer(container) 360 | } 361 | } 362 | return nil 363 | } 364 | 365 | func (s *ServiceRuntime) StopAllButLatest(env string, stopCutoff int64) error { 366 | 367 | containers, err := s.ManagedContainers() 368 | if err != nil { 369 | return err 370 | } 371 | 372 | for _, c := range containers { 373 | s.StopAllButLatestService(s.EnvFor(c)["GALAXY_APP"], stopCutoff) 374 | } 375 | 376 | return nil 377 | } 378 | */ 379 | 380 | // Stop any running galaxy containers that are not assigned to us 381 | // TODO: We call ManagedContainers a lot, repeatedly listing and inspecting all containers. 382 | func (s *ServiceRuntime) StopUnassigned(env, pool string) error { 383 | containers, err := s.ManagedContainers() 384 | if err != nil { 385 | return err 386 | } 387 | 388 | for _, container := range containers { 389 | name := s.EnvFor(container)["GALAXY_APP"] 390 | 391 | pools, err := s.configStore.ListAssignedPools(env, name) 392 | if err != nil { 393 | log.Errorf("ERROR: Unable to list pool assignments for %s: %s", container.Name, err) 394 | continue 395 | } 396 | 397 | if len(pools) == 0 || !utils.StringInSlice(pool, pools) { 398 | log.Warnf("galaxy container %s not assigned to %s/%s", container.Name, env, pool) 399 | s.stopContainer(container) 400 | } 401 | } 402 | return nil 403 | } 404 | 405 | func (s *ServiceRuntime) StopAll(env string) error { 406 | 407 | containers, err := s.ManagedContainers() 408 | if err != nil { 409 | return err 410 | } 411 | 412 | for _, c := range containers { 413 | s.stopContainer(c) 414 | } 415 | 416 | return nil 417 | } 418 | 419 | func (s *ServiceRuntime) GetImageByName(img string) (*docker.APIImages, error) { 420 | imgs, err := s.dockerClient.ListImages(docker.ListImagesOptions{All: true}) 421 | if err != nil { 422 | panic(err) 423 | } 424 | 425 | for _, image := range imgs { 426 | if utils.StringInSlice(img, image.RepoTags) { 427 | return &image, nil 428 | } 429 | } 430 | return nil, nil 431 | 432 | } 433 | 434 | func (s *ServiceRuntime) RunCommand(env string, appCfg config.App, cmd []string) (*docker.Container, error) { 435 | 436 | // see if we have the image locally 437 | fmt.Fprintf(os.Stderr, "Pulling latest image for %s\n", appCfg.Version()) 438 | _, err := s.PullImage(appCfg.Version(), appCfg.VersionID()) 439 | if err != nil { 440 | return nil, err 441 | } 442 | 443 | instanceId, err := s.NextInstanceSlot(appCfg.Name(), strconv.FormatInt(appCfg.ID(), 10)) 444 | if err != nil { 445 | return nil, err 446 | } 447 | 448 | envVars := []string{"ENV=" + env} 449 | 450 | for key, value := range appCfg.Env() { 451 | if key == "ENV" { 452 | continue 453 | } 454 | envVars = append(envVars, strings.ToUpper(key)+"="+s.replaceVarEnv(value, s.hostIP)) 455 | } 456 | envVars = append(envVars, "GALAXY_APP="+appCfg.Name()) 457 | envVars = append(envVars, "GALAXY_VERSION="+strconv.FormatInt(appCfg.ID(), 10)) 458 | envVars = append(envVars, fmt.Sprintf("GALAXY_INSTANCE=%s", strconv.FormatInt(int64(instanceId), 10))) 459 | 460 | runCmd := []string{"/bin/sh", "-c", strings.Join(cmd, " ")} 461 | 462 | hostConfig := &docker.HostConfig{} 463 | if s.dns != "" { 464 | hostConfig.DNS = []string{s.dns} 465 | } 466 | 467 | container, err := s.dockerClient.CreateContainer(docker.CreateContainerOptions{ 468 | Config: &docker.Config{ 469 | Image: appCfg.Version(), 470 | Env: envVars, 471 | AttachStdout: true, 472 | AttachStderr: true, 473 | Cmd: runCmd, 474 | OpenStdin: false, 475 | }, 476 | HostConfig: hostConfig, 477 | }) 478 | 479 | if err != nil { 480 | return nil, err 481 | } 482 | 483 | c := make(chan os.Signal, 1) 484 | signal.Notify(c, os.Interrupt, os.Kill) 485 | go func(s *ServiceRuntime, containerId string) { 486 | <-c 487 | log.Println("Stopping container...") 488 | err := s.dockerClient.StopContainer(containerId, 3) 489 | if err != nil { 490 | log.Printf("ERROR: Unable to stop container: %s", err) 491 | } 492 | err = s.dockerClient.RemoveContainer(docker.RemoveContainerOptions{ 493 | ID: containerId, 494 | }) 495 | if err != nil { 496 | log.Printf("ERROR: Unable to stop container: %s", err) 497 | } 498 | 499 | }(s, container.ID) 500 | 501 | defer s.dockerClient.RemoveContainer(docker.RemoveContainerOptions{ 502 | ID: container.ID, 503 | }) 504 | err = s.dockerClient.StartContainer(container.ID, nil) 505 | 506 | if err != nil { 507 | return container, err 508 | } 509 | 510 | err = s.dockerClient.AttachToContainer(docker.AttachToContainerOptions{ 511 | Container: container.ID, 512 | OutputStream: os.Stdout, 513 | ErrorStream: os.Stderr, 514 | Logs: true, 515 | Stream: true, 516 | Stdout: true, 517 | Stderr: true, 518 | }) 519 | 520 | if err != nil { 521 | log.Printf("ERROR: Unable to attach to running container: %s", err.Error()) 522 | } 523 | 524 | s.dockerClient.WaitContainer(container.ID) 525 | 526 | return container, err 527 | } 528 | 529 | func (s *ServiceRuntime) StartInteractive(env, pool string, appCfg config.App) error { 530 | 531 | // see if we have the image locally 532 | fmt.Fprintf(os.Stderr, "Pulling latest image for %s\n", appCfg.Version()) 533 | _, err := s.PullImage(appCfg.Version(), appCfg.VersionID()) 534 | if err != nil { 535 | return err 536 | } 537 | 538 | args := []string{ 539 | "run", "--rm", "-i", 540 | } 541 | args = append(args, "-e") 542 | args = append(args, "ENV"+"="+env) 543 | 544 | for key, value := range appCfg.Env() { 545 | if key == "ENV" { 546 | continue 547 | } 548 | 549 | args = append(args, "-e") 550 | args = append(args, strings.ToUpper(key)+"="+s.replaceVarEnv(value, s.hostIP)) 551 | } 552 | 553 | args = append(args, "-e") 554 | args = append(args, fmt.Sprintf("HOST_IP=%s", s.hostIP)) 555 | if s.dns != "" { 556 | args = append(args, "--dns") 557 | args = append(args, s.dns) 558 | } 559 | args = append(args, "-e") 560 | args = append(args, fmt.Sprintf("GALAXY_APP=%s", appCfg.Name())) 561 | args = append(args, "-e") 562 | args = append(args, fmt.Sprintf("GALAXY_VERSION=%s", strconv.FormatInt(appCfg.ID(), 10))) 563 | 564 | instanceId, err := s.NextInstanceSlot(appCfg.Name(), strconv.FormatInt(appCfg.ID(), 10)) 565 | if err != nil { 566 | return err 567 | } 568 | args = append(args, "-e") 569 | args = append(args, fmt.Sprintf("GALAXY_INSTANCE=%s", strconv.FormatInt(int64(instanceId), 10))) 570 | 571 | publicDns, err := EC2PublicHostname() 572 | if err != nil { 573 | log.Warnf("Unable to determine public hostname. Not on AWS? %s", err) 574 | publicDns = "127.0.0.1" 575 | } 576 | 577 | args = append(args, "-e") 578 | args = append(args, fmt.Sprintf("PUBLIC_HOSTNAME=%s", publicDns)) 579 | 580 | mem := appCfg.GetMemory(pool) 581 | if mem != "" { 582 | args = append(args, "-m") 583 | args = append(args, mem) 584 | } 585 | 586 | cpu := appCfg.GetCPUShares(pool) 587 | if cpu != "" { 588 | args = append(args, "-c") 589 | args = append(args, cpu) 590 | } 591 | 592 | args = append(args, []string{"-t", appCfg.Version(), "/bin/sh"}...) 593 | // shell out to docker run to get signal forwarded and terminal setup correctly 594 | //cmd := exec.Command("docker", "run", "-rm", "-i", "-t", appCfg.Version(), "/bin/bash") 595 | cmd := exec.Command("docker", args...) 596 | 597 | cmd.Stdin = os.Stdin 598 | cmd.Stdout = os.Stdout 599 | cmd.Stderr = os.Stderr 600 | err = cmd.Start() 601 | if err != nil { 602 | log.Fatal(err) 603 | } 604 | 605 | err = cmd.Wait() 606 | if err != nil { 607 | fmt.Fprintf(os.Stderr, "Command finished with error: %v\n", err) 608 | } 609 | 610 | return err 611 | } 612 | 613 | func (s *ServiceRuntime) Start(env, pool string, appCfg config.App) (*docker.Container, error) { 614 | 615 | img := appCfg.Version() 616 | 617 | image, err := s.PullImage(img, appCfg.VersionID()) 618 | if err != nil { 619 | return nil, err 620 | } 621 | 622 | if utils.StripSHA(image.ID) != appCfg.VersionID() { 623 | log.Warnf("warning: ID for image %s doesn't match configuration", img) 624 | } 625 | 626 | // setup env vars from etcd 627 | var envVars []string 628 | envVars = append(envVars, "ENV"+"="+env) 629 | 630 | for key, value := range appCfg.Env() { 631 | if key == "ENV" { 632 | continue 633 | } 634 | envVars = append(envVars, strings.ToUpper(key)+"="+s.replaceVarEnv(value, s.hostIP)) 635 | } 636 | 637 | instanceId, err := s.NextInstanceSlot(appCfg.Name(), strconv.FormatInt(appCfg.ID(), 10)) 638 | if err != nil { 639 | return nil, err 640 | } 641 | 642 | envVars = append(envVars, fmt.Sprintf("HOST_IP=%s", s.hostIP)) 643 | envVars = append(envVars, fmt.Sprintf("GALAXY_APP=%s", appCfg.Name())) 644 | envVars = append(envVars, fmt.Sprintf("GALAXY_VERSION=%s", strconv.FormatInt(appCfg.ID(), 10))) 645 | envVars = append(envVars, fmt.Sprintf("GALAXY_INSTANCE=%s", strconv.FormatInt(int64(instanceId), 10))) 646 | 647 | publicDns, err := EC2PublicHostname() 648 | if err != nil { 649 | log.Warnf("Unable to determine public hostname. Not on AWS? %s", err) 650 | publicDns = "127.0.0.1" 651 | } 652 | envVars = append(envVars, fmt.Sprintf("PUBLIC_HOSTNAME=%s", publicDns)) 653 | 654 | containerName := appCfg.ContainerName() + "." + strconv.FormatInt(int64(instanceId), 10) 655 | container, err := s.dockerClient.InspectContainer(containerName) 656 | _, ok := err.(*docker.NoSuchContainer) 657 | if err != nil && !ok { 658 | return nil, err 659 | } 660 | 661 | // If there's an existing container, stop it if it's running and re-create it. 662 | if container != nil { 663 | if container.State.Running || container.State.Restarting || container.State.Paused { 664 | log.Printf("Stopping %s version %s running as %s", appCfg.Name(), appCfg.Version(), container.ID[0:12]) 665 | err := s.dockerClient.StopContainer(container.ID, 10) 666 | if err != nil { 667 | return nil, err 668 | } 669 | } 670 | 671 | log.Printf("Removing %s version %s running as %s", appCfg.Name(), appCfg.Version(), container.ID[0:12]) 672 | err = s.dockerClient.RemoveContainer(docker.RemoveContainerOptions{ 673 | ID: container.ID, 674 | }) 675 | if err != nil { 676 | return nil, err 677 | } 678 | container = nil 679 | } 680 | 681 | if container == nil { 682 | 683 | config := &docker.Config{ 684 | Image: img, 685 | Env: envVars, 686 | } 687 | 688 | mem := appCfg.GetMemory(pool) 689 | if mem != "" { 690 | m, err := utils.ParseMemory(mem) 691 | if err != nil { 692 | return nil, err 693 | } 694 | config.Memory = m 695 | } 696 | 697 | cpu := appCfg.GetCPUShares(pool) 698 | if cpu != "" { 699 | if c, err := strconv.Atoi(cpu); err == nil { 700 | config.CPUShares = int64(c) 701 | } 702 | } 703 | 704 | hostConfig := &docker.HostConfig{ 705 | PublishAllPorts: true, 706 | RestartPolicy: docker.RestartPolicy{ 707 | Name: "on-failure", 708 | MaximumRetryCount: 16, 709 | }, 710 | LogConfig: docker.LogConfig{ 711 | Type: "syslog", 712 | Config: map[string]string{"tag": containerName}, 713 | }, 714 | } 715 | 716 | if s.dns != "" { 717 | hostConfig.DNS = []string{s.dns} 718 | } 719 | 720 | log.Printf("Creating %s version %s", appCfg.Name(), appCfg.Version()) 721 | container, err = s.dockerClient.CreateContainer(docker.CreateContainerOptions{ 722 | Name: containerName, 723 | Config: config, 724 | HostConfig: hostConfig, 725 | }) 726 | if err != nil { 727 | return nil, err 728 | } 729 | } 730 | 731 | log.Printf("Starting %s version %s running as %s", appCfg.Name(), appCfg.Version(), container.ID[0:12]) 732 | 733 | err = s.dockerClient.StartContainer(container.ID, nil) 734 | 735 | return container, err 736 | } 737 | 738 | // TODO: not called, is this needed? 739 | /* 740 | func (s *ServiceRuntime) StartIfNotRunning(env, pool string, appCfg config.App) (bool, *docker.Container, error) { 741 | 742 | containers, err := s.ManagedContainers() 743 | if err != nil { 744 | return false, nil, err 745 | } 746 | 747 | image, err := s.InspectImage(appCfg.Version()) 748 | if err != nil { 749 | return false, nil, err 750 | } 751 | 752 | var running *docker.Container 753 | for _, container := range containers { 754 | cenv := s.EnvFor(container) 755 | if cenv["GALAXY_APP"] == appCfg.Name() && 756 | cenv["GALAXY_VERSION"] == strconv.FormatInt(appCfg.ID(), 10) && 757 | image.ID == container.Image { 758 | running = container 759 | break 760 | } 761 | } 762 | 763 | if running != nil { 764 | return false, running, nil 765 | } 766 | 767 | err = s.dockerClient.RemoveContainer(docker.RemoveContainerOptions{ 768 | ID: appCfg.ContainerName(), 769 | }) 770 | _, ok := err.(*docker.NoSuchContainer) 771 | if err != nil && !ok { 772 | return false, nil, err 773 | } 774 | 775 | container, err := s.Start(env, pool, appCfg) 776 | return true, container, err 777 | } 778 | */ 779 | 780 | // Find a best match for docker authentication 781 | // Docker's config is a bunch of special-cases, try to cover most of them here. 782 | // TODO: This may not work at all when we switch to a private V2 registry 783 | func findAuth(registry string) docker.AuthConfiguration { 784 | // Ignore the error. If .dockercfg doesn't exist, maybe we don't need auth 785 | auths, _ := docker.NewAuthConfigurationsFromDockerCfg() 786 | if auths == nil || auths.Configs == nil { 787 | return docker.AuthConfiguration{} 788 | } 789 | 790 | auth, ok := auths.Configs[registry] 791 | if ok { 792 | return auth 793 | } 794 | // no exact match, so let's try harder 795 | 796 | // Docker only uses the hostname for private indexes 797 | for reg, auth := range auths.Configs { 798 | // extract the hostname if the key is a url 799 | if u, e := url.Parse(reg); e == nil && u.Host != "" { 800 | reg = u.Host 801 | } 802 | if registry == reg { 803 | return auth 804 | } 805 | } 806 | 807 | // Still no match 808 | // Try the default docker index server 809 | return auths.Configs[defaultIndexServer] 810 | } 811 | 812 | // Pull a docker image. 813 | // If we have an image matching the tag, and the given id matches the current 814 | // image, don't fetch a new one from the registry. 815 | func (s *ServiceRuntime) PullImage(version, id string) (*docker.Image, error) { 816 | image, err := s.InspectImage(version) 817 | 818 | if err != nil && err != docker.ErrNoSuchImage { 819 | return nil, err 820 | } 821 | 822 | if image != nil && utils.StripSHA(image.ID) == id { 823 | return image, nil 824 | } 825 | 826 | registry, repository, tag := utils.SplitDockerImage(version) 827 | 828 | // pull it down locally 829 | pullOpts := docker.PullImageOptions{ 830 | Repository: repository, 831 | Tag: tag, 832 | OutputStream: log.DefaultLogger} 833 | 834 | dockerAuth := findAuth(registry) 835 | 836 | if registry != "" { 837 | pullOpts.Repository = registry + "/" + repository 838 | } else { 839 | pullOpts.Repository = repository 840 | } 841 | pullOpts.Registry = registry 842 | pullOpts.Tag = tag 843 | 844 | retries := 0 845 | for { 846 | retries += 1 847 | err = s.dockerClient.PullImage(pullOpts, dockerAuth) 848 | if err != nil { 849 | 850 | if retries > 3 { 851 | return image, err 852 | } 853 | log.Errorf("ERROR: error pulling image %s. Attempt %d: %s", version, retries, err) 854 | continue 855 | } 856 | break 857 | } 858 | 859 | return s.InspectImage(version) 860 | } 861 | 862 | func (s *ServiceRuntime) RegisterAll(env, pool, hostIP string) ([]*config.ServiceRegistration, error) { 863 | // make sure any old containers that shouldn't be running are gone 864 | // FIXME: I don't like how a "Register" function has the possible side 865 | // effect of stopping containers 866 | s.StopUnassigned(env, pool) 867 | 868 | containers, err := s.ManagedContainers() 869 | if err != nil { 870 | return nil, err 871 | } 872 | 873 | registrations := []*config.ServiceRegistration{} 874 | 875 | for _, container := range containers { 876 | name := s.EnvFor(container)["GALAXY_APP"] 877 | 878 | registration, err := s.configStore.RegisterService(env, pool, hostIP, container) 879 | if err != nil { 880 | log.Printf("ERROR: Could not register %s: %s\n", name, err.Error()) 881 | continue 882 | } 883 | registrations = append(registrations, registration) 884 | } 885 | 886 | return registrations, nil 887 | 888 | } 889 | 890 | func (s *ServiceRuntime) UnRegisterAll(env, pool, hostIP string) ([]*docker.Container, error) { 891 | 892 | containers, err := s.ManagedContainers() 893 | if err != nil { 894 | return nil, err 895 | } 896 | 897 | removed := []*docker.Container{} 898 | 899 | for _, container := range containers { 900 | name := s.EnvFor(container)["GALAXY_APP"] 901 | _, err = s.configStore.UnRegisterService(env, pool, hostIP, container) 902 | if err != nil { 903 | log.Printf("ERROR: Could not unregister %s: %s\n", name, err) 904 | return removed, err 905 | } 906 | 907 | removed = append(removed, container) 908 | log.Printf("Unregistered %s as %s", container.ID[0:12], name) 909 | } 910 | 911 | return removed, nil 912 | } 913 | 914 | // RegisterEvents monitors the docker daemon for events, and returns those 915 | // that require registration action over the listener chan. 916 | func (s *ServiceRuntime) RegisterEvents(env, pool, hostIP string, listener chan ContainerEvent) error { 917 | go func() { 918 | c := make(chan *docker.APIEvents) 919 | 920 | watching := false 921 | for { 922 | 923 | err := s.Ping() 924 | if err != nil { 925 | log.Errorf("ERROR: Unable to ping docker daemaon: %s", err) 926 | if watching { 927 | s.dockerClient.RemoveEventListener(c) 928 | watching = false 929 | } 930 | time.Sleep(10 * time.Second) 931 | continue 932 | 933 | } 934 | 935 | if !watching { 936 | err = s.dockerClient.AddEventListener(c) 937 | if err != nil && err != docker.ErrListenerAlreadyExists { 938 | log.Printf("ERROR: Error registering docker event listener: %s", err) 939 | time.Sleep(10 * time.Second) 940 | continue 941 | } 942 | watching = true 943 | } 944 | 945 | select { 946 | 947 | case e := <-c: 948 | if e.Status == "start" || e.Status == "stop" || e.Status == "die" { 949 | container, err := s.InspectContainer(e.ID) 950 | if err != nil { 951 | log.Printf("ERROR: Error inspecting container: %s", err) 952 | continue 953 | } 954 | 955 | if container == nil { 956 | log.Printf("WARN: Nil container returned for %s", e.ID[:12]) 957 | continue 958 | } 959 | 960 | name := s.EnvFor(container)["GALAXY_APP"] 961 | if name != "" { 962 | registration, err := s.configStore.GetServiceRegistration(env, pool, hostIP, container) 963 | if err != nil { 964 | log.Printf("WARN: Could not find service registration for %s/%s: %s", name, container.ID[:12], err) 965 | continue 966 | } 967 | 968 | if registration == nil && e.Status != "start" { 969 | continue 970 | } 971 | 972 | // if a container is restarting, don't continue re-registering the app 973 | if container.State.Restarting { 974 | if e.Status == "die" { 975 | log.Println("WARN: restarting", container.Name) 976 | } 977 | continue 978 | } 979 | 980 | listener <- ContainerEvent{ 981 | Status: e.Status, 982 | Container: container, 983 | ServiceRegistration: registration, 984 | } 985 | } 986 | 987 | } 988 | case <-time.After(10 * time.Second): 989 | // check for docker liveness 990 | } 991 | 992 | } 993 | }() 994 | return nil 995 | } 996 | 997 | func (s *ServiceRuntime) EnvFor(container *docker.Container) map[string]string { 998 | env := map[string]string{} 999 | for _, item := range container.Config.Env { 1000 | sep := strings.Index(item, "=") 1001 | k := item[0:sep] 1002 | v := item[sep+1:] 1003 | env[k] = v 1004 | } 1005 | return env 1006 | } 1007 | 1008 | func (s *ServiceRuntime) ManagedContainers() ([]*docker.Container, error) { 1009 | apps := []*docker.Container{} 1010 | containers, err := s.dockerClient.ListContainers(docker.ListContainersOptions{ 1011 | All: true, 1012 | }) 1013 | if err != nil { 1014 | return apps, err 1015 | } 1016 | 1017 | for _, c := range containers { 1018 | container, err := s.dockerClient.InspectContainer(c.ID) 1019 | if err != nil { 1020 | log.Printf("ERROR: Unable to inspect container: %s\n", c.ID) 1021 | continue 1022 | } 1023 | name := s.EnvFor(container)["GALAXY_APP"] 1024 | if name != "" && (container.State.Running || container.State.Restarting) { 1025 | apps = append(apps, container) 1026 | } 1027 | } 1028 | return apps, nil 1029 | } 1030 | 1031 | func (s *ServiceRuntime) instanceIds(app, versionId string) ([]int, error) { 1032 | containers, err := s.ManagedContainers() 1033 | if err != nil { 1034 | return []int{}, err 1035 | } 1036 | 1037 | instances := []int{} 1038 | for _, c := range containers { 1039 | ga := s.EnvFor(c)["GALAXY_APP"] 1040 | 1041 | if ga != app { 1042 | continue 1043 | } 1044 | 1045 | gi := s.EnvFor(c)["GALAXY_INSTANCE"] 1046 | gv := s.EnvFor(c)["GALAXY_VERSION"] 1047 | if gi != "" { 1048 | i, err := strconv.ParseInt(gi, 10, 64) 1049 | if err != nil { 1050 | log.Warnf("WARN: Invalid number %s for %s. Ignoring.", gi, c.ID[:12]) 1051 | continue 1052 | } 1053 | 1054 | if versionId != "" && gv != versionId { 1055 | continue 1056 | } 1057 | instances = append(instances, int(i)) 1058 | } 1059 | } 1060 | return instances, nil 1061 | } 1062 | 1063 | func (s *ServiceRuntime) InstanceCount(app, versionId string) (int, error) { 1064 | instances, err := s.instanceIds(app, versionId) 1065 | return len(instances), err 1066 | } 1067 | 1068 | func (s *ServiceRuntime) NextInstanceSlot(app, versionId string) (int, error) { 1069 | instances, err := s.instanceIds(app, versionId) 1070 | if err != nil { 1071 | return 0, err 1072 | } 1073 | 1074 | return utils.NextSlot(instances), nil 1075 | } 1076 | 1077 | func (s ServiceRuntime) replaceVarEnv(in, hostIp string) string { 1078 | out := strings.Replace(in, "$HOST_IP", hostIp, -1) 1079 | return strings.Replace(out, "$DOCKER_IP", s.dockerIP, -1) 1080 | } 1081 | -------------------------------------------------------------------------------- /utils/ssh.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | "syscall" 10 | 11 | "github.com/litl/galaxy/log" 12 | ) 13 | 14 | func SSHCmd(host string, command string, background bool, debug bool) { 15 | 16 | port := "22" 17 | hostPort := strings.SplitN(host, ":", 2) 18 | if len(hostPort) > 1 { 19 | host, port = hostPort[0], hostPort[1] 20 | } 21 | 22 | cmd := exec.Command("/usr/bin/ssh", 23 | "-o", "RequestTTY=yes", 24 | host, 25 | "-p", port, 26 | "-C", "/bin/sh", "-i", "-l", "-c", "'"+command+"'") 27 | 28 | cmd.Stdin = os.Stdin 29 | cmd.Stdout = os.Stdout 30 | var b []byte 31 | buf := bytes.NewBuffer(b) 32 | cmd.Stderr = buf 33 | err := cmd.Start() 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | if err := cmd.Wait(); err != nil { 39 | log.Error(buf.String()) 40 | if exiterr, ok := err.(*exec.ExitError); ok { 41 | // The program has exited with an exit code != 0 42 | 43 | // This works on both Unix and Windows. Although package 44 | // syscall is generally platform dependent, WaitStatus is 45 | // defined for both Unix and Windows and in both cases has 46 | // an ExitStatus() method with the same signature. 47 | if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { 48 | fmt.Fprintf(os.Stderr, "Command finished with error: %v\n", err) 49 | os.Exit(status.ExitStatus()) 50 | } 51 | } else { 52 | fmt.Fprintf(os.Stderr, "Command finished with error: %v\n", err) 53 | os.Exit(1) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/codegangsta/cli" 10 | 11 | "os" 12 | ) 13 | 14 | type SliceVar []string 15 | 16 | func (s *SliceVar) Set(value string) error { 17 | *s = append(*s, value) 18 | return nil 19 | } 20 | 21 | func (s *SliceVar) String() string { 22 | return strings.Join(*s, ",") 23 | } 24 | 25 | type OutputBuffer struct { 26 | Output []string 27 | } 28 | 29 | func (o *OutputBuffer) Log(msg string) { 30 | o.Output = append(o.Output, msg) 31 | } 32 | 33 | // HumanDuration returns a human-readable approximation of a duration 34 | // (eg. "About a minute", "4 hours ago", etc.) 35 | func HumanDuration(d time.Duration) string { 36 | if seconds := int(d.Seconds()); seconds < 1 { 37 | return "Less than a second" 38 | } else if seconds < 60 { 39 | return fmt.Sprintf("%d seconds", seconds) 40 | } else if minutes := int(d.Minutes()); minutes == 1 { 41 | return "About a minute" 42 | } else if minutes < 60 { 43 | return fmt.Sprintf("%d minutes", minutes) 44 | } else if hours := int(d.Hours()); hours == 1 { 45 | return "About an hour" 46 | } else if hours < 48 { 47 | return fmt.Sprintf("%d hours", hours) 48 | } else if hours < 24*7*2 { 49 | return fmt.Sprintf("%d days", hours/24) 50 | } else if hours < 24*30*3 { 51 | return fmt.Sprintf("%d weeks", hours/24/7) 52 | } else if hours < 24*365*2 { 53 | return fmt.Sprintf("%d months", hours/24/30) 54 | } 55 | return fmt.Sprintf("%f years", d.Hours()/24/365) 56 | } 57 | 58 | func SplitDockerImage(img string) (string, string, string) { 59 | index := 0 60 | repository := img 61 | var registry, tag string 62 | if strings.Contains(img, "/") { 63 | separator := strings.Index(img, "/") 64 | registry = img[index:separator] 65 | index = separator + 1 66 | repository = img[index:] 67 | } 68 | 69 | if strings.Contains(img, ":") { 70 | separator := strings.Index(img, ":") 71 | repository = img[index:separator] 72 | index = separator + 1 73 | tag = img[index:] 74 | } 75 | 76 | return registry, repository, tag 77 | } 78 | 79 | func StringInSlice(a string, list []string) bool { 80 | for _, b := range list { 81 | if b == a { 82 | return true 83 | } 84 | } 85 | return false 86 | } 87 | 88 | func RemoveStringInSlice(a string, list []string) []string { 89 | r := []string{} 90 | for _, v := range list { 91 | if v != a { 92 | r = append(r, v) 93 | } 94 | } 95 | return r 96 | } 97 | 98 | func GetEnv(name, defaultValue string) string { 99 | if os.Getenv(name) == "" { 100 | return defaultValue 101 | } 102 | return os.Getenv(name) 103 | } 104 | 105 | func HomeDir() string { 106 | return os.Getenv("HOME") 107 | } 108 | 109 | func GalaxyEnv(c *cli.Context) string { 110 | if c.GlobalString("env") != "" { 111 | return strings.TrimSpace(c.GlobalString("env")) 112 | } 113 | return strings.TrimSpace(GetEnv("GALAXY_ENV", "")) 114 | } 115 | 116 | func GalaxyPool(c *cli.Context) string { 117 | if c.GlobalString("pool") != "" { 118 | return strings.TrimSpace(c.GlobalString("pool")) 119 | } 120 | return strings.TrimSpace(GetEnv("GALAXY_POOL", "")) 121 | } 122 | 123 | func GalaxyRedisHost(c *cli.Context) string { 124 | if c.GlobalString("registry") != "" { 125 | return strings.TrimSpace(c.GlobalString("registry")) 126 | } 127 | 128 | return strings.TrimSpace(GetEnv("GALAXY_REGISTRY_URL", "")) 129 | } 130 | 131 | // NextSlot finds the first available index in an array of integers 132 | func NextSlot(used []int) int { 133 | free := 0 134 | RESTART: 135 | for _, v := range used { 136 | if v == free { 137 | free = free + 1 138 | goto RESTART 139 | } 140 | } 141 | return free 142 | } 143 | 144 | func ParseMemory(mem string) (int64, error) { 145 | if mem == "" { 146 | return 0, nil 147 | } 148 | 149 | multiplier := int64(1) 150 | if strings.HasSuffix(mem, "b") { 151 | mem = mem[:len(mem)-1] 152 | } 153 | 154 | if strings.HasSuffix(mem, "k") { 155 | multiplier = int64(1024) 156 | mem = mem[:len(mem)-1] 157 | } 158 | 159 | if strings.HasSuffix(mem, "m") { 160 | multiplier = int64(1024) * int64(1024) 161 | mem = mem[:len(mem)-1] 162 | } 163 | 164 | if strings.HasSuffix(mem, "g") { 165 | multiplier = int64(1024) * int64(1024) * int64(1024) 166 | mem = mem[:len(mem)-1] 167 | } 168 | 169 | i, err := strconv.ParseInt(mem, 10, 64) 170 | if err != nil { 171 | return 0, err 172 | } 173 | return i * multiplier, nil 174 | } 175 | 176 | // strip the leading sha256: from content adressable images 177 | func StripSHA(s string) string { 178 | return strings.TrimPrefix(s, "sha256:") 179 | } 180 | -------------------------------------------------------------------------------- /utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSplitDockerImageRepository(t *testing.T) { 8 | registry, repository, tag := SplitDockerImage("ubuntu") 9 | 10 | if registry != "" { 11 | t.Fail() 12 | } 13 | if repository != "ubuntu" { 14 | t.Fail() 15 | } 16 | if tag != "" { 17 | t.Fail() 18 | } 19 | } 20 | 21 | func TestSplitDockerImageWithRegistry(t *testing.T) { 22 | registry, repository, tag := SplitDockerImage("custom.registry/ubuntu") 23 | 24 | if registry != "custom.registry" { 25 | t.Fail() 26 | } 27 | if repository != "ubuntu" { 28 | t.Fail() 29 | } 30 | if tag != "" { 31 | t.Fail() 32 | } 33 | } 34 | 35 | func TestSplitDockerImageWithPublicRegistry(t *testing.T) { 36 | registry, repository, tag := SplitDockerImage("username/ubuntu") 37 | 38 | if registry != "username" { 39 | t.Fail() 40 | } 41 | if repository != "ubuntu" { 42 | t.Fail() 43 | } 44 | if tag != "" { 45 | t.Fail() 46 | } 47 | } 48 | 49 | func TestSplitDockerImageWithRegistryAndTag(t *testing.T) { 50 | registry, repository, tag := SplitDockerImage("custom.registry/ubuntu:12.04") 51 | 52 | if registry != "custom.registry" { 53 | t.Fail() 54 | } 55 | if repository != "ubuntu" { 56 | t.Fail() 57 | } 58 | if tag != "12.04" { 59 | t.Fail() 60 | } 61 | } 62 | 63 | func TestSplitDockerImageWithRepositoryAndTag(t *testing.T) { 64 | registry, repository, tag := SplitDockerImage("ubuntu:12.04") 65 | 66 | if registry != "" { 67 | t.Fail() 68 | } 69 | 70 | if repository != "ubuntu" { 71 | t.Fail() 72 | } 73 | 74 | if tag != "12.04" { 75 | t.Fail() 76 | } 77 | } 78 | 79 | func TestNextSlotEmpty(t *testing.T) { 80 | if NextSlot([]int{}) != 0 { 81 | t.Fatal("Expected 0") 82 | } 83 | } 84 | 85 | func TestNextSlotSimple(t *testing.T) { 86 | if NextSlot([]int{0}) != 1 { 87 | t.Fatal("Expected 1") 88 | } 89 | } 90 | 91 | func TestNextSlotGap(t *testing.T) { 92 | if NextSlot([]int{0, 1, 3}) != 2 { 93 | t.Fatal("Expected 2") 94 | } 95 | } 96 | 97 | func TestNextSlotEnd(t *testing.T) { 98 | if NextSlot([]int{0, 1, 2, 3}) != 4 { 99 | t.Fatal("Expected 4") 100 | } 101 | } 102 | 103 | func TestParseMemBlank(t *testing.T) { 104 | i, err := ParseMemory("") 105 | if err != nil { 106 | t.Fatalf("Expected 0. Got %s", err) 107 | } 108 | 109 | if i != 0 { 110 | t.Fatal("Expected 0") 111 | } 112 | } 113 | 114 | func TestParseMemNaN(t *testing.T) { 115 | _, err := ParseMemory("abc") 116 | if err == nil { 117 | t.Fatalf("Expected error. Got %s", nil) 118 | } 119 | } 120 | 121 | func TestParseMemBasic(t *testing.T) { 122 | i, err := ParseMemory("1024") 123 | if err != nil { 124 | t.Fatalf("Expected 1024. Got %s", err) 125 | } 126 | if i != 1024 { 127 | t.Fatal("Expected 1024") 128 | } 129 | } 130 | 131 | func TestParseMemByteSuffix(t *testing.T) { 132 | i, err := ParseMemory("2048b") 133 | if err != nil { 134 | t.Fatalf("Expected 2048. Got %s", err) 135 | } 136 | if i != 2048 { 137 | t.Fatal("Expected 2048") 138 | } 139 | } 140 | 141 | func TestParseMemKiloSuffix(t *testing.T) { 142 | i, err := ParseMemory("2k") 143 | if err != nil { 144 | t.Fatalf("Expected 2048. Got %s", err) 145 | } 146 | if i != 2048 { 147 | t.Fatal("Expected 2048") 148 | } 149 | } 150 | 151 | func TestParseMemMegSuffix(t *testing.T) { 152 | i, err := ParseMemory("3m") 153 | if err != nil { 154 | t.Fatalf("Expected 3145728. Got %s", err) 155 | } 156 | if i != 3145728 { 157 | t.Fatal("Expected 3145728") 158 | } 159 | } 160 | 161 | func TestParseMemGigSuffix(t *testing.T) { 162 | i, err := ParseMemory("4g") 163 | if err != nil { 164 | t.Fatalf("Expected 4294967296. Got %s", err) 165 | } 166 | if i != 4294967296 { 167 | t.Fatal("Expected 4294967296") 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /utils/vmap.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | // VersionedMap is a CRDT where each key contains a version history of prior values. 9 | // The value of the key is the value with the latest version. VersionMaps can be combined 10 | // such that they always converge to the same values for all keys. 11 | type VersionedMap struct { 12 | values map[string][]mapEntry 13 | } 14 | 15 | type mapEntry struct { 16 | value string 17 | version int64 18 | } 19 | 20 | func NewVersionedMap() *VersionedMap { 21 | return &VersionedMap{ 22 | values: make(map[string][]mapEntry), 23 | } 24 | } 25 | 26 | func (v *VersionedMap) currentVersion(key string) int64 { 27 | next := int64(0) 28 | for _, mapEntry := range v.values[key] { 29 | if mapEntry.version > next { 30 | next = mapEntry.version 31 | } 32 | } 33 | return next 34 | } 35 | 36 | func (v *VersionedMap) nextVersion(key string) int64 { 37 | return v.currentVersion(key) + 1 38 | } 39 | 40 | func (v *VersionedMap) SetVersion(key, value string, version int64) { 41 | entries := v.values[key] 42 | v.values[key] = append(entries, mapEntry{ 43 | value: value, 44 | version: version, 45 | }) 46 | } 47 | 48 | func (v *VersionedMap) UnSetVersion(key string, version int64) { 49 | entries := v.values[key] 50 | v.values[key] = append(entries, mapEntry{ 51 | value: "", 52 | version: version, 53 | }) 54 | } 55 | 56 | func (v *VersionedMap) Set(key, value string) { 57 | v.SetVersion(key, value, v.nextVersion(key)) 58 | } 59 | 60 | func (v *VersionedMap) UnSet(key string) { 61 | v.UnSetVersion(key, v.nextVersion(key)) 62 | } 63 | 64 | func (v *VersionedMap) Get(key string) string { 65 | entries := v.values[key] 66 | maxEntry := mapEntry{} 67 | for _, entry := range entries { 68 | // value is max(version) 69 | if entry.version > maxEntry.version { 70 | maxEntry = entry 71 | } 72 | 73 | // if there is a conflict, prefer setting a value over unsetting one 74 | // as well the largest value as a tie-breaker if two sets conflict. 75 | if entry.version == maxEntry.version && entry.value > maxEntry.value { 76 | maxEntry = entry 77 | } 78 | 79 | } 80 | return maxEntry.value 81 | } 82 | 83 | func (v *VersionedMap) Keys() []string { 84 | keys := []string{} 85 | for k := range v.values { 86 | keys = append(keys, k) 87 | } 88 | return keys 89 | } 90 | 91 | func (v *VersionedMap) LatestVersion() int64 { 92 | latest := int64(0) 93 | for _, entries := range v.values { 94 | for _, mapEntry := range entries { 95 | if mapEntry.version > latest { 96 | latest = mapEntry.version 97 | } 98 | } 99 | } 100 | return latest 101 | } 102 | 103 | func (v *VersionedMap) Merge(other *VersionedMap) { 104 | for k, entries := range other.values { 105 | v.values[k] = append(v.values[k], entries...) 106 | } 107 | } 108 | 109 | func (v *VersionedMap) MarshalMap() map[string]string { 110 | result := make(map[string]string) 111 | for key, entries := range v.values { 112 | for _, mapEntry := range entries { 113 | op := "s" 114 | if mapEntry.value == "" { 115 | op = "u" 116 | } 117 | mapKey := strings.Join([]string{key, op, strconv.FormatInt(mapEntry.version, 10)}, ":") 118 | result[mapKey] = mapEntry.value 119 | } 120 | 121 | } 122 | return result 123 | } 124 | 125 | func (v *VersionedMap) UnmarshalMap(serialized map[string]string) error { 126 | 127 | for key, val := range serialized { 128 | parts := strings.Split(key, ":") 129 | version, err := strconv.ParseInt(parts[2], 10, 64) 130 | if err != nil { 131 | return err 132 | } 133 | if parts[1] == "s" { 134 | v.SetVersion(parts[0], val, version) 135 | } else { 136 | v.UnSetVersion(parts[0], version) 137 | } 138 | } 139 | return nil 140 | } 141 | 142 | // MarshalExpiredMap returns historical entries that have been 143 | // superseded by newer values 144 | func (v *VersionedMap) MarshalExpiredMap(age int64) map[string]string { 145 | result := make(map[string]string) 146 | for key, entries := range v.values { 147 | currentVersion := v.currentVersion(key) 148 | for _, mapEntry := range entries { 149 | if mapEntry.version >= currentVersion-age { 150 | continue 151 | } 152 | op := "s" 153 | if mapEntry.value == "" { 154 | op = "u" 155 | } 156 | mapKey := strings.Join([]string{key, op, strconv.FormatInt(mapEntry.version, 10)}, ":") 157 | result[mapKey] = mapEntry.value 158 | } 159 | 160 | } 161 | return result 162 | } 163 | -------------------------------------------------------------------------------- /utils/vmap_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSet(t *testing.T) { 8 | vmap := NewVersionedMap() 9 | vmap.SetVersion("k1", "v1", 2) 10 | vmap.SetVersion("k1", "v2", 1) 11 | vmap.Set("k2", "v2") 12 | vmap.SetVersion("k2", "v3", 3) 13 | vmap.SetVersion("k2", "v4", 2) 14 | 15 | if vmap.Get("k1") != "v1" { 16 | t.Fail() 17 | } 18 | 19 | if vmap.Get("k2") != "v3" { 20 | t.Fail() 21 | } 22 | } 23 | 24 | func TestMerge(t *testing.T) { 25 | vmap1 := NewVersionedMap() 26 | vmap1.Set("k1", "v1") 27 | 28 | vmap2 := NewVersionedMap() 29 | vmap2.Set("k1", "v2") 30 | 31 | vmap1.Merge(vmap2) 32 | vmap2.Merge(vmap1) 33 | 34 | if vmap1.Get("k1") != "v2" { 35 | t.Fail() 36 | } 37 | if vmap2.Get("k1") != "v2" { 38 | t.Fail() 39 | } 40 | } 41 | 42 | func TestUnset(t *testing.T) { 43 | vmap := NewVersionedMap() 44 | vmap.Set("k1", "v1") 45 | vmap.UnSet("k1") 46 | 47 | if vmap.Get("k1") != "" { 48 | t.Fail() 49 | } 50 | } 51 | 52 | func TestUnsetConflict(t *testing.T) { 53 | vmap := NewVersionedMap() 54 | vmap.Set("k1", "v1") 55 | vmap.Set("k1", "v2") 56 | vmap.UnSetVersion("k1", 2) 57 | 58 | if vmap.Get("k1") != "v2" { 59 | t.Fail() 60 | } 61 | 62 | vmap = NewVersionedMap() 63 | vmap.Set("k1", "v1") 64 | vmap.UnSetVersion("k1", 2) 65 | vmap.SetVersion("k1", "v2", 2) 66 | 67 | if vmap.Get("k1") != "v2" { 68 | t.Fail() 69 | } 70 | 71 | vmap = NewVersionedMap() 72 | vmap.Set("k1", "v1") 73 | vmap.UnSet("k1") 74 | vmap.SetVersion("k1", "v2", 2) 75 | vmap.SetVersion("k1", "v3", 2) 76 | 77 | if vmap.Get("k1") != "v3" { 78 | t.Fail() 79 | } 80 | } 81 | 82 | func TestMarshalMap(t *testing.T) { 83 | 84 | vmap := NewVersionedMap() 85 | vmap.Set("k1", "v1") 86 | vmap.Set("k1", "v2") 87 | vmap.UnSetVersion("k1", 2) 88 | 89 | vmap.Set("k2", "v1") 90 | vmap.Set("k2", "v2") 91 | 92 | serialized := vmap.MarshalMap() 93 | if serialized["k1:s:1"] != "v1" { 94 | t.Fail() 95 | } 96 | if serialized["k1:s:2"] != "v2" { 97 | t.Fail() 98 | } 99 | if serialized["k1:u:2"] != "" { 100 | t.Fail() 101 | } 102 | if serialized["k2:s:1"] != "v1" { 103 | t.Fail() 104 | } 105 | if serialized["k2:s:2"] != "v2" { 106 | t.Fail() 107 | } 108 | } 109 | 110 | func TestUnmarshalMap(t *testing.T) { 111 | 112 | serialized := map[string]string{ 113 | "k1:s:1": "v1", 114 | "k1:s:2": "v2", 115 | "k1:u:2": "", 116 | "k2:s:1": "v1", 117 | "k2:s:2": "v2", 118 | "k3:s:1": "v1", 119 | "k3:u:2": "", 120 | } 121 | 122 | vmap := NewVersionedMap() 123 | vmap.UnmarshalMap(serialized) 124 | 125 | if vmap.Get("k1") != "v2" { 126 | t.Fail() 127 | } 128 | if vmap.Get("k2") != "v2" { 129 | t.Fail() 130 | } 131 | if vmap.Get("k3") != "" { 132 | t.Fail() 133 | } 134 | 135 | } 136 | 137 | func TestLatestversion(t *testing.T) { 138 | vmap := NewVersionedMap() 139 | vmap.Set("k1", "v1") 140 | vmap.Set("k2", "v1") 141 | vmap.SetVersion("k2", "v2", 3) 142 | 143 | if vmap.LatestVersion() != 3 { 144 | t.Fail() 145 | } 146 | } 147 | 148 | func TestMarshalExpired(t *testing.T) { 149 | vmap := NewVersionedMap() 150 | vmap.Set("k1", "v1") 151 | vmap.Set("k2", "v1") 152 | vmap.Set("k1", "v2") 153 | 154 | old := vmap.MarshalExpiredMap(0) 155 | if len(old) != 1 { 156 | t.Fatalf("Expected 1 expired entry") 157 | } 158 | 159 | if old["k1:s:1"] != "v1" { 160 | t.Fatalf("Expected value not found. Got %#v", old) 161 | } 162 | 163 | vmap.Set("k2", "v2") 164 | vmap.Set("k2", "v3") 165 | 166 | old = vmap.MarshalExpiredMap(0) 167 | if len(old) != 3 { 168 | t.Fatalf("Expected 3 expired entry") 169 | } 170 | 171 | if old["k1:s:1"] != "v1" { 172 | t.Fatalf("Expected value not found. Got %#v", old) 173 | } 174 | 175 | if old["k2:s:1"] != "v1" { 176 | t.Fatalf("Expected value not found. Got %#v", old) 177 | } 178 | 179 | if old["k2:s:2"] != "v2" { 180 | t.Fatalf("Expected value not found. Got %#v", old) 181 | } 182 | } 183 | 184 | func TestMarshalExpiredWithAge(t *testing.T) { 185 | vmap := NewVersionedMap() 186 | vmap.Set("k1", "v1") 187 | vmap.Set("k1", "v2") 188 | vmap.Set("k1", "v3") 189 | vmap.Set("k1", "v4") 190 | vmap.Set("k1", "v5") 191 | vmap.Set("k1", "v6") 192 | 193 | old := vmap.MarshalExpiredMap(2) 194 | if len(old) != 3 { 195 | t.Fatalf("Expected 3 expired entry") 196 | } 197 | 198 | if old["k1:s:1"] != "v1" { 199 | t.Fatalf("Expected value not found. Got %#v", old) 200 | } 201 | 202 | if old["k1:s:2"] != "v2" { 203 | t.Fatalf("Expected value not found. Got %#v", old) 204 | } 205 | if old["k1:s:3"] != "v3" { 206 | t.Fatalf("Expected value not found. Got %#v", old) 207 | } 208 | } 209 | --------------------------------------------------------------------------------