diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 0a4e1990..a77a02fa 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -27,9 +27,15 @@ We strongly recommend you to write your message in English for 2 main reasons: 4. Further info a. Which OS / distro? -b. Uses VirtualBox? Which version? +b. Which VirtualBox version? (If running Mac OS X) c. Can you provide us your Azkfile.js? +```js +systems({ + 'my-app': {} + }); +``` + **ProTip**: run `azk version --full` to gather those info. 5. Do you have any suggestions on how to tackle this? diff --git a/Azkfile.js b/Azkfile.js index 7ae2a394..96ae4f50 100644 --- a/Azkfile.js +++ b/Azkfile.js @@ -3,35 +3,39 @@ */ var mounts = { - "/azk/#{manifest.dir}" : sync('./', { shell: true }), + "/azk/#{manifest.dir}" : sync('./', { shell: true }), + "/azk/#{manifest.dir}/docs" : path("./docs"), "/azk/#{manifest.dir}/package" : path("./package"), - "/azk/#{manifest.dir}/docs" : path("./docs"), "/azk/#{manifest.dir}/node_modules" : persistent('node_modules-#{system.name}'), - "/azk/#{manifest.dir}/.package-envs" : path("./.package-envs"), - "/azk/demos" : path("../demos"), - "/azk/build" : persistent('build-#{system.name}'), - "/azk/lib" : persistent('lib-#{system.name}'), - "/azk/data" : persistent('data-#{system.name}'), - "/azk/aptly" : persistent('aptly-#{system.name}'), - "/var/lib/docker" : persistent('docker_files-#{system.name}'), - "/root/.npm" : persistent('npm-cache'), - "/usr/local/bin/make" : path("./src/libexec/make"), - "/root/.aptly.conf" : path("./src/libexec/aptly.json"), - "/.tmux.conf" : path(env.HOME + "/.tmux.conf"), + "/azk/#{manifest.dir}/.package-envs": path("./.package-envs"), + "/azk/demos" : path("../demos"), + "/azk/build" : persistent('build-#{system.name}'), + "/azk/lib" : persistent('lib-#{system.name}'), + "/azk/data" : persistent('data-#{system.name}'), + "/azk/aptly" : persistent('aptly-#{system.name}'), + "/var/lib/docker" : persistent('docker_files-#{system.name}'), + "/root/.npm" : persistent('npm-cache'), + "/usr/local/bin/make": path("./src/libexec/make"), // Force `make -e` + "/root/.aptly.conf" : path("./src/libexec/aptly.json"), + "/.tmux.conf" : path(env.HOME + "/.tmux.conf"), }; var envs = { - PATH: "/azk/#{manifest.dir}/bin:/usr/local/bundle/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + VERSION: "#{azk.version}", + PATH : "/azk/#{manifest.dir}/bin:/usr/local/bundle/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", AZK_ENV : "development", AZK_DATA_PATH: "/azk/data", AZK_LIB_PATH : "/azk/lib", AZK_NAMESPACE: "azk.linux", AZK_PACKAGE_PATH: "/azk/build", + + // dind options + LOG : "file", + EXTRA_SCRIPT : "/azk/#{manifest.dir}/src/libexec/init_azk", + + // Balancer configuration to not conflict AZK_BALANCER_HOST: "linux.azk.io", AZK_BALANCER_PORT: 8080, - LOG: "file", - EXTRA_SCRIPT: "/azk/#{manifest.dir}/src/libexec/init_azk", - VERSION: "#{azk.version}", }; systems({ @@ -73,6 +77,11 @@ systems({ image: { docker: 'azukiapp/dind:ubuntu15' }, }, + 'dind-ubuntu16': { + extends: "dind-ubuntu12", + image: { docker: 'azukiapp/dind:ubuntu16' }, + }, + 'dind-fedora': { extends: "dind-ubuntu12", image: { docker: 'azukiapp/dind:fedora20' }, @@ -109,6 +118,11 @@ systems({ image: { docker: 'azukiapp/dind:ubuntu15' }, }, + 'pkg-ubuntu16-test': { + extends: "pkg-ubuntu12-test", + image: { docker: 'azukiapp/dind:ubuntu16' }, + }, + 'pkg-fedora20-test': { extends: "pkg-ubuntu12-test", image: { docker: 'azukiapp/dind:fedora20' }, diff --git a/CHANGELOG.md b/CHANGELOG.md index d40d82d1..dfcbd072 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,32 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## v0.19.0 - (2016-09-28) + +* Bug + * [Core] Simplification the images downloads process; + * [Core] Fixing regex bug in expand envs (not expand numbers: `$1` or `${2}`); + * [Core] Adding image envs in expand envs variable process; + * [Core] Adding support to escape variables in command options (shell and system); + * [Cli] Fixing error thrown when 'azk scale' command was run, regardless it was successful or not; + * [Cli] Fixing escape character in `azk vm ssh`; + * [Sync] Fixing bug when the path to be synced contained whitespace (#672); + * [Sync] Removing `system.name` from the destination sync path, which avoids multiple syncs for extended systems; + * [Systems] Fixing load envs from file with `=` in value; + +* Enhancements + * [Suggestions] Refactoring court to support more suggestion for one evidence; + * [Suggestions] Adding support wordpress; + * [Core] Support images of other repositories beyond Docker Hub; + * [Sync] Adding support special characters in sync paths; + * [Sync] Updating `chokidar` lib (file watching), for a better performance and bug fixes; + * [Sync] Refactoring the sync system to sync the file tree from the modified file, not the whole tree; + * [Sync] Improving support to `.gitignore` file, ensuring all the paths listed in it are properly resolved; + * [Sync] Improving fault tolerance for the sync process; + * [Sync] Removing a synced folder shouldn't break the agent nor the sync process; + * [Package] Adding `ubuntu 16.04` support; + * [Balancer] Support for tweaking Load Balancer settings via environment (`AZK_BALANCER_{WORKERS,WORKER_MAX_SOCKETS,TCP_TIMEOUT,DEAD_BACKEND_TTL}`); + ## v0.18.0 - (2016-04-07) * Enhancements diff --git a/Makefile b/Makefile index 0bca2930..4b7e57a9 100644 --- a/Makefile +++ b/Makefile @@ -136,7 +136,7 @@ PACKAGE_NPM_VERSION_FILE := ${PATH_AZK_NVM}/npm_versions package_check_node_dependencies: ${NODE_PACKAGE} @if [ ! "$$(${AZK_BIN} nvm npm --version)" = "${NPM_VERSION}" ] ; then \ - rm ${PACKAGE_NPM_VERSION_FILE}; \ + rm -f ${PACKAGE_NPM_VERSION_FILE}; \ fi # Build package folders tree diff --git a/README.md b/README.md index 176cfceb..f10eabd9 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ More information [here][azkfile]. ## Contributions -Check our [Contributing Guide](CONTRIBUTING.md) for instructions on how to help the project! +Check our [Contributing Guide](.github/CONTRIBUTING.md) for instructions on how to help the project! Share the love and star us here in Github! diff --git a/docs/content/en/installation/linux.md b/docs/content/en/installation/linux.md index 8439aad7..6f914eb2 100644 --- a/docs/content/en/installation/linux.md +++ b/docs/content/en/installation/linux.md @@ -4,7 +4,7 @@ ## Requirements -* **Distributions (tested)**: Ubuntu 12.04/14.04/15.10, Fedora 20/21/22 and Arch Linux +* **Distributions (tested)**: Ubuntu 12.04/14.04/15.10/16.04, Fedora 20/21/22 and Arch Linux * **Architecture**: 64-bits * [Docker][docker] 1.8.1 or later * Not running any services on ports `80` and `53` @@ -109,6 +109,10 @@ The easiest way to install `azk` is to use the script below. It will identify yo # Ubuntu Wily (15.10) $ echo "deb [arch=amd64] http://repo.azukiapp.com wily main" | \ sudo tee /etc/apt/sources.list.d/azuki.list + + # Ubuntu Xenial Xerus (16.04) + $ echo "deb [arch=amd64] http://repo.azukiapp.com xenial main" | \ + sudo tee /etc/apt/sources.list.d/azuki.list ``` 3. Update the list of packages and install azk: diff --git a/docs/content/en/reference/cli/restart.md b/docs/content/en/reference/cli/restart.md index e4710d17..1ed30cba 100644 --- a/docs/content/en/reference/cli/restart.md +++ b/docs/content/en/reference/cli/restart.md @@ -1,6 +1,12 @@ ## azk restart - Stops either all instances of the systems in the current `Azkfile.js` or the one specified and starts them back again. If an error occurs during reboot, all systems will be stopped. +Restart works by memorizing the current status of the systems in the current `Azkfile`, stops them and then get them back online again. + +If a `[system]` is specified with the command, `azk` will just restart the specified system. + +To restart a system and its dependencies is necessary to pass a list of systems to be restarted: `azk restart system_dependency,system_top`. + +If an error occurs during the reboot, all systems will be stopped. #### Usage: diff --git a/docs/content/en/reference/cli/scale.md b/docs/content/en/reference/cli/scale.md index f9f0f2c9..c4187055 100644 --- a/docs/content/en/reference/cli/scale.md +++ b/docs/content/en/reference/cli/scale.md @@ -1,6 +1,6 @@ ## azk scale - Scales (up or down) one or more systems. + Scales the default or a specified system to a total amount of instances. #### Usage: @@ -10,7 +10,7 @@ ``` system System name where the action will take place. - to Number of available instances after scaling. + to New total amount of instances. ``` #### Options: diff --git a/docs/content/en/reference/cli/shell.md b/docs/content/en/reference/cli/shell.md index 66e03d5d..6698f802 100644 --- a/docs/content/en/reference/cli/shell.md +++ b/docs/content/en/reference/cli/shell.md @@ -1,6 +1,6 @@ ## azk shell - Initializes a shell with the instance context, or executes an arbitrary command. + Initializes a new shell instance with the application context, or executes an arbitrary command if provided. #### Usage: diff --git a/docs/content/pt-BR/installation/linux.md b/docs/content/pt-BR/installation/linux.md index eb0ed3cb..4139c006 100644 --- a/docs/content/pt-BR/installation/linux.md +++ b/docs/content/pt-BR/installation/linux.md @@ -4,7 +4,7 @@ ## Requisitos -* **Distribuições (testadas)**: Ubuntu 12.04/14.04/15.10, Fedora 20/21/22 e Arch Linux +* **Distribuições (testadas)**: Ubuntu 12.04/14.04/15.10/16.04, Fedora 20/21/22 e Arch Linux * **Arquitetura**: 64-bits * [Docker][docker] 1.8.1 ou mais recente * Não estar rodando nenhum serviço nas portas `80` e `53` @@ -109,6 +109,10 @@ A forma mais fácil de instalar o `azk` é utilizar o script abaixo. Ele vai ide # Ubuntu Wily (15.10) $ echo "deb [arch=amd64] http://repo.azukiapp.com wily main" | \ sudo tee /etc/apt/sources.list.d/azuki.list + + # Ubuntu Xenial Xerus (16.04) + $ echo "deb [arch=amd64] http://repo.azukiapp.com xenial main" | \ + sudo tee /etc/apt/sources.list.d/azuki.list ``` 3. Atualize a lista de pacotes e instale o azk: diff --git a/docs/content/pt-BR/reference/azkfilejs/expandable_properties.md b/docs/content/pt-BR/reference/azkfilejs/expandable_properties.md index 62967052..25b21cce 100644 --- a/docs/content/pt-BR/reference/azkfilejs/expandable_properties.md +++ b/docs/content/pt-BR/reference/azkfilejs/expandable_properties.md @@ -319,3 +319,23 @@ systems({ }, }); ``` + +Caso seja necessário é possível escapar uma variável para que ela não seja expandida: + +```js +systems({ + web: { + image: { docker: "azukiapp/ruby" }, + command: ["bundle", "exec", "rails", "-p", "\\$HTTP_PORT"], + envs: { + HTTP_PORT: "8080", + }, + }, +}); +``` + +O mesmo se aplica para o comando [shell][../cli/shell.md]: + +```sh +$ azk shell web -c `echo \$PATH` +``` diff --git a/docs/content/pt-BR/reference/cli/restart.md b/docs/content/pt-BR/reference/cli/restart.md index b9f98199..c0cb2203 100644 --- a/docs/content/pt-BR/reference/cli/restart.md +++ b/docs/content/pt-BR/reference/cli/restart.md @@ -1,6 +1,12 @@ ## azk restart - Para todas as instâncias dos sistemas do `Azkfile.js`, ou do especificado, e inicia novamente. Se encontrar algum erro durante o processe de inicialização, todos os sistemas são parados. +Restart funciona por memorizar o estatus atual dos sistemas do `Azfile.js`, para eles e inicia novamente. + +Se um `[system]` for especificado com o comando, `azk` irá reiniciar apenas este sistema. + +Para reiniciar um sistema e suas dependências é necessário passar uma lista de sistemas a serem reiniciados: `azk restart system_dependency,system_top`. + +Se ocorrer algum erro durante a inicialização de algum sistema, todos sistemas serão parados. #### Uso: diff --git a/docs/content/pt-BR/reference/cli/scale.md b/docs/content/pt-BR/reference/cli/scale.md index e67c55dc..f303e5c4 100644 --- a/docs/content/pt-BR/reference/cli/scale.md +++ b/docs/content/pt-BR/reference/cli/scale.md @@ -1,6 +1,6 @@ ## azk scale -Escalona (para cima ou para baixo) um ou mais sistemas. +Escalona o sistema padrão ou o específicado para um número total de instâncias. #### Uso: @@ -10,7 +10,7 @@ Escalona (para cima ou para baixo) um ou mais sistemas. ``` system Nome do sistema que receberá a ação. - to Número de instâncias disponívels após o escalonamento. + to Nova quantidade total de instâncias. ``` #### Opções: diff --git a/docs/content/pt-BR/reference/cli/shell.md b/docs/content/pt-BR/reference/cli/shell.md index 3db87490..83f1eb02 100644 --- a/docs/content/pt-BR/reference/cli/shell.md +++ b/docs/content/pt-BR/reference/cli/shell.md @@ -1,6 +1,6 @@ ## azk shell - Inicializa um shell com o contexto da instância, ou executa um comando arbitrário. +Inicializa uma nova instância shell com o contexto da aplicação ou executa um comando arbitrário se provido. #### Uso: diff --git a/docs/content/pt-BR/reference/cli/start.md b/docs/content/pt-BR/reference/cli/start.md index 2a840339..5412340a 100644 --- a/docs/content/pt-BR/reference/cli/start.md +++ b/docs/content/pt-BR/reference/cli/start.md @@ -23,12 +23,12 @@ --rebuild, -B Força a recriação ou o download da imagem antes de iniciar a instância. --open, -o Abre a URL do sistema no navegador padrão. --open-with=, -a Abre a URL do sistema no navegador espeficado. - --no-color Remove cores na saída padrão + --no-color Remove cores na saída padrão. --quiet, -q Nunca perguntar. --help, -h Mostrar ajuda de uso. - --log=, -l Defini o nível de log (padrão: error). - --verbose, -v Defini o nível de detalhes da saída - suporta múltiplos (-vv == --verbose 2) [padrão: 0]. - --git-ref= branch, tag ou commit para clonar no Git + --log=, -l Define o nível de log (padrão: error). + --verbose, -v Define o nível de detalhes da saída - suporta múltiplos (-vv == --verbose 2) [padrão: 0]. + --git-ref= branch, tag ou commit para clonar no Git. ``` #### Exemplos: diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 545dfc2e..daa74d31 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "azk", - "version": "0.18.0", + "version": "0.19.0", "dependencies": { "archiver": { "version": "0.21.0", @@ -4492,42 +4492,53 @@ "version": "0.2.0" }, "chokidar": { - "version": "1.0.1", + "version": "1.6.0", "dependencies": { "anymatch": { "version": "1.3.0", "dependencies": { + "arrify": { + "version": "1.0.1" + }, "micromatch": { - "version": "2.1.6", + "version": "2.3.10", "dependencies": { "arr-diff": { - "version": "1.0.1", + "version": "2.0.0", "dependencies": { - "array-slice": { - "version": "0.2.3" + "arr-flatten": { + "version": "1.0.1" } } }, + "array-unique": { + "version": "0.2.1" + }, "braces": { - "version": "1.8.0", + "version": "1.8.5", "dependencies": { "expand-range": { - "version": "1.8.1", + "version": "1.8.2", "dependencies": { "fill-range": { - "version": "2.2.2", + "version": "2.2.3", "dependencies": { "is-number": { - "version": "1.1.2" + "version": "2.1.0" }, "isobject": { - "version": "1.0.0" + "version": "2.1.0", + "dependencies": { + "isarray": { + "version": "1.0.0" + } + } }, "randomatic": { - "version": "1.1.0" + "version": "1.1.5" }, "repeat-string": { - "version": "1.5.2" + "version": "1.5.4" } } } @@ -4541,63 +4552,66 @@ } } }, - "debug": { - "version": "2.2.0", + "expand-brackets": { + "version": "0.1.5", "dependencies": { - "ms": { - "version": "0.7.1" + "is-posix-bracket": { + "version": "0.1.1" } } }, - "expand-brackets": { - "version": "0.1.1" + "extglob": { + "version": "0.3.2" }, "filename-regex": { "version": "2.0.0" }, + "is-extglob": { + "version": "1.0.0" + }, "kind-of": { - "version": "1.1.0" + "version": "3.0.3", + "dependencies": { + "is-buffer": { + "version": "1.1.3" + } + } + }, + "normalize-path": { + "version": "2.0.1" }, "object.omit": { - "version": "0.2.1", + "version": "2.0.0", "dependencies": { "for-own": { - "version": "0.1.3", + "version": "0.1.4", "dependencies": { "for-in": { - "version": "0.1.4" + "version": "0.1.5" } } }, - "isobject": { - "version": "0.2.0" + "is-extendable": { + "version": "0.1.1" } } }, "parse-glob": { - "version": "3.0.2", + "version": "3.0.4", "dependencies": { "glob-base": { - "version": "0.2.0" + "version": "0.3.0" }, "is-dotfile": { - "version": "1.0.0" - }, - "is-extglob": { - "version": "1.0.0" + "version": "1.0.2" } } }, "regex-cache": { - "version": "0.4.2", + "version": "0.4.3", "dependencies": { "is-equal-shallow": { - "version": "0.1.2", - "dependencies": { - "is-primitive": { - "version": "1.0.0" - } - } + "version": "0.1.3" }, "is-primitive": { "version": "2.0.0" @@ -4608,916 +4622,875 @@ } } }, - "arrify": { - "version": "1.0.0" - }, "async-each": { - "version": "0.1.6" + "version": "1.0.0" }, "glob-parent": { - "version": "1.2.0" + "version": "2.0.0" + }, + "inherits": { + "version": "2.0.1" }, "is-binary-path": { - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "binary-extensions": { - "version": "1.3.0" + "version": "1.4.1" } } }, "is-glob": { - "version": "1.1.3" + "version": "2.0.1", + "dependencies": { + "is-extglob": { + "version": "1.0.0" + } + } + }, + "path-is-absolute": { + "version": "1.0.0" }, "readdirp": { - "version": "1.3.0", + "version": "2.1.0", "dependencies": { "graceful-fs": { - "version": "2.0.3" + "version": "4.1.4" }, "minimatch": { - "version": "0.2.14", + "version": "3.0.2", "dependencies": { - "lru-cache": { - "version": "2.6.2" - }, - "sigmund": { - "version": "1.0.0" + "brace-expansion": { + "version": "1.1.5", + "dependencies": { + "balanced-match": { + "version": "0.4.1" + }, + "concat-map": { + "version": "0.0.1" + } + } } } }, "readable-stream": { - "version": "1.0.33", + "version": "2.1.4", "dependencies": { + "buffer-shims": { + "version": "1.0.0" + }, "core-util-is": { - "version": "1.0.1" + "version": "1.0.2" }, "isarray": { - "version": "0.0.1" + "version": "1.0.0" + }, + "process-nextick-args": { + "version": "1.0.7" }, "string_decoder": { "version": "0.10.31" }, - "inherits": { - "version": "2.0.1" - } - } - } - } - } - } - }, - "cli-router": { - "version": "0.3.7", - "dependencies": { - "docopt": { - "version": "0.6.2", - "resolved": "git+https://github.com/gullitmiranda/docopt.coffee.git#45ed4031549a7b083cca3a9e6af149003e03878e" - }, - "ramda": { - "version": "0.14.0" - }, - "source-map-support": { - "version": "0.2.10", - "dependencies": { - "source-map": { - "version": "0.1.32", - "dependencies": { - "amdefine": { - "version": "1.0.0" + "util-deprecate": { + "version": "1.0.2" } } - } - } - } - } - }, - "cli-table": { - "version": "0.3.1", - "resolved": "git://github.com/gullitmiranda/cli-table.git#f763bb5680d15de1cdb670f5908378bbd455a258", - "dependencies": { - "colors": { - "version": "1.0.3" - } - } - }, - "concurrent-transform": { - "version": "1.0.0" - }, - "connectivity": { - "version": "0.1.1", - "dependencies": { - "once": { - "version": "1.3.1", - "dependencies": { - "wrappy": { + }, + "set-immediate-shim": { "version": "1.0.1" } } - } - } - }, - "crash-report-sender": { - "version": "0.5.0", - "dependencies": { - "async": { - "version": "1.5.1" - }, - "jstream": { - "version": "0.2.8", - "dependencies": { - "clarinet": { - "version": "0.11.0" - } - } }, - "lodash.merge": { - "version": "3.3.2", + "fsevents": { + "version": "1.0.12", "dependencies": { - "lodash._arraycopy": { - "version": "3.0.0" - }, - "lodash._arrayeach": { - "version": "3.0.0" + "nan": { + "version": "2.3.5" }, - "lodash._createassigner": { - "version": "3.1.1", + "node-pre-gyp": { + "version": "0.6.25", "dependencies": { - "lodash._bindcallback": { - "version": "3.0.1" - }, - "lodash._isiterateecall": { - "version": "3.0.9" - }, - "lodash.restparam": { - "version": "3.6.1" + "nopt": { + "version": "3.0.6", + "dependencies": { + "abbrev": { + "version": "1.0.7" + } + } } } }, - "lodash._getnative": { - "version": "3.9.1" + "ansi": { + "version": "0.3.1" }, - "lodash.isarguments": { - "version": "3.0.4" + "ansi-regex": { + "version": "2.0.0" }, - "lodash.isarray": { - "version": "3.0.4" + "ansi-styles": { + "version": "2.2.1" }, - "lodash.isplainobject": { - "version": "3.2.0", - "dependencies": { - "lodash._basefor": { - "version": "3.0.2" - } - } + "are-we-there-yet": { + "version": "1.1.2" }, - "lodash.istypedarray": { - "version": "3.0.2" + "asn1": { + "version": "0.2.3" }, - "lodash.keys": { - "version": "3.1.2" + "assert-plus": { + "version": "0.2.0" }, - "lodash.keysin": { - "version": "3.0.8" + "async": { + "version": "1.5.2" }, - "lodash.toplainobject": { - "version": "3.0.0", - "dependencies": { - "lodash._basecopy": { - "version": "3.0.1" - } - } - } - } - }, - "node-uuid": { - "version": "1.4.7" - }, - "request": { - "version": "2.67.0", - "dependencies": { - "bl": { - "version": "1.0.0", - "dependencies": { - "readable-stream": { - "version": "2.0.5", - "dependencies": { - "core-util-is": { - "version": "1.0.2" - }, - "inherits": { - "version": "2.0.1" - }, - "isarray": { - "version": "0.0.1" - }, - "process-nextick-args": { - "version": "1.0.6" - }, - "string_decoder": { - "version": "0.10.31" - }, - "util-deprecate": { - "version": "1.0.2" - } - } - } - } + "aws-sign2": { + "version": "0.6.0" + }, + "bl": { + "version": "1.0.3" + }, + "block-stream": { + "version": "0.0.8" + }, + "boom": { + "version": "2.10.1" }, "caseless": { "version": "0.11.0" }, + "chalk": { + "version": "1.1.3" + }, + "combined-stream": { + "version": "1.0.5" + }, + "commander": { + "version": "2.9.0" + }, + "core-util-is": { + "version": "1.0.2" + }, + "cryptiles": { + "version": "2.0.5" + }, + "deep-extend": { + "version": "0.4.1" + }, + "debug": { + "version": "2.2.0" + }, + "delayed-stream": { + "version": "1.0.0" + }, + "delegates": { + "version": "1.0.0" + }, + "ecc-jsbn": { + "version": "0.1.1" + }, + "escape-string-regexp": { + "version": "1.0.5" + }, "extend": { "version": "3.0.0" }, + "extsprintf": { + "version": "1.0.2" + }, "forever-agent": { "version": "0.6.1" }, "form-data": { - "version": "1.0.0-rc3" + "version": "1.0.0-rc4" + }, + "fstream": { + "version": "1.0.8" + }, + "gauge": { + "version": "1.2.7" + }, + "generate-function": { + "version": "2.0.0" + }, + "generate-object-property": { + "version": "1.2.0" + }, + "graceful-fs": { + "version": "4.1.3" + }, + "graceful-readlink": { + "version": "1.0.1" + }, + "har-validator": { + "version": "2.0.6" + }, + "has-ansi": { + "version": "2.0.0" + }, + "has-unicode": { + "version": "2.0.0" + }, + "hawk": { + "version": "3.1.3" + }, + "hoek": { + "version": "2.16.3" + }, + "http-signature": { + "version": "1.1.1" + }, + "inherits": { + "version": "2.0.1" + }, + "ini": { + "version": "1.3.4" + }, + "is-my-json-valid": { + "version": "2.13.1" + }, + "is-property": { + "version": "1.0.2" + }, + "is-typedarray": { + "version": "1.0.0" + }, + "isarray": { + "version": "1.0.0" + }, + "isstream": { + "version": "0.1.2" + }, + "jodid25519": { + "version": "1.0.2" + }, + "jsbn": { + "version": "0.1.0" + }, + "json-schema": { + "version": "0.2.2" }, "json-stringify-safe": { "version": "5.0.1" }, + "jsonpointer": { + "version": "2.0.0" + }, + "jsprim": { + "version": "1.2.2" + }, + "lodash.pad": { + "version": "4.1.0" + }, + "lodash.padend": { + "version": "4.2.0" + }, + "lodash.padstart": { + "version": "4.2.0" + }, + "lodash.repeat": { + "version": "4.0.0" + }, + "lodash.tostring": { + "version": "4.1.2" + }, "mime-types": { - "version": "2.1.8", - "dependencies": { - "mime-db": { - "version": "1.20.0" - } - } + "version": "2.1.10" + }, + "mime-db": { + "version": "1.22.0" + }, + "minimist": { + "version": "0.0.8" + }, + "mkdirp": { + "version": "0.5.1" + }, + "ms": { + "version": "0.7.1" + }, + "node-uuid": { + "version": "1.4.7" + }, + "npmlog": { + "version": "2.0.3" + }, + "oauth-sign": { + "version": "0.8.1" + }, + "once": { + "version": "1.3.3" + }, + "pinkie": { + "version": "2.0.4" + }, + "pinkie-promise": { + "version": "2.0.0" + }, + "process-nextick-args": { + "version": "1.0.6" }, "qs": { - "version": "5.2.0" + "version": "6.0.2" + }, + "readable-stream": { + "version": "2.0.6" + }, + "request": { + "version": "2.69.0" + }, + "semver": { + "version": "5.1.0" + }, + "sntp": { + "version": "1.0.9" + }, + "sshpk": { + "version": "1.7.4" + }, + "string_decoder": { + "version": "0.10.31" + }, + "stringstream": { + "version": "0.0.5" + }, + "strip-ansi": { + "version": "3.0.1" + }, + "strip-json-comments": { + "version": "1.0.4" + }, + "supports-color": { + "version": "2.0.0" + }, + "tar": { + "version": "2.2.1" + }, + "tar-pack": { + "version": "3.1.3" + }, + "tough-cookie": { + "version": "2.2.2" }, "tunnel-agent": { "version": "0.4.2" }, - "tough-cookie": { - "version": "2.2.1" + "tweetnacl": { + "version": "0.14.3" }, - "http-signature": { - "version": "1.1.0", + "uid-number": { + "version": "0.0.6" + }, + "util-deprecate": { + "version": "1.0.2" + }, + "verror": { + "version": "1.3.6" + }, + "wrappy": { + "version": "1.0.1" + }, + "xtend": { + "version": "4.0.1" + }, + "dashdash": { + "version": "1.13.0", "dependencies": { "assert-plus": { - "version": "0.1.5" - }, - "jsprim": { - "version": "1.2.2", + "version": "1.0.0" + } + } + }, + "rc": { + "version": "1.1.6", + "dependencies": { + "minimist": { + "version": "1.2.0" + } + } + }, + "aws4": { + "version": "1.3.2", + "dependencies": { + "lru-cache": { + "version": "4.0.1", "dependencies": { - "extsprintf": { + "pseudomap": { "version": "1.0.2" }, - "json-schema": { - "version": "0.2.2" - }, - "verror": { - "version": "1.3.6" + "yallist": { + "version": "2.0.0" } } - }, - "sshpk": { - "version": "1.7.1", + } + } + }, + "fstream-ignore": { + "version": "1.0.3", + "dependencies": { + "minimatch": { + "version": "3.0.0", "dependencies": { - "asn1": { - "version": "0.2.3" - }, - "assert-plus": { - "version": "0.2.0" - }, - "dashdash": { - "version": "1.11.0", + "brace-expansion": { + "version": "1.1.3", "dependencies": { - "assert-plus": { - "version": "0.1.5" + "balanced-match": { + "version": "0.3.0" + }, + "concat-map": { + "version": "0.0.1" } } - }, - "jsbn": { - "version": "0.1.0" - }, - "tweetnacl": { - "version": "0.13.2" - }, - "jodid25519": { - "version": "1.0.2" - }, - "ecc-jsbn": { - "version": "0.1.1" } } } } }, - "oauth-sign": { - "version": "0.8.0" - }, - "hawk": { - "version": "3.1.2", - "dependencies": { - "hoek": { - "version": "2.16.3" - }, - "boom": { - "version": "2.10.1" - }, - "cryptiles": { - "version": "2.0.5" - }, - "sntp": { - "version": "1.0.9" - } - } - }, - "aws-sign2": { - "version": "0.6.0" - }, - "stringstream": { - "version": "0.0.5" - }, - "combined-stream": { - "version": "1.0.5", + "rimraf": { + "version": "2.5.2", "dependencies": { - "delayed-stream": { - "version": "1.0.0" - } - } - }, - "isstream": { - "version": "0.1.2" - }, - "is-typedarray": { - "version": "1.0.0" - }, - "har-validator": { - "version": "2.0.3", - "dependencies": { - "chalk": { - "version": "1.1.1", + "glob": { + "version": "7.0.3", "dependencies": { - "ansi-styles": { - "version": "2.1.0" - }, - "escape-string-regexp": { - "version": "1.0.4" - }, - "has-ansi": { - "version": "2.0.0", + "inflight": { + "version": "1.0.4", "dependencies": { - "ansi-regex": { - "version": "2.0.0" + "wrappy": { + "version": "1.0.1" } } }, - "strip-ansi": { + "inherits": { + "version": "2.0.1" + }, + "minimatch": { "version": "3.0.0", "dependencies": { - "ansi-regex": { - "version": "2.0.0" + "brace-expansion": { + "version": "1.1.3", + "dependencies": { + "balanced-match": { + "version": "0.3.0" + }, + "concat-map": { + "version": "0.0.1" + } + } } } }, - "supports-color": { - "version": "2.0.0" - } - } - }, - "commander": { - "version": "2.9.0", - "dependencies": { - "graceful-readlink": { - "version": "1.0.1" - } - } - }, - "is-my-json-valid": { - "version": "2.12.3", - "dependencies": { - "generate-function": { - "version": "2.0.0" - }, - "generate-object-property": { - "version": "1.2.0", + "once": { + "version": "1.3.3", "dependencies": { - "is-property": { - "version": "1.0.2" + "wrappy": { + "version": "1.0.1" } } }, - "jsonpointer": { - "version": "2.0.0" - }, - "xtend": { - "version": "4.0.1" - } - } - }, - "pinkie-promise": { - "version": "2.0.0", - "dependencies": { - "pinkie": { - "version": "2.0.1" + "path-is-absolute": { + "version": "1.0.0" } } } } } } + } + } + }, + "cli-router": { + "version": "0.3.7", + "dependencies": { + "docopt": { + "version": "0.6.2", + "resolved": "git+https://github.com/gullitmiranda/docopt.coffee.git#45ed4031549a7b083cca3a9e6af149003e03878e" }, - "rollbar": { - "version": "0.5.11", + "ramda": { + "version": "0.14.0" + }, + "source-map-support": { + "version": "0.2.10", "dependencies": { - "lru-cache": { - "version": "2.2.4" - }, - "json-stringify-safe": { - "version": "5.0.1" - }, - "async": { - "version": "1.2.1" + "source-map": { + "version": "0.1.32", + "dependencies": { + "amdefine": { + "version": "1.0.0" + } + } } } } } }, - "dirdiff": { - "version": "0.0.1", + "cli-table": { + "version": "0.3.1", + "resolved": "git://github.com/gullitmiranda/cli-table.git#f763bb5680d15de1cdb670f5908378bbd455a258", "dependencies": { - "glob": { - "version": "3.1.21", + "colors": { + "version": "1.0.3" + } + } + }, + "concurrent-transform": { + "version": "1.0.0" + }, + "connectivity": { + "version": "0.1.1", + "dependencies": { + "once": { + "version": "1.3.1", "dependencies": { - "minimatch": { - "version": "0.2.14", - "dependencies": { - "lru-cache": { - "version": "2.6.4" - }, - "sigmund": { - "version": "1.0.1" - } - } - }, - "graceful-fs": { - "version": "1.2.3" - }, - "inherits": { - "version": "1.0.0" + "wrappy": { + "version": "1.0.1" } } } } }, - "docker-registry-downloader": { - "version": "0.3.2", + "crash-report-sender": { + "version": "0.5.0", "dependencies": { - "babel-regenerator-runtime": { - "version": "6.3.13" + "async": { + "version": "1.5.1" }, - "pretty-bytes": { - "version": "1.0.4", + "jstream": { + "version": "0.2.8", "dependencies": { - "get-stdin": { - "version": "4.0.1" + "clarinet": { + "version": "0.11.0" + } + } + }, + "lodash.merge": { + "version": "3.3.2", + "dependencies": { + "lodash._arraycopy": { + "version": "3.0.0" }, - "meow": { - "version": "3.7.0", + "lodash._arrayeach": { + "version": "3.0.0" + }, + "lodash._createassigner": { + "version": "3.1.1", "dependencies": { - "camelcase-keys": { - "version": "2.0.0", - "dependencies": { - "camelcase": { - "version": "2.0.1" - } - } - }, - "decamelize": { - "version": "1.1.2", - "dependencies": { - "escape-string-regexp": { - "version": "1.0.4" - } - } - }, - "loud-rejection": { - "version": "1.2.0", - "dependencies": { - "signal-exit": { - "version": "2.1.2" - } - } - }, - "map-obj": { - "version": "1.0.1" + "lodash._bindcallback": { + "version": "3.0.1" }, - "minimist": { - "version": "1.2.0" + "lodash._isiterateecall": { + "version": "3.0.9" }, - "normalize-package-data": { - "version": "2.3.5", + "lodash.restparam": { + "version": "3.6.1" + } + } + }, + "lodash._getnative": { + "version": "3.9.1" + }, + "lodash.isarguments": { + "version": "3.0.4" + }, + "lodash.isarray": { + "version": "3.0.4" + }, + "lodash.isplainobject": { + "version": "3.2.0", + "dependencies": { + "lodash._basefor": { + "version": "3.0.2" + } + } + }, + "lodash.istypedarray": { + "version": "3.0.2" + }, + "lodash.keys": { + "version": "3.1.2" + }, + "lodash.keysin": { + "version": "3.0.8" + }, + "lodash.toplainobject": { + "version": "3.0.0", + "dependencies": { + "lodash._basecopy": { + "version": "3.0.1" + } + } + } + } + }, + "node-uuid": { + "version": "1.4.7" + }, + "request": { + "version": "2.67.0", + "dependencies": { + "bl": { + "version": "1.0.0", + "dependencies": { + "readable-stream": { + "version": "2.0.5", "dependencies": { - "hosted-git-info": { - "version": "2.1.4" + "core-util-is": { + "version": "1.0.2" }, - "is-builtin-module": { - "version": "1.0.0", - "dependencies": { - "builtin-modules": { - "version": "1.1.1" - } - } + "inherits": { + "version": "2.0.1" }, - "validate-npm-package-license": { - "version": "3.0.1", - "dependencies": { - "spdx-correct": { - "version": "1.0.2", - "dependencies": { - "spdx-license-ids": { - "version": "1.2.0" - } - } - }, - "spdx-expression-parse": { - "version": "1.0.2", - "dependencies": { - "spdx-exceptions": { - "version": "1.0.4" - }, - "spdx-license-ids": { - "version": "1.2.0" - } - } - } - } - } - } - }, - "object-assign": { - "version": "4.0.1" - }, - "read-pkg-up": { - "version": "1.0.1", - "dependencies": { - "find-up": { - "version": "1.1.0", - "dependencies": { - "path-exists": { - "version": "2.1.0" - }, - "pinkie-promise": { - "version": "2.0.0", - "dependencies": { - "pinkie": { - "version": "2.0.1" - } - } - } - } + "isarray": { + "version": "0.0.1" }, - "read-pkg": { - "version": "1.1.0", - "dependencies": { - "load-json-file": { - "version": "1.1.0", - "dependencies": { - "graceful-fs": { - "version": "4.1.2" - }, - "parse-json": { - "version": "2.2.0", - "dependencies": { - "error-ex": { - "version": "1.3.0", - "dependencies": { - "is-arrayish": { - "version": "0.2.1" - } - } - } - } - }, - "pify": { - "version": "2.3.0" - }, - "pinkie-promise": { - "version": "2.0.0", - "dependencies": { - "pinkie": { - "version": "2.0.1" - } - } - }, - "strip-bom": { - "version": "2.0.0", - "dependencies": { - "is-utf8": { - "version": "0.2.1" - } - } - } - } - }, - "path-type": { - "version": "1.1.0", - "dependencies": { - "graceful-fs": { - "version": "4.1.2" - }, - "pify": { - "version": "2.3.0" - }, - "pinkie-promise": { - "version": "2.0.0", - "dependencies": { - "pinkie": { - "version": "2.0.1" - } - } - } - } - } - } - } - } - }, - "redent": { - "version": "1.0.0", - "dependencies": { - "indent-string": { - "version": "2.1.0", - "dependencies": { - "repeating": { - "version": "2.0.0", - "dependencies": { - "is-finite": { - "version": "1.0.1", - "dependencies": { - "number-is-nan": { - "version": "1.0.0" - } - } - } - } - } - } + "process-nextick-args": { + "version": "1.0.6" }, - "strip-indent": { - "version": "1.0.1" + "string_decoder": { + "version": "0.10.31" + }, + "util-deprecate": { + "version": "1.0.2" } } - }, - "trim-newlines": { - "version": "1.0.0" } } - } - } - }, - "progress": { - "version": "1.1.8" - }, - "requestretry": { - "version": "1.6.0", - "dependencies": { - "fg-lodash": { - "version": "0.0.2", + }, + "caseless": { + "version": "0.11.0" + }, + "extend": { + "version": "3.0.0" + }, + "forever-agent": { + "version": "0.6.1" + }, + "form-data": { + "version": "1.0.0-rc3" + }, + "json-stringify-safe": { + "version": "5.0.1" + }, + "mime-types": { + "version": "2.1.8", "dependencies": { - "lodash": { - "version": "2.4.2" - }, - "underscore.string": { - "version": "2.3.3" + "mime-db": { + "version": "1.20.0" } } }, - "request": { - "version": "2.67.0", + "qs": { + "version": "5.2.0" + }, + "tunnel-agent": { + "version": "0.4.2" + }, + "tough-cookie": { + "version": "2.2.1" + }, + "http-signature": { + "version": "1.1.0", "dependencies": { - "bl": { - "version": "1.0.0", - "dependencies": { - "readable-stream": { - "version": "2.0.5", - "dependencies": { - "core-util-is": { - "version": "1.0.2" - }, - "inherits": { - "version": "2.0.1" - }, - "isarray": { - "version": "0.0.1" - }, - "process-nextick-args": { - "version": "1.0.6" - }, - "string_decoder": { - "version": "0.10.31" - }, - "util-deprecate": { - "version": "1.0.2" - } - } - } - } - }, - "caseless": { - "version": "0.11.0" - }, - "extend": { - "version": "3.0.0" - }, - "forever-agent": { - "version": "0.6.1" - }, - "form-data": { - "version": "1.0.0-rc3", - "dependencies": { - "async": { - "version": "1.5.2" - } - } - }, - "json-stringify-safe": { - "version": "5.0.1" + "assert-plus": { + "version": "0.1.5" }, - "mime-types": { - "version": "2.1.9", + "jsprim": { + "version": "1.2.2", "dependencies": { - "mime-db": { - "version": "1.21.0" - } - } - }, - "node-uuid": { - "version": "1.4.7" - }, - "qs": { - "version": "5.2.0" - }, - "tunnel-agent": { - "version": "0.4.2" - }, - "tough-cookie": { - "version": "2.2.1" + "extsprintf": { + "version": "1.0.2" + }, + "json-schema": { + "version": "0.2.2" + }, + "verror": { + "version": "1.3.6" + } + } }, - "http-signature": { - "version": "1.1.0", + "sshpk": { + "version": "1.7.1", "dependencies": { - "assert-plus": { - "version": "0.1.5" + "asn1": { + "version": "0.2.3" }, - "jsprim": { - "version": "1.2.2", - "dependencies": { - "extsprintf": { - "version": "1.0.2" - }, - "json-schema": { - "version": "0.2.2" - }, - "verror": { - "version": "1.3.6" - } - } + "assert-plus": { + "version": "0.2.0" }, - "sshpk": { - "version": "1.7.3", + "dashdash": { + "version": "1.11.0", "dependencies": { - "asn1": { - "version": "0.2.3" - }, "assert-plus": { - "version": "0.2.0" - }, - "dashdash": { - "version": "1.12.1", - "dependencies": { - "assert-plus": { - "version": "0.1.5" - } - } - }, - "jsbn": { - "version": "0.1.0" - }, - "tweetnacl": { - "version": "0.13.3" - }, - "jodid25519": { - "version": "1.0.2" - }, - "ecc-jsbn": { - "version": "0.1.1" + "version": "0.1.5" } } - } - } - }, - "oauth-sign": { - "version": "0.8.0" - }, - "hawk": { - "version": "3.1.2", - "dependencies": { - "hoek": { - "version": "2.16.3" }, - "boom": { - "version": "2.10.1" + "jsbn": { + "version": "0.1.0" + }, + "tweetnacl": { + "version": "0.13.2" }, - "cryptiles": { - "version": "2.0.5" + "jodid25519": { + "version": "1.0.2" }, - "sntp": { - "version": "1.0.9" + "ecc-jsbn": { + "version": "0.1.1" } } + } + } + }, + "oauth-sign": { + "version": "0.8.0" + }, + "hawk": { + "version": "3.1.2", + "dependencies": { + "hoek": { + "version": "2.16.3" }, - "aws-sign2": { - "version": "0.6.0" - }, - "stringstream": { - "version": "0.0.5" - }, - "combined-stream": { - "version": "1.0.5", - "dependencies": { - "delayed-stream": { - "version": "1.0.0" - } - } + "boom": { + "version": "2.10.1" }, - "isstream": { - "version": "0.1.2" + "cryptiles": { + "version": "2.0.5" }, - "is-typedarray": { + "sntp": { + "version": "1.0.9" + } + } + }, + "aws-sign2": { + "version": "0.6.0" + }, + "stringstream": { + "version": "0.0.5" + }, + "combined-stream": { + "version": "1.0.5", + "dependencies": { + "delayed-stream": { "version": "1.0.0" - }, - "har-validator": { - "version": "2.0.3", + } + } + }, + "isstream": { + "version": "0.1.2" + }, + "is-typedarray": { + "version": "1.0.0" + }, + "har-validator": { + "version": "2.0.3", + "dependencies": { + "chalk": { + "version": "1.1.1", "dependencies": { - "commander": { - "version": "2.9.0", + "ansi-styles": { + "version": "2.1.0" + }, + "escape-string-regexp": { + "version": "1.0.4" + }, + "has-ansi": { + "version": "2.0.0", "dependencies": { - "graceful-readlink": { - "version": "1.0.1" + "ansi-regex": { + "version": "2.0.0" } } }, - "is-my-json-valid": { - "version": "2.12.3", + "strip-ansi": { + "version": "3.0.0", "dependencies": { - "generate-function": { - "version": "2.0.0" - }, - "generate-object-property": { - "version": "1.2.0", - "dependencies": { - "is-property": { - "version": "1.0.2" - } - } - }, - "jsonpointer": { + "ansi-regex": { "version": "2.0.0" - }, - "xtend": { - "version": "4.0.1" } } }, - "pinkie-promise": { - "version": "2.0.0", + "supports-color": { + "version": "2.0.0" + } + } + }, + "commander": { + "version": "2.9.0", + "dependencies": { + "graceful-readlink": { + "version": "1.0.1" + } + } + }, + "is-my-json-valid": { + "version": "2.12.3", + "dependencies": { + "generate-function": { + "version": "2.0.0" + }, + "generate-object-property": { + "version": "1.2.0", "dependencies": { - "pinkie": { - "version": "2.0.1" + "is-property": { + "version": "1.0.2" } } + }, + "jsonpointer": { + "version": "2.0.0" + }, + "xtend": { + "version": "4.0.1" + } + } + }, + "pinkie-promise": { + "version": "2.0.0", + "dependencies": { + "pinkie": { + "version": "2.0.1" } } } } - }, - "when": { - "version": "3.7.7" } } }, - "source-map-support": { - "version": "0.4.0", + "rollbar": { + "version": "0.5.11", "dependencies": { - "source-map": { - "version": "0.1.32", + "lru-cache": { + "version": "2.2.4" + }, + "json-stringify-safe": { + "version": "5.0.1" + }, + "async": { + "version": "1.2.1" + } + } + } + } + }, + "dirdiff": { + "version": "0.0.1", + "dependencies": { + "glob": { + "version": "3.1.21", + "dependencies": { + "minimatch": { + "version": "0.2.14", "dependencies": { - "amdefine": { - "version": "1.0.0" + "lru-cache": { + "version": "2.6.4" + }, + "sigmund": { + "version": "1.0.1" } } + }, + "graceful-fs": { + "version": "1.2.3" + }, + "inherits": { + "version": "1.0.0" } } - }, - "yargs": { - "version": "1.3.3" } } }, @@ -7455,10 +7428,6 @@ "printf": { "version": "0.2.0" }, - "progress": { - "version": "1.1.5", - "resolved": "git://github.com/nuxlli/node-progress.git#f9ab9f0f663628ea236ecdd2c7317c48c84fd8d6" - }, "request": { "version": "2.51.0", "dependencies": { @@ -7579,9 +7548,6 @@ "retry": { "version": "0.6.1" }, - "rsync": { - "version": "0.4.0" - }, "scp2": { "version": "0.1.4", "dependencies": { @@ -7841,6 +7807,83 @@ "version": "2.0.0" } } + }, + "multi-progress": { + "version": "1.0.0", + "dependencies": { + "progress": { + "version": "1.1.8" + } + } + }, + "sinon": { + "version": "1.17.4", + "dependencies": { + "formatio": { + "version": "1.1.1" + }, + "util": { + "version": "0.10.3", + "dependencies": { + "inherits": { + "version": "2.0.1" + } + } + }, + "lolex": { + "version": "1.3.2" + }, + "samsam": { + "version": "1.1.2" + } + } + }, + "active-handles": { + "version": "1.1.0", + "dependencies": { + "ansicolors": { + "version": "0.3.2" + }, + "cardinal": { + "version": "0.5.0", + "dependencies": { + "redeyed": { + "version": "0.5.0", + "dependencies": { + "esprima-fb": { + "version": "12001.1.0-dev-harmony-fb" + } + } + }, + "ansicolors": { + "version": "0.2.1" + } + } + }, + "function-origin": { + "version": "1.1.1", + "dependencies": { + "bindings": { + "version": "1.2.1" + }, + "nan": { + "version": "2.3.5" + } + } + }, + "xtend": { + "version": "4.0.1" + } + } + }, + "rsyncwrapper": { + "version": "1.0.0", + "resolved": "git+https://github.com/nuxlli/rsyncwrapper.git#37e01ce4cb328b8ee5d8709c507296d26e3127bc", + "dependencies": { + "lodash": { + "version": "2.4.2" + } + } } } } diff --git a/package.json b/package.json index 8d21658a..d242bb6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "azk", - "version": "0.18.0", + "version": "0.19.0", "description": "Orchestrate development environments with agility and automation", "main": "index.js", "scripts": { @@ -35,13 +35,13 @@ "babel-polyfill": "^6.3.14", "bluebird": "^3.1.1", "chalk": "^1.1.1", - "chokidar": "^1.0.1", + "chokidar": "^1.6.0", "cli-router": "^0.3.7", "cli-table": "git+https://github.com/gullitmiranda/cli-table#master", "connectivity": "^0.1.1", "crash-report-sender": "^0.5.0", - "docker-registry-downloader": "^0.3.2", "dockerode": "^2.2.2", + "dotenv": "^1.1.0", "exec-sh": "^0.1.3", "express": "^4.9.7", "express-ws": "^0.2.6", @@ -61,6 +61,7 @@ "memcachedjs": "0.0.1", "memorystream": "^0.2.0", "moment": "^2.8.1", + "multi-progress": "^1.0.0", "netmask": "^1.0.5", "node-uuid": "^1.4.1", "open": "0.0.5", @@ -70,10 +71,9 @@ "postal": "^1.0.2", "prettyjson": "^1.0.0", "printf": "^0.2.0", - "progress": "git+https://github.com/nuxlli/node-progress", "request": "^2.45.0", "retry": "^0.6.1", - "rsync": "^0.4.0", + "rsyncwrapper": "git+https://github.com/nuxlli/rsyncwrapper.git", "scp2": "^0.1.4", "semver": "^4.1.0", "source-map-support": "^0.4.0", @@ -88,18 +88,19 @@ "xregexp": "^2.0.0" }, "devDependencies": { + "active-handles": "^1.1.0", "archy": "^1.0.0", "azk-dev": "^0.3.0", "babel": "^6.3.26", "concurrent-transform": "^1.0.0", "dirdiff": "0.0.1", - "dotenv": "^1.1.0", "gulp": "^3.8.11", "gulp-awspublish": "^3.0.0", "gulp-if": "^1.2.5", "gulp-rename": "^1.2.2", "hotswap": "^1.1.0", "pretty-hrtime": "^1.0.0", + "sinon": "^1.17.4", "tmp": "0.0.23" } } diff --git a/shared/locales/en-US.js b/shared/locales/en-US.js index 7f5f62d9..e06d2647 100644 --- a/shared/locales/en-US.js +++ b/shared/locales/en-US.js @@ -255,16 +255,14 @@ module.exports = { generator: { found : [ "", - "[%(systemName)s] `A %(__type)s` system was detected at '%(dir)s'.", - "[%(systemName)s] The image suggested was `%(image)s`.", + "A `%(__type)s` system was detected at `%(dir)s`:", + "- The image suggested was `%(image)s`.", ].join("\n"), foundWithoutVersion: [ "", - "[%(systemName)s] A `%(__type)s` system was detected at '%(dir)s'.", - "[%(systemName)s] The image suggested was `%(image)s`.", - "[%(systemName)s] ! It was not possible to detect the `%(__type)s` specific version, so the standard version was suggested instead.", - "[%(systemName)s] ! To change the image version you must edit the `Azkfile.js` file.", - "[%(systemName)s] ! For more information see the documentation at http://docs.azk.io/en/images/index.html.", + "It was not possible to detect specific version(s) for `%(types)s`, so the standard(s) version was suggested instead.", + "To change the image(s) version you must edit the `Azkfile.js` file.", + "For more information see the documentation at http://docs.azk.io/en/images/index.html.", ].join("\n") }, @@ -375,21 +373,9 @@ module.exports = { options : "${green}Options:${green.close}", usage : '${blue}Usage:${blue.close}', }, - helpers: { - pull: { - pulling : 'Pulling repository %s...', - bar_progress : ' :title [:bar] :percent :progress', - bar_status : ' :title :msg', - pull_getLayersDiff: "${blue}⇲${blue.close}" + " comparing registry layers and local layers...", - pull_layers_left : "${blue}⇲${blue.close}" + " %(non_existent_locally_ids_count)s layers left to download.", - pull_start : "${blue}⇲${blue.close}" + " pulling %(left_to_download_count)s/%(total_registry_layers)s layers.", - pull_ended : "\n" + "${blue}✓${blue.close}" + " completed download of `" + "${yellow}%(image)s${yellow.close}" + "`\n", - already_being : "${yellow}⇲${yellow.close}" + " image already being pulled. Please wait...", - } - }, init: { already_exists: "'%s' already exists (try: `--force`)", - generated : "'%s' generated", + generated : "\n'%s' generated", github : "\nTip:\n Adds the `.azk` to .gitignore\n echo '.azk' >> .gitignore \n", not_found : "System(s) not found, generating with example system.", }, @@ -536,6 +522,16 @@ module.exports = { doctor: { deprecated: "Command `azk doctor` is deprecated, use `azk version --full` instead.", }, + views: { + image_pull: { + pull_ended : "\n${blue}✓${blue.close} completed download of `${yellow}%(image)s${yellow.close}`", + digest : "\n${blue}✓${blue.close} digest `${yellow}%(digest)s${yellow.close}`", + bar_progress : ':id: :msg [:bar] :percent :etas', + download : '${blue}⇲${blue.close} layer %(id)s', + pulling_fs_layer : '${blue}⇲${blue.close} layer %(id)s', + pulling_extracting: '${yellow}↻${yellow.close} layer %(id)s', + } + } }, docker: { diff --git a/shared/locales/usage-en-US.txt b/shared/locales/usage-en-US.txt index 6bf449a0..1fd3961d 100644 --- a/shared/locales/usage-en-US.txt +++ b/shared/locales/usage-en-US.txt @@ -21,7 +21,8 @@ Usage: azk start [] [] [--git-ref=] [--reprovision --rebuild --no-remove --open --open-with=] [-qh --no-color -l ] [-v]... azk status [] [--long --short --text] [-qh --no-color -l ] [-v]... azk stop [] [--no-remove] [-qh --no-color -l ] [-v]... - azk vm (ssh|start|status|installed|stop|remove) [--force] [-qh --no-color -l ] [-v]... [-- ...] + azk vm ssh [-qh --no-color -l --tty --no-tty] [-v]... [-- ...] + azk vm (start|status|installed|stop|remove) [--force] [-qh --no-color -l ] [-v]... azk [agent|config|vm] [--help] [-qh --no-color -l ] [-v]... azk help [] [-q --no-color -l ] [-v]... azk version [--full --logo] [-qh --no-color -l ] [-v]... diff --git a/shared/scripts/install.sh b/shared/scripts/install.sh index e47bc4b3..6e67e708 100755 --- a/shared/scripts/install.sh +++ b/shared/scripts/install.sh @@ -84,6 +84,9 @@ main(){ "15.10" ) UBUNTU_CODENAME="wily" ;; + "16.04" ) + UBUNTU_CODENAME="xenial" + ;; esac if [ -z "${UBUNTU_CODENAME}" ]; then diff --git a/spec/cli/download_part_spec.js b/spec/cli/download_part_spec.js deleted file mode 100644 index 359c4e7a..00000000 --- a/spec/cli/download_part_spec.js +++ /dev/null @@ -1,75 +0,0 @@ -import h from 'spec/spec_helper'; -import { DownloadPart } from 'azk/cli/download_part'; - -describe('DownloadPart progressbar', function() { - - var download_part, tick_callback; - var total_ticks = 0; - - tick_callback = function(number) { - total_ticks += number; - }; - total_ticks = 0; - - beforeEach(function () { - var msg = { - id: 'abcd', - progressDetail: { - current: 10, - total : 100 - } - }; - total_ticks = 0; - download_part = new DownloadPart(msg, 5, tick_callback); - }); - - it('should DownloadPart be instantiated', function() { - h.expect(download_part).to.not.be.undefined; - }); - - it('should have bars to spend', function() { - h.expect(download_part._representing_bars).to.be.equal(5); - }); - - it('should split total in equal parts', function() { - h.expect(download_part._part_size).to.be.equal(20); - }); - - it('should call _progress_bar tick()', function() { - download_part.update({ id: 'abcd', progressDetail: { current: 0, total : 100 } }); - h.expect(total_ticks).to.be.equal(0); - - download_part.update({ id: 'abcd', progressDetail: { current: 10, total : 100 } }); - h.expect(total_ticks).to.be.equal(0.5); - - download_part.update({ id: 'abcd', progressDetail: { current: 20, total : 100 } }); - h.expect(total_ticks).to.be.equal(1); - - download_part.update({ id: 'abcd', progressDetail: { current: 30, total : 100 } }); - h.expect(total_ticks).to.be.equal(1.5); - - download_part.update({ id: 'abcd', progressDetail: { current: 40, total : 100 } }); - h.expect(total_ticks).to.be.equal(2); - - download_part.update({ id: 'abcd', progressDetail: { current: 50, total : 100 } }); - h.expect(total_ticks).to.be.equal(2.5); - - download_part.update({ id: 'abcd', progressDetail: { current: 60, total : 100 } }); - h.expect(total_ticks).to.be.equal(3); - - download_part.update({ id: 'abcd', progressDetail: { current: 100, total : 100 } }); - h.expect(total_ticks).to.be.equal(5); - }); - - it('should call all ticks when download finished', function() { - download_part.update({ id: 'abcd', progressDetail: { current: 0, total : 100 } }); - h.expect(total_ticks).to.be.equal(0); - - download_part.update({ id: 'abcd', progressDetail: { current: 40, total : 100 } }); - h.expect(total_ticks).to.be.equal(2); - - download_part.setComplete(); - h.expect(total_ticks).to.be.equal(5); - }); - -}); diff --git a/spec/cli/smart_progress_bar_spec.js b/spec/cli/smart_progress_bar_spec.js deleted file mode 100644 index faf2a99c..00000000 --- a/spec/cli/smart_progress_bar_spec.js +++ /dev/null @@ -1,101 +0,0 @@ -import h from 'spec/spec_helper'; -import { _ } from 'azk'; -import { SmartProgressBar } from 'azk/cli/smart_progress_bar'; - -describe('SmartProgressBar progressbar', function() { - - var smartProgressBar, fake_progress_bar; - - beforeEach(function () { - fake_progress_bar = { total_ticks: 0 }; - fake_progress_bar.tick = function(number) { - this.total_ticks += number; - }; - - smartProgressBar = new SmartProgressBar(50, 10, fake_progress_bar); - }); - - it('should SmartProgressBar be instantiated', function() { - h.expect(smartProgressBar).to.not.be.undefined; - }); - - it('should have parts', function() { - h.expect(_.isArray(smartProgressBar._download_parts)).to.be.true; - }); - - it('should have bars count', function() { - h.expect(_.isNumber(smartProgressBar._bar_count)).to.be.true; - }); - - it('should have layers count', function() { - h.expect(_.isNumber(smartProgressBar._layers_count)).to.be.true; - }); - - it('should calculate bars per layers', function() { - h.expect(smartProgressBar._bars_per_layers).to.be.equal(5); - }); - - it('should calculate percentage tick', function() { - h.expect(smartProgressBar._percentage_tick).to.be.equal(0.2); - }); - - it('should add and get a part', function() { - var msg = { - id: 'abcd', - progressDetail: { - current: 200, - total : 2000 - } - }; - - smartProgressBar.receiveMessage(msg, 'download'); - - var part1 = smartProgressBar.getPart(msg); - h.expect(part1.id).to.be.equal('abcd'); - h.expect(part1.current_downloaded_size).to.be.equal(200); - h.expect(part1.total_downloaded_size).to.be.equal(2000); - }); - - it('should calculate percetage', function() { - var msg = { - id: 'abcd', - progressDetail: { - current: 200, - total : 2000 - } - }; - - smartProgressBar.receiveMessage(msg, 'download'); - - var download_part1 = smartProgressBar.getPart(msg); - h.expect(download_part1.getTotalPercentage()).to.be.equal(0.10); - }); - - it('should calculate percetage', function() { - var msg = { - id : 'abcd', - progressDetail: { - current: 200, - total : 2000 - } - }; - - smartProgressBar.receiveMessage(msg, 'download'); - - var download_part1 = smartProgressBar.getPart(msg); - h.expect(download_part1.getTotalPercentage()).to.be.equal(0.10); - }); - - it('should update downloaded_part when receives same message', function() { - var msg = { id: 'abcd', progressDetail: { current: 200, total : 2000 } }; - smartProgressBar.receiveMessage(msg, 'download'); - h.expect(smartProgressBar.getPart(msg).current_downloaded_size) - .to.be.equal(200); - - msg = { id: 'abcd', progressDetail: { current: 300, total : 2000 } }; - smartProgressBar.receiveMessage(msg, 'download'); - h.expect(smartProgressBar.getPart(msg).current_downloaded_size) - .to.be.equal(300); - }); - -}); diff --git a/spec/cmds/docker_spec.js b/spec/cmds/docker_spec.js index 5aa63b6a..32d04ab1 100644 --- a/spec/cmds/docker_spec.js +++ b/spec/cmds/docker_spec.js @@ -6,8 +6,7 @@ h.describeRequireVm('Azk cli, docker controller', function() { var ui = h.mockUI(beforeEach, outputs); var cli_options = {}; - var cli = new Cli(cli_options) - .route('docker'); + var cli = new Cli(cli_options).route('docker'); var doc_opts = { exit: false }; var run_options = { ui: ui, cwd: __dirname }; @@ -20,20 +19,7 @@ h.describeRequireVm('Azk cli, docker controller', function() { h.expect(options).to.have.property('docker', true); h.expect(options).to.have.property('__doubledash', true); h.expect(options['docker-args']).to.deep.eql(['version']); - h.expect(outputs[0]).to.match(RegExp(h.escapeRegExp("docker version"))); + h.expect(outputs[0]).to.match(RegExp(h.escapeRegExp("\"docker\" \"version\""))); }); }); - - it("should run a `docker -- images` command", function() { - doc_opts.argv = ['docker', '--', 'images']; - var options = cli.router.cleanParams(cli.docopt(doc_opts)); - return cli.run(doc_opts, run_options) - .then((code) => { - h.expect(code).to.equal(0); - h.expect(options).to.have.property('docker', true); - h.expect(options).to.have.property('__doubledash', true); - h.expect(options['docker-args']).to.deep.eql(['images']); - h.expect(outputs[0]).to.match(RegExp(h.escapeRegExp("docker images"))); - }); - }); }); diff --git a/spec/docker/pull_spec.js b/spec/docker/pull_spec.js index c1f19332..40a46717 100644 --- a/spec/docker/pull_spec.js +++ b/spec/docker/pull_spec.js @@ -36,7 +36,7 @@ describe("Azk docker module, image pull @slow", function() { }); it("should raise error to internal error", function() { - var result = h.docker.pull('http://127.0.0.1/invalid', 'not_exist'); + var result = h.docker.pull('scratch', 'not_exist'); return h.expect(result).to.be.rejectedWith(Error, /500/); }); }); diff --git a/spec/fixtures/sync/rsyncignore.txt b/spec/fixtures/sync/rsyncignore.txt index 97877579..9991cc44 100644 --- a/spec/fixtures/sync/rsyncignore.txt +++ b/spec/fixtures/sync/rsyncignore.txt @@ -1 +1 @@ -bar/ +/bar diff --git a/spec/fixtures/sync/test_1/.syncignore b/spec/fixtures/sync/test_1/.syncignore index a0044f55..0661686d 100644 --- a/spec/fixtures/sync/test_1/.syncignore +++ b/spec/fixtures/sync/test_1/.syncignore @@ -1 +1 @@ -foo/ +/foo diff --git a/spec/fixtures/sync/test_2/.syncignore b/spec/fixtures/sync/test_2/.syncignore new file mode 100644 index 00000000..a0044f55 --- /dev/null +++ b/spec/fixtures/sync/test_2/.syncignore @@ -0,0 +1 @@ +foo/ diff --git a/spec/fixtures/sync/test_2/b ar/Fred.txt b/spec/fixtures/sync/test_2/b ar/Fred.txt new file mode 100644 index 00000000..d7f3c876 --- /dev/null +++ b/spec/fixtures/sync/test_2/b ar/Fred.txt @@ -0,0 +1 @@ +I am Fred \ No newline at end of file diff --git a/spec/fixtures/sync/test_2/b ar/barney.txt b/spec/fixtures/sync/test_2/b ar/barney.txt new file mode 100644 index 00000000..4d4b6682 --- /dev/null +++ b/spec/fixtures/sync/test_2/b ar/barney.txt @@ -0,0 +1 @@ +I am Barney \ No newline at end of file diff --git a/spec/fixtures/sync/test_2/b ar/clothes/blue_fir.json b/spec/fixtures/sync/test_2/b ar/clothes/blue_fir.json new file mode 100644 index 00000000..face17f5 --- /dev/null +++ b/spec/fixtures/sync/test_2/b ar/clothes/blue_fir.json @@ -0,0 +1,4 @@ +{ + "color": "blue", + "spots": "black" +} \ No newline at end of file diff --git a/spec/fixtures/sync/test_2/b ar/clothes/red_fur.json b/spec/fixtures/sync/test_2/b ar/clothes/red_fur.json new file mode 100644 index 00000000..2057c00b --- /dev/null +++ b/spec/fixtures/sync/test_2/b ar/clothes/red_fur.json @@ -0,0 +1,4 @@ +{ + "color": "red", + "spots": "black" +} \ No newline at end of file diff --git a/spec/fixtures/sync/test_2/b ar/clothes/white_fir.json b/spec/fixtures/sync/test_2/b ar/clothes/white_fir.json new file mode 100644 index 00000000..d26700df --- /dev/null +++ b/spec/fixtures/sync/test_2/b ar/clothes/white_fir.json @@ -0,0 +1,4 @@ +{ + "color": "white", + "spots": "black" +} \ No newline at end of file diff --git a/spec/fixtures/sync/test_2/fo o/Barney.txt b/spec/fixtures/sync/test_2/fo o/Barney.txt new file mode 100644 index 00000000..792945c2 --- /dev/null +++ b/spec/fixtures/sync/test_2/fo o/Barney.txt @@ -0,0 +1 @@ +I am Barney Gumble \ No newline at end of file diff --git a/spec/fixtures/sync/test_2/fo o/Moe.txt b/spec/fixtures/sync/test_2/fo o/Moe.txt new file mode 100644 index 00000000..4552ae4f --- /dev/null +++ b/spec/fixtures/sync/test_2/fo o/Moe.txt @@ -0,0 +1 @@ +I am Moe-Sislak \ No newline at end of file diff --git a/spec/fixtures/sync/test_2/ig nored/not_sync.txt b/spec/fixtures/sync/test_2/ig nored/not_sync.txt new file mode 100644 index 00000000..e69de29b diff --git a/spec/fixtures/test-app/.env b/spec/fixtures/test-app/.env index 15afcc66..be71a262 100644 --- a/spec/fixtures/test-app/.env +++ b/spec/fixtures/test-app/.env @@ -1 +1,2 @@ FROM_DOT_ENV=azk is beautiful +BASE64=base64 hash== diff --git a/spec/fixtures/test-app/.syncignore b/spec/fixtures/test-app/.syncignore new file mode 100644 index 00000000..2771fe38 --- /dev/null +++ b/spec/fixtures/test-app/.syncignore @@ -0,0 +1 @@ +/ignore \ No newline at end of file diff --git a/spec/fixtures/test-app/ignore/file-to-be-ignored b/spec/fixtures/test-app/ignore/file-to-be-ignored new file mode 100644 index 00000000..e69de29b diff --git "a/spec/fixtures/test-app/special:'` \"\\/test file 1" "b/spec/fixtures/test-app/special:'` \"\\/test file 1" new file mode 100644 index 00000000..e69de29b diff --git "a/spec/fixtures/test-app/special:'` \"\\/test file 2" "b/spec/fixtures/test-app/special:'` \"\\/test file 2" new file mode 100644 index 00000000..e69de29b diff --git a/spec/generator/court_spec.js b/spec/generator/court_spec.js index 86ee178f..408049d2 100644 --- a/spec/generator/court_spec.js +++ b/spec/generator/court_spec.js @@ -125,7 +125,7 @@ describe('Azk generator tool court veredict:', function() { h.expect(filteredEvidences[0][0]).to.have.property('ruleName', 'node-0.12'); h.expect(filteredEvidences[1]).to.containSubset([{'ruleName': 'ruby_on_rails'}]); - h.expect(filteredEvidences[1]).to.containSubset([{'ruleName': 'postgres-9.4'}]); + h.expect(filteredEvidences[1]).to.containSubset([{'ruleName': 'postgres'}]); }); }); diff --git a/spec/generator/rules/generation/mysql_and_postgres_gen_spec.js b/spec/generator/rules/generation/mysql_and_postgres_gen_spec.js index 3ddce144..fc9a23bb 100644 --- a/spec/generator/rules/generation/mysql_and_postgres_gen_spec.js +++ b/spec/generator/rules/generation/mysql_and_postgres_gen_spec.js @@ -126,9 +126,14 @@ describe('Azk generator db', function() { h.expect(system).to.have.deep.property('options.shell', '/bin/bash'); h.expect(system).to.have.deep.property('options.wait', 150); h.expect(system).to.not.have.deep.property('options.workdir'); - h.expect(system).to.have.deep.property('options.mounts').and.to.eql( - { '/var/lib/postgresql/data': { type: 'persistent', value: 'postgresql', options: {} }, - '/var/log/postgresql': { type: 'path', value: './log/postgresql', options: {} } } ); + h.expect(system).to.have.deep.property('options.mounts').and.to.eql({ + '/var/lib/postgresql/data': { + type: 'persistent', value: 'postgres-data', options: {} + }, + '/var/log/postgresql': { + type: 'persistent', value: 'postgres-log', options: {} + } + }); }); }); diff --git a/spec/generator/rules/generation/mysql_gen_spec.js b/spec/generator/rules/generation/mysql_gen_spec.js index 79c26933..7df1232f 100644 --- a/spec/generator/rules/generation/mysql_gen_spec.js +++ b/spec/generator/rules/generation/mysql_gen_spec.js @@ -49,8 +49,9 @@ describe('Azk generator generation mysql rule', function() { h.expect(mysqlSystem).to.have.deep.property('depends').and.to.eql([]); h.expect(mysqlSystem).to.have.deep.property('options.envs.MYSQL_USER', 'azk'); - h.expect(mysqlSystem).to.have.deep.property('options.envs.MYSQL_PASSWORD', 'azk'); - h.expect(mysqlSystem).to.have.deep.property('options.envs.MYSQL_DATABASE', 'project_development'); + h.expect(mysqlSystem).to.have.deep.property('options.envs.MYSQL_PASS', 'azk'); + h.expect(mysqlSystem).to.have.deep.property('options.envs.MYSQL_ALLOW_EMPTY_PASSWORD', 'true'); + h.expect(mysqlSystem).to.have.deep.property('options.envs.MYSQL_DATABASE', 'azk'); h.expect(mysqlSystem).to.not.have.deep.property('options.provision'); h.expect(mysqlSystem).to.not.have.deep.property('options.command'); diff --git a/spec/spec_helper.js b/spec/spec_helper.js index 90537dbf..26a42caa 100644 --- a/spec/spec_helper.js +++ b/spec/spec_helper.js @@ -5,6 +5,10 @@ import { nfcall, promisify } from 'azk/utils/promises'; import { Client as AgentClient } from 'azk/agent/client'; import Utils from 'azk/utils'; +// Active to debug event loop +// https://github.com/nodejs/node/issues/1128 +// import activeHandles from 'active-handles'; + var lazy = lazy_require({ MemoryStream: 'memorystream', dirdiff : 'dirdiff', @@ -26,6 +30,10 @@ var Helpers = { return (_.contains(process.argv, '--no-required-agent') || process.env.AZK_NO_REQUIRED_AGENT); }, + get sinon() { + return require('sinon'); + }, + get docker() { return require('azk/docker').default; }, @@ -101,6 +109,7 @@ before(() => { after((done) => { process.nextTick(() => { require('azk/utils/postal').unsubscribeAll(); + // activeHandles.print(); done(); }); }); diff --git a/spec/spec_helpers/mock_manifest.js b/spec/spec_helpers/mock_manifest.js index 5976686e..9eea3657 100644 --- a/spec/spec_helpers/mock_manifest.js +++ b/spec/spec_helpers/mock_manifest.js @@ -74,6 +74,7 @@ export function extend(h) { var mounts_with_sync = { '/azk' : { type: 'sync', value: '.' }, + '/azk/special' : { type: 'sync', value: 'special:\'` "\\'}, '/azk/bin' : { type: 'sync', value: 'bin', shell: true }, '/azk/lib' : { type: 'sync', value: 'lib', shell: true, daemon: false }, '/azk/tmp' : { type: 'persistent', value: 'tmp' }, @@ -198,6 +199,7 @@ export function extend(h) { }, 'example-sync': { extends: "example", + command: "exit 0", mounts: _.cloneDeep(mounts_with_sync), }, 'example-http-domain': { diff --git a/spec/sync/index_spec.js b/spec/sync/index_spec.js index 782d45f8..e2f81a57 100644 --- a/spec/sync/index_spec.js +++ b/spec/sync/index_spec.js @@ -1,6 +1,7 @@ import h from 'spec/spec_helper'; -import { config, path, lazy_require } from 'azk'; +import { _, config, path, lazy_require } from 'azk'; import { async, all } from 'azk/utils/promises'; +import { mkdirp } from 'file-async'; var lazy = lazy_require({ Sync : ['azk/sync'], @@ -10,12 +11,12 @@ var lazy = lazy_require({ }); describe("Azk sync, main module", function() { - var example_fixtures = h.fixture_path('sync/test_1/'); var invalid_fixtures = path.join(h.fixture_path('sync/test_1/'), 'invalid'); - function make_copy() { + function make_copy(fixture_path = 'sync/test_1/') { + var fixtures = h.fixture_path(fixture_path); return all([ - h.copyToTmp(example_fixtures), + h.copyToTmp(fixtures), h.tmp_dir() ]); } @@ -30,40 +31,42 @@ describe("Azk sync, main module", function() { it("should sync two folders", function() { return async(function* () { var [origin, dest] = yield make_copy(); - var code = yield lazy.Sync.sync(origin, dest); - var result = yield h.diff(origin, dest); - h.expect(code).to.eql(0); - h.expect(result).to.have.property('deviation', 0); + var result = yield lazy.Sync.sync(origin, dest); + var diff = yield h.diff(origin, dest); + + h.expect(result).to.have.property('code', 0); + h.expect(diff).to.have.property('deviation', 0); }); }); it("should sync one file between two folders", function() { return async(function* () { - var include = "bar/clothes/barney.txt"; var [origin, dest] = yield make_copy(); - var code = yield lazy.Sync.sync(origin, dest, { include }); + var file = "bar/clothes/blue_fir.json"; + origin = path.join(origin, file); + dest = path.join(dest, file); + yield mkdirp(path.join(dest, "..")); + var result = yield lazy.Sync.sync(origin, dest); // Compare folders and files - var result_folder = yield h.diff(origin, dest); - var result_file = yield h.diff(path.join(origin, include), path.join(dest, include)); + var result_file = yield h.diff(origin, dest); - h.expect(code).to.eql(0); + h.expect(result).to.have.property('code', 0); h.expect(result_file).to.have.property('deviation', 0); - h.expect(result_folder).to.have.property('deviation', 10); }); }); it("should sync two folders but exclude a file list", function() { return async(function* () { - var except = "foo/"; + var except = "/foo/"; var [origin, dest] = yield make_copy(); - var code = yield lazy.Sync.sync(origin, dest, { except }); + var result = yield lazy.Sync.sync(origin, dest, { except }); // Compare folders and subfolders var result_folder = yield h.diff(origin, dest); var result_subfolder = yield h.diff(path.join(origin, "bar"), path.join(dest, "bar")); - h.expect(code).to.eql(0); + h.expect(result).to.have.property('code', 0); h.expect(result_folder).to.have.property('deviation', 3); h.expect(result_subfolder).to.have.property('deviation', 0); }); @@ -71,52 +74,91 @@ describe("Azk sync, main module", function() { it("should sync two folders but exclude paths from text file", function() { return async(function* () { - var except_from = h.fixture_path("sync/rsyncignore.txt"); + var except_from = h.fixture_path("sync/rsyncignore.txt"); var [origin, dest] = yield make_copy(); - var code = yield lazy.Sync.sync(origin, dest, { except_from }); + var result = yield lazy.Sync.sync(origin, dest, { except_from }); // Compare folders and subfolders var result_folder = yield h.diff(origin, dest); var result_subfolder = yield h.diff(path.join(origin, "foo"), path.join(dest, "foo")); - h.expect(code).to.eql(0); + h.expect(result).to.have.property('code', 0); h.expect(result_folder).to.have.property('deviation', 7); h.expect(result_subfolder).to.have.property('deviation', 0); }); }); + it("should sync two folders containing special character on path", function() { + return async(function* () { + var [origin, dest] = yield make_copy('test-app/special:\'` "\\'); + dest = path.join(dest, 'special:\'` "\\'); + + var result = yield lazy.Sync.sync(origin, dest); + var diff = yield h.diff(origin, dest); + + h.expect(result).to.have.property('code', 0); + h.expect(diff).to.have.property('deviation', 0); + }); + }); + it("should not sync an invalid folder", function() { return async(function* () { var origin = invalid_fixtures; var dest = yield h.tmp_dir(); var promise = lazy.Sync.sync(origin, dest); - return h.expect(promise).to.be.rejected.and.eventually.have.property('code', 23); + return h.expect(promise).to.be.rejected.and.eventually.have.property('code', 'ENOENT'); }); }); h.describeRequireVm("with enabled vm", function() { - it("should sync two folders", function() { - return async(function* () { - var name = config("agent:vm:name"); - var dest = path.join("/tmp", lazy.uuid.v4()); - var opts = { ssh: lazy.Client.ssh_opts() }; - - // Make destination folder - var vm_code = yield lazy.VM.ssh(name, "mkdir -p " + dest); - h.expect(vm_code).to.equal(0); - - // Sync folders - var code = yield lazy.Sync.sync(example_fixtures, dest, opts); - h.expect(code).to.eql(0); - - // Test destination folder in vm - var file = path.join(dest, "bar/Fred.txt"); - var folder = path.join(dest, "bar/clothes"); - var cmd = "test -f " + file + " && test -d " + folder; - vm_code = yield lazy.VM.ssh(name, cmd); - h.expect(vm_code).to.equal(0); - }); + let vm_name, options; + let fixture = 'test-app/special:\'` "\\'; + let example_fixtures = h.fixture_path(fixture); + + before(() => { + vm_name = config('agent:vm:name'); + options = { ssh: lazy.Client.ssh_opts(), except: ['test file 2'] }; + }); + + it("should sync relative path", function* () { + let dest = path.join('/tmp', lazy.uuid.v4()); + let relative_sufix = path.join(example_fixtures, '..', '..'); + let opts = _.merge({}, options, { relative_sufix }); + + // Sync folders + var result = yield lazy.Sync.sync(example_fixtures, dest, opts); + h.expect(result).to.have.property('code', 0); + + // Test destination folder in vm + dest = path.join(dest, fixture).replace(/([`"\\])/g, '\\$1'); + let file = path.join(dest, 'test file 1'); + let folder = path.join(dest, 'test file 2'); + let cmd = `test -f "${file}" && test ! -f "${folder}"`; + let vm_code = yield lazy.VM.ssh(vm_name, cmd); + + h.expect(vm_code).to.equal(0, 'files no synced to destination'); + }); + + it("should sync two folders", function* () { + var dest = path.join('/tmp', lazy.uuid.v4(), "a b'`\\\""); + + // Make destination folder + var vm_code = yield lazy.VM.ssh(vm_name, "mkdir -p " + path.join(dest, "..")); + h.expect(vm_code).to.equal(0); + + // Sync folders + var result = yield lazy.Sync.sync(example_fixtures, dest, options); + h.expect(result).to.have.property('code', 0); + + // Test destination folder in vm + dest = dest.replace(/([`"\\])/g, '\\$1'); + var file = path.join(dest, 'test file 1'); + var folder = path.join(dest, 'test file 2'); + var cmd = `test -f "${file}" && test ! -f "${folder}"`; + vm_code = yield lazy.VM.ssh(vm_name, cmd); + + h.expect(vm_code).to.equal(0, 'files no synced to destination'); }); }); }); diff --git a/spec/sync/watcher_spec.js b/spec/sync/watcher_spec.js index c8847132..e728fb94 100644 --- a/spec/sync/watcher_spec.js +++ b/spec/sync/watcher_spec.js @@ -68,7 +68,7 @@ describe("Azk sync, Watcher module", function() { var msg = yield wait_msg; h.expect(msg).to.have.deep.property('op', 'change'); - h.expect(msg).to.have.deep.property('filepath', file); + h.expect(msg).to.have.deep.property('filepath', origin_file); h.expect(msg).to.have.deep.property('status', 'done'); var content = yield fsAsync.readFile(dest_file); @@ -117,7 +117,7 @@ describe("Azk sync, Watcher module", function() { var msg_change = yield wait_msgs; h.expect(msg_change).to.have.property('op', 'change'); - h.expect(msg_change).to.have.property('filepath', file); + h.expect(msg_change).to.have.property('filepath', origin_file); h.expect(msg_change).to.have.property('status', 'done'); var content = yield fsAsync.readFile(dest_file); diff --git a/spec/sync/worker_spec.js b/spec/sync/worker_spec.js index 9186e82d..65dbee17 100644 --- a/spec/sync/worker_spec.js +++ b/spec/sync/worker_spec.js @@ -2,6 +2,8 @@ import h from 'spec/spec_helper'; import { _, path, lazy_require, fsAsync } from 'azk'; import { defer, async, all } from 'azk/utils/promises'; +require('source-map-support').install({}); + var lazy = lazy_require({ Sync : ['azk/sync'], Worker : ['azk/sync/worker'], @@ -12,7 +14,7 @@ var lazy = lazy_require({ describe("Azk sync, Worker module", function() { this.timeout(20000); - var worker; + var workers = []; var example_fixtures = h.fixture_path('sync/test_1/'); var invalid_fixtures = path.join(h.fixture_path('sync/test_1/'), 'invalid'); @@ -29,8 +31,9 @@ describe("Azk sync, Worker module", function() { return this.emit('sending', ...args); } } - var bus = new FakeProcess(); - worker = new lazy.Worker(bus); + let bus = new FakeProcess(); + let worker = new lazy.Worker(bus); + workers.push(worker); return [bus, worker]; } @@ -56,23 +59,25 @@ describe("Azk sync, Worker module", function() { }); } - afterEach(() => worker.unwatch()); + afterEach(() => { + _.each(workers, (worker) => worker.unwatch()); + }); describe("with a watch to sync a two folders", function() { var origin, dest, bus; - beforeEach(() => { - return async(function* () { - [origin, dest] = yield make_copy(); - [bus] = create_worker(); - yield fsAsync.remove(path.join(origin, ".syncignore")); + beforeEach(function* () { + [origin, dest] = yield make_copy(); + [bus] = create_worker(); - var msg = yield run_and_wait_msg(bus, "watch", () => { - return bus.emit("message", { origin, destination: dest }); - }); + // No test excludes yet + yield fsAsync.remove(path.join(origin, ".syncignore")); - h.expect(msg).to.have.property('op', 'watch'); - h.expect(msg).to.have.property('status', 'ready'); + var msg = yield run_and_wait_msg(bus, "watch", () => { + return bus.emit("message", { origin, destination: dest }); }); + + h.expect(msg).to.have.property('op', 'watch'); + h.expect(msg).to.have.property('status', 'ready'); }); it("should have done initial sync", function() { @@ -80,22 +85,20 @@ describe("Azk sync, Worker module", function() { return h.expect(result).to.eventually.have.property('deviation', 0); }); - it("should sync a added file", function() { - return async(function* () { - var file = "bar/foo.bar.txt"; - var origin_file = path.join(origin, file); + it("should sync a added file", function* () { + var file = "bar/foo.bar.txt"; + var origin_file = path.join(origin, file); - var msg = yield run_and_wait_msg(bus, () => { - return fsAsync.writeFile(origin_file, "foobar"); - }); + var msg = yield run_and_wait_msg(bus, () => { + return fsAsync.writeFile(origin_file, "foobar"); + }); - h.expect(msg).to.have.property('op', 'add'); - h.expect(msg).to.have.property('filepath', file); - h.expect(msg).to.have.property('status', 'done'); + h.expect(msg).to.have.property('op', 'add'); + h.expect(msg).to.have.property('filepath', origin_file); + h.expect(msg).to.have.property('status', 'done'); - var result = yield h.diff(origin, dest); - return h.expect(result).to.have.property('deviation', 0); - }); + var result = yield h.diff(origin, dest); + return h.expect(result).to.have.property('deviation', 0); }); it("should sync a changed file", function() { @@ -109,7 +112,7 @@ describe("Azk sync, Worker module", function() { }); h.expect(msg).to.have.property('op', 'change'); - h.expect(msg).to.have.property('filepath', file); + h.expect(msg).to.have.property('filepath', origin_file); h.expect(msg).to.have.property('status', 'done'); var content = yield fsAsync.readFile(dest_file); @@ -117,7 +120,7 @@ describe("Azk sync, Worker module", function() { }); }); - it("should sync a removed file", function() { + it("should sync a removed file, syncing your parent", function() { return async(function* () { var file = "foo/Moe.txt"; var origin_file = path.join(origin, file); @@ -127,7 +130,7 @@ describe("Azk sync, Worker module", function() { return fsAsync.remove(origin_file); }); h.expect(msg).to.have.property('op', 'unlink'); - h.expect(msg).to.have.property('filepath', file); + h.expect(msg).to.have.property('filepath', path.join(origin_file, '..')); h.expect(msg).to.have.property('status', 'done'); var exists = yield fsAsync.exists(dest_file); @@ -135,44 +138,40 @@ describe("Azk sync, Worker module", function() { }); }); - it("should sync a removed folder", function() { - return async(function* () { - var folder = "foo"; - var origin_folder = path.join(origin, folder); - var dest_folder = path.join(origin, folder); - - // Save all messages and call check via event emitter - var wait_msgs = defer((resolve) => { - var msgs = []; - var call = (msg) => { - msgs.push(JSON.parse(msg)); - if (msgs.length >= 3) { - bus.removeListener('sending', call); - resolve(msgs); - } - }; - bus.on('sending', call); - }); - - var msg = yield run_and_wait_msg(bus, 'unlinkDir', () => { - return fsAsync.remove(origin_folder); - }); - h.expect(msg).to.have.property('op', 'unlinkDir'); - h.expect(msg).to.have.property('filepath', folder); - h.expect(msg).to.have.property('status', 'done'); - - // Check unlink partials - var msgs = yield wait_msgs; - h.expect(msgs).to.include.something.that.deep.eql( - {"op": "unlink", "status":"done", "filepath":"foo/Barney.txt"} - ); - h.expect(msgs).to.include.something.that.deep.eql( - {"op": "unlink", "status":"done", "filepath":"foo/Moe.txt"} - ); + it("should sync a removed folder", function* () { + var folder = "foo"; + var origin_folder = path.join(origin, folder); + var dest_folder = path.join(origin, folder); + + // Save all messages and call check via event emitter + var wait_msgs = defer((resolve) => { + var msgs = []; + var call = (msg) => { + msgs.push(JSON.parse(msg)); + if (msgs.length >= 3) { + bus.removeListener('sending', call); + resolve(msgs); + } + }; + bus.on('sending', call); + }); - var exists = yield fsAsync.exists(dest_folder); - h.expect(exists).to.fail; + var msg = yield run_and_wait_msg(bus, 'unlinkDir', () => { + return fsAsync.remove(origin_folder); }); + h.expect(msg).to.have.property('op', 'unlinkDir'); + h.expect(msg).to.have.property('filepath', origin); + h.expect(msg).to.have.property('status', 'done'); + + // Check unlink partials + var msgs = yield wait_msgs; + h.expect(msgs).to.length(3); + h.expect(msgs).to.containSubset([ + {"op": "unlink", "status":"done", "filepath": origin} + ]); + + var exists = yield fsAsync.exists(dest_folder); + h.expect(exists).to.fail; }); }); @@ -181,12 +180,12 @@ describe("Azk sync, Worker module", function() { var [bus] = create_worker(); var opts = { except: ["foo/"] }; - var msg = yield run_and_wait_msg(bus, () => { + var msg = yield run_and_wait_msg(bus, 'watch', () => { return bus.emit("message", { origin, destination: dest, opts }); }); - h.expect(msg).to.have.property('op', 'sync'); - h.expect(msg).to.have.property('status', 'done'); + h.expect(msg).to.have.property('op', 'watch'); + h.expect(msg).to.have.property('status', 'ready'); var exists = yield fsAsync.exists(path.join(dest, "foo")); h.expect(exists).to.fail; @@ -197,10 +196,10 @@ describe("Azk sync, Worker module", function() { it("should not include content patterns files from except_from option", function* () { var [origin, dest] = yield make_copy(); - worker = create_worker()[1]; + let worker = create_worker()[1]; yield worker.watch(origin, dest, { except_from: h.fixture_path("sync/rsyncignore.txt"), - except: [ "ignored" ] + except: [ "/ignored" ] }); var wait = defer((resolve) => { @@ -221,10 +220,10 @@ describe("Azk sync, Worker module", function() { it("should exclude the .gitignore content for default", function* () { var [origin, dest] = yield make_copy(); - yield fsAsync.writeFile(path.join(origin, ".gitignore"), "ignored/"); + yield fsAsync.writeFile(path.join(origin, ".gitignore"), "/ignored"); yield fsAsync.remove(path.join(origin, ".syncignore")); - worker = create_worker()[1]; + let worker = create_worker()[1]; yield worker.watch(origin, dest, {}); var exists = yield fsAsync.exists(path.join(dest, "ignored")); @@ -247,7 +246,7 @@ describe("Azk sync, Worker module", function() { it("should exclude the .syncignore content for default in preference to .gitignore", function* () { var [origin, dest] = yield make_copy(); - worker = create_worker()[1]; + let worker = create_worker()[1]; yield worker.watch(origin, dest, {}); var exists = yield fsAsync.exists(path.join(dest, "ignored")); @@ -321,11 +320,11 @@ describe("Azk sync, Worker module", function() { h.expect(msg).to.have.property('status', 'fail'); if (lazy.semver.cmp(rsync_version, '>=', '3.1.0')) { - h.expect(msg).to.have.deep.property('err.code', 3); - h.expect(msg).to.have.deep.property('err.err').and.match(/Error: rsync.*3/); + h.expect(msg).to.have.deep.property('code', 3); + h.expect(msg).to.have.deep.property('message').and.match(/rsync.*3/); } else { - h.expect(msg).to.have.deep.property('err.code', 12); - h.expect(msg).to.have.deep.property('err.err').and.match(/Error: rsync.*12/); + h.expect(msg).to.have.deep.property('code', 12); + h.expect(msg).to.have.deep.property('message').and.match(/rsync.*12/); } }); }); @@ -342,8 +341,8 @@ describe("Azk sync, Worker module", function() { h.expect(msg).to.have.property('op', 'sync'); h.expect(msg).to.have.property('status', 'fail'); - h.expect(msg).to.have.deep.property('err.code', 101); - h.expect(msg).to.have.deep.property('err.err').and.match(/Sync: origin path not exist/); + h.expect(msg).to.have.deep.property('code', 101); + h.expect(msg).to.have.deep.property('err').and.match(/Sync: origin path not exist/); }); }); }); diff --git a/spec/system/index_spec.js b/spec/system/index_spec.js index 7f8d8aea..4e9ba74d 100644 --- a/spec/system/index_spec.js +++ b/spec/system/index_spec.js @@ -122,32 +122,28 @@ describe("Azk system class, main set", function() { } else { share_folder = manifest.cwd; } - var sync_folder = path.join( - config("paths:sync_folders"), - manifest.namespace, system.name, manifest.cwd - ); var persistent_folder = path.join( config("paths:persistent_folders"), manifest.namespace ); var mounts = system.mounts; - h.expect(mounts).to.have.property('/azk', path.join(sync_folder, '.')); - h.expect(mounts).to.have.property('/azk/bin', path.join(sync_folder, 'bin')); + h.expect(mounts).to.have.property('/azk' , system.sync_folder('/azk')); + h.expect(mounts).to.have.property('/azk/bin', system.sync_folder('/azk/bin')); h.expect(mounts).to.have.property('/azk/lib', path.join(share_folder, 'lib')); h.expect(mounts).to.have.property('/azk/tmp', path.join(persistent_folder, 'tmp')); h.expect(mounts).to.have.property('/azk/log', path.join(persistent_folder, 'log')); var shellVolumes = system.shellOptions().volumes; - h.expect(shellVolumes).to.have.property('/azk', path.join(share_folder, '.')); - h.expect(shellVolumes).to.have.property('/azk/bin', path.join(sync_folder, 'bin')); - h.expect(shellVolumes).to.have.property('/azk/lib', path.join(sync_folder, 'lib')); + h.expect(shellVolumes).to.have.property('/azk/bin', system.sync_folder('/azk/bin')); + h.expect(shellVolumes).to.have.property('/azk/lib', system.sync_folder('/azk/lib')); + h.expect(shellVolumes).to.have.property('/azk' , path.join(share_folder, '.')); h.expect(shellVolumes).to.have.property('/azk/tmp', path.join(persistent_folder, 'tmp')); h.expect(shellVolumes).to.have.property('/azk/log', path.join(persistent_folder, 'log')); var daemonVolumes = system.daemonOptions().volumes; - h.expect(daemonVolumes).to.have.property('/azk', path.join(sync_folder, '.')); - h.expect(daemonVolumes).to.have.property('/azk/bin', path.join(sync_folder, 'bin')); + h.expect(daemonVolumes).to.have.property('/azk' , system.sync_folder('/azk')); + h.expect(daemonVolumes).to.have.property('/azk/bin', system.sync_folder('/azk/bin')); h.expect(daemonVolumes).to.have.property('/azk/lib', path.join(share_folder, 'lib')); h.expect(daemonVolumes).to.have.property('/azk/tmp', path.join(persistent_folder, 'tmp')); h.expect(daemonVolumes).to.have.property('/azk/log', path.join(persistent_folder, 'log')); @@ -231,7 +227,10 @@ describe("Azk system class, main set", function() { h.expect(options).to.have.property("working_dir").and.eql(system.workdir); h.expect(options).to.have.property("dns").and.eql(net.nameServers()); h.expect(options).to.have.property("env").and.eql({ - HTTP_PORT: "5000", ECHO_DATA: "data", FROM_DOT_ENV: "azk is beautiful" + BASE64 : "base64 hash==", + HTTP_PORT : "5000", + ECHO_DATA : "data", + FROM_DOT_ENV: "azk is beautiful" }); }); @@ -431,10 +430,11 @@ describe("Azk system class, main set", function() { h.expect(options).to.have.property("volumes"); h.expect(options).to.have.deep.property("annotations.azk.seq", 2); h.expect(options).to.have.property("env").and.eql({ - ECHO_DATA : "data", - FROM_DOT_ENV : "azk is beautiful", - HTTP_PORT : "5000", - FOO : "BAR" + BASE64 : "base64 hash==", + ECHO_DATA : "data", + FROM_DOT_ENV: "azk is beautiful", + HTTP_PORT : "5000", + FOO : "BAR" }); h.expect(mounts).to.have.property( @@ -488,7 +488,9 @@ describe("Azk system class, main set", function() { h.expect(options).to.have.property("working_dir").and.eql(system.workdir); h.expect(options).to.have.property("dns").and.eql(net.nameServers()); h.expect(options).to.have.property("env").and.eql({ - ECHO_DATA: "data", FROM_DOT_ENV: "azk is beautiful" + BASE64 : "base64 hash==", + ECHO_DATA : "data", + FROM_DOT_ENV: "azk is beautiful", }); }); diff --git a/spec/system/run_spec.js b/spec/system/run_spec.js index 32267d86..c5ea8a5e 100644 --- a/spec/system/run_spec.js +++ b/spec/system/run_spec.js @@ -1,9 +1,15 @@ import h from 'spec/spec_helper'; -import { _ } from 'azk'; -import { publish, subscribe } from 'azk/utils/postal'; -import { async, defer, promiseResolve, promiseReject } from 'azk/utils/promises'; +import { _, path, config, lazy_require } from 'azk'; +import { publish } from 'azk/utils/postal'; +import { async, defer, promiseResolve } from 'azk/utils/promises'; import { ImageNotAvailable } from 'azk/utils/errors'; +var lazy = lazy_require({ + VM : ['azk/agent/vm'], + spawnAsync : ['azk/utils/spawn_helper'], + Client : ['azk/agent/client'], +}); + describe("Azk system class, run set", function() { var manifest, system; @@ -28,11 +34,11 @@ describe("Azk system class, run set", function() { it("should run a command in a shell for a system", function() { return async(function* () { var exitResult = yield system.runShell({ - command: ["ls -ls; exit"], + command: ["ls -ls bin; exit"], stdout: mocks.stdout, stderr: mocks.stderr }); h.expect(exitResult).to.have.property("code", 0); - h.expect(outputs).to.have.property("stdout").match(/.*src/); + h.expect(outputs).to.have.property("stdout").match(/test\-app/); }); }); @@ -154,6 +160,7 @@ describe("Azk system class, run set", function() { it("load from .env file", function() { h.expect(envs).to.include.something.that.match(/FROM_DOT_ENV=azk is beautiful/); + h.expect(envs).to.include.something.that.match(/BASE64=base64 hash==/); }); it("should expand envs in properties", function*() { @@ -189,96 +196,94 @@ describe("Azk system class, run set", function() { }); describe("check image before run", function() { - // TODO: Replace this merge for a mock class - var system, image_mock = { - pull() { - return defer((resolve) => { - process.nextTick(() => { - publish("spec.image_mock.status", { type: "event" }); - resolve(this); - }); - }); - }, - - check() { - return promiseResolve(null); - }, - - inspect() { - return promiseReject({}); - } - }; - - var events; - var _subscription; - var _subscription2; + let system, subscription; + let stubs = []; - before(() => { + beforeEach(() => { system = manifest.system("empty"); - system.image = _.merge({}, system.image, image_mock); - - _subscription = subscribe('spec.image_mock.status', (event) => { - events.push(event); - }); - - }); - after(() => { - _subscription.unsubscribe(); }); - beforeEach(() => { - events = []; + afterEach(() => { + _.each(stubs, (stub) => stub.restore()); + stubs = []; + if (!_.isEmpty(subscription)) { + subscription.unsubscribe(); + subscription = null; + } }); it("should raise error if image not found", function() { + stubs.push(h.sinon.stub(system.image, 'pull').returns(promiseResolve(false))); + stubs.push(h.sinon.stub(system.image, 'check').returns(promiseResolve(null))); + var result = system.runShell({ command: [], image_pull: false}); + return h.expect(result).to.rejectedWith(ImageNotAvailable); + }); - // mock check to return null - system.image.check = function () { + it("should add system to event object", function* () { + // stub to generate a event + var stub = h.sinon.stub(system.image, 'check', () => { return defer((resolve) => { process.nextTick(() => { publish("image.check.status", { type: "action", context: "image", action: "check_image" }); - resolve(null); + resolve(system.image); }); }); - }; + }); + stubs.push(stub); - var result = system.runShell({ command: [], image_pull: false}); - return h.expect(result).to.rejectedWith(ImageNotAvailable); + var wait_msg = null; + var topic = 'system.run.image.check.status'; + [wait_msg, subscription] = yield h.wait_subscription(topic); + + yield system.runDaemon().catch(() => {}); + + let msg = (yield wait_msg)[0]; + + h.expect(msg).to.eql({ + type: 'action', + context: 'image', + action: 'check_image', + system: system + }); }); + }); + }); - it("should add system to event object", function() { - return async(function* () { + h.describeRequireVm("with enabled vm", function () { + this.timeout(20000); - // force azk to think that the image is builded - system.image.builded = true; + var system, name; - // mock check to return null - system.image.check = function () { - return defer((resolve) => { - process.nextTick(() => { - publish("image.check.status", { type: "action", context: "image", action: "check_image" }); - resolve(system.image); - }); - }); - }; + beforeEach(() => { + name = config("agent:vm:name"); + system = manifest.system('example-sync'); + system.provisioned = new Date(); + }); - _subscription2 = subscribe('system.run.image.check.status', (event) => { - events.push(event); - }); + afterEach(function* () { + yield system.stopWatching(); + yield lazy.Client.closeWs(); + }); - yield system.runDaemon() - .catch(() => {}); + it("run watch and sync files", function* () { + yield system.runWatch(true); - _subscription2.unsubscribe(); + var cmd, result, dest; - h.expect(events).to.have.deep.property("[0]").and.eql( - { type: 'action', - context: 'image', - action: 'check_image', - system: system } - ); - }); - }); + dest = system.sync_folder('/azk/bin'); + cmd = "test -f " + path.join(dest, 'test-app'); + result = yield lazy.VM.ssh(name, cmd); + h.expect(result).to.eq(0); + + dest = system.sync_folder('/azk'); + cmd = "test -f " + path.join(dest, 'src', 'bashttpd'); + result = yield lazy.VM.ssh(name, cmd); + h.expect(result).to.eq(0); + + dest = system.sync_folder('/azk'); + cmd = "test -d " + path.join(dest, 'lib'); + result = yield lazy.VM.ssh(name, cmd); + h.expect(result).to.eq(1); }); }); }); diff --git a/spec/utils/shell_spec.js b/spec/utils/shell_spec.js new file mode 100644 index 00000000..1e79f0c3 --- /dev/null +++ b/spec/utils/shell_spec.js @@ -0,0 +1,72 @@ +import h from 'spec/spec_helper'; +import { lazy_require } from 'azk'; + +var lazy = lazy_require({ + replaceEnvs : ['azk/utils/shell'], +}); + +describe("Azk utils/shell module", function() { + describe("call replaceEnvs", function() { + it("should expand env in a string", function() { + var result; + + result = lazy.replaceEnvs("${PATH}"); + h.expect(result).to.equal("PATH"); + + result = lazy.replaceEnvs("${PATH} AZK_ID"); + h.expect(result).to.equal("PATH AZK_ID"); + + result = lazy.replaceEnvs("${PATH} $AZK_ID"); + h.expect(result).to.equal("PATH AZK_ID"); + + result = lazy.replaceEnvs('foo \\\\${BAR}'); + h.expect(result).to.equal('foo \\\\BAR'); + }); + + it("should support replace_for", function() { + var result = lazy.replaceEnvs("foo: ${BAR}", "${env.$1}"); + h.expect(result).to.equal("foo: ${env.BAR}"); + }); + + it("should not replace if not have a var", function() { + var result = lazy.replaceEnvs("foo bar"); + h.expect(result).to.equal("foo bar"); + }); + + it("should ignore special", function() { + var result; + + result = lazy.replaceEnvs("${1} $AZK_ID"); + h.expect(result).to.equal("${1} AZK_ID"); + + result = lazy.replaceEnvs("${@} $AZK_ID"); + h.expect(result).to.equal("${@} AZK_ID"); + + result = lazy.replaceEnvs("$? $AZK_ID"); + h.expect(result).to.equal("$? AZK_ID"); + }); + + it("should support escaped vars with slash", function() { + var result; + + result = lazy.replaceEnvs('foo \\${BAR}'); + h.expect(result).to.equal('foo ${BAR}'); + + result = lazy.replaceEnvs('foo \\ ${BAR}'); + h.expect(result).to.equal('foo \\ BAR'); + + result = lazy.replaceEnvs('foo \\\\\\${BAR}'); + h.expect(result).to.equal('foo \\\\${BAR}'); + + result = lazy.replaceEnvs('foo \\$BAR ${FOO}'); + h.expect(result).to.equal('foo $BAR FOO'); + }); + + it("should support escaped in json", function() { + var result; + + result = lazy.replaceEnvs(JSON.stringify('foo \\${BAR}'), "$1", true); + h.expect(JSON.parse(result)).to.equal('foo ${BAR}'); + }); + }); +}); diff --git a/src/agent/api/ws.js b/src/agent/api/ws.js index d2225d3f..0f37559a 100644 --- a/src/agent/api/ws.js +++ b/src/agent/api/ws.js @@ -53,7 +53,7 @@ var Controller = { return module.cache.rsync_watcher; }, - watch: function(ws, data, req_id) { + watch(ws, data, req_id) { var host_folder = data.host_folder; var guest_folder = data.guest_folder; var opts = data.opts; @@ -69,22 +69,37 @@ var Controller = { }); }, - unwatch: function(ws, data, req_id) { + unwatch(ws, data, req_id) { var host_folder = data.host_folder; var guest_folder = data.guest_folder; - var result = this.rsync_watcher.unwatch(host_folder, guest_folder) ? 'done' : 'fail'; - this._send(ws, req_id, { status: result }); + try { + var result = this.rsync_watcher.unwatch(host_folder, guest_folder) ? 'done' : 'fail'; + this._send(ws, req_id, { status: result }); + } catch (err) { + this._logError(err); + this._send(ws, req_id, { status: 'fail' }); + } }, - watchers: function(ws, data, req_id) { + watchers(ws, data, req_id) { var result = this.rsync_watcher.wathcers(); this._send(ws, req_id, result); }, - _send: function(ws, id, data) { + _send(ws, id, data) { if (!data.id) { data.id = id; } - ws.send(JSON.stringify(data)); + try { + ws.send(JSON.stringify(data)); + } catch (err) { + this._logError(err); + } + }, + + _logError(err) { + let keys = ["message", "arguments", "type", "name", ...Object.keys(err)]; + let error = JSON.stringify(err, keys); + log.error("send websocket error %s", error, {}); } }; diff --git a/src/agent/balancer.js b/src/agent/balancer.js index 947b8b8f..b1454af1 100644 --- a/src/agent/balancer.js +++ b/src/agent/balancer.js @@ -337,9 +337,10 @@ var Balancer = { user: process.getuid(), server: { accessLog: log, - workers: 3, - maxSockets: 100, - deadBackendTTL: 30 + workers: config('agent:balancer:workers'), + maxSockets: config('agent:balancer:worker_max_sockets'), + tcpTimeout: config('agent:balancer:tcp_timeout'), + deadBackendTTL: config('agent:balancer:dead_backend_ttl'), }, http: { port, bind }, driver: ["memcached://" + memcached_socket] diff --git a/src/agent/client.js b/src/agent/client.js index 93bbec02..4531b96b 100644 --- a/src/agent/client.js +++ b/src/agent/client.js @@ -60,7 +60,7 @@ var WebSocketClient = { this._cb[message.id] = item.callback; this._ws.send(JSON.stringify(message)); } - resolve(); + return resolve(); }); this._ws.on('close', () => { @@ -85,15 +85,29 @@ var WebSocketClient = { }, close() { - this._ws.on('error', (err) => { - log.error('Error closing websocket:', err); - }); + return defer((resolve, reject) => { + let status = this._status; + if (status !== 'closed' && status !== 'closing') { + this._ws.removeAllListeners('error'); + this._ws.removeAllListeners('close'); - if (this._status !== 'closed') { - this._ws.close(); - } - this._status = 'closed'; - return true; + this._ws.on('close', () => { + this._status = 'closed'; + log.debug('Websocket closed.'); + resolve(true); + }); + + this._ws.on('error', (err) => { + log.error('Error closing websocket:', err); + reject(err); + }); + + this._status = 'closing'; + this._ws.terminate(); + } else { + resolve(false); + } + }); }, send(message, callback = null, retry = 0) { @@ -124,7 +138,7 @@ var WebSocketClient = { }, _generate_msg_id() { - return lazy.uuid.v1().split('-')[0]; + return lazy.uuid.v4().split('-')[4]; } }; @@ -229,7 +243,7 @@ var Client = { }; }, - close_ws() { + closeWs() { return WebSocketClient.close(); }, diff --git a/src/agent/configure.js b/src/agent/configure.js index e3322662..a54fde16 100644 --- a/src/agent/configure.js +++ b/src/agent/configure.js @@ -1,13 +1,12 @@ import { _, t, os, log, lazy_require, fsAsync } from 'azk'; import { publish } from 'azk/utils/postal'; -import { async, promisify, nfcall, thenAll, promiseResolve } from 'azk/utils/promises'; +import { async, promisify, thenAll, promiseResolve } from 'azk/utils/promises'; import { config, set_config } from 'azk'; import { UIProxy } from 'azk/cli/ui'; import { OSNotSupported, DependencyError } from 'azk/utils/errors'; -import { net, envDefaultArray } from 'azk/utils'; +import { net, envDefaultArray, which } from 'azk/utils'; import Azk from 'azk'; -import which from 'which'; // Search for command in path import semver from 'semver'; import { isIPv4 } from 'net'; @@ -270,7 +269,7 @@ export class Configure extends UIProxy { } _which(command, save_key = null) { - return nfcall(which, command) + return which(command) .then((fullpath) => { if (save_key) { var obj = {}; diff --git a/src/cli/cli_controller.js b/src/cli/cli_controller.js index ed11d3f1..e62c9987 100644 --- a/src/cli/cli_controller.js +++ b/src/cli/cli_controller.js @@ -6,6 +6,7 @@ export class CliController extends RouterController { constructor(...args) { super(...args); this._verbose_level = 0; + this._views = {}; } _configure(opts) { @@ -63,4 +64,12 @@ export class CliController extends RouterController { var [, result] = cli_run(cmd, this.cwd, this.ui); return result; } + + view(name) { + if (_.isEmpty(this._views[name])) { + let View = require(`./views/${name}_view`); + this._views[name] = new View(this.ui, this); + } + return this._views[name]; + } } diff --git a/src/cli/download_part.js b/src/cli/download_part.js deleted file mode 100644 index d39bd482..00000000 --- a/src/cli/download_part.js +++ /dev/null @@ -1,55 +0,0 @@ -import { log } from 'azk'; - -export class DownloadPart { - constructor(msg, representing_bars, tick_calback) { - // set progress bar link - this._tick_calback = tick_calback; - - // get info from docker remote message - this.id = msg.id; - this.total_downloaded_size = msg.progressDetail.total; - - // associate with progress bar - this._representing_bars = representing_bars; - this._part_size = this._calculate_part_size(); - this._last_tick_current = 0; - - // get current size from message - this.update(msg); - - log.debug('\n>>---------\n DownloadPart:', this, '\n>>---------\n'); - } - - getTotalPercentage() { - return this.current_downloaded_size / this.total_downloaded_size; - } - - update(msg) { - var current_after_last_tick, ticks_to_be_called; - - // get current size from progressDetail docker remote message - this.current_downloaded_size = msg.progressDetail.current; - - // current chunk - current_after_last_tick = this.current_downloaded_size - this._last_tick_current; - - // calculate percentual bar tick - ticks_to_be_called = current_after_last_tick / this._part_size; - this._tick_calback(ticks_to_be_called); - - // save _last_tick_current to get chunk on next round - this._last_tick_current = this.current_downloaded_size; - } - - setComplete() { - this.update({ - progressDetail: { - current: this.total_downloaded_size - } - }); - } - - _calculate_part_size() { - return this.total_downloaded_size / this._representing_bars; - } -} diff --git a/src/cli/helpers.js b/src/cli/helpers.js index b648d786..74664164 100644 --- a/src/cli/helpers.js +++ b/src/cli/helpers.js @@ -1,5 +1,4 @@ import { _, log, lazy_require, config, t } from 'azk'; -import { SmartProgressBar } from 'azk/cli/smart_progress_bar'; import { ManifestError } from 'azk/utils/errors'; var lazy = lazy_require({ @@ -131,68 +130,6 @@ var Helpers = { }; }, - newPullProgressBar(cmd) { - return (msg) => { - if (msg.type !== "pull_msg") { - return msg; - } - - // pull end - if (msg.end) { - cmd.ok('commands.helpers.pull.pull_ended', msg); - return false; - } - - // manual message, not parsed - if (msg.traslation) { - cmd.ok(msg.traslation, msg.data); - return false; - } - - if (!_.isNumber(this.non_existent_locally_ids_count)) { - this.non_existent_locally_ids_count = msg.registry_result.non_existent_locally_ids_count; - } - - // parse messages by type - var status = msg.statusParsed; - switch (status.type) { - case 'download_complete': - this.smartProgressBar && this.smartProgressBar.receiveMessage(msg, status.type); - break; - - case 'download': - if (_.isUndefined(this.bar)) { - // show message: ⇲ pulling 5/14 layers. - cmd.ok('commands.helpers.pull.pull_start', { - left_to_download_count : msg.registry_result.non_existent_locally_ids_count, - total_registry_layers : msg.registry_result.registry_layers_ids_count, - }); - - // create a new progress-bar - this.bar = cmd.createProgressBar(' [:bar] :percent :layers_left/:layers_total ', { - complete: '=', - incomplete: ' ', - width: 50, - total: 50 - }); - - // control progress-bar with SmartProgressBar - this.smartProgressBar = new SmartProgressBar( - 50, - this.non_existent_locally_ids_count, - this.bar); - } - this.smartProgressBar.receiveMessage(msg, status.type); - break; - - case 'pulling_another': - cmd.ok('commands.helpers.pull.already_being', msg); - break; - } - return false; - }; - }, - escapeCapture(callback) { // Escape sequence var escapeBuffer = false; diff --git a/src/cli/index.js b/src/cli/index.js index 3844d325..ee661a4a 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -66,13 +66,17 @@ export function cli(args, cwd, ui = UI) { if (isPromise(result)) { result - .then((code) => { + .then((code_or_err) => { // Convert a not number in a UnknownError - if (!_.isNumber(code)) { - log.warn('[cli] command not return error or code, return: %j', code); - throw new UnknownError(code); + if (!_.isNumber(code_or_err)) { + log.warn('[cli] command not return error or code, return: %j', code_or_err); + // If is undefined, skip throwing the exception + if (_.isUndefined(code_or_err)) { + return 0; + } + throw new UnknownError(code_or_err); } - return code; + return code_or_err; }) .catch((error) => { return error_handler(error, { ui }); diff --git a/src/cli/smart_progress_bar.js b/src/cli/smart_progress_bar.js deleted file mode 100644 index 5f625c50..00000000 --- a/src/cli/smart_progress_bar.js +++ /dev/null @@ -1,78 +0,0 @@ -import { _, log } from 'azk'; -import { DownloadPart } from 'azk/cli/download_part'; -// var ProgressBar = require('progress'); - -export class SmartProgressBar { - - /** - * manages a progress bar without knowing total size previously - * @param {number} bar_count total bars in progress bar - * @param {number} layers_count total layers left do download - * @param {object} progress_bar the progress bar - */ - constructor(bar_count, layers_count, progress_bar) { - this._progress_bar = progress_bar; - - this._download_parts = []; - this._bar_count = bar_count; - this._layers_count = layers_count; - this._bars_per_layers = this._calculate_bars_per_layers(); - this._percentage_tick = this._calculate_percentage_tick(); - this._download_finished_count = 0; - - log.debug('\n>>---------\n SmartProgressBar:', this, '\n>>---------\n'); - } - - _calculate_bars_per_layers() { - return this._bar_count / this._layers_count; - } - - _calculate_percentage_tick() { - return 1.0 / this._bars_per_layers; - } - - receiveMessage(msg, msg_type) { - var current_download_part = this.getPart(msg); - - if (_.isUndefined(current_download_part)) { - current_download_part = new DownloadPart(msg, - this._bars_per_layers, - this._internal_tick.bind(this)); - - this._download_parts.push(current_download_part); - } - - if (msg_type === 'download_complete') { - // prevent maximum layers downloaded overflow - if (this._download_finished_count < this._layers_count) { - this._download_finished_count++; - } - // set layer to 100% and tick - current_download_part.setComplete(); - } else if (msg_type === 'download') { - // update and tick - current_download_part.update(msg); - } - - } - - _internal_tick(tick_value) { - - // prevent progress-bar overflow - if (this._progress_bar.curr + tick_value > this._bar_count) { - return; - } - - this._progress_bar.tick(tick_value, { - layers_left : this._download_finished_count, - layers_total: this._layers_count, - }); - } - - getPart(msg) { - return _.find(this._download_parts, function(part) { - return part.id === msg.id; - }); - } - -} diff --git a/src/cli/ui.js b/src/cli/ui.js index 91699405..eeb1c5c1 100644 --- a/src/cli/ui.js +++ b/src/cli/ui.js @@ -9,9 +9,12 @@ var lazy = lazy_require({ execShLib: 'exec-sh', open : 'open', colors : ['azk/utils/colors'], + Multiprogress: 'multi-progress', }); -var tables = {}; +var multiprogress = null; +var bars = {}; +var tables = {}; let colors_labels = { ok : 'green', @@ -97,11 +100,6 @@ var UI = { }, 500); }, - createProgressBar(...args) { - var ProgressBar = require('progress'); - return new ProgressBar(...args); - }, - stdout() { return process.stdout; }, @@ -145,6 +143,39 @@ var UI = { this.output(tables[name].toString()); }, + newProgress(format, options, output) { + output = output || this.stdout(); + this.terminateProgress(); + multiprogress = { + multi : new lazy.Multiprogress(output), + format: `${label_color('ok')}: ${format}`, + options + }; + }, + + terminateProgress() { + if (!_.isEmpty(multiprogress)) { multiprogress.multi.terminate(); } + multiprogress = null; + bars = {}; + }, + + newBar(name, format = null, options = null) { + format = format || multiprogress.format; + options = options || multiprogress.options; + bars[name] = multiprogress.multi.newBar(format, options); + return bars[name]; + }, + + tickBar(name, len, tokens = {}, total = null) { + let bar = bars[name] || this.newBar(name); + if (total) { + bar.total = total; + bar.curr = len; + len = 0; + } + bar.tick(len, tokens); + }, + // User interactions methods execSh(command, options = {}, callback = null) { if (_.isFunction(options)) { diff --git a/src/cli/views/ask_send_error_view.js b/src/cli/views/ask_send_error_view.js index c73d27d9..edc2b24a 100644 --- a/src/cli/views/ask_send_error_view.js +++ b/src/cli/views/ask_send_error_view.js @@ -1,5 +1,5 @@ import { _, t, isBlank, lazy_require } from 'azk'; -import { View } from './view'; +import View from './view'; import { async, promiseResolve } from 'azk/utils/promises'; var lazy = lazy_require({ @@ -109,3 +109,5 @@ export class AskSendErrorView extends View { }); } } + +export default AskSendErrorView; diff --git a/src/cli/views/image_pull_view.js b/src/cli/views/image_pull_view.js new file mode 100644 index 00000000..04693c7a --- /dev/null +++ b/src/cli/views/image_pull_view.js @@ -0,0 +1,49 @@ +import { t } from 'azk'; +import View from './view'; + +const TRANSLATION_KEY = 'commands.views.image_pull'; + +export default class ImagePullView extends View { + render(msg) { + if (msg.type !== "pull_msg") { + return msg; + } + + if (msg.end) { + this.ok(`${TRANSLATION_KEY}.pull_ended`, msg); + return false; + } + + // parse messages by type + let status = msg.statusParsed; + + try { + switch (status.type) { + case 'pulling_from': + this.newProgress(':id: :msg [:bar] :progress', { + complete: '=', + incomplete: ' ', + width: 30, + total: 100, + }); + break; + case 'pulling_fs_layer': + case 'download': + case 'pulling_extracting': + let progress = msg.progressDetail; + let tokens = { + id: t(`${TRANSLATION_KEY}.${status.type}`, msg), + msg: status.msg, + progress: progress.label || '', + }; + this.tickBar(msg.id, progress.current, tokens, progress.total); + break; + case 'pulling_digest': + this.ok(`${TRANSLATION_KEY}.digest`, status); + break; + } + } catch (e) { + // Ignore progress errors; + } + } +} diff --git a/src/cli/views/version_view.js b/src/cli/views/version_view.js index 2a4fcc72..a248f112 100644 --- a/src/cli/views/version_view.js +++ b/src/cli/views/version_view.js @@ -1,7 +1,7 @@ import { _ } from 'azk'; -import { View } from './view'; +import View from './view'; -export class VersionView extends View { +export default class VersionView extends View { render(data, with_logo = false) { let formated = this.format(data); this.output(with_logo ? this.add_logo(formated) : formated.join("\n")); diff --git a/src/cli/views/view.js b/src/cli/views/view.js index 474b1d6f..e6a27c5f 100644 --- a/src/cli/views/view.js +++ b/src/cli/views/view.js @@ -1,4 +1,10 @@ import { UIProxy } from '../ui'; export class View extends UIProxy { + constructor(ui, controller) { + super(ui); + this.controller = controller; + } } + +export default View; diff --git a/src/cmds/docker.js b/src/cmds/docker.js index 3e114f01..f80ef58a 100644 --- a/src/cmds/docker.js +++ b/src/cmds/docker.js @@ -6,37 +6,37 @@ import { async } from 'azk/utils/promises'; export default class Docker extends CliTrackerController { index(opts) { return async(this, function* () { - var cmd; - var args = _.map(opts['docker-args'], (arg) => { - return arg.replace(/'/g, "'\"'\"'"); - }); - if (!config('agent:requires_vm')) { - args = _.map(args, (arg) => arg.match(/^.* .*$/) ? `"${arg}"` : arg); - cmd = `/bin/sh -c 'docker ${args.join(" ")}'`; - log.debug("docker direct options: %s", cmd); + var args = _.map(opts['docker-args'], (arg) => arg.replace(/(["\\`])/g, "\\$1")); + let cmd = `/bin/sh -c 'docker "${args.join('" "')}"'`; + log.debug("docker direct options: %s", cmd, {}); return this.ui.execSh(cmd); } else { // Require agent is started yield Helpers.requireAgent(this.ui); + // cmd + let cmd = ["vm", "ssh"]; + // If is interactive mode force ssh tty - var ssh_args = []; if (this.ui.isInteractive()) { - ssh_args.push("-t"); + cmd.push("-t"); } + // init command + cmd.push('--'); + // Move to current folder var point = config('agent:vm:mount_point'); - ssh_args.push(`cd ${utils.docker.resolvePath(this.cwd || '', point)};`); + cmd.push("cd"); + cmd.push(utils.docker.resolvePath(this.cwd || '', point)); + cmd.push(" ;"); // Adding escape arguments and mount docker command - args = _.map(args, (arg) => arg.match(/^.* .*$/) ? `\\"${arg}\\"` : arg); - ssh_args.push(`docker ${args.join(" ")}`); - - cmd = ["vm", "ssh", "--", ...ssh_args]; - log.debug("docker vm options: %j", cmd); + cmd.push("docker"); + cmd = [...cmd, ...opts['docker-args']]; + log.debug("docker vm options: %j", cmd, {}); return this.runShellInternally(cmd); } }); diff --git a/src/cmds/scale.js b/src/cmds/scale.js index 37f96e7c..67fc1cab 100644 --- a/src/cmds/scale.js +++ b/src/cmds/scale.js @@ -48,6 +48,7 @@ export default class Scale extends CliTrackerController { var system = systems[i]; yield this._scale(system, parseInt(opts.to || 1), opts); } + return 0; }); } @@ -71,8 +72,7 @@ export default class Scale extends CliTrackerController { if (!event) { return; } var type; if (event.type === "pull_msg") { - var pullProgressBar = Helpers.newPullProgressBar(this.ui); - pullProgressBar(event); + this.view('image_pull').render(event); } else { var keys = ["commands", "scale"]; switch (event.type) { diff --git a/src/cmds/shell.js b/src/cmds/shell.js index 5a146fe5..fe46e152 100644 --- a/src/cmds/shell.js +++ b/src/cmds/shell.js @@ -15,7 +15,7 @@ export default class Shell extends CliTrackerController { var args = this.normalized_params.arguments; var _subscription = subscribe('docker.pull.status', (data) => { - Helpers.newPullProgressBar(this.ui)(data); + this.view('image_pull').render(data); }); return asyncUnsubscribe(this, _subscription, function* () { @@ -132,7 +132,6 @@ export default class Shell extends CliTrackerController { _escapeAndPullProgress(escape, system, show_logs, verbose) { return (event) => { - var pullProgressBar = Helpers.newPullProgressBar(this.ui); var escapeCapture = Helpers.escapeCapture(escape); // show verbose output @@ -144,7 +143,7 @@ export default class Shell extends CliTrackerController { escapeCapture(event); } else if (show_logs) { if (show_logs && event.type === "pull_msg") { - pullProgressBar(event); + this.view('image_pull').render(event); } else if (event.type === "action") { var keys = ["commands", "scale"]; var actions = ["pull_image", "build_image"]; diff --git a/src/cmds/version.js b/src/cmds/version.js index 5da693d3..08faceea 100644 --- a/src/cmds/version.js +++ b/src/cmds/version.js @@ -3,7 +3,6 @@ import { Helpers } from 'azk/cli/helpers'; import { config, lazy_require } from 'azk'; import { async } from 'azk/utils/promises'; import { deviceInfo } from 'azk/utils'; -import { VersionView } from 'azk/cli/views/version_view'; import Azk from 'azk'; var lazy = lazy_require({ @@ -63,12 +62,12 @@ export default class Version extends CliTrackerController { }; if (require_vm && agent.agent) { - var ip = config('agent:vm:ip'); - data.use_vm = data.use_vm + ', ip: ' + this.ui.c.yellow(ip); + let ip = config('agent:vm:ip'); + data.use_vm = `${data.use_vm}, IP: ${ip}`; } // Show doctor info - (new VersionView(this.ui)).render(data, opts.logo); + this.view('version').render(data, opts.logo); return 0; }); diff --git a/src/cmds/vm.js b/src/cmds/vm.js index e73037ac..5ac7e365 100644 --- a/src/cmds/vm.js +++ b/src/cmds/vm.js @@ -56,13 +56,30 @@ export default class VM extends CliTrackerController { this.require_running(vm_info); yield Helpers.requireAgent(this.ui); - var ssh_url = `${config('agent:vm:user')}@${config('agent:vm:ip')}`; - var ssh_opts = "StrictHostKeyChecking=no -o LogLevel=quiet -o UserKnownHostsFile=/dev/null"; - var args = (params['ssh-args'] || []).join(`" "`); - var script = `ssh -i ${config('agent:vm:ssh_key')} -o ${ssh_opts} ${ssh_url} "${args}"`; + var cmd = ["ssh"]; + cmd.push('-i', config('agent:vm:ssh_key')); + cmd.push('-o', "StrictHostKeyChecking=no"); + cmd.push('-o', "LogLevel=quiet"); + cmd.push('-o', "UserKnownHostsFile=/dev/null"); + cmd.push(`${config('agent:vm:user')}@${config('agent:vm:ip')}`); + + if (params.tty && this.ui.isInteractive()) { + cmd.push('-t'); + } else if (params['no-tty']) { + cmd.push('-T'); + } - log.debug("vm ssh command:", script); - return this.ui.execSh(script); + var args = _.map((params['ssh-args'] || []), (arg) => { + return arg + .replace(/(\\)/g, '\\\\\\') + .replace(/([\s'"\\])/g, '\\$1') + .replace(/([`])/g, "\\\\\\$1"); + }); + cmd.push("--", `"${args.join('" "')}"`); + + cmd = cmd.join(' '); + log.debug("vm ssh command:", cmd); + return this.ui.execSh(cmd); }); } diff --git a/src/config.js b/src/config.js index 34b12e4e..2f5e4e7b 100644 --- a/src/config.js +++ b/src/config.js @@ -130,6 +130,10 @@ var options = mergeConfig({ ip : new Dynamic("agent:balancer:ip"), host: envs('AZK_BALANCER_HOST'), port: envs('AZK_BALANCER_PORT', 80), + workers: envs('AZK_BALANCER_WORKERS', 3), + worker_max_sockets: envs('AZK_BALANCER_WORKER_MAX_SOCKETS', 100), + tcp_timeout: envs('AZK_BALANCER_TCP_TIMEOUT', 63), // weird enough default that it can be greped easily + dead_backend_ttl: envs('AZK_BALANCER_DEAD_BACKEND_TTL', 30), file_dns: "/etc/resolver/" + envs('AZK_BALANCER_HOST'), }, dns: { @@ -226,7 +230,7 @@ var options = mergeConfig({ namespace : 'azk.test', repository : 'azk_test', build_name : 'azkbuildtest', - image_empty: 'cevich/empty_base_image:latest', + image_empty: 'nerwysh/empty:latest', }, tracker: { disable: true, diff --git a/src/docker/docker.js b/src/docker/docker.js index 814b8a3e..8f12d8da 100644 --- a/src/docker/docker.js +++ b/src/docker/docker.js @@ -175,9 +175,10 @@ export class Docker extends promisifyClass('dockerode') { azkListContainers(...args) { return this.listContainers(...args).then((containers) => { return _.reduce(containers, (result, container) => { - if (container.Names && container.Names[0].match(this.c_regex)) { - container.Name = container.Names[0]; - container.Annotations = Container.unserializeAnnotations(container.Name); + let name = _.find(container.Names, (name) => name.match(this.c_regex)); + if (!_.isEmpty(name)) { + container.Name = name; + container.Annotations = Container.unserializeAnnotations(name); container.State = Container.parseStatus(container.Status); container.NetworkSettings = { Access: Container.parsePorts(container.Ports) }; result.push(container); diff --git a/src/docker/pull.js b/src/docker/pull.js index e2bb6288..920de4f6 100644 --- a/src/docker/pull.js +++ b/src/docker/pull.js @@ -4,7 +4,8 @@ import { defer } from 'azk/utils/promises'; import { ProvisionNotFound, ProvisionPullError } from 'azk/utils/errors'; var lazy = lazy_require({ - XRegExp: ['xregexp', 'XRegExp'] + XRegExp: ['xregexp', 'XRegExp'], + JStream : 'jstream', }); var msg_regex = { @@ -14,6 +15,7 @@ var msg_regex = { pulling_complete : 'Pull complete', pulling_digest : 'Digest: (?.*)', pulling_repository : 'Pulling repository (?.*)', + pulling_from : 'Pulling from (?.*)', pulling_layers : 'Pulling dependent layers', pulling_metadata : 'Pulling metadata', pulling_fs_layer : 'Pulling fs layer', @@ -48,7 +50,16 @@ function parse_status(msg) { return result; } -export function pull(docker, repository, tag, stdout, registry_result) { +function publish_status(data) { + publish("docker.pull.status", data); +} + +function parse_progress_label(progress) { + let match = progress.match(/\]\s*(.*)$/); + return match[1]; +} + +export function pull(docker, repository, tag) { var image = `${repository}:${tag}`; var promise = docker.createImage({ fromImage: repository, @@ -56,27 +67,25 @@ export function pull(docker, repository, tag, stdout, registry_result) { }); return promise.then((stream) => { return defer((resolve, reject) => { - stream.on('data', (data) => { - try { - var msg = JSON.parse(data.toString()); - msg.type = "pull_msg"; - msg.registry_result = registry_result; - - if (msg.error) { - if (msg.error.match(/404/) || msg.error.match(/not found$/)) { - return reject(new ProvisionNotFound(image)); - } - reject(new ProvisionPullError(image, msg.error)); - } else { - // parse message - msg.statusParsed = parse_status(msg.status); - publish("docker.pull.status", msg); + stream.pipe(new lazy.JStream()).on('data', (msg) => { + msg.type = "pull_msg"; + if (msg.error) { + if (msg.error.match(/404/) || msg.error.match(/not found$/)) { + return reject(new ProvisionNotFound(image)); + } + reject(new ProvisionPullError(image, msg.error)); + } else { + // parse message + msg.statusParsed = parse_status(msg.status); + if (!_.isEmpty(msg.progress)) { + msg.progressDetail.label = parse_progress_label(msg.progress); } - } catch (e) {} + publish_status(msg); + } }); stream.on('end', () => { - publish("docker.pull.status", { type: "pull_msg", statusParsed: {}, end: true, image}); + publish_status({ type: "pull_msg", statusParsed: {}, end: true, image}); resolve(docker.findImage(image)); }); }); diff --git a/src/generator/court.js b/src/generator/court.js index e2c5636a..cd10894b 100644 --- a/src/generator/court.js +++ b/src/generator/court.js @@ -58,7 +58,7 @@ export class Court extends UIProxy { constructor(rules_folder, ui) { super(ui); - this.sugestionChooser = new SugestionChooser(path.join(__dirname, "suggestions"), this); + this.sugestionChooser = new SugestionChooser(this, path.join(__dirname, "suggestions")); this.__rules = { runtime : [], @@ -226,7 +226,7 @@ export class Court extends UIProxy { } _getEvidencesByFolder() { - return _.groupBy(this.__evidences, function(evidence) { + return _.groupBy(this.__evidences, (evidence) => { return path.dirname(evidence.fullpath); }); } @@ -237,14 +237,12 @@ export class Court extends UIProxy { _analysis() { this._replacesEvidences(); - this.__folder_evidences_suggestion = []; - _.forEach(this.__evidences_by_folder, function(value, key) { - var folders_evidence_suggestion = this.sugestionChooser.suggest(value); - this.__folder_evidences_suggestion.push({ + this.__folder_evidences_suggestion = _.map(this.__evidences_by_folder, (value, key) => { + return { path: key, - suggestions: folders_evidence_suggestion - }); - }, this); + suggestions: this.sugestionChooser.suggest(value) + }; + }); } _veredict() { @@ -292,11 +290,13 @@ export class Court extends UIProxy { }, this); this.__systems_suggestions = this.__convertFoldersToSystems(this.__folder_evidences_suggestion); + // console.log("this.__systems_suggestions", this.__systems_suggestions); } // convert __folder_evidences_suggestion to 'systems data' to mustache templates __convertFoldersToSystems() { - var systems = {}; + let systems = {}; + let not_version = []; _.forEach(this.__folder_evidences_suggestion, function(folder_evidence_suggestion) { var folderName = folder_evidence_suggestion.path; @@ -350,20 +350,16 @@ export class Court extends UIProxy { // create a new system var systemSuggestion = systems[suggestion.name] = suggestion; - if (!evidence_suggestion.version) { - this.ok('generator.foundWithoutVersion', { - __type: evidence_suggestion.name, - dir: folderName, - systemName: suggestion.name, - image: JSON.stringify(systemSuggestion.image) - }); - } else { - this.ok('generator.found', { - __type: evidence_suggestion.name, - dir: folderName, - systemName: suggestion.name, - image: JSON.stringify(systemSuggestion.image) - }); + var data_info = { + __type: evidence_suggestion.name, + dir: folderName, + systemName: suggestion.name, + image: JSON.stringify(systemSuggestion.image) + }; + + this.ok('generator.found', data_info); + if (!evidence_suggestion.version && evidence_suggestion.ruleType !== "database") { + not_version.push(evidence_suggestion.name); } // replace `#{app.dir}` by system template path @@ -373,6 +369,12 @@ export class Court extends UIProxy { }, this); }, this); + if (not_version.length > 0) { + this.info('generator.foundWithoutVersion', { + types: not_version.join(',') + }); + } + return systems; } diff --git a/src/generator/rules.js b/src/generator/rules.js index b92af9f1..4c1a0f83 100644 --- a/src/generator/rules.js +++ b/src/generator/rules.js @@ -69,7 +69,7 @@ export class BaseRule extends UIProxy { var version = this.getVersion(content); if (this.type === 'database') { - if (!this.checkDatabase(content)) { return null; } + if (!this.checkDatabase(content, path)) { return null; } } var ruleFilter = framework && !_.isEmpty(this.version_rules) ? framework : version; diff --git a/src/generator/rules/mysql.js b/src/generator/rules/mysql.js index f3d55ccf..d7246cbe 100644 --- a/src/generator/rules/mysql.js +++ b/src/generator/rules/mysql.js @@ -1,11 +1,12 @@ import { BaseRule } from 'azk/generator/rules'; +import { path } from 'azk'; export class Rule extends BaseRule { constructor(ui) { super(ui); this.type = "database"; this.name = "mysql"; - this.rule_name = "mysql-5.6"; + this.rule_name = "mysql"; // Suggest a docker image // http://images.azk.io/#/mysql @@ -14,12 +15,18 @@ export class Rule extends BaseRule { } relevantsFiles() { - return ['Gemfile']; + return ['Gemfile', 'wp-login.php']; } - checkDatabase(content) { - // TODO: check and return gem version (maybe usefull) - var regex = /^\s*gem ['"]mysql2?['"]/gm; - return regex.test(content); + checkDatabase(content, full_path) { + var filename = path.basename(full_path); + switch (filename) { + case "wp-login.php": + return true; + default: + // TODO: check and return gem version (maybe usefull) + var regex = /^\s*gem ['"]mysql2?['"]/gm; + return regex.test(content); + } } } diff --git a/src/generator/rules/postgres.js b/src/generator/rules/postgres.js index 3ea7b47f..3a2433ee 100644 --- a/src/generator/rules/postgres.js +++ b/src/generator/rules/postgres.js @@ -5,7 +5,7 @@ export class Rule extends BaseRule { super(ui); this.type = "database"; this.name = "postgres"; - this.rule_name = "postgres-9.4"; + this.rule_name = "postgres"; // Suggest a docker image // http://images.azk.io/#/mysql diff --git a/src/generator/rules/wordpress.js b/src/generator/rules/wordpress.js new file mode 100644 index 00000000..cbe302f6 --- /dev/null +++ b/src/generator/rules/wordpress.js @@ -0,0 +1,10 @@ +import { Rule as BaseRule } from 'azk/generator/rules/php'; + +export class Rule extends BaseRule { + constructor(ui) { + super(ui); + this.name = "wordpress"; + this.rule_name = this.name; + this.replaces = ['php']; + } +} diff --git a/src/generator/sugestion_chooser.js b/src/generator/sugestion_chooser.js index a9b97a35..b4f1fb13 100644 --- a/src/generator/sugestion_chooser.js +++ b/src/generator/sugestion_chooser.js @@ -5,31 +5,32 @@ var glob = require('glob'); var path = require('path'); export class SugestionChooser extends UIProxy { - constructor(sugestions_folder, ui) { + constructor(ui, sugestions_folder) { super(ui); - this.__sugestions = []; + this.__suggestions = []; this.load(sugestions_folder); } get suggestions() { - return this.__sugestions; + return this.__suggestions; } load(dir) { - _.each(glob.sync(path.join(dir, '**/*.js')), (file) => { - var Suggestion = require(file).Suggestion; - if (Suggestion) { - var suggestion = new Suggestion(this); - this.__sugestions.push(suggestion); - } + let suggestions = glob.sync(path.join(dir, '**/*.js')); + this.__suggestions = _.map(suggestions, (file) => { + return new (require(file).Suggestion)(); }); } suggest(evidences) { return _.map(evidences, (evidence) => { var suggestionChoosen = _.find(this.suggestions, (suggestion) => { - var diff = _.difference([ evidence.ruleName ], suggestion.ruleNamesList); - return diff.length === 0; + if (suggestion.analytics) { + return suggestion.examine(evidence, evidences); + } else { + var list = suggestion.ruleNamesList || []; + return list.indexOf(evidence.ruleName) > -1; + } }); if (suggestionChoosen) { diff --git a/src/generator/suggestions/index.js b/src/generator/suggestions/index.js index 6647d663..c661b469 100644 --- a/src/generator/suggestions/index.js +++ b/src/generator/suggestions/index.js @@ -1,3 +1,4 @@ +import { _ } from 'azk'; import { UIProxy } from 'azk/cli/ui'; export class Suggestion extends UIProxy { @@ -5,7 +6,7 @@ export class Suggestion extends UIProxy { super(...args); // Initial Azkfile.js suggestion - this.suggestion = { + this.__suggestion = { __type : 'example', name : 'example', depends : [], @@ -29,6 +30,14 @@ export class Suggestion extends UIProxy { }; } + get suggestion() { + return this.__suggestion; + } + + set suggestion(value) { + this.__suggestion = value; + } + extend(...args) { return require('azk/utils')._.extend({}, ...args); } @@ -36,4 +45,9 @@ export class Suggestion extends UIProxy { suggest() { return this.suggestion; } + + hasEvidence(evidences, criteria) { + var evidence = _.find(evidences, criteria); + return !_.isEmpty(evidence); + } } diff --git a/src/generator/suggestions/mysql-5.6.js b/src/generator/suggestions/mysql-5.6.js deleted file mode 100644 index 11fb76b8..00000000 --- a/src/generator/suggestions/mysql-5.6.js +++ /dev/null @@ -1,46 +0,0 @@ -import { Suggestion as DefaultSuggestion } from 'azk/generator/suggestions'; - -export class Suggestion extends DefaultSuggestion { - constructor(...args) { - super(...args); - - var name = 'mysql'; - var version = '5.6'; - // Readable name for this suggestion - this.name = `${name}-${version}`; - - // Which rules they suggestion is valid - this.ruleNamesList = [`${name}-${version}`]; - - // Initial Azkfile.js suggestion - this.suggestion = this.extend({}, this.suggestion, { - __type: `${name} ${version}`, - image : { docker: `azukiapp/${name}:${version}` }, - ports:{ - data: "3306/tcp", - }, - balancer: false, - http: false, - command: null, - workdir: null, - mounts: { - '/var/lib/mysql': {type: 'persistent', value: '#{manifest.dir}/mysql'}, - }, - wait: 150, - envs: { - // set instances variables - MYSQL_USER : "azk", - MYSQL_PASSWORD: "azk", - MYSQL_DATABASE: "#{manifest.dir}_development", - }, - export_envs_comment: [ - 'check this gist to configure your database', - 'https://gist.github.com/gullitmiranda/62082f2e47c364ef9617' - ], - export_envs: { - DATABASE_URL: "mysql2://#{envs.MYSQL_USER}:#{envs.MYSQL_PASSWORD}@#{net.host}" + - ":#{net.port.data}/#{envs.MYSQL_DATABASE}", - }, - }); - } -} diff --git a/src/generator/suggestions/mysql.js b/src/generator/suggestions/mysql.js new file mode 100644 index 00000000..65769ffe --- /dev/null +++ b/src/generator/suggestions/mysql.js @@ -0,0 +1,40 @@ +import { Suggestion as DefaultSuggestion } from 'azk/generator/suggestions/postgres'; + +export class Suggestion extends DefaultSuggestion { + constructor(...args) { + super(...args); + this.analytics = true; + this.name = 'mysql'; + } + + get suggestion() { + // Initial Azkfile.js suggestion + let suggestion = this.extend({}, super.suggestion, { + __type: `${this.name} ${this.version}`, + ports:{ + data: "3306/tcp", + }, + envs: { + MYSQL_USER: "azk", + MYSQL_PASS: "azk", + MYSQL_DATABASE: "azk", + MYSQL_ALLOW_EMPTY_PASSWORD: true, + }, + mounts: { + '/var/lib/mysql': {type: 'persistent', value: '#{manifest.dir}/mysql'}, + }, + }); + + return suggestion; + } + + examine(evidence, evidences) { + let is_db = super.examine(evidence, evidences); + if (is_db) { + this.protocol = 'mysql2'; + this.version = "5.6"; + this.image = "azukiapp/mysql:" + this.version; + } + return is_db; + } +} diff --git a/src/generator/suggestions/postgres-9.4.js b/src/generator/suggestions/postgres-9.4.js deleted file mode 100644 index 6cea886c..00000000 --- a/src/generator/suggestions/postgres-9.4.js +++ /dev/null @@ -1,48 +0,0 @@ -import { _ } from 'azk'; -import { UIProxy } from 'azk/cli/ui'; -import { example_system } from 'azk/generator/rules'; - -export class Suggestion extends UIProxy { - constructor(...args) { - super(...args); - - // Readable name for this suggestion - this.name = 'postgres'; - - // Which rules they suggestion is valid - this.ruleNamesList = ['postgres-9.4']; - - // Initial Azkfile.js suggestion - this.suggestion = _.extend({}, example_system, { - __type : 'postgres', - image : { docker: 'azukiapp/postgres:9.4' }, - ports:{ - data: "5432/tcp", - }, - balancer: false, - http: false, - command: null, - workdir: null, - mounts : { - '/var/lib/postgresql/data' : {type: 'persistent', value: 'postgresql'}, - '/var/log/postgresql' : {type: 'path', value: './log/postgresql'}, - }, - wait: 150, - envs: { - // set instances variables - // Move this to .env file - POSTGRES_USER: "azk", - POSTGRES_PASS: "azk", - POSTGRES_DB : "postgres_development", - }, - export_envs_comment: [ - 'check this gist to configure your database', - 'https://gist.github.com/gullitmiranda/62082f2e47c364ef9617' - ], - export_envs: { - DATABASE_URL: "postgres://#{envs.POSTGRES_USER}:#{envs.POSTGRES_PASS}" + - "@#{net.host}:#{net.port.data}/#{envs.POSTGRES_DB}", - }, - }); - } -} diff --git a/src/generator/suggestions/postgres.js b/src/generator/suggestions/postgres.js new file mode 100644 index 00000000..fb98f4cf --- /dev/null +++ b/src/generator/suggestions/postgres.js @@ -0,0 +1,68 @@ +import { _ } from 'azk'; +import { example_system } from 'azk/generator/rules'; +import { Suggestion as DefaultSuggestion } from 'azk/generator/suggestions'; + +export class Suggestion extends DefaultSuggestion { + constructor(...args) { + super(...args); + // Readable name for this suggestion + this.name = 'postgres'; + this.analytics = true; + } + + get suggestion() { + let name = this.name; + let upper_name = name.toUpperCase(); + let suggestion = _.extend({}, example_system, { + __type : 'postgres', + image : { docker: `${this.image}` }, + ports:{ + data: "5432/tcp", + }, + balancer: false, + http: false, + command: null, + workdir: null, + mounts : { + [`/var/lib/postgresql/data`]: {type: 'persistent', value: `${name}-data`}, + [`/var/log/postgresql`] : {type: 'persistent', value: `${name}-log`}, + }, + wait: 150, + envs: { + // set instances variables + // Move this to .env file + [`${upper_name}_USER`]: "azk", + [`${upper_name}_PASS`]: "azk", + [`${upper_name}_DB` ]: "azk", + }, + export_envs: { + DATABASE_URL: `${this.protocol || name}://#{envs.${upper_name}_USER}:#{envs.${upper_name}_PASS}` + + `@#{net.host}:#{net.port.data}/#{envs.${upper_name}_DATABASE}`, + }, + }); + + if (this.rails) { + suggestion.export_envs_comment = [ + 'check this gist to configure your database', + 'https://gist.github.com/gullitmiranda/62082f2e47c364ef9617' + ]; + } + + return suggestion; + } + + examine(evidence, evidences) { + let is_db = evidence.ruleType === "database" && + evidence.name === this.name; + + if (is_db) { + this.image = "azukiapp/postgres:9.4"; + // Is rails app? + this.rails = this.hasEvidence(evidences, { + ruleType: "framework", name: "ruby_on_rails" + }); + } + + return is_db; + } +} diff --git a/src/generator/suggestions/wordpress.js b/src/generator/suggestions/wordpress.js new file mode 100644 index 00000000..94042f40 --- /dev/null +++ b/src/generator/suggestions/wordpress.js @@ -0,0 +1,33 @@ +import { Suggestion as DefaultSuggestion } from 'azk/generator/suggestions'; + +export class Suggestion extends DefaultSuggestion { + constructor(...args) { + super(...args); + + var name = 'wordpress'; + // Readable name for this suggestion + this.name = `${name}`; + + // Which rules they suggestion is valid + this.ruleNamesList = [`${name}`]; + + // Initial Azkfile.js suggestion + this.suggestion = this.extend(this.suggestion, { + __type : `${name}`, + image : { docker: `azukiapp/php-fpm` }, + http : true, + scalable : { default: 1 }, + command : null, + mounts : { + "/azk/#{app.dir}": {type: 'path', value: '.'}, + }, + ports: { + http: "80/tcp", + }, + envs : { + // set instances variables + APP_DIR: '/azk/#{app.dir}', + } + }); + } +} diff --git a/src/images/index.js b/src/images/index.js index e574c736..b0e06091 100644 --- a/src/images/index.js +++ b/src/images/index.js @@ -1,18 +1,17 @@ import { _, fs, t, path, isBlank, lazy_require } from 'azk'; import { publish } from 'azk/utils/postal'; -import { async, defer } from 'azk/utils/promises'; -import { ManifestError, NoInternetConnection, LostInternetConnection } from 'azk/utils/errors'; +import { async } from 'azk/utils/promises'; +import { ManifestError, NoInternetConnection } from 'azk/utils/errors'; import { net } from 'azk/utils'; import Utils from 'azk/utils'; import { default as tracker } from 'azk/utils/tracker'; -var AVAILABLE_PROVIDERS = ["docker", "dockerfile"]; -var default_tag = "latest"; +const AVAILABLE_PROVIDERS = ["docker", "dockerfile"]; +const DEFAULT_TAG = "latest"; var lazy = lazy_require({ DImage : ['azk/docker', 'Image'], docker : ['azk/docker', 'default'], - Syncronizer: ['docker-registry-downloader'], }); export class Image { @@ -35,7 +34,7 @@ export class Image { this.path = options.path; } this.repository = options.repository; - this.tag = this.tag || options.tag || default_tag; + this.tag = this.tag || options.tag || DEFAULT_TAG; } if (_.isEmpty(this.name)) { @@ -43,130 +42,65 @@ export class Image { } } - check() { - return defer(() => { - publish("image.check.status", { type: "action", context: "image", action: "check_image" }); - return lazy.docker.findImage(this.name); - }); + check(notify = true) { + if (notify) { + publish("image.check.status", { + type: "action", context: "image", action: "check_image" + }); + } + return lazy.docker.findImage(this.name); } - pull(options, stdout) { - return async(this, function* () { - // split docker namespace and docker repository - var namespace = ''; - var repository = ''; - var splited = this.repository.split('\/'); - if (splited.length === 2) { - namespace = splited[0]; - repository = splited[1]; - } else { - namespace = 'library'; - repository = this.repository; - } - - // check if exists local image - this.repository = namespace + '/' + repository; - var image = yield this.check(); - - // check official docker image without "library/" namespace - if (isBlank(image) && namespace === 'library') { - this.repository = repository; - image = yield this.check(); - } - - // download from registry - if (isBlank(image) || options.build_force) { - this.repository = namespace + '/' + repository; - publish("image.pull.status", { type: "action", context: "image", action: "pull_image", data: this }); - - var registry_result; - var output; - - var currentOnline = yield net.isOnlineCheck(); - if ( !currentOnline ) { - throw new NoInternetConnection(); - } - - // get size and layers count - try { - registry_result = yield this.getDownloadInfo( - lazy.docker.modem, - namespace, - repository, - this.tag); - - output = _.isObject(stdout) && stdout; - // docker pull - image = yield lazy.docker.pull(this.repository, this.tag, output, registry_result); - } catch (err) { - output = (err || '').toString(); - throw new LostInternetConnection(' ' + output); - } + pullOrBuild(options) { + return (this.provider === "dockerfile") ? this.build(options) : this.pull(options); + } - yield this._track('pull'); - } - return this.check(); - }); + checkOrGet(options, force = false) { + if (force) { + return this.pullOrBuild(options); + } else { + return this.check().then((image) => { + return (isBlank(image)) ? this.checkOrGet(options, true) : image; + }); + } } - getDownloadInfo(dockerode_modem, namespace, repository, repo_tag) { + pull() { return async(this, function* () { + publish("image.pull.status", { + type: "action", context: "image", action: "pull_image", data: this + }); - var docker_socket = { dockerode_modem: dockerode_modem }; - var request_options = { - timeout: 10000, - maxAttempts: 3, - retryDelay: 500 - }; - - var syncronizer = new lazy.Syncronizer(docker_socket, request_options); - yield syncronizer.initialize(); - var tag = repo_tag; - - // get token from Docker Hub - var registry_infos; - var hubResult; - var getLayersDiff_result; - - hubResult = yield syncronizer.dockerHub.images(namespace, repository); - - // Get layers diff - getLayersDiff_result = yield syncronizer.getLayersDiff(hubResult, tag); - - // Check what layer we do not have locally - var registry_layers_ids = getLayersDiff_result.registry_layers_ids; - var non_existent_locally_ids = getLayersDiff_result.non_existent_locally_ids; - - registry_infos = { - registry_layers_ids_count : registry_layers_ids.length, - non_existent_locally_ids_count : non_existent_locally_ids.length - }; + // Check is online before try pull + var currentOnline = yield net.isOnlineCheck(); + if ( !currentOnline ) { + throw new NoInternetConnection(); + } - publish("image.getDownloadInfo.status", { - type : "pull_msg", - traslation : "commands.helpers.pull.pull_getLayersDiff", - data : registry_infos - }); + yield this._track('pull'); + yield lazy.docker.pull(this.repository, this.tag); - return registry_infos; + return this.check(false); }); } build(options) { return async(this, function* () { - var image = yield this.check(); - if (options.build_force || isBlank(image)) { - publish("image.build.status", - { type: 'action', context: 'image', action: 'build_image', data: this }); - image = yield lazy.docker.build({ - dockerfile: this.path, - tag: this.name, - verbose: options.provision_verbose, - stdout: options.stdout - }); - } + publish("image.build.status", { + type: 'action', context: 'image', action: 'build_image', data: this + }); + + let build_opts = { + dockerfile: this.path, + tag: this.name, + verbose: options.provision_verbose, + stdout: options.stdout + }; + + yield lazy.docker.build(build_opts); yield this._track('build'); - return image; + + return this.check(false); }); } @@ -222,7 +156,7 @@ export class Image { var imageParsed = lazy.DImage.parseRepositoryTag(value); this.repository = imageParsed.repository; - this.tag = imageParsed.tag || default_tag; + this.tag = imageParsed.tag || DEFAULT_TAG; } get name() { @@ -267,9 +201,7 @@ export class Image { return provider; } - // - // Tracker - // + // Tracker pull and build _track(event_type_name) { return tracker.sendEvent("image", (trackerEvent) => { // get event_type diff --git a/src/libexec/package-tools/mac/generate.sh b/src/libexec/package-tools/mac/generate.sh index ef758a11..d8d979af 100755 --- a/src/libexec/package-tools/mac/generate.sh +++ b/src/libexec/package-tools/mac/generate.sh @@ -48,15 +48,36 @@ class ${CLASS_NAME} < Formula url "http://${MAC_BUCKET_URL}/mac/azk_${VERSION}.tar.gz" version "${VERSION}" sha256 "${SHA256}" + head "https://github.com/azukiapp/azk.git" ${CONFLICTS} depends_on :macos => :mountain_lion depends_on :arch => :x86_64 def install - prefix.install Dir['*'] - prefix.install Dir['.nvmrc'] - prefix.install Dir['.dependencies'] - prefix.install Dir['.package-envs'] + items_path = '.' + if build.head? + ENV.deparallelize + ENV['HOMEBREW_TEMP'] = buildpath + system 'make', '-e', 'package_mac' + + items_path = 'package/brew/*/usr/lib/azk' + items = %w{ bin lib node_modules shared package.json npm-shrinkwrap.json CHANGELOG.md LICENSE README.md } + else + items = ['*'] + end + items += %w{ .dependencies .nvmrc .package-envs } + + items.each do |item| + prefix.install Dir["#{items_path}/#{item}"] + end + end + + def post_install + rmtree './package' if build.head? + end + + test do + system "azk", "version" end end diff --git a/src/libexec/package-tools/pack.sh b/src/libexec/package-tools/pack.sh index 4cb075d3..53d1b40e 100755 --- a/src/libexec/package-tools/pack.sh +++ b/src/libexec/package-tools/pack.sh @@ -301,12 +301,6 @@ if [[ $BUILD_DEB == true ]]; then step_run "Cleaning environment" rm -Rf package/deb package/public fi - step_run "Downloading libnss-resolver" --exit \ - mkdir -p package/deb \ - && wget -q "${LIBNSS_RESOLVER_REPO}/ubuntu12-libnss-resolver_${LIBNSS_RESOLVER_VERSION}_amd64.deb" -O "package/deb/precise-libnss-resolver_${LIBNSS_RESOLVER_VERSION}_amd64.deb" \ - && wget -q "${LIBNSS_RESOLVER_REPO}/ubuntu14-libnss-resolver_${LIBNSS_RESOLVER_VERSION}_amd64.deb" -O "package/deb/trusty-libnss-resolver_${LIBNSS_RESOLVER_VERSION}_amd64.deb" \ - && wget -q "${LIBNSS_RESOLVER_REPO}/ubuntu15-libnss-resolver_${LIBNSS_RESOLVER_VERSION}_amd64.deb" -O "package/deb/wily-libnss-resolver_${LIBNSS_RESOLVER_VERSION}_amd64.deb" - EXTRA_FLAGS="" if [[ $LINUX_BUILD_WAS_EXECUTED == true || $NO_CLEAN_LINUX == true ]]; then EXTRA_FLAGS="LINUX_CLEAN=" @@ -316,12 +310,16 @@ if [[ $BUILD_DEB == true ]]; then azk shell package -- rm -rf /azk/aptly/public/pool/main/a/azk${PACKAGE_SUFFIX}/azk${PACKAGE_SUFFIX}_${VERSION_NUMBER}*_amd64.deb \ && make package_deb ${EXTRA_FLAGS} - UBUNTU_VERSIONS=( "ubuntu12:precise" "ubuntu14:trusty" "ubuntu15:wily" ) + UBUNTU_VERSIONS=( "ubuntu12:precise" "ubuntu14:trusty" "ubuntu15:wily" "ubuntu16:xenial") for UBUNTU_VERSION in "${UBUNTU_VERSIONS[@]}"; do UBUNTU_VERSION_NUMBER="${UBUNTU_VERSION%%:*}" UBUNTU_VERSION_CODENAME="${UBUNTU_VERSION##*:}" + step_run "Downloading libnss-resolver for ${UBUNTU_VERSION}" --exit \ + mkdir -p package/deb \ + && wget -q "${LIBNSS_RESOLVER_REPO}/${UBUNTU_VERSION_NUMBER}-libnss-resolver_${LIBNSS_RESOLVER_VERSION}_amd64.deb" -O "package/deb/${UBUNTU_VERSION_CODENAME}-libnss-resolver_${LIBNSS_RESOLVER_VERSION}_amd64.deb" + step_run "Generating ${UBUNTU_VERSION_NUMBER} repository" \ linux_generate ubuntu ${LIBNSS_RESOLVER_VERSION} ${UBUNTU_VERSION_CODENAME} ${CLEAN_REPO_ARGS} diff --git a/src/libexec/package-tools/test.sh b/src/libexec/package-tools/test.sh index 55fe7ce3..df8f6dd0 100755 --- a/src/libexec/package-tools/test.sh +++ b/src/libexec/package-tools/test.sh @@ -6,6 +6,7 @@ if [[ $# < 1 ]] || [[ $# > 2 ]]; then echo " - ubuntu12" echo " - ubuntu14" echo " - ubuntu15" + echo " - ubuntu16" echo " - fedora20" echo " - fedora23" exit 1 @@ -38,6 +39,10 @@ case "${SO}" in DISTRO='ubuntu' CODENAME='wily' ;; + ubuntu16 ) + DISTRO='ubuntu' + CODENAME='xenial' + ;; fedora20 ) DISTRO='fedora' CODENAME='fedora20' diff --git a/src/sync/index.js b/src/sync/index.js index f2c57fdd..7e3e2573 100644 --- a/src/sync/index.js +++ b/src/sync/index.js @@ -1,82 +1,112 @@ -import { _, log, path, t } from 'azk'; +import { _, path, t } from 'azk'; import { defer } from 'azk/utils/promises'; +import { which } from 'azk/utils'; +import { spawnAsync } from 'azk/utils/spawn_helper'; +import { stat } from 'file-async'; + +function wrap(value) { + if (_.isArray(value)) { return value; } + if (!_.isEmpty(value)) { return [value]; } + return value; +} + +const SEP = path.sep; // Module -var Sync = { - sync(origin, destination, opts = {}) { - var shell = opts.ssh ? `ssh ${opts.ssh.opts}` : '/bin/bash'; - destination = opts.ssh ? `${opts.ssh.url}:${destination}` : destination; - - var r = new require('rsync')() - .shell(shell) - .flags('az') - .set('delete') - .source(path.join(origin, '/')) - .destination(destination); - - if (opts.include) { - r.include(this._process_include(opts.include)); - r.exclude(['*/', '*']); - } else { - if (opts.except) { r.exclude(opts.except); } - if (opts.except_from) { - r.set('exclude-from', path.resolve(origin, opts.except_from)); - } - } +export default class Sync { + static sync(origin, destination, opts = {}) { + return stat(origin).then((stats) => { + let args = ['-az']; + let include = [], exclude = []; + let DIR_SEP = ''; - return defer((resolve, reject) => { - r.execute(function(err, code, cmd) { - log.debug('[sync] rsync command:', cmd); - if (err) { - err = err.stack ? err.stack : err.toString(); - log.error('[sync] fail', err); - return reject({ err, code }); + if (!stats.isFile()) { + DIR_SEP = SEP; + include = wrap(opts.include || []); + exclude = wrap(opts.except || []); + + if (opts.except_from) { + args.push('--exclude-from'); + args.push(this._escape_arg(path.resolve(origin, opts.except_from))); } - return resolve(code); - }); - }); - }, + } + + if (!_.isEmpty(opts.relative_sufix)) { + let relative = path.relative(opts.relative_sufix, origin); + origin = `${opts.relative_sufix}${SEP}.${SEP}${relative}`; + args.push('--relative'); + let fix_relatives = (item) => { + item = (item.match(/^\//)) ? path.join(SEP, relative, item) : item; + return item; + }; + include = _.map(include, fix_relatives); + exclude = _.map(exclude, fix_relatives); + } + + include = _.map(include, this._escape_arg); + exclude = _.map(exclude, this._escape_arg); + + let rsync_options = { + args, include, exclude, + src : this._escape_arg(`${origin}${DIR_SEP}`), + delete: true, + }; - version() { - var version_output = ''; - var r = new require('rsync')().set('version').output((data) => { - version_output += data.toString(); + if (opts.ssh) { + // Extra escape for use in ssh command + destination = destination.replace(/(['\s\\])/g,'\\\\$1'); + destination = destination.replace(/(")/g,'\\\\\\\\$1'); + destination = destination.replace(/(`)/g,'\\\\\\$1'); + + rsync_options.dest = `"${opts.ssh.url}:${destination}"`; + rsync_options.ssh = true; + rsync_options.sshCmdArgs = [opts.ssh.opts]; + } else { + rsync_options.dest = this._escape_arg(destination); + } + + return this.rsync(rsync_options); }); + } + + static rsync(rsync_options) { + var rsync = require('rsyncwrapper'); return defer((resolve, reject) => { - r.execute(function(err, code) { + rsync(rsync_options, (err, stdout, stderr, cmd) => { + let result = { stdout, stderr, cmd, code: 0 }; if (err) { - return reject({ err, code }); - } - var _version = version_output.match(/.*version\ (\d+\.\d+\.\d+)/); - if (!_.isEmpty(_version) && _version.length >= 2) { - return resolve(_version[1]); - } else { - return reject({ - err: t('errors.rsync_invalid_version_format', { - rsync_version: version_output - }) - }); + result.code = err.code; + return reject(_.assign(err, result)); } + return resolve(result); }); }); - }, - - _process_include(include) { - if (!_.isArray(include)) { include = [include]; } - - var includes = []; - _.each(include, (include) => { - _.reduce(include.split('/'), (acc, p) => { - if (acc === include) { return acc; } - acc = acc.concat(p) === include ? acc.concat(p) : acc.concat(path.join(p, '/')); - includes.push(acc); - return acc; - }, ''); - }); + } + + static version() { + return which('rsync') + .then((fullpath) => { + return spawnAsync(fullpath, ['--version']); + }) + .then(({error_code, message}) => { + if (error_code !== 0) { + throw { err: message, code: error_code }; + } - return includes; + var _version = message.match(/.*version\ (\d+\.\d+\.\d+)/); + if (!_.isEmpty(_version) && _version.length >= 2) { + return _version[1]; + } else { + throw({ + err: t('errors.rsync_invalid_version_format', { rsync_version: message }) + }); + } + }); } -}; + static _escape_arg(arg) { + return `"${arg.replace(/(["`\\])/g,'\\$1')}"`; + } +} export { Sync }; diff --git a/src/sync/watcher.js b/src/sync/watcher.js index 1c987f8f..6a8e4d61 100644 --- a/src/sync/watcher.js +++ b/src/sync/watcher.js @@ -13,12 +13,13 @@ export class Watcher extends IPublisher { watch(origin, destination, opts) { var id = this.calculate_id(origin, destination); - return defer((resolve, reject) => { - log.debug('Adding watcher from folder', origin, 'to', destination); + let promise = defer((resolve, reject) => { + log.info('[sync] Adding watcher, from: %s, to: %s, opts: %j', origin, destination, opts, {}); if (this.workers[id]) { this.workers[id].count++; this.publish('init', { status: 'exists' }); - log.debug('Current watchers:\n', this.workers); + log.info ('[sync] Existing watcher %s, count: %s', id, this.workers[id].count, {}); + log.debug('[sync] Current watchers: %j', _.keys(this.workers), {}); return resolve(); } @@ -39,39 +40,66 @@ export class Watcher extends IPublisher { child.on('restart', () => { this.publish('restart', { op: 'restart', status: 'init' }); child.send({origin, destination, opts}); - log.info('[sync] Sync process restarted', { origin, destination }); + log.info('[sync] Sync process restarted, from: %s, to: %s', origin, destination, {}); }); child.on('exit:code', (code) => { - var level = code !== null && code > 0 && code !== 130 ? 'warn' : 'info'; - log[level]('[sync] Sync process exited with code', code, { origin, destination }); + let level = code !== null && code > 0 && code !== 130 ? 'warn' : 'info'; + let msg = '[sync] Sync process exited, from: %s, to: %s, msg: %s'; + log[level](msg, origin, destination, code, {}); }); child.on('message', (data) => { - log.debug('[sync] Watcher received message', data); data = JSON.parse(data); + log.debug('[sync] Watcher received message: %j', data, {}); this.publish(data.op, data); - if (data.op === "watch" && data.status === 'ready') { - return resolve(true); - } else if (data.op === "sync" && data.status === "fail") { - this.unwatch(origin, destination); - return reject(data.err); + + if (data.op === 'watch') { + switch (data.status) { + case 'ready': + return resolve(true); + } + } else if (data.op === "sync") { + if (data.cmd) { + log.debug('[sync] Rsync command: %s', data.cmd, {}); + } + + switch (data.status) { + case 'done': + log.info('[sync] Sync completed, from: %s, to: %s', origin, destination, {}); + break; + case 'fail': + log.error('[sync] Sync failed: %j', data, {}); + if (data.level === "critical" || !promise.isFulfilled()) { + this.unwatch(origin, destination); + let error = new Error(data.message); + return reject(_.assign(error, data)); + } + } } }); child.on('start', (process) => { - log.debug('[sync] process started with pid', process); - child.send({origin, destination, opts}); + log.debug('[sync] Sync process started with PID %s', process.childData.pid, {}); + child.send({ origin, destination, opts }); }); child.start(); }); + + return promise; } unwatch(origin, destination) { - log.info('[sync] Removing watcher from folder', origin, 'to', destination); - var result = this._remove_worker(this.calculate_id(origin, destination)); - log.debug('[sync] Current watchers: ' + _.keys(this.workers)); + log.info('[sync] Removing watcher, from: %s, to: %s', origin, destination, {}); + var id = this.calculate_id(origin, destination); + var result = this._remove_worker(id); + + if (this.workers[id]) { + log.info ('[sync] Watcher ', id, ', count:', this.workers[id].count); + } + + log.debug('[sync] Current watchers: %j', _.keys(this.workers), {}); this.publish('finish', { op: 'finish', status: 'done' }); return result; } @@ -99,8 +127,6 @@ export class Watcher extends IPublisher { } _remove_worker(id) { - log.info('[sync] workers ' + _.keys(this.workers)); - log.info('[sync] call to remove worker id ' + id); var worker = this.workers[id]; if (worker) { if (--worker.count <= 0) { @@ -109,7 +135,7 @@ export class Watcher extends IPublisher { } } else { id = JSON.parse(id); - log.info('[sync] Trying to stop an unexisting watcher:', id); + log.info('[sync] Trying to stop an unexisting watcher: %j', id, {}); } return true; } diff --git a/src/sync/worker.js b/src/sync/worker.js index 93c33632..9bad7a44 100644 --- a/src/sync/worker.js +++ b/src/sync/worker.js @@ -1,28 +1,36 @@ -import { _, lazy_require, path, log, fsAsync } from 'azk'; -import { defer, async } from 'azk/utils/promises'; +import { _, lazy_require, path, fsAsync } from 'azk'; +import { defer, async, promiseResolve } from 'azk/utils/promises'; + +// Only for debug +// require('source-map-support').install({}); var lazy = lazy_require({ Sync : ['azk/sync'], chokidar: 'chokidar' }); +const WATCH_IDLE = 0, WATCH_INIT = 1, WATCH_READY = 2; + export class Worker { constructor(process) { + this.chok = null; + this.status = WATCH_IDLE; this.process = process; this.process.on('message', (data) => { process.title = "azk sync worker " + data.origin + " " + data.destination; this.watch(data.origin, data.destination, data.opts).then(() => {}, () => {}); }); - - this.chok = null; } watch(origin, destination, opts = {}) { this.unwatch(); + this.status = WATCH_INIT; - log.debug('[sync] call to watch and sync: %s => %s', origin, destination); return async(this, function* () { try { + // Be sure watcher is in proper status + if (this.status !== WATCH_INIT) { return; } + var exists = yield fsAsync.exists(origin); if (!exists) { throw { err: 'Sync: origin path not exist', code: 101 }; @@ -33,17 +41,26 @@ export class Worker { throw new Error(`The type of the file ${origin} is not supported for synchronization`); } - if (stats.isDirectory()) { - opts = yield this._check_for_except_from(origin, opts); - } + // Calculate includes, excludes and watch pattern + let patterns_ary = [], ignored = []; + let except_from = yield this._check_for_except_from(origin, stats.isDirectory(), opts.except_from); + [ patterns_ary, ignored, opts.include, opts.except ] = yield this._process_patterns(origin, except_from, opts); + delete opts.except_from; + + // Be sure watcher is in proper status + if (this.status !== WATCH_INIT) { return; } yield lazy.Sync.sync(origin, destination, opts); this._send('sync', 'done'); - yield this._start_watcher(origin, destination, opts); + + // Be sure watcher is in proper status + if (this.status !== WATCH_INIT) { return; } + yield this._start_watcher(patterns_ary, ignored, origin, destination, opts); this._send('watch', 'ready'); + this.status = WATCH_READY; } catch (err) { - log.error('[sync] fail', (err.stack ? err.stack : err.toString())); - this._send('sync', 'fail', { err }); + this.unwatch(); + this._send('sync', 'fail', err); throw err; } }); @@ -55,88 +72,155 @@ export class Worker { this.chok.close(); this.chok = null; } + this.status = WATCH_IDLE; } - _start_watcher(origin, destination, opts = {}) { - var patterns_ary = this._watch_patterns_ary(origin, opts); - return defer((resolve, reject) => { - this.chok = lazy.chokidar.watch(patterns_ary, { ignoreInitial: true }); - this.chok.on('all', (event, filepath) => { - filepath = path.relative(origin, filepath); - - var include = (event === 'unlinkDir') ? [`${filepath}/\*`, filepath] : [ filepath ]; - var sync_opts = { include, ssh: opts.ssh || null }; - - log.debug('[sync]', event, 'file', filepath); - - lazy.Sync - .sync(origin, destination, sync_opts ) - .then(() => this._send(event, 'done', { filepath })) - .catch((err) => this._send(event, 'fail', _.merge(err, { filepath }))); - }) - .on('ready', () => resolve(true)) - .on('error', (err) => reject(err)); + _start_watcher(patterns_ary, ignored, src, dest, opts = {}) { + let promise = defer((resolve, reject) => { + this.chok = lazy.chokidar.watch(patterns_ary, { ignored: ignored, ignoreInitial: true }) + .on('all', (event, filepath) => { + this._sync(event, filepath, src, dest, opts); + }) + .on('ready', () => { + this.chok.removeAllListeners('error'); + resolve(true); + }) + .on('error', (err) => { + if (promise.isFulfilled()) { + err.level = "warning"; + this._send('watch', 'fail', err); + // restart + this.watch(src, dest, opts); + } else { + err.level = "critical"; + reject(err); + } + }); }); + return promise; } - _check_for_except_from(origin, opts) { + _sync(event, filepath, src, dest, opts) { return async(this, function* () { - // Find from exceptions in files - opts = _.clone(opts); - opts.except = _.flatten([opts.except || []]); + if (event === "unlink" || event === "unlinkDir") { + var exists = yield fsAsync.exists(filepath); + if (!exists) { + filepath = path.join(filepath, '..'); + return this._sync(event, filepath, src, dest, opts); + } + } - var exists, file, file_content = ''; - var candidates = opts.except_from ? [opts.except_from] : []; + let destination = path.join(dest, path.relative(src, filepath)); + let [ include, except ] = this._include_and_except(filepath, src, dest, opts); + let sync_opts = { + ssh: opts.ssh || null, + relative_sufix: opts.relative_sufix, + include, except, + }; + + return lazy.Sync + .sync(filepath, destination, sync_opts) + .then((result) => this._send(event, 'done', _.merge(result, { filepath }))) + .catch((err) => { + err = _.assign(err, { filepath, level: "warning" }); + this._send(event, 'fail', err); + }); + }); + } + + _include_and_except(origin, src, dest, opts) { + let include = opts.include; + let except = opts.except; + + if (origin !== src) { + let relative = path.relative(src, origin); + let regex = new RegExp(`^\/${relative}\/`); + let reduce_fn = (acc, file) => { + if ((file.match(/^\//) || file.match(/^\.\//)) && regex.test(file)) { + acc.push(file.replace(regex, '/')); + } else if (!(file.match(/^\//) || file.match(/^\.\//))) { + acc.push(file); + } + return acc; + }; + include = _.reduce(include, reduce_fn, []); + except = _.reduce(except, reduce_fn, []); + } + + return [include, except]; + } + + _check_for_except_from(origin, is_dir, except_from) { + return async(this, function* () { + if (!is_dir) { return null; } + + let candidates = except_from ? [except_from] : []; candidates = candidates.concat([ path.join(origin, ".syncignore"), path.join(origin, ".gitignore"), ]); - delete opts.except_from; - - for (var i = 0; i < candidates.length; i++) { - file = candidates[i]; - exists = yield fsAsync.exists(file); - if (exists) { - opts.except_from = file; - file_content = yield fsAsync.readFile(file); - file_content = file_content.toString(); - break; - } + let exists; + for (let i = 0; i < candidates.length; i++) { + exists = yield fsAsync.exists(candidates[i]); + if (exists) { return candidates[i]; } } - opts.except = opts.except.concat( - _.without(file_content.split('\n'), '') - ); - - return opts; + return null; }); } - _watch_patterns_ary(origin, opts = {}) { - // TODO Support include - opts = _.clone(opts); - opts.include = ['.']; - opts.except = _.flatten([opts.except || []]); - - return opts.include.map((pattern) => { - return path.resolve(origin, pattern); - }).concat(opts.except.map((pattern) => { - return '!' + path.resolve(origin, pattern); - })); + _process_patterns(origin, except_from, opts = {}) { + return this + ._exclude_candidates(opts.except || [], except_from) + .then((candidates) => { + let include = [...(opts.include || [])]; + let watch = _.map([".", ...include], (i) => path.resolve(origin, i)); + let unwatch = []; + + let exclude = _.reduce(candidates, (acc, pattern) => { + if (pattern.match(/^!/)) { + pattern = pattern.replace(/^!(.*)/, '$1'); + include.push(pattern); + watch.push(path.join(origin, pattern)); + } else if (pattern.match(/^\//) || pattern.match(/^\.\//) || pattern.match(/\*\*/)) { + acc.push(pattern); + unwatch.push(path.join(origin, pattern)); + } else { + acc.push(pattern); + unwatch.push(path.join(origin, '**', pattern)); + } + return acc; + }, []); + + return [watch, unwatch, include, exclude]; + }); + } + + _exclude_candidates(except, except_from) { + if (!_.isEmpty(except_from)) { + return fsAsync.readFile(except_from).then((content) => { + content = content.toString().split('\n'); + return except.concat(_.filter(content, (pattern) => { + return !_.isEmpty(pattern) && !pattern.match(/^\s*#/); + })); + }); + } + return promiseResolve(except); } _send(op, status, opts = {}) { + if (opts instanceof Error) { + opts = JSON.stringify(opts, ["message", "arguments", "type", "name"].concat(Object.keys(opts))); + opts = JSON.parse(opts); + } this.process.send(JSON.stringify(_.merge({ op, status }, opts))); } } -// // If this file is a main process, it means that // this process is being forked by azk itself -// if (require.main === module) { process.title = 'azk sync worker'; - log.debug('[sync]', "sync worker spawned"); new Worker(process); } diff --git a/src/system/index.js b/src/system/index.js index 86a47860..31ead67b 100644 --- a/src/system/index.js +++ b/src/system/index.js @@ -3,10 +3,12 @@ import { version, config } from 'azk'; import { net } from 'azk/utils'; var lazy = lazy_require({ - Image : ['azk/images'], - Run : ['azk/system/run'], - Scale : ['azk/system/scale'], - Balancer : ['azk/system/balancer'], + Image : ['azk/images'], + Run : ['azk/system/run'], + Scale : ['azk/system/scale'], + Balancer : ['azk/system/balancer'], + replaceEnvs: ['azk/utils/shell'], + dotenv : 'dotenv', }); var XRegExp = require('xregexp').XRegExp; @@ -14,9 +16,6 @@ var regex_port = new XRegExp( "(?[0-9]{1,})(:(?[0-9]{1,})){0,1}(/(?tcp|udp)){0,1}", "x" ); -// https://regex101.com/r/rK1eJ0/2 -var regex_envs = /(?:\${[=|-]?([A-Z|\d|_]+)})|(?:\$[=|-]?([A-Z|\d|_]+))/g; - export class System { constructor(manifest, name, image, options = {}) { this.manifest = manifest; @@ -377,7 +376,7 @@ export class System { } // Private methods - _make_options(daemon, options = {}) { + _make_options(daemon, options = {}, image_conf = {}) { // Default values options = _.defaults(options, { workdir: this.options.workdir, @@ -391,8 +390,14 @@ export class System { dns_servers: this.options.dns_servers, }); + var img_envs = {}; + _.forEach(image_conf.Env, (env_data) => { + env_data = env_data.split("="); + img_envs[env_data[0]] = env_data[1]; + }); + // Map ports to docker configs: ports and envs - var envs = _.merge({}, this.envs, this._envs_from_file(), options.envs); + var envs = _.merge({}, img_envs, this.envs, this._envs_from_file(), options.envs); var ports = {}; var parsed_ports = this._parse_ports(options.ports); @@ -444,9 +449,7 @@ export class System { }; // Expand envs - var template = JSON.stringify(finalOptions); - template = template.replace(regex_envs, "$${envs.$1$2}"); - finalOptions = JSON.parse(utils.template(template, { envs })); + finalOptions = this._expand_envs(finalOptions, envs); // Not expand and not stringify finalOptions.env = envs; @@ -455,6 +458,25 @@ export class System { return finalOptions; } + _expand_envs(options, envs) { + // https://regex101.com/r/zX1qU4/1 + var keep_special = /\${((?:[^\d]*?[@?\#])|(?:\d*?))}/g; + + // Prepare template + var template = JSON.stringify(options); + template = lazy.replaceEnvs(template, "#{envs.$1}", true); + template = template.replace(keep_special, "#{_keep_special('$1')}"); + + // Replaces + var expanded = utils.template(template, { + envs, + _keep_special: (special) => `\${${special}}`, + }); + + // Parse result + return JSON.parse(expanded); + } + _shell_command(options) { // Set a default shell // cmd.shell have preference over system.shell @@ -498,17 +520,12 @@ export class System { } _envs_from_file() { - var envs = {}; - var file = path.join(this.manifest.manifestPath, '.env'); + let envs = {}; + const file = path.join(this.manifest.manifestPath, '.env'); if (fs.existsSync(file)) { var content = fs.readFileSync(file).toString(); - _.each(content.split('\n'), (entry) => { - if (entry.match(/.*=.*/)) { - entry = entry.split('='); - envs[entry[0]] = entry[1]; - } - }); + envs = lazy.dotenv.parse(content); } return envs; @@ -574,15 +591,11 @@ export class System { return JSON.parse(utils.template(template, data)); } - _replace_keep_keys(template) { + _replace_keep_keys(str) { // https://regex101.com/r/gF4uT4/1 - let regexes = { - net_envs: /(?:(?:[#|$]{|<%)[=|-]?)\s*((?:envs|net)\.[\S]+?)\s*(?:}|%>)/g, - envs : regex_envs, - }; - return template - .replace(regexes.net_envs, "#{_keep_key('$1')}") - .replace(regexes.envs , "#{_keep_key('$1$2', '$$')}"); + let net_envs = /(?:(?:[#|$]{|<%)[=|-]?)\s*((?:envs|net)\.[\S]+?)\s*(?:}|%>)/g; + str = str.replace(net_envs, "#{_keep_key('$1')}"); + return lazy.replaceEnvs(str, "#{_keep_key('$1', '$')}", true); } _resolved_path(mount_path) { @@ -592,12 +605,6 @@ export class System { return path.resolve(this.manifest.manifestPath, mount_path); } - _sync_path(mount_path) { - var sync_base_path = config('paths:sync_folders'); - sync_base_path = path.join(sync_base_path, this.manifest.namespace, this.name); - return path.join(sync_base_path, this._resolved_path(mount_path)); - } - _mounts_to_volumes(mounts, daemon = true) { var volumes = {}; @@ -612,21 +619,21 @@ export class System { mount.options = _.defaults(mount.options || {}, {resolve: true}); - var target = null; - switch (mount.type) { - case 'path': - target = mount.value; + let target = null; + let path_fn = () => { + target = mount.value; - if (mount.options.resolve) { - if (!target.match(/^\//)) { - target = this._resolved_path(target); - } - - target = (fs.existsSync(target)) ? - utils.docker.resolvePath(target) : null; + if (mount.options.resolve) { + if (!target.match(/^\//)) { + target = this._resolved_path(target); } - break; + target = (fs.existsSync(target)) ? + utils.docker.resolvePath(target) : null; + } + }; + + switch (mount.type) { case 'persistent': target = path.join(persist_base, mount.value); break; @@ -634,18 +641,15 @@ export class System { case 'sync': if (daemon && mount.options.daemon !== false || !daemon && mount.options.shell === true) { - target = this._sync_path(mount.value); + target = this.sync_folder(point); } else { - target = mount.value; - - if (!target.match(/^\//)) { - target = this._resolved_path(target); - } - - target = (fs.existsSync(target)) ? - utils.docker.resolvePath(target) : null; + path_fn(); } break; + + case 'path': + path_fn(); + break; } if (!_.isEmpty(target)) { @@ -656,18 +660,19 @@ export class System { }, volumes); } - _mounts_to_syncs(mounts) { - var syncs = {}; + sync_folder(point = '') { + let id = utils.calculateHash(path.join(this.name, point)); + return path.join(config('paths:sync_folders'), this.manifest.namespace, id); + } + _mounts_to_syncs(mounts) { return _.reduce(mounts, (syncs, mount, mount_key) => { if (mount.type === 'sync') { - - var host_sync_path = this._resolved_path(mount.value); - var mounted_subpaths = _.reduce(mounts, (subpaths, mount, dir) => { if ( dir !== mount_key && dir.indexOf(mount_key) === 0) { - var regex = new RegExp(`^${mount_key}`); - subpaths = subpaths.concat([path.normalize(dir.replace(regex, './'))]); + let regex = new RegExp(`^${mount_key}`); + let exclude = `/${path.normalize(dir.replace(regex, './'))}`; + subpaths.push(exclude); } return subpaths; }, []); @@ -677,12 +682,13 @@ export class System { .concat(mounted_subpaths) .concat(['.syncignore', '.gitignore', '.azk/', '.git/'])); + var host_sync_path = this._resolved_path(mount.value); syncs[host_sync_path] = { - guest_folder: this._sync_path(mount.value), - options : mount.options, + guest_folder : this.sync_folder(mount_key), + options : mount.options, }; } return syncs; - }, syncs); + }, {}); } } diff --git a/src/system/run.js b/src/system/run.js index a768723d..ea0f1300 100644 --- a/src/system/run.js +++ b/src/system/run.js @@ -31,6 +31,7 @@ var Run = { if ((!options.provision_force) && system.provisioned) { return null; } + system.provisioned = null; log.debug('provision steps', steps); // provision command (require /bin/sh) @@ -76,7 +77,7 @@ var Run = { var deps_envs = yield system.checkDependsAndReturnEnvs(options, false); options.envs = _.merge(deps_envs, options.envs || {}); - var image = yield this._check_image(system, options); + var image = _.isEmpty(options.image_data) ? yield this._check_image(system, options) : options.image_data; var docker_opt = system.shellOptions(options, image.Config); // Force env TERM in interatives shells (like a ssh) @@ -159,46 +160,39 @@ var Run = { runWatch(system, daemon = true, silent = false) { var topic = "system.sync.status"; - if (_.isEmpty(system.syncs)) { - return true; - } - - if (!silent) { - publish(topic, { type : "sync", system : system.name }); - } - - return thenAll(_.map(system.syncs || {}, (sync_data, host_folder) => { - return async(this, function* () { - if (daemon && sync_data.options.daemon === false || - !daemon && sync_data.options.shell !== true) { - return promiseResolve(); - } + let syncs = system.syncs || {}; + if (_.isEmpty(syncs)) { return promiseResolve(true); } + if (!silent) { publish(topic, { type : "sync", system : system.name }); } + return this + ._clean_sync_folder(system, syncs) + .then(() => this._watch_syncs(system, syncs, topic, daemon)); + }, - if (config('agent:requires_vm')) { - sync_data.options = _.defaults(sync_data.options, { use_vm: true, ssh: lazy.Client.ssh_opts() }); - } + _watch_syncs(system, syncs, topic, daemon) { + return thenAll(_.map(syncs, (sync_data, host_folder) => { + if (daemon && sync_data.options.daemon === false || + !daemon && sync_data.options.shell !== true) { + return promiseResolve(); + } - var clean_sync_folder = yield this._clean_sync_folder(system, host_folder); - if (clean_sync_folder !== 0) { - // TODO: throw proper error - throw new NotBeenImplementedError('SyncError'); - } + if (config('agent:requires_vm')) { + sync_data.options = _.defaults(sync_data.options, { use_vm: true, ssh: lazy.Client.ssh_opts() }); + } - var pub_data = { - system : system.name, - host_folder : host_folder, - guest_folder: sync_data.guest_folder, - options : sync_data.options - }; + var pub_data = { + system : system.name, + host_folder : host_folder, + guest_folder: sync_data.guest_folder, + options : sync_data.options + }; - publish(topic, _.assign({ type : "sync_start" }, pub_data)); + publish(topic, _.assign({ type : "sync_start" }, pub_data)); - return lazy.Client - .watch(host_folder, sync_data.guest_folder, sync_data.options) - .then(() => { - publish(topic, _.assign({ type : "sync_done" }, pub_data)); - }); - }); + return lazy.Client + .watch(host_folder, sync_data.guest_folder, sync_data.options) + .then(() => { + publish(topic, _.assign({ type : "sync_done" }, pub_data)); + }); })); }, @@ -364,10 +358,10 @@ var Run = { }); }, - // Check and pull image + // Check image, pull or build if not found _check_image(system, options) { options = _.defaults(options, { - image_pull: true, + build_force: false, }); var _subscription = subscribe("image.check.status", (msg, env) => { @@ -376,32 +370,15 @@ var Run = { }); return asyncUnsubscribe(this, _subscription, function* () { - var promise; - - if ((options.build_force || options.image_pull) && !system.image.builded) { - if (system.image.provider === 'docker') { - promise = system.image.pull(options); - } else if (system.image.provider === 'dockerfile') { - promise = system.image.build(options); + let promise = system.image.checkOrGet(options, options.build_force); + var image = yield promise.then((image) => { + if (isBlank(image)) { + throw new ImageNotAvailable(system.name, system.image.name); } + return image; + }); - // save the date provisioning - system.image.builded = new Date(); - } else { - promise = system.image.check() - .then((image) => { - if (isBlank(image)) { - throw new ImageNotAvailable(system.name, system.image.name); - } - return image; - }); - } - - var image = yield promise; - - if (!isBlank(image)) { - return image.inspect(); - } + return image.inspect(); }); }, @@ -447,7 +424,7 @@ var Run = { }); }, - _clean_sync_folder(system, host_folder) { + _clean_sync_folder(system, syncs) { return async(this, function* () { var local_user = config('agent:vm:user'); var uid, gid; @@ -459,27 +436,26 @@ var Run = { gid = uid; } - var mounted_sync_folders = '/sync_folders'; - var current_sync_folder = path.join(mounted_sync_folders, system.manifest.namespace, system.name, host_folder); + let sync_folder = path.join(system.sync_folder(), '..'); + let sync_folders = path.join(sync_folder, '..'); - var find_exec = `-exec chown -h ${uid}:${gid} '{}' \\;`; - var find_args = `${current_sync_folder} \\( -not -user ${uid} -or -not -group ${gid} \\) ${find_exec}`; + let find_exec = `-exec chown -h ${uid}:${gid} '{}' \\;`; + let find_args = `"${sync_folder}" \\( -not -user ${uid} -or -not -group ${gid} \\) ${find_exec}`; // Script to fix sync folder - var script = [ - `mkdir -p ${current_sync_folder}`, - `find ${find_args}`, - ].join(" && "); + let cmds = _.map(syncs, (sync_data) => `mkdir -p "${sync_data.guest_folder}"`); + cmds.push(`find ${find_args}`); + let script = cmds.join(' && '); // Docker params - var image_name = config('docker:image_default'); - var cmd = ["/bin/bash", "-c", script]; - var docker_opts = { + let image_name = config('docker:image_default'); + let cmd = ["/bin/bash", "-c", script]; + let docker_opts = { interactive: false, extra: { HostConfig: { Binds: [ - `${config('paths:sync_folders')}:${mounted_sync_folders}`, + `${config('paths:sync_folders')}:${sync_folders}`, "/etc/passwd:/etc/passwd" ] } @@ -491,7 +467,10 @@ var Run = { var data = yield container.inspect(); yield container.remove(); - return data.State.ExitCode; + if (data.State.ExitCode !== 0) { + // TODO: throw proper error + throw new NotBeenImplementedError('SyncError'); + } }); }, diff --git a/src/utils/index.js b/src/utils/index.js index 3864cfb9..9b175689 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,8 +1,8 @@ -var { join } = require('path'); -var crypto = require('crypto'); -var _ = require('lodash'); -var fs = require('fs'); -var defer = require('azk/utils/promises').defer; +import { join } from 'path'; +import _ from 'lodash'; +import fs from 'fs'; +import { defer, nfcall } from './promises'; +import lazy_require from './lazy_require'; var Utils = { get default () { return Utils; }, @@ -11,77 +11,7 @@ var Utils = { get docker () { return require('azk/utils/docker'); }, get Versions() { return require('azk/utils/versions'); }, - /** - * `lazy_require` can postpone loading of external dependencies. - * They are only loaded when they are used. - * `lazy_require` also have some interesting syntactic sugar. - * - * Each object key passed can be one of the bellow forms: - * - * ----------------------- - * 1. { key: 'libName' } - * Do a simple `require` from 'libName' to `lazy.key` - * - * @example - * let lazy = lazy_require({ fsLib: 'fs' }); - * // lazy.fsLib === require('fs') - * - * ----------------------- - * 2. { propertyName: ['libName'] } - * Require libName and return propertyName from libName to `lazy.propertyName` - * - * @example - * let lazy = lazy_require({ exists: ['fs'] }); - * // lazy.exists === require('fs').exists - * - * ----------------------- - * 3. { key: ['libName', 'propertyName'] } - * Require libName and return propertyName from libName to `lazy.key` - * - * @example - * let lazy = lazy_require({ fsExistsFunc: ['fs', 'exists'] }); - * // lazy.fsExistsFunc === require('fs').exists - * - * ----------------------- - * 4. { key: function } - * Run this function when this key is accessed. Function's return will be on `lazy.key` - * - * @example - * let lazy = lazy_require({ - * foo: function() { - * return new require('bar')() - * } - * }); - * // lazy.foo === require('bar') - * - * - * @param {Object} loads Object with key-value configurations - * @return {Object} Lazy object to use - */ - lazy_require(loads) { - var lazy = {}; - _.each(loads, (func, getter) => { - if (!_.isFunction(func)) { - var opts = func; - - // Only name module support - if (_.isString(opts)) { - opts = [opts]; - } else if (_.isEmpty(opts[1])) { - opts[1] = getter; - } - - // Require function - func = () => { - var mod = require(opts[0]); - return _.isEmpty(opts[1]) ? mod : mod[opts[1]]; - }; - } - lazy.__defineGetter__(getter, func); - }); - - return lazy; - }, + lazy_require: lazy_require, envs(key, defaultValue) { var value = process.env[key]; @@ -115,6 +45,10 @@ var Utils = { return options; }, + which(command) { + return nfcall(require('which'), command); + }, + cd(target, func) { var result, old = process.cwd(); @@ -163,7 +97,7 @@ var Utils = { }, calculateHash(string) { - var shasum = crypto.createHash('sha1'); + var shasum = require('crypto').createHash('sha1'); shasum.update(string); return shasum.digest('hex'); }, diff --git a/src/utils/lazy_require.js b/src/utils/lazy_require.js new file mode 100644 index 00000000..dbb14078 --- /dev/null +++ b/src/utils/lazy_require.js @@ -0,0 +1,73 @@ +import _ from 'lodash'; + +/** + * `lazy_require` can postpone loading of external dependencies. + * They are only loaded when they are used. + * `lazy_require` also have some interesting syntactic sugar. + * + * Each object key passed can be one of the bellow forms: + * + * ----------------------- + * 1. { key: 'libName' } + * Do a simple `require` from 'libName' to `lazy.key` + * + * @example + * let lazy = lazy_require({ fsLib: 'fs' }); + * // lazy.fsLib === require('fs') + * + * ----------------------- + * 2. { propertyName: ['libName'] } + * Require libName and return propertyName from libName to `lazy.propertyName` + * + * @example + * let lazy = lazy_require({ exists: ['fs'] }); + * // lazy.exists === require('fs').exists + * + * ----------------------- + * 3. { key: ['libName', 'propertyName'] } + * Require libName and return propertyName from libName to `lazy.key` + * + * @example + * let lazy = lazy_require({ fsExistsFunc: ['fs', 'exists'] }); + * // lazy.fsExistsFunc === require('fs').exists + * + * ----------------------- + * 4. { key: function } + * Run this function when this key is accessed. Function's return will be on `lazy.key` + * + * @example + * let lazy = lazy_require({ + * foo: function() { + * return new require('bar')() + * } + * }); + * // lazy.foo === require('bar') + * + * + * @param {Object} loads Object with key-value configurations + * @return {Object} Lazy object to use + */ +export default function lazy_require(loads) { + var lazy = {}; + _.each(loads, (func, getter) => { + if (!_.isFunction(func)) { + var opts = func; + + // Only name module support + if (_.isString(opts)) { + opts = [opts]; + } else if (_.isEmpty(opts[1])) { + opts[1] = getter; + } + + // Require function + func = () => { + var mod = require(opts[0]); + return _.isEmpty(opts[1]) ? mod : mod[opts[1]]; + }; + } + lazy.__defineGetter__(getter, func); + }); + + return lazy; +} diff --git a/src/utils/shell.js b/src/utils/shell.js new file mode 100644 index 00000000..42015325 --- /dev/null +++ b/src/utils/shell.js @@ -0,0 +1,15 @@ +// https://regex101.com/r/rK1eJ0/6 +var regex_envs = /(\\*)(\$(?:(?:[=|-]?([A-Z0-9_]*[A-Z_]+[A-Z0-9_]*)|(?:{[=|-]?([A-Z0-9_]*[A-Z_]+[A-Z0-9_]*)}))))/g; + +export function replaceEnvs(str, replace_for = "$1", json = false) { + return str.replace(regex_envs, (_match, slashes, v1, v2, v3) => { + var slashes_size = slashes.length / (json ? 2 : 1); + if (slashes_size % 2 === 0) { + return `${slashes}${replace_for.replace("$1", v2 || v3)}`; + } else if (slashes_size) { + return `${slashes.slice(0, slashes.length - (json ? 2 : 1))}${v1}`; + } else { + return _match; + } + }); +}