├── .drone.yml ├── .gitignore ├── .goxc.json ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── cliconfig ├── config.go ├── create_config.go └── create_environment.go ├── commands ├── compose │ ├── actions.go │ └── compose_cmds.go ├── config.go ├── depcon.go ├── format.go └── marathon │ ├── app_bg_cmds.go │ ├── app_cmds.go │ ├── app_cmds_test.go │ ├── app_log_cmds.go │ ├── deploy_cmds.go │ ├── event_cmds.go │ ├── group_cmds.go │ ├── marathon_cmds.go │ ├── resources │ ├── test.env │ └── testcontext.json │ ├── server_cmds.go │ ├── task_cmds.go │ ├── templatectx.go │ ├── templatectx_funcs.go │ ├── templatectx_test.go │ └── templates.go ├── compose ├── compose.go ├── compose_wrapper.go ├── logfactory.go └── struct.go ├── docker-release ├── .gitignore ├── Dockerfile └── env.sample ├── main.go ├── marathon ├── application.go ├── application_test.go ├── bluegreen │ ├── bluegreen.go │ ├── deployment.go │ ├── haproxy.go │ └── util.go ├── deployment.go ├── error.go ├── event.go ├── event_types.go ├── group.go ├── group_test.go ├── marathon.go ├── marathon_test.go ├── resources │ └── schema.json ├── server.go ├── struct.go ├── task.go ├── testdata │ ├── apps │ │ ├── app_params.json │ │ ├── get_app_response.json │ │ └── list_apps_response.json │ ├── common │ │ └── deployid_response.json │ └── groups │ │ ├── get_group_response.json │ │ └── list_groups_response.json └── wait.go ├── pkg ├── cli │ ├── output.go │ └── util.go ├── encoding │ ├── encoder.go │ ├── json.go │ └── yaml.go ├── envsubst │ ├── envsubst.go │ └── envsubst_test.go ├── httpclient │ ├── client.go │ ├── client_test.go │ └── methods.go ├── logger │ ├── logformat.go │ └── logger.go ├── mockrest │ └── mockwebserver.go └── userdir │ └── userdir.go ├── samples ├── docker-compose-params.yml └── docker-compose.yml └── utils └── utils.go /.drone.yml: -------------------------------------------------------------------------------- 1 | build: 2 | image: golang:1.5 3 | environment: 4 | - GO15VENDOREXPERIMENT=1 5 | commands: 6 | - go get github.com/stretchr/testify 7 | - go get -v ./... 8 | - go build 9 | - go test ./... 10 | -------------------------------------------------------------------------------- /.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 | *.test 24 | *.prof 25 | 26 | # App and IDEA 27 | depcon 28 | depcon.iml 29 | .idea 30 | tests/ 31 | build/ 32 | .goxc.local.json 33 | .DS_Store 34 | samples/* 35 | .vscode 36 | dist/ -------------------------------------------------------------------------------- /.goxc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ArtifactsDest": "build", 3 | "ConfigVersion": "0.9", 4 | "PackageVersion": "0.1", 5 | "TaskSettings": { 6 | "bintray": { 7 | "user": "gondor", 8 | "package": "depcon", 9 | "repository": "utils", 10 | "subject": "pacesys" 11 | }, 12 | "publish-github": { 13 | "owner": "ContainX", 14 | "repository": "depcon" 15 | }, 16 | "debs": { 17 | "metadata": { 18 | "description": "Depcon - Mesos/Marathon deployer", 19 | "maintainer": "Jeremy Unruh (https://github.com/gondor)" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.8 4 | env: 5 | - "PATH=$HOME/gopath/bin:$PATH" 6 | before_install: 7 | - go get github.com/mitchellh/gox 8 | - go get github.com/tcnksm/ghr 9 | - go get github.com/stretchr/testify 10 | install: 11 | - go get -v ./... 12 | script: 13 | - go test ./... 14 | - go build 15 | after_success: 16 | - gox -osarch "darwin/amd64 linux/amd64 windows/amd64" -ldflags "-X main.VERSION=0.9.2-$TRAVIS_BUILD_NUMBER -X 'main.BUILD_DATE=$(date -u '+%Y-%m-%d %H:%M:%S')'" -output "dist/{{.Dir}}-{{.OS}}_{{.Arch}}" 17 | - ghr --username ContainX --token $GITHUB_TOKEN --replace --prerelease --debug pre-release dist/ 18 | 19 | # whitelist 20 | branches: 21 | only: 22 | - master 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION = 0.9.5 2 | 3 | GO_FMT = gofmt -s -w -l . 4 | GO_XC = goxc -os="linux darwin windows" -tasks-="rmbin" 5 | 6 | GOXC_FILE = .goxc.local.json 7 | 8 | all: deps compile 9 | 10 | compile: goxc 11 | 12 | goxc: 13 | $(shell echo '{\n "ConfigVersion": "0.9",\n "PackageVersion": "$(VERSION)",' > $(GOXC_FILE)) 14 | $(shell echo ' "TaskSettings": {' >> $(GOXC_FILE)) 15 | $(shell echo ' "bintray": {\n "apikey": "$(BINTRAY_APIKEY)"' >> $(GOXC_FILE)) 16 | $(shell echo ' },' >> $(GOXC_FILE)) 17 | $(shell echo ' "publish-github": {' >> $(GOXC_FILE)) 18 | $(shell echo ' "apikey": "$(GITHUB_APIKEY)",' >> $(GOXC_FILE)) 19 | $(shell echo ' "body": "",' >> $(GOXC_FILE)) 20 | $(shell echo ' "include": "*.tar.gz,*.deb,depcon-linux64,depcon-osx64,depcon-win64.exe"' >> $(GOXC_FILE)) 21 | $(shell echo ' }\n } \n}' >> $(GOXC_FILE)) 22 | $(GO_XC) 23 | cp build/$(VERSION)/linux_amd64/depcon build/$(VERSION)/depcon-linux64 24 | cp build/$(VERSION)/darwin_amd64/depcon build/$(VERSION)/depcon-osx64 25 | cp build/$(VERSION)/windows_amd64/depcon.exe build/$(VERSION)/depcon-win64.exe 26 | 27 | deps: 28 | go get 29 | 30 | format: 31 | $(GO_FMT) 32 | 33 | bintray: 34 | $(GO_XC) bintray 35 | 36 | github: 37 | $(GO_XC) publish-github 38 | 39 | docker-build: 40 | cp build/$(VERSION)/linux_amd64/depcon docker-release/depcon 41 | docker build -t containx/depcon docker-release/ 42 | docker tag containx/depcon containx/depcon:$(VERSION) 43 | 44 | docker-push: 45 | docker push containx/depcon 46 | docker push containx/depcon:$(VERSION) 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Depcon (*Container Deployment*) 2 | 3 | [![Build Status](https://travis-ci.org/ContainX/depcon.svg)](https://travis-ci.org/ContainX/depcon) [![release](http://github-release-version.herokuapp.com/github/ContainX/depcon/release.svg?style=flat)](https://github.com/ContainX/depcon/releases/latest) [![GoDoc](https://godoc.org/github.com/ContainX/depcon?status.svg)](https://godoc.org/github.com/ContainX/depcon) [![Join the chat at https://gitter.im/ContainX/community](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/ContainX/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | An advanced deployment tool for docker containers against Mesos/Marathon, Kubernetes and Amazon ECS 6 | 7 | For full up to date documentation and usage please see: 8 | 9 | ### [http://depcon.containx.io](http://depcon.containx.io) 10 | 11 | ## Overview 12 | 13 | Depcon makes managing clusters that run docker containers a breeze. It offers the ability to define environments such as test, pre-prod, production against Marathon (initial release), Kubernetes and Amazon ECS. 14 | 15 | **Some key features of Depcon are:** 16 | - Variable interpolation in deployment descriptors 17 | - Output results in Column, YAML and JSON formats for easy integration with automation 18 | - Accepts deployment descriptors in either JSON or YAML format 19 | - **Full Mesos/Marathon support** 20 | - Application, Group & Task management 21 | - Partial application updates: CPU, Memory & Scaling 22 | - Quick application rollback 23 | - Leader election, Server info and elapsed response 24 | - **Docker compose support** 25 | - Supports all major operating systems 26 | - Works with Docker Machine aka Boot2Docker 27 | - Parameter support -- `${PARAMS}` can be placed in compose files 28 | - Future releases will offer a proposed Open Deployment Descriptor format which will allow Depcon to deploy a common descriptor against Marathon, Kubernetes, ECS and Docker Swarm. 29 | - Ability to wait until a new or updated container deployment is healthy 30 | 31 | ### Binary Installation 32 | 33 | Binaries are available through GitHub releases. You can download the appropriate binary, package and version from the [Releases](https://github.com/ContainX/depcon/releases) page 34 | 35 | ### Build and Install the Binaries from Source 36 | 37 | **Pre-Requisites** 38 | * GOLANG 1.6+ 39 | 40 | Add Depcon and its package dependencies to your go `src` directory 41 | 42 | go get -v github.com/ContainX/depcon 43 | 44 | Once the `get` has completed, you should find your new `depcon` (or `depcon.exe`) executable sitting inside the `$GOPATH/bin/` 45 | 46 | To update Depcon's dependencies, use `go get` with the `-u` option. 47 | 48 | go get -u -v github.com/ContainX/depcon 49 | 50 | 51 | ### Running Depcon in Docker 52 | 53 | With each release we publish a very small docker image containing depcon. 54 | 55 | **Quick Example** 56 | ``` 57 | # Run depcon in the background capture cid, add alias 58 | $ cid=$(docker run -itd -v $PWD:/data pacesys/depcon) 59 | $ alias depcon="docker exec -it ${cid} depcon" 60 | 61 | # Use depcon like it was native 62 | $ depcon app list 63 | 64 | ``` 65 | 66 | For additional instructions in testing depcon within docker (useful for CI systems) see the docker hub repository at: https://hub.docker.com/r/pacesys/depcon/ 67 | 68 | ## Global options in Depcon 69 | 70 | #### Output Options 71 | 72 | Depcon makes it easy to integrate with third party systems. Any command or query in depcon has the options to list results in tabular, json or yaml formats. 73 | 74 | For example: `depcon app list -o json` would return a list of running applications in JSON form. You can also use `-o yaml` for yaml or no option which by default results in table/tabular form. 75 | 76 | ## Using Depcon with Mesos/Marathon 77 | 78 | ### Applications 79 | 80 | Below are examples with application managements 81 | 82 | #### Listing deployed applications 83 | 84 | List all applications 85 | 86 | ``` 87 | $ depcon app list 88 | ``` 89 | 90 | #### Getting details about a running application by it's ID 91 | 92 | Gets an application details by Id 93 | 94 | ``` 95 | $ depcon app get myapp 96 | ``` 97 | 98 | #### Destroy/Delete a running application 99 | 100 | Remove an application [applicationId] and all of it's instances 101 | 102 | ``` 103 | $ depcon app destroy myapp 104 | ``` 105 | 106 | #### Scale an Application 107 | 108 | Scales [appliationId] to total [instances] 109 | 110 | ``` 111 | $ depcon app scale myapp 2 112 | ``` 113 | 114 | #### Restart a running application 115 | 116 | Restarts an application by Id 117 | 118 | ``` 119 | $ depcon app restart myapp 120 | ``` 121 | 122 | #### Update a running application 123 | 124 | ``` 125 | // Update CPU resources 126 | $ depcon app update cpu myapp 0.5 127 | 128 | // Update Memory to 400mb 129 | $ depcon app update mem myapp 400 130 | ``` 131 | 132 | ## Using Depcon as a Docker Compose client 133 | 134 | Depcon supports Docker Compose natively on all major operating systems. This feature is currently in beta, please report any found issues. 135 | 136 | **Available Docker Compose Actions** 137 | 138 | ``` 139 | $ depcon compose 140 | 141 | Usage: 142 | depcon compose [command] 143 | 144 | Available Commands: 145 | build Build or rebuild services 146 | kill Kill containers 147 | logs View output from containers 148 | port Stops services 149 | ps List containers 150 | up Create and start containers 151 | pull Pulls service imagess 152 | restart Restart running containers 153 | rm Remove stopped containers 154 | start Start services 155 | stop Stops services 156 | up Create and start containers 157 | 158 | Flags: 159 | --compose-file="docker-compose.yml": Docker compose file 160 | -h, --help[=false]: help for compose 161 | --name="depcon_proj": Project name for this composition 162 | 163 | 164 | Global Flags: 165 | -e, --env="": Specifies the Environment name to use (eg. test | prod | etc). This can be omitted if only a single environment has been defined 166 | -o, --output="column": Specifies the output format [column | json | yaml] 167 | --verbose[=false]: Enables debug/verbose logging 168 | 169 | 170 | Use "depcon compose [command] --help" for more information about a command. 171 | ``` 172 | 173 | The examples below assume `docker-compose.yml` is found in the execution directory. If the compose file is located in another location then 174 | the global `--compose-file` flag can be invoked. 175 | 176 | #### Creating and Starting containers 177 | 178 | ``` 179 | $ depcon compose up 180 | ``` 181 | 182 | #### Creating and Starting a specific service 183 | 184 | ``` 185 | $ depcon compose up redis 186 | ``` 187 | 188 | #### Stopping compose services 189 | 190 | ``` 191 | $ depcon compose kill 192 | ``` 193 | 194 | ### Using parameters within Compose templates 195 | 196 | Depcon offers extenability on top of tradditional Docker compose. It allows params to be placed within compose files in the format of `${PARAM}`. Depcon allows these params to be resolved via the flag `--param PARAM=value` during use or via exported env variables. 197 | 198 | Take a look at the `samples/docker-compose-params.yml` in Depcon source repo. Here's an example of params using the referenced sample compose file. 199 | 200 | ``` 201 | // Inline params 202 | $ depcon compose up redis --compose-file samples/docker-compose-params.yml -p REDIS_PORT=6379 203 | 204 | // As env variables 205 | $ export REDIS_PORT=6379 206 | $ depcon compose up redis --compose-file samples/docker-compose-params.yml 207 | ``` 208 | 209 | ## License 210 | 211 | This software is licensed under the Apache 2 license, quoted below. 212 | 213 | Copyright 2016 Jeremy Unruh 214 | 215 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 216 | use this file except in compliance with the License. You may obtain a copy of 217 | the License at 218 | 219 | http://www.apache.org/licenses/LICENSE-2.0 220 | 221 | Unless required by applicable law or agreed to in writing, software 222 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 223 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 224 | License for the specific language governing permissions and limitations under 225 | the License. 226 | -------------------------------------------------------------------------------- /cliconfig/config.go: -------------------------------------------------------------------------------- 1 | // Provides storage and retrieval of user preferences and cluster environment configuration 2 | package cliconfig 3 | 4 | // Some functions have been imported for docker/cliconfig 5 | 6 | import ( 7 | "encoding/base64" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "github.com/ContainX/depcon/pkg/userdir" 12 | "io" 13 | "os" 14 | "path/filepath" 15 | "strings" 16 | ) 17 | 18 | const ( 19 | ConfigFileName = "config.json" 20 | DotConfigDir = ".depcon" 21 | TypeMarathon = "marathon" 22 | TypeKubernetes = "kubernetes" 23 | TypeECS = "ecs" 24 | ) 25 | 26 | var ( 27 | configDir = os.Getenv("DEPCON_CONFIG") 28 | ErrEnvNotFound = errors.New("Specified environment was not found") 29 | ) 30 | 31 | func init() { 32 | if configDir == "" { 33 | configDir = filepath.Join(userdir.Get(), DotConfigDir) 34 | } 35 | } 36 | 37 | // ConfigDir returns the directory the configuration file is stored in 38 | func ConfigDir() string { 39 | return configDir 40 | } 41 | 42 | // SetConfigDir sets the directory the configuration file is stored in 43 | func SetConfigDir(dir string) { 44 | configDir = dir 45 | } 46 | 47 | type ConfigFile struct { 48 | Format string `json:"format,omitempty"` 49 | RootService bool `json:"rootservice"` 50 | Environments map[string]*ConfigEnvironment `json:"environments,omitempty"` 51 | DefaultEnv string `json:"default,omitempty"` 52 | filename string // not serialized 53 | } 54 | 55 | type ConfigEnvironment struct { 56 | // currently only supporting marathon as initial release 57 | Marathon *ServiceConfig `json:"marathon,omitempty"` 58 | } 59 | 60 | type ServiceConfig struct { 61 | Username string `json:"username,omitempty"` 62 | Password string `json:"password,omitempty"` 63 | Token string `json:"token"` 64 | HostUrl string `json:"serveraddress,omitempty"` 65 | Features map[string]string `json:"features,omitempty"` 66 | Name string `json:"-"` 67 | } 68 | 69 | func HasExistingConfig() (*ConfigFile, bool) { 70 | configFile, err := Load("") 71 | return configFile, err == nil 72 | } 73 | 74 | func (configFile *ConfigFile) LoadFromReader(configData io.Reader) error { 75 | if err := json.NewDecoder(configData).Decode(&configFile); err != nil { 76 | return err 77 | } 78 | var err error 79 | for _, configEnv := range configFile.Environments { 80 | configEnv.Marathon.Password, err = DecodePassword(configEnv.Marathon.Password) 81 | if err != nil { 82 | return err 83 | } 84 | } 85 | return nil 86 | } 87 | 88 | func Load(configDir string) (*ConfigFile, error) { 89 | if configDir == "" { 90 | configDir = ConfigDir() 91 | } 92 | 93 | configFile := ConfigFile{ 94 | Format: "column", 95 | Environments: make(map[string]*ConfigEnvironment), 96 | filename: filepath.Join(configDir, ConfigFileName), 97 | } 98 | 99 | _, err := os.Stat(configFile.filename) 100 | if err == nil { 101 | file, err := os.Open(configFile.filename) 102 | if err != nil { 103 | return &configFile, err 104 | } 105 | 106 | defer file.Close() 107 | err = configFile.LoadFromReader(file) 108 | return &configFile, err 109 | } 110 | return &configFile, err 111 | } 112 | 113 | // Determines if the configuration has only a single environment defined and the user prefers a rooted service 114 | // Returns the environment type and true or else "" and false 115 | func (configFile *ConfigFile) DetermineIfServiceIsRooted() (string, bool) { 116 | if len(configFile.Environments) > 1 { 117 | return "", false 118 | } 119 | 120 | // FIXME: Once we support more than Marathon (post initial release) - determine type 121 | return TypeMarathon, configFile.RootService 122 | } 123 | 124 | func (configEnv *ConfigEnvironment) EnvironmentType() string { 125 | // FIXME: Once we support more than Marathon (post initial release) - determine type 126 | return TypeMarathon 127 | } 128 | 129 | func (configFile *ConfigFile) AddEnvironment() { 130 | serviceEnv := createEnvironment() 131 | configEnv := &ConfigEnvironment{ 132 | Marathon: serviceEnv, 133 | } 134 | configFile.Environments[serviceEnv.Name] = configEnv 135 | configFile.Save() 136 | } 137 | 138 | func (configFile *ConfigFile) AddMarathonEnvironment(name, host, user, pass, token string) { 139 | service := &ServiceConfig{} 140 | service.Name = name 141 | service.HostUrl = host 142 | service.Username = user 143 | service.Password = pass 144 | service.Token = token 145 | 146 | configEnv := &ConfigEnvironment{ 147 | Marathon: service, 148 | } 149 | if len(configFile.Environments) == 0 { 150 | configFile.DefaultEnv = name 151 | configFile.RootService = true 152 | } 153 | configFile.Environments[name] = configEnv 154 | configFile.Save() 155 | } 156 | 157 | // Removes the specified environment from the configuration 158 | // {name} - name of the environment 159 | // {force} - if true will not prompt for confirmation 160 | // 161 | // Will return ErrEnvNotFound if the environment could not be found 162 | func (configFile *ConfigFile) RemoveEnvironment(name string, force bool) error { 163 | configEnv := configFile.Environments[name] 164 | if configEnv == nil { 165 | return ErrEnvNotFound 166 | } 167 | if !force { 168 | if !getBoolAnswer(fmt.Sprintf("Are you sure you would like to remove '%s'", name), true) { 169 | return nil 170 | } 171 | } 172 | if configFile.DefaultEnv == name { 173 | configFile.DefaultEnv = "" 174 | } 175 | delete(configFile.Environments, name) 176 | configFile.Save() 177 | 178 | return nil 179 | } 180 | 181 | // Sets the default environment to use. This is used by other parts of the application to eliminate the user always 182 | // specifying and environment 183 | // {name} - the environment name 184 | // 185 | // Will return ErrEnvNOtFound if the environment could not be found 186 | func (configFile *ConfigFile) SetDefaultEnvironment(name string) error { 187 | configEnv := configFile.Environments[name] 188 | if configEnv == nil { 189 | return ErrEnvNotFound 190 | } 191 | configFile.DefaultEnv = name 192 | configFile.Save() 193 | return nil 194 | } 195 | 196 | // Renames an environment and updates the default environment if it matches the current old 197 | // {oldName} - the old environment name 198 | // {newName} - the new environment name 199 | // 200 | // Will return ErrEnvNOtFound if the old environment could not be found 201 | func (configFile *ConfigFile) RenameEnvironment(oldName, newName string) error { 202 | configEnv := configFile.Environments[oldName] 203 | if configEnv == nil { 204 | return ErrEnvNotFound 205 | } 206 | delete(configFile.Environments, oldName) 207 | configFile.Environments[newName] = configEnv 208 | 209 | if configFile.DefaultEnv == oldName { 210 | configFile.DefaultEnv = newName 211 | } 212 | configFile.Save() 213 | return nil 214 | } 215 | 216 | // Returns the Configuration for the specified environment. If the environment 217 | // is not found then 218 | func (configFile *ConfigFile) GetEnvironment(name string) (*ConfigEnvironment, error) { 219 | configEnv := configFile.Environments[name] 220 | if configEnv == nil { 221 | return nil, ErrEnvNotFound 222 | } 223 | return configEnv, nil 224 | } 225 | 226 | func (configFile *ConfigFile) GetEnvironments() []string { 227 | keys := make([]string, 0, len(configFile.Environments)) 228 | for k := range configFile.Environments { 229 | keys = append(keys, k) 230 | } 231 | return keys 232 | } 233 | 234 | func (configFile *ConfigFile) SaveToWriter(writer io.Writer) error { 235 | tmpEnvConfigs := make(map[string]*ConfigEnvironment, len(configFile.Environments)) 236 | for k, configEnv := range configFile.Environments { 237 | configEnvCopy := configEnv 238 | 239 | if configEnvCopy.Marathon != nil { 240 | configEnvCopy.Marathon.Password = EncodePassword(configEnvCopy.Marathon) 241 | configEnvCopy.Marathon.Name = "" 242 | } 243 | tmpEnvConfigs[k] = configEnvCopy 244 | } 245 | saveEnvConfigs := configFile.Environments 246 | configFile.Environments = tmpEnvConfigs 247 | 248 | defer func() { configFile.Environments = saveEnvConfigs }() 249 | 250 | data, err := json.MarshalIndent(configFile, "", "\t") 251 | if err != nil { 252 | return err 253 | } 254 | _, err = writer.Write(data) 255 | return err 256 | } 257 | 258 | func (configFile *ConfigFile) Save() error { 259 | if configFile.Filename() == "" { 260 | configFile.filename = filepath.Join(configDir, ConfigFileName) 261 | } 262 | 263 | if err := os.MkdirAll(filepath.Dir(configFile.filename), 0700); err != nil { 264 | return err 265 | } 266 | f, err := os.OpenFile(configFile.filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) 267 | if err != nil { 268 | return err 269 | } 270 | defer f.Close() 271 | return configFile.SaveToWriter(f) 272 | } 273 | 274 | // EncodePassword creates a base64 encoded string using the authorization info. If the username 275 | // is not specified then an empty password is returned since we need both 276 | func EncodePassword(auth *ServiceConfig) string { 277 | if auth.Username == "" { 278 | return "" 279 | } 280 | authStr := auth.Username + ":" + auth.Password 281 | msg := []byte(authStr) 282 | encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg))) 283 | base64.StdEncoding.Encode(encoded, msg) 284 | return string(encoded) 285 | } 286 | 287 | func DecodePassword(authStr string) (string, error) { 288 | 289 | if authStr == "" { 290 | return "", nil 291 | } 292 | 293 | decLen := base64.StdEncoding.DecodedLen(len(authStr)) 294 | decoded := make([]byte, decLen) 295 | authByte := []byte(authStr) 296 | n, err := base64.StdEncoding.Decode(decoded, authByte) 297 | if err != nil { 298 | return "", err 299 | } 300 | if n > decLen { 301 | return "", fmt.Errorf("Something went wrong decoding service authentication") 302 | } 303 | arr := strings.SplitN(string(decoded), ":", 2) 304 | if len(arr) != 2 { 305 | return "", fmt.Errorf("Invalid auth configuration file") 306 | } 307 | password := strings.Trim(arr[1], "\x00") 308 | return password, nil 309 | } 310 | 311 | // Filename returns the name of the configuration file 312 | func (configFile *ConfigFile) Filename() string { 313 | return configFile.filename 314 | } 315 | -------------------------------------------------------------------------------- /cliconfig/create_config.go: -------------------------------------------------------------------------------- 1 | package cliconfig 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | StrDefaultY string = "[Y/n]" 10 | StrDefaultN string = "[y/N]" 11 | rootServiceQuestion = `When only a single environment has been defined, should we root the service command. 12 | 13 | For example: "depcon mar app list" would become "depcon app list", eliminating the marathon service command since 14 | we are only dealing with a single service and know what it is. 15 | 16 | Root single environment` 17 | ) 18 | 19 | func CreateMemoryMarathonConfig(host, user, password, token string) *ConfigFile { 20 | configFile, _ := Load("") 21 | configFile.RootService = true 22 | configFile.Format = "column" 23 | serviceEnv := &ServiceConfig{ 24 | Name: "memory", 25 | HostUrl: host, 26 | Username: user, 27 | Password: password, 28 | Token: token, 29 | } 30 | configEnv := &ConfigEnvironment{ 31 | Marathon: serviceEnv, 32 | } 33 | configFile.Environments[serviceEnv.Name] = configEnv 34 | configFile.DefaultEnv = serviceEnv.Name 35 | //configFile.Save() 36 | return configFile 37 | } 38 | 39 | func CreateNewConfigFromUserInput() *ConfigFile { 40 | fmt.Println("\n-------------------------------[ Generating Initital Configuration ]-------------------------------") 41 | 42 | configFile, _ := Load("") 43 | configFile.RootService = getBoolAnswer(rootServiceQuestion, true) 44 | configFile.Format = getDefaultFormatOption() 45 | serviceEnv := createEnvironment() 46 | configEnv := &ConfigEnvironment{ 47 | Marathon: serviceEnv, 48 | } 49 | configFile.Environments[serviceEnv.Name] = configEnv 50 | configFile.Save() 51 | return configFile 52 | } 53 | 54 | func getDefaultFormatOption() string { 55 | 56 | var response string 57 | fmt.Println("Default output format (can be overridden via runtime flag)") 58 | fmt.Println("1 - column") 59 | fmt.Println("2 - json") 60 | fmt.Println("3 - yaml") 61 | fmt.Printf("Option: ") 62 | 63 | fmt.Scanf("%s", &response) 64 | fmt.Println("") 65 | 66 | if strings.HasPrefix(response, "2") { 67 | return "json" 68 | } 69 | if strings.HasPrefix(response, "3") { 70 | return "yaml" 71 | } 72 | return "column" 73 | } 74 | 75 | // Asks a yes or no question and returns the boolean equivalent 76 | func getBoolAnswer(question string, defaultTrue bool) bool { 77 | var response string 78 | var yn string 79 | 80 | if defaultTrue { 81 | yn = StrDefaultY 82 | } else { 83 | yn = StrDefaultN 84 | } 85 | 86 | fmt.Printf("\n%s %s? ", question, yn) 87 | fmt.Scanf("%s", &response) 88 | 89 | if response == "" { 90 | if defaultTrue { 91 | return true 92 | } 93 | return false 94 | } 95 | 96 | response = strings.ToLower(response) 97 | if strings.HasPrefix(response, "y") { 98 | return true 99 | } else if strings.HasPrefix(response, "n") { 100 | return false 101 | } 102 | 103 | fmt.Printf("\nERROR: Must respond with 'y' or 'no'\n") 104 | return getBoolAnswer(question, defaultTrue) 105 | } 106 | -------------------------------------------------------------------------------- /cliconfig/create_environment.go: -------------------------------------------------------------------------------- 1 | package cliconfig 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ContainX/depcon/utils" 6 | "github.com/bgentry/speakeasy" 7 | "net/url" 8 | "os" 9 | "regexp" 10 | ) 11 | 12 | const ( 13 | AlphaNumDash string = `^[a-zA-Z0-9_-]*$` 14 | ) 15 | 16 | var ( 17 | RegExAlphaNumDash *regexp.Regexp = regexp.MustCompile(AlphaNumDash) 18 | ) 19 | 20 | func getAlpaNumDash(question string) string { 21 | var response string 22 | 23 | fmt.Printf("%s: ", question) 24 | fmt.Scanf("%s", &response) 25 | 26 | if RegExAlphaNumDash.MatchString(response) { 27 | return response 28 | } 29 | 30 | fmt.Printf("\nERROR: '%s' must contain valid characters within %s\n", response, AlphaNumDash) 31 | return getAlpaNumDash(question) 32 | } 33 | 34 | func getPassword(question string) string { 35 | password, err := speakeasy.Ask(fmt.Sprintf("%s", question)) 36 | if err != nil { 37 | fmt.Printf("\nERROR: %s\n", err.Error()) 38 | return getPassword(question) 39 | } 40 | return password 41 | } 42 | 43 | func getPasswordWithVerify() string { 44 | pass1 := getPassword("Password: ") 45 | pass2 := getPassword("Verify Password: ") 46 | if pass1 != pass2 { 47 | fmt.Println("Password and Verify Password don't match") 48 | return getPasswordWithVerify() 49 | } 50 | return pass1 51 | } 52 | 53 | func getTokenWithVerify() string { 54 | token := getPassword("Token: ") 55 | token2 := getPassword("Verify Token: ") 56 | if token != token2 { 57 | fmt.Println("Token and Verify Token don't match") 58 | return getTokenWithVerify() 59 | } 60 | return token 61 | } 62 | 63 | // Asks the user for the remote URI of the Marathon service 64 | func getMarathonURL(count int) string { 65 | if count > 5 { 66 | fmt.Printf("Too many retries obtaining Marathon URL. If depcon is running within docker please insure 'docker run -it' is set.\n") 67 | os.Exit(1) 68 | } 69 | var response string 70 | fmt.Print("Marathon URL (eg. http://hostname:8080) : ") 71 | fmt.Scanf("%s", &response) 72 | 73 | err := ValidateMarathonURL(response) 74 | if err == nil { 75 | return response 76 | } 77 | 78 | fmt.Printf("\n%s", err.Error()) 79 | return getMarathonURL(count + 1) 80 | } 81 | 82 | func ValidateMarathonURL(marathonURL string) error { 83 | _, err := url.ParseRequestURI(marathonURL) 84 | if err != nil || !utils.HasURLScheme(marathonURL) { 85 | return fmt.Errorf("ERROR: '%s' must be a valid URL", marathonURL) 86 | } 87 | return nil 88 | } 89 | 90 | func createEnvironment() *ServiceConfig { 91 | service := ServiceConfig{} 92 | service.Name = getAlpaNumDash("Environment Name (eg. test, stage, prod) ") 93 | service.HostUrl = getMarathonURL(0) 94 | 95 | if getBoolAnswer("Authentication Required", false) { 96 | service.Username = getAlpaNumDash("Username") 97 | service.Password = getPasswordWithVerify() 98 | } 99 | 100 | if getBoolAnswer("Token Required", false) { 101 | service.Token = getTokenWithVerify() 102 | } 103 | 104 | fmt.Println("") 105 | return &service 106 | } 107 | -------------------------------------------------------------------------------- /commands/compose/actions.go: -------------------------------------------------------------------------------- 1 | package compose 2 | 3 | import ( 4 | "errors" 5 | "github.com/ContainX/depcon/compose" 6 | "github.com/ContainX/depcon/pkg/cli" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var ( 11 | PortInvalidArgs error = errors.New("Arguments must be in the form of: SERVICE PRIVATE_PORT") 12 | ) 13 | 14 | type ComposeAction func(c compose.Compose, cmd *cobra.Command, args []string) error 15 | 16 | type ComposePreHook func(composeFile, projName string, cmd *cobra.Command) compose.Compose 17 | 18 | func execAction(action ComposeAction) func(cmd *cobra.Command, args []string) { 19 | return execActionWithPreHook(action, defaultCompose) 20 | } 21 | 22 | func execActionWithPreHook(action ComposeAction, preHook ComposePreHook) func(cmd *cobra.Command, args []string) { 23 | return func(cmd *cobra.Command, args []string) { 24 | composeFile, _ := cmd.Flags().GetString(COMPOSE_FILE_FLAG) 25 | projName, _ := cmd.Flags().GetString(PROJECT_NAME_FLAG) 26 | 27 | compose := preHook(composeFile, projName, cmd) 28 | err := action(compose, cmd, args) 29 | 30 | if err != nil { 31 | cli.Output(nil, err) 32 | } 33 | } 34 | } 35 | 36 | func defaultCompose(composeFile, projName string, cmd *cobra.Command) compose.Compose { 37 | params, _ := cmd.Flags().GetStringSlice(PARAMS_FLAG) 38 | ignore, _ := cmd.Flags().GetBool(IGNORE_MISSING) 39 | 40 | context := &compose.Context{ 41 | ComposeFile: composeFile, 42 | ProjectName: projName, 43 | EnvParams: cli.NameValueSliceToMap(params), 44 | ErrorOnMissingParams: !ignore, 45 | } 46 | return compose.NewCompose(context) 47 | } 48 | 49 | func logs(c compose.Compose, cmd *cobra.Command, args []string) error { 50 | return c.Logs(args...) 51 | } 52 | 53 | func build(c compose.Compose, cmd *cobra.Command, args []string) error { 54 | return c.Build(args...) 55 | } 56 | 57 | func delete(c compose.Compose, cmd *cobra.Command, args []string) error { 58 | return c.Delete(args...) 59 | } 60 | 61 | func ps(c compose.Compose, cmd *cobra.Command, args []string) error { 62 | q, _ := cmd.Flags().GetBool(QUIET_FLAG) 63 | return c.PS(q) 64 | } 65 | 66 | func restart(c compose.Compose, cmd *cobra.Command, args []string) error { 67 | return c.Restart(args...) 68 | } 69 | 70 | func pull(c compose.Compose, cmd *cobra.Command, args []string) error { 71 | return c.Pull(args...) 72 | } 73 | 74 | func start(c compose.Compose, cmd *cobra.Command, args []string) error { 75 | return c.Start(args...) 76 | } 77 | 78 | func stop(c compose.Compose, cmd *cobra.Command, args []string) error { 79 | return c.Stop(args...) 80 | } 81 | 82 | func kill(c compose.Compose, cmd *cobra.Command, args []string) error { 83 | return c.Kill(args...) 84 | } 85 | 86 | func port(c compose.Compose, cmd *cobra.Command, args []string) error { 87 | if len(args) != 2 { 88 | return PortInvalidArgs 89 | } 90 | index, _ := cmd.Flags().GetInt(INDEX_FLAG) 91 | proto, _ := cmd.Flags().GetString(PROTO_FLAG) 92 | 93 | return c.Port(index, proto, args[0], args[1]) 94 | } 95 | 96 | func up(c compose.Compose, cmd *cobra.Command, args []string) error { 97 | return c.Up(args...) 98 | } 99 | -------------------------------------------------------------------------------- /commands/compose/compose_cmds.go: -------------------------------------------------------------------------------- 1 | package compose 2 | 3 | import ( 4 | "github.com/ContainX/depcon/cliconfig" 5 | "github.com/ContainX/depcon/compose" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | const ( 10 | COMPOSE_FILE_FLAG string = "compose-file" 11 | PROJECT_NAME_FLAG string = "name" 12 | PARAMS_FLAG string = "param" 13 | QUIET_FLAG string = "quiet" 14 | INDEX_FLAG string = "index" 15 | PROTO_FLAG string = "protocol" 16 | IGNORE_MISSING string = "ignore" 17 | ) 18 | 19 | var ( 20 | composeCmd = &cobra.Command{ 21 | Use: "compose", 22 | Short: "Docker Compose support with param substition", 23 | Long: `Using libcompose to manage docker compose files with param substition support 24 | 25 | See compose's subcommands for available choices`, 26 | } 27 | 28 | upCmd = &cobra.Command{ 29 | Use: "up [services ...]", 30 | Short: "Create and start containers", 31 | Run: execAction(up), 32 | } 33 | 34 | killCmd = &cobra.Command{ 35 | Use: "kill [services ...]", 36 | Short: "Kill containers", 37 | Run: execAction(kill), 38 | } 39 | 40 | logCmd = &cobra.Command{ 41 | Use: "logs [services ...]", 42 | Short: "View output from containers", 43 | Run: execAction(logs), 44 | } 45 | 46 | rmCmd = &cobra.Command{ 47 | Use: "rm [services ...]", 48 | Short: "Remove stopped containers", 49 | Run: execAction(delete), 50 | } 51 | 52 | buildCmd = &cobra.Command{ 53 | Use: "build [services ...]", 54 | Short: "Build or rebuild services", 55 | Run: execAction(build), 56 | } 57 | 58 | psCmd = &cobra.Command{ 59 | Use: "ps", 60 | Short: "List containers", 61 | Run: execAction(ps), 62 | } 63 | 64 | restartCmd = &cobra.Command{ 65 | Use: "restart [services ...]", 66 | Short: "Restart running containers", 67 | Run: execAction(restart), 68 | } 69 | 70 | pullCmd = &cobra.Command{ 71 | Use: "pull [services ...]", 72 | Short: "Pulls service imagess", 73 | Run: execAction(pull), 74 | } 75 | 76 | startCmd = &cobra.Command{ 77 | Use: "start [services ...]", 78 | Short: "Start services", 79 | Run: execAction(start), 80 | } 81 | 82 | stopCmd = &cobra.Command{ 83 | Use: "stop [services ...]", 84 | Short: "Stops services", 85 | Run: execAction(stop), 86 | } 87 | 88 | portCmd = &cobra.Command{ 89 | Use: "port [service] [private_port]", 90 | Short: "Stops services", 91 | Run: execAction(port), 92 | } 93 | ) 94 | 95 | // Associates the compose service to the given command 96 | func AddComposeToCmd(rc *cobra.Command, c *cliconfig.ConfigFile) { 97 | rc.AddCommand(composeCmd) 98 | } 99 | 100 | func init() { 101 | composeCmd.PersistentFlags().BoolP(IGNORE_MISSING, "i", false, `Ignore missing ${PARAMS} that are declared in app config that could not be resolved 102 | CAUTION: This can be dangerous if some params define versions or other required information.`) 103 | composeCmd.PersistentFlags().StringSliceP(PARAMS_FLAG, "p", nil, `Adds a param(s) that can be used for substitution. 104 | eg. -p MYVAR=value would replace ${MYVAR} with "value" in the compose file. 105 | These take precidence over env vars`) 106 | 107 | psCmd.Flags().BoolP(QUIET_FLAG, "q", false, "Only display IDs") 108 | portCmd.Flags().Int(INDEX_FLAG, 1, "index of the container if there are multiple instances of a service [default: 1]") 109 | portCmd.Flags().String(PROTO_FLAG, "tcp", "tcp or udp [default: tcp]") 110 | 111 | composeCmd.PersistentFlags().String(COMPOSE_FILE_FLAG, "docker-compose.yml", "Docker compose file") 112 | composeCmd.PersistentFlags().String(PROJECT_NAME_FLAG, compose.DEFAULT_PROJECT, "Project name for this composition") 113 | composeCmd.AddCommand(buildCmd, killCmd, logCmd, portCmd, psCmd, upCmd, pullCmd, restartCmd, rmCmd, startCmd, stopCmd, upCmd) 114 | } 115 | -------------------------------------------------------------------------------- /commands/config.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/ContainX/depcon/cliconfig" 7 | "github.com/ContainX/depcon/pkg/cli" 8 | "github.com/ContainX/depcon/utils" 9 | "github.com/spf13/cobra" 10 | "io" 11 | "text/template" 12 | ) 13 | 14 | const ( 15 | T_CONFIG_ENV = ` 16 | {{ "NAME" }} {{ "TYPE" }} {{ "ENDPOINT" }} {{ "AUTH" }} {{ "DEFAULT" }} 17 | {{ range . }}{{ .Name }} {{ .EnvType }} {{ .HostURL }} {{ .Auth | boolToYesNo }} {{ .Default | defaultEnvToStr }} 18 | {{end}}` 19 | 20 | NAME_FLAG = "name" 21 | URL_FLAG = "url" 22 | USER_FLAG = "user" 23 | PASSWORD_FLAG = "pass" 24 | TOKEN_FLAG = "token" 25 | ) 26 | 27 | type ConfigEnvironments struct { 28 | DefaultEnv string 29 | Envs map[string]*cliconfig.ConfigEnvironment 30 | } 31 | 32 | type EnvironmentSummary struct { 33 | Name string 34 | EnvType string 35 | HostURL string 36 | Auth bool 37 | Default bool 38 | } 39 | 40 | var ValidOutputs []string = []string{"json", "yaml", "column"} 41 | var ErrInvalidOutputFormat = errors.New("Invalid Output specified. Must be 'json','yaml' or 'column'") 42 | var ErrInvalidRootOption = errors.New("Invalid chroot option specified. Must be 'true' or 'false'") 43 | 44 | var configCmd = &cobra.Command{ 45 | Use: "config", 46 | Short: "DepCon configuration", 47 | Long: `Manage DepCon configuration (eg. default, list, adding and removing of environments, default output, service rooting) 48 | 49 | See config's subcommands for available choices`, 50 | } 51 | 52 | var configEnvCmd = &cobra.Command{ 53 | Use: "env", 54 | Short: "Configuration environments define remote Marathon and other supported services by a name. ", 55 | Long: `Manage configuration environments (eg. default, list, adding and removing of environments) 56 | 57 | See env's subcommands for available choices`, 58 | } 59 | 60 | var configRemoveCmd = &cobra.Command{ 61 | Use: "delete [name]", 62 | Short: "Remove a defined environment by it's [name]", 63 | Run: func(cmd *cobra.Command, args []string) { 64 | if cli.EvalPrintUsage(Usage(cmd), args, 1) { 65 | return 66 | } 67 | _, err := configFile.GetEnvironment(args[0]) 68 | if err != nil { 69 | cli.Output(nil, err) 70 | } else { 71 | err := configFile.RemoveEnvironment(args[0], false) 72 | if err != nil { 73 | cli.Output(nil, err) 74 | } 75 | } 76 | }, 77 | } 78 | 79 | var configAddCmd = &cobra.Command{ 80 | Use: "add", 81 | Short: "Adds a new environment (cli prompts)", 82 | Run: func(cmd *cobra.Command, args []string) { 83 | configFile.AddEnvironment() 84 | }, 85 | } 86 | 87 | var configAddMarathonCmd = &cobra.Command{ 88 | Use: "add-marathon [name]", 89 | Short: "Adds a new marathon environment using flags", 90 | Long: `Adds a new Marathon environment with given name. Name is a shortname to quickly switch environemnts in depcon. Typical examples are 91 | qa, stage, prod, etc. Name argument only accepts: ^[a-zA-Z0-9_-]*$ 92 | 93 | NOTE: If this is the first environment then chrooting and column output are the default global options`, 94 | Run: func(cmd *cobra.Command, args []string) { 95 | if cli.EvalPrintUsage(Usage(cmd), args, 1) { 96 | return 97 | } 98 | name := args[0] 99 | 100 | if name == "" || !cliconfig.RegExAlphaNumDash.MatchString(name) { 101 | cli.Output(nil, fmt.Errorf("'%s' must contain valid characters within %s\n", name, cliconfig.AlphaNumDash)) 102 | } 103 | 104 | url, _ := cmd.Flags().GetString(URL_FLAG) 105 | user, _ := cmd.Flags().GetString(USER_FLAG) 106 | pass, _ := cmd.Flags().GetString(PASSWORD_FLAG) 107 | token, _ := cmd.Flags().GetString(TOKEN_FLAG) 108 | 109 | if err := cliconfig.ValidateMarathonURL(url); err != nil { 110 | cli.Output(nil, err) 111 | } 112 | 113 | configFile.AddMarathonEnvironment(name, url, user, pass, token) 114 | fmt.Printf("\nEnvironment: %s - was added successfully\n", name) 115 | }, 116 | } 117 | 118 | var configUpdateCmd = &cobra.Command{ 119 | Use: "update [name]", 120 | Short: "Updates an existing environment", 121 | Long: `Every flag is option and only set flags will be updated wit the flag value`, 122 | Run: func(cmd *cobra.Command, args []string) { 123 | if cli.EvalPrintUsage(Usage(cmd), args, 1) { 124 | return 125 | } 126 | 127 | ce, err := configFile.GetEnvironment(args[0]) 128 | if err != nil { 129 | cli.Output(nil, err) 130 | } 131 | 132 | url, _ := cmd.Flags().GetString(URL_FLAG) 133 | user, _ := cmd.Flags().GetString(USER_FLAG) 134 | pass, _ := cmd.Flags().GetString(PASSWORD_FLAG) 135 | token, _ := cmd.Flags().GetString(TOKEN_FLAG) 136 | 137 | if url != "" { 138 | if err := cliconfig.ValidateMarathonURL(url); err != nil { 139 | cli.Output(nil, err) 140 | } 141 | ce.Marathon.HostUrl = url 142 | } 143 | if user != "" { 144 | ce.Marathon.Username = user 145 | } 146 | if pass != "" { 147 | ce.Marathon.Password = pass 148 | } 149 | if token != "" { 150 | ce.Marathon.Token = token 151 | } 152 | 153 | if err := configFile.Save(); err != nil { 154 | cli.Output(nil, err) 155 | } 156 | 157 | fmt.Printf("\nEnvironment: %s - was updated successfully\n", args[0]) 158 | }, 159 | } 160 | 161 | var configListCmd = &cobra.Command{ 162 | Use: "list", 163 | Short: "List current environments", 164 | Run: func(cmd *cobra.Command, args []string) { 165 | ce := &ConfigEnvironments{DefaultEnv: configFile.DefaultEnv, Envs: configFile.Environments} 166 | template := templateFor(T_CONFIG_ENV, ce.toEnvironmentMap()) 167 | cli.Output(template, nil) 168 | }, 169 | } 170 | 171 | var configDefaultCmd = &cobra.Command{ 172 | Use: "default [name]", 173 | Short: "Sets the default environment [name] to use (eg. -e envname can be eliminated when set and using default)", 174 | Run: func(cmd *cobra.Command, args []string) { 175 | if cli.EvalPrintUsage(Usage(cmd), args, 1) { 176 | return 177 | } 178 | err := configFile.SetDefaultEnvironment(args[0]) 179 | if err != nil { 180 | cli.Output(nil, err) 181 | } else { 182 | fmt.Printf("\nDefault environment is now '%s'\n\n", args[0]) 183 | } 184 | }, 185 | } 186 | 187 | var configOutputCmd = &cobra.Command{ 188 | Use: "output [json | column]", 189 | Short: "Sets the default output to use when -o flag is not specified. Values are 'json, 'yaml' or 'column'", 190 | Run: func(cmd *cobra.Command, args []string) { 191 | if cli.EvalPrintUsage(Usage(cmd), args, 1) { 192 | return 193 | } 194 | format := args[0] 195 | 196 | if utils.Contains(ValidOutputs, format) { 197 | configFile.Format = format 198 | configFile.Save() 199 | fmt.Printf("\nDefault cli.Output is now '%s'\n\n", format) 200 | } else { 201 | cli.Output(nil, ErrInvalidOutputFormat) 202 | } 203 | }, 204 | } 205 | 206 | var configRootServiceCmd = &cobra.Command{ 207 | Use: "chroot [true | false]", 208 | Short: "If true DepCon will root the service based on the current configuration environment. (eg. ./depcon mar app would be ./depcon app)", 209 | Run: func(cmd *cobra.Command, args []string) { 210 | if cli.EvalPrintUsage(Usage(cmd), args, 1) { 211 | return 212 | } 213 | chroot := args[0] 214 | if chroot == "true" || chroot == "false" { 215 | rootBool := chroot == "true" 216 | configFile.RootService = rootBool 217 | configFile.Save() 218 | if rootBool { 219 | fmt.Println("\nService rooting is now enabled") 220 | } else { 221 | fmt.Println("\nService rooting is now disabled") 222 | } 223 | } else { 224 | cli.Output(nil, ErrInvalidRootOption) 225 | } 226 | }, 227 | } 228 | 229 | var configRenameCmd = &cobra.Command{ 230 | Use: "rename [oldName] [newName]", 231 | Short: "Renames an environment from specified [oldName] to the [newName]", 232 | Run: func(cmd *cobra.Command, args []string) { 233 | if cli.EvalPrintUsage(Usage(cmd), args, 2) { 234 | return 235 | } 236 | err := configFile.RenameEnvironment(args[0], args[1]) 237 | if err != nil { 238 | cli.Output(nil, err) 239 | } else { 240 | fmt.Printf("\nEnvironment '%s' has been renamed to '%s'\n\n", args[0], args[1]) 241 | } 242 | }, 243 | } 244 | 245 | func init() { 246 | configAddMarathonCmd.Flags().String(URL_FLAG, "http://localhost:8080", "Marathon URL (eg. http://host:port)") 247 | configAddMarathonCmd.Flags().String(USER_FLAG, "", "Optional: username if authentication is enabled") 248 | configAddMarathonCmd.Flags().String(PASSWORD_FLAG, "", "Optional: password if authentication is enabled") 249 | configAddMarathonCmd.Flags().String(TOKEN_FLAG, "", "Optional: token if authorization is enabled") 250 | 251 | configUpdateCmd.Flags().String(URL_FLAG, "", "Marathon URL (eg. http://host:port)") 252 | configUpdateCmd.Flags().String(USER_FLAG, "", "Optional: username if authentication is enabled") 253 | configUpdateCmd.Flags().String(PASSWORD_FLAG, "", "Optional: password if authentication is enabled") 254 | configUpdateCmd.Flags().String(TOKEN_FLAG, "", "Optional: token if authorization is enabled") 255 | 256 | configEnvCmd.AddCommand(configAddCmd, configAddMarathonCmd, configListCmd, configDefaultCmd, configRenameCmd, configUpdateCmd, configRemoveCmd) 257 | configCmd.AddCommand(configEnvCmd, configOutputCmd, configRootServiceCmd) 258 | } 259 | 260 | type ConfigTemplate struct { 261 | cli.FormatData 262 | } 263 | 264 | func templateFor(template string, data interface{}) ConfigTemplate { 265 | return ConfigTemplate{cli.FormatData{Template: template, Data: data, Funcs: buildFuncMap()}} 266 | } 267 | 268 | func (d ConfigTemplate) ToColumns(output io.Writer) error { 269 | return d.FormatData.ToColumns(output) 270 | } 271 | 272 | func (d ConfigTemplate) Data() cli.FormatData { 273 | return d.FormatData 274 | } 275 | 276 | func (e ConfigEnvironments) toEnvironmentMap() []*EnvironmentSummary { 277 | 278 | arr := []*EnvironmentSummary{} 279 | 280 | for k, v := range e.Envs { 281 | var sc cliconfig.ServiceConfig 282 | switch v.EnvironmentType() { 283 | case cliconfig.TypeMarathon: 284 | sc = *v.Marathon 285 | } 286 | arr = append(arr, &EnvironmentSummary{ 287 | Name: k, 288 | EnvType: v.EnvironmentType(), 289 | HostURL: sc.HostUrl, 290 | Auth: sc.Username != "" || sc.Token != "", 291 | Default: k == e.DefaultEnv, 292 | }) 293 | } 294 | return arr 295 | } 296 | 297 | func buildFuncMap() template.FuncMap { 298 | funcMap := template.FuncMap{ 299 | "defaultEnvToStr": defaultEnvToStr, 300 | } 301 | return funcMap 302 | } 303 | 304 | func defaultEnvToStr(b bool) string { 305 | if b { 306 | return "true" 307 | } 308 | return "-" 309 | } 310 | 311 | func Usage(c *cobra.Command) func() error { 312 | 313 | return func() error { 314 | return c.UsageFunc()(c) 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /commands/depcon.go: -------------------------------------------------------------------------------- 1 | // Defines all the CLI command definitions and execution against internal frameworks 2 | package commands 3 | 4 | import ( 5 | "fmt" 6 | "github.com/ContainX/depcon/cliconfig" 7 | "github.com/ContainX/depcon/commands/compose" 8 | "github.com/ContainX/depcon/commands/marathon" 9 | "github.com/ContainX/depcon/pkg/logger" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | "os" 13 | "strings" 14 | ) 15 | 16 | const ( 17 | FlagVerbose = "verbose" 18 | EnvDepconMode = "DEPCON_MODE" 19 | ModeMarathon = "marathon" 20 | EnvMarathonHost = "MARATHON_HOST" 21 | EnvMarathonUser = "MARATHON_USER" 22 | EnvMarathonPass = "MARATHON_PASS" 23 | EnvMarathonToken = "MARATHON_TOKEN" 24 | FlagEnv = "env" 25 | ViperEnv = "env_name" 26 | EnvHelp = `Specifies the Environment name to use (eg. test | prod | etc). This can be omitted if only a single environment has been defined` 27 | DepConHelp = ` 28 | DEPCON (Deploy Containers) 29 | 30 | == Version: %s - Built: %s == 31 | 32 | Provides management and deployment aid across user-defined clusters such as 33 | - Mesos/Marathon 34 | - Kubernetes 35 | - Amazon ECS (EC2 Container Service) 36 | ` 37 | ) 38 | 39 | var ( 40 | configFile *cliconfig.ConfigFile 41 | 42 | // Root command for CLI command hierarchy 43 | rootCmd = &cobra.Command{ 44 | Use: "depcon", 45 | Short: "Manage container clusters and deployments", 46 | PersistentPreRun: configureLogging, 47 | } 48 | 49 | // Default logging levels 50 | logLevels = map[string]logger.LogLevel{ 51 | "depcon": logger.WARNING, 52 | "client": logger.WARNING, 53 | "depcon.deploy.wait": logger.INFO, 54 | "depcon.marathon": logger.WARNING, 55 | "depcon.marshal": logger.WARNING, 56 | "depcon.compose": logger.WARNING, 57 | "depcon.marathon.bg": logger.INFO, 58 | } 59 | 60 | Version string = "" 61 | BuildDate string = "" 62 | ) 63 | 64 | func init() { 65 | logger.InitWithDefaultLogger("depcon") 66 | rootCmd.PersistentFlags().StringP(FlagEnv, "e", "", EnvHelp) 67 | rootCmd.PersistentFlags().Bool(FlagVerbose, false, "Enables debug/verbose logging") 68 | viper.BindPFlag(FlagEnv, rootCmd.PersistentFlags().Lookup(FlagEnv)) 69 | } 70 | 71 | // Main Entry point called by main - responsible for detecting if this is a first run without a config 72 | // to force initial setup 73 | func Execute() { 74 | rootCmd.Long = fmt.Sprintf(DepConHelp, Version, BuildDate) 75 | file, found := cliconfig.HasExistingConfig() 76 | if found { 77 | configFile = file 78 | executeWithExistingConfig() 79 | } else { 80 | if len(os.Getenv(EnvDepconMode)) > 0 && os.Getenv(EnvDepconMode) == ModeMarathon { 81 | configFile = marathonConfigFromEnv() 82 | executeWithExistingConfig() 83 | } else { 84 | if len(os.Args) >= 4 && os.Args[1] == "config" && os.Args[2] == "env" && os.Args[3] == "add-marathon" { 85 | configFile, _ = cliconfig.Load("") 86 | rootCmd.AddCommand(configCmd) 87 | rootCmd.Execute() 88 | return 89 | } 90 | logger.Logger().Errorf("%s file not found. Generating initial configuration", file.Filename()) 91 | configFile = cliconfig.CreateNewConfigFromUserInput() 92 | } 93 | } 94 | } 95 | 96 | func marathonConfigFromEnv() *cliconfig.ConfigFile { 97 | return cliconfig.CreateMemoryMarathonConfig(os.Getenv(EnvMarathonHost), os.Getenv(EnvMarathonUser), os.Getenv(EnvMarathonPass), os.Getenv(EnvMarathonToken)) 98 | } 99 | 100 | func determineEnvironment() string { 101 | envName := findEnvNameFromArgs() 102 | 103 | if envName == "" { 104 | if _, single := configFile.DetermineIfServiceIsRooted(); single { 105 | envName = configFile.GetEnvironments()[0] 106 | } else { 107 | if configFile.DefaultEnv != "" { 108 | envName = configFile.DefaultEnv 109 | } else { 110 | rootCmd.Execute() 111 | logger.Logger().Error("Multiple environments are defined in config. You must execute with -e envname.") 112 | printValidEnvironments() 113 | return "" 114 | } 115 | } 116 | } 117 | return envName 118 | } 119 | 120 | func executeWithExistingConfig() { 121 | envName := determineEnvironment() 122 | if envName == "" { 123 | os.Exit(1) 124 | } 125 | if _, err := configFile.GetEnvironment(envName); err != nil { 126 | logger.Logger().Errorf("'%s' environment could not be found in config (%s)\n\n", envName, configFile.Filename()) 127 | printValidEnvironments() 128 | os.Exit(1) 129 | } else { 130 | viper.Set(ViperEnv, envName) 131 | if configFile.RootService { 132 | marathon.AddJailedMarathonToCmd(rootCmd, configFile) 133 | } else { 134 | marathon.AddMarathonToCmd(rootCmd, configFile) 135 | } 136 | } 137 | compose.AddComposeToCmd(rootCmd, nil) 138 | rootCmd.AddCommand(configCmd) 139 | rootCmd.Execute() 140 | } 141 | 142 | // Profiles the user with a list of current environments found within the config.json based on 143 | // a user error or invalid flags 144 | func printValidEnvironments() { 145 | envs := configFile.GetEnvironments() 146 | fmt.Println("Valid Environments:") 147 | for _, env := range envs { 148 | fmt.Printf("- %s\n", env) 149 | } 150 | fmt.Println("") 151 | } 152 | 153 | func findEnvNameFromArgs() string { 154 | if len(os.Args) < 2 { 155 | return "" 156 | } 157 | f := os.Args[1] 158 | if f == "-e" && len(os.Args) > 2 { 159 | return os.Args[2] 160 | } 161 | if strings.HasPrefix(os.Args[1], "--env=") { 162 | split := strings.Split(os.Args[1], "=") 163 | return split[1] 164 | } 165 | return "" 166 | } 167 | 168 | // Configures the logging levels based on the logLevels map. If --verbose is flagged 169 | // then all categories defined in the map become DEBUG 170 | func configureLogging(cmd *cobra.Command, args []string) { 171 | verbose, _ := cmd.Flags().GetBool(FlagVerbose) 172 | 173 | for category, level := range logLevels { 174 | if verbose { 175 | logger.SetLevel(logger.DEBUG, category) 176 | } else { 177 | logger.SetLevel(level, category) 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /commands/format.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ContainX/depcon/pkg/cli" 6 | "github.com/ContainX/depcon/pkg/encoding" 7 | "github.com/ContainX/depcon/pkg/logger" 8 | "os" 9 | ) 10 | 11 | const ( 12 | FLAG_FORMAT string = "output" 13 | TypeJSON string = "json" 14 | TypeYAML string = "yaml" 15 | TypeColumn string = "column" 16 | ) 17 | 18 | var log = logger.GetLogger("depcon") 19 | 20 | func init() { 21 | cli.Register(&cli.CLIWriter{FormatWriter: PrintFormat, ErrorWriter: PrintError}) 22 | rootCmd.PersistentFlags().StringP(FLAG_FORMAT, "o", "column", "Specifies the output format [column | json | yaml]") 23 | } 24 | 25 | func getFormatType() string { 26 | if rootCmd.PersistentFlags().Changed(FLAG_FORMAT) { 27 | format, err := rootCmd.PersistentFlags().GetString(FLAG_FORMAT) 28 | if err == nil { 29 | return format 30 | } 31 | } 32 | if configFile != nil && configFile.Format != "" { 33 | return configFile.Format 34 | } 35 | return "column" 36 | } 37 | 38 | func PrintError(err error) { 39 | log.Errorf("%v", err.Error()) 40 | os.Exit(1) 41 | } 42 | 43 | func PrintFormat(formatter cli.Formatter) { 44 | switch getFormatType() { 45 | case TypeJSON: 46 | printEncodedType(formatter, encoding.JSON) 47 | case TypeYAML: 48 | printEncodedType(formatter, encoding.YAML) 49 | default: 50 | printColumn(formatter) 51 | } 52 | } 53 | 54 | func printEncodedType(formatter cli.Formatter, encoder encoding.EncoderType) { 55 | e, _ := encoding.NewEncoder(encoder) 56 | str, _ := e.MarshalIndent(formatter.Data().Data) 57 | fmt.Println(str) 58 | } 59 | 60 | func printColumn(formatter cli.Formatter) { 61 | err := formatter.ToColumns(os.Stdout) 62 | if err != nil { 63 | log.Errorf("Error: %s", err.Error()) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /commands/marathon/app_bg_cmds.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "time" 7 | 8 | "github.com/ContainX/depcon/marathon/bluegreen" 9 | "github.com/ContainX/depcon/pkg/cli" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | const ( 14 | INSTANCES_FLAG = "instances" 15 | STEP_DELAY_FLAG = "stepdel" 16 | RESUME_FLAG = "resume" 17 | LB_FLAG = "lb" 18 | LB_TIMEOUT_FLAG = "lb-timeout" 19 | BG_DRYRUN_FLAG = "dry" 20 | ) 21 | 22 | var bgCmd = &cobra.Command{ 23 | Use: "bluegreen [file(.json | .yaml)]", 24 | Short: "Marathon blue/green deployments", 25 | Long: `Blue/Green deployments handled through HAProxy or Marathon-LB 26 | 27 | See bluegreen's subcommands for available choices`, 28 | Run: deployBlueGreenCmd, 29 | } 30 | 31 | func init() { 32 | bgCmd.Flags().String(LB_FLAG, "http://localhost:9090", "HAProxy URL and Stats Port") 33 | bgCmd.Flags().Int(LB_TIMEOUT_FLAG, 300, "HAProxy timeout - default 300 seconds") 34 | bgCmd.Flags().Int(INSTANCES_FLAG, 1, "Initial intances of the app to create") 35 | bgCmd.Flags().Int(STEP_DELAY_FLAG, 6, "Delay (in seconds) to wait between successive deployment steps. ") 36 | bgCmd.Flags().Bool(RESUME_FLAG, true, "Resume from a previous deployment") 37 | bgCmd.Flags().BoolP(IGNORE_MISSING, "i", false, `Ignore missing ${PARAMS} that are declared in app config that could not be resolved 38 | CAUTION: This can be dangerous if some params define versions or other required information.`) 39 | bgCmd.Flags().StringP(ENV_FILE_FLAG, "c", "", `Adds a file with a param(s) that can be used for substitution. 40 | These take precidence over env vars`) 41 | bgCmd.Flags().StringSliceP(PARAMS_FLAG, "p", nil, `Adds a param(s) that can be used for substitution. 42 | eg. -p MYVAR=value would replace ${MYVAR} with "value" in the application file. 43 | These take precidence over env vars`) 44 | bgCmd.Flags().Bool(BG_DRYRUN_FLAG, false, "Dry run (no deployment or scaling)") 45 | 46 | } 47 | 48 | func deployBlueGreenCmd(cmd *cobra.Command, args []string) { 49 | if cli.EvalPrintUsage(Usage(cmd), args, 1) { 50 | return 51 | } 52 | a, err := bgc(cmd).DeployBlueGreenFromFile(args[0]) 53 | if err != nil { 54 | cli.Output(nil, err) 55 | os.Exit(1) 56 | } 57 | cli.Output(templateFor(T_APPLICATION, a), err) 58 | } 59 | 60 | func bgc(c *cobra.Command) bluegreen.BlueGreen { 61 | 62 | paramsFile, _ := c.Flags().GetString(ENV_FILE_FLAG) 63 | params, _ := c.Flags().GetStringSlice(PARAMS_FLAG) 64 | ignore, _ := c.Flags().GetBool(IGNORE_MISSING) 65 | sd, _ := c.Flags().GetInt(STEP_DELAY_FLAG) 66 | lbtimeout, _ := c.Flags().GetInt(LB_TIMEOUT_FLAG) 67 | 68 | // Create Options 69 | opts := bluegreen.NewBlueGreenOptions() 70 | opts.Resume, _ = c.Flags().GetBool(RESUME_FLAG) 71 | opts.LoadBalancer, _ = c.Flags().GetString(LB_FLAG) 72 | opts.InitialInstances, _ = c.Flags().GetInt(INSTANCES_FLAG) 73 | opts.ErrorOnMissingParams = !ignore 74 | opts.StepDelay = time.Duration(sd) * time.Second 75 | opts.ProxyWaitTimeout = time.Duration(lbtimeout) * time.Second 76 | opts.DryRun, _ = c.Flags().GetBool(BG_DRYRUN_FLAG) 77 | 78 | if paramsFile != "" { 79 | envParams, _ := parseParamsFile(paramsFile) 80 | opts.EnvParams = envParams 81 | } else { 82 | opts.EnvParams = make(map[string]string) 83 | } 84 | 85 | if params != nil { 86 | for _, p := range params { 87 | if strings.Contains(p, "=") { 88 | v := strings.Split(p, "=") 89 | opts.EnvParams[v[0]] = v[1] 90 | } 91 | } 92 | } 93 | 94 | return bluegreen.NewBlueGreenClient(client(c), opts) 95 | } 96 | -------------------------------------------------------------------------------- /commands/marathon/app_cmds.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/ContainX/depcon/marathon" 12 | "github.com/ContainX/depcon/pkg/cli" 13 | "github.com/ContainX/depcon/pkg/encoding" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | const ( 18 | HOST_FLAG = "host" 19 | SCALE_FLAG = "scale" 20 | FORMAT_FLAG = "format" 21 | TEMPLATE_CTX_FLAG = "tempctx" 22 | DEFAULT_CTX = "template-context.json" 23 | STOP_DEPLOYS_FLAG = "stop-deploys" 24 | ) 25 | 26 | var appCmd = &cobra.Command{ 27 | Use: "app", 28 | Short: "Marathon application management", 29 | Long: `Manage applications in a marathon cluster (eg. creating, listing, details) 30 | 31 | See app's subcommands for available choices`, 32 | } 33 | 34 | var appCreateCmd = &cobra.Command{ 35 | Use: "create [file(.json | .yaml)]", 36 | Short: "Create a new application with the [file(.json | .yaml)]", 37 | Long: "Creates a new App in the cluster. This is an alias for the 'deploy create' command", 38 | Run: deployAppOrGroup, 39 | } 40 | 41 | var appUpdateCmd = &cobra.Command{ 42 | Use: "update", 43 | Short: "Updates a running application. See subcommands for available choices", 44 | } 45 | 46 | var appUpdateCPUCmd = &cobra.Command{ 47 | Use: "cpu [applicationId] [cpu_shares]", 48 | Short: "Updates [applicationId] to have [cpu_shares]", 49 | Run: updateAppCPU, 50 | } 51 | 52 | var appUpdateMemoryCmd = &cobra.Command{ 53 | Use: "mem [applicationId] [amount]", 54 | Short: "Updates [applicationId] to have [amount] of memory in MB", 55 | Run: updateAppMemory, 56 | } 57 | 58 | var appListCmd = &cobra.Command{ 59 | Use: "list (optional filtering - label=mylabel | id=/services | cmd=java ...)", 60 | Short: "List all applications", 61 | Run: func(cmd *cobra.Command, args []string) { 62 | filter := "" 63 | if len(args) > 0 { 64 | filter = args[0] 65 | } 66 | v, e := client(cmd).ListApplicationsWithFilters(filter) 67 | 68 | cli.Output(templateFor(templateFormat(T_APPLICATIONS, cmd), v), e) 69 | }, 70 | } 71 | 72 | var appGetCmd = &cobra.Command{ 73 | Use: "get [applicationId]", 74 | Short: "Gets an application details by Id", 75 | Long: `Retrieves the specified [appliationId] application`, 76 | Run: func(cmd *cobra.Command, args []string) { 77 | if cli.EvalPrintUsage(Usage(cmd), args, 1) { 78 | return 79 | } 80 | v, e := client(cmd).GetApplication(args[0]) 81 | cli.Output(templateFor(templateFormat(T_APPLICATION, cmd), v), e) 82 | }, 83 | } 84 | 85 | var appVersionsCmd = &cobra.Command{ 86 | Use: "versions [applicationId]", 87 | Short: "Gets the versions that have been deployed with Marathon for [applicationId]", 88 | Long: `Retrieves the list of versions for [appliationId] application`, 89 | Run: func(cmd *cobra.Command, args []string) { 90 | if cli.EvalPrintUsage(Usage(cmd), args, 1) { 91 | return 92 | } 93 | v, e := client(cmd).ListVersions(args[0]) 94 | cli.Output(templateFor(T_VERSIONS, v), e) 95 | }, 96 | } 97 | 98 | var appDestroyCmd = &cobra.Command{ 99 | Use: "destroy [applicationId]", 100 | Short: "Remove an application [applicationId] and all of it's instances", 101 | Long: `Removes the specified [appliationId] application`, 102 | Run: destroyApp, 103 | } 104 | 105 | var appRestartCmd = &cobra.Command{ 106 | Use: "restart [applicationId]", 107 | Short: "Restarts an application by Id", 108 | Long: `Restarts the specified [appliationId] application`, 109 | Run: restartApp, 110 | } 111 | 112 | var appScaleCmd = &cobra.Command{ 113 | Use: "scale [applicationId] [instances]", 114 | Short: "Scales [appliationId] to total [instances]", 115 | Run: scaleApp, 116 | } 117 | 118 | var appPauseCmd = &cobra.Command{ 119 | Use: "pause [applicationId]", 120 | Short: "Suspends the [applicationId", 121 | Run: pauseApp, 122 | } 123 | 124 | var appRollbackCmd = &cobra.Command{ 125 | Use: "rollback [applicationId] (version)", 126 | Short: "Rolls an [appliationId] to a specific (version : optional)", 127 | Long: `Rolls an [appliationId] to a specific [version] - See: "depcon app versions" for a list of versions`, 128 | Run: rollbackAppVersion, 129 | } 130 | 131 | var appConvertFileCmd = &cobra.Command{ 132 | Use: "convert [from.(json | yaml)] [to.(json | yaml)]", 133 | Short: "Utilty to convert an application file from json to yaml or yaml to json.", 134 | Run: convertFile, 135 | } 136 | 137 | func init() { 138 | appUpdateCmd.AddCommand(appUpdateCPUCmd, appUpdateMemoryCmd) 139 | appCmd.AddCommand(appListCmd, appGetCmd, logCmd, appCreateCmd, appUpdateCmd, appDestroyCmd, appRollbackCmd, bgCmd, appRestartCmd, appScaleCmd, appPauseCmd, appVersionsCmd, appConvertFileCmd) 140 | 141 | // Create Flags 142 | addDeployCreateFlags(appCreateCmd) 143 | 144 | appListCmd.Flags().String(FORMAT_FLAG, "", "Custom output format. Example: '{{range .Apps}}{{ .Container.Docker.Image }}{{end}}'") 145 | appGetCmd.Flags().String(FORMAT_FLAG, "", "Custom output format. Example: '{{ .ID }}'") 146 | applyCommonAppFlags(appUpdateCPUCmd, appUpdateMemoryCmd, appRollbackCmd, appDestroyCmd, appRestartCmd, appScaleCmd, appPauseCmd) 147 | } 148 | 149 | func exitWithError(err error) { 150 | cli.Output(nil, err) 151 | os.Exit(1) 152 | } 153 | 154 | func parseParamsFile(filename string) (map[string]string, error) { 155 | paramsFile, err := os.Open(filename) 156 | if err != nil { 157 | return nil, err 158 | } 159 | bytes, err := ioutil.ReadAll(paramsFile) 160 | if err != nil { 161 | return nil, err 162 | } 163 | data := string(bytes) 164 | params := strings.Split(data, "\n") 165 | 166 | envmap := make(map[string]string) 167 | for _, p := range params { 168 | if strings.Contains(p, "=") { 169 | v := strings.Split(p, "=") 170 | envmap[v[0]] = v[1] 171 | } 172 | } 173 | return envmap, nil 174 | } 175 | 176 | func restartApp(cmd *cobra.Command, args []string) { 177 | if cli.EvalPrintUsage(Usage(cmd), args, 1) { 178 | os.Exit(1) 179 | } 180 | 181 | force, _ := cmd.Flags().GetBool(FORCE_FLAG) 182 | 183 | v, e := client(cmd).RestartApplication(args[0], force) 184 | cli.Output(templateFor(T_DEPLOYMENT_ID, v), e) 185 | waitForDeploymentIfFlagged(cmd, v.DeploymentID) 186 | } 187 | 188 | func destroyApp(cmd *cobra.Command, args []string) { 189 | if cli.EvalPrintUsage(Usage(cmd), args, 1) { 190 | os.Exit(1) 191 | } 192 | 193 | v, e := client(cmd).DestroyApplication(args[0]) 194 | cli.Output(templateFor(T_DEPLOYMENT_ID, v), e) 195 | waitForDeploymentIfFlagged(cmd, v.DeploymentID) 196 | } 197 | 198 | func scaleApp(cmd *cobra.Command, args []string) { 199 | if cli.EvalPrintUsage(Usage(cmd), args, 2) { 200 | os.Exit(1) 201 | } 202 | 203 | instances, err := strconv.Atoi(args[1]) 204 | if err != nil { 205 | cli.Output(nil, err) 206 | os.Exit(1) 207 | } 208 | v, e := client(cmd).ScaleApplication(args[0], instances) 209 | cli.Output(templateFor(T_DEPLOYMENT_ID, v), e) 210 | waitForDeploymentIfFlagged(cmd, v.DeploymentID) 211 | } 212 | 213 | func pauseApp(cmd *cobra.Command, args []string) { 214 | if cli.EvalPrintUsage(Usage(cmd), args, 1) { 215 | os.Exit(1) 216 | } 217 | 218 | v, e := client(cmd).PauseApplication(args[0]) 219 | cli.Output(templateFor(T_DEPLOYMENT_ID, v), e) 220 | waitForDeploymentIfFlagged(cmd, v.DeploymentID) 221 | } 222 | 223 | func updateAppCPU(cmd *cobra.Command, args []string) { 224 | if cli.EvalPrintUsage(Usage(cmd), args, 2) { 225 | os.Exit(1) 226 | } 227 | 228 | wait, _ := cmd.Flags().GetBool(WAIT_FLAG) 229 | cpu, err := strconv.ParseFloat(args[1], 64) 230 | 231 | if err != nil { 232 | cli.Output(nil, err) 233 | os.Exit(1) 234 | } 235 | update := marathon.NewApplication(args[0]).CPU(cpu) 236 | v, e := client(cmd).UpdateApplication(update, wait, false) 237 | cli.Output(templateFor(T_APPLICATION, v), e) 238 | } 239 | 240 | func updateAppMemory(cmd *cobra.Command, args []string) { 241 | if cli.EvalPrintUsage(Usage(cmd), args, 2) { 242 | os.Exit(1) 243 | } 244 | 245 | wait, _ := cmd.Flags().GetBool(WAIT_FLAG) 246 | mem, err := strconv.ParseFloat(args[1], 64) 247 | 248 | if err != nil { 249 | cli.Output(nil, err) 250 | os.Exit(1) 251 | } 252 | update := marathon.NewApplication(args[0]).Memory(mem) 253 | v, e := client(cmd).UpdateApplication(update, wait, false) 254 | cli.Output(templateFor(T_APPLICATION, v), e) 255 | } 256 | 257 | func rollbackAppVersion(cmd *cobra.Command, args []string) { 258 | if cli.EvalPrintUsage(Usage(cmd), args, 1) { 259 | os.Exit(1) 260 | } 261 | 262 | wait, _ := cmd.Flags().GetBool(WAIT_FLAG) 263 | version := "" 264 | 265 | if len(args) > 1 { 266 | version = args[1] 267 | } else { 268 | versions, e := client(cmd).ListVersions(args[0]) 269 | if e == nil && len(versions.Versions) > 1 { 270 | version = versions.Versions[1] 271 | } 272 | } 273 | update := marathon.NewApplication(args[0]).RollbackVersion(version) 274 | v, e := client(cmd).UpdateApplication(update, wait, false) 275 | cli.Output(templateFor(T_APPLICATION, v), e) 276 | } 277 | 278 | func convertFile(cmd *cobra.Command, args []string) { 279 | if cli.EvalPrintUsage(Usage(cmd), args, 2) { 280 | os.Exit(1) 281 | } 282 | if err := encoding.ConvertFile(args[0], args[1], &marathon.Application{}); err != nil { 283 | cli.Output(nil, err) 284 | os.Exit(1) 285 | } 286 | fmt.Printf("Source file %s has been re-written into new format in %s\n\n", args[0], args[1]) 287 | } 288 | 289 | func waitForDeploymentIfFlagged(cmd *cobra.Command, depId string) { 290 | if found, err := cmd.Flags().GetBool(WAIT_FLAG); err == nil && found { 291 | client(cmd).WaitForDeployment(depId, time.Duration(80)*time.Second) 292 | } 293 | } 294 | 295 | func applyCommonAppFlags(cmd ...*cobra.Command) { 296 | for _, c := range cmd { 297 | c.Flags().BoolP(WAIT_FLAG, "w", false, "Wait for application to become healthy") 298 | c.Flags().DurationP(TIMEOUT_FLAG, "t", time.Duration(0), "Max duration to wait for application health (ex. 90s | 2m). See docs for ordering") 299 | } 300 | } 301 | 302 | func templateFormat(template string, cmd *cobra.Command) string { 303 | t := template 304 | tv, _ := cmd.Flags().GetString(FORMAT_FLAG) 305 | if len(tv) > 0 { 306 | t = tv 307 | } 308 | return t 309 | } 310 | -------------------------------------------------------------------------------- /commands/marathon/app_cmds_test.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | l "log" 5 | "testing" 6 | ) 7 | 8 | func TestParseParamFile(t *testing.T) { 9 | envParams, _ := parseParamsFile("resources/test.env") 10 | el, ok := envParams["APP1_VERSION"] 11 | if !ok && el != "3" { 12 | l.Printf("Actual envParams %v", envParams) 13 | l.Panic("Expected envFile parsed correctly") 14 | } 15 | el, ok = envParams["APP2_VERSION"] 16 | if !ok && el != "345" { 17 | l.Printf("Actual envParams %v", envParams) 18 | l.Panic("Expected envFile parsed correctly") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /commands/marathon/app_log_cmds.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ContainX/depcon/pkg/cli" 6 | "github.com/ContainX/depcon/pkg/logger" 7 | ml "github.com/ContainX/go-mesoslog/mesoslog" 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | "net/url" 11 | "strings" 12 | ) 13 | 14 | const ( 15 | STDERR_FLAG = "stderr" 16 | FOLLOW_FLAG = "follow" 17 | POLL_FLAG = "poll" 18 | COMPLETED_FLAG = "completed" 19 | LATEST_FLAG = "latest" 20 | ) 21 | 22 | var logCmd = &cobra.Command{ 23 | Use: "log [appId]", 24 | Short: "Log or Tail Mesos application logs", 25 | Long: "Log or Tail Mesos application logs", 26 | Run: showLogCmd, 27 | } 28 | 29 | var log = logger.GetLogger("depcon") 30 | 31 | func init() { 32 | logCmd.Flags().BoolP(STDERR_FLAG, "s", false, "Show StdErr vs default StdOut log") 33 | logCmd.Flags().BoolP(FOLLOW_FLAG, "f", false, "Tail/Follow log") 34 | logCmd.Flags().IntP(POLL_FLAG, "p", 5, "Log poll time (duration) in seconds") 35 | logCmd.Flags().BoolP(COMPLETED_FLAG, "c", false, "Use completed tasks (default: running tasks)") 36 | logCmd.Flags().BoolP(LATEST_FLAG, "l", false, "Use latest task (single) (default: all tasks)") 37 | 38 | } 39 | 40 | func showLogCmd(cmd *cobra.Command, args []string) { 41 | if cli.EvalPrintUsage(Usage(cmd), args, 1) { 42 | return 43 | } 44 | 45 | host := getMesosHost() 46 | logType := ml.STDOUT 47 | completedTasks, _ := cmd.Flags().GetBool(COMPLETED_FLAG) 48 | latestTasks, _ := cmd.Flags().GetBool(LATEST_FLAG) 49 | 50 | if stderr, _ := cmd.Flags().GetBool(STDERR_FLAG); stderr { 51 | logType = ml.STDERR 52 | } 53 | 54 | c, _ := ml.NewMesosClientWithOptions(host, 5050, &ml.MesosClientOptions{ 55 | SearchCompletedTasks: completedTasks, 56 | ShowLatestOnly: latestTasks, 57 | }) 58 | appId := getMesosAppIdentifier(cmd, c, args[0]) 59 | 60 | if follow, _ := cmd.Flags().GetBool(FOLLOW_FLAG); follow { 61 | duration, _ := cmd.Flags().GetInt(POLL_FLAG) 62 | if duration < 1 { 63 | duration = 5 64 | 65 | } 66 | if err := c.TailLog(appId, logType, duration); err != nil { 67 | log.Fatal(err) 68 | } 69 | return 70 | } 71 | 72 | logs, err := c.GetLog(appId, logType, "") 73 | if err != nil { 74 | log.Fatal(err) 75 | } 76 | 77 | showBreaks := len(logs) > 1 78 | for _, log := range logs { 79 | if showBreaks { 80 | fmt.Printf("\n::: [ %s - Logs For: %s ] ::: \n", args[0], log.TaskID) 81 | } 82 | fmt.Printf("%s\n", log.Log) 83 | if showBreaks { 84 | fmt.Printf("\n!!! [ %s - End Logs For: %s ] !!! \n", args[0], log.TaskID) 85 | } 86 | } 87 | } 88 | 89 | func getMesosAppIdentifier(cmd *cobra.Command, c *ml.MesosClient, appId string) string { 90 | return c.GetAppNameForPath(appId) 91 | } 92 | 93 | func getMesosHost() string { 94 | envName := viper.GetString("env_name") 95 | mc := *configFile.Environments[envName].Marathon 96 | 97 | u, err := url.Parse(mc.HostUrl) 98 | if err != nil { 99 | log.Fatal(err) 100 | } 101 | if strings.Index(u.Host, ":") > 0 { 102 | return strings.Split(u.Host, ":")[0] 103 | } 104 | return u.Host 105 | } 106 | -------------------------------------------------------------------------------- /commands/marathon/deploy_cmds.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "github.com/ContainX/depcon/marathon" 8 | "github.com/ContainX/depcon/pkg/cli" 9 | "github.com/ContainX/depcon/pkg/encoding" 10 | "github.com/spf13/cobra" 11 | "os" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | var deployCmd = &cobra.Command{ 17 | Use: "deploy", 18 | Short: "Marathon deployment management", 19 | Long: `Manage deployments in a marathon cluster (eg. creating, listing, monitoring) 20 | 21 | See deploy's subcommands for available choices`, 22 | } 23 | 24 | var deployListCmd = &cobra.Command{ 25 | Use: "list", 26 | Short: "List all deployments", 27 | Run: func(cmd *cobra.Command, args []string) { 28 | v, e := client(cmd).ListDeployments() 29 | cli.Output(templateFor(T_DEPLOYMENTS, v), e) 30 | }, 31 | } 32 | 33 | var deployDeleteCmd = &cobra.Command{ 34 | Use: "delete [deploymentId]", 35 | Short: "Delete a deployment by [deploymentID]", 36 | Run: func(cmd *cobra.Command, args []string) { 37 | if cli.EvalPrintUsage(Usage(cmd), args, 1) { 38 | return 39 | } 40 | force, _ := cmd.Flags().GetBool(FORCE_FLAG) 41 | 42 | v, e := client(cmd).DeleteDeployment(args[0], force) 43 | cli.Output(templateFor(T_DEPLOYMENT_ID, v), e) 44 | }, 45 | } 46 | 47 | var deleteIfDeployingCmd = &cobra.Command{ 48 | Use: "cancel-app [appid]", 49 | Short: "Conditional Match: Delete a deployment based on the specified [appid]", 50 | Run: func(cmd *cobra.Command, args []string) { 51 | if cli.EvalPrintUsage(Usage(cmd), args, 1) { 52 | return 53 | } 54 | v, e := client(cmd).CancelAppDeployment(args[0], false) 55 | if v != nil || e != nil { 56 | cli.Output(templateFor(T_DEPLOYMENT_ID, v), e) 57 | } else { 58 | if e != nil { 59 | cli.Output(templateFor(T_DEPLOYMENT_ID, v), e) 60 | } 61 | } 62 | 63 | }, 64 | } 65 | 66 | var deployCreateCmd = &cobra.Command{ 67 | Use: "create", 68 | Short: "Creates a new app or group by introspecting the incoming descriptor. Useful for deployment pipelines", 69 | Long: ` 70 | Creates a new app or group by introspecting the incoming descriptor. Useful for deployment pipelines. 71 | 72 | This command has a small penalty of unmarshalling the descriptor twice. One for introspection and 73 | the other for delegation to the origin (app or group) 74 | `, 75 | 76 | Run: deployAppOrGroup, 77 | } 78 | 79 | func init() { 80 | addDeployCreateFlags(deployCreateCmd) 81 | deployDeleteCmd.Flags().BoolP(FORCE_FLAG, "f", false, "If set to true, then the deployment is still canceled but no rollback deployment is created.") 82 | deployCmd.AddCommand(deployCreateCmd, deployListCmd, deployDeleteCmd, deleteIfDeployingCmd) 83 | } 84 | 85 | func addDeployCreateFlags(cmd *cobra.Command) { 86 | cmd.Flags().BoolP(WAIT_FLAG, "w", false, "Wait for deployment to become healthy") 87 | cmd.Flags().String(TEMPLATE_CTX_FLAG, DEFAULT_CTX, "Provides data per environment in JSON form to do a first pass parse of descriptor as template") 88 | cmd.Flags().BoolP(FORCE_FLAG, "f", false, "Force deployment (updates application if it already exists)") 89 | cmd.Flags().Bool(STOP_DEPLOYS_FLAG, false, "Stop an existing deployment for this app (if exists) and use this revision") 90 | cmd.Flags().BoolP(IGNORE_MISSING, "i", false, `Ignore missing ${PARAMS} that are declared in app config that could not be resolved 91 | CAUTION: This can be dangerous if some params define versions or other required information.`) 92 | cmd.Flags().StringP(ENV_FILE_FLAG, "c", "", `Adds a file with a param(s) that can be used for substitution. 93 | These take precidence over env vars`) 94 | cmd.Flags().StringSliceP(PARAMS_FLAG, "p", nil, `Adds a param(s) that can be used for substitution. 95 | eg. -p MYVAR=value would replace ${MYVAR} with "value" in the application file. 96 | These take precidence over env vars`) 97 | 98 | cmd.Flags().Bool(DRYRUN_FLAG, false, "Preview the parsed template - don't actually deploy") 99 | 100 | cmd.Flags().DurationP(TIMEOUT_FLAG, "t", time.Duration(0), "Max duration to wait for application health (ex. 90s | 2m). See docs for ordering") 101 | 102 | } 103 | 104 | func deployAppOrGroup(cmd *cobra.Command, args []string) { 105 | 106 | if cli.EvalPrintUsage(Usage(cmd), args, 1) { 107 | return 108 | } 109 | 110 | filename := args[0] 111 | wait, _ := cmd.Flags().GetBool(WAIT_FLAG) 112 | force, _ := cmd.Flags().GetBool(FORCE_FLAG) 113 | paramsFile, _ := cmd.Flags().GetString(ENV_FILE_FLAG) 114 | params, _ := cmd.Flags().GetStringSlice(PARAMS_FLAG) 115 | ignore, _ := cmd.Flags().GetBool(IGNORE_MISSING) 116 | stop_deploy, _ := cmd.Flags().GetBool(STOP_DEPLOYS_FLAG) 117 | tempctx, _ := cmd.Flags().GetString(TEMPLATE_CTX_FLAG) 118 | dryrun, _ := cmd.Flags().GetBool(DRYRUN_FLAG) 119 | options := &marathon.CreateOptions{Wait: wait, Force: force, ErrorOnMissingParams: !ignore, StopDeploy: stop_deploy, DryRun: dryrun} 120 | 121 | descriptor := ParseDescriptor(tempctx, filename, "") 122 | et, err := encoding.NewEncoderFromFileExt(filename) 123 | if err != nil { 124 | exitWithError(err) 125 | } 126 | 127 | ag := &marathon.AppOrGroup{} 128 | if err := et.UnMarshalStr(descriptor, ag); err != nil { 129 | exitWithError(err) 130 | } 131 | 132 | if paramsFile != "" { 133 | envParams, _ := parseParamsFile(paramsFile) 134 | options.EnvParams = envParams 135 | } else { 136 | options.EnvParams = make(map[string]string) 137 | } 138 | 139 | if params != nil { 140 | for _, p := range params { 141 | if strings.Contains(p, "=") { 142 | v := strings.Split(p, "=") 143 | options.EnvParams[v[0]] = v[1] 144 | } 145 | } 146 | } 147 | 148 | if ag.IsApplication() { 149 | result, e := client(cmd).CreateApplicationFromString(filename, descriptor, options) 150 | outputDeployment(result, e) 151 | cli.Output(templateFor(T_APPLICATION, result), e) 152 | } else { 153 | result, e := client(cmd).CreateGroupFromString(filename, descriptor, options) 154 | outputDeployment(result, e) 155 | 156 | if e != nil { 157 | cli.Output(nil, e) 158 | } 159 | 160 | arr := flattenGroup(result, []*marathon.Group{}) 161 | cli.Output(templateFor(T_GROUPS, arr), e) 162 | } 163 | } 164 | 165 | func outputDeployment(result interface{}, e error) { 166 | if e != nil && e == marathon.ErrorAppExists { 167 | exitWithError(errors.New(fmt.Sprintf("%s, consider using the --force flag to update when an application exists", e.Error()))) 168 | } 169 | 170 | if result == nil { 171 | if e != nil { 172 | exitWithError(e) 173 | } 174 | os.Exit(1) 175 | } 176 | } 177 | 178 | func ParseDescriptor(tempctx, filename, rootDir string) string { 179 | b := &bytes.Buffer{} 180 | 181 | r, err := LoadTemplateContext(tempctx) 182 | if err != nil { 183 | exitWithError(err) 184 | } 185 | 186 | if err := r.Transform(b, filename, rootDir); err != nil { 187 | exitWithError(err) 188 | } 189 | return b.String() 190 | } 191 | -------------------------------------------------------------------------------- /commands/marathon/event_cmds.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var eventCmd = &cobra.Command{ 8 | Use: "event", 9 | Short: "Marathon event streaming and subscription management", 10 | Long: `Manage subscriptions and listen to live streaming 11 | 12 | See events's subcommands for available choices`, 13 | } 14 | 15 | func init() { 16 | } 17 | -------------------------------------------------------------------------------- /commands/marathon/group_cmds.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ContainX/depcon/marathon" 6 | "github.com/ContainX/depcon/pkg/cli" 7 | "github.com/ContainX/depcon/pkg/encoding" 8 | "github.com/spf13/cobra" 9 | "os" 10 | ) 11 | 12 | var groupCmd = &cobra.Command{ 13 | Use: "group", 14 | Short: "Marathon application groups", 15 | Long: `Manage application groups in a marathon cluster (eg. creating, listing, details) 16 | 17 | See group's subcommands for available choices`, 18 | } 19 | 20 | var groupListCmd = &cobra.Command{ 21 | Use: "list", 22 | Short: "List all applications", 23 | Run: listGroups, 24 | } 25 | 26 | var groupGetCmd = &cobra.Command{ 27 | Use: "get [groupId]", 28 | Short: "Get a group details by [groupId]", 29 | Run: getGroup, 30 | } 31 | 32 | var groupDestroyCmd = &cobra.Command{ 33 | Use: "destroy [groupId]", 34 | Short: "Removes a group by [groupId] and all of it's resources (nested groups and app instances)", 35 | Run: destroyGroup, 36 | } 37 | 38 | var groupCreateCmd = &cobra.Command{ 39 | Use: "create [file(.json | .yaml)]", 40 | Short: "Create a new Group with the [file(.json | .yaml)]", 41 | Long: "Creates a new Group in the cluster. This is an alias for the 'deploy create' command", 42 | Run: deployAppOrGroup, 43 | } 44 | 45 | var groupConvertFileCmd = &cobra.Command{ 46 | Use: "convert [from.(json | yaml)] [to.(json | yaml)]", 47 | Short: "Utilty to convert an group file from json to yaml or yaml to json.", 48 | Run: convertGroupFile, 49 | } 50 | 51 | func init() { 52 | groupCmd.AddCommand(groupListCmd, groupGetCmd, groupCreateCmd, groupDestroyCmd, groupConvertFileCmd) 53 | 54 | // Destroy Flags 55 | groupDestroyCmd.Flags().BoolP(WAIT_FLAG, "w", false, "Wait for destroy to complete") 56 | // Create Flags 57 | addDeployCreateFlags(groupCreateCmd) 58 | } 59 | 60 | func listGroups(cmd *cobra.Command, args []string) { 61 | v, e := client(cmd).ListGroups() 62 | arr := []*marathon.Group{} 63 | 64 | for _, group := range v.Groups { 65 | arr = flattenGroup(group, arr) 66 | } 67 | 68 | cli.Output(templateFor(T_GROUPS, arr), e) 69 | } 70 | 71 | func getGroup(cmd *cobra.Command, args []string) { 72 | if cli.EvalPrintUsage(Usage(cmd), args, 1) { 73 | return 74 | } 75 | 76 | v, e := client(cmd).GetGroup(args[0]) 77 | arr := flattenGroup(v, []*marathon.Group{}) 78 | cli.Output(templateFor(T_GROUPS, arr), e) 79 | } 80 | 81 | func destroyGroup(cmd *cobra.Command, args []string) { 82 | if cli.EvalPrintUsage(Usage(cmd), args, 1) { 83 | return 84 | } 85 | v, e := client(cmd).DestroyGroup(args[0]) 86 | cli.Output(templateFor(T_DEPLOYMENT_ID, v), e) 87 | } 88 | 89 | func flattenGroup(g *marathon.Group, arr []*marathon.Group) []*marathon.Group { 90 | arr = append(arr, g) 91 | for _, cg := range g.Groups { 92 | arr = flattenGroup(cg, arr) 93 | } 94 | return arr 95 | } 96 | 97 | func convertGroupFile(cmd *cobra.Command, args []string) { 98 | if cli.EvalPrintUsage(Usage(cmd), args, 2) { 99 | os.Exit(1) 100 | } 101 | if err := encoding.ConvertFile(args[0], args[1], &marathon.Groups{}); err != nil { 102 | cli.Output(nil, err) 103 | os.Exit(1) 104 | } 105 | fmt.Printf("Source file %s has been re-written into new format in %s\n\n", args[0], args[1]) 106 | } 107 | -------------------------------------------------------------------------------- /commands/marathon/marathon_cmds.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | "github.com/ContainX/depcon/cliconfig" 5 | "github.com/ContainX/depcon/marathon" 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | const ( 11 | WAIT_FLAG string = "wait" 12 | TIMEOUT_FLAG string = "wait-timeout" 13 | FORCE_FLAG string = "force" 14 | DETAIL_FLAG string = "detail" 15 | PARAMS_FLAG string = "param" 16 | ENV_FILE_FLAG string = "env-file" 17 | IGNORE_MISSING string = "ignore" 18 | INSECURE_FLAG string = "insecure" 19 | ENV_NAME string = "env_name" 20 | DRYRUN_FLAG string = "dry-run" 21 | ) 22 | 23 | var ( 24 | marathonCmd = &cobra.Command{ 25 | Use: "mar", 26 | Short: "Manage apache marathon services", 27 | Long: `Manage apache marathon services (eg. apps, deployments, tasks) 28 | 29 | See marathon's subcommands for available choices`, 30 | } 31 | marathonClient marathon.Marathon 32 | configFile *cliconfig.ConfigFile 33 | ) 34 | 35 | // Associates the marathon service to the given command 36 | func AddMarathonToCmd(rc *cobra.Command, c *cliconfig.ConfigFile) { 37 | configFile = c 38 | associateServiceCommands(marathonCmd) 39 | rc.AddCommand(marathonCmd) 40 | } 41 | 42 | func AddToMarathonCommand(child *cobra.Command) { 43 | marathonCmd.AddCommand(child) 44 | } 45 | 46 | // Jails (chroots) marathon by including only it's sub commands 47 | // when we only have a single environment declared and already know the cluster type 48 | func AddJailedMarathonToCmd(rc *cobra.Command, c *cliconfig.ConfigFile) { 49 | configFile = c 50 | associateServiceCommands(rc) 51 | } 52 | 53 | // Associates all marathon service commands to specified parent 54 | func associateServiceCommands(parent *cobra.Command) { 55 | parent.PersistentFlags().Bool(INSECURE_FLAG, false, "Skips Insecure TLS/HTTPS Certificate checks") 56 | viper.BindPFlag(INSECURE_FLAG, parent.PersistentFlags().Lookup(INSECURE_FLAG)) 57 | 58 | parent.AddCommand(appCmd, groupCmd, deployCmd, taskCmd, eventCmd, serverCmd) 59 | } 60 | 61 | func client(c *cobra.Command) marathon.Marathon { 62 | if marathonClient == nil { 63 | envName := viper.GetString(ENV_NAME) 64 | insecure := viper.GetBool(INSECURE_FLAG) 65 | mc := *configFile.Environments[envName].Marathon 66 | opts := &marathon.MarathonOptions{} 67 | if timeout, err := c.Flags().GetDuration(TIMEOUT_FLAG); err == nil { 68 | opts.WaitTimeout = timeout 69 | } 70 | opts.TLSAllowInsecure = insecure 71 | 72 | marathonClient = marathon.NewMarathonClientWithOpts(mc.HostUrl, mc.Username, mc.Password, mc.Token, opts) 73 | 74 | } 75 | return marathonClient 76 | } 77 | 78 | func Usage(c *cobra.Command) func() error { 79 | 80 | return func() error { 81 | return c.UsageFunc()(c) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /commands/marathon/resources/test.env: -------------------------------------------------------------------------------- 1 | APP1_VERSION=3 2 | APP2_VERSION=5 3 | NODE_EXPORTER_VERSION=f9e8e0fe04a3 -------------------------------------------------------------------------------- /commands/marathon/resources/testcontext.json: -------------------------------------------------------------------------------- 1 | { 2 | "environments": { 3 | "-": { 4 | "apps": { 5 | "appa": { 6 | "mem": 200, 7 | "instances": 1, 8 | "cpus": 0.1 9 | }, 10 | "appb": { 11 | "mem": 850, 12 | "instances": 1 13 | } 14 | } 15 | }, 16 | "prod": { 17 | "apps": { 18 | "appa": { 19 | "mem": 300, 20 | "instances": 3 21 | } 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /commands/marathon/server_cmds.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | "github.com/ContainX/depcon/pkg/cli" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | var serverCmd = &cobra.Command{ 9 | Use: "server", 10 | Short: "Marathon server information", 11 | Long: `View Marathon server information and current leader 12 | 13 | See server's subcommands for available choices`, 14 | } 15 | 16 | var serverInfoCmd = &cobra.Command{ 17 | Use: "info", 18 | Short: "Get info about the Marathon Instance", 19 | Run: func(cmd *cobra.Command, args []string) { 20 | v, e := client(cmd).GetMarathonInfo() 21 | cli.Output(templateFor(T_MARATHON_INFO, v), e) 22 | }, 23 | } 24 | 25 | var serverLeaderCmd = &cobra.Command{ 26 | Use: "leader", 27 | Short: "Marathon leader management", 28 | } 29 | 30 | var serverLeaderGetCmd = &cobra.Command{ 31 | Use: "get", 32 | Short: "Show the current leader", 33 | Run: func(cmd *cobra.Command, args []string) { 34 | v, e := client(cmd).GetCurrentLeader() 35 | cli.Output(templateFor(T_LEADER_INFO, v), e) 36 | }, 37 | } 38 | 39 | var serverPingCmd = &cobra.Command{ 40 | Use: "ping", 41 | Short: "Ping the current marathon host", 42 | Run: func(cmd *cobra.Command, args []string) { 43 | v, e := client(cmd).Ping() 44 | cli.Output(templateFor(T_PING, v), e) 45 | }, 46 | } 47 | 48 | var serverLeaderAbdicateCmd = &cobra.Command{ 49 | Use: "abdicate", 50 | Short: "Force the current leader to relinquish control (elect a new leader)", 51 | Run: func(cmd *cobra.Command, args []string) { 52 | v, e := client(cmd).AbdicateLeader() 53 | cli.Output(templateFor(T_MESSAGE, v), e) 54 | }, 55 | } 56 | 57 | func init() { 58 | serverLeaderCmd.AddCommand(serverLeaderGetCmd, serverLeaderAbdicateCmd) 59 | serverCmd.AddCommand(serverInfoCmd, serverLeaderCmd, serverPingCmd) 60 | } 61 | -------------------------------------------------------------------------------- /commands/marathon/task_cmds.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ContainX/depcon/pkg/cli" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var taskCmd = &cobra.Command{ 10 | Use: "task", 11 | Short: "Marathon task management", 12 | Long: `Manage tasks in a marathon cluster (eg. creating, listing, monitoring, kill) 13 | 14 | See tasks's subcommands for available choices`, 15 | } 16 | 17 | var taskListCmd = &cobra.Command{ 18 | Use: "list", 19 | Short: "List all tasks", 20 | Run: func(cmd *cobra.Command, args []string) { 21 | v, e := client(cmd).ListTasks() 22 | cli.Output(templateFor(T_TASKS, v), e) 23 | }, 24 | } 25 | 26 | var taskQueueCmd = &cobra.Command{ 27 | Use: "queue", 28 | Short: "List all queued tasks", 29 | Run: func(cmd *cobra.Command, args []string) { 30 | v, e := client(cmd).ListQueue() 31 | cli.Output(templateFor(T_QUEUED_TASKS, v), e) 32 | }, 33 | } 34 | 35 | var appTaskGetCmd = &cobra.Command{ 36 | Use: "get [applicationId]", 37 | Short: "List tasks for the application [applicationId]", 38 | Run: appTasks, 39 | } 40 | 41 | var appTaskKillallCmd = &cobra.Command{ 42 | Use: "killall [applicationId]", 43 | Short: "Kill tasks belonging to [applicationId]", 44 | Run: appKillAllTasks, 45 | } 46 | 47 | var appTaskKillCmd = &cobra.Command{ 48 | Use: "kill [taskId]", 49 | Short: "Kill a task [taskId] that belongs to an application", 50 | Run: appKillTask, 51 | } 52 | 53 | func init() { 54 | taskCmd.AddCommand(taskListCmd, appTaskGetCmd, appTaskKillCmd, appTaskKillallCmd, taskQueueCmd) 55 | 56 | // Task List Flags 57 | appTaskGetCmd.Flags().BoolP(DETAIL_FLAG, "d", false, "Prints each task instance in detailed form vs. table summary") 58 | // Task Kill Flags 59 | appTaskKillallCmd.Flags().String(HOST_FLAG, "", "Kill only those tasks running on host [host]. Default: none.") 60 | appTaskKillallCmd.Flags().Bool(SCALE_FLAG, false, "Scale the app down (i.e. decrement its instances setting by the number of tasks killed)") 61 | appTaskKillCmd.Flags().Bool(SCALE_FLAG, false, "Scale the app down (i.e. decrement its instances setting by the number of tasks killed)") 62 | } 63 | 64 | func appTasks(cmd *cobra.Command, args []string) { 65 | if cli.EvalPrintUsage(Usage(cmd), args, 1) { 66 | return 67 | } 68 | 69 | detailed, _ := cmd.Flags().GetBool(DETAIL_FLAG) 70 | 71 | v, e := client(cmd).GetTasks(args[0]) 72 | 73 | if detailed && e == nil { 74 | fmt.Println("") 75 | for _, t := range v { 76 | fmt.Printf("::: Task: %s\n\n", t.ID) 77 | cli.Output(templateFor(T_TASKS, v), e) 78 | } 79 | } else { 80 | cli.Output(templateFor(T_TASKS, v), e) 81 | } 82 | 83 | } 84 | 85 | func appKillAllTasks(cmd *cobra.Command, args []string) { 86 | if cli.EvalPrintUsage(Usage(cmd), args, 1) { 87 | return 88 | } 89 | 90 | host, _ := cmd.Flags().GetString(HOST_FLAG) 91 | scale, _ := cmd.Flags().GetBool(SCALE_FLAG) 92 | 93 | v, e := client(cmd).KillAppTasks(args[0], host, scale) 94 | cli.Output(templateFor(T_TASKS, v), e) 95 | } 96 | 97 | func appKillTask(cmd *cobra.Command, args []string) { 98 | if cli.EvalPrintUsage(Usage(cmd), args, 1) { 99 | return 100 | } 101 | scale, _ := cmd.Flags().GetBool(SCALE_FLAG) 102 | v, e := client(cmd).KillAppTask(args[0], scale) 103 | cli.Output(templateFor(T_TASK, v), e) 104 | } 105 | -------------------------------------------------------------------------------- /commands/marathon/templatectx.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "os" 7 | "text/template" 8 | 9 | "fmt" 10 | "github.com/ContainX/depcon/pkg/encoding" 11 | "github.com/spf13/viper" 12 | "path/filepath" 13 | "strings" 14 | ) 15 | 16 | const ( 17 | DefaultEnv = "-" 18 | DefaultRootPath = "." 19 | ContextErrFmt = "Error parsing template context: %s - %s" 20 | ) 21 | 22 | // Template based Functions 23 | var Funcs = FuncMap() 24 | 25 | type TemplateContext struct { 26 | Environments map[string]*TemplateEnvironment `json:"environments,omitempty"` 27 | } 28 | 29 | type TemplateEnvironment struct { 30 | Apps map[string]map[string]interface{} `json:"apps,omitempty"` 31 | } 32 | 33 | func (ctx *TemplateContext) Transform(writer io.Writer, descriptor, rootDir string) error { 34 | return ctx.TransformWithEnv(writer, descriptor, rootDir, viper.GetString(ENV_NAME)) 35 | } 36 | 37 | func (ctx *TemplateContext) TransformWithEnv(writer io.Writer, descriptor, rootDir, env string) error { 38 | var t *template.Template 39 | 40 | if b, err := ioutil.ReadFile(descriptor); err != nil { 41 | return err 42 | } else { 43 | var e error 44 | t = template.New(descriptor).Funcs(Funcs) 45 | t, e = t.Parse(string(b)) 46 | if e != nil { 47 | return e 48 | } 49 | 50 | if rootDir == "" { 51 | rootDir = DefaultRootPath 52 | } 53 | 54 | if matches, err := filepath.Glob(fmt.Sprintf("%s/**/*.tmpl", rootDir)); err == nil && len(matches) > 0 { 55 | if t, e = t.ParseFiles(matches...); err != nil { 56 | return err 57 | } 58 | } 59 | } 60 | m := ctx.mergeAppWithDefault(strings.ToLower(env)) 61 | 62 | if err := t.Execute(writer, m); err != nil { 63 | return err 64 | } 65 | return nil 66 | } 67 | 68 | // Validates the specified app is declared within the current envirnoment. If it is any values missing from 69 | // specific environment are propagated from the default environment 70 | func (ctx *TemplateContext) mergeAppWithDefault(env string) map[string]map[string]interface{} { 71 | if _, exists := ctx.Environments[DefaultEnv]; !exists { 72 | if _, cok := ctx.Environments[env]; !cok { 73 | return make(map[string]map[string]interface{}) 74 | } 75 | return ctx.Environments[env].Apps 76 | } 77 | 78 | defm := ctx.Environments[DefaultEnv].Apps 79 | 80 | if ctx.Environments[env] == nil { 81 | return defm 82 | } 83 | 84 | envm := ctx.Environments[env].Apps 85 | 86 | merged := make(map[string]map[string]interface{}) 87 | 88 | for app, props := range envm { 89 | merged[app] = props 90 | } 91 | 92 | for app, props := range defm { 93 | if _, exists := merged[app]; !exists { 94 | merged[app] = props 95 | } else { 96 | for k, v := range props { 97 | if _, ok := merged[app][k]; !ok { 98 | merged[app][k] = v 99 | } 100 | } 101 | 102 | } 103 | } 104 | return merged 105 | } 106 | 107 | func recovery() { 108 | recover() 109 | } 110 | 111 | func TemplateExists(filename string) bool { 112 | 113 | if len(filename) > 0 { 114 | if _, err := os.Stat(filename); err == nil { 115 | return true 116 | } 117 | } 118 | return false 119 | } 120 | 121 | func LoadTemplateContext(filename string) (*TemplateContext, error) { 122 | // Return empty context if non-exists 123 | if !TemplateExists(filename) { 124 | fmt.Println("Ignoring context file (not found) - template-context.json") 125 | return &TemplateContext{Environments: make(map[string]*TemplateEnvironment)}, nil 126 | } 127 | 128 | ctx, err := os.Open(filename) 129 | if err != nil { 130 | return nil, err 131 | } 132 | 133 | encoder, err := encoding.NewEncoder(encoding.JSON) 134 | if err != nil { 135 | return nil, fmt.Errorf(ContextErrFmt, filename, err.Error()) 136 | } 137 | 138 | result := &TemplateContext{Environments: make(map[string]*TemplateEnvironment)} 139 | 140 | if err := encoder.UnMarshal(ctx, result); err != nil { 141 | return nil, fmt.Errorf(ContextErrFmt, filename, err.Error()) 142 | } 143 | return result, nil 144 | } 145 | -------------------------------------------------------------------------------- /commands/marathon/templatectx_funcs.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/viper" 6 | "math" 7 | "os" 8 | "reflect" 9 | "strconv" 10 | "strings" 11 | "text/template" 12 | ) 13 | 14 | func FuncMap() template.FuncMap { 15 | return template.FuncMap{ 16 | "default": dfault, 17 | "dict": dict, 18 | "isEnv": isEnv, 19 | "isNotEnv": isNotEnv, 20 | "empty": empty, 21 | "max": max, 22 | "min": min, 23 | "toInt64": toInt64, 24 | "trim": strings.TrimSpace, 25 | "upper": strings.ToUpper, 26 | "lower": strings.ToLower, 27 | "title": strings.Title, 28 | "trimAll": func(a, b string) string { return strings.Trim(b, a) }, 29 | "trimSuffix": func(a, b string) string { return strings.TrimSuffix(b, a) }, 30 | "trimPrefix": func(a, b string) string { return strings.TrimPrefix(b, a) }, 31 | "split": split, 32 | "env": func(s string) string { return os.Getenv(s) }, 33 | "expandenv": func(s string) string { return os.ExpandEnv(s) }, 34 | "N": N, 35 | } 36 | } 37 | 38 | func isEnv(value string) bool { 39 | if len(value) > 0 { 40 | current := strings.ToLower(viper.GetString(ENV_NAME)) 41 | return current == strings.ToLower(value) 42 | } 43 | return false 44 | } 45 | 46 | func isNotEnv(value string) bool { 47 | return !isEnv(value) 48 | } 49 | 50 | func dfault(args ...interface{}) interface{} { 51 | arg := args[0] 52 | if len(args) < 2 { 53 | return arg 54 | } 55 | value := args[1] 56 | 57 | defer recovery() 58 | 59 | v := reflect.ValueOf(value) 60 | switch v.Kind() { 61 | case reflect.String, reflect.Slice, reflect.Array, reflect.Map: 62 | if v.Len() == 0 { 63 | return arg 64 | } 65 | case reflect.Bool: 66 | if !v.Bool() { 67 | return arg 68 | } 69 | default: 70 | return value 71 | } 72 | 73 | return value 74 | } 75 | 76 | // empty returns true if the given value has the zero value for its type. 77 | func empty(given interface{}) bool { 78 | g := reflect.ValueOf(given) 79 | if !g.IsValid() { 80 | return true 81 | } 82 | 83 | // Basically adapted from text/template.isTrue 84 | switch g.Kind() { 85 | default: 86 | return g.IsNil() 87 | case reflect.Array, reflect.Slice, reflect.Map, reflect.String: 88 | return g.Len() == 0 89 | case reflect.Bool: 90 | return g.Bool() == false 91 | case reflect.Complex64, reflect.Complex128: 92 | return g.Complex() == 0 93 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 94 | return g.Int() == 0 95 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 96 | return g.Uint() == 0 97 | case reflect.Float32, reflect.Float64: 98 | return g.Float() == 0 99 | case reflect.Struct: 100 | return false 101 | } 102 | } 103 | 104 | func max(a interface{}, i ...interface{}) int64 { 105 | aa := toInt64(a) 106 | for _, b := range i { 107 | bb := toInt64(b) 108 | if bb > aa { 109 | aa = bb 110 | } 111 | } 112 | return aa 113 | } 114 | 115 | func min(a interface{}, i ...interface{}) int64 { 116 | aa := toInt64(a) 117 | for _, b := range i { 118 | bb := toInt64(b) 119 | if bb < aa { 120 | aa = bb 121 | } 122 | } 123 | return aa 124 | } 125 | 126 | // toInt64 converts integer types to 64-bit integers 127 | func toInt64(v interface{}) int64 { 128 | 129 | if str, ok := v.(string); ok { 130 | iv, err := strconv.ParseInt(str, 10, 64) 131 | if err != nil { 132 | return 0 133 | } 134 | return iv 135 | } 136 | 137 | val := reflect.Indirect(reflect.ValueOf(v)) 138 | switch val.Kind() { 139 | case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: 140 | return val.Int() 141 | case reflect.Uint8, reflect.Uint16, reflect.Uint32: 142 | return int64(val.Uint()) 143 | case reflect.Uint, reflect.Uint64: 144 | tv := val.Uint() 145 | if tv <= math.MaxInt64 { 146 | return int64(tv) 147 | } 148 | // TODO: What is the sensible thing to do here? 149 | return math.MaxInt64 150 | case reflect.Float32, reflect.Float64: 151 | return int64(val.Float()) 152 | case reflect.Bool: 153 | if val.Bool() == true { 154 | return 1 155 | } 156 | return 0 157 | default: 158 | return 0 159 | } 160 | } 161 | 162 | func split(sep, orig string) map[string]string { 163 | parts := strings.Split(orig, sep) 164 | res := make(map[string]string, len(parts)) 165 | for i, v := range parts { 166 | res["_"+strconv.Itoa(i)] = v 167 | } 168 | return res 169 | } 170 | 171 | func dict(v ...interface{}) map[string]interface{} { 172 | dict := map[string]interface{}{} 173 | lenv := len(v) 174 | for i := 0; i < lenv; i += 2 { 175 | key := strval(v[i]) 176 | if i+1 >= lenv { 177 | dict[key] = "" 178 | continue 179 | } 180 | dict[key] = v[i+1] 181 | } 182 | return dict 183 | } 184 | 185 | func strval(v interface{}) string { 186 | switch v := v.(type) { 187 | case string: 188 | return v 189 | case []byte: 190 | return string(v) 191 | case error: 192 | return v.Error() 193 | case fmt.Stringer: 194 | return v.String() 195 | default: 196 | return fmt.Sprintf("%v", v) 197 | } 198 | } 199 | 200 | func N(start, end int) (stream chan int) { 201 | stream = make(chan int) 202 | go func() { 203 | for i := start; i <= end; i++ { 204 | stream <- i 205 | } 206 | close(stream) 207 | }() 208 | return 209 | } 210 | -------------------------------------------------------------------------------- /commands/marathon/templatectx_test.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestMergeFunctionality(t *testing.T) { 9 | tc, err := LoadTemplateContext("resources/testcontext.json") 10 | assert.NoError(t, err) 11 | 12 | m := tc.mergeAppWithDefault("prod") 13 | 14 | assert.Equal(t, float64(0.1), m["appa"]["cpus"]) 15 | assert.Equal(t, float64(3), m["appa"]["instances"]) 16 | assert.Equal(t, float64(300), m["appa"]["mem"]) 17 | 18 | } 19 | -------------------------------------------------------------------------------- /commands/marathon/templates.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | "github.com/ContainX/depcon/marathon" 5 | "github.com/ContainX/depcon/pkg/cli" 6 | "github.com/ContainX/depcon/utils" 7 | "io" 8 | "text/template" 9 | ) 10 | 11 | const ( 12 | T_APPLICATIONS = ` 13 | {{ "ID" }} {{ "INSTANCES" }} {{ "CPU" }} {{ "MEM" }} {{ "PORTS" }} {{ "CONTAINER" }} {{ "VERSION" }} 14 | {{range .Apps}}{{ .ID }} {{ .Instances }} {{ .CPUs | floatToString }} {{ .Mem | floatToString }} {{ .Ports | intConcat }} {{ .Container | dockerImage }} {{ .Version }} 15 | {{end}}` 16 | 17 | T_APPLICATION = ` 18 | {{ "ID" }} {{ .ID }} 19 | {{ "CPUs:" }} {{ .CPUs | floatToString }} 20 | {{ "Memory:" }} {{ .Mem | floatToString }} 21 | {{ "Ports:" }} {{ .Ports | intConcat }} 22 | {{ "Instances:" }} {{ .Instances | intToString }} 23 | {{ "Version:" }} {{ .Version }} 24 | {{ "Tasks:" }} {{ "Staged" | pad }} {{ .TasksStaged | intToString }} 25 | {{ "Running" | pad }} {{ .TasksRunning | intToString }} 26 | {{ "Healthy" | pad }} {{ .TasksHealthy | intToString }} 27 | {{ "UnHealthy" | pad }} {{ .TasksUnHealthy | intToString }} 28 | 29 | {{ if hasDocker .Container }} 30 | {{ "Container: " }} {{ "Type" | pad }} {{ "Docker" }} 31 | {{ "Image" | pad }} {{ .Container.Docker.Image }} 32 | {{ "Network" | pad }} {{ .Container.Docker.Network }} 33 | {{- end}} 34 | {{ "Environment:" }} 35 | {{ range $key, $value := .Env }} {{ $key | pad }} {{ $value }} 36 | {{end}} 37 | {{ "Labels:" }} 38 | {{ range $key, $value := .Labels }} {{ $key | pad }} {{ $value }} 39 | {{end}} 40 | ` 41 | 42 | T_VERSIONS = ` 43 | {{ "VERSIONS" }} 44 | {{ range .Versions }}{{ . }} 45 | {{end}}` 46 | 47 | T_DEPLOYMENT_ID = ` 48 | {{ "DEPLOYMENT_ID" }} {{ "VERSION" }} 49 | {{ .DeploymentID }} {{ .Version }}` 50 | 51 | T_TASKS = ` 52 | {{ "APP_ID" }} {{ "HOST" }} {{ "VERSION" }} {{ "STARTED" }} {{ "TASK_ID" }} 53 | {{ range . }}{{ .AppID }} {{ .Host }} {{ .Version }} {{ .StartedAt | fdate }} {{ .ID }} 54 | {{end}}` 55 | 56 | T_TASK = ` 57 | {{ "ID:" }} {{ .ID }} 58 | {{ "AppID:" }} {{ .AppID }} 59 | {{ "Staged:" }} {{ .StagedAt | fdate }} 60 | {{ "Started:" }} {{ .StartedAt | fdate }} 61 | {{ "Host:" }} {{ .Host }} 62 | {{ "Ports:" }} {{ .Ports | intConcat }} 63 | ` 64 | T_DEPLOYMENTS = ` 65 | {{ "DEPLOYMENT_ID" }} {{ "VERSION" }} {{ "PROGRESS" }} {{ "APPS" }} 66 | {{ range . }}{{ .DeployID }} {{ .Version }} {{ .CurrentStep | intToString }}/{{ .TotalSteps | intToString }} {{ .AffectedApps | idConcat }} 67 | {{end}}` 68 | T_LEADER_INFO = ` 69 | {{ "Leader:" }} {{ .Leader }} 70 | ` 71 | 72 | T_PING = ` 73 | {{ "HOST" }} {{ "DURATION" }} 74 | {{ .Host }} {{ .Elapsed | msDur }} 75 | ` 76 | 77 | T_MARATHON_INFO = ` 78 | {{ "INFO" }} 79 | {{ "Name:" }} {{ .Name }} 80 | {{ "Version:" }} {{ .Version }} 81 | {{ "FrameworkId:" }} {{ .FrameworkId }} 82 | {{ "Leader:" }} {{ .Leader }} 83 | 84 | {{ "HTTP CONFIG" }} 85 | {{ "HTTP Port:" }} {{ .HttpConfig.HttpPort | valString }} 86 | {{ "HTTPS Port:" }} {{ .HttpConfig.HttpsPort | valString }} 87 | 88 | {{ "MARATHON CONFIG" }} 89 | {{ "Checkpoint:" }} {{ .MarathonConfig.Checkpoint | valString }} 90 | {{ "Executor:" }} {{ .MarathonConfig.Executor }} 91 | {{ "HA:" }} {{ .MarathonConfig.Ha | valString }} 92 | {{ "Master:" }} {{ .MarathonConfig.Master }} 93 | {{ "Failover Timeout:" }} {{ .MarathonConfig.FailoverTimeout | valString }} 94 | {{ "Local Port (Min):" }} {{ .MarathonConfig.LocalPortMin | valString }} 95 | {{ "Local Port (Max):" }} {{ .MarathonConfig.LocalPortMax | valString }} 96 | 97 | {{ "ZOOKEEPER CONFIG" }} 98 | {{ "ZK:" }} {{ .ZookeeperConfig.Zk }} 99 | {{ "Timeout:" }} {{ .ZookeeperConfig.ZkTimeout | valString }} 100 | ` 101 | T_QUEUED_TASKS = ` 102 | {{ "APP_ID" }} {{ "VERSION" }} {{ "OVERDUE" }} 103 | {{ range .Queue }}{{ .App.ID }} {{ .App.Version }} {{ .Delay.overdue | valString }} 104 | {{end}}` 105 | 106 | T_MESSAGE = ` 107 | {{ "Message:" }} {{ .Message }} 108 | ` 109 | T_GROUPS = ` 110 | {{ "ID" }} {{ "VERSION" }} {{ "GROUPS" }} {{ "APPS" }} 111 | {{ range . }}{{ .GroupID }} {{ .Version }} {{ .Groups | len | valString }} {{ .Apps | len | valString }} 112 | {{end}}` 113 | ) 114 | 115 | type Templated struct { 116 | cli.FormatData 117 | } 118 | 119 | func templateFor(template string, data interface{}) Templated { 120 | return Templated{cli.FormatData{Template: template, Data: data, Funcs: buildFuncMap()}} 121 | } 122 | 123 | func (d Templated) ToColumns(output io.Writer) error { 124 | return d.FormatData.ToColumns(output) 125 | } 126 | 127 | func (d Templated) Data() cli.FormatData { 128 | return d.FormatData 129 | } 130 | 131 | func buildFuncMap() template.FuncMap { 132 | funcMap := template.FuncMap{ 133 | "intConcat": utils.ConcatInts, 134 | "idConcat": utils.ConcatIdentifiers, 135 | "dockerImage": dockerImageOrEmpty, 136 | "hasDocker": hasDocker, 137 | } 138 | return funcMap 139 | } 140 | 141 | func hasDocker(c *marathon.Container) bool { 142 | return c != nil && c.Docker != nil 143 | } 144 | 145 | func dockerImageOrEmpty(c *marathon.Container) string { 146 | if c != nil && c.Docker != nil { 147 | return c.Docker.Image 148 | } 149 | return "" 150 | } 151 | -------------------------------------------------------------------------------- /compose/compose.go: -------------------------------------------------------------------------------- 1 | package compose 2 | 3 | const ( 4 | DEFAULT_PROJECT string = "depcon_proj" 5 | ) 6 | 7 | type Compose interface { 8 | Up(services ...string) error 9 | 10 | Kill(services ...string) error 11 | 12 | Logs(services ...string) error 13 | 14 | Delete(services ...string) error 15 | 16 | Build(services ...string) error 17 | 18 | Restart(services ...string) error 19 | 20 | Pull(services ...string) error 21 | 22 | Start(services ...string) error 23 | 24 | Stop(services ...string) error 25 | 26 | Port(index int, proto, service, port string) error 27 | 28 | PS(quiet bool) error 29 | } 30 | -------------------------------------------------------------------------------- /compose/compose_wrapper.go: -------------------------------------------------------------------------------- 1 | package compose 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/ContainX/depcon/pkg/envsubst" 7 | "github.com/docker/distribution/context" 8 | "github.com/docker/libcompose/docker" 9 | "github.com/docker/libcompose/docker/ctx" 10 | "github.com/docker/libcompose/project" 11 | "github.com/docker/libcompose/project/options" 12 | "io/ioutil" 13 | "os" 14 | "strings" 15 | ) 16 | 17 | const ( 18 | DOCKER_TLS_VERIFY string = "DOCKER_TLS_VERIFY" 19 | ) 20 | 21 | var ( 22 | ErrorParamsMissing = errors.New("One or more ${PARAMS} that were defined in the compose file could not be resolved.") 23 | ) 24 | 25 | type ComposeWrapper struct { 26 | context *Context 27 | project project.APIProject 28 | } 29 | 30 | func NewCompose(context *Context) Compose { 31 | c := new(ComposeWrapper) 32 | c.context = context 33 | project, err := c.createDockerContext() 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | c.project = project 38 | return c 39 | } 40 | 41 | func (c *ComposeWrapper) Up(services ...string) error { 42 | options := options.Up{Create: options.Create{}} 43 | return c.project.Up(context.Background(), options, services...) 44 | } 45 | 46 | func (c *ComposeWrapper) Kill(services ...string) error { 47 | return c.project.Kill(context.Background(), "SIGKILL", services...) 48 | } 49 | 50 | func (c *ComposeWrapper) Build(services ...string) error { 51 | options := options.Build{} 52 | return c.project.Build(context.Background(), options, services...) 53 | } 54 | 55 | func (c *ComposeWrapper) Restart(services ...string) error { 56 | timeout := 10 57 | return c.project.Restart(context.Background(), timeout, services...) 58 | } 59 | 60 | func (c *ComposeWrapper) Pull(services ...string) error { 61 | return c.project.Pull(context.Background(), services...) 62 | } 63 | 64 | func (c *ComposeWrapper) Delete(services ...string) error { 65 | options := options.Delete{} 66 | return c.project.Delete(context.Background(), options, services...) 67 | } 68 | 69 | func (c *ComposeWrapper) Logs(services ...string) error { 70 | return c.project.Log(context.Background(), true, services...) 71 | } 72 | 73 | func (c *ComposeWrapper) Start(services ...string) error { 74 | return c.execStartStop(true, services...) 75 | } 76 | 77 | func (c *ComposeWrapper) Stop(services ...string) error { 78 | return c.execStartStop(false, services...) 79 | } 80 | 81 | func (c *ComposeWrapper) execStartStop(start bool, services ...string) error { 82 | if start { 83 | return c.project.Start(context.Background(), services...) 84 | } 85 | options := options.Down{} 86 | return c.project.Down(context.Background(), options, services...) 87 | } 88 | 89 | func (c *ComposeWrapper) Port(index int, proto, service, port string) error { 90 | 91 | output, err := c.project.Port(context.Background(), index, proto, service, port) 92 | if err != nil { 93 | return err 94 | } 95 | fmt.Println(output) 96 | return nil 97 | } 98 | 99 | func (c *ComposeWrapper) PS(quiet bool) error { 100 | if allInfo, err := c.project.Ps(context.Background()); err == nil { 101 | os.Stdout.WriteString(allInfo.String([]string{"Name", "Command", "State", "Ports"}, !quiet)) 102 | } 103 | return nil 104 | } 105 | 106 | func (c *ComposeWrapper) createDockerContext() (project.APIProject, error) { 107 | 108 | if c.context.EnvParams != nil && len(c.context.EnvParams) > 0 { 109 | file, err := os.Open(c.context.ComposeFile) 110 | if err != nil { 111 | return nil, fmt.Errorf("Error opening filename %s, %s", c.context.ComposeFile, err.Error()) 112 | } 113 | parsed, missing := envsubst.SubstFileTokens(file, c.context.EnvParams) 114 | log.Debug("Map: %v\nParsed: %s\n", c.context.EnvParams, parsed) 115 | 116 | if c.context.ErrorOnMissingParams && missing { 117 | return nil, ErrorParamsMissing 118 | } 119 | file, err = ioutil.TempFile("", "depcon") 120 | if err != nil { 121 | return nil, err 122 | } 123 | err = ioutil.WriteFile(file.Name(), []byte(parsed), os.ModeTemporary) 124 | if err != nil { 125 | return nil, err 126 | } 127 | c.context.ComposeFile = file.Name() 128 | } 129 | return docker.NewProject(&ctx.Context{ 130 | Context: project.Context{ 131 | ComposeFiles: strings.Split(c.context.ComposeFile, ","), 132 | ProjectName: c.context.ProjectName, 133 | }, 134 | }, nil) 135 | } 136 | -------------------------------------------------------------------------------- /compose/logfactory.go: -------------------------------------------------------------------------------- 1 | package compose 2 | 3 | import ( 4 | depconLog "github.com/ContainX/depcon/pkg/logger" 5 | "github.com/docker/libcompose/logger" 6 | "io" 7 | "os" 8 | ) 9 | 10 | var log = depconLog.GetLogger("depcon.compose") 11 | 12 | type ComposeLogger struct { 13 | } 14 | 15 | func (n *ComposeLogger) Out(b []byte) { 16 | log.Infof("%v", b) 17 | } 18 | 19 | func (n *ComposeLogger) Err(b []byte) { 20 | log.Errorf("%v", b) 21 | } 22 | 23 | func (n *ComposeLogger) ErrWriter() io.Writer { 24 | return os.Stderr 25 | } 26 | 27 | func (n *ComposeLogger) OutWriter() io.Writer { 28 | return os.Stdout 29 | } 30 | 31 | func (n *ComposeLogger) Create(name string) logger.Logger { 32 | return &ComposeLogger{} 33 | } 34 | -------------------------------------------------------------------------------- /compose/struct.go: -------------------------------------------------------------------------------- 1 | package compose 2 | 3 | type Context struct { 4 | ComposeFile string 5 | ProjectName string 6 | EnvParams map[string]string 7 | ErrorOnMissingParams bool 8 | } 9 | -------------------------------------------------------------------------------- /docker-release/.gitignore: -------------------------------------------------------------------------------- 1 | depcon 2 | -------------------------------------------------------------------------------- /docker-release/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | RUN apk update && apk add ca-certificates 3 | 4 | ADD depcon /usr/bin 5 | 6 | ENV DEPCON_MODE="" \ 7 | MARATHON_HOST="http://localhost:8080" \ 8 | MARATHON_USER="" \ 9 | MARATHON_PASS="" 10 | 11 | ENTRYPOINT ["/usr/bin/depcon"] 12 | -------------------------------------------------------------------------------- /docker-release/env.sample: -------------------------------------------------------------------------------- 1 | DEPCON_MODE=marathon 2 | MARATHON_HOST=http://hostname:8080 3 | 4 | # Uncomment below if authentication is enabled within Marathon 5 | #MARATHON_USER=someuser 6 | #MARATHON_PASS=somepass 7 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2015 Jeremy Unruh . 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "github.com/ContainX/depcon/commands" 18 | ) 19 | 20 | var VERSION string = "" 21 | var BUILD_DATE string = "" 22 | 23 | func main() { 24 | commands.Version = VERSION 25 | commands.BuildDate = BUILD_DATE 26 | commands.Execute() 27 | } 28 | -------------------------------------------------------------------------------- /marathon/application.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/ContainX/depcon/pkg/encoding" 7 | "github.com/ContainX/depcon/pkg/envsubst" 8 | "github.com/ContainX/depcon/pkg/httpclient" 9 | "github.com/ContainX/depcon/utils" 10 | "io" 11 | "os" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | const ( 17 | ActionRestart = "restart" 18 | ActionVersions = "versions" 19 | PathTasks = "tasks" 20 | ) 21 | 22 | var ( 23 | ErrorAppExists = errors.New("The application already exists") 24 | ErrorGroupExists = errors.New("The group already exists") 25 | ErrorInvalidGroupId = errors.New("The group identifier is invalid") 26 | ErrorNoAppExists = errors.New("The application does not exist. Create an application before updating") 27 | ErrorGroupAppExists = errors.New("The group does not exist. Create a group before updating") 28 | ErrorAppParamsMissing = errors.New("One or more ${PARAMS} that were defined in the app configuration could not be resolved.") 29 | ) 30 | 31 | func (c *MarathonClient) CreateApplicationFromFile(filename string, opts *CreateOptions) (*Application, error) { 32 | app, err := c.ParseApplicationFromFile(filename, opts) 33 | if err != nil { 34 | return app, err 35 | } 36 | 37 | if opts.StopDeploy { 38 | if deployment, err := c.CancelAppDeployment(app.ID, false); err == nil && deployment != nil { 39 | log.Infof("Previous deployment found.. cancelling and waiting until complete.") 40 | c.WaitForDeployment(deployment.DeploymentID, time.Second*30) 41 | } 42 | } 43 | 44 | return c.CreateApplication(app, opts.Wait, opts.Force) 45 | } 46 | 47 | func (c *MarathonClient) CreateApplicationFromString(filename string, appstr string, opts *CreateOptions) (*Application, error) { 48 | et, err := encoding.EncoderTypeFromExt(filename) 49 | if err != nil { 50 | return nil, err 51 | } 52 | app, err := c.ParseApplicationFromString(strings.NewReader(appstr), et, opts) 53 | 54 | if err != nil { 55 | return app, err 56 | } 57 | 58 | if opts.StopDeploy { 59 | if deployment, err := c.CancelAppDeployment(app.ID, false); err == nil && deployment != nil { 60 | c.logOutput(log.Infof, "Previous deployment found.. cancelling and waiting until complete.") 61 | c.WaitForDeployment(deployment.DeploymentID, time.Second*30) 62 | } 63 | } 64 | 65 | return c.CreateApplication(app, opts.Wait, opts.Force) 66 | 67 | } 68 | 69 | func (c *MarathonClient) ParseApplicationFromFile(filename string, opts *CreateOptions) (*Application, error) { 70 | log.Infof("Creating Application from file: %s", filename) 71 | 72 | file, err := os.Open(filename) 73 | if err != nil { 74 | return nil, fmt.Errorf("Error opening filename %s, %s", filename, err.Error()) 75 | } 76 | 77 | if et, err := encoding.EncoderTypeFromExt(filename); err != nil { 78 | return nil, err 79 | } else { 80 | return c.ParseApplicationFromString(file, et, opts) 81 | } 82 | } 83 | 84 | func (c *MarathonClient) ParseApplicationFromString(r io.Reader, et encoding.EncoderType, opts *CreateOptions) (*Application, error) { 85 | 86 | options := initCreateOptions(opts) 87 | 88 | var encoder encoding.Encoder 89 | var err error 90 | 91 | encoder, err = encoding.NewEncoder(et) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | parsed, missing := envsubst.SubstFileTokens(r, options.EnvParams) 97 | 98 | if opts.ErrorOnMissingParams && missing { 99 | return nil, ErrorAppParamsMissing 100 | } 101 | 102 | if opts.DryRun { 103 | fmt.Printf("Create Application :: DryRun :: Template Output\n\n%s", parsed) 104 | os.Exit(0) 105 | } 106 | 107 | app := new(Application) 108 | err = encoder.UnMarshalStr(parsed, &app) 109 | if err != nil { 110 | return nil, err 111 | } 112 | return app, nil 113 | } 114 | 115 | func (c *MarathonClient) CreateApplication(app *Application, wait, force bool) (*Application, error) { 116 | c.logOutput(log.Infof, "Creating Application '%s', wait: %v, force: %v", app.ID, wait, force) 117 | 118 | result := new(Application) 119 | resp := c.http.HttpPost(c.marathonUrl(API_APPS), app, result) 120 | if resp.Error != nil { 121 | if resp.Error == httpclient.ErrorMessage { 122 | if resp.Status == 409 { 123 | if force { 124 | return c.UpdateApplication(app, wait, force) 125 | } 126 | return nil, ErrorAppExists 127 | } 128 | if resp.Status == 422 { 129 | return nil, fmt.Errorf("Error occurred: %s", resp.Content) 130 | } 131 | return nil, fmt.Errorf("Error occurred (Status %v) Body -> %s", resp.Status, resp.Content) 132 | } 133 | return nil, resp.Error 134 | } 135 | if wait { 136 | err := c.WaitForApplication(result.ID, c.determineTimeout(app)) 137 | if err != nil { 138 | return result, err 139 | } 140 | } 141 | app, err := c.GetApplication(result.ID) 142 | if err == nil { 143 | return app, nil 144 | } 145 | return result, nil 146 | } 147 | 148 | func (c *MarathonClient) UpdateApplication(app *Application, wait bool, force bool) (*Application, error) { 149 | log.Infof("Update Application '%s', wait = %v", app.ID, wait) 150 | result := new(DeploymentID) 151 | id := utils.TrimRootPath(app.ID) 152 | app.ID = "" 153 | url := c.marathonUrl(API_APPS, id) 154 | if force { 155 | url = fmt.Sprintf("%v?force=%v", c.marathonUrl(API_APPS, id), force) 156 | } 157 | resp := c.http.HttpPut(url, app, result) 158 | 159 | if resp.Error != nil { 160 | if resp.Error == httpclient.ErrorMessage { 161 | if resp.Status == 422 { 162 | return nil, ErrorNoAppExists 163 | } 164 | } 165 | return nil, resp.Error 166 | } 167 | if wait { 168 | if err := c.WaitForDeployment(result.DeploymentID, c.determineTimeout(app)); err != nil { 169 | return nil, err 170 | } 171 | err := c.WaitForApplication(id, c.determineTimeout(app)) 172 | if err != nil { 173 | return nil, err 174 | } 175 | } 176 | // Get the latest version of the application to return 177 | app, err := c.GetApplication(id) 178 | return app, err 179 | } 180 | 181 | func (c *MarathonClient) ListApplications() (*Applications, error) { 182 | return c.ListApplicationsWithFilters("") 183 | } 184 | 185 | func (c *MarathonClient) ListApplicationsWithFilters(filter string) (*Applications, error) { 186 | log.Debugf("Enter: ListApplications") 187 | 188 | apps := new(Applications) 189 | 190 | url := c.marathonUrl(API_APPS) 191 | if len(filter) > 0 { 192 | if strings.Contains(filter, "=") == false { 193 | filter = fmt.Sprintf("id=%s", filter) 194 | } 195 | url = fmt.Sprintf("%s?%s", url, filter) 196 | } 197 | resp := c.http.HttpGet(url, apps) 198 | if resp.Error != nil { 199 | return nil, resp.Error 200 | } 201 | return apps, nil 202 | } 203 | 204 | func (c *MarathonClient) GetApplication(id string) (*Application, error) { 205 | log.Debugf("Enter: GetApplication: %s", id) 206 | app := new(AppById) 207 | resp := c.http.HttpGet(c.marathonUrl(API_APPS, id), app) 208 | if resp.Error != nil { 209 | return nil, resp.Error 210 | } 211 | return &app.App, nil 212 | } 213 | 214 | func (c *MarathonClient) HasApplication(id string) (bool, error) { 215 | app, err := c.GetApplication(id) 216 | 217 | if err != nil { 218 | if err == httpclient.ErrorNotFound { 219 | return false, nil 220 | } 221 | return false, err 222 | } 223 | return app != nil, nil 224 | } 225 | 226 | func (c *MarathonClient) DestroyApplication(id string) (*DeploymentID, error) { 227 | log.Infof("Deleting Application '%s'", id) 228 | deploymentId := new(DeploymentID) 229 | 230 | resp := c.http.HttpDelete(c.marathonUrl(API_APPS, id), nil, deploymentId) 231 | if resp.Error != nil { 232 | return nil, resp.Error 233 | } 234 | return deploymentId, nil 235 | } 236 | 237 | func (c *MarathonClient) RestartApplication(id string, force bool) (*DeploymentID, error) { 238 | log.Infof("Restarting Application '%s', force: %v", id, force) 239 | 240 | deploymentId := new(DeploymentID) 241 | 242 | uri := fmt.Sprintf("%s?force=%v", c.marathonUrl(API_APPS, id, ActionRestart), force) 243 | resp := c.http.HttpPost(uri, nil, deploymentId) 244 | if resp.Error != nil { 245 | return nil, resp.Error 246 | } 247 | return deploymentId, nil 248 | } 249 | 250 | func (c *MarathonClient) PauseApplication(id string) (*DeploymentID, error) { 251 | log.Infof("Suspending Application '%s'", id) 252 | deploymentId := new(DeploymentID) 253 | 254 | uri := fmt.Sprintf("%s?scale=true&force=true", c.marathonUrl(API_APPS, id, "tasks")) 255 | resp := c.http.HttpDelete(uri, nil, deploymentId) 256 | if resp.Error != nil { 257 | return nil, resp.Error 258 | } 259 | return deploymentId, nil 260 | } 261 | 262 | func (c *MarathonClient) ScaleApplication(id string, instances int) (*DeploymentID, error) { 263 | log.Infof("Scale Application '%s' to %v instances", id, instances) 264 | 265 | update := new(Application) 266 | update.ID = id 267 | update.Instances = instances 268 | deploymentID := new(DeploymentID) 269 | resp := c.http.HttpPut(c.marathonUrl(API_APPS, id), &update, deploymentID) 270 | if resp.Error != nil { 271 | return nil, resp.Error 272 | } 273 | return deploymentID, nil 274 | } 275 | 276 | func (c *MarathonClient) ListVersions(id string) (*Versions, error) { 277 | versions := new(Versions) 278 | resp := c.http.HttpGet(c.marathonUrl(API_APPS, id, ActionVersions), versions) 279 | if resp.Error != nil { 280 | return nil, resp.Error 281 | } 282 | return versions, nil 283 | 284 | } 285 | 286 | func NewApplication(id string) *Application { 287 | application := new(Application) 288 | application.ID = id 289 | return application 290 | } 291 | 292 | // The number of instances that the application should run 293 | // {count} - number of instances 294 | func (app *Application) Count(count int) *Application { 295 | app.Instances = count 296 | return app 297 | } 298 | 299 | // The amount of memory in MB to assign per instance 300 | // {memory} - memory in MB 301 | func (app *Application) Memory(memory float64) *Application { 302 | app.Mem = memory 303 | return app 304 | } 305 | 306 | // The amount of CPU shares to assign per instance 307 | // {cpu} - the CPU shares 308 | func (app *Application) CPU(cpu float64) *Application { 309 | app.CPUs = cpu 310 | return app 311 | } 312 | 313 | // Rolls back an application to a specific version 314 | // {version} - the version to rollback 315 | func (app *Application) RollbackVersion(version string) *Application { 316 | app.Version = version 317 | return app 318 | } 319 | 320 | func (c *MarathonClient) determineTimeout(app *Application) time.Duration { 321 | if c.opts != nil && c.opts.WaitTimeout > 0 { 322 | return c.opts.WaitTimeout 323 | } 324 | 325 | if app == nil { 326 | return DefaultTimeout 327 | } 328 | 329 | max := DefaultTimeout 330 | 331 | if len(app.HealthChecks) > 0 { 332 | for _, h := range app.HealthChecks { 333 | grace := time.Duration(h.GracePeriodSeconds) * time.Second 334 | if grace > max { 335 | max = grace 336 | } 337 | } 338 | log.Debugf("determineTimeout: Max is %d\n", max) 339 | return max 340 | } 341 | return DefaultTimeout 342 | } 343 | -------------------------------------------------------------------------------- /marathon/application_test.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | "github.com/ContainX/depcon/pkg/mockrest" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | const ( 10 | AppsFolder = "testdata/apps/" 11 | CommonFolder = "testdata/common/" 12 | ) 13 | 14 | func TestCreateApplicationFromFile(t *testing.T) { 15 | 16 | envParams := make(map[string]string, 1) 17 | envParams["NODE_EXPORTER_VERSION"] = "1" 18 | 19 | opts := &CreateOptions{Wait: false, Force: true, ErrorOnMissingParams: true, EnvParams: envParams} 20 | 21 | c := MarathonClient{} 22 | app, err := c.ParseApplicationFromFile(AppsFolder+"app_params.json", opts) 23 | if err != nil { 24 | log.Panicf("Expected success %v", err) 25 | } 26 | log.Debug("%v", app) 27 | if app.Labels["tags"] != "prom-metrics" { 28 | log.Panic("Expected Labels parsed correctly") 29 | } 30 | if app.Instances != int(2) { 31 | log.Panic("Expected Instances parsed correctly") 32 | } 33 | } 34 | 35 | func TestListApplications(t *testing.T) { 36 | s := mockrest.StartNewWithFile(AppsFolder + "list_apps_response.json") 37 | defer s.Stop() 38 | 39 | c := NewMarathonClient(s.URL, "", "", "") 40 | apps, err := c.ListApplications() 41 | 42 | assert.Nil(t, err, "Error response was not expected") 43 | assert.Equal(t, "/myapp", apps.Apps[0].ID) 44 | } 45 | 46 | func TestGetApplication(t *testing.T) { 47 | s := mockrest.StartNewWithFile(AppsFolder + "get_app_response.json") 48 | defer s.Stop() 49 | 50 | c := NewMarathonClient(s.URL, "", "", "") 51 | app, err := c.GetApplication("storage/redis-x") 52 | 53 | assert.Nil(t, err, "Error response was not expected") 54 | assert.Equal(t, "/storage/redis-x", app.ID) 55 | assert.Equal(t, 1, len(app.Ports)) 56 | assert.Equal(t, "redis", app.Container.Docker.Image) 57 | assert.Equal(t, "cache", app.Labels["role"]) 58 | } 59 | 60 | func TestHasApplication(t *testing.T) { 61 | s := mockrest.StartNewWithFile(AppsFolder + "get_app_response.json") 62 | defer s.Stop() 63 | 64 | c := NewMarathonClient(s.URL, "", "", "") 65 | ok, err := c.HasApplication("/storage/redis-x") 66 | 67 | assert.Nil(t, err, "Error response was not expected") 68 | assert.Equal(t, true, ok) 69 | } 70 | 71 | func TestHasApplicationInvalid(t *testing.T) { 72 | s := mockrest.StartNewWithStatusCode(404) 73 | defer s.Stop() 74 | 75 | c := NewMarathonClient(s.URL, "", "", "") 76 | ok, _ := c.HasApplication("/storage/redis-invalid") 77 | 78 | assert.Equal(t, false, ok) 79 | } 80 | 81 | func TestDestroyApplication(t *testing.T) { 82 | s := mockrest.StartNewWithFile(CommonFolder + "deployid_response.json") 83 | defer s.Stop() 84 | 85 | c := NewMarathonClient(s.URL, "", "", "") 86 | depId, err := c.DestroyApplication("/someapp") 87 | assert.Nil(t, err, "Error response was not expected") 88 | assert.Equal(t, "5ed4c0c5-9ff8-4a6f-a0cd-f57f59a34b43", depId.DeploymentID) 89 | } 90 | 91 | func TestRestartApplication(t *testing.T) { 92 | s := mockrest.StartNewWithFile(CommonFolder + "deployid_response.json") 93 | defer s.Stop() 94 | 95 | c := NewMarathonClient(s.URL, "", "", "") 96 | depId, err := c.DestroyApplication("/someapp") 97 | assert.Nil(t, err, "Error response was not expected") 98 | assert.Equal(t, "5ed4c0c5-9ff8-4a6f-a0cd-f57f59a34b43", depId.DeploymentID) 99 | } 100 | 101 | func TestScaleApplication(t *testing.T) { 102 | s := mockrest.StartNewWithFile(CommonFolder + "deployid_response.json") 103 | defer s.Stop() 104 | 105 | c := NewMarathonClient(s.URL, "", "", "") 106 | depId, err := c.ScaleApplication("/someapp", 5) 107 | assert.Nil(t, err, "Error response was not expected") 108 | assert.Equal(t, "5ed4c0c5-9ff8-4a6f-a0cd-f57f59a34b43", depId.DeploymentID) 109 | } 110 | 111 | func TestCreateApplicationInvalidAppId(t *testing.T) { 112 | s := mockrest.StartNewWithStatusCode(422) 113 | defer s.Stop() 114 | 115 | c := NewMarathonClient(s.URL, "", "", "") 116 | _, err := c.CreateApplication(NewApplication("/someapp"), false, false) 117 | 118 | assert.NotNil(t, err, "Expecting Error") 119 | } 120 | 121 | func TestNewApplication(t *testing.T) { 122 | app := NewApplication("/some/application") 123 | assert.Equal(t, "/some/application", app.ID) 124 | } 125 | -------------------------------------------------------------------------------- /marathon/bluegreen/bluegreen.go: -------------------------------------------------------------------------------- 1 | package bluegreen 2 | 3 | import ( 4 | "github.com/ContainX/depcon/marathon" 5 | "github.com/ContainX/depcon/pkg/httpclient" 6 | "time" 7 | ) 8 | 9 | type BlueGreen interface { 10 | 11 | // Starts a blue green deployment. If the application exists then the deployment will slowly 12 | // release the new version, draining connections from the HAProxy balancer during the process 13 | // {filename} - the file name of the json | yaml application 14 | // {opts} - blue/green options 15 | DeployBlueGreenFromFile(filename string) (*marathon.Application, error) 16 | 17 | // Starts a blue green deployment. If the application exists then the deployment will slowly 18 | // release the new version, draining connections from the HAProxy balancer during the process 19 | // {app} - the application to deploy/update 20 | // {opts} - blue/green options 21 | DeployBlueGreen(app *marathon.Application) (*marathon.Application, error) 22 | } 23 | 24 | type BlueGreenOptions struct { 25 | // The max time to wait on HAProxy to drain connections (in seconds) 26 | ProxyWaitTimeout time.Duration 27 | // Initial number of app instances to create 28 | InitialInstances int 29 | // Delay (in seconds) to wait between each successive deployment step 30 | StepDelay time.Duration 31 | // Resume from previous deployment 32 | Resume bool 33 | // Marathon-LB stats endpoint - ex: http://host:9090 34 | LoadBalancer string 35 | // if true will attempt to wait until the NEW application or group is running 36 | Wait bool 37 | // If true an error will be returned on params defined in the configuration file that 38 | // could not resolve to user input and environment variables 39 | ErrorOnMissingParams bool 40 | // Additional environment params - looks at this map for token substitution which takes 41 | // priority over matching environment variables 42 | EnvParams map[string]string 43 | // Do not actually deploy or scale. Dry run only 44 | DryRun bool 45 | } 46 | 47 | type BGClient struct { 48 | marathon marathon.Marathon 49 | opts *BlueGreenOptions 50 | http *httpclient.HttpClient 51 | } 52 | 53 | type appState struct { 54 | colour string 55 | nextPort int 56 | existingApp *marathon.Application 57 | resuming bool 58 | } 59 | 60 | type proxyInfo struct { 61 | hmap map[string]int 62 | backends [][]string 63 | instanceCount int 64 | } 65 | 66 | func NewBlueGreenClient(marathon marathon.Marathon, opts *BlueGreenOptions) BlueGreen { 67 | c := new(BGClient) 68 | c.marathon = marathon 69 | c.opts = opts 70 | c.http = httpclient.DefaultHttpClient() 71 | return c 72 | } 73 | 74 | func NewBlueGreenOptions() *BlueGreenOptions { 75 | opts := &BlueGreenOptions{} 76 | opts.InitialInstances = 1 77 | opts.ProxyWaitTimeout = time.Duration(300) * time.Second 78 | opts.StepDelay = time.Duration(6) * time.Second 79 | return opts 80 | } 81 | -------------------------------------------------------------------------------- /marathon/bluegreen/deployment.go: -------------------------------------------------------------------------------- 1 | package bluegreen 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/ContainX/depcon/marathon" 7 | "github.com/ContainX/depcon/pkg/logger" 8 | "os" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | const ( 14 | DeployGroup = "HAPROXY_DEPLOYMENT_GROUP" 15 | DeployGroupAltPort = "HAPROXY_DEPLOYMENT_ALT_PORT" 16 | DeployGroupColour = "HAPROXY_DEPLOYMENT_COLOUR" 17 | DeployProxyPort = "HAPROXY_0_PORT" 18 | DeployTargetInstances = "HAPROXY_DEPLOYMENT_TARGET_INSTANCES" 19 | DeployStartedAt = "HAPROXY_DEPLOYMENT_STARTED_AT" 20 | ProxyAppId = "HAPROXY_APP_ID" 21 | ColourBlue = "blue" 22 | ColourGreen = "green" 23 | ) 24 | 25 | var ( 26 | ErrorNoLabels = errors.New("No labels found. Please define the HAPROXY_DEPLOYMENT_GROUP and HAPROXY_DEPLOYMENT_ALT_PORT label") 27 | ErrorNoServicePortSet = errors.New("No service port set") 28 | LabelFormatErr = "Please define the %s label" 29 | log = logger.GetLogger("depcon.marathon.bg") 30 | ) 31 | 32 | func (c *BGClient) DeployBlueGreenFromFile(filename string) (*marathon.Application, error) { 33 | 34 | log.Debugf("Enter DeployBlueGreenFromFile") 35 | 36 | parseOpts := &marathon.CreateOptions{ 37 | ErrorOnMissingParams: c.opts.ErrorOnMissingParams, 38 | EnvParams: c.opts.EnvParams, 39 | } 40 | app, err := c.marathon.ParseApplicationFromFile(filename, parseOpts) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return c.DeployBlueGreen(app) 45 | } 46 | 47 | func (c *BGClient) DeployBlueGreen(app *marathon.Application) (*marathon.Application, error) { 48 | 49 | log.Debugf("Enter DeployBlueGreen") 50 | 51 | // Before we return the client lets make sure the LoadBalancer is properly defined 52 | c.isProxyAlive() 53 | 54 | if app.Labels == nil || len(app.Labels) == 0 { 55 | return nil, ErrorNoLabels 56 | } 57 | 58 | if !labelExists(app, DeployGroup) { 59 | return nil, fmt.Errorf(LabelFormatErr, DeployGroup) 60 | } 61 | 62 | if !labelExists(app, DeployGroupAltPort) { 63 | return nil, fmt.Errorf(LabelFormatErr, DeployGroupAltPort) 64 | } 65 | 66 | group := app.Labels[DeployGroup] 67 | groupAltPort, err := strconv.Atoi(app.Labels[DeployGroupAltPort]) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | app.Labels[ProxyAppId] = app.ID 73 | servicePort := findServicePort(app) 74 | 75 | if servicePort <= 0 { 76 | return nil, ErrorNoServicePortSet 77 | } 78 | 79 | state, err := c.bgAppInfo(group, groupAltPort) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | app = c.updateServicePort(app, state.nextPort) 85 | 86 | app.ID = formatIdentifier(app.ID, state.colour) 87 | 88 | if state.existingApp != nil { 89 | app.Instances = c.opts.InitialInstances 90 | app.Labels[DeployTargetInstances] = strconv.Itoa(state.existingApp.Instances) 91 | } else { 92 | app.Labels[DeployTargetInstances] = strconv.Itoa(app.Instances) 93 | } 94 | 95 | app.Labels[DeployGroupColour] = state.colour 96 | app.Labels[DeployStartedAt] = time.Now().Format(time.RFC3339) 97 | app.Labels[DeployProxyPort] = strconv.Itoa(servicePort) 98 | 99 | if c.opts.DryRun { 100 | return app, nil 101 | } 102 | 103 | c.startDeployment(app, state) 104 | 105 | return c.marathon.GetApplication(app.ID) 106 | } 107 | 108 | func (c *BGClient) updateServicePort(app *marathon.Application, port int) *marathon.Application { 109 | log.Debugf("Entering updateServicePort, port=%d", port) 110 | if app.Container != nil && app.Container.Docker != nil { 111 | if app.Container.Docker.PortMappings != nil && len(app.Container.Docker.PortMappings) > 0 { 112 | app.Container.Docker.PortMappings[0].ServicePort = port 113 | } 114 | if len(app.Ports) > 0 { 115 | app.Ports[0] = port 116 | } 117 | } 118 | return app 119 | } 120 | 121 | func (c *BGClient) startDeployment(app *marathon.Application, state *appState) bool { 122 | log.Debugf("startDeployment: resuming: %v", state.resuming) 123 | if !state.resuming { 124 | a, err := c.marathon.CreateApplication(app, true, false) 125 | if err != nil { 126 | log.Errorf("Unable to create application: %s", err.Error()) 127 | os.Exit(1) 128 | } 129 | app = a 130 | } 131 | if state.existingApp != nil { 132 | return c.checkIfTasksDrained(app, state.existingApp, time.Now()) 133 | } 134 | return false 135 | } 136 | 137 | func (c *BGClient) bgAppInfo(deployGroup string, deployGroupAltPort int) (*appState, error) { 138 | apps, err := c.marathon.ListApplications() 139 | 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | var existingApp marathon.Application 145 | 146 | colour := ColourBlue 147 | nextPort := deployGroupAltPort 148 | resume := false 149 | exists := false 150 | 151 | for _, app := range apps.Apps { 152 | log.Debugf("bgAppInfo: loop %s", app.ID) 153 | if len(app.Labels) <= 0 { 154 | continue 155 | } 156 | if labelExists(&app, DeployGroup) && labelExists(&app, DeployGroupColour) && app.Labels[DeployGroup] == deployGroup { 157 | if exists { 158 | if c.opts.Resume { 159 | log.Infof("Found previous deployment -- resuming") 160 | resume = true 161 | if deployStartTimeCompare(&existingApp, &app) == -1 { 162 | break 163 | } 164 | } else { 165 | return nil, errors.New("There appears to be an existing deployment in progress") 166 | } 167 | } 168 | prev_colour := app.Labels[DeployGroupColour] 169 | prev_port := app.Ports[0] 170 | 171 | log.Debugf("bgAppInfo: assigning %s to existing app: %s = %s", app.ID, app.Labels[DeployGroup], deployGroup) 172 | existingApp = app 173 | exists = true 174 | 175 | if prev_port == deployGroupAltPort { 176 | nextPort, _ = strconv.Atoi(app.Labels[DeployProxyPort]) 177 | } else { 178 | nextPort = deployGroupAltPort 179 | } 180 | 181 | if prev_colour == ColourBlue { 182 | colour = ColourGreen 183 | } else { 184 | colour = ColourBlue 185 | } 186 | } 187 | } 188 | 189 | as := &appState{ 190 | nextPort: nextPort, 191 | colour: colour, 192 | resuming: resume, 193 | } 194 | 195 | if exists { 196 | as.existingApp = &existingApp 197 | log.Debugf("bgAppInfo: Returning %s, np: %d, clr: %s", sprintApp(as.existingApp), as.nextPort, as.colour) 198 | } else { 199 | log.Debugf("bgAppInfo: Returning np: %d, clr: %s", as.nextPort, as.colour) 200 | } 201 | return as, nil 202 | } 203 | 204 | func sprintApp(a *marathon.Application) string { 205 | return fmt.Sprintf("[id: %s, i: %d, lbls: %v]", a.ID, a.Instances, a.Labels) 206 | } 207 | -------------------------------------------------------------------------------- /marathon/bluegreen/haproxy.go: -------------------------------------------------------------------------------- 1 | package bluegreen 2 | 3 | import ( 4 | "encoding/csv" 5 | "fmt" 6 | "github.com/ContainX/depcon/marathon" 7 | "github.com/ContainX/depcon/utils" 8 | "io" 9 | "math" 10 | "net" 11 | "net/url" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | const ( 19 | HAProxyStatsQP = "/haproxy?stats;csv" 20 | HAProxyPidsQP = "/_haproxy_getpids" 21 | BackendRE = `(?i)^(\d+)_(\d+)_(\d+)_(\d+)_(\d+)$` 22 | ) 23 | 24 | // Simple HTTP Test to determine if the current LB is the correct URL. Better to test this before we modify Marathon with this 25 | // existing deployment 26 | func (c *BGClient) isProxyAlive() { 27 | resp := c.http.HttpGet(c.opts.LoadBalancer+HAProxyStatsQP, nil) 28 | if resp.Error != nil { 29 | log.Fatalf("HAProxy is not responding or is invalid or Stats service not enabled.\n\n", resp.Error.Error()) 30 | } 31 | } 32 | 33 | func (c *BGClient) checkIfTasksDrained(app, existingApp *marathon.Application, stepStartedAt time.Time) bool { 34 | time.Sleep(c.opts.StepDelay) 35 | 36 | existingApp = c.refreshAppOrPanic(existingApp.ID) 37 | app = c.refreshAppOrPanic(app.ID) 38 | 39 | targetInstances, _ := strconv.Atoi(app.Labels[DeployTargetInstances]) 40 | log.Infof("Existing app running %d instance, new app running %d instances", existingApp.Instances, app.Instances) 41 | 42 | hosts, err := proxiesFromURI(c.opts.LoadBalancer) 43 | if err != nil { 44 | log.Errorf("Error with HAProxy Stats URL: %s", err.Error()) 45 | } 46 | 47 | var errCaught error = nil 48 | var csvData string 49 | 50 | for _, h := range hosts { 51 | log.Debugf("Querying HAProxy stats: %s", h+HAProxyStatsQP) 52 | resp := c.http.HttpGet(h+HAProxyStatsQP, nil) 53 | if resp.Error != nil { 54 | errCaught = resp.Error 55 | } else { 56 | csvData = csvData + resp.Content 57 | } 58 | 59 | resp = c.http.HttpGet(h+HAProxyPidsQP, nil) 60 | if resp.Error != nil { 61 | errCaught = resp.Error 62 | } else { 63 | pids := strings.Split(resp.Content, " ") 64 | log.Debugf("Pids: %v, length: %d, time constraint: %v", pids, len(pids), (time.Now().Sub(stepStartedAt) < c.opts.StepDelay)) 65 | if len(pids) > 1 && time.Now().Sub(stepStartedAt) < c.opts.StepDelay { 66 | log.Info("Waiting for %d, pids on %s", len(pids), h) 67 | return c.checkIfTasksDrained(app, existingApp, stepStartedAt) 68 | } 69 | } 70 | 71 | if errCaught != nil { 72 | log.Warningf("Caught error when retrieving HAProxy stats from %s: Error (%s)", h, errCaught.Error()) 73 | return c.checkIfTasksDrained(app, existingApp, stepStartedAt) 74 | } 75 | } 76 | 77 | pinfo := parseProxyBackends(csvData, app) 78 | if len(pinfo.backends)/pinfo.instanceCount != (app.Instances + existingApp.Instances) { 79 | log.Debugf("HAProxy hasn't updated: %d / %d != (%d + %d)", len(pinfo.backends), pinfo.instanceCount, app.Instances, existingApp.Instances) 80 | // HAProxy hasn't updated yet, try again 81 | return c.checkIfTasksDrained(app, existingApp, stepStartedAt) 82 | } 83 | 84 | backendsUp := backendsForStatus(pinfo, "UP") 85 | if len(backendsUp)/pinfo.instanceCount < targetInstances { 86 | log.Debugf("Waiting until health state: %d / %d < %d", len(backendsUp), pinfo.instanceCount, targetInstances) 87 | // Wait until we're in a health state 88 | return c.checkIfTasksDrained(app, existingApp, stepStartedAt) 89 | } 90 | 91 | // Double check that current draining backends are finished serving requests 92 | backendsDrained := backendsForStatus(pinfo, "MAINT") 93 | if len(backendsDrained)/pinfo.instanceCount < 1 { 94 | log.Debugf("No backends have started draining yet: %d / %d < 1", len(backendsDrained), pinfo.instanceCount) 95 | // No backends have started draining yet 96 | return c.checkIfTasksDrained(app, existingApp, stepStartedAt) 97 | } 98 | 99 | for _, be := range backendsDrained { 100 | // Verify that the backends have no sessions or pending connections. 101 | // This is likely overkill, but we'll do it anyway to be safe. 102 | if intOrZero(string(be[pinfo.hmap["qcur"]])) > 0 || intOrZero(string(be[pinfo.hmap["scur"]])) > 0 { 103 | // Backends are not yet defined 104 | return c.checkIfTasksDrained(app, existingApp, stepStartedAt) 105 | } 106 | } 107 | 108 | // If we made it here, all the backends are drained and we can start removing tasks, with prejudice 109 | hostPorts := hostPortsFromBackends(pinfo.hmap, backendsDrained, pinfo.instanceCount) 110 | tasksToKill := findTasksToKill(existingApp.Tasks, hostPorts) 111 | 112 | log.Infof("There are %d drained backends, about to kill & scale for these tasks:\n%s", len(tasksToKill), strings.Join(tasksToKill, "\n")) 113 | 114 | if app.Instances == targetInstances && len(tasksToKill) == existingApp.Instances { 115 | log.Infof("About to delete old app %s", existingApp.ID) 116 | if _, err := c.marathon.DestroyApplication(existingApp.ID); err != nil { 117 | return false 118 | } 119 | return true 120 | } 121 | 122 | // Scale new app up 123 | instances := int(math.Floor(float64(app.Instances + (app.Instances+1)/2))) 124 | if instances >= existingApp.Instances { 125 | instances = targetInstances 126 | } 127 | log.Infof("Scaling new app up to %d instances", instances) 128 | if _, err := c.marathon.ScaleApplication(app.ID, instances); err != nil { 129 | panic("Failed to scale application: " + err.Error()) 130 | } 131 | 132 | //Scale old app down 133 | log.Info("Scaling old app down to %d instances", len(tasksToKill)) 134 | if err := c.marathon.KillTasksAndScale(tasksToKill...); err != nil { 135 | log.Errorf("Failure killing tasks: %v", tasksToKill) 136 | } 137 | return c.checkIfTasksDrained(app, existingApp, time.Now()) 138 | 139 | } 140 | 141 | func findTasksToKill(tasks []*marathon.Task, hostPorts map[string][]int) []string { 142 | tasksToKill := map[string]string{} 143 | for _, task := range tasks { 144 | if _, ok := hostPorts[task.Host]; ok { 145 | for _, p := range hostPorts[task.Host] { 146 | if utils.IntInSlice(p, task.Ports) { 147 | tasksToKill[task.ID] = task.ID 148 | } 149 | } 150 | } 151 | } 152 | return utils.MapStringKeysToSlice(tasksToKill) 153 | } 154 | 155 | func hostPortsFromBackends(hmap map[string]int, backends [][]string, instanceCount int) map[string][]int { 156 | regex := regexp.MustCompile(BackendRE) 157 | counts := map[string]int{} 158 | hostPorts := map[string][]int{} 159 | 160 | for _, be := range backends { 161 | svname := string(be[hmap["svname"]]) 162 | if _, ok := counts[svname]; ok { 163 | counts[svname] += 1 164 | } else { 165 | counts[svname] = 1 166 | } 167 | 168 | if counts[svname] == instanceCount { 169 | if regex.MatchString(svname) { 170 | m := regex.FindStringSubmatch(svname) 171 | host := strings.Join(m[1:5], ".") 172 | port := m[5] 173 | 174 | if _, ok := hostPorts[host]; ok { 175 | hostPorts[host] = append(hostPorts[host], intOrZero(port)) 176 | } else { 177 | hostPorts[host] = []int{intOrZero(port)} 178 | } 179 | } 180 | } 181 | } 182 | return hostPorts 183 | } 184 | 185 | func backendsForStatus(pinfo *proxyInfo, status string) [][]string { 186 | var results [][]string 187 | for _, b := range pinfo.backends { 188 | if b[pinfo.hmap["status"]] == status { 189 | results = append(results, b) 190 | } 191 | } 192 | return results 193 | } 194 | 195 | func parseProxyBackends(data string, app *marathon.Application) *proxyInfo { 196 | 197 | pi := &proxyInfo{ 198 | instanceCount: 0, 199 | hmap: map[string]int{}, 200 | backends: make([][]string, 0), 201 | } 202 | 203 | var headers []string 204 | 205 | r := csv.NewReader(strings.NewReader(data)) 206 | for { 207 | row, err := r.Read() 208 | if err == io.EOF { 209 | break 210 | } 211 | if err != nil { 212 | log.Fatal(err) 213 | } 214 | 215 | if []rune(row[0])[0] == '#' { 216 | headers = row 217 | pi.instanceCount += 1 218 | continue 219 | } 220 | 221 | if row[0] == fmt.Sprintf("%s_%s", app.Labels[DeployGroup], app.Labels[DeployProxyPort]) && notBackFrontend(row[1]) { 222 | pi.backends = append(pi.backends, row) 223 | } 224 | } 225 | 226 | log.Infof("Found %d backends across %d HAProxy instances", len(pi.backends), pi.instanceCount) 227 | 228 | // Create header map of column to index 229 | for i := 0; i < len(headers); i++ { 230 | pi.hmap[headers[i]] = i 231 | } 232 | return pi 233 | } 234 | 235 | func notBackFrontend(value string) bool { 236 | return value != "BACKEND" && value != "FRONTEND" 237 | } 238 | 239 | func (c *BGClient) refreshAppOrPanic(id string) *marathon.Application { 240 | log.Debugf("Enter: refreshAppOrPanic -> %s", id) 241 | // Retry in case of minor network errors 242 | for i := 0; i < 3; i++ { 243 | if a, err := c.marathon.GetApplication(id); err != nil { 244 | log.Errorf("Error refresh app info: %s, Will retry %d more times before giving up", err.Error(), 3-(i+1)) 245 | time.Sleep(time.Duration(3) * time.Second) 246 | } else { 247 | log.Debugf("refreshAppOrPanic: returning %s", sprintApp(a)) 248 | return a 249 | } 250 | } 251 | panic("Failure to refresh application " + id) 252 | } 253 | 254 | func proxiesFromURI(uri string) ([]string, error) { 255 | url, err := url.Parse(uri) 256 | if err != nil { 257 | return nil, err 258 | } 259 | hp := strings.Split(url.Host, ":") 260 | host := hp[0] 261 | port := hp[1] 262 | 263 | ips, err := net.LookupIP(host) 264 | if err != nil { 265 | log.Debugf("Lookup IP failed for: %s, error: %s", host, err.Error()) 266 | return []string{url.String()}, nil 267 | } 268 | 269 | results := []string{} 270 | for _, ip := range ips { 271 | url.Host = ip.String() + ":" + port 272 | results = append(results, url.String()) 273 | } 274 | return results, nil 275 | } 276 | -------------------------------------------------------------------------------- /marathon/bluegreen/util.go: -------------------------------------------------------------------------------- 1 | package bluegreen 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ContainX/depcon/marathon" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | func deployStartTimeCompare(existingApp, currentApp *marathon.Application) int { 11 | if labelExists(existingApp, DeployStartedAt) && labelExists(currentApp, DeployStartedAt) { 12 | return strings.Compare(existingApp.Labels[DeployStartedAt], currentApp.Labels[DeployStartedAt]) 13 | } 14 | return 0 15 | } 16 | 17 | func findServicePort(app *marathon.Application) int { 18 | if app.Container != nil && len(app.Container.Docker.PortMappings) > 0 { 19 | return app.Container.Docker.PortMappings[0].ServicePort 20 | } 21 | if len(app.Ports) > 0 { 22 | return app.Ports[0] 23 | } 24 | return 0 25 | } 26 | 27 | func labelExists(app *marathon.Application, label string) bool { 28 | _, exist := app.Labels[label] 29 | return exist 30 | } 31 | 32 | func formatIdentifier(appId, colour string) string { 33 | id := fmt.Sprintf("%s-%s", appId, colour) 34 | if []rune(id)[0] != '/' { 35 | id = "/" + id 36 | } 37 | return id 38 | } 39 | 40 | func intOrZero(s string) int { 41 | if v, err := strconv.Atoi(s); err != nil { 42 | return 0 43 | } else { 44 | return v 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /marathon/deployment.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/ContainX/depcon/pkg/httpclient" 7 | "strings" 8 | ) 9 | 10 | func (c *MarathonClient) ListDeployments() ([]*Deploy, error) { 11 | var deploys []*Deploy 12 | resp := c.http.HttpGet(c.marathonUrl(API_DEPLOYMENTS), &deploys) 13 | if resp.Error != nil { 14 | return nil, resp.Error 15 | } 16 | return deploys, nil 17 | } 18 | 19 | func (c *MarathonClient) HasDeployment(id string) (bool, error) { 20 | deployments, err := c.ListDeployments() 21 | if err != nil { 22 | return false, err 23 | } 24 | for _, deployment := range deployments { 25 | if deployment.DeployID == id { 26 | return true, nil 27 | } 28 | } 29 | return false, nil 30 | } 31 | 32 | func (c *MarathonClient) DeleteDeployment(id string, force bool) (*DeploymentID, error) { 33 | deploymentID := new(DeploymentID) 34 | uri := fmt.Sprintf("%s?force=%v", c.marathonUrl(API_DEPLOYMENTS, id), force) 35 | resp := c.http.HttpDelete(uri, nil, deploymentID) 36 | if resp.Error != nil { 37 | if resp.Error == httpclient.ErrorNotFound { 38 | return nil, errors.New(fmt.Sprintf("Deployment '%s' was not found", id)) 39 | } 40 | return nil, resp.Error 41 | } 42 | return deploymentID, nil 43 | } 44 | 45 | func (c *MarathonClient) CancelAppDeployment(appId string, matchPrefix bool) (*DeploymentID, error) { 46 | if deployments, err := c.ListDeployments(); err == nil { 47 | for _, value := range deployments { 48 | for _, id := range value.AffectedApps { 49 | if doesIDMatch(appId, id, matchPrefix) { 50 | log.Infof("Removing matched deployment: %s for app: %s", value.DeployID, id) 51 | return c.DeleteDeployment(value.DeployID, true) 52 | } 53 | } 54 | } 55 | } else { 56 | return nil, err 57 | } 58 | return nil, nil 59 | } 60 | 61 | func doesIDMatch(appId, otherId string, matchPrefix bool) bool { 62 | if matchPrefix { 63 | return strings.HasPrefix(otherId, appId) 64 | } 65 | return appId == otherId 66 | } 67 | -------------------------------------------------------------------------------- /marathon/error.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | ErrorTimeout = errors.New("The operation has timed out") 9 | ErrorDeploymentNotfound = errors.New("Failed to get deployment in allocated time") 10 | ) 11 | -------------------------------------------------------------------------------- /marathon/event.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/ContainX/depcon/pkg/encoding" 9 | "github.com/donovanhide/eventsource" 10 | ) 11 | 12 | func (c *MarathonClient) CreateEventStreamListener(channel EventsChannel, filter int) error { 13 | c.Lock() 14 | defer c.Unlock() 15 | 16 | // no-op if already listening on a stream 17 | if c.eventStreamState != nil { 18 | return nil 19 | } 20 | 21 | go func() { 22 | for { 23 | stream, err := c.setupSSEStream() 24 | if err != nil { 25 | log.Debugf("error connecting to SSE subscription: %s", err.Error()) 26 | <-time.After(5 * time.Second) 27 | continue 28 | } 29 | err = c.listenToSSE(stream) 30 | if err != nil { 31 | log.Errorf("error on SSE subscription: %s", err) 32 | } 33 | stream.Close() 34 | } 35 | }() 36 | 37 | c.eventStreamState = &EventStreamState{ 38 | channel: channel, 39 | filter: filter, 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func (c *MarathonClient) CloseEventStreamListener(channel EventsChannel) { 46 | c.Lock() 47 | defer c.Unlock() 48 | 49 | c.eventStreamState = nil 50 | } 51 | 52 | func (c *MarathonClient) setupSSEStream() (*eventsource.Stream, error) { 53 | request, err := c.http.CreateHttpRequest(http.MethodGet, c.marathonUrl(API_EVENTS), nil) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | stream, err := eventsource.SubscribeWith("", http.DefaultClient, request) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | return stream, nil 64 | } 65 | 66 | func (c *MarathonClient) listenToSSE(stream *eventsource.Stream) error { 67 | for { 68 | select { 69 | case ev := <-stream.Events: 70 | if err := c.handleStreamEvent(ev.Data()); err != nil { 71 | log.Errorf("error setting up SSE Stream: %v", err) 72 | } 73 | case err := <-stream.Errors: 74 | log.Errorf("error setting up SSE Stream: %v", err) 75 | } 76 | } 77 | } 78 | 79 | func (c *MarathonClient) handleStreamEvent(data string) error { 80 | if data == "" { 81 | return nil 82 | } 83 | 84 | eventType := new(EventType) 85 | 86 | if err := encoding.DefaultJSONEncoder().UnMarshalStr(data, eventType); err != nil { 87 | return fmt.Errorf("failed to decode event, content: %s, error: %s", data, err) 88 | } 89 | 90 | event, err := c.GetEvent(eventType.EventType) 91 | if err != nil { 92 | return fmt.Errorf("unable to handle event type, type: %s, error: %s", eventType.EventType, err) 93 | } 94 | 95 | if err := encoding.DefaultJSONEncoder().UnMarshalStr(data, event.Event); err != nil { 96 | return fmt.Errorf("failed to decode event, id: %d, error: %s", event.ID, err) 97 | } 98 | 99 | if event.ID&c.eventStreamState.filter != 0 { 100 | go func(ch EventsChannel, e *Event) { 101 | ch <- e 102 | }(c.eventStreamState.channel, event) 103 | } 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /marathon/group.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ContainX/depcon/pkg/encoding" 6 | "github.com/ContainX/depcon/pkg/envsubst" 7 | "github.com/ContainX/depcon/pkg/httpclient" 8 | "io" 9 | "os" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | func (c *MarathonClient) CreateGroupFromString(filename string, grpstr string, opts *CreateOptions) (*Group, error) { 15 | et, err := encoding.EncoderTypeFromExt(filename) 16 | if err != nil { 17 | return nil, err 18 | } 19 | group, err := c.ParseGroupFromString(strings.NewReader(grpstr), et, opts) 20 | 21 | if err != nil { 22 | return group, err 23 | } 24 | 25 | if opts.StopDeploy { 26 | if deployment, err := c.CancelAppDeployment(group.GroupID, true); err == nil && deployment != nil { 27 | c.logOutput(log.Infof, "Previous deployment found.. cancelling and waiting until complete.") 28 | c.WaitForDeployment(deployment.DeploymentID, time.Second*30) 29 | } 30 | } 31 | 32 | return c.CreateGroup(group, opts.Wait, opts.Force) 33 | } 34 | 35 | func (c *MarathonClient) CreateGroupFromFile(filename string, opts *CreateOptions) (*Group, error) { 36 | log.Infof("Creating Group from file: %s", filename) 37 | 38 | group, err := c.ParseGroupFromFile(filename, opts) 39 | if err != nil { 40 | return group, err 41 | } 42 | 43 | if opts.StopDeploy { 44 | if deployment, err := c.CancelAppDeployment(group.GroupID, true); err == nil && deployment != nil { 45 | log.Infof("Previous deployment found.. cancelling and waiting until complete.") 46 | c.WaitForDeployment(deployment.DeploymentID, time.Second*30) 47 | } 48 | } 49 | 50 | return c.CreateGroup(group, opts.Wait, opts.Force) 51 | } 52 | 53 | func (c *MarathonClient) ParseGroupFromFile(filename string, opts *CreateOptions) (*Group, error) { 54 | log.Infof("Creating Group from file: %s", filename) 55 | 56 | file, err := os.Open(filename) 57 | if err != nil { 58 | return nil, fmt.Errorf("Error opening filename %s, %s", filename, err.Error()) 59 | } 60 | 61 | if et, err := encoding.EncoderTypeFromExt(filename); err != nil { 62 | return nil, err 63 | } else { 64 | return c.ParseGroupFromString(file, et, opts) 65 | } 66 | } 67 | 68 | func (c *MarathonClient) ParseGroupFromString(r io.Reader, et encoding.EncoderType, opts *CreateOptions) (*Group, error) { 69 | 70 | options := initCreateOptions(opts) 71 | 72 | var encoder encoding.Encoder 73 | var err error 74 | 75 | encoder, err = encoding.NewEncoder(et) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | parsed, missing := envsubst.SubstFileTokens(r, options.EnvParams) 81 | 82 | if opts.ErrorOnMissingParams && missing { 83 | return nil, ErrorAppParamsMissing 84 | } 85 | 86 | if opts.DryRun { 87 | fmt.Printf("Create Group :: DryRun :: Template Output\n\n%s", parsed) 88 | os.Exit(0) 89 | } 90 | 91 | group := new(Group) 92 | err = encoder.UnMarshalStr(parsed, &group) 93 | if err != nil { 94 | return nil, err 95 | } 96 | return group, nil 97 | } 98 | 99 | func (c *MarathonClient) CreateGroup(group *Group, wait, force bool) (*Group, error) { 100 | c.logOutput(log.Infof, "Creating Group '%s', wait: %v, force: %v", group.GroupID, wait, force) 101 | result := new(DeploymentID) 102 | resp := c.http.HttpPost(c.marathonUrl(API_GROUPS), group, result) 103 | if resp.Error != nil { 104 | if resp.Error == httpclient.ErrorMessage { 105 | if resp.Status == 409 { 106 | if force { 107 | return c.UpdateGroup(group, wait) 108 | } 109 | return nil, ErrorGroupExists 110 | } 111 | if resp.Status == 422 { 112 | return nil, ErrorInvalidGroupId 113 | } 114 | return nil, fmt.Errorf("Error occurred (Status %v) Body -> %s", resp.Status, resp.Content) 115 | } 116 | return nil, resp.Error 117 | } 118 | if wait { 119 | if err := c.WaitForDeployment(result.DeploymentID, time.Duration(500)*time.Second); err != nil { 120 | return nil, err 121 | } 122 | } 123 | return group, nil 124 | } 125 | 126 | func (c *MarathonClient) UpdateGroup(group *Group, wait bool) (*Group, error) { 127 | log.Info("Update Group '%s', wait = %v", group.GroupID, wait) 128 | result := new(DeploymentID) 129 | resp := c.http.HttpPut(c.marathonUrl(API_GROUPS), group, result) 130 | 131 | if resp.Error != nil { 132 | if resp.Error == httpclient.ErrorMessage { 133 | if resp.Status == 422 { 134 | return nil, ErrorGroupAppExists 135 | } 136 | } 137 | return nil, resp.Error 138 | } 139 | if wait { 140 | if err := c.WaitForDeployment(result.DeploymentID, c.determineTimeout(nil)); err != nil { 141 | return nil, err 142 | } 143 | } 144 | // Get the latest version of the application to return 145 | return c.GetGroup(group.GroupID) 146 | } 147 | 148 | func (c *MarathonClient) ListGroups() (*Groups, error) { 149 | groups := new(Groups) 150 | 151 | resp := c.http.HttpGet(c.marathonUrl(API_GROUPS), groups) 152 | if resp.Error != nil { 153 | return nil, resp.Error 154 | } 155 | return groups, nil 156 | } 157 | 158 | func (c *MarathonClient) GetGroup(id string) (*Group, error) { 159 | group := new(Group) 160 | resp := c.http.HttpGet(c.marathonUrl(API_GROUPS, id), group) 161 | if resp.Error != nil { 162 | return nil, resp.Error 163 | } 164 | return group, nil 165 | } 166 | 167 | func (c *MarathonClient) DestroyGroup(id string) (*DeploymentID, error) { 168 | deploymentId := new(DeploymentID) 169 | resp := c.http.HttpDelete(fmt.Sprintf("%s?force=true", c.marathonUrl(API_GROUPS, id)), nil, deploymentId) 170 | if resp.Error != nil { 171 | return nil, resp.Error 172 | } 173 | return deploymentId, nil 174 | } 175 | -------------------------------------------------------------------------------- /marathon/group_test.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | "github.com/ContainX/depcon/pkg/mockrest" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | const ( 10 | GroupsFolder = "testdata/groups/" 11 | ) 12 | 13 | func TestListGroups(t *testing.T) { 14 | s := mockrest.StartNewWithFile(GroupsFolder + "list_groups_response.json") 15 | defer s.Stop() 16 | 17 | c := NewMarathonClient(s.URL, "", "", "") 18 | groups, err := c.ListGroups() 19 | 20 | assert.Nil(t, err, "Error response was not expected") 21 | assert.Equal(t, 1, len(groups.Groups), "Expected 1 nested group") 22 | assert.Equal(t, 0, len(groups.Apps), "Expected 0 top level apps") 23 | } 24 | 25 | func TestGetGroup(t *testing.T) { 26 | s := mockrest.StartNewWithFile(GroupsFolder + "get_group_response.json") 27 | defer s.Stop() 28 | 29 | c := NewMarathonClient(s.URL, "", "", "") 30 | group, err := c.GetGroup("/sites") 31 | 32 | assert.Nil(t, err, "Error response was not expected") 33 | assert.Equal(t, "/sites", group.GroupID, "Expected /sites identifier") 34 | } 35 | 36 | func TestDestroyGroup(t *testing.T) { 37 | s := mockrest.StartNewWithFile(CommonFolder + "deployid_response.json") 38 | defer s.Stop() 39 | 40 | c := NewMarathonClient(s.URL, "", "", "") 41 | depId, err := c.DestroyGroup("/sites") 42 | assert.Nil(t, err, "Error response was not expected") 43 | assert.Equal(t, "5ed4c0c5-9ff8-4a6f-a0cd-f57f59a34b43", depId.DeploymentID) 44 | } 45 | -------------------------------------------------------------------------------- /marathon/marathon_test.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import "testing" 4 | 5 | func TestNewMarathonClient(t *testing.T) { 6 | opts := &MarathonOptions{} 7 | client := createMarathonClient("username", "password", "", opts, "localhost") 8 | 9 | if client == nil { 10 | t.Error() 11 | } 12 | 13 | var i interface{} = client 14 | _, ok := i.(*MarathonClient) 15 | if !ok { 16 | t.Error() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /marathon/server.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import "net/url" 4 | 5 | func (c *MarathonClient) GetMarathonInfo() (*MarathonInfo, error) { 6 | info := new(MarathonInfo) 7 | 8 | resp := c.http.HttpGet(c.marathonUrl(API_INFO), info) 9 | if resp.Error != nil { 10 | return nil, resp.Error 11 | } 12 | return info, nil 13 | } 14 | 15 | func (c *MarathonClient) GetCurrentLeader() (*LeaderInfo, error) { 16 | info := new(LeaderInfo) 17 | 18 | resp := c.http.HttpGet(c.marathonUrl(API_LEADER), info) 19 | if resp.Error != nil { 20 | return nil, resp.Error 21 | } 22 | return info, nil 23 | } 24 | 25 | func (c *MarathonClient) AbdicateLeader() (*Message, error) { 26 | msg := new(Message) 27 | resp := c.http.HttpDelete(c.marathonUrl(API_LEADER), nil, msg) 28 | if resp.Error != nil { 29 | return nil, resp.Error 30 | } 31 | return msg, nil 32 | } 33 | 34 | func (c *MarathonClient) Ping() (*MarathonPing, error) { 35 | resp := c.http.HttpGet(c.marathonUrl(API_PING), nil) 36 | if resp.Error != nil { 37 | return nil, resp.Error 38 | } 39 | host := c.getHost() 40 | if u, err := url.Parse(host); err == nil { 41 | host = u.Host 42 | } 43 | 44 | return &MarathonPing{Host: host, Elapsed: resp.Elapsed}, nil 45 | } 46 | -------------------------------------------------------------------------------- /marathon/task.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | func (c *MarathonClient) ListTasks() ([]*Task, error) { 9 | tasks := new(Tasks) 10 | resp := c.http.HttpGet(c.marathonUrl(API_TASKS), &tasks) 11 | if resp.Error != nil { 12 | return nil, resp.Error 13 | } 14 | return tasks.Tasks, nil 15 | } 16 | 17 | func (c *MarathonClient) KillAppTasks(id string, host string, scale bool) ([]*Task, error) { 18 | tasks := new(Tasks) 19 | 20 | url := c.marathonUrl(API_APPS, id, PathTasks) 21 | if host != "" || scale { 22 | if host == "" { 23 | url = fmt.Sprintf("%s?scale=%v", url, scale) 24 | } else { 25 | url = fmt.Sprintf("%s?host=%s&scale=%v", url, host, scale) 26 | } 27 | } 28 | resp := c.http.HttpDelete(url, nil, tasks) 29 | if resp.Error != nil { 30 | return nil, resp.Error 31 | } 32 | return tasks.Tasks, nil 33 | } 34 | 35 | func (c *MarathonClient) KillAppTask(taskId string, scale bool) (*Task, error) { 36 | task := new(Task) 37 | app := taskId[0:strings.LastIndex(taskId, ".")] 38 | url := c.marathonUrl(API_APPS, app, PathTasks, taskId) 39 | 40 | if scale { 41 | url = fmt.Sprintf("%s?scale=%v", url, scale) 42 | } 43 | resp := c.http.HttpDelete(url, nil, task) 44 | if resp.Error != nil { 45 | return nil, resp.Error 46 | } 47 | return task, nil 48 | } 49 | 50 | func (c *MarathonClient) KillTasksAndScale(ids ...string) error { 51 | tasks := new(KillTasksScale) 52 | tasks.IDs = ids 53 | 54 | url := c.marathonUrl(API_TASKS_DELETE) 55 | url = fmt.Sprintf("%s?scale=true", url) 56 | resp := c.http.HttpPost(url, tasks, &Tasks{}) 57 | 58 | if resp.Error != nil { 59 | log.Error(resp.Error.Error()) 60 | return resp.Error 61 | } 62 | return nil 63 | } 64 | 65 | func (c *MarathonClient) GetTasks(id string) ([]*Task, error) { 66 | tasks := new(Tasks) 67 | resp := c.http.HttpGet(c.marathonUrl(API_APPS, id, PathTasks), &tasks) 68 | if resp.Error != nil { 69 | return nil, resp.Error 70 | } 71 | return tasks.Tasks, nil 72 | } 73 | 74 | func (c *MarathonClient) ListQueue() (*Queue, error) { 75 | q := new(Queue) 76 | resp := c.http.HttpGet(c.marathonUrl(API_QUEUE), &q) 77 | if resp.Error != nil { 78 | return nil, resp.Error 79 | } 80 | return q, nil 81 | } 82 | -------------------------------------------------------------------------------- /marathon/testdata/apps/app_params.json: -------------------------------------------------------------------------------- 1 | { 2 | "acceptedResourceRoles": ["*"], 3 | "args": [], 4 | "container": { 5 | "type": "DOCKER", 6 | "docker": { 7 | "network": "BRIDGE", 8 | "portMappings": [ 9 | {"containerPort": 9100, "hostPort": 0, "protocol": "tcp"} 10 | ], 11 | "image": "prom/node-exporter:${NODE_EXPORTER_VERSION}" 12 | }, 13 | "volumes": [ ] 14 | }, 15 | "env": { 16 | "CONSUL_CONNECT": "consul.service.consul:8500" 17 | }, 18 | "labels": { 19 | "tags": "prom-metrics" 20 | }, 21 | "constraints": [["hostname", "UNIQUE"]], 22 | "id": "node-exporter", 23 | "instances": 2, 24 | "cpus": 0.1, 25 | "mem": 64, 26 | "requirePorts": true, 27 | "ports": [9100], 28 | "upgradeStrategy": { 29 | "minimumHealthCapacity": 0 30 | }, 31 | "healthChecks": [{ 32 | "protocol": "HTTP", 33 | "portIndex": 0, 34 | "path": "/", 35 | "gracePeriodSeconds": 10, 36 | "intervalSeconds": 30, 37 | "maxConsecutiveFailures": 3 38 | }] 39 | } -------------------------------------------------------------------------------- /marathon/testdata/apps/get_app_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "id": "/storage/redis-x", 4 | "cmd": null, 5 | "args": null, 6 | "user": null, 7 | "env": { 8 | "TAGS": "storage,redis,cache" 9 | }, 10 | "instances": 1, 11 | "cpus": 0.1, 12 | "mem": 30, 13 | "disk": 0, 14 | "executor": "", 15 | "constraints": [ 16 | [ 17 | "hostname", 18 | "UNIQUE" 19 | ] 20 | ], 21 | "uris": [], 22 | "fetch": [], 23 | "storeUrls": [], 24 | "ports": [ 25 | 7777 26 | ], 27 | "requirePorts": false, 28 | "backoffSeconds": 1, 29 | "backoffFactor": 1.15, 30 | "maxLaunchDelaySeconds": 3600, 31 | "container": { 32 | "type": "DOCKER", 33 | "volumes": [], 34 | "docker": { 35 | "image": "redis", 36 | "network": "BRIDGE", 37 | "portMappings": [ 38 | { 39 | "containerPort": 6379, 40 | "hostPort": 7000, 41 | "servicePort": 7777, 42 | "protocol": "tcp" 43 | } 44 | ], 45 | "privileged": false, 46 | "parameters": [ 47 | { 48 | "key": "label", 49 | "value": "Name=storage" 50 | }, 51 | { 52 | "key": "label", 53 | "value": "System=cache" 54 | } 55 | ], 56 | "forcePullImage": false 57 | } 58 | }, 59 | "healthChecks": [ 60 | { 61 | "protocol": "TCP", 62 | "portIndex": 0, 63 | "gracePeriodSeconds": 50, 64 | "intervalSeconds": 30, 65 | "timeoutSeconds": 20, 66 | "maxConsecutiveFailures": 10, 67 | "ignoreHttp1xx": false 68 | } 69 | ], 70 | "dependencies": [], 71 | "upgradeStrategy": { 72 | "minimumHealthCapacity": 1, 73 | "maximumOverCapacity": 1 74 | }, 75 | "labels": { 76 | "HAPROXY_0_MODE": "tcp", 77 | "HAPROXY_GROUP": "external", 78 | "group": "storage", 79 | "role": "cache" 80 | }, 81 | "acceptedResourceRoles": null, 82 | "ipAddress": null, 83 | "version": "2016-04-04T05:38:27.077Z", 84 | "versionInfo": { 85 | "lastScalingAt": "2016-04-04T05:38:27.077Z", 86 | "lastConfigChangeAt": "2016-04-04T05:38:27.077Z" 87 | }, 88 | "tasksStaged": 0, 89 | "tasksRunning": 1, 90 | "tasksHealthy": 1, 91 | "tasksUnhealthy": 0, 92 | "deployments": [], 93 | "tasks": [ 94 | { 95 | "id": "storage_redisbg.749505e2-fa27-11e5-ad09-bc764e063d34", 96 | "host": "1.1.1.1", 97 | "ipAddresses": [], 98 | "ports": [ 99 | 7000 100 | ], 101 | "startedAt": "2016-04-04T05:38:33.447Z", 102 | "stagedAt": "2016-04-04T05:38:27.126Z", 103 | "version": "2016-04-04T05:38:27.077Z", 104 | "slaveId": "c89792fd-f90a-400d-8465-fddddf72969b-S0", 105 | "appId": "/storage/redis-x", 106 | "healthCheckResults": [ 107 | { 108 | "alive": true, 109 | "consecutiveFailures": 0, 110 | "firstSuccess": "2016-04-04T05:38:57.133Z", 111 | "lastFailure": null, 112 | "lastSuccess": "2016-04-06T05:47:51.182Z", 113 | "taskId": "storage_redisbg.749505e2-fa27-11e5-ad09-bc764e063d34" 114 | } 115 | ] 116 | } 117 | ] 118 | } 119 | } -------------------------------------------------------------------------------- /marathon/testdata/apps/list_apps_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "id": "/myapp", 5 | "cmd": "env && sleep 60", 6 | "args": null, 7 | "user": null, 8 | "env": { 9 | "LD_LIBRARY_PATH": "/usr/local/lib/myLib" 10 | }, 11 | "instances": 3, 12 | "cpus": 0.1, 13 | "mem": 5, 14 | "disk": 0, 15 | "executor": "", 16 | "constraints": [ 17 | [ 18 | "hostname", 19 | "UNIQUE", 20 | "" 21 | ] 22 | ], 23 | "uris": [ 24 | "https://raw.github.com/mesosphere/marathon/master/README.md" 25 | ], 26 | "storeUrls": [], 27 | "ports": [ 28 | 10013, 29 | 10015 30 | ], 31 | "requirePorts": false, 32 | "backoffSeconds": 1, 33 | "backoffFactor": 1.15, 34 | "maxLaunchDelaySeconds": 3600, 35 | "container": null, 36 | "healthChecks": [], 37 | "dependencies": [], 38 | "upgradeStrategy": { 39 | "minimumHealthCapacity": 1, 40 | "maximumOverCapacity": 1 41 | }, 42 | "labels": {}, 43 | "acceptedResourceRoles": null, 44 | "version": "2015-09-25T15:13:48.343Z", 45 | "versionInfo": { 46 | "lastScalingAt": "2015-09-25T15:13:48.343Z", 47 | "lastConfigChangeAt": "2015-09-25T15:13:48.343Z" 48 | }, 49 | "tasksStaged": 0, 50 | "tasksRunning": 0, 51 | "tasksHealthy": 0, 52 | "tasksUnhealthy": 0, 53 | "deployments": [ 54 | { 55 | "id": "9538079c-3898-4e32-aa31-799bf9097f74" 56 | } 57 | ] 58 | } 59 | ] 60 | } -------------------------------------------------------------------------------- /marathon/testdata/common/deployid_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "deploymentId": "5ed4c0c5-9ff8-4a6f-a0cd-f57f59a34b43", 3 | "version": "2015-09-29T15:59:51.164Z" 4 | } -------------------------------------------------------------------------------- /marathon/testdata/groups/get_group_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "/sites", 3 | "apps": [], 4 | "groups": [ 5 | { 6 | "id": "/sites/wordpress", 7 | "apps": [ 8 | { 9 | "id": "/sites/wordpress/myblog", 10 | "cmd": null, 11 | "args": null, 12 | "user": null, 13 | "env": { 14 | "WORDPRESS_DB_HOST": "sites-mysql.consul.local", 15 | "WORDPRESS_DB_NAME": "myblog", 16 | "WORDPRESS_DB_PASSWORD": "Redcell#1" 17 | }, 18 | "instances": 2, 19 | "cpus": 0.7, 20 | "mem": 600, 21 | "disk": 0, 22 | "executor": "", 23 | "constraints": [ 24 | [ 25 | "hostname", 26 | "UNIQUE" 27 | ] 28 | ], 29 | "uris": [ 30 | "file:///etc/docker.tar.gz" 31 | ], 32 | "fetch": [ 33 | { 34 | "uri": "file:///etc/docker.tar.gz", 35 | "extract": true, 36 | "executable": false, 37 | "cache": false 38 | } 39 | ], 40 | "storeUrls": [], 41 | "ports": [ 42 | 8102 43 | ], 44 | "requirePorts": false, 45 | "backoffSeconds": 1, 46 | "backoffFactor": 1.15, 47 | "maxLaunchDelaySeconds": 3600, 48 | "container": { 49 | "type": "DOCKER", 50 | "volumes": [], 51 | "docker": { 52 | "image": "wordpress:4.4.2", 53 | "network": "BRIDGE", 54 | "portMappings": [ 55 | { 56 | "containerPort": 80, 57 | "hostPort": 0, 58 | "servicePort": 8102, 59 | "protocol": "tcp" 60 | } 61 | ], 62 | "privileged": false, 63 | "parameters": [ 64 | { 65 | "key": "hostname", 66 | "value": "sites.consul.local" 67 | }, 68 | { 69 | "key": "volume-driver", 70 | "value": "nfs" 71 | }, 72 | { 73 | "key": "volume", 74 | "value": "nfs.local/mnt/data/sites/wordpress:/var/www/html" 75 | }, 76 | { 77 | "key": "net", 78 | "value": "weave" 79 | } 80 | ], 81 | "forcePullImage": false 82 | } 83 | }, 84 | "healthChecks": [ 85 | { 86 | "path": "/", 87 | "protocol": "HTTP", 88 | "portIndex": 0, 89 | "gracePeriodSeconds": 300, 90 | "intervalSeconds": 30, 91 | "timeoutSeconds": 20, 92 | "maxConsecutiveFailures": 3, 93 | "ignoreHttp1xx": false 94 | } 95 | ], 96 | "dependencies": [ 97 | "/sites/db/mysql" 98 | ], 99 | "upgradeStrategy": { 100 | "minimumHealthCapacity": 1, 101 | "maximumOverCapacity": 1 102 | }, 103 | "labels": { 104 | "HAPROXY_0_VHOST": "myblog-beta.com", 105 | "HAPROXY_GROUP": "external", 106 | "group": "blog", 107 | "role": "myblog.com" 108 | }, 109 | "acceptedResourceRoles": null, 110 | "ipAddress": null, 111 | "version": "2016-04-05T16:54:12.256Z", 112 | "versionInfo": { 113 | "lastScalingAt": "2016-04-05T16:54:12.256Z", 114 | "lastConfigChangeAt": "2016-04-05T16:54:12.256Z" 115 | } 116 | } 117 | ], 118 | "groups": [], 119 | "dependencies": [], 120 | "version": "2016-04-05T16:54:12.256Z" 121 | }, 122 | { 123 | "id": "/sites/db", 124 | "apps": [ 125 | { 126 | "id": "/sites/db/mysql", 127 | "cmd": null, 128 | "args": null, 129 | "user": null, 130 | "env": { 131 | "MYSQL_DATABASE": "myblog", 132 | "MYSQL_ROOT_PASSWORD": "password" 133 | }, 134 | "instances": 1, 135 | "cpus": 0.5, 136 | "mem": 600, 137 | "disk": 0, 138 | "executor": "", 139 | "constraints": [ 140 | [ 141 | "hostname", 142 | "UNIQUE" 143 | ] 144 | ], 145 | "uris": [ 146 | "file:///etc/docker.tar.gz" 147 | ], 148 | "fetch": [ 149 | { 150 | "uri": "file:///etc/docker.tar.gz", 151 | "extract": true, 152 | "executable": false, 153 | "cache": false 154 | } 155 | ], 156 | "storeUrls": [], 157 | "ports": [ 158 | 10002 159 | ], 160 | "requirePorts": false, 161 | "backoffSeconds": 1, 162 | "backoffFactor": 1.15, 163 | "maxLaunchDelaySeconds": 3600, 164 | "container": { 165 | "type": "DOCKER", 166 | "volumes": [], 167 | "docker": { 168 | "image": "mysql/mysql-server:5.6", 169 | "network": "BRIDGE", 170 | "portMappings": [ 171 | { 172 | "containerPort": 3306, 173 | "hostPort": 0, 174 | "servicePort": 10002, 175 | "protocol": "tcp" 176 | } 177 | ], 178 | "privileged": false, 179 | "parameters": [ 180 | { 181 | "key": "hostname", 182 | "value": "sites-mysql.consul.local" 183 | }, 184 | { 185 | "key": "volume-driver", 186 | "value": "nfs" 187 | }, 188 | { 189 | "key": "volume", 190 | "value": "nfs.local/mnt/data/sites/mysql:/var/lib/mysql" 191 | }, 192 | { 193 | "key": "net", 194 | "value": "weave" 195 | } 196 | ], 197 | "forcePullImage": false 198 | } 199 | }, 200 | "healthChecks": [ 201 | { 202 | "protocol": "TCP", 203 | "portIndex": 0, 204 | "gracePeriodSeconds": 300, 205 | "intervalSeconds": 30, 206 | "timeoutSeconds": 20, 207 | "maxConsecutiveFailures": 3, 208 | "ignoreHttp1xx": false 209 | } 210 | ], 211 | "dependencies": [], 212 | "upgradeStrategy": { 213 | "minimumHealthCapacity": 1, 214 | "maximumOverCapacity": 1 215 | }, 216 | "labels": { 217 | "group": "database", 218 | "role": "myblog" 219 | }, 220 | "acceptedResourceRoles": null, 221 | "ipAddress": null, 222 | "version": "2016-04-05T16:54:12.256Z", 223 | "versionInfo": { 224 | "lastScalingAt": "2016-04-05T16:54:12.256Z", 225 | "lastConfigChangeAt": "2016-04-05T16:54:12.256Z" 226 | } 227 | } 228 | ], 229 | "groups": [], 230 | "dependencies": [], 231 | "version": "2016-04-05T16:54:12.256Z" 232 | } 233 | ], 234 | "dependencies": [], 235 | "version": "2016-04-05T16:54:12.256Z" 236 | } -------------------------------------------------------------------------------- /marathon/testdata/groups/list_groups_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "/", 3 | "apps": [], 4 | "groups": [ 5 | { 6 | "id": "/sites", 7 | "apps": [], 8 | "groups": [ 9 | { 10 | "id": "/sites/wordpress", 11 | "apps": [ 12 | { 13 | "id": "/sites/wordpress/myblog", 14 | "cmd": null, 15 | "args": null, 16 | "user": null, 17 | "env": { 18 | "WORDPRESS_DB_HOST": "sites-mysql.consul.local", 19 | "WORDPRESS_DB_NAME": "myblog", 20 | "WORDPRESS_DB_PASSWORD": "Redcell#1" 21 | }, 22 | "instances": 2, 23 | "cpus": 0.7, 24 | "mem": 600, 25 | "disk": 0, 26 | "executor": "", 27 | "constraints": [ 28 | [ 29 | "hostname", 30 | "UNIQUE" 31 | ] 32 | ], 33 | "uris": [ 34 | "file:///etc/docker.tar.gz" 35 | ], 36 | "fetch": [ 37 | { 38 | "uri": "file:///etc/docker.tar.gz", 39 | "extract": true, 40 | "executable": false, 41 | "cache": false 42 | } 43 | ], 44 | "storeUrls": [], 45 | "ports": [ 46 | 8102 47 | ], 48 | "requirePorts": false, 49 | "backoffSeconds": 1, 50 | "backoffFactor": 1.15, 51 | "maxLaunchDelaySeconds": 3600, 52 | "container": { 53 | "type": "DOCKER", 54 | "volumes": [], 55 | "docker": { 56 | "image": "wordpress:4.4.2", 57 | "network": "BRIDGE", 58 | "portMappings": [ 59 | { 60 | "containerPort": 80, 61 | "hostPort": 0, 62 | "servicePort": 8102, 63 | "protocol": "tcp" 64 | } 65 | ], 66 | "privileged": false, 67 | "parameters": [ 68 | { 69 | "key": "hostname", 70 | "value": "sites.consul.local" 71 | }, 72 | { 73 | "key": "volume-driver", 74 | "value": "nfs" 75 | }, 76 | { 77 | "key": "volume", 78 | "value": "nfs.local/mnt/data/sites/wordpress:/var/www/html" 79 | }, 80 | { 81 | "key": "net", 82 | "value": "weave" 83 | } 84 | ], 85 | "forcePullImage": false 86 | } 87 | }, 88 | "healthChecks": [ 89 | { 90 | "path": "/", 91 | "protocol": "HTTP", 92 | "portIndex": 0, 93 | "gracePeriodSeconds": 300, 94 | "intervalSeconds": 30, 95 | "timeoutSeconds": 20, 96 | "maxConsecutiveFailures": 3, 97 | "ignoreHttp1xx": false 98 | } 99 | ], 100 | "dependencies": [ 101 | "/sites/db/mysql" 102 | ], 103 | "upgradeStrategy": { 104 | "minimumHealthCapacity": 1, 105 | "maximumOverCapacity": 1 106 | }, 107 | "labels": { 108 | "HAPROXY_0_VHOST": "myblog-beta.com", 109 | "HAPROXY_GROUP": "external", 110 | "group": "blog", 111 | "role": "myblog.com" 112 | }, 113 | "acceptedResourceRoles": null, 114 | "ipAddress": null, 115 | "version": "2016-04-05T16:54:12.256Z", 116 | "versionInfo": { 117 | "lastScalingAt": "2016-04-05T16:54:12.256Z", 118 | "lastConfigChangeAt": "2016-04-05T16:54:12.256Z" 119 | } 120 | } 121 | ], 122 | "groups": [], 123 | "dependencies": [], 124 | "version": "2016-04-05T16:54:12.256Z" 125 | }, 126 | { 127 | "id": "/sites/db", 128 | "apps": [ 129 | { 130 | "id": "/sites/db/mysql", 131 | "cmd": null, 132 | "args": null, 133 | "user": null, 134 | "env": { 135 | "MYSQL_DATABASE": "myblog", 136 | "MYSQL_ROOT_PASSWORD": "password" 137 | }, 138 | "instances": 1, 139 | "cpus": 0.5, 140 | "mem": 600, 141 | "disk": 0, 142 | "executor": "", 143 | "constraints": [ 144 | [ 145 | "hostname", 146 | "UNIQUE" 147 | ] 148 | ], 149 | "uris": [ 150 | "file:///etc/docker.tar.gz" 151 | ], 152 | "fetch": [ 153 | { 154 | "uri": "file:///etc/docker.tar.gz", 155 | "extract": true, 156 | "executable": false, 157 | "cache": false 158 | } 159 | ], 160 | "storeUrls": [], 161 | "ports": [ 162 | 10002 163 | ], 164 | "requirePorts": false, 165 | "backoffSeconds": 1, 166 | "backoffFactor": 1.15, 167 | "maxLaunchDelaySeconds": 3600, 168 | "container": { 169 | "type": "DOCKER", 170 | "volumes": [], 171 | "docker": { 172 | "image": "mysql/mysql-server:5.6", 173 | "network": "BRIDGE", 174 | "portMappings": [ 175 | { 176 | "containerPort": 3306, 177 | "hostPort": 0, 178 | "servicePort": 10002, 179 | "protocol": "tcp" 180 | } 181 | ], 182 | "privileged": false, 183 | "parameters": [ 184 | { 185 | "key": "hostname", 186 | "value": "sites-mysql.consul.local" 187 | }, 188 | { 189 | "key": "volume-driver", 190 | "value": "nfs" 191 | }, 192 | { 193 | "key": "volume", 194 | "value": "nfs.local/mnt/data/sites/mysql:/var/lib/mysql" 195 | }, 196 | { 197 | "key": "net", 198 | "value": "weave" 199 | } 200 | ], 201 | "forcePullImage": false 202 | } 203 | }, 204 | "healthChecks": [ 205 | { 206 | "protocol": "TCP", 207 | "portIndex": 0, 208 | "gracePeriodSeconds": 300, 209 | "intervalSeconds": 30, 210 | "timeoutSeconds": 20, 211 | "maxConsecutiveFailures": 3, 212 | "ignoreHttp1xx": false 213 | } 214 | ], 215 | "dependencies": [], 216 | "upgradeStrategy": { 217 | "minimumHealthCapacity": 1, 218 | "maximumOverCapacity": 1 219 | }, 220 | "labels": { 221 | "group": "database", 222 | "role": "myblog" 223 | }, 224 | "acceptedResourceRoles": null, 225 | "ipAddress": null, 226 | "version": "2016-04-05T16:54:12.256Z", 227 | "versionInfo": { 228 | "lastScalingAt": "2016-04-05T16:54:12.256Z", 229 | "lastConfigChangeAt": "2016-04-05T16:54:12.256Z" 230 | } 231 | } 232 | ], 233 | "groups": [], 234 | "dependencies": [], 235 | "version": "2016-04-05T16:54:12.256Z" 236 | } 237 | ], 238 | "dependencies": [], 239 | "version": "2016-04-05T16:54:12.256Z" 240 | } 241 | ] 242 | } -------------------------------------------------------------------------------- /marathon/wait.go: -------------------------------------------------------------------------------- 1 | package marathon 2 | 3 | import ( 4 | "github.com/ContainX/depcon/pkg/logger" 5 | "github.com/ContainX/depcon/utils" 6 | "time" 7 | ) 8 | 9 | var logWait = logger.GetLogger("depcon.deploy.wait") 10 | 11 | func (c *MarathonClient) WaitForApplication(id string, timeout time.Duration) error { 12 | t_now := time.Now() 13 | t_stop := t_now.Add(timeout) 14 | 15 | c.logWaitApplication(id) 16 | for { 17 | if time.Now().After(t_stop) { 18 | return ErrorTimeout 19 | } 20 | 21 | app, err := c.GetApplication(id) 22 | if err == nil { 23 | if app.DeploymentID == nil || len(app.DeploymentID) <= 0 { 24 | logWait.Infof("Application deployment has completed for %s, elapsed time %s", id, utils.ElapsedStr(time.Since(t_now))) 25 | if app.HealthChecks != nil && len(app.HealthChecks) > 0 { 26 | err := c.WaitForApplicationHealthy(id, timeout) 27 | if err != nil { 28 | logWait.Errorf("Error waiting for application '%s' to become healthy: %s", id, err.Error()) 29 | } 30 | } else { 31 | logWait.Warningf("No health checks defined for '%s', skipping waiting for healthy state", id) 32 | } 33 | return nil 34 | } 35 | } 36 | c.logWaitApplication(id) 37 | time.Sleep(time.Duration(2) * time.Second) 38 | } 39 | } 40 | 41 | func (c *MarathonClient) WaitForApplicationHealthy(id string, timeout time.Duration) error { 42 | t_now := time.Now() 43 | t_stop := t_now.Add(timeout) 44 | duration := time.Duration(2) * time.Second 45 | for { 46 | if time.Now().After(t_stop) { 47 | return ErrorTimeout 48 | } 49 | app, err := c.GetApplication(id) 50 | if err != nil { 51 | return err 52 | } 53 | total := app.TasksStaged + app.TasksRunning 54 | diff := total - app.TasksHealthy 55 | if diff == 0 { 56 | logWait.Infof("%v of %v expected instances are healthy. Elapsed health check time of %s", app.TasksHealthy, total, utils.ElapsedStr(time.Since(t_now))) 57 | return nil 58 | } 59 | logWait.Infof("%v healthy instances. Waiting for %v total instances. Retrying check in %v seconds", app.TasksHealthy, total, duration) 60 | time.Sleep(duration) 61 | } 62 | } 63 | 64 | func (c *MarathonClient) WaitForDeployment(id string, timeout time.Duration) error { 65 | 66 | t_now := time.Now() 67 | t_stop := t_now.Add(timeout) 68 | 69 | c.logWaitDeployment(id) 70 | 71 | for { 72 | if time.Now().After(t_stop) { 73 | return ErrorTimeout 74 | } 75 | if found, _ := c.HasDeployment(id); !found { 76 | c.logOutput(logWait.Infof, "Deployment has completed for %s, elapsed time %s", id, utils.ElapsedStr(time.Since(t_now))) 77 | return nil 78 | } 79 | c.logWaitDeployment(id) 80 | time.Sleep(time.Duration(2) * time.Second) 81 | } 82 | } 83 | 84 | func (c *MarathonClient) logWaitDeployment(id string) { 85 | c.logOutput(logWait.Infof, "Waiting for deployment %s", id) 86 | } 87 | 88 | func (c *MarathonClient) logWaitApplication(id string) { 89 | c.logOutput(logWait.Infof, "Waiting for application deployment to complete for %s", id) 90 | } 91 | -------------------------------------------------------------------------------- /pkg/cli/output.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strconv" 7 | "text/tabwriter" 8 | "text/template" 9 | "time" 10 | ) 11 | 12 | // Specializes in formatting a type into Column format 13 | type Formatter interface { 14 | ToColumns(output io.Writer) error 15 | Data() FormatData 16 | } 17 | 18 | type FormatData struct { 19 | Data interface{} 20 | Template string 21 | Funcs template.FuncMap 22 | } 23 | 24 | // Handles writing the formatted type into the desired output and global formatting 25 | type CLIWriter struct { 26 | FormatWriter func(f Formatter) 27 | ErrorWriter func(err error) 28 | } 29 | 30 | var writer *CLIWriter 31 | 32 | func Register(w *CLIWriter) { 33 | writer = w 34 | } 35 | 36 | func Output(f Formatter, err error) { 37 | if err == nil { 38 | writer.FormatWriter(f) 39 | } else { 40 | writer.ErrorWriter(err) 41 | } 42 | } 43 | 44 | func FlushWriter(w *tabwriter.Writer) { 45 | fmt.Fprintln(w, "") 46 | w.Flush() 47 | } 48 | 49 | func NewTabWriter(output io.Writer) *tabwriter.Writer { 50 | w := new(tabwriter.Writer) 51 | w.Init(output, 0, 8, 2, '\t', 0) 52 | return w 53 | } 54 | 55 | func (d FormatData) ToColumns(output io.Writer) error { 56 | w := NewTabWriter(output) 57 | t := template.New("output").Funcs(buildFuncMap(d.Funcs)) 58 | t, _ = t.Parse(d.Template) 59 | if err := t.Execute(w, d.Data); err != nil { 60 | return err 61 | } 62 | FlushWriter(w) 63 | return nil 64 | } 65 | 66 | func buildFuncMap(userFuncs template.FuncMap) template.FuncMap { 67 | funcMap := template.FuncMap{ 68 | "floatToString": floatToString, 69 | "intToString": strconv.Itoa, 70 | "valString": valueToString, 71 | "pad": padString, 72 | "fdate": FormatDate, 73 | "msDur": durationToMilliseconds, 74 | "boolToYesNo": boolToYesNo, 75 | } 76 | 77 | if userFuncs != nil { 78 | for k, v := range userFuncs { 79 | funcMap[k] = v 80 | } 81 | } 82 | 83 | return funcMap 84 | } 85 | 86 | func durationToMilliseconds(t time.Duration) string { 87 | return fmt.Sprintf("%d ms", t.Nanoseconds()/int64(time.Millisecond)) 88 | } 89 | 90 | func padString(s string) string { 91 | return fmt.Sprintf("%-25s:", s) 92 | } 93 | 94 | func floatToString(f float64) string { 95 | return fmt.Sprintf("%.2f", f) 96 | } 97 | 98 | func valueToString(v interface{}) string { 99 | return fmt.Sprintf("%v", v) 100 | } 101 | 102 | func boolToYesNo(b bool) string { 103 | if b { 104 | return "Y" 105 | } 106 | return "N" 107 | } 108 | -------------------------------------------------------------------------------- /pkg/cli/util.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | ) 7 | 8 | func EvalPrintUsage(usage_func func() error, args []string, minlen int) bool { 9 | if len(args) < minlen { 10 | usage_func() 11 | return true 12 | } 13 | return false 14 | } 15 | 16 | func FormatDate(date string) string { 17 | t, err := time.Parse(time.RFC3339, date) 18 | if err != nil { 19 | return date 20 | } 21 | return t.Local().Format("2006-01-02 15:04:05") 22 | } 23 | 24 | func NameValueSliceToMap(params []string) map[string]string { 25 | if params == nil { 26 | return nil 27 | } 28 | envmap := make(map[string]string) 29 | for _, p := range params { 30 | if strings.Contains(p, "=") { 31 | v := strings.Split(p, "=") 32 | envmap[v[0]] = v[1] 33 | } 34 | } 35 | return envmap 36 | } 37 | -------------------------------------------------------------------------------- /pkg/encoding/encoder.go: -------------------------------------------------------------------------------- 1 | // YAML and JSON encoding 2 | package encoding 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | ) 11 | 12 | type EncoderType int 13 | 14 | const ( 15 | JSON EncoderType = 1 + iota 16 | YAML 17 | ) 18 | 19 | var ( 20 | ErrorInvalidExtension = errors.New("File extension must be [.json | .yml | .yaml]") 21 | defaultJSONEncoder = newJSONEncoder() 22 | defaultYAMLEncoder = newYAMLEncoder() 23 | ) 24 | 25 | type Encoder interface { 26 | MarshalIndent(data interface{}) (string, error) 27 | 28 | Marshal(data interface{}) (string, error) 29 | 30 | UnMarshal(r io.Reader, result interface{}) error 31 | 32 | UnMarshalStr(data string, result interface{}) error 33 | } 34 | 35 | func NewEncoder(encoder EncoderType) (Encoder, error) { 36 | switch encoder { 37 | case JSON: 38 | return newJSONEncoder(), nil 39 | case YAML: 40 | return newYAMLEncoder(), nil 41 | default: 42 | panic(fmt.Errorf("Unsupported encoder type")) 43 | } 44 | } 45 | 46 | func DefaultJSONEncoder() Encoder { 47 | return defaultJSONEncoder 48 | } 49 | 50 | func DefaultYAMLEncoder() Encoder { 51 | return defaultYAMLEncoder 52 | } 53 | 54 | func NewEncoderFromFileExt(filename string) (Encoder, error) { 55 | 56 | if et, err := EncoderTypeFromExt(filename); err != nil { 57 | return nil, err 58 | } else { 59 | return NewEncoder(et) 60 | } 61 | } 62 | 63 | func EncoderTypeFromExt(filename string) (EncoderType, error) { 64 | switch filepath.Ext(filename) { 65 | case ".yml", ".yaml": 66 | return YAML, nil 67 | case ".json": 68 | return JSON, nil 69 | } 70 | return JSON, ErrorInvalidExtension 71 | 72 | } 73 | 74 | func ConvertFile(infile, outfile string, dataType interface{}) error { 75 | var fromEnc, toEnc Encoder 76 | var encErr error 77 | 78 | if fromEnc, encErr = NewEncoderFromFileExt(infile); encErr != nil { 79 | return encErr 80 | } 81 | 82 | if toEnc, encErr = NewEncoderFromFileExt(outfile); encErr != nil { 83 | return encErr 84 | } 85 | 86 | file, err := os.Open(infile) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | if err := fromEnc.UnMarshal(file, dataType); err != nil { 92 | return err 93 | } 94 | if err := os.MkdirAll(filepath.Dir(outfile), 0700); err != nil { 95 | return err 96 | } 97 | f, err := os.OpenFile(outfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) 98 | if err != nil { 99 | return err 100 | } 101 | defer f.Close() 102 | if data, err := toEnc.MarshalIndent(dataType); err != nil { 103 | return err 104 | } else { 105 | f.WriteString(data) 106 | } 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /pkg/encoding/json.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | // An encoder that marshal's and unmarshal's Json which implements the Encoder interface 10 | type JSONEncoder struct{} 11 | 12 | func newJSONEncoder() *JSONEncoder { 13 | return &JSONEncoder{} 14 | } 15 | 16 | func (e *JSONEncoder) MarshalIndent(data interface{}) (string, error) { 17 | if response, err := json.MarshalIndent(data, "", " "); err != nil { 18 | return "", err 19 | } else { 20 | return string(response), err 21 | } 22 | } 23 | 24 | func (e *JSONEncoder) Marshal(data interface{}) (string, error) { 25 | if response, err := json.Marshal(data); err != nil { 26 | return "", err 27 | } else { 28 | return string(response), err 29 | } 30 | } 31 | 32 | func (e *JSONEncoder) UnMarshal(r io.Reader, result interface{}) error { 33 | decoder := json.NewDecoder(r) 34 | if err := decoder.Decode(result); err != nil { 35 | return err 36 | } 37 | return nil 38 | } 39 | 40 | func (e *JSONEncoder) UnMarshalStr(data string, result interface{}) error { 41 | return e.UnMarshal(strings.NewReader(data), result) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/encoding/yaml.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "github.com/ghodss/yaml" 5 | "io" 6 | "io/ioutil" 7 | "strings" 8 | ) 9 | 10 | // An encoder that marshal's and unmarshal's YAML which implements the Encoder interface 11 | type YAMLEncoder struct{} 12 | 13 | func newYAMLEncoder() *YAMLEncoder { 14 | return &YAMLEncoder{} 15 | } 16 | 17 | func (e *YAMLEncoder) MarshalIndent(data interface{}) (string, error) { 18 | return e.Marshal(data) 19 | } 20 | 21 | func (e *YAMLEncoder) Marshal(data interface{}) (string, error) { 22 | 23 | if response, err := yaml.Marshal(data); err != nil { 24 | return "", err 25 | } else { 26 | return string(response), err 27 | } 28 | } 29 | 30 | func (e *YAMLEncoder) UnMarshal(r io.Reader, result interface{}) error { 31 | b, err := ioutil.ReadAll(r) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | if err := yaml.Unmarshal(b, result); err != nil { 37 | return err 38 | } 39 | return nil 40 | } 41 | 42 | func (e *YAMLEncoder) UnMarshalStr(data string, result interface{}) error { 43 | return e.UnMarshal(strings.NewReader(data), result) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/envsubst/envsubst.go: -------------------------------------------------------------------------------- 1 | /* 2 | This is a semi-clone of original source found at : https://github.com/nparry/envterpolate and changed to adapt to use 3 | cases needed for depcon 4 | */ 5 | package envsubst 6 | 7 | import ( 8 | "bufio" 9 | "bytes" 10 | "github.com/ContainX/depcon/pkg/logger" 11 | "io" 12 | "os" 13 | "unicode" 14 | ) 15 | 16 | var log = logger.GetLogger("depcon") 17 | 18 | type runeReader interface { 19 | ReadRune() (rune, int, error) 20 | } 21 | 22 | type runeWriter interface { 23 | WriteRune(rune) (int, error) 24 | } 25 | 26 | type state int 27 | 28 | const ( 29 | initial state = iota 30 | readingVarName 31 | readingBracedVarName 32 | ) 33 | 34 | type varNameTokenStatus int 35 | 36 | const ( 37 | complete varNameTokenStatus = iota 38 | incomplete 39 | ) 40 | 41 | type undefinedVariableBehavior int 42 | 43 | const ( 44 | remove undefinedVariableBehavior = iota 45 | preserve 46 | ) 47 | 48 | type envsubst struct { 49 | state state 50 | buffer bytes.Buffer 51 | target runeWriter 52 | undefinedBehavior undefinedVariableBehavior 53 | resolver func(string) string 54 | } 55 | 56 | func isVarNameCharacter(char rune, isFirstLetter bool) bool { 57 | if !isFirstLetter && unicode.IsDigit(char) { 58 | return true 59 | } 60 | return unicode.IsLetter(char) || char == '_' 61 | } 62 | 63 | func standaloneDollarString(varNameTokenStatus varNameTokenStatus, state state) string { 64 | switch { 65 | case state == readingVarName: 66 | return "$" 67 | case varNameTokenStatus == incomplete: 68 | return "${" 69 | } 70 | 71 | return "${}" 72 | } 73 | 74 | func writeString(s string, target runeWriter) error { 75 | for _, char := range s { 76 | if err := writeRune(char, target); err != nil { 77 | return err 78 | } 79 | } 80 | 81 | return nil 82 | } 83 | 84 | func writeRune(char rune, target runeWriter) error { 85 | _, err := target.WriteRune(char) 86 | return err 87 | } 88 | 89 | func substituteVariableReferences(source runeReader, target runeWriter, undefinedBehavior undefinedVariableBehavior, resolver func(string) string) error { 90 | et := envsubst{ 91 | target: target, 92 | undefinedBehavior: undefinedBehavior, 93 | resolver: resolver, 94 | } 95 | 96 | for char, size, _ := source.ReadRune(); size != 0; char, size, _ = source.ReadRune() { 97 | if err := et.processRune(char); err != nil { 98 | return err 99 | } 100 | } 101 | 102 | return et.endOfInput() 103 | } 104 | 105 | func (et *envsubst) processRune(char rune) error { 106 | switch et.state { 107 | case initial: 108 | switch { 109 | case char == '$': 110 | et.state = readingVarName 111 | default: 112 | return writeRune(char, et.target) 113 | } 114 | case readingVarName: 115 | switch { 116 | // case isVarNameCharacter(char, et.buffer.Len() == 0): 117 | // return writeRune(char, &et.buffer) 118 | case char == '{' && et.buffer.Len() == 0: 119 | et.state = readingBracedVarName 120 | default: 121 | return et.flushBufferAndProcessNextRune(complete, char) 122 | } 123 | case readingBracedVarName: 124 | switch { 125 | case isVarNameCharacter(char, et.buffer.Len() == 0): 126 | return writeRune(char, &et.buffer) 127 | case char == '}': 128 | return et.flushBuffer(complete) 129 | default: 130 | return et.flushBufferAndProcessNextRune(incomplete, char) 131 | } 132 | } 133 | 134 | return nil 135 | } 136 | 137 | func (et *envsubst) endOfInput() error { 138 | if et.state != initial { 139 | return et.flushBuffer(incomplete) 140 | } 141 | 142 | return nil 143 | } 144 | 145 | func (et *envsubst) flushBufferAndProcessNextRune(bufferStatus varNameTokenStatus, nextChar rune) error { 146 | if err := et.flushBuffer(bufferStatus); err != nil { 147 | return err 148 | } 149 | 150 | return et.processRune(nextChar) 151 | } 152 | 153 | func (et *envsubst) flushBuffer(bufferStatus varNameTokenStatus) error { 154 | var err error 155 | 156 | switch { 157 | case et.buffer.Len() == 0: 158 | err = writeString(standaloneDollarString(bufferStatus, et.state), et.target) 159 | case et.state == readingBracedVarName && bufferStatus == incomplete: 160 | err = writeString("${"+et.buffer.String(), et.target) 161 | default: 162 | err = writeString(et.resolve(et.buffer.String()), et.target) 163 | } 164 | 165 | et.state = initial 166 | et.buffer.Reset() 167 | 168 | return err 169 | } 170 | 171 | func (et *envsubst) resolve(variableName string) string { 172 | resolvedValue := et.resolver(variableName) 173 | if len(resolvedValue) == 0 && et.undefinedBehavior == preserve { 174 | if et.state == readingBracedVarName { 175 | return "${" + variableName + "}" 176 | } 177 | return "$" + variableName 178 | } 179 | return resolvedValue 180 | } 181 | 182 | func Substitute(in io.Reader, preserveUndef bool, resolver func(string) string) string { 183 | undefinedBehavior := remove 184 | if preserveUndef { 185 | undefinedBehavior = preserve 186 | } 187 | 188 | buf := new(bytes.Buffer) 189 | if err := substituteVariableReferences(bufio.NewReader(in), buf, undefinedBehavior, resolver); err != nil { 190 | log.Fatal(err) 191 | } 192 | return buf.String() 193 | } 194 | 195 | func SubstFileTokens(in io.Reader, params map[string]string) (parsed string, missing bool) { 196 | parsed = Substitute(in, true, func(s string) string { 197 | if params != nil && params[s] != "" { 198 | return params[s] 199 | } 200 | if os.Getenv(s) == "" { 201 | log.Warning("Cannot find a value for varible ${%s} in template", s) 202 | missing = true 203 | } 204 | return os.Getenv(s) 205 | }) 206 | return parsed, missing 207 | } 208 | -------------------------------------------------------------------------------- /pkg/envsubst/envsubst_test.go: -------------------------------------------------------------------------------- 1 | package envsubst 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | var theWordIsGo = map[string]string{ 10 | "WORD": "go", 11 | } 12 | 13 | var funWithDigits = map[string]string{ 14 | "FUN1": "gopher", 15 | } 16 | 17 | // Wrap substituteVariableReferences for easy testing 18 | func subst(input string, vars map[string]string) string { 19 | return substitute(input, false, vars) 20 | } 21 | 22 | func substitute(input string, preserveUndef bool, vars map[string]string) string { 23 | return Substitute(strings.NewReader(input), preserveUndef, func(s string) string { 24 | return vars[s] 25 | }) 26 | } 27 | 28 | func TestEmptyInput(t *testing.T) { 29 | result := subst("", theWordIsGo) 30 | assert.Equal(t, result, "") 31 | } 32 | 33 | func TestNoVariables(t *testing.T) { 34 | result := subst("hello world", theWordIsGo) 35 | assert.Equal(t, "hello world", result) 36 | } 37 | 38 | func TestSimpleVariable(t *testing.T) { 39 | result := subst("hello $WORD world", theWordIsGo) 40 | assert.Equal(t, "hello $WORD world", result) 41 | 42 | result = subst("hello ${WORD} world", theWordIsGo) 43 | assert.Equal(t, "hello go world", result) 44 | } 45 | 46 | func TestSimpleVariableAtStart(t *testing.T) { 47 | result := subst("$WORD home world", theWordIsGo) 48 | assert.Equal(t, "$WORD home world", result) 49 | 50 | result = subst("${WORD} home world", theWordIsGo) 51 | assert.Equal(t, "go home world", result) 52 | } 53 | 54 | func TestSimpleVariableAtEnd(t *testing.T) { 55 | result := subst("let's $WORD", theWordIsGo) 56 | assert.Equal(t, "let's $WORD", result) 57 | 58 | result = subst("let's ${WORD}", theWordIsGo) 59 | assert.Equal(t, "let's go", result) 60 | } 61 | 62 | func TestOnlyVariable(t *testing.T) { 63 | result := subst("$WORD", theWordIsGo) 64 | assert.Equal(t, "$WORD", result) 65 | 66 | result = subst("${WORD}", theWordIsGo) 67 | assert.Equal(t, result, "go") 68 | } 69 | 70 | func TestRunOnVariable(t *testing.T) { 71 | result := subst("$WORD$WORD$WORD!", theWordIsGo) 72 | assert.Equal(t, "$WORD$WORD$WORD!", result) 73 | 74 | result = subst("${WORD}${WORD}${WORD}!", theWordIsGo) 75 | assert.Equal(t, "gogogo!", result) 76 | } 77 | 78 | func TestRunOnVariableWithNonVariableTextPrefix(t *testing.T) { 79 | result := subst("$WORD,no$WORD", theWordIsGo) 80 | assert.Equal(t, "$WORD,no$WORD", result) 81 | 82 | result = subst("${WORD},no${WORD}", theWordIsGo) 83 | assert.Equal(t, "go,nogo", result) 84 | } 85 | 86 | func TestSimpleStandAloneDollar(t *testing.T) { 87 | result := subst("2 $ for your $WORD thoughts", theWordIsGo) 88 | assert.Equal(t, "2 $ for your $WORD thoughts", result) 89 | 90 | result = subst("2 ${} for your $WORD thoughts", theWordIsGo) 91 | assert.Equal(t, "2 ${} for your $WORD thoughts", result) 92 | } 93 | 94 | func TestSimpleStandAloneDollarAtStart(t *testing.T) { 95 | result := subst("$ for your $WORD thoughts", theWordIsGo) 96 | assert.Equal(t, "$ for your $WORD thoughts", result) 97 | 98 | result = subst("${} for your $WORD thoughts", theWordIsGo) 99 | assert.Equal(t, "${} for your $WORD thoughts", result) 100 | } 101 | 102 | func TestSimpleStandAloneDollarAtEnd(t *testing.T) { 103 | result := subst("$WORD, find some $", theWordIsGo) 104 | assert.Equal(t, result, "$WORD, find some $") 105 | 106 | result = subst("${WORD}, find some ${}", theWordIsGo) 107 | assert.Equal(t, "go, find some ${}", result) 108 | } 109 | 110 | func TestOnlyStandAloneDollar(t *testing.T) { 111 | result := subst("$", theWordIsGo) 112 | assert.Equal(t, "$", result) 113 | 114 | result = subst("${}", theWordIsGo) 115 | assert.Equal(t, "${}", result) 116 | } 117 | 118 | func TestStandAloneDollarSuffix(t *testing.T) { 119 | result := subst("$WORD$", theWordIsGo) 120 | assert.Equal(t, "$WORD$", result) 121 | 122 | result = subst("${WORD}${}", theWordIsGo) 123 | assert.Equal(t, "go${}", result) 124 | } 125 | 126 | func TestBracingStartsMidtoken(t *testing.T) { 127 | result := subst("$WORD{FISH}", theWordIsGo) 128 | assert.Equal(t, "$WORD{FISH}", result) 129 | 130 | result = subst("$WORD{WORD}", theWordIsGo) 131 | assert.Equal(t, "$WORD{WORD}", result) 132 | 133 | result = subst("$WO{RD}", theWordIsGo) 134 | assert.Equal(t, "$WO{RD}", result) 135 | } 136 | 137 | func TestUnclosedBraceWithPrefix(t *testing.T) { 138 | result := subst("$WORD{WORD", theWordIsGo) 139 | assert.Equal(t, "$WORD{WORD", result) 140 | } 141 | 142 | func TestUnclosedBracedDollar(t *testing.T) { 143 | result := subst("${", theWordIsGo) 144 | assert.Equal(t, "${", result) 145 | } 146 | 147 | func TestUnclosedBracedDollarAndSubsequentTokens(t *testing.T) { 148 | result := subst("${ stuff }", theWordIsGo) 149 | assert.Equal(t, "${ stuff }", result) 150 | } 151 | 152 | func TestUnclosedBracedDollarWithSuffix(t *testing.T) { 153 | result := subst("${WORD", theWordIsGo) 154 | assert.Equal(t, "${WORD", result) 155 | } 156 | 157 | func TestUnclosedBracedDollarWithSuffixAndSubsequentTokens(t *testing.T) { 158 | result := subst("no ${WORD yet", theWordIsGo) 159 | assert.Equal(t, "no ${WORD yet", result) 160 | } 161 | 162 | func TestDoubleBracedDollar(t *testing.T) { 163 | result := subst("this ${{WORD}} should not be touched", theWordIsGo) 164 | assert.Equal(t, "this ${{WORD}} should not be touched", result) 165 | } 166 | 167 | func TestUndefinedVariablesAreRemovedByDefault(t *testing.T) { 168 | result := subst("nothing $WORD2 to see here", theWordIsGo) 169 | assert.Equal(t, "nothing $WORD2 to see here", result) 170 | 171 | result = subst("nothing ${WORD2} to see here", theWordIsGo) 172 | assert.Equal(t, "nothing to see here", result) 173 | 174 | result = subst("nothing${WORD2}to see here", theWordIsGo) 175 | assert.Equal(t, "nothingto see here", result) 176 | } 177 | 178 | func TestUndefinedVariablesArePreservedWhenWanted(t *testing.T) { 179 | result := substitute("nothing $WORD2 to see here", true, theWordIsGo) 180 | assert.Equal(t, "nothing $WORD2 to see here", result) 181 | 182 | result = substitute("nothing ${WORD2} to see here", true, theWordIsGo) 183 | assert.Equal(t, "nothing ${WORD2} to see here", result) 184 | 185 | result = substitute("nothing${WORD2}to see here", true, theWordIsGo) 186 | assert.Equal(t, "nothing${WORD2}to see here", result) 187 | } 188 | 189 | func TestVariableNamesDontBeginWithADigit(t *testing.T) { 190 | result := subst("$1A", theWordIsGo) 191 | assert.Equal(t, "$1A", result) 192 | 193 | result = subst("${1A}", theWordIsGo) 194 | assert.Equal(t, "${1A}", result) 195 | } 196 | 197 | func TestVariableNamesAllowDigitsAfterFirstCharacter(t *testing.T) { 198 | result := subst("$FUN1", funWithDigits) 199 | assert.Equal(t, "$FUN1", result) 200 | 201 | result = subst("${FUN1}", funWithDigits) 202 | assert.Equal(t, "gopher", result) 203 | 204 | result = subst("no $FUN2", funWithDigits) 205 | assert.Equal(t, "no $FUN2", result) 206 | 207 | result = subst("no ${FUN2}", funWithDigits) 208 | assert.Equal(t, "no ", result) 209 | } 210 | -------------------------------------------------------------------------------- /pkg/httpclient/client.go: -------------------------------------------------------------------------------- 1 | // HTTP Client which handles generic error routing and marshaling 2 | package httpclient 3 | 4 | import ( 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "github.com/ContainX/depcon/pkg/encoding" 9 | "github.com/ContainX/depcon/pkg/logger" 10 | "io" 11 | "io/ioutil" 12 | "net/http" 13 | "strings" 14 | "sync" 15 | "time" 16 | ) 17 | 18 | var log = logger.GetLogger("client") 19 | 20 | type Response struct { 21 | Status int 22 | Content string 23 | Elapsed time.Duration 24 | Error error 25 | } 26 | 27 | type Request struct { 28 | // Http Method type 29 | method Method 30 | // Complete URL including params 31 | url string 32 | // Post data 33 | data string 34 | // Expected data type 35 | result interface{} 36 | // encoding type (optional : default JSON) 37 | encodingType encoding.EncoderType 38 | } 39 | 40 | type HttpClientConfig struct { 41 | sync.RWMutex 42 | // Http Basic Auth Username 43 | HttpUser string 44 | // Http Basic Auth Password 45 | HttpPass string 46 | // Http Authorization Token 47 | HttpToken string 48 | // Request timeout 49 | RequestTimeout int 50 | // TLS Insecure Skip Verify 51 | TLSInsecureSkipVerify bool 52 | } 53 | 54 | type HttpClient struct { 55 | config *HttpClientConfig 56 | http *http.Client 57 | } 58 | 59 | var ( 60 | // invalid or error response 61 | ErrorInvalidResponse = errors.New("Invalid response from Remote") 62 | // some resource does not exists 63 | ErrorNotFound = errors.New("The resource does not exist") 64 | // Generic Error Message 65 | ErrorMessage = errors.New("Unknown error message was captured") 66 | // Not Authorized 67 | ErrorNotAuthorized = errors.New("Not Authorized to perform this action - Status: 403") 68 | // Not Authenticated 69 | ErrorNotAuthenticated = errors.New("Not Authenticated to perform this action - Status: 401") 70 | ) 71 | 72 | func NewDefaultConfig() *HttpClientConfig { 73 | return &HttpClientConfig{RWMutex: sync.RWMutex{}, RequestTimeout: 30, TLSInsecureSkipVerify: false} 74 | } 75 | 76 | func DefaultHttpClient() *HttpClient { 77 | return NewHttpClient(NewDefaultConfig()) 78 | } 79 | 80 | func NewHttpClient(config *HttpClientConfig) *HttpClient { 81 | hc := &HttpClient{ 82 | config: config, 83 | http: &http.Client{ 84 | Timeout: time.Duration(config.RequestTimeout) * time.Second, 85 | }, 86 | } 87 | if config.TLSInsecureSkipVerify { 88 | tr := &http.Transport{ 89 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 90 | } 91 | hc.http.Transport = tr 92 | } 93 | return hc 94 | } 95 | 96 | func NewResponse(status int, elapsed time.Duration, content string, err error) *Response { 97 | return &Response{Status: status, Elapsed: elapsed, Content: content, Error: err} 98 | } 99 | 100 | func (h *HttpClient) HttpGet(url string, result interface{}) *Response { 101 | return h.invoke(&Request{method: GET, url: url, result: result}) 102 | } 103 | 104 | func (h *HttpClient) HttpPut(url string, data interface{}, result interface{}) *Response { 105 | return h.httpCall(PUT, url, data, result) 106 | } 107 | 108 | func (h *HttpClient) HttpDelete(url string, data interface{}, result interface{}) *Response { 109 | return h.httpCall(DELETE, url, data, result) 110 | } 111 | 112 | func (h *HttpClient) HttpPost(url string, data interface{}, result interface{}) *Response { 113 | return h.httpCall(POST, url, data, result) 114 | } 115 | 116 | func (h *HttpClient) httpCall(method Method, url string, data interface{}, result interface{}) *Response { 117 | var body string 118 | if data != nil { 119 | body = h.convertBody(data) 120 | } 121 | 122 | r := &Request{ 123 | method: method, 124 | url: url, 125 | data: body, 126 | result: result, 127 | } 128 | 129 | return h.invoke(r) 130 | } 131 | 132 | // Creates a net/http Request and associates default headers and authentication 133 | // parameters 134 | func (h *HttpClient) CreateHttpRequest(method, urlStr string, body io.Reader) (*http.Request, error) { 135 | request, err := http.NewRequest(method, urlStr, body) 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | AddDefaultHeaders(request) 141 | AddAuthentication(h.config, request) 142 | 143 | return request, nil 144 | } 145 | 146 | func (h *HttpClient) invoke(r *Request) *Response { 147 | 148 | log.Debug("%s - %s, Body:\n%s", r.method.String(), r.url, r.data) 149 | 150 | request, err := h.CreateHttpRequest(r.method.String(), r.url, strings.NewReader(r.data)) 151 | 152 | if err != nil { 153 | return &Response{Error: err} 154 | } 155 | 156 | req_start := time.Now() 157 | response, err := h.http.Do(request) 158 | req_elapsed := time.Now().Sub(req_start) 159 | 160 | if err != nil { 161 | return NewResponse(0, req_elapsed, "", err) 162 | } 163 | 164 | status := response.StatusCode 165 | var content string 166 | if response.ContentLength != 0 { 167 | defer response.Body.Close() 168 | rc, err := ioutil.ReadAll(response.Body) 169 | if err != nil { 170 | return NewResponse(status, req_elapsed, "", err) 171 | } 172 | content = string(rc) 173 | } 174 | 175 | log.Debug("Status: %v, RAW: %s", status, content) 176 | 177 | if status >= 200 && status < 300 { 178 | if r.result != nil { 179 | h.convert(r, content) 180 | } 181 | return NewResponse(status, req_elapsed, content, nil) 182 | } 183 | 184 | switch status { 185 | case 500: 186 | return NewResponse(status, req_elapsed, content, ErrorInvalidResponse) 187 | case 404: 188 | return NewResponse(status, req_elapsed, content, ErrorNotFound) 189 | case 403: 190 | return NewResponse(status, req_elapsed, content, ErrorNotAuthorized) 191 | case 401: 192 | return NewResponse(status, req_elapsed, content, ErrorNotAuthenticated) 193 | } 194 | 195 | return NewResponse(status, req_elapsed, content, ErrorMessage) 196 | } 197 | 198 | func (h *HttpClient) convertBody(data interface{}) string { 199 | if data == nil { 200 | return "" 201 | } 202 | encoder, _ := encoding.NewEncoder(encoding.JSON) 203 | body, _ := encoder.Marshal(data) 204 | return body 205 | } 206 | 207 | func (h *HttpClient) convert(r *Request, content string) error { 208 | um, _ := encoding.NewEncoder(encoding.JSON) 209 | if r.encodingType != 0 { 210 | um, _ = encoding.NewEncoder(r.encodingType) 211 | } 212 | um.UnMarshalStr(content, r.result) 213 | return nil 214 | 215 | } 216 | 217 | func AddDefaultHeaders(req *http.Request) { 218 | req.Header.Add("Content-Type", "application/json") 219 | req.Header.Add("Accept", "application/json") 220 | } 221 | 222 | func AddAuthentication(c *HttpClientConfig, req *http.Request) { 223 | if c.HttpToken != "" { 224 | req.Header.Set("Authorization", fmt.Sprintf("token=%v", c.HttpToken)) 225 | return 226 | } 227 | if c.HttpUser != "" { 228 | req.SetBasicAuth(c.HttpUser, c.HttpPass) 229 | } 230 | } 231 | 232 | func (h *HttpClient) Unwrap() *http.Client { 233 | return h.http 234 | } 235 | 236 | func (h *HttpClient) Configuration() *HttpClientConfig { 237 | return h.config 238 | } 239 | -------------------------------------------------------------------------------- /pkg/httpclient/client_test.go: -------------------------------------------------------------------------------- 1 | package httpclient 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | ) 7 | 8 | func TestHttpClient_Configuration(t *testing.T) { 9 | httpConfig := NewDefaultConfig() 10 | httpConfig.HttpUser = "username" 11 | httpConfig.HttpPass = "passwd" 12 | httpConfig.HttpToken = "" 13 | httpConfig.RWMutex = sync.RWMutex{} 14 | 15 | client := NewHttpClient(httpConfig) 16 | if client == nil { 17 | t.Error() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pkg/httpclient/methods.go: -------------------------------------------------------------------------------- 1 | package httpclient 2 | 3 | type Method int 4 | 5 | const ( 6 | GET Method = 1 + iota 7 | POST 8 | PUT 9 | DELETE 10 | HEAD 11 | ) 12 | 13 | var methods = [...]string{ 14 | "GET", 15 | "POST", 16 | "PUT", 17 | "DELETE", 18 | "HEAD", 19 | } 20 | 21 | func (method Method) String() string { 22 | return methods[method-1] 23 | } 24 | -------------------------------------------------------------------------------- /pkg/logger/logformat.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package logger 4 | 5 | /// 6 | // Color overrides and formatting changes for +nix environments 7 | // 8 | 9 | import ( 10 | "github.com/op/go-logging" 11 | "os" 12 | ) 13 | 14 | var format = logging.MustStringFormatter( 15 | "%{color}%{time:2006-01-02 15:04:05} %{level:.7s} [%{module}]:%{color:reset} %{message}", 16 | ) 17 | 18 | func init() { 19 | backend := logging.NewLogBackend(os.Stdout, "", 0) 20 | backendFmt := logging.NewBackendFormatter(backend, format) 21 | logging.SetBackend(backendFmt) 22 | } 23 | -------------------------------------------------------------------------------- /pkg/logger/logger.go: -------------------------------------------------------------------------------- 1 | // Logging configuration and management 2 | package logger 3 | 4 | // Loosely wraps go-logging to offer a possible facade in the future for other logging frameworks 5 | 6 | import ( 7 | "github.com/op/go-logging" 8 | ) 9 | 10 | type LogLevel int 11 | 12 | const ( 13 | CRITICAL LogLevel = iota 14 | ERROR 15 | WARNING 16 | NOTICE 17 | INFO 18 | DEBUG 19 | ) 20 | 21 | var levelTypes = []logging.Level{ 22 | logging.CRITICAL, 23 | logging.ERROR, 24 | logging.WARNING, 25 | logging.NOTICE, 26 | logging.INFO, 27 | logging.DEBUG, 28 | } 29 | 30 | var dlog *logging.Logger 31 | 32 | func InitWithDefaultLogger(module string) { 33 | dlog = logging.MustGetLogger(module) 34 | } 35 | 36 | func (p LogLevel) unWrap() logging.Level { 37 | return levelTypes[p] 38 | } 39 | 40 | func SetLevel(level LogLevel, module string) { 41 | logging.SetLevel(level.unWrap(), module) 42 | } 43 | 44 | func GetLogger(module string) *logging.Logger { 45 | return logging.MustGetLogger(module) 46 | } 47 | 48 | func Logger() *logging.Logger { 49 | return dlog 50 | } 51 | -------------------------------------------------------------------------------- /pkg/mockrest/mockwebserver.go: -------------------------------------------------------------------------------- 1 | package mockrest 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "time" 9 | ) 10 | 11 | // A scriptable server which wraps httptest. Allows us to test the cluster clients by responding back with appropriate schemas 12 | 13 | type Server struct { 14 | testServer *httptest.Server 15 | handlers chan http.HandlerFunc 16 | requests chan *http.Request 17 | URL string 18 | } 19 | 20 | func New() *Server { 21 | return &Server{ 22 | handlers: make(chan http.HandlerFunc), 23 | requests: make(chan *http.Request), 24 | } 25 | } 26 | 27 | func StartNewWithBody(body string) *Server { 28 | s := New() 29 | s.URL = s.Start() 30 | s.Enqueue(func(w http.ResponseWriter, r *http.Request) { 31 | fmt.Fprintln(w, body) 32 | }) 33 | return s 34 | } 35 | 36 | func StartNewWithStatusCode(status int) *Server { 37 | s := New() 38 | s.URL = s.Start() 39 | s.Enqueue(func(w http.ResponseWriter, r *http.Request) { 40 | w.WriteHeader(status) 41 | }) 42 | return s 43 | } 44 | 45 | func StartNewWithFile(file string) *Server { 46 | s := New() 47 | s.URL = s.Start() 48 | 49 | var output string 50 | b, err := ioutil.ReadFile(file) 51 | if err != nil { 52 | output = err.Error() 53 | } else { 54 | output = string(b) 55 | } 56 | 57 | s.Enqueue(func(w http.ResponseWriter, r *http.Request) { 58 | fmt.Fprintln(w, output) 59 | }) 60 | 61 | return s 62 | } 63 | 64 | func (s *Server) Start() string { 65 | s.testServer = httptest.NewServer(s) 66 | s.URL = s.testServer.URL 67 | return s.testServer.URL 68 | } 69 | 70 | func (s *Server) Stop() { 71 | s.testServer.Close() 72 | } 73 | 74 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 75 | go func() { 76 | s.requests <- r 77 | }() 78 | 79 | select { 80 | case h := <-s.handlers: 81 | h.ServeHTTP(w, r) 82 | default: 83 | w.WriteHeader(200) 84 | } 85 | } 86 | 87 | func (s *Server) Enqueue(h http.HandlerFunc) { 88 | go func() { 89 | s.handlers <- h 90 | }() 91 | } 92 | 93 | func (s *Server) TakeRequest() *http.Request { 94 | return <-s.requests 95 | } 96 | 97 | func (s *Server) TakeRequestWithTimeout(duration time.Duration) *http.Request { 98 | select { 99 | case r := <-s.requests: 100 | return r 101 | case <-time.After(duration): 102 | return nil 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /pkg/userdir/userdir.go: -------------------------------------------------------------------------------- 1 | package userdir 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | ) 7 | 8 | // Key returns the env var name for the user's home dir 9 | func Key() string { 10 | if runtime.GOOS == "windows" { 11 | return "USERPROFILE" 12 | } 13 | return "HOME" 14 | } 15 | 16 | // Get returns the home directory of the current user 17 | func Get() string { 18 | return os.Getenv(Key()) 19 | } 20 | -------------------------------------------------------------------------------- /samples/docker-compose-params.yml: -------------------------------------------------------------------------------- 1 | web: 2 | image: ubuntu 3 | links: 4 | - redis 5 | redis: 6 | image: redis 7 | ports: 8 | - "${REDIS_PORT}:${REDIS_PORT}" 9 | -------------------------------------------------------------------------------- /samples/docker-compose.yml: -------------------------------------------------------------------------------- 1 | web: 2 | image: ubuntu 3 | links: 4 | - redis 5 | redis: 6 | image: redis 7 | ports: 8 | - "6379:6379" 9 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | // Common utilities 2 | package utils 3 | 4 | import ( 5 | "bytes" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | var ( 13 | urlPrefix []string = []string{"http://", "https://"} 14 | ) 15 | 16 | func TrimRootPath(id string) string { 17 | if strings.HasPrefix(id, "/") { 18 | return strings.TrimPrefix(id, "/") 19 | } 20 | return id 21 | } 22 | 23 | func Contains(elements []string, value string) bool { 24 | for _, element := range elements { 25 | if element == value { 26 | return true 27 | } 28 | } 29 | return false 30 | } 31 | 32 | func BuildPath(host string, elements []string) string { 33 | var buffer bytes.Buffer 34 | buffer.WriteString(TrimRootPath(host)) 35 | for _, e := range elements { 36 | buffer.WriteString("/") 37 | buffer.WriteString(e) 38 | } 39 | return buffer.String() 40 | } 41 | 42 | func ConcatInts(iarr []int) string { 43 | var b bytes.Buffer 44 | for idx, i := range iarr { 45 | if idx > 0 { 46 | b.WriteString(" ,") 47 | } 48 | b.WriteString(strconv.Itoa(i)) 49 | } 50 | return b.String() 51 | } 52 | 53 | /** Concats a string array of ids in the form of /id with an output string of id, id2, etc */ 54 | func ConcatIdentifiers(ids []string) string { 55 | if ids == nil { 56 | return "" 57 | } 58 | var b bytes.Buffer 59 | for idx, id := range ids { 60 | if idx > 0 { 61 | b.WriteString(" ,") 62 | } 63 | b.WriteString(id) 64 | } 65 | return b.String() 66 | } 67 | 68 | func HasURLScheme(url string) bool { 69 | return HasPrefix(url, urlPrefix...) 70 | } 71 | 72 | func HasPrefix(str string, prefixes ...string) bool { 73 | for _, prefix := range prefixes { 74 | if strings.HasPrefix(str, prefix) { 75 | return true 76 | } 77 | } 78 | return false 79 | } 80 | 81 | func ElapsedStr(d time.Duration) string { 82 | return fmt.Sprintf("%0.2f sec(s)", d.Seconds()) 83 | } 84 | 85 | func IntInSlice(a int, list []int) bool { 86 | for _, b := range list { 87 | if b == a { 88 | return true 89 | } 90 | } 91 | return false 92 | } 93 | 94 | func StringInSlice(a string, list []string) bool { 95 | for _, b := range list { 96 | if b == a { 97 | return true 98 | } 99 | } 100 | return false 101 | } 102 | 103 | func MapStringKeysToSlice(m map[string]string) []string { 104 | keys := make([]string, 0, len(m)) 105 | for k := range m { 106 | keys = append(keys, k) 107 | } 108 | return keys 109 | } 110 | --------------------------------------------------------------------------------