├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE.md ├── Makefile ├── README.md ├── actions ├── component_actions.go ├── component_actions_test.go ├── general_actions.go ├── workspace_actions.go └── workspace_actions_test.go ├── cmd └── elc.go ├── core ├── bootstrap.go ├── component.go ├── component_config.go ├── context.go ├── core.go ├── git.go ├── home-config.go ├── mock_pc.go ├── pc.go ├── workspace.go └── workspace_config.go ├── doc └── commands.md ├── examples ├── php-separated-fpms │ ├── apps │ │ ├── app1 │ │ │ └── public │ │ │ │ └── index.php │ │ └── app2 │ │ │ └── public │ │ │ └── index.php │ ├── home │ │ └── .gitignore │ ├── infra │ │ └── proxy │ │ │ └── docker-compose.yml │ ├── templates │ │ └── fpm-8.1 │ │ │ ├── docker-compose.yml │ │ │ ├── nginx │ │ │ └── default.conf.template │ │ │ └── php │ │ │ └── Dockerfile │ └── workspace.yaml └── php-shared-fpm │ ├── apps │ ├── app1 │ │ └── public │ │ │ └── index.php │ └── app2 │ │ └── public │ │ └── index.php │ ├── home │ └── .gitignore │ ├── infra │ ├── fpm │ │ ├── docker-compose.yml │ │ ├── nginx │ │ │ ├── app1.conf.template │ │ │ └── app2.conf.template │ │ └── php │ │ │ └── Dockerfile │ └── proxy │ │ └── docker-compose.yml │ └── workspace.yaml ├── gen.sh ├── get.sh ├── go.mod ├── main.go └── version.sh /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ "*" ] 6 | pull_request: 7 | branches: [ "*" ] 8 | 9 | env: 10 | GO111MODULE: on 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Go 18 | uses: actions/setup-go@v2 19 | with: 20 | go-version: 1.19.13 21 | - name: Install dependencies 22 | run: | 23 | go get . 24 | - name: Install tools 25 | run: | 26 | go install github.com/golang/mock/mockgen@v1.6.0 27 | - name: Generate mocks 28 | run: | 29 | ./gen.sh 30 | - name: Build 31 | run: go build -v ./... 32 | - name: Test 33 | run: go test -v ./... -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | build 3 | dev 4 | go.sum 5 | example/env.yaml 6 | *.out 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION := $(shell ./version.sh) 2 | 3 | .PHONY: all build gen deps test coverage 4 | 5 | all: build 6 | 7 | build: deps 8 | go build -o build/elc -ldflags="-X 'github.com/ensi-platform/elc/core.Version=${VERSION}'" main.go 9 | 10 | deps: 11 | go get 12 | 13 | install: 14 | mkdir -p /opt/elc 15 | sudo cp ./build/elc /opt/elc/elc-v${VERSION} 16 | sudo ln -sf /opt/elc/elc-v${VERSION} /usr/local/bin/elc 17 | 18 | test: 19 | go test -v ./... 20 | 21 | coverage: 22 | go test -coverprofile=coverage.out -v ./... 23 | go tool cover -html=coverage.out 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ELC - Ensi Local Ctl 2 | 3 | [![Test](https://github.com/ensi-platform/elc/actions/workflows/test.yml/badge.svg)](https://github.com/ensi-platform/elc/actions/workflows/test.yml) 4 | 5 | ELC - инструмент для развёртывания микросервисов на машине разработчика, целью которого является запуск всех необходимых для разработки 6 | программ в контейнере. 7 | 8 | Особенности: 9 | - позволяет описать конфигурацию запуска всех компонентов системы в одном месте, упрощая начало работы с проектом до клонирования одного репозитория 10 | - упрощает конфигурирование, вводя свой набор переменных, часть из которых рассчитывается на лету 11 | - сокращает количество и размер команд, необходимых для запуска проекта 12 | - позволяет запускать .git хуки в контейнере 13 | 14 | ## Установка (Linux, WSL) 15 | 16 | ```bash 17 | curl -sSL https://raw.githubusercontent.com/ensi-platform/elc/master/get.sh | sudo bash 18 | ``` 19 | 20 | ## Сборка из исходников 21 | 22 | Зависимости: 23 | - go 24 | - make 25 | 26 | ```bash 27 | git clone git@github.com:ensi-platform/elc.git 28 | cd elc 29 | 30 | make 31 | make install 32 | ``` 33 | 34 | ## Использование 35 | 36 | ### Workspace 37 | 38 | Workspace - это описание сервисов системы; файлы конфигурации, необходимые для запуска севрисов; данные сервисов. 39 | Workspace - это папка в которой находится файл workspace.yaml. 40 | 41 | Структура файла: 42 | 43 | ```yaml 44 | name: elc-example-1 # название воркспейса, используется для генерации названий контейнеров/доменов 45 | elc_min_version: 0.2.3 # минимальная версия elc необхоимая для запуска этого воркспейса 46 | variables: # глобальные переменные 47 | DEFAULT_APPS_ROOT: ${WORKSPACE_PATH}/apps 48 | APPS_ROOT: ${APPS_ROOT:-$DEFAULT_APPS_ROOT} 49 | NETWORK: ${NETWORK:-example} 50 | BASE_DOMAIN: ${BASE_DOMAIN:-example.127.0.0.1.nip.io} 51 | GROUP_ID: ${GROUP_ID:-1000} 52 | USER_ID: ${USER_ID:-1000} 53 | HOME_PATH: ${WORKSPACE_PATH}/home 54 | 55 | templates: # шаблоны сервисов 56 | fpm-8.1: # название шаблона 57 | path: ${WORKSPACE_PATH}/templates/fpm-8.1 # путь до папки шаблона 58 | compose_file: ${TPL_PATH}/docker-compose.yml 59 | after_clone_hook: ${TPL_PATH}/hooks/after-clone.sh 60 | variables: # переменные шаблона 61 | APP_IMAGE: fpm-8.1:latest 62 | BASE_IMAGE: php:8.1-fpm-alpine 63 | NGINX_IMAGE: nginx:1.19-alpine 64 | 65 | services: # список сервисов 66 | proxy: # название сервиса 67 | path: ${WORKSPACE_PATH}/infra/proxy # путь до папки сервиса (корень git репозитория) 68 | variables: 69 | APP_IMAGE: jwilder/nginx-proxy:latest 70 | 71 | app1: 72 | path: ${APPS_ROOT}/app1 73 | extends: fpm-8.1 # использование шаблона 74 | repository: git@github.com:example/app1.git 75 | tags: 76 | - frontend 77 | dependencies: # зависимости сервиса (другие сервисы, которые надо запустить) 78 | app2: [default] # в режиме default надо запустить сервис app2 79 | app2: 80 | path: ${APPS_ROOT}/app2 81 | compose_file: ${SVC_PATH}/docker-compose.yml 82 | tags: 83 | - backend 84 | extends: fpm-8.1 85 | 86 | modules: # список модулей (пакетов, которые сами не могут быть запущены) 87 | package1: 88 | path: /path/to/package/on/host 89 | hosted_in: app1 # название сервиса в контейнере которого надо выполнять команды для работы с пакетом 90 | exec_path: /path/to/package/in/container 91 | ``` 92 | 93 | ### Основные понятия 94 | 95 | **Сервис** - папка с docker-compose.yml файлом и дополнительными конфигами. В описании сервиса вы можете указать путь до папки, 96 | путь до файла docker-compose.yml и список переменных, которые будут доступны в файле docker-compose.yml. 97 | 98 | **Переменная** - может быть задана на уровне сервиса, на уровне шаблона, глобально или через файл env.yaml. При запуске серивса в файле docker-compose.yml 99 | будут доступны все переменные в этой цепочке. 100 | В качестве значений переменных можно указывать другие переменные: `MY_VAR: ${MY_OTHER_VAR}`. 101 | Кроме того можно указывать значение по умолчанию, если переменная не определена: `MY_VAR: ${MY_OTHER_VAR:-default value}`. 102 | Значением по умолчанию может быть даже другая переменная: `MY_VAR: ${MY_OTHER_VAR:-$ANOTHER_VAR}`. 103 | Ссылаться можно только на переменные, которые определены выше текущей. 104 | 105 | **Шаблон** - тоже что и сервис, только на него можно ссылаться из сервиса чтобы наследовать значения. 106 | 107 | **Модуль** - папка с файлами, которые не являются самостоятельным сервисом, но могут быть примонтированы в контейнер сервиса. 108 | Модуль нужен, когда вы хотите, находясь в в папке на хосте, запустить инструмент в контейнере. Для этого вы указываете сервис, чей контейнер использовать, 109 | и путь внутри этого контейнера. 110 | Монтировать папку модуля в контейнер сервиса нужно самостоятельно через docker-compose.yml файл. 111 | 112 | **Режим и Зависимости** 113 | Зависимости сервиса - это другие сервисы, которые должны быть запущены перед тем как будет запущен сам сервис. 114 | Не всегда сервису необходимы все зависимости, поэтому для зависимостей можно указывать в каких режимах их запускать. 115 | Например в режиме dev сервису нужны database и proxy, а в режиме benchmark ещё нужны app2 и app3. 116 | По умолчанию используется режим `default`. Git-хуки выполняются в режиме `hook`. 117 | 118 | **Тэги** 119 | Многие команды можно применить сразу к нескольким сервисам. Чтобы обозначить какой-то часто используемый набор сервисов, 120 | можно назначить им одинаковый тэг и в дальшейгем, вместо перечисления названий сервисов в команде можно использовать флаг `--tag=`. 121 | 122 | ## Возможности ELC 123 | 124 | [Список всех команд](/doc/commands.md) 125 | 126 | **Управление воркспейсами** 127 | 128 | Перед тем как работать с сервисами воркспейса, воркспейс нужно зарегистрировать. 129 | 130 | ```bash 131 | elc workspace add project1 /path/to/project1/workspace 132 | elc workspace set-root project1 /path/to/project1 133 | ``` 134 | 135 | Далее есть два варианта работы с воркспейсами. Первый - включить режим автоматического определения воркспейса на основании 136 | того в какой папке вы находитесь. 137 | ``` 138 | elc workspace select auto 139 | ``` 140 | Второй вариант - это явно выбрать один воркспейс 141 | ``` 142 | elc workspace select project1 143 | ``` 144 | 145 | Кроме того, вы всегда можете указать в каком воркспейсе выполнить действие указав опцию `--workspace=project1`. 146 | 147 | **Управление процессами** 148 | 149 | ```bash 150 | elc start app1 151 | elc destroy app1 # оставновить и удалить контейнеры сервиса 152 | elc restart app1 153 | elc restart --hard app1 # удалить контейнеры сервиса и создать снова 154 | ``` 155 | 156 | Всё то же самое можно делать находясь в папке сервиса не указывая его название 157 | ```bash 158 | elc start 159 | elc stop 160 | ``` 161 | 162 | Можно указать сразу несколько сервисов перечислив их имена или используя тэг 163 | ```bash 164 | elc start app1 app2 app3 165 | elc start --tag=core-services 166 | ``` 167 | 168 | Можно указать режим запуска сервиса 169 | ```bash 170 | elc start --mode=benchmark 171 | ``` 172 | 173 | **Выполнение команд в контейнере** 174 | 175 | ```bash 176 | elc exec 177 | elc exec composer install 178 | ``` 179 | 180 | Вы можете войти в контейнер запусти в нём shell 181 | ```bash 182 | host$ elc exec bash 183 | app1# composer install 184 | ``` 185 | 186 | Команду exec можно опустить - все нераспознанные команды считаются аргументами для exec 187 | ```bash 188 | elc composer install 189 | elc bash 190 | ``` 191 | 192 | Можно выполнить команду в другом сервисе (не в текущей папке) 193 | ```bash 194 | elc --component=db psql 195 | elc -c db psql 196 | ``` 197 | Или даже в другом воркспейсе 198 | ```bash 199 | elc --workspace=project2 --component=db psql 200 | elc -w project2 -c db psql 201 | ``` 202 | 203 | **Git хуки** 204 | 205 | Часто для выполнения хуков гита нужна та же среда что и для работы сервиса, соответственно и хуки должны выполняться внутри контейнера. 206 | Для этого elc умеет генерировать хуки, которые будут запускать скрипты расположенные особым образом в репозитории сервиса. 207 | 208 | ```bash 209 | elc set-hooks ./hooks-dir 210 | ``` 211 | 212 | Папка hooks-dir должна иметь следуюзую структуру: 213 | ``` 214 | ./hooks-dir/ 215 | ├── pre-commit 216 | │ ├── lint-openapi.sh 217 | │ ├── lint-php.sh 218 | │ └── php-cs-fixer.sh 219 | └── pre-push 220 | ├── composer-validate.sh 221 | ├── test-code.sh 222 | └── var-dump-checker.sh 223 | ``` 224 | Т.е. название подпапки - это название хука, а внутри сколько угодно скриптов, которые будут выполены при запуске хука. 225 | 226 | **Прочее** 227 | 228 | Вы можете выполнить любую команду docker-compose в рамках текущего сервиса 229 | ```bash 230 | elc compose 231 | elc compose logs -f app 232 | ``` 233 | 234 | ## License 235 | 236 | Distributed under the MIT License. See [LICENSE.md](LICENSE.md). -------------------------------------------------------------------------------- /actions/component_actions.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/ensi-platform/elc/core" 7 | ) 8 | 9 | func resolveCompNames(ws *core.Workspace, options *core.GlobalOptions, namesFromArgs []string) ([]string, error) { 10 | var compNames []string 11 | 12 | if options.Tag != "" { 13 | compNames = ws.FindComponentNamesByTag(options.Tag) 14 | if len(compNames) == 0 { 15 | return nil, errors.New(fmt.Sprintf("components with tag %s not found", options.Tag)) 16 | } 17 | } else if options.ComponentName != "" { 18 | compNames = []string{options.ComponentName} 19 | } else if len(namesFromArgs) > 0 { 20 | compNames = namesFromArgs 21 | } else { 22 | currentCompName, err := ws.ComponentNameByPath() 23 | if err != nil { 24 | return nil, err 25 | } 26 | compNames = []string{currentCompName} 27 | } 28 | 29 | return compNames, nil 30 | } 31 | 32 | func ListCompNames(ws *core.Workspace, options *core.GlobalOptions) ([]string, error) { 33 | var compNames []string 34 | if options.Tag == "" { 35 | return ws.GetComponentNamesList(), nil 36 | } 37 | 38 | compNames = ws.FindComponentNamesByTag(options.Tag) 39 | if len(compNames) == 0 { 40 | return nil, errors.New(fmt.Sprintf("components with tag %s not found", options.Tag)) 41 | } 42 | 43 | return compNames, nil 44 | } 45 | 46 | func StartServiceAction(options *core.GlobalOptions, svcNames []string) error { 47 | ws, err := core.GetWorkspaceConfig(options.WorkspaceName) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | compNames, err := resolveCompNames(ws, options, svcNames) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | for _, compName := range compNames { 58 | fmt.Printf("# component: %s\n", compName) 59 | comp, err := ws.ComponentByName(compName) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | err = comp.Start(options) 65 | if err != nil { 66 | fmt.Printf("Error: %s\n", err) 67 | } 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func StopServiceAction(stopAll bool, svcNames []string, destroy bool, options *core.GlobalOptions) error { 74 | ws, err := core.GetWorkspaceConfig(options.WorkspaceName) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | var compNames []string 80 | 81 | if stopAll { 82 | compNames = ws.GetComponentNames() 83 | } else { 84 | compNames, err = resolveCompNames(ws, options, svcNames) 85 | if err != nil { 86 | return err 87 | } 88 | } 89 | 90 | for _, compName := range compNames { 91 | fmt.Printf("# component: %s\n", compName) 92 | comp, err := ws.ComponentByName(compName) 93 | if err != nil { 94 | return err 95 | } 96 | if destroy { 97 | err = comp.Destroy(options) 98 | } else { 99 | err = comp.Stop(options) 100 | } 101 | if err != nil { 102 | fmt.Printf("Error: %s\n", err) 103 | } 104 | } 105 | 106 | return nil 107 | } 108 | 109 | func RestartServiceAction(hardRestart bool, svcNames []string, options *core.GlobalOptions) error { 110 | ws, err := core.GetWorkspaceConfig(options.WorkspaceName) 111 | if err != nil { 112 | return err 113 | } 114 | 115 | compNames, err := resolveCompNames(ws, options, svcNames) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | for _, compName := range compNames { 121 | fmt.Printf("# component: %s\n", compName) 122 | comp, err := ws.ComponentByName(compName) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | err = comp.Restart(hardRestart, options) 128 | if err != nil { 129 | fmt.Printf("Error: %s\n", err) 130 | } 131 | } 132 | 133 | return nil 134 | } 135 | 136 | func PrintVarsAction(options *core.GlobalOptions, svcNames []string) error { 137 | ws, err := core.GetWorkspaceConfig(options.WorkspaceName) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | compNames, err := resolveCompNames(ws, options, svcNames) 143 | if err != nil { 144 | return err 145 | } 146 | 147 | if len(compNames) > 1 { 148 | return errors.New("too many components for show") 149 | } 150 | 151 | comp, err := ws.ComponentByName(compNames[0]) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | err = comp.DumpVars() 157 | if err != nil { 158 | return err 159 | } 160 | 161 | return nil 162 | } 163 | 164 | func ComposeCommandAction(options *core.GlobalOptions, args []string) error { 165 | ws, err := core.GetWorkspaceConfig(options.WorkspaceName) 166 | if err != nil { 167 | return err 168 | } 169 | 170 | compNames, err := resolveCompNames(ws, options, []string{}) 171 | if err != nil { 172 | return err 173 | } 174 | 175 | if len(compNames) > 1 { 176 | return errors.New("too many components") 177 | } 178 | 179 | comp, err := ws.ComponentByName(compNames[0]) 180 | if err != nil { 181 | return err 182 | } 183 | 184 | options.Cmd = args 185 | 186 | _, err = comp.Compose(options) 187 | if err != nil { 188 | return err 189 | } 190 | 191 | return nil 192 | } 193 | 194 | func WrapCommandAction(options *core.GlobalOptions, command []string) error { 195 | ws, err := core.GetWorkspaceConfig(options.WorkspaceName) 196 | if err != nil { 197 | return err 198 | } 199 | 200 | compNames, err := resolveCompNames(ws, options, []string{}) 201 | if err != nil { 202 | return err 203 | } 204 | 205 | if len(compNames) > 1 { 206 | return errors.New("too many components") 207 | } 208 | 209 | comp, err := ws.ComponentByName(compNames[0]) 210 | if err != nil { 211 | return err 212 | } 213 | 214 | var hostName string 215 | 216 | if comp.Config.HostedIn != "" { 217 | hostName = comp.Config.HostedIn 218 | } else { 219 | hostName = comp.Name 220 | } 221 | 222 | hostComp, err := ws.ComponentByName(hostName) 223 | if err != nil { 224 | return err 225 | } 226 | 227 | _, err = hostComp.Wrap(command, options) 228 | if err != nil { 229 | return err 230 | } 231 | 232 | return nil 233 | } 234 | 235 | func ExecAction(options *core.GlobalOptions) error { 236 | ws, err := core.GetWorkspaceConfig(options.WorkspaceName) 237 | if err != nil { 238 | return err 239 | } 240 | 241 | compNames, err := resolveCompNames(ws, options, []string{}) 242 | if err != nil { 243 | return err 244 | } 245 | 246 | if len(compNames) > 1 { 247 | return errors.New("too many components") 248 | } 249 | 250 | comp, err := ws.ComponentByName(compNames[0]) 251 | if err != nil { 252 | return err 253 | } 254 | 255 | var hostName string 256 | 257 | if comp.Config.HostedIn != "" { 258 | hostName = comp.Config.HostedIn 259 | } else { 260 | hostName = comp.Name 261 | } 262 | 263 | hostComp, err := ws.ComponentByName(hostName) 264 | if err != nil { 265 | return err 266 | } 267 | 268 | if comp.Config.ExecPath != "" { 269 | options.WorkingDir, err = ws.Context.RenderString(comp.Config.ExecPath) 270 | if err != nil { 271 | return err 272 | } 273 | } 274 | 275 | _, err = hostComp.Exec(options) 276 | if err != nil { 277 | return err 278 | } 279 | 280 | return nil 281 | } 282 | 283 | func RunAction(options *core.GlobalOptions) error { 284 | ws, err := core.GetWorkspaceConfig(options.WorkspaceName) 285 | if err != nil { 286 | return err 287 | } 288 | 289 | compNames, err := resolveCompNames(ws, options, []string{}) 290 | if err != nil { 291 | return err 292 | } 293 | 294 | if len(compNames) > 1 { 295 | return errors.New("too many components") 296 | } 297 | 298 | comp, err := ws.ComponentByName(compNames[0]) 299 | if err != nil { 300 | return err 301 | } 302 | 303 | var hostName string 304 | 305 | if comp.Config.HostedIn != "" { 306 | hostName = comp.Config.HostedIn 307 | } else { 308 | hostName = comp.Name 309 | } 310 | 311 | hostComp, err := ws.ComponentByName(hostName) 312 | if err != nil { 313 | return err 314 | } 315 | 316 | if comp.Config.ExecPath != "" { 317 | options.WorkingDir, err = ws.Context.RenderString(comp.Config.ExecPath) 318 | if err != nil { 319 | return err 320 | } 321 | } 322 | 323 | _, err = hostComp.Run(options) 324 | if err != nil { 325 | return err 326 | } 327 | 328 | return nil 329 | } 330 | 331 | func SetGitHooksAction(options *core.GlobalOptions, scriptsFolder string, elcBinary string) error { 332 | ws, err := core.GetWorkspaceConfig(options.WorkspaceName) 333 | if err != nil { 334 | return err 335 | } 336 | 337 | compNames, err := resolveCompNames(ws, options, []string{}) 338 | if err != nil { 339 | return err 340 | } 341 | 342 | for _, compName := range compNames { 343 | fmt.Printf("# component: %s\n", compName) 344 | comp, err := ws.ComponentByName(compName) 345 | if err != nil { 346 | return err 347 | } 348 | 349 | err = comp.UpdateHooks(options, elcBinary, scriptsFolder) 350 | if err != nil { 351 | fmt.Printf("Error: %s\n", err) 352 | } 353 | } 354 | 355 | return nil 356 | } 357 | 358 | func CloneComponentAction(options *core.GlobalOptions, svcNames []string, noHook bool) error { 359 | ws, err := core.GetWorkspaceConfig(options.WorkspaceName) 360 | if err != nil { 361 | return err 362 | } 363 | 364 | compNames, err := resolveCompNames(ws, options, svcNames) 365 | if err != nil { 366 | return err 367 | } 368 | 369 | for _, compName := range compNames { 370 | fmt.Printf("# component: %s\n", compName) 371 | comp, err := ws.ComponentByName(compName) 372 | if err != nil { 373 | return err 374 | } 375 | 376 | err = comp.Clone(options, noHook) 377 | if err != nil { 378 | fmt.Printf("Error: %s\n", err) 379 | } 380 | } 381 | 382 | return nil 383 | } 384 | 385 | func ListServicesAction(options *core.GlobalOptions) error { 386 | ws, err := core.GetWorkspaceConfig(options.WorkspaceName) 387 | if err != nil { 388 | return err 389 | } 390 | 391 | compNames, err := ListCompNames(ws, options) 392 | if err != nil { 393 | return err 394 | } 395 | 396 | for _, compName := range compNames { 397 | _, _ = core.Pc.Println(compName) 398 | } 399 | 400 | return nil 401 | } 402 | -------------------------------------------------------------------------------- /actions/component_actions_test.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "github.com/ensi-platform/elc/core" 5 | "github.com/golang/mock/gomock" 6 | "path" 7 | "testing" 8 | ) 9 | 10 | const fakeHomeConfigPath = "/tmp/home/.elc.yaml" 11 | const fakeWorkspacePath = "/tmp/workspaces/project1" 12 | 13 | const baseHomeConfig = ` 14 | current_workspace: project1 15 | update_command: update 16 | workspaces: 17 | - name: project1 18 | path: /tmp/workspaces/project1 19 | - name: project2 20 | path: /tmp/workspaces/project2 21 | ` 22 | 23 | func setupMockPc(t *testing.T) *core.MockPC { 24 | ctrl := gomock.NewController(t) 25 | defer ctrl.Finish() 26 | 27 | mockPc := core.NewMockPC(ctrl) 28 | core.Pc = mockPc 29 | return mockPc 30 | } 31 | 32 | func expectReadHomeConfig(mockPC *core.MockPC) { 33 | mockPC.EXPECT().HomeDir().Return("/tmp/home", nil) 34 | mockPC.EXPECT().FileExists(fakeHomeConfigPath).Return(true) 35 | mockPC.EXPECT().ReadFile(fakeHomeConfigPath).Return([]byte(baseHomeConfig), nil) 36 | } 37 | 38 | func expectReadWorkspaceConfig(mockPC *core.MockPC, workspacePath string, config string, env string) { 39 | configPath := path.Join(workspacePath, "workspace.yaml") 40 | envPath := path.Join(workspacePath, "env.yaml") 41 | mockPC.EXPECT().Getwd(). 42 | Return(path.Join(workspacePath, "apps/test"), nil) 43 | mockPC.EXPECT().ReadFile(configPath). 44 | Return([]byte(config), nil) 45 | 46 | envExists := env != "" 47 | mockPC.EXPECT().FileExists(envPath). 48 | Return(envExists) 49 | if envExists { 50 | mockPC.EXPECT().ReadFile(envPath). 51 | Return([]byte(env), nil) 52 | } 53 | } 54 | 55 | const workspaceConfig = ` 56 | name: ensi 57 | services: 58 | test: 59 | path: "${WORKSPACE_PATH}/apps/test" 60 | ` 61 | 62 | func TestServiceStart(t *testing.T) { 63 | mockPc := setupMockPc(t) 64 | expectReadHomeConfig(mockPc) 65 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfig, "") 66 | 67 | composeFilePath := path.Join(fakeWorkspacePath, "apps/test/docker-compose.yml") 68 | 69 | mockPc.EXPECT(). 70 | FileExists(gomock.Any()). 71 | Return(true) 72 | 73 | mockPc.EXPECT(). 74 | ExecToString([]string{"docker", "compose", "-f", composeFilePath, "ps", "--status=running", "-q"}, gomock.Any()). 75 | Return(0, "", nil) 76 | 77 | mockPc.EXPECT(). 78 | ExecInteractive([]string{"docker", "compose", "-f", composeFilePath, "up", "-d"}, gomock.Any()). 79 | Return(0, nil) 80 | 81 | _ = StartServiceAction(&core.GlobalOptions{}, []string{}) 82 | } 83 | 84 | const workspaceConfigWithDeps = `name: ensi 85 | variables: 86 | USER_ID: "1000" 87 | GROUP_ID: "1000" 88 | aliases: 89 | als: dep3 90 | services: 91 | dep1: 92 | path: "${WORKSPACE_PATH}/apps/dep1" 93 | dep2: 94 | path: "${WORKSPACE_PATH}/apps/dep2" 95 | dep3: 96 | path: "${WORKSPACE_PATH}/apps/dep3" 97 | test: 98 | path: "${WORKSPACE_PATH}/apps/test" 99 | dependencies: 100 | dep1: [default] 101 | dep2: [default, hook] 102 | dep3: [] 103 | ` 104 | 105 | func expectStartService(mockPC *core.MockPC, composeFilePath string) { 106 | mockPC.EXPECT(). 107 | FileExists(gomock.Any()). 108 | Return(true) 109 | 110 | mockPC.EXPECT(). 111 | ExecToString([]string{"docker", "compose", "-f", composeFilePath, "ps", "--status=running", "-q"}, gomock.Any()). 112 | Return(0, "", nil) 113 | 114 | mockPC.EXPECT(). 115 | ExecInteractive([]string{"docker", "compose", "-f", composeFilePath, "up", "-d"}, gomock.Any()). 116 | Return(0, nil) 117 | } 118 | 119 | func expectStopService(mockPC *core.MockPC, composeFilePath string) { 120 | mockPC.EXPECT(). 121 | FileExists(gomock.Any()). 122 | Return(true) 123 | 124 | mockPC.EXPECT(). 125 | ExecToString([]string{"docker", "compose", "-f", composeFilePath, "ps", "--status=running", "-q"}, gomock.Any()). 126 | Return(0, "asdasd", nil) 127 | 128 | mockPC.EXPECT(). 129 | ExecInteractive([]string{"docker", "compose", "-f", composeFilePath, "stop"}, gomock.Any()). 130 | Return(0, nil) 131 | } 132 | 133 | func expectDestroyService(mockPC *core.MockPC, composeFilePath string) { 134 | mockPC.EXPECT(). 135 | FileExists(gomock.Any()). 136 | Return(true) 137 | 138 | mockPC.EXPECT(). 139 | ExecToString([]string{"docker", "compose", "-f", composeFilePath, "ps", "--status=running", "-q"}, gomock.Any()). 140 | Return(0, "asdasd", nil) 141 | 142 | mockPC.EXPECT(). 143 | ExecInteractive([]string{"docker", "compose", "-f", composeFilePath, "down"}, gomock.Any()). 144 | Return(0, nil) 145 | } 146 | 147 | func TestServiceStartDefaultMode(t *testing.T) { 148 | mockPc := setupMockPc(t) 149 | expectReadHomeConfig(mockPc) 150 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithDeps, "") 151 | 152 | expectStartService(mockPc, path.Join(fakeWorkspacePath, "apps/dep1/docker-compose.yml")) 153 | expectStartService(mockPc, path.Join(fakeWorkspacePath, "apps/dep2/docker-compose.yml")) 154 | expectStartService(mockPc, path.Join(fakeWorkspacePath, "apps/test/docker-compose.yml")) 155 | 156 | _ = StartServiceAction(&core.GlobalOptions{ 157 | Mode: "default", 158 | }, []string{}) 159 | } 160 | 161 | func TestServiceStartHookMode(t *testing.T) { 162 | mockPc := setupMockPc(t) 163 | expectReadHomeConfig(mockPc) 164 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithDeps, "") 165 | 166 | expectStartService(mockPc, path.Join(fakeWorkspacePath, "apps/dep2/docker-compose.yml")) 167 | expectStartService(mockPc, path.Join(fakeWorkspacePath, "apps/test/docker-compose.yml")) 168 | 169 | _ = StartServiceAction(&core.GlobalOptions{ 170 | Mode: "hook", 171 | }, []string{}) 172 | } 173 | 174 | func TestServiceStartByName(t *testing.T) { 175 | mockPc := setupMockPc(t) 176 | expectReadHomeConfig(mockPc) 177 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithDeps, "") 178 | 179 | expectStartService(mockPc, path.Join(fakeWorkspacePath, "apps/dep1/docker-compose.yml")) 180 | 181 | _ = StartServiceAction(&core.GlobalOptions{}, []string{"dep1"}) 182 | } 183 | 184 | func TestServiceStartByNames(t *testing.T) { 185 | mockPc := setupMockPc(t) 186 | expectReadHomeConfig(mockPc) 187 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithDeps, "") 188 | 189 | expectStartService(mockPc, path.Join(fakeWorkspacePath, "apps/dep1/docker-compose.yml")) 190 | expectStartService(mockPc, path.Join(fakeWorkspacePath, "apps/dep3/docker-compose.yml")) 191 | 192 | _ = StartServiceAction(&core.GlobalOptions{}, []string{"dep1", "dep3"}) 193 | } 194 | 195 | func TestServiceStartByAlias(t *testing.T) { 196 | mockPc := setupMockPc(t) 197 | expectReadHomeConfig(mockPc) 198 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithDeps, "") 199 | 200 | expectStartService(mockPc, path.Join(fakeWorkspacePath, "apps/dep3/docker-compose.yml")) 201 | 202 | _ = StartServiceAction(&core.GlobalOptions{}, []string{"als"}) 203 | } 204 | 205 | func TestServiceStop(t *testing.T) { 206 | mockPc := setupMockPc(t) 207 | expectReadHomeConfig(mockPc) 208 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithDeps, "") 209 | 210 | expectStopService(mockPc, path.Join(fakeWorkspacePath, "apps/test/docker-compose.yml")) 211 | 212 | _ = StopServiceAction(false, []string{}, false, &core.GlobalOptions{}) 213 | } 214 | 215 | func TestServiceStopByName(t *testing.T) { 216 | mockPc := setupMockPc(t) 217 | expectReadHomeConfig(mockPc) 218 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithDeps, "") 219 | 220 | expectStopService(mockPc, path.Join(fakeWorkspacePath, "apps/dep1/docker-compose.yml")) 221 | 222 | _ = StopServiceAction(false, []string{"dep1"}, false, &core.GlobalOptions{}) 223 | } 224 | 225 | func TestServiceStopByNames(t *testing.T) { 226 | mockPc := setupMockPc(t) 227 | expectReadHomeConfig(mockPc) 228 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithDeps, "") 229 | 230 | expectStopService(mockPc, path.Join(fakeWorkspacePath, "apps/dep1/docker-compose.yml")) 231 | expectStopService(mockPc, path.Join(fakeWorkspacePath, "apps/dep2/docker-compose.yml")) 232 | 233 | _ = StopServiceAction(false, []string{"dep1", "dep2"}, false, &core.GlobalOptions{}) 234 | } 235 | 236 | func TestServiceStopAll(t *testing.T) { 237 | mockPc := setupMockPc(t) 238 | expectReadHomeConfig(mockPc) 239 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithDeps, "") 240 | 241 | expectStopService(mockPc, path.Join(fakeWorkspacePath, "apps/dep1/docker-compose.yml")) 242 | expectStopService(mockPc, path.Join(fakeWorkspacePath, "apps/dep2/docker-compose.yml")) 243 | expectStopService(mockPc, path.Join(fakeWorkspacePath, "apps/dep3/docker-compose.yml")) 244 | expectStopService(mockPc, path.Join(fakeWorkspacePath, "apps/test/docker-compose.yml")) 245 | 246 | _ = StopServiceAction(true, []string{}, false, &core.GlobalOptions{}) 247 | } 248 | 249 | func TestServiceDestroy(t *testing.T) { 250 | mockPc := setupMockPc(t) 251 | expectReadHomeConfig(mockPc) 252 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithDeps, "") 253 | 254 | expectDestroyService(mockPc, path.Join(fakeWorkspacePath, "apps/test/docker-compose.yml")) 255 | 256 | _ = StopServiceAction(false, []string{}, true, &core.GlobalOptions{}) 257 | } 258 | 259 | func TestServiceDestroyByName(t *testing.T) { 260 | mockPc := setupMockPc(t) 261 | expectReadHomeConfig(mockPc) 262 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithDeps, "") 263 | 264 | expectDestroyService(mockPc, path.Join(fakeWorkspacePath, "apps/dep1/docker-compose.yml")) 265 | 266 | _ = StopServiceAction(false, []string{"dep1"}, true, &core.GlobalOptions{}) 267 | } 268 | 269 | func TestServiceDestroyByNames(t *testing.T) { 270 | mockPc := setupMockPc(t) 271 | expectReadHomeConfig(mockPc) 272 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithDeps, "") 273 | 274 | expectDestroyService(mockPc, path.Join(fakeWorkspacePath, "apps/dep1/docker-compose.yml")) 275 | expectDestroyService(mockPc, path.Join(fakeWorkspacePath, "apps/dep2/docker-compose.yml")) 276 | 277 | _ = StopServiceAction(false, []string{"dep1", "dep2"}, true, &core.GlobalOptions{}) 278 | } 279 | 280 | func TestServiceDestroyAll(t *testing.T) { 281 | mockPc := setupMockPc(t) 282 | expectReadHomeConfig(mockPc) 283 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithDeps, "") 284 | 285 | expectDestroyService(mockPc, path.Join(fakeWorkspacePath, "apps/dep1/docker-compose.yml")) 286 | expectDestroyService(mockPc, path.Join(fakeWorkspacePath, "apps/dep2/docker-compose.yml")) 287 | expectDestroyService(mockPc, path.Join(fakeWorkspacePath, "apps/dep3/docker-compose.yml")) 288 | expectDestroyService(mockPc, path.Join(fakeWorkspacePath, "apps/test/docker-compose.yml")) 289 | 290 | _ = StopServiceAction(true, []string{}, true, &core.GlobalOptions{}) 291 | } 292 | 293 | func TestServiceRestart(t *testing.T) { 294 | mockPc := setupMockPc(t) 295 | expectReadHomeConfig(mockPc) 296 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithDeps, "") 297 | 298 | expectStartService(mockPc, path.Join(fakeWorkspacePath, "apps/test/docker-compose.yml")) 299 | expectStopService(mockPc, path.Join(fakeWorkspacePath, "apps/test/docker-compose.yml")) 300 | 301 | _ = RestartServiceAction(false, []string{}, &core.GlobalOptions{}) 302 | } 303 | 304 | func TestServiceRestartHard(t *testing.T) { 305 | mockPc := setupMockPc(t) 306 | expectReadHomeConfig(mockPc) 307 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithDeps, "") 308 | 309 | expectStartService(mockPc, path.Join(fakeWorkspacePath, "apps/test/docker-compose.yml")) 310 | expectDestroyService(mockPc, path.Join(fakeWorkspacePath, "apps/test/docker-compose.yml")) 311 | 312 | _ = RestartServiceAction(true, []string{}, &core.GlobalOptions{}) 313 | } 314 | 315 | func TestServiceCompose(t *testing.T) { 316 | mockPc := setupMockPc(t) 317 | expectReadHomeConfig(mockPc) 318 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithDeps, "") 319 | 320 | mockPc.EXPECT(). 321 | FileExists(gomock.Any()). 322 | Return(true) 323 | 324 | mockPc.EXPECT(). 325 | ExecInteractive([]string{"docker", "compose", "-f", path.Join(fakeWorkspacePath, "apps/test/docker-compose.yml"), "some", "command"}, gomock.Any()). 326 | Return(0, nil) 327 | 328 | _ = ComposeCommandAction(&core.GlobalOptions{}, []string{"some", "command"}) 329 | } 330 | 331 | func TestServiceComposeByName(t *testing.T) { 332 | mockPc := setupMockPc(t) 333 | expectReadHomeConfig(mockPc) 334 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithDeps, "") 335 | 336 | mockPc.EXPECT(). 337 | FileExists(gomock.Any()). 338 | Return(true) 339 | 340 | mockPc.EXPECT(). 341 | ExecInteractive([]string{"docker", "compose", "-f", path.Join(fakeWorkspacePath, "apps/dep1/docker-compose.yml"), "some", "command"}, gomock.Any()). 342 | Return(0, nil) 343 | 344 | _ = ComposeCommandAction(&core.GlobalOptions{ 345 | ComponentName: "dep1", 346 | }, []string{"some", "command"}) 347 | } 348 | 349 | func TestServiceExec(t *testing.T) { 350 | mockPc := setupMockPc(t) 351 | expectReadHomeConfig(mockPc) 352 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithDeps, "") 353 | 354 | expectStartService(mockPc, path.Join(fakeWorkspacePath, "apps/test/docker-compose.yml")) 355 | mockPc.EXPECT(). 356 | IsTerminal(). 357 | Return(true) 358 | mockPc.EXPECT(). 359 | ExecInteractive([]string{"docker", "compose", "-f", path.Join(fakeWorkspacePath, "apps/test/docker-compose.yml"), "exec", "-u", "1000:1000", "app", "some", "command"}, gomock.Any()). 360 | Return(0, nil) 361 | 362 | _ = ExecAction(&core.GlobalOptions{ 363 | Cmd: []string{"some", "command"}, 364 | UID: -1, 365 | }) 366 | } 367 | 368 | func TestServiceExecWithoutTty(t *testing.T) { 369 | mockPc := setupMockPc(t) 370 | expectReadHomeConfig(mockPc) 371 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithDeps, "") 372 | 373 | expectStartService(mockPc, path.Join(fakeWorkspacePath, "apps/test/docker-compose.yml")) 374 | mockPc.EXPECT(). 375 | IsTerminal(). 376 | Return(false) 377 | mockPc.EXPECT(). 378 | ExecInteractive([]string{"docker", "compose", "-f", path.Join(fakeWorkspacePath, "apps/test/docker-compose.yml"), "exec", "-u", "1000:1000", "-T", "app", "some", "command"}, gomock.Any()). 379 | Return(0, nil) 380 | 381 | _ = ExecAction(&core.GlobalOptions{ 382 | Cmd: []string{"some", "command"}, 383 | UID: -1, 384 | }) 385 | } 386 | 387 | func TestServiceExecWithUid(t *testing.T) { 388 | mockPc := setupMockPc(t) 389 | expectReadHomeConfig(mockPc) 390 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithDeps, "") 391 | 392 | expectStartService(mockPc, path.Join(fakeWorkspacePath, "apps/test/docker-compose.yml")) 393 | mockPc.EXPECT(). 394 | IsTerminal(). 395 | Return(true) 396 | mockPc.EXPECT(). 397 | ExecInteractive([]string{"docker", "compose", "-f", path.Join(fakeWorkspacePath, "apps/test/docker-compose.yml"), "exec", "-u", "1001", "app", "some", "command"}, gomock.Any()). 398 | Return(0, nil) 399 | 400 | _ = ExecAction(&core.GlobalOptions{ 401 | Cmd: []string{"some", "command"}, 402 | UID: 1001, 403 | }) 404 | } 405 | 406 | func TestServiceRun(t *testing.T) { 407 | mockPc := setupMockPc(t) 408 | expectReadHomeConfig(mockPc) 409 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithDeps, "") 410 | 411 | mockPc.EXPECT(). 412 | FileExists(gomock.Any()). 413 | Return(true) 414 | 415 | mockPc.EXPECT(). 416 | IsTerminal(). 417 | Return(true) 418 | mockPc.EXPECT(). 419 | ExecInteractive([]string{"docker", "compose", "-f", path.Join(fakeWorkspacePath, "apps/test/docker-compose.yml"), "run", "--rm", "--entrypoint=''", "-u", "1000:1000", "app", "some", "command"}, gomock.Any()). 420 | Return(0, nil) 421 | 422 | _ = RunAction(&core.GlobalOptions{ 423 | Cmd: []string{"some", "command"}, 424 | UID: -1, 425 | }) 426 | } 427 | 428 | const workspaceConfigWithVars = ` 429 | name: ensi 430 | variables: 431 | V_GL: vglobal 432 | V_GL_SIMPLE_VAR: ${V_GL}-a 433 | V_GL_WITH_DEFAULT: ${UNDEFINED:-default} 434 | V_GL_WITH_DEFAULT_VAR: ${UNDEFINED:-$V_GL} 435 | services: 436 | test: 437 | path: "${WORKSPACE_PATH}/apps/test" 438 | variables: 439 | V_IN_SVC: vinsvc 440 | test1: 441 | path: "${WORKSPACE_PATH}/apps/test1" 442 | extends: tpl1 443 | variables: 444 | V_IN_SVC: vinsvc 445 | 446 | templates: 447 | tpl1: 448 | path: "${WORKSPACE_PATH}/templates/tpl1" 449 | variables: 450 | V_IN_TPL: vintpl 451 | ` 452 | 453 | func TestServiceVars(t *testing.T) { 454 | mockPc := setupMockPc(t) 455 | expectReadHomeConfig(mockPc) 456 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithVars, "") 457 | 458 | mockPc.EXPECT().Println("WORKSPACE_PATH=/tmp/workspaces/project1") 459 | mockPc.EXPECT().Println("WORKSPACE_NAME=ensi") 460 | 461 | mockPc.EXPECT().Println("V_GL=vglobal") 462 | mockPc.EXPECT().Println("V_GL_SIMPLE_VAR=vglobal-a") 463 | mockPc.EXPECT().Println("V_GL_WITH_DEFAULT=default") 464 | mockPc.EXPECT().Println("V_GL_WITH_DEFAULT_VAR=vglobal") 465 | 466 | mockPc.EXPECT().Println("APP_NAME=test") 467 | mockPc.EXPECT().Println("COMPOSE_PROJECT_NAME=ensi-test") 468 | mockPc.EXPECT().Println("SVC_PATH=/tmp/workspaces/project1/apps/test") 469 | mockPc.EXPECT().Println("COMPOSE_FILE=/tmp/workspaces/project1/apps/test/docker-compose.yml") 470 | 471 | mockPc.EXPECT().Println("V_IN_SVC=vinsvc") 472 | 473 | _ = PrintVarsAction(&core.GlobalOptions{}, []string{}) 474 | } 475 | 476 | func TestServiceVarsWithTpl(t *testing.T) { 477 | mockPc := setupMockPc(t) 478 | expectReadHomeConfig(mockPc) 479 | expectReadWorkspaceConfig(mockPc, fakeWorkspacePath, workspaceConfigWithVars, "") 480 | 481 | mockPc.EXPECT().Println("WORKSPACE_PATH=/tmp/workspaces/project1") 482 | mockPc.EXPECT().Println("WORKSPACE_NAME=ensi") 483 | 484 | mockPc.EXPECT().Println("V_GL=vglobal") 485 | mockPc.EXPECT().Println("V_GL_SIMPLE_VAR=vglobal-a") 486 | mockPc.EXPECT().Println("V_GL_WITH_DEFAULT=default") 487 | mockPc.EXPECT().Println("V_GL_WITH_DEFAULT_VAR=vglobal") 488 | 489 | mockPc.EXPECT().Println("V_IN_TPL=vintpl") 490 | 491 | mockPc.EXPECT().Println("TPL_PATH=/tmp/workspaces/project1/templates/tpl1") 492 | mockPc.EXPECT().Println("COMPOSE_FILE=/tmp/workspaces/project1/templates/tpl1/docker-compose.yml") 493 | mockPc.EXPECT().Println("APP_NAME=test1") 494 | mockPc.EXPECT().Println("COMPOSE_PROJECT_NAME=ensi-test1") 495 | mockPc.EXPECT().Println("SVC_PATH=/tmp/workspaces/project1/apps/test1") 496 | 497 | mockPc.EXPECT().Println("V_IN_SVC=vinsvc") 498 | 499 | _ = PrintVarsAction(&core.GlobalOptions{}, []string{"test1"}) 500 | } 501 | -------------------------------------------------------------------------------- /actions/general_actions.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ensi-platform/elc/core" 6 | ) 7 | 8 | func UpdateBinaryAction(version string) error { 9 | env := make([]string, 0) 10 | if version != "" { 11 | env = append(env, fmt.Sprintf("VERSION=%s", version)) 12 | } 13 | 14 | hc, err := core.CheckAndLoadHC() 15 | if err != nil { 16 | return err 17 | } 18 | 19 | _, err = core.Pc.ExecInteractive([]string{"bash", "-c", hc.UpdateCommand}, env) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | return nil 25 | } 26 | 27 | func FixUpdateBinaryCommandAction() error { 28 | hc, err := core.CheckAndLoadHC() 29 | if err != nil { 30 | return err 31 | } 32 | 33 | hc.UpdateCommand = core.DefaultUpdateCommand 34 | err = core.SaveHomeConfig(hc) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /actions/workspace_actions.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/ensi-platform/elc/core" 7 | ) 8 | 9 | func ListWorkspacesAction() error { 10 | hc, err := core.CheckAndLoadHC() 11 | if err != nil { 12 | return err 13 | } 14 | for _, workspace := range hc.Workspaces { 15 | _, _ = core.Pc.Printf("%-10s %s\n", workspace.Name, workspace.Path) 16 | } 17 | return nil 18 | } 19 | 20 | func AddWorkspaceAction(name string, wsPath string) error { 21 | hc, err := core.CheckAndLoadHC() 22 | if err != nil { 23 | return err 24 | } 25 | 26 | ws := hc.FindWorkspace(name) 27 | if ws != nil { 28 | return errors.New(fmt.Sprintf("workspace with name '%s' already exists", name)) 29 | } 30 | 31 | err = hc.AddWorkspace(name, wsPath) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | _, _ = core.Pc.Printf("workspace '%s' is added\n", name) 37 | 38 | if hc.CurrentWorkspace == "" { 39 | hc.CurrentWorkspace = name 40 | err = core.SaveHomeConfig(hc) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | _, _ = core.Pc.Printf("active workspace changed to '%s'\n", name) 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func RemoveWorkspaceAction(name string) error { 52 | hc, err := core.CheckAndLoadHC() 53 | if err != nil { 54 | return err 55 | } 56 | 57 | _, _ = core.Pc.Printf("workspace '%s' is removed\n", name) 58 | 59 | return hc.RemoveWorkspace(name) 60 | } 61 | 62 | func ShowCurrentWorkspaceAction(options *core.GlobalOptions) error { 63 | hc, err := core.CheckAndLoadHC() 64 | if err != nil { 65 | return err 66 | } 67 | hci, err := hc.GetCurrentWorkspace(options.WorkspaceName) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | _, _ = core.Pc.Println(hci.Name) 73 | return nil 74 | } 75 | 76 | func SelectWorkspaceAction(name string) error { 77 | hc, err := core.CheckAndLoadHC() 78 | if err != nil { 79 | return err 80 | } 81 | 82 | if name != "auto" { 83 | ws := hc.FindWorkspace(name) 84 | if ws == nil { 85 | return errors.New(fmt.Sprintf("workspace with name '%s' is not defined", name)) 86 | } 87 | } 88 | 89 | hc.CurrentWorkspace = name 90 | err = core.SaveHomeConfig(hc) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | _, _ = core.Pc.Printf("active workspace changed to '%s'\n", name) 96 | 97 | return nil 98 | } 99 | 100 | func SetRootPathAction(name string, rootPath string) error { 101 | hc, err := core.CheckAndLoadHC() 102 | if err != nil { 103 | return err 104 | } 105 | 106 | ws := hc.FindWorkspace(name) 107 | if ws == nil { 108 | return errors.New(fmt.Sprintf("workspace with name '%s' is not defined", name)) 109 | } 110 | 111 | ws.RootPath = rootPath 112 | err = core.SaveHomeConfig(hc) 113 | if err != nil { 114 | return err 115 | } 116 | 117 | _, _ = core.Pc.Printf("path saved\n") 118 | 119 | return nil 120 | } 121 | -------------------------------------------------------------------------------- /actions/workspace_actions_test.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "github.com/ensi-platform/elc/core" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestWorkspaceShow(t *testing.T) { 10 | mockPc := setupMockPc(t) 11 | expectReadHomeConfig(mockPc) 12 | 13 | mockPc.EXPECT().Println("project1") 14 | 15 | _ = ShowCurrentWorkspaceAction(&core.GlobalOptions{}) 16 | } 17 | 18 | func TestWorkspaceList(t *testing.T) { 19 | mockPc := setupMockPc(t) 20 | expectReadHomeConfig(mockPc) 21 | 22 | mockPc.EXPECT().Printf("%-10s %s\n", "project1", "/tmp/workspaces/project1") 23 | mockPc.EXPECT().Printf("%-10s %s\n", "project2", "/tmp/workspaces/project2") 24 | 25 | _ = ListWorkspacesAction() 26 | } 27 | 28 | func TestWorkspaceAdd(t *testing.T) { 29 | mockPc := setupMockPc(t) 30 | expectReadHomeConfig(mockPc) 31 | 32 | const homeConfigForAdd = `current_workspace: project1 33 | update_command: update 34 | workspaces: 35 | - name: project1 36 | path: /tmp/workspaces/project1 37 | root_path: "" 38 | - name: project2 39 | path: /tmp/workspaces/project2 40 | root_path: "" 41 | - name: project3 42 | path: /tmp/workspaces/project3 43 | root_path: "" 44 | ` 45 | 46 | mockPc.EXPECT().WriteFile(fakeHomeConfigPath, []byte(homeConfigForAdd), os.FileMode(0644)) 47 | mockPc.EXPECT().Printf("workspace '%s' is added\n", "project3") 48 | 49 | _ = AddWorkspaceAction("project3", "/tmp/workspaces/project3") 50 | } 51 | 52 | func TestWorkspaceSelect(t *testing.T) { 53 | mockPc := setupMockPc(t) 54 | expectReadHomeConfig(mockPc) 55 | 56 | const homeConfigForSelect = `current_workspace: project2 57 | update_command: update 58 | workspaces: 59 | - name: project1 60 | path: /tmp/workspaces/project1 61 | root_path: "" 62 | - name: project2 63 | path: /tmp/workspaces/project2 64 | root_path: "" 65 | ` 66 | 67 | mockPc.EXPECT().WriteFile(fakeHomeConfigPath, []byte(homeConfigForSelect), os.FileMode(0644)) 68 | mockPc.EXPECT().Printf("active workspace changed to '%s'\n", "project2") 69 | 70 | _ = SelectWorkspaceAction("project2") 71 | } 72 | -------------------------------------------------------------------------------- /cmd/elc.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/ensi-platform/elc/actions" 5 | "github.com/ensi-platform/elc/core" 6 | "github.com/spf13/cobra" 7 | "os" 8 | ) 9 | 10 | var globalOptions core.GlobalOptions 11 | 12 | func parseStartFlags(cmd *cobra.Command) { 13 | cmd.Flags().BoolVar(&globalOptions.Force, "force", false, "force start dependencies, even if service already started") 14 | cmd.Flags().StringVar(&globalOptions.Mode, "mode", "default", "start only dependencies with specified mode, by default starts 'default' dependencies") 15 | } 16 | 17 | func parseExecFlags(cmd *cobra.Command) { 18 | cmd.Flags().IntVar(&globalOptions.UID, "uid", -1, "use another uid, by default uses uid of current user") 19 | cmd.Flags().BoolVar(&globalOptions.NoTty, "no-tty", false, "disable pseudo-TTY allocation") 20 | } 21 | 22 | func InitCobra() *cobra.Command { 23 | globalOptions = core.GlobalOptions{} 24 | var rootCmd = &cobra.Command{ 25 | Use: "elc [command]", 26 | Args: cobra.MinimumNArgs(0), 27 | SilenceUsage: true, 28 | //SilenceErrors: true, 29 | Version: core.Version, 30 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 31 | core.Pc = &core.RealPC{} 32 | }, 33 | RunE: func(cmd *cobra.Command, args []string) error { 34 | if len(args) == 0 { 35 | return cmd.Help() 36 | } else { 37 | globalOptions.Cmd = args 38 | return actions.ExecAction(&globalOptions) 39 | } 40 | }, 41 | } 42 | 43 | rootCmd.Flags().SetInterspersed(false) 44 | 45 | rootCmd.PersistentFlags().StringVarP(&globalOptions.ComponentName, "component", "c", "", "name of component") 46 | rootCmd.PersistentFlags().StringVarP(&globalOptions.WorkspaceName, "workspace", "w", "", "name of workspace") 47 | rootCmd.PersistentFlags().StringVar(&globalOptions.ComponentName, "svc", "", "name of current component (deprecated, alias for component)") 48 | rootCmd.PersistentFlags().BoolVar(&globalOptions.Debug, "debug", false, "print debug messages") 49 | rootCmd.PersistentFlags().BoolVar(&globalOptions.DryRun, "dry-run", false, "do not execute real command, only debug") 50 | rootCmd.PersistentFlags().StringVar(&globalOptions.Tag, "tag", "", "select all components with tag") 51 | 52 | parseStartFlags(rootCmd) 53 | parseExecFlags(rootCmd) 54 | 55 | NewWorkspaceCommand(rootCmd) 56 | NewServiceStartCommand(rootCmd) 57 | NewServiceStopCommand(rootCmd) 58 | NewServiceDestroyCommand(rootCmd) 59 | NewServiceRestartCommand(rootCmd) 60 | NewServiceVarsCommand(rootCmd) 61 | NewServiceComposeCommand(rootCmd) 62 | NewServiceWrapCommand(rootCmd) 63 | NewServiceExecCommand(rootCmd) 64 | NewServiceRunCommand(rootCmd) 65 | NewServiceSetHooksCommand(rootCmd) 66 | NewUpdateCommand(rootCmd) 67 | NewFixUpdateCommand(rootCmd) 68 | NewServiceCloneCommand(rootCmd) 69 | NewServiceListCommand(rootCmd) 70 | 71 | return rootCmd 72 | } 73 | 74 | func NewWorkspaceCommand(parentCommand *cobra.Command) { 75 | var command = &cobra.Command{ 76 | Use: "workspace", 77 | Aliases: []string{"ws"}, 78 | } 79 | NewWorkspaceListCommand(command) 80 | NewWorkspaceAddCommand(command) 81 | NewWorkspaceRemoveCommand(command) 82 | NewWorkspaceShowCommand(command) 83 | NewWorkspaceSelectCommand(command) 84 | NewWorkspaceSetRootCommand(command) 85 | parentCommand.AddCommand(command) 86 | } 87 | 88 | func NewWorkspaceListCommand(parentCommand *cobra.Command) { 89 | var command = &cobra.Command{ 90 | Use: "list", 91 | Aliases: []string{"ls"}, 92 | Short: "Show list of registered workspaces", 93 | Long: "Show list of registered workspaces.", 94 | RunE: func(cmd *cobra.Command, args []string) error { 95 | return actions.ListWorkspacesAction() 96 | }, 97 | } 98 | parentCommand.AddCommand(command) 99 | } 100 | 101 | func NewWorkspaceAddCommand(parentCommand *cobra.Command) { 102 | var command = &cobra.Command{ 103 | Use: "add [NAME] [PATH]", 104 | Short: "Register new workspace", 105 | Long: "Register new workspace.", 106 | Args: cobra.ExactArgs(2), 107 | RunE: func(cmd *cobra.Command, args []string) error { 108 | name := args[0] 109 | wsPath := args[1] 110 | 111 | return actions.AddWorkspaceAction(name, wsPath) 112 | }, 113 | } 114 | parentCommand.AddCommand(command) 115 | } 116 | 117 | func NewWorkspaceRemoveCommand(parentCommand *cobra.Command) { 118 | var command = &cobra.Command{ 119 | Use: "remove [NAME]", 120 | Short: "Remove workspace from ~/.elc.yaml", 121 | Long: "Remove workspace from ~/.elc.yaml.", 122 | Args: cobra.ExactArgs(1), 123 | RunE: func(cmd *cobra.Command, args []string) error { 124 | name := args[0] 125 | 126 | return actions.RemoveWorkspaceAction(name) 127 | }, 128 | } 129 | parentCommand.AddCommand(command) 130 | } 131 | 132 | func NewWorkspaceShowCommand(parentCommand *cobra.Command) { 133 | var command = &cobra.Command{ 134 | Use: "show", 135 | Short: "Print current workspace name", 136 | Long: "Print current workspace name.", 137 | RunE: func(cmd *cobra.Command, args []string) error { 138 | core.Pc = &core.RealPC{} 139 | return actions.ShowCurrentWorkspaceAction(&globalOptions) 140 | }, 141 | } 142 | parentCommand.AddCommand(command) 143 | } 144 | 145 | func NewWorkspaceSelectCommand(parentCommand *cobra.Command) { 146 | var command = &cobra.Command{ 147 | Use: "select [NAME]", 148 | Short: "Set current workspace", 149 | Long: "Set workspace with name NAME as current.", 150 | Args: cobra.ExactArgs(1), 151 | RunE: func(cmd *cobra.Command, args []string) error { 152 | name := args[0] 153 | return actions.SelectWorkspaceAction(name) 154 | }, 155 | } 156 | parentCommand.AddCommand(command) 157 | } 158 | 159 | func NewWorkspaceSetRootCommand(parentCommand *cobra.Command) { 160 | var command = &cobra.Command{ 161 | Use: "set-root [NAME] [PATH]", 162 | Short: "Set root path for workspace", 163 | Long: "Set root path for workspace.", 164 | Args: cobra.ExactArgs(2), 165 | RunE: func(cmd *cobra.Command, args []string) error { 166 | return actions.SetRootPathAction(args[0], args[1]) 167 | }, 168 | } 169 | parentCommand.AddCommand(command) 170 | } 171 | 172 | func NewServiceStartCommand(parentCommand *cobra.Command) { 173 | var command = &cobra.Command{ 174 | Use: "start [OPTIONS] [NAME]", 175 | Short: "Start one or more services", 176 | Long: "Start one or more services.\nBy default starts service found with current directory, but you can pass one or more service names instead.", 177 | Args: cobra.ArbitraryArgs, 178 | RunE: func(cmd *cobra.Command, args []string) error { 179 | return actions.StartServiceAction(&globalOptions, args) 180 | }, 181 | } 182 | parseStartFlags(command) 183 | parentCommand.AddCommand(command) 184 | } 185 | 186 | func NewServiceStopCommand(parentCommand *cobra.Command) { 187 | var stopAll bool 188 | var command = &cobra.Command{ 189 | Use: "stop [OPTIONS] [NAME]", 190 | Short: "Stop one or more services", 191 | Long: "Stop one or more services.\nBy default stops service found with current directory, but you can pass one or more service names instead.", 192 | Args: cobra.ArbitraryArgs, 193 | RunE: func(cmd *cobra.Command, args []string) error { 194 | return actions.StopServiceAction(stopAll, args, false, &globalOptions) 195 | }, 196 | } 197 | command.Flags().BoolVar(&stopAll, "all", false, "stop all services") 198 | parentCommand.AddCommand(command) 199 | } 200 | 201 | func NewServiceDestroyCommand(parentCommand *cobra.Command) { 202 | var destroyAll bool 203 | var command = &cobra.Command{ 204 | Use: "destroy [OPTIONS] [NAME]", 205 | Short: "Stop and remove containers of one or more services", 206 | Long: "Stop and remove containers of one or more services.\nBy default destroys service found with current directory, but you can pass one or more service names instead.", 207 | Args: cobra.ArbitraryArgs, 208 | RunE: func(cmd *cobra.Command, args []string) error { 209 | return actions.StopServiceAction(destroyAll, args, true, &globalOptions) 210 | }, 211 | } 212 | command.Flags().BoolVar(&destroyAll, "all", false, "destroy all services") 213 | parentCommand.AddCommand(command) 214 | } 215 | 216 | func NewServiceRestartCommand(parentCommand *cobra.Command) { 217 | var hardRestart bool 218 | var command = &cobra.Command{ 219 | Use: "restart [OPTIONS] [NAME]", 220 | Short: "Restart one or more services", 221 | Long: "Restart one or more services.\nBy default restart service found with current directory, but you can pass one or more service names instead.", 222 | Args: cobra.ArbitraryArgs, 223 | RunE: func(cmd *cobra.Command, args []string) error { 224 | return actions.RestartServiceAction(hardRestart, args, &globalOptions) 225 | }, 226 | } 227 | command.Flags().BoolVar(&hardRestart, "hard", false, "destroy container instead of stop it before start") 228 | parentCommand.AddCommand(command) 229 | } 230 | 231 | func NewServiceVarsCommand(parentCommand *cobra.Command) { 232 | var command = &cobra.Command{ 233 | Use: "vars [NAME]", 234 | Short: "Print all variables computed for service", 235 | Long: "Print all variables computed for service.\nBy default uses service found with current directory, but you can pass name of another service instead.", 236 | Args: cobra.ArbitraryArgs, 237 | RunE: func(cmd *cobra.Command, args []string) error { 238 | return actions.PrintVarsAction(&globalOptions, args) 239 | }, 240 | } 241 | parentCommand.AddCommand(command) 242 | } 243 | 244 | func NewServiceComposeCommand(parentCommand *cobra.Command) { 245 | var command = &cobra.Command{ 246 | Use: "compose [OPTIONS] [COMMAND]", 247 | Short: "Run docker-compose command", 248 | Long: "Run docker-compose command.\nBy default uses service found with current directory.", 249 | Args: cobra.MinimumNArgs(0), 250 | RunE: func(cmd *cobra.Command, args []string) error { 251 | if len(args) == 0 { 252 | return cmd.Help() 253 | } else { 254 | return actions.ComposeCommandAction(&globalOptions, args) 255 | } 256 | }, 257 | } 258 | command.Flags().SetInterspersed(false) 259 | parentCommand.AddCommand(command) 260 | } 261 | 262 | func NewServiceWrapCommand(parentCommand *cobra.Command) { 263 | var command = &cobra.Command{ 264 | Use: "wrap [COMMAND]", 265 | Short: "Execute command on host with env variables for service. For module uses variables of linked service", 266 | Long: "Execute command on host with env variables for service.\nFor module uses variables of linked service.\nBy default uses service/module found with current directory.", 267 | Args: cobra.ArbitraryArgs, 268 | RunE: func(cmd *cobra.Command, args []string) error { 269 | return actions.WrapCommandAction(&globalOptions, args) 270 | }, 271 | } 272 | parentCommand.AddCommand(command) 273 | } 274 | 275 | func NewServiceExecCommand(parentCommand *cobra.Command) { 276 | var command = &cobra.Command{ 277 | Use: "exec [OPTIONS] [COMMAND]", 278 | Short: "Execute command in container. For module uses container of linked service", 279 | Long: "Execute command in container. For module uses container of linked service.\nBy default uses service/module found with current directory. Starts service if it is not running.", 280 | Args: cobra.MinimumNArgs(0), 281 | RunE: func(cmd *cobra.Command, args []string) error { 282 | if len(args) == 0 { 283 | return cmd.Usage() 284 | } else { 285 | globalOptions.Cmd = args 286 | return actions.ExecAction(&globalOptions) 287 | } 288 | }, 289 | } 290 | command.Flags().SetInterspersed(false) 291 | parseStartFlags(command) 292 | parseExecFlags(command) 293 | parentCommand.AddCommand(command) 294 | } 295 | func NewServiceRunCommand(parentCommand *cobra.Command) { 296 | var command = &cobra.Command{ 297 | Use: "run [OPTIONS] [COMMAND]", 298 | Short: "Run new container with command. For module uses container of linked service", 299 | Long: "Run new container with command. For module uses container of linked service.\nBy default uses service/module found with current directory. Starts service if it is not running.", 300 | Args: cobra.MinimumNArgs(0), 301 | RunE: func(cmd *cobra.Command, args []string) error { 302 | if len(args) == 0 { 303 | return cmd.Usage() 304 | } else { 305 | globalOptions.Cmd = args 306 | return actions.RunAction(&globalOptions) 307 | } 308 | }, 309 | } 310 | command.Flags().SetInterspersed(false) 311 | parseExecFlags(command) 312 | parentCommand.AddCommand(command) 313 | } 314 | 315 | func NewServiceSetHooksCommand(parentCommand *cobra.Command) { 316 | var command = &cobra.Command{ 317 | Use: "set-hooks [HOOKS_DIR]", 318 | Short: "Install hooks from specified folder to .git/hooks", 319 | Long: "Install hooks from specified folder to .git/hooks.\nHOOKS_PATH must contain subdirectories with names as git hooks, eg. 'pre-commit'.\nOne subdirectory can contain one or many scripts with .sh extension.\nEvery script will be wrapped with 'elc --tag=hook' command.", 320 | Args: cobra.MinimumNArgs(1), 321 | RunE: func(cmd *cobra.Command, args []string) error { 322 | return actions.SetGitHooksAction(&globalOptions, args[0], os.Args[0]) 323 | }, 324 | } 325 | parentCommand.AddCommand(command) 326 | } 327 | 328 | func NewUpdateCommand(parentCommand *cobra.Command) { 329 | var version string 330 | var command = &cobra.Command{ 331 | Use: "update", 332 | Short: "Update elc binary", 333 | Long: "Update elc binary.\nDownload new version of ELC, place it to /opt/elc/ and update symlink at /usr/local/bin.", 334 | Args: cobra.NoArgs, 335 | RunE: func(cmd *cobra.Command, args []string) error { 336 | return actions.UpdateBinaryAction(version) 337 | }, 338 | } 339 | command.Flags().StringVar(&version, "version", "", "desired version of elc") 340 | parentCommand.AddCommand(command) 341 | } 342 | 343 | func NewFixUpdateCommand(parentCommand *cobra.Command) { 344 | var command = &cobra.Command{ 345 | Use: "fix-update-command", 346 | Short: "Set actual update command to ~/.elc.yaml", 347 | Args: cobra.NoArgs, 348 | RunE: func(cmd *cobra.Command, args []string) error { 349 | return actions.FixUpdateBinaryCommandAction() 350 | }, 351 | } 352 | parentCommand.AddCommand(command) 353 | } 354 | 355 | func NewServiceCloneCommand(parentCommand *cobra.Command) { 356 | var noHook bool 357 | var command = &cobra.Command{ 358 | Use: "clone [NAME]", 359 | Short: "Clone component to its path", 360 | Long: "Clone component to its path.", 361 | SilenceUsage: false, 362 | SilenceErrors: false, 363 | Args: cobra.ArbitraryArgs, 364 | RunE: func(cmd *cobra.Command, args []string) error { 365 | return actions.CloneComponentAction(&globalOptions, args, noHook) 366 | }, 367 | } 368 | 369 | command.Flags().BoolVar(&noHook, "no-hook", false, "do not execute hook script after cloning") 370 | parentCommand.AddCommand(command) 371 | } 372 | 373 | func NewServiceListCommand(parentCommand *cobra.Command) { 374 | var command = &cobra.Command{ 375 | Use: "list [OPTIONS]", 376 | Short: "Show list of services", 377 | Long: "Show list of services.\nCan be used in scripts for loops.", 378 | Args: cobra.ArbitraryArgs, 379 | RunE: func(cmd *cobra.Command, args []string) error { 380 | return actions.ListServicesAction(&globalOptions) 381 | }, 382 | } 383 | parentCommand.AddCommand(command) 384 | } 385 | -------------------------------------------------------------------------------- /core/bootstrap.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "path" 5 | ) 6 | 7 | func CheckAndLoadHC() (*HomeConfig, error) { 8 | homeDir, err := Pc.HomeDir() 9 | if err != nil { 10 | return nil, err 11 | } 12 | homeConfigPath := path.Join(homeDir, ".elc.yaml") 13 | err = CheckHomeConfigIsEmpty(homeConfigPath) 14 | if err != nil { 15 | return nil, err 16 | } 17 | hc, err := LoadHomeConfig(homeConfigPath) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | return hc, nil 23 | } 24 | 25 | func GetWorkspaceConfig(wsName string) (*Workspace, error) { 26 | hc, err := CheckAndLoadHC() 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | wsPath, err := hc.GetCurrentWsPath(wsName) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | cwd, err := Pc.Getwd() 37 | if err != nil { 38 | return nil, err 39 | } 40 | ws := NewWorkspace(wsPath, cwd) 41 | 42 | err = ws.LoadConfig() 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | err = ws.checkVersion() 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | err = ws.init() 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | return ws, nil 58 | } 59 | -------------------------------------------------------------------------------- /core/component.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type Component struct { 11 | Name string 12 | Config *ComponentConfig 13 | Template *ComponentConfig 14 | JustStarted bool 15 | Context *Context 16 | Workspace *Workspace 17 | } 18 | 19 | func NewComponent(compName string, compCfg *ComponentConfig, ws *Workspace) *Component { 20 | return &Component{ 21 | Name: compName, 22 | Config: compCfg, 23 | Workspace: ws, 24 | } 25 | } 26 | 27 | func (comp *Component) init() error { 28 | ctx := make(Context, len(*comp.Workspace.Context)) 29 | copy(ctx, *comp.Workspace.Context) 30 | 31 | ctx = ctx.add("APP_NAME", comp.Name) 32 | ctx = ctx.add("COMPOSE_PROJECT_NAME", fmt.Sprintf("%s-%s", comp.Workspace.Config.Name, comp.Name)) 33 | svcPath, err := ctx.RenderString(comp.Config.Path) 34 | if err != nil { 35 | return err 36 | } 37 | ctx = ctx.add("SVC_PATH", svcPath) 38 | 39 | if comp.Config.Extends != "" { 40 | tpl, found := comp.Workspace.Config.Components[comp.Config.Extends] 41 | if !found { 42 | return errors.New(fmt.Sprintf("template '%s' is not found", comp.Config.Extends)) 43 | } 44 | comp.Template = &tpl 45 | 46 | tplPath, err := ctx.RenderString(tpl.Path) 47 | if err != nil { 48 | return err 49 | } 50 | ctx = ctx.add("TPL_PATH", tplPath) 51 | if tpl.ComposeFile == "" { 52 | tpl.ComposeFile = "${TPL_PATH}/docker-compose.yml" 53 | } 54 | composeFile, err := ctx.RenderString(tpl.ComposeFile) 55 | if err != nil { 56 | return err 57 | } 58 | ctx = ctx.add("COMPOSE_FILE", composeFile) 59 | for _, pair := range tpl.Variables { 60 | value, err := ctx.RenderString(pair.Value.(string)) 61 | if err != nil { 62 | return err 63 | } 64 | ctx = ctx.add(pair.Key.(string), value) 65 | } 66 | } 67 | 68 | if comp.Config.ComposeFile != "" { 69 | composeFile, err := ctx.RenderString(comp.Config.ComposeFile) 70 | if err != nil { 71 | return err 72 | } 73 | ctx = ctx.add("COMPOSE_FILE", composeFile) 74 | } 75 | composeFile, found := ctx.find("COMPOSE_FILE") 76 | if !found || composeFile == "" { 77 | composeFile, err := ctx.RenderString("${SVC_PATH}/docker-compose.yml") 78 | if err != nil { 79 | return err 80 | } 81 | ctx = ctx.add("COMPOSE_FILE", composeFile) 82 | } 83 | 84 | for _, pair := range comp.Config.Variables { 85 | value, err := ctx.RenderString(pair.Value.(string)) 86 | if err != nil { 87 | return err 88 | } 89 | ctx = ctx.add(pair.Key.(string), value) 90 | } 91 | 92 | comp.Context = &ctx 93 | 94 | return nil 95 | } 96 | 97 | func (comp *Component) execComposeToString(composeCommand []string, options *GlobalOptions) (string, error) { 98 | composeFile, _ := comp.Context.find("COMPOSE_FILE") 99 | command := append([]string{"docker", "compose", "-f", composeFile}, composeCommand...) 100 | 101 | if options.Debug { 102 | _, _ = Pc.Printf(">> %s\n", strings.Join(command, " ")) 103 | } 104 | 105 | if !options.DryRun { 106 | _, out, err := Pc.ExecToString(command, comp.Context.renderMapToEnv()) 107 | if err != nil { 108 | return "", err 109 | } 110 | return out, nil 111 | } 112 | 113 | return "", nil 114 | } 115 | 116 | func (comp *Component) execComposeInteractive(composeCommand []string, options *GlobalOptions) (int, error) { 117 | composeFile, _ := comp.Context.find("COMPOSE_FILE") 118 | command := append([]string{"docker", "compose", "-f", composeFile}, composeCommand...) 119 | 120 | if options.Debug { 121 | _, _ = Pc.Printf(">> %s\n", strings.Join(command, " ")) 122 | } 123 | 124 | if !options.DryRun { 125 | code, err := Pc.ExecInteractive(command, comp.Context.renderMapToEnv()) 126 | if err != nil { 127 | return 0, err 128 | } 129 | 130 | return code, nil 131 | } 132 | 133 | return 0, nil 134 | } 135 | 136 | func (comp *Component) execInteractive(command []string, options *GlobalOptions) (int, error) { 137 | if options.Debug { 138 | _, _ = Pc.Printf(">> %s\n", strings.Join(command, " ")) 139 | } 140 | 141 | if !options.DryRun { 142 | code, err := Pc.ExecInteractive(command, comp.Context.renderMapToEnv()) 143 | if err != nil { 144 | return 0, err 145 | } 146 | return code, nil 147 | } 148 | 149 | return 0, nil 150 | } 151 | 152 | func (comp *Component) IsRunning(options *GlobalOptions) (bool, error) { 153 | out, err := comp.execComposeToString([]string{"ps", "--status=running", "-q"}, options) 154 | if err != nil { 155 | return false, err 156 | } 157 | 158 | return out != "", nil 159 | } 160 | 161 | func (comp *Component) IsCloned() (bool, error) { 162 | svcPath, found := comp.Context.find("SVC_PATH") 163 | if !found { 164 | return false, errors.New("path of component is not defined.Check workspace.yaml") 165 | } 166 | 167 | return Pc.FileExists(svcPath), nil 168 | } 169 | 170 | func (comp *Component) Start(options *GlobalOptions) error { 171 | if comp.JustStarted { 172 | return nil 173 | } 174 | 175 | cloned, err := comp.IsCloned() 176 | if err != nil { 177 | return err 178 | } 179 | 180 | if !cloned { 181 | _, _ = Pc.Println("component is not cloned") 182 | return nil 183 | } 184 | 185 | running, err := comp.IsRunning(options) 186 | if err != nil { 187 | return err 188 | } 189 | 190 | if !running || options.Force { 191 | err := comp.startDependencies(options) 192 | if err != nil { 193 | return err 194 | } 195 | } 196 | 197 | if !running { 198 | _, err = comp.execComposeInteractive([]string{"up", "-d"}, options) 199 | if err != nil { 200 | return err 201 | } 202 | } 203 | 204 | return nil 205 | } 206 | 207 | func (comp *Component) startDependencies(params *GlobalOptions) error { 208 | for _, depName := range comp.Config.GetDeps(params.Mode) { 209 | depComp, found := comp.Workspace.Components[depName] 210 | if !found { 211 | return errors.New(fmt.Sprintf("dependency with name '%s' is not defined", depName)) 212 | } 213 | err := depComp.Start(params) 214 | if err != nil { 215 | return err 216 | } 217 | } 218 | 219 | return nil 220 | } 221 | 222 | func (comp *Component) Stop(options *GlobalOptions) error { 223 | cloned, err := comp.IsCloned() 224 | if err != nil { 225 | return err 226 | } 227 | 228 | if !cloned { 229 | _, _ = Pc.Println("component is not cloned") 230 | return nil 231 | } 232 | 233 | running, err := comp.IsRunning(options) 234 | if err != nil { 235 | return err 236 | } 237 | if running { 238 | _, err = comp.execComposeInteractive([]string{"stop"}, options) 239 | if err != nil { 240 | return err 241 | } 242 | } 243 | 244 | return nil 245 | } 246 | 247 | func (comp *Component) Destroy(options *GlobalOptions) error { 248 | cloned, err := comp.IsCloned() 249 | if err != nil { 250 | return err 251 | } 252 | 253 | if !cloned { 254 | _, _ = Pc.Println("component is not cloned") 255 | return nil 256 | } 257 | 258 | running, err := comp.IsRunning(options) 259 | if err != nil { 260 | return err 261 | } 262 | if running { 263 | _, err := comp.execComposeInteractive([]string{"down"}, options) 264 | if err != nil { 265 | return err 266 | } 267 | } 268 | 269 | return nil 270 | } 271 | 272 | func (comp *Component) Restart(hard bool, options *GlobalOptions) error { 273 | var err error 274 | 275 | if hard { 276 | err = comp.Destroy(options) 277 | if err != nil { 278 | return err 279 | } 280 | } else { 281 | err = comp.Stop(options) 282 | if err != nil { 283 | return err 284 | } 285 | } 286 | err = comp.Start(&GlobalOptions{}) 287 | if err != nil { 288 | return err 289 | } 290 | 291 | return nil 292 | } 293 | 294 | func (comp *Component) Compose(params *GlobalOptions) (int, error) { 295 | cloned, err := comp.IsCloned() 296 | if err != nil { 297 | return 1, err 298 | } 299 | 300 | if !cloned { 301 | _, _ = Pc.Println("component is not cloned") 302 | return 1, nil 303 | } 304 | 305 | code, err := comp.execComposeInteractive(params.Cmd, params) 306 | if err != nil { 307 | return 0, err 308 | } 309 | 310 | return code, nil 311 | } 312 | 313 | func (comp *Component) Exec(options *GlobalOptions) (int, error) { 314 | err := comp.Start(options) 315 | if err != nil { 316 | return 0, err 317 | } 318 | 319 | command := []string{"exec"} 320 | if options.WorkingDir != "" { 321 | command = append(command, "-w", options.WorkingDir) 322 | } 323 | if options.UID > -1 { 324 | command = append(command, "-u", strconv.Itoa(options.UID)) 325 | } else { 326 | userId, found := comp.Context.find("USER_ID") 327 | if !found { 328 | return 0, errors.New("variable USER_ID is not defined") 329 | } 330 | 331 | groupId, found := comp.Context.find("GROUP_ID") 332 | if !found { 333 | return 0, errors.New("variable USER_ID is not defined") 334 | } 335 | 336 | command = append(command, "-u", fmt.Sprintf("%s:%s", userId, groupId)) 337 | } 338 | 339 | if options.NoTty || !Pc.IsTerminal() { 340 | command = append(command, "-T") 341 | } 342 | command = append(command, "app") 343 | 344 | command = append(command, options.Cmd...) 345 | code, err := comp.execComposeInteractive(command, options) 346 | if err != nil { 347 | return 0, err 348 | } 349 | 350 | return code, nil 351 | } 352 | 353 | func (comp *Component) Run(options *GlobalOptions) (int, error) { 354 | cloned, err := comp.IsCloned() 355 | if err != nil { 356 | return 1, err 357 | } 358 | 359 | if !cloned { 360 | _, _ = Pc.Println("component is not cloned") 361 | return 1, nil 362 | } 363 | 364 | command := []string{"run", "--rm", "--entrypoint=''"} 365 | if options.WorkingDir != "" { 366 | command = append(command, "-w", options.WorkingDir) 367 | } 368 | if options.UID > -1 { 369 | command = append(command, "-u", strconv.Itoa(options.UID)) 370 | } else { 371 | userId, found := comp.Context.find("USER_ID") 372 | if !found { 373 | return 0, errors.New("variable USER_ID is not defined") 374 | } 375 | 376 | groupId, found := comp.Context.find("GROUP_ID") 377 | if !found { 378 | return 0, errors.New("variable USER_ID is not defined") 379 | } 380 | 381 | command = append(command, "-u", fmt.Sprintf("%s:%s", userId, groupId)) 382 | } 383 | 384 | if options.NoTty || !Pc.IsTerminal() { 385 | command = append(command, "-T") 386 | } 387 | command = append(command, "app") 388 | 389 | command = append(command, options.Cmd...) 390 | code, err := comp.execComposeInteractive(command, options) 391 | if err != nil { 392 | return 0, err 393 | } 394 | 395 | return code, nil 396 | } 397 | 398 | func (comp *Component) Wrap(command []string, options *GlobalOptions) (int, error) { 399 | code, err := comp.execInteractive(command, options) 400 | if err != nil { 401 | return 0, err 402 | } 403 | 404 | return code, nil 405 | } 406 | 407 | func (comp *Component) DumpVars() error { 408 | for _, line := range comp.Context.renderMapToEnv() { 409 | _, _ = Pc.Println(line) 410 | } 411 | 412 | return nil 413 | } 414 | 415 | func (comp *Component) getAfterCloneHook() string { 416 | if comp.Config.AfterCloneHook != "" { 417 | return comp.Config.AfterCloneHook 418 | } 419 | 420 | if comp.Template != nil { 421 | return comp.Template.AfterCloneHook 422 | } 423 | 424 | return "" 425 | } 426 | 427 | func (comp *Component) Clone(options *GlobalOptions, noHook bool) error { 428 | cloned, err := comp.IsCloned() 429 | if err != nil { 430 | return err 431 | } 432 | 433 | if cloned { 434 | _, _ = Pc.Println("component is already cloned") 435 | return nil 436 | } 437 | 438 | if comp.Config.Repository == "" { 439 | return errors.New(fmt.Sprintf("repository of component %s is not defined. Check workspace.yaml", comp.Name)) 440 | } 441 | svcPath, found := comp.Context.find("SVC_PATH") 442 | if !found { 443 | return errors.New("path of component is not defined.Check workspace.yaml") 444 | } 445 | if Pc.FileExists(svcPath) { 446 | _, _ = Pc.Printf("Folder of component %s already exists. Skip.\n", comp.Name) 447 | return nil 448 | } else { 449 | _, err := comp.execInteractive([]string{"git", "clone", comp.Config.Repository, svcPath}, options) 450 | if err != nil { 451 | return err 452 | } 453 | 454 | if !noHook { 455 | afterCloneHook := comp.getAfterCloneHook() 456 | if afterCloneHook == "" { 457 | return nil 458 | } 459 | 460 | afterCloneHook, err = comp.Context.RenderString(afterCloneHook) 461 | if err != nil { 462 | return err 463 | } 464 | 465 | if afterCloneHook != "" { 466 | _, err = comp.execInteractive([]string{afterCloneHook}, options) 467 | if err != nil { 468 | return err 469 | } 470 | } 471 | } 472 | return nil 473 | } 474 | } 475 | 476 | func (comp *Component) UpdateHooks(options *GlobalOptions, elcBinary string, scriptsFolder string) error { 477 | svcPath, found := comp.Context.find("SVC_PATH") 478 | if !found { 479 | return errors.New("path of component is not defined.Check workspace.yaml") 480 | } 481 | 482 | return GenerateHookScripts(options, svcPath, elcBinary, scriptsFolder) 483 | } 484 | -------------------------------------------------------------------------------- /core/component_config.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "gopkg.in/yaml.v2" 4 | 5 | type ModeList []string 6 | 7 | func (s ModeList) contains(v string) bool { 8 | for _, item := range s { 9 | if item == v { 10 | return true 11 | } 12 | } 13 | return false 14 | } 15 | 16 | type ComponentConfig struct { 17 | Alias string `yaml:"alias"` 18 | ComposeFile string `yaml:"compose_file"` 19 | Dependencies map[string]ModeList `yaml:"dependencies"` 20 | ExecPath string `yaml:"exec_path"` 21 | Extends string `yaml:"extends"` 22 | HostedIn string `yaml:"hosted_in"` 23 | Hostname string `yaml:"hostname"` 24 | IsTemplate bool `yaml:"is_template"` 25 | Path string `yaml:"path"` 26 | Replace bool `yaml:"replace"` 27 | Variables yaml.MapSlice `yaml:"variables"` 28 | Repository string `yaml:"repository"` 29 | Tags []string `yaml:"tags"` 30 | AfterCloneHook string `yaml:"after_clone_hook"` 31 | } 32 | 33 | func (cc ComponentConfig) merge(cc2 ComponentConfig) ComponentConfig { 34 | if cc2.Replace { 35 | return cc2 36 | } 37 | 38 | if cc2.Path != "" { 39 | cc.Path = cc2.Path 40 | } 41 | if cc2.ComposeFile != "" { 42 | cc.ComposeFile = cc2.ComposeFile 43 | } 44 | if cc2.Extends != "" { 45 | cc.Extends = cc2.Extends 46 | } 47 | if cc2.HostedIn != "" { 48 | cc.HostedIn = cc2.HostedIn 49 | } 50 | if cc2.ExecPath != "" { 51 | cc.ExecPath = cc2.ExecPath 52 | } 53 | if cc2.Alias != "" { 54 | cc.Alias = cc2.Alias 55 | } 56 | if cc2.Repository != "" { 57 | cc.Repository = cc2.Repository 58 | } 59 | if cc2.AfterCloneHook != "" { 60 | cc.AfterCloneHook = cc2.AfterCloneHook 61 | } 62 | 63 | cc.Variables = append(cc.Variables, cc2.Variables...) 64 | cc.Tags = append(cc.Tags, cc2.Tags...) 65 | 66 | for depSvc, modes := range cc2.Dependencies { 67 | if cc.Dependencies[depSvc] == nil { 68 | cc.Dependencies[depSvc] = make([]string, 1) 69 | } 70 | for _, mode := range modes { 71 | if !cc.Dependencies[depSvc].contains(mode) { 72 | cc.Dependencies[depSvc] = append(cc.Dependencies[depSvc], mode) 73 | } 74 | } 75 | } 76 | 77 | return cc 78 | } 79 | 80 | func (cc *ComponentConfig) GetDeps(mode string) []string { 81 | var result []string 82 | for key, modes := range cc.Dependencies { 83 | if modes.contains(mode) { 84 | result = append(result, key) 85 | } 86 | } 87 | 88 | return result 89 | } 90 | -------------------------------------------------------------------------------- /core/context.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "fmt" 4 | 5 | type Context [][]string 6 | 7 | func (ctx *Context) find(name string) (string, bool) { 8 | for _, pair := range *ctx { 9 | if pair[0] == name { 10 | return pair[1], true 11 | } 12 | } 13 | return "", false 14 | } 15 | 16 | func (ctx Context) remove(name string) Context { 17 | index := -1 18 | for i, pair := range ctx { 19 | if pair[0] == name { 20 | index = i 21 | } 22 | } 23 | 24 | if index > -1 { 25 | return append(ctx[:index], ctx[index+1:]...) 26 | } 27 | 28 | return ctx 29 | } 30 | 31 | func (ctx *Context) add(name string, value string) Context { 32 | tmp := ctx.remove(name) 33 | return append(tmp, []string{name, value}) 34 | } 35 | 36 | func (ctx *Context) RenderString(str string) (string, error) { 37 | return substVars(str, ctx) 38 | } 39 | 40 | func (ctx *Context) renderMapToEnv() []string { 41 | var result []string 42 | for _, pair := range *ctx { 43 | result = append(result, fmt.Sprintf("%s=%s", pair[0], pair[1])) 44 | } 45 | 46 | return result 47 | } 48 | -------------------------------------------------------------------------------- /core/core.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | var Version string 11 | 12 | type GlobalOptions struct { 13 | WorkspaceName string 14 | ComponentName string 15 | Debug bool 16 | Cmd []string 17 | Force bool 18 | Mode string 19 | WorkingDir string 20 | UID int 21 | Tag string 22 | DryRun bool 23 | NoTty bool 24 | } 25 | 26 | func contains(list []string, item string) bool { 27 | for _, value := range list { 28 | if value == item { 29 | return true 30 | } 31 | } 32 | return false 33 | } 34 | 35 | type reResult map[string]string 36 | 37 | func reFindMaps(pattern string, subject string) ([]reResult, error) { 38 | re, err := regexp.Compile(pattern) 39 | if err != nil { 40 | return nil, err 41 | } 42 | matches := re.FindAllStringSubmatch(subject, -1) 43 | names := re.SubexpNames() 44 | var result []reResult 45 | for _, match := range matches { 46 | foundFields := reResult{} 47 | for i, field := range match { 48 | if names[i] == "" { 49 | continue 50 | } 51 | foundFields[names[i]] = field 52 | } 53 | result = append(result, foundFields) 54 | } 55 | 56 | return result, nil 57 | } 58 | 59 | func substVars(expr string, ctx *Context) (string, error) { 60 | foundVars, err := reFindMaps(`\$\{(?P[^:}]+)(:-(?P[^}]+))?\}`, expr) 61 | if err != nil { 62 | return "", err 63 | } 64 | 65 | for _, foundVar := range foundVars { 66 | varName := foundVar["name"] 67 | value, found := ctx.find(varName) 68 | if !found { 69 | value, found = foundVar["value"] 70 | if !found { 71 | return "", errors.New(fmt.Sprintf("variable %s is not set", varName)) 72 | } 73 | 74 | if strings.HasPrefix(value, "$") { 75 | varRef := strings.TrimLeft(value, "$") 76 | value, found = ctx.find(varRef) 77 | if !found { 78 | return "", errors.New(fmt.Sprintf("variable %s is not set", varRef)) 79 | } 80 | } 81 | } 82 | re, err := regexp.Compile(fmt.Sprintf(`\$\{%s(?::-[^}]+)?\}`, varName)) 83 | if err != nil { 84 | return "", err 85 | } 86 | expr = re.ReplaceAllString(expr, value) 87 | } 88 | 89 | return expr, nil 90 | } 91 | -------------------------------------------------------------------------------- /core/git.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | var hookNames = []string{ 10 | "applypatch-msg", 11 | "pre-applypatch", 12 | "post-applypatch", 13 | "pre-commit", 14 | "pre-merge-commit", 15 | "prepare-commit-msg", 16 | "commit-msg", 17 | "post-commit", 18 | "pre-rebase", 19 | "post-checkout", 20 | "post-merge", 21 | "pre-push", 22 | "pre-receive", 23 | "update", 24 | "proc-receive", 25 | "post-receive", 26 | "post-update", 27 | "reference-transaction", 28 | "push-to-checkout", 29 | "pre-auto-gc", 30 | "post-rewrite", 31 | "sendemail-validate", 32 | "fsmonitor-watchman", 33 | "p4-changelist", 34 | "p4-prepare-changelist", 35 | "p4-post-changelist", 36 | "p4-pre-submit", 37 | "post-index-change", 38 | } 39 | 40 | var hookScript = `#!/bin/bash 41 | set -e 42 | 43 | ELC_BINARY="%s" 44 | HOOKS_FOLDER="%s" 45 | HOOK_NAME="%s" 46 | 47 | if command -v $ELC_BINARY &> /dev/null; then 48 | $ELC_BINARY --mode=hook --no-tty $0 49 | else 50 | for script in ./$HOOKS_FOLDER/$HOOK_NAME/* ; do 51 | if [ -f $script ]; then 52 | $script 53 | fi 54 | done 55 | fi 56 | ` 57 | 58 | func GenerateHookScripts(options *GlobalOptions, svcPath string, elcBinary string, scriptsFolder string) error { 59 | gitPath := fmt.Sprintf("%s/.git", svcPath) 60 | if Pc.FileExists(gitPath) == false { 61 | _, _ = Pc.Println(fmt.Sprintf("\033[0;33mRepository %s is not exists, skip hooks installation.\033[0m", gitPath)) 62 | return nil 63 | } 64 | 65 | hooksPath := fmt.Sprintf("%s/hooks", gitPath) 66 | if Pc.FileExists(hooksPath) == false { 67 | if options.Debug { 68 | _, _ = Pc.Printf("mkdir %s\n", hooksPath) 69 | } 70 | 71 | if !options.DryRun { 72 | err := Pc.CreateDir(hooksPath) 73 | if err != nil { 74 | return err 75 | } 76 | } 77 | } 78 | 79 | scriptsFolder = strings.ReplaceAll(scriptsFolder, "./", "") 80 | scriptsFolder = strings.Trim(scriptsFolder, "/") 81 | 82 | scriptsFolderPath := fmt.Sprintf("%s/%s", svcPath, scriptsFolder) 83 | if Pc.FileExists(scriptsFolderPath) == false { 84 | _, _ = Pc.Println(fmt.Sprintf("\033[0;33mFolder %s is not exists, skip hooks installation.\033[0m", scriptsFolderPath)) 85 | return nil 86 | } 87 | 88 | for _, hookName := range hookNames { 89 | scriptPath := fmt.Sprintf("%s/%s", hooksPath, hookName) 90 | scriptContent := fmt.Sprintf(hookScript, elcBinary, scriptsFolder, hookName) 91 | 92 | var filePermissions os.FileMode = 0775 93 | 94 | if Pc.FileExists(scriptPath) == false { 95 | if options.Debug { 96 | _, _ = Pc.Printf("touch %s\n", scriptPath) 97 | _, _ = Pc.Printf("chmod %s %s\n", filePermissions, scriptPath) 98 | } 99 | 100 | if !options.DryRun { 101 | err := Pc.CreateFile(scriptPath) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | err = Pc.Chmod(scriptPath, filePermissions) 107 | if err != nil { 108 | return err 109 | } 110 | } 111 | } 112 | 113 | if options.Debug { 114 | _, _ = Pc.Printf("echo \"