diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 0670033..cdbc836 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -3,9 +3,12 @@ current_version = 0.5.0 commit = True tag = False -[bumpversion:file:pydockenv/__init__.py] +[bumpversion:file:meta.json] + +[bumpversion:file:cmd/root.go] [bumpversion:file:HISTORY.md] search = ## Unreleased -replace = ## [v{new_version} - {now:%Y-%m-%d}](https://github.com/se7entyse7en/pydockenv/compare/v{current_version}...v{new_version}) - +replace = ## Unreleased + - + - ## [v{new_version} - {now:%Y-%m-%d}](https://github.com/se7entyse7en/pydockenv/compare/v{current_version}...v{new_version}) diff --git a/.flake8 b/.flake8 index 0f2b2ec..aec1659 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,2 @@ [flake8] max-complexity = 10 -exclude = setup.py,.tox diff --git a/.gitignore b/.gitignore index 32e127f..a1781eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,20 @@ +### Go ### +bin/pydockenv_exec* + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +### Python ### # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/.isort.cfg b/.isort.cfg index 0a422bc..57b611e 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -5,7 +5,5 @@ order_by_type=True lines_after_imports=2 indent=' ' atomic=True -known_docker=docker -known_first_party=pydockenv,tests -sections=STDLIB,THIRDPARTY,DOCKER,FIRSTPARTY,LOCALFOLDER +sections=STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER default_section=THIRDPARTY diff --git a/.travis.yml b/.travis.yml index 5e252a2..0eac105 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,14 @@ dist: xenial -language: python -python: - - "3.6" - - "3.7" - - "3.8" - services: - docker -before_install: - - pip install . - # For some reason if this is not done before, the image won't be pulled. - # This is not required when running locally. - - docker pull alpine/socat:latest - -script: - - python -m unittest discover +matrix: + include: + - language: go + script: echo "TO DO" + - language: python + before_script: + - pip install -r requirements-dev.txt + script: + - flake8 + - isort -rc -c -vb diff --git a/HISTORY.md b/HISTORY.md index 86a4c1e..d798d8d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,17 @@ # HISTORY +## Unreleased + +### Changed + +- Removed commands `load`, `save` and `export` to be re-added/ported into `go` as well +- [documentation] Updated the "Installation", "Development" and "Examples" sections of the README +- [internal] Ported all the code from `python` to `go`: + - easier installation: no more issues with interpreter version used and potential packages conflict + - easier development setup: no meta usage + - deeper integration with docker + + ## [v0.5.0 - 2019-10-20](https://github.com/se7entyse7en/pydockenv/compare/v0.4.1...v0.5.0) ### Added diff --git a/Makefile b/Makefile index c2a8e0f..b8e95fd 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,39 @@ TWINE_CONFIG_FILE ?= .pypirc -clean: +clean-compile: + rm -f bin/pydockenv_exec* +clean-build: rm -rf dist -check: - flake8 --config .flake8 - isort -rc -c $(git ls-tree -r HEAD --name-only | grep "\.py") -build: clean check - python setup.py sdist bdist_wheel + +compile-linux-i686: GOOS = linux +compile-linux-i686: GOARCH = 386 +compile-linux-x86_64: GOOS = linux +compile-linux-x86_64: GOARCH = amd64 +compile-darwin: GOOS = darwin +compile-darwin: GOARCH = amd64 +compile-linux-i686 compile-linux-x86_64 compile-darwin: clean-compile + GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o bin/pydockenv_exec_$(GOOS)_$(GOARCH) + +compile-all: compile-linux-i686 compile-linux-x86_64 compile-darwin + +compile-dev: clean-compile + go build -o bin/pydockenv_exec + +build-linux-i686: PLAT_NAME = manylinux1_i686 +build-linux-i686: EXEC_SUFFIX = _linux_386 +build-linux-i686: compile-linux-i686 +build-linux-x86_64: PLAT_NAME = manylinux1_x86_64 +build-linux-x86_64: EXEC_SUFFIX = _linux_amd64 +build-linux-x86_64: compile-linux-x86_64 +build-darwin: PLAT_NAME = macosx_10_4_x86_64 +build-darwin: EXEC_SUFFIX = _darwin_amd64 +build-darwin: compile-darwin +build-linux-i686 build-linux-x86_64 build-darwin: clean-build + mv bin/pydockenv_exec{$(EXEC_SUFFIX),} + python setup.py bdist_wheel --plat-name $(PLAT_NAME) + rm bin/pydockenv_exec + +build-all: build-linux-i686 build-linux-x86_64 build-darwin publish-check: @if [[ $$(git rev-parse --abbrev-ref HEAD) != "master" ]]; then \ @@ -25,10 +52,10 @@ publish-pypi: twine check dist/* twine upload --config-file $(TWINE_CONFIG_FILE) --repository $(PYPI_REPOSITORY) dist/* -publish-test: PYPI_REPOSITORY=testpypi -publish-test: build publish-pypi -publish: PYPI_REPOSITORY=pypi -publish: build publish-check publish-pypi +publish-test: PYPI_REPOSITORY = testpypi +publish-test: build-all publish-pypi +publish: PYPI_REPOSITORY = pypi +publish: build-all publish-check publish-pypi git push origin --tags bump-major: PART = major @@ -38,4 +65,7 @@ bump-patch: PART = patch bump-major bump-minor bump-patch: bumpversion $(PART) -.PHONY: clean check build publish-check publish bump-major bump-minor bump-patch +.PHONY: clean-compile clean-build \ + compile-linux-i686 compile-linux-x86_64 compile-darwin compile-all compile-dev \ + build-linux-i686 build-linux-x86_64 build-darwin build-all \ + publish-check publish bump-major bump-minor bump-patch diff --git a/README.md b/README.md index cdbd591..42edfc8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ ![Python versions](https://img.shields.io/pypi/pyversions/pydockenv.svg) ![Pypi](https://img.shields.io/pypi/v/pydockenv.svg) -![Travis build](https://img.shields.io/travis/se7entyse7en/pydockenv.svg) ![License](https://img.shields.io/github/license/se7entyse7en/pydockenv.svg) *Notice: This project is currently in alpha stage* @@ -11,36 +10,22 @@ ## Installation -To install `pydockenv` run the following: +To install `pydockenv` simply run the following: ``` -pip install --user pydockenv -``` - -To avoid conflicts this installs `pydockenv` to the Python user install directory. In order to run the `pydockenv` binary, you will need to have that directory in your `PATH`. You can do this by running these lines: -``` -export PY_USER_BIN=$(python -c 'import site; print(site.USER_BASE + "/bin")') -export PATH=$PY_USER_BIN:$PATH -``` - -`pydockenv` supports only python >=3.6 at the moment and will use the `python` binary. In case your system has another version installed, you can use a different interpreter by specifying its path through the `PYDOCKENV_INTERPRETER` environment variable: -``` -PYDOCKENV_INTERPRETER=path/to/binary pydockenv [...] -# or -export PYDOCKENV_INTERPRETER=path/to/binary -pydockenv [...] +pip install pydockenv ``` ## Why? I assume that everybody landing here knows the great advantages that virtual environment brings. The reason I've started this project is that Docker provides even better isolation from the underlying system, and brings the advantage of being really portable across different systems. -In my personal experience sometimes is difficult to replicate the same local virtual environment, and eventually save it and share it with somebody else, especially if the one you want to share the environment with runs, for example, a different operating system. +In my personal experience sometimes it is difficult to replicate the same local virtual environment, and eventually save it and share it with somebody else, especially if the one you want to share the environment with runs, for example, a different operating system. Using Docker as the engine of the virtual environment makes the environment itself isolated, easily sharable, and also eventually ready-to-be-deployed given that it is still a Docker container. ## Quickstart -The installation will provide you with the `pydockenv` binary that let you create, save, load an environment and handle its dependencies. +The installation will provide you with the `pydockenv` binary that lets you create, save, load an environment, and handle its dependencies. Let's start by creating an environment! @@ -117,7 +102,7 @@ and that's it! You can list all your environments and all the packages installed ``` # list all environments pydockenv list-environments -# list packages installed in current environment +# list packages installed in the current environment pydockenv list-packages ``` @@ -150,27 +135,135 @@ Here are some examples that are available in the `examples` directory to show mo ### Hello World! -File: `examples/hello_world.py` +File: `examples/hello_world.py`. -This first example just shows how different environments work. The script simply prints the "Hello World!" string followed by the Python version being used. You can run this on different environments and see how the output changes. See the following gif. +This first example just shows how different environments work. The script simply prints the `Hello World!` string followed by the Python version being used. You can run this in different environments and see how the output changes. -![](https://raw.githubusercontent.com/se7entyse7en/pydockenv/master/assets/hello-world.gif) - -### Requests +- Environment created with Python 3.8: +``` +✔ se7entyse7en in ~/Projects/se7entyse7en/pydockenv/examples $ pydockenv create --name=hello-world --version=3.8 . +INFO[0000] Creating virtual environment... name=hello-world project-dir=. toml-file= version=3.8 +ERROR: You must give at least one requirement to install (see "pip help install") +INFO[0017] Virtual environment created! name=hello-world project-dir=. toml-file= version=3.8 +✔ se7entyse7en in ~/Projects/se7entyse7en/pydockenv/examples $ source pydockenv activate hello-world +INFO[0000] Activating virtual environment... name=hello-world +INFO[0000] Virtual environment activated! name=hello-world +(hello-world) ✔ se7entyse7en in ~/Projects/se7entyse7en/pydockenv/examples $ pydockenv run python hello_world.py +INFO[0000] Running command... command="[python hello_world.py]" detach=false env-vars="map[]" ports="[]" +Hello World! +Python version: 3.8.3 (default, Jun 9 2020, 17:39:39) +[GCC 8.3.0] +INFO[0000] Command ran! command="[python hello_world.py]" detach=false env-vars="map[]" ports="[]" +``` -File: `examples/requests_get.py` +- Environment created with Python 3.7. +``` +✔ se7entyse7en in ~/Projects/se7entyse7en/pydockenv/examples $ pydockenv create --name=hello-world --version=3.7 . +INFO[0000] Creating virtual environment... name=hello-world project-dir=. toml-file= version=3.7 +ERROR: You must give at least one requirement to install (see "pip help install") +INFO[0013] Virtual environment created! name=hello-world project-dir=. toml-file= version=3.7 +✔ se7entyse7en in ~/Projects/se7entyse7en/pydockenv/examples $ source pydockenv activate hello-world +INFO[0000] Activating virtual environment... name=hello-world +INFO[0000] Virtual environment activated! name=hello-world +(hello-world) ✔ se7entyse7en in ~/Projects/se7entyse7en/pydockenv/examples $ pydockenv run python hello_world.py +INFO[0000] Running command... command="[python hello_world.py]" detach=false env-vars="map[]" ports="[]" +Hello World! +Python version: 3.7.7 (default, Jun 9 2020, 17:58:51) +[GCC 8.3.0] +INFO[0000] Command ran! +``` -This second example shows how you can install external packages and run Python scripts by passing arguments as you would do normally. See the following gif. +### Requests -![](https://raw.githubusercontent.com/se7entyse7en/pydockenv/master/assets/requests-get.gif) +File: `examples/requests_get.py`. + +This second example shows how you can install external packages and run Python scripts by passing arguments as you would do normally. + +``` +✔ se7entyse7en in ~/Projects/se7entyse7en/pydockenv/examples $ pydockenv create --name=requests --version=3.8 . +INFO[0000] Creating virtual environment... name=requests project-dir=. toml-file= version=3.8 +INFO[0001] Virtual environment created! name=requests project-dir=. toml-file= version=3.8 +✔ se7entyse7en in ~/Projects/se7entyse7en/pydockenv/examples $ source pydockenv activate requests +INFO[0000] Activating virtual environment... name=requests +INFO[0000] Virtual environment activated! name=requests +(requests) ✔ se7entyse7en in ~/Projects/se7entyse7en/pydockenv/examples $ pydockenv install requests +INFO[0000] Installing packages... file= packages="[requests]" +Collecting requests + Downloading requests-2.24.0-py2.py3-none-any.whl (61 kB) + |████████████████████████████████| 61 kB 155 kB/s +Collecting urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 + Downloading urllib3-1.25.9-py2.py3-none-any.whl (126 kB) + |████████████████████████████████| 126 kB 1.2 MB/s +Collecting idna<3,>=2.5 + Downloading idna-2.9-py2.py3-none-any.whl (58 kB) + |████████████████████████████████| 58 kB 1.2 MB/s +Collecting chardet<4,>=3.0.2 + Downloading chardet-3.0.4-py2.py3-none-any.whl (133 kB) + |████████████████████████████████| 133 kB 1.4 MB/s +Collecting certifi>=2017.4.17 + Downloading certifi-2020.6.20-py2.py3-none-any.whl (156 kB) + |████████████████████████████████| 156 kB 1.4 MB/s +Installing collected packages: urllib3, idna, chardet, certifi, requests +Successfully installed certifi-2020.6.20 chardet-3.0.4 idna-2.9 requests-2.24.0 urllib3-1.25.9 +INFO[0005] Packages installed! file= packages="[requests]" +(requests) (base) ✔ (☸|gke_athenian-1_us-east1-c_production-cluster:default) se7entyse7en in ~/Projects/se7entyse7en/pydockenv/examples (go-porting)  $ pydockenv run requests_get.py https://github.com +INFO[0000] Running command... command="[requests_get.py https://github.com]" detach=false env-vars="map[]" ports="[]" +OCI runtime exec failed: exec failed: container_linux.go:349: starting container process caused "exec: \"requests_get.py\": executable file not found in $PATH": unknown +INFO[0000] Command ran! command="[requests_get.py https://github.com]" detach=false env-vars="map[]" ports="[]" +(requests) ✔ se7entyse7en in ~/Projects/se7entyse7en/pydockenv/examples $ pydockenv run python requests_get.py https://github.com +INFO[0000] Running command... command="[python requests_get.py https://github.com]" detach=false env-vars="map[]" ports="[]" +Requested https://github.com: status code = 200 +INFO[0000] Command ran! +``` ### Flask web app -File: `examples/flask_hello_world.py` - -This third example shows how you can run a Flask web application. This example is important as it shows some caveats that make the experience of using `pydockenv` not completely identical to using a local environment. Given the environment runs inside a container, the host must be `0.0.0.0` and not `localhost`, and the port being used must be told to `pydockenv` using the `-p/--port` flag of the `run` command. See the following gif. - -![](https://raw.githubusercontent.com/se7entyse7en/pydockenv/master/assets/flask-hello-world.gif) +File: `examples/flask_hello_world.py`. + +This third example shows how you can run a Flask web application. This example is important as it shows some caveats that make the experience of using `pydockenv` not completely identical to using a local environment. Given the environment runs inside a container, the host must be `0.0.0.0` and not `localhost`, and the port being used must be told to `pydockenv` using the `-p/--port` flag of the `run` command. + +``` +✔ se7entyse7en in ~/Projects/se7entyse7en/pydockenv/examples $ pydockenv create --name=flask --version=3.8 . +INFO[0000] Creating virtual environment... name=flask project-dir=. toml-file= version=3.8 +INFO[0000] Virtual environment created! name=flask project-dir=. toml-file= version=3.8 +(base) ✔ (☸|gke_athenian-1_us-east1-c_production-cluster:default) se7entyse7en in ~/Projects/se7entyse7en/pydockenv/examples (go-porting)  $ source pydockenv activate flask +INFO[0000] Activating virtual environment... name=flask +INFO[0000] Virtual environment activated! name=flask +(flask) ✔ se7entyse7en in ~/Projects/se7entyse7en/pydockenv/examples $ pydockenv install flask +INFO[0000] Installing packages... file= packages="[flask]" +Collecting flask + Downloading Flask-1.1.2-py2.py3-none-any.whl (94 kB) + |████████████████████████████████| 94 kB 359 kB/s +Collecting itsdangerous>=0.24 + Downloading itsdangerous-1.1.0-py2.py3-none-any.whl (16 kB) +Collecting Werkzeug>=0.15 + Downloading Werkzeug-1.0.1-py2.py3-none-any.whl (298 kB) + |████████████████████████████████| 298 kB 1.5 MB/s +Collecting Jinja2>=2.10.1 + Downloading Jinja2-2.11.2-py2.py3-none-any.whl (125 kB) + |████████████████████████████████| 125 kB 1.7 MB/s +Collecting click>=5.1 + Downloading click-7.1.2-py2.py3-none-any.whl (82 kB) + |████████████████████████████████| 82 kB 671 kB/s +Collecting MarkupSafe>=0.23 + Downloading MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl (32 kB) +Installing collected packages: itsdangerous, Werkzeug, MarkupSafe, Jinja2, click, flask +Successfully installed Jinja2-2.11.2 MarkupSafe-1.1.1 Werkzeug-1.0.1 click-7.1.2 flask-1.1.2 itsdangerous-1.1.0 +INFO[0004] Packages installed! file= packages="[flask]" +(flask) ✔ se7entyse7en in ~/Projects/se7entyse7en/pydockenv/examples $ pydockenv run -p 5000 python flask_hello_world.py +INFO[0000] Running command... command="[python flask_hello_world.py]" detach=false env-vars="map[]" ports="[5000]" + * Serving Flask app "flask_hello_world" (lazy loading) + * Environment: production + WARNING: This is a development server. Do not use it in a production deployment. + Use a production WSGI server instead. + * Debug mode: on + * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit) + * Restarting with stat + * Debugger is active! + * Debugger PIN: 241-187-134 +``` + +Now you can go to `localhost:5000` and the Flask server will respond. ## Commands reference @@ -189,30 +282,20 @@ This third example shows how you can run a Flask web application. This example i ## Development -To setup you environment to develop `pydockenv` run the followings: -1. Clone the repository where you prefer: -``` -git clone https://github.com/se7entyse7en/pydockenv.git -``` -2. Enter in the project root directory: -``` -cd -``` -3. Install `pydockenv` in editable mode in another path: +To test changes locally during development you need to compile the program since it is called by a bash script. Once you have the project cloned locally you can do as follows from the root of the project: + +1. create a link to `pydockenv` from `dev-pydockenv` that should be placed in a path of your choice that is included in `PATH`: ``` -export PYTHONPATH=/lib/python3.7/site-packages -mkdir -p /lib/python3.7/site-packages -pip install --prefix -e . +ln -s `pwd`/bin/pydockenv /usr/local/bin/dev-pydockenv ``` -I personally use `~/.local-dev` as ``. - -4. Rename `pydockvenv` into `dev-pydockenv` or whatever you want: +2. make `dev-pydockenv` call the local compiled binary: ``` -mv /bin/{,dev-}pydockenv +export PYDOCKENV_EXEC_PATH=$(pwd)/bin ``` -5. Add `/bin` to you `$PATH`: +3. compile the program: ``` -export PATH=/bin:$PATH +make compile-dev ``` +This last step has to be ran everytime a new change has to tested. Now you have `dev-pydockenv` that runs the development version of `pydockenv`! diff --git a/assets/flask-hello-world.gif b/assets/flask-hello-world.gif deleted file mode 100644 index f2e1f2a..0000000 Binary files a/assets/flask-hello-world.gif and /dev/null differ diff --git a/assets/hello-world.gif b/assets/hello-world.gif deleted file mode 100644 index ebfb224..0000000 Binary files a/assets/hello-world.gif and /dev/null differ diff --git a/assets/requests-get.gif b/assets/requests-get.gif deleted file mode 100644 index 8db45b4..0000000 Binary files a/assets/requests-get.gif and /dev/null differ diff --git a/bin/pydockenv b/bin/pydockenv index f902199..6f559c3 100755 --- a/bin/pydockenv +++ b/bin/pydockenv @@ -23,10 +23,8 @@ else fi fi -python=${PYDOCKENV_INTERPRETER:-python} - -pydockenv_entry_point=$($python -c 'from pydockenv import cli; print(cli.__file__)') -$python $pydockenv_entry_point $@ +pydockenv_exec_base=$(python -c 'import sys; import os; print(os.path.join(sys.prefix, "bin"))') +${PYDOCKENV_EXEC_PATH:-$pydockenv_exec_base}/pydockenv_exec $@ exitCode=$? if [ $exitCode -eq 0 ] diff --git a/cmd/activate.go b/cmd/activate.go new file mode 100644 index 0000000..c0a9f75 --- /dev/null +++ b/cmd/activate.go @@ -0,0 +1,50 @@ +package cmd + +import ( + "github.com/se7entyse7en/pydockenv/internal/environment" + "github.com/se7entyse7en/pydockenv/log" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var activateCmd = &cobra.Command{ + Use: "activate [environment-name]", + Short: "Activate a virtual environment", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + logger := log.Logger + err := log.SetupLogger(cmd) + if err != nil { + logger.WithError(err).Fatal("Cannot setup logger") + } + + cmdArgs, err := parseActivateArgs(cmd, args) + if err != nil { + logger.WithError(err).Fatal("Cannot parse arguments") + } + + ctxLogger := logger.WithFields(logrus.Fields{ + "name": cmdArgs.Name, + }) + ctxLogger.Info("Activating virtual environment...") + + if err := environment.Activate(cmdArgs.Name); err != nil { + logger.WithError(err).Fatal("Cannot activate virtual environment") + } + + ctxLogger.Info("Virtual environment activated!") + }, +} + +func parseActivateArgs(cmd *cobra.Command, args []string) (*activateArgs, error) { + name := args[0] + return &activateArgs{Name: name}, nil +} + +type activateArgs struct { + Name string +} + +func init() { + rootCmd.AddCommand(activateCmd) +} diff --git a/cmd/create.go b/cmd/create.go new file mode 100644 index 0000000..0f42782 --- /dev/null +++ b/cmd/create.go @@ -0,0 +1,116 @@ +package cmd + +import ( + "io/ioutil" + + "github.com/pelletier/go-toml" + "github.com/se7entyse7en/pydockenv/internal/environment" + "github.com/se7entyse7en/pydockenv/log" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var createCmd = &cobra.Command{ + Use: "create [project-dir]", + Short: "Create a virtual environment", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + logger := log.Logger + err := log.SetupLogger(cmd) + if err != nil { + logger.WithError(err).Fatal("Cannot setup logger") + } + + cmdArgs, err := parseCreateArgs(cmd, args) + if err != nil { + logger.WithError(err).Fatal("Cannot parse arguments") + } + + conf, err := parseConfig(cmdArgs) + + ctxLogger := logger.WithFields(logrus.Fields{ + "project-dir": cmdArgs.ProjectDir, + "toml-file": cmdArgs.TomlFile, + "name": conf.Name, + "version": conf.Python, + }) + ctxLogger.Info("Creating virtual environment...") + + if err := environment.Create(conf); err != nil { + logger.WithError(err).Fatal("Cannot create virtual environment") + } + + ctxLogger.Info("Virtual environment created!") + }, +} + +func parseCreateArgs(cmd *cobra.Command, args []string) (*createArgs, error) { + projectDir := args[0] + + tomlFile, err := getStringArg(cmd, "toml-file") + if err != nil { + return nil, err + } + + name, err := getStringArg(cmd, "name") + if err != nil { + return nil, err + } + + version, err := getStringArg(cmd, "version") + if err != nil { + return nil, err + } + + return &createArgs{ + ProjectDir: projectDir, + TomlFile: tomlFile, + Name: name, + Version: version, + }, nil +} + +func parseConfig(cmdArgs *createArgs) (*environment.Config, error) { + if cmdArgs.TomlFile != "" { + b, err := ioutil.ReadFile(cmdArgs.TomlFile) + if err != nil { + return nil, err + } + + rawConfig := environment.RawConfig{} + toml.Unmarshal(b, &rawConfig) + + conf := rawConfig.Tool.Pydockenv + conf.ProjectDir = cmdArgs.ProjectDir + if conf.Python == "" { + conf.Python = "latest" + } + return conf, nil + } + + python := "latest" + if cmdArgs.Version != "" { + python = cmdArgs.Version + } + + return &environment.Config{ + Name: cmdArgs.Name, + ProjectDir: cmdArgs.ProjectDir, + Python: python, + }, nil +} + +type createArgs struct { + ProjectDir string + TomlFile string + Name string + Version string +} + +func init() { + rootCmd.AddCommand(createCmd) + + createCmd.Flags().StringP("toml-file", "f", "", "Toml file") + createCmd.Flags().StringP("name", "n", "", "Environment name") + createCmd.Flags().StringP("version", "v", "", "Python version") +} diff --git a/cmd/deactivate.go b/cmd/deactivate.go new file mode 100644 index 0000000..6d71eee --- /dev/null +++ b/cmd/deactivate.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "github.com/se7entyse7en/pydockenv/internal/environment" + "github.com/se7entyse7en/pydockenv/log" + "github.com/spf13/cobra" +) + +var deactivateCmd = &cobra.Command{ + Use: "deactivate", + Short: "Deactivate the current virtual environment", + Run: func(cmd *cobra.Command, args []string) { + logger := log.Logger + err := log.SetupLogger(cmd) + if err != nil { + logger.WithError(err).Fatal("Cannot setup logger") + } + + logger.Info("Deactivating virtual environment...") + + if err := environment.Deactivate(); err != nil { + logger.WithError(err).Fatal("Cannot deactivate virtual environment") + } + + logger.Info("Virtual environment deactivated!") + }, +} + +func init() { + rootCmd.AddCommand(deactivateCmd) +} diff --git a/cmd/install.go b/cmd/install.go new file mode 100644 index 0000000..4f8b82e --- /dev/null +++ b/cmd/install.go @@ -0,0 +1,64 @@ +package cmd + +import ( + "github.com/se7entyse7en/pydockenv/internal/dependency" + "github.com/se7entyse7en/pydockenv/log" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var installCmd = &cobra.Command{ + Use: "install [package_1] [package_2] ... [package_n]", + Short: "Install packages from args or from a requirements file", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + logger := log.Logger + err := log.SetupLogger(cmd) + if err != nil { + logger.WithError(err).Fatal("Cannot setup logger") + } + + cmdArgs, err := parseInstallArgs(cmd, args) + if err != nil { + logger.WithError(err).Fatal("Cannot parse arguments") + } + + ctxLogger := logger.WithFields(logrus.Fields{ + "packages": cmdArgs.Packages, + "file": cmdArgs.RequirementsFile, + }) + ctxLogger.Info("Installing packages...") + + if err := dependency.Install(&dependency.Requirements{ + FileName: cmdArgs.RequirementsFile, + Packages: dependency.Packages{RawDependencies: cmdArgs.Packages}, + }); err != nil { + logger.WithError(err).Fatal("Cannot install packages") + } + + ctxLogger.Info("Packages installed!") + }, +} + +func parseInstallArgs(cmd *cobra.Command, args []string) (*installArgs, error) { + requirementsFile, err := getStringArg(cmd, "file") + if err != nil { + return nil, err + } + + return &installArgs{ + Packages: args, + RequirementsFile: requirementsFile, + }, nil +} + +type installArgs struct { + Packages []string + RequirementsFile string +} + +func init() { + rootCmd.AddCommand(installCmd) + + installCmd.Flags().StringP("file", "f", "", "Requirements file") +} diff --git a/cmd/list_environments.go b/cmd/list_environments.go new file mode 100644 index 0000000..488b814 --- /dev/null +++ b/cmd/list_environments.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "github.com/se7entyse7en/pydockenv/internal/environment" + "github.com/se7entyse7en/pydockenv/log" + "github.com/spf13/cobra" +) + +var listEnvironmentsCmd = &cobra.Command{ + Use: "list-environments", + Short: "List all the virtual environments", + Run: func(cmd *cobra.Command, args []string) { + logger := log.Logger + err := log.SetupLogger(cmd) + if err != nil { + logger.WithError(err).Fatal("Cannot setup logger") + } + + logger.Info("Listing virtual environments...") + + if err := environment.ListEnvironments(); err != nil { + logger.WithError(err).Fatal("Cannot list virtual environments") + } + + logger.Info("Virtual environments listed!") + }, +} + +func init() { + rootCmd.AddCommand(listEnvironmentsCmd) +} diff --git a/cmd/list_packages.go b/cmd/list_packages.go new file mode 100644 index 0000000..5b2fe44 --- /dev/null +++ b/cmd/list_packages.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "github.com/se7entyse7en/pydockenv/internal/dependency" + "github.com/se7entyse7en/pydockenv/log" + "github.com/spf13/cobra" +) + +var listPackagesCmd = &cobra.Command{ + Use: "list-packages", + Short: "List the packages installed for the current virtual environments", + Run: func(cmd *cobra.Command, args []string) { + logger := log.Logger + err := log.SetupLogger(cmd) + if err != nil { + logger.WithError(err).Fatal("Cannot setup logger") + } + + logger.Info("Listing packages...") + + if err := dependency.ListPackage(); err != nil { + logger.WithError(err).Fatal("Cannot list packages") + } + + logger.Info("Packages listed!") + }, +} + +func init() { + rootCmd.AddCommand(listPackagesCmd) +} diff --git a/cmd/remove.go b/cmd/remove.go new file mode 100644 index 0000000..a8f1c21 --- /dev/null +++ b/cmd/remove.go @@ -0,0 +1,50 @@ +package cmd + +import ( + "github.com/se7entyse7en/pydockenv/internal/environment" + "github.com/se7entyse7en/pydockenv/log" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var removeCmd = &cobra.Command{ + Use: "remove [environment-name]", + Short: "Remove a virtual environment", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + logger := log.Logger + err := log.SetupLogger(cmd) + if err != nil { + logger.WithError(err).Fatal("Cannot setup logger") + } + + cmdArgs, err := parseRemoveArgs(cmd, args) + if err != nil { + logger.WithError(err).Fatal("Cannot parse arguments") + } + + ctxLogger := logger.WithFields(logrus.Fields{ + "name": cmdArgs.Name, + }) + ctxLogger.Info("Removing virtual environment...") + + if err := environment.Remove(cmdArgs.Name); err != nil { + logger.WithError(err).Fatal("Cannot remove virtual environment") + } + + ctxLogger.Info("Virtual environment removed!") + }, +} + +func parseRemoveArgs(cmd *cobra.Command, args []string) (*removeArgs, error) { + name := args[0] + return &removeArgs{Name: name}, nil +} + +type removeArgs struct { + Name string +} + +func init() { + rootCmd.AddCommand(removeCmd) +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..08d4810 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,53 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +var VERSION = "0.5.0" + +var rootCmd = &cobra.Command{ + Use: "pydockenv", + Short: "A CLI tool to handle Python virtual environments", +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func getStringArg(cmd *cobra.Command, argName string) (string, error) { + value, err := cmd.Flags().GetString(argName) + if err != nil { + return "", fmt.Errorf("cannot read %s: %w", argName, err) + } + + return value, nil +} + +func getStringArrayArg(cmd *cobra.Command, argName string) ([]string, error) { + value, err := cmd.Flags().GetStringArray(argName) + if err != nil { + return []string{}, fmt.Errorf("cannot read %s: %w", argName, err) + } + + return value, nil +} + +func getBoolArg(cmd *cobra.Command, argName string) (bool, error) { + value, err := cmd.Flags().GetBool(argName) + if err != nil { + return false, fmt.Errorf("cannot read %s: %w", argName, err) + } + + return value, nil +} + +func init() { + rootCmd.PersistentFlags().StringP("log-level", "l", "info", "Log level") +} diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 0000000..09df0d2 --- /dev/null +++ b/cmd/run.go @@ -0,0 +1,104 @@ +package cmd + +import ( + "strconv" + "strings" + + "github.com/se7entyse7en/pydockenv/internal/executor" + "github.com/se7entyse7en/pydockenv/log" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var runCmd = &cobra.Command{ + Use: "run [args]", + Short: "Run a command", + Args: cobra.ArbitraryArgs, + Run: func(cmd *cobra.Command, args []string) { + logger := log.Logger + err := log.SetupLogger(cmd) + if err != nil { + logger.WithError(err).Fatal("Cannot setup logger") + } + + cmdArgs, err := parseRunArgs(cmd, args) + if err != nil { + logger.WithError(err).Fatal("Cannot parse arguments") + } + + ctxLogger := logger.WithFields(logrus.Fields{ + "command": cmdArgs.Cmd, + "detach": cmdArgs.Detach, + "env-vars": cmdArgs.EnvVars, + "ports": cmdArgs.Ports, + }) + + ctxLogger.Info("Running command...") + + err = executor.Execute(cmdArgs.Cmd, &executor.ExecOptions{ + Detach: cmdArgs.Detach, + EnvVars: cmdArgs.EnvVars, + Ports: cmdArgs.Ports, + }) + if err != nil { + ctxLogger.WithError(err).Fatal("Cannot run command in container") + } + + ctxLogger.Info("Command ran!") + }, +} + +func parseRunArgs(cmd *cobra.Command, args []string) (*runArgs, error) { + detach, err := getBoolArg(cmd, "detach") + if err != nil { + return nil, err + } + + envVars := make(map[string]string) + rawEnvVars, err := getStringArrayArg(cmd, "env-var") + if err != nil { + return nil, err + } + + for _, rev := range rawEnvVars { + splitted := strings.Split(rev, "=") + envVars[splitted[0]] = splitted[1] + } + + var ports []int + rawPorts, err := getStringArrayArg(cmd, "port") + if err != nil { + return nil, err + } + + for _, rp := range rawPorts { + p, err := strconv.Atoi(rp) + if err != nil { + return nil, err + } + + ports = append(ports, p) + } + + return &runArgs{ + Cmd: args, + Detach: detach, + EnvVars: envVars, + Ports: ports, + }, nil +} + +type runArgs struct { + Cmd []string + Detach bool + EnvVars map[string]string + Ports []int +} + +func init() { + rootCmd.AddCommand(runCmd) + + runCmd.Flags().BoolP("detach", "d", false, "Whether to detach from container") + runCmd.Flags().StringArrayP("env-var", "e", []string{}, "Environment variables to set") + runCmd.Flags().StringArrayP("port", "p", []string{}, "Ports to reach") +} diff --git a/cmd/shell.go b/cmd/shell.go new file mode 100644 index 0000000..50d2ab1 --- /dev/null +++ b/cmd/shell.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "github.com/se7entyse7en/pydockenv/internal/executor" + "github.com/se7entyse7en/pydockenv/log" + "github.com/spf13/cobra" +) + +var shellCmd = &cobra.Command{ + Use: "shell [args]", + Short: "Run Python", + Args: cobra.ArbitraryArgs, + Run: func(cmd *cobra.Command, args []string) { + logger := log.Logger + err := log.SetupLogger(cmd) + if err != nil { + logger.WithError(err).Fatal("Cannot setup logger") + } + + logger.Info("Running Python...") + + command := []string{"python"} + command = append(command, args...) + err = executor.Execute(command, &executor.ExecOptions{}) + if err != nil { + logger.WithError(err).Fatal("Cannot run Python in container") + } + + logger.Info("Python ran!") + }, +} + +func init() { + rootCmd.AddCommand(shellCmd) +} diff --git a/cmd/status.go b/cmd/status.go new file mode 100644 index 0000000..dad9415 --- /dev/null +++ b/cmd/status.go @@ -0,0 +1,27 @@ +package cmd + +import ( + "github.com/se7entyse7en/pydockenv/internal/environment" + "github.com/se7entyse7en/pydockenv/log" + "github.com/spf13/cobra" +) + +var statusCmd = &cobra.Command{ + Use: "status", + Short: "Show the current active virtual environment", + Run: func(cmd *cobra.Command, args []string) { + logger := log.Logger + err := log.SetupLogger(cmd) + if err != nil { + logger.WithError(err).Fatal("Cannot setup logger") + } + + if err := environment.Status(); err != nil { + logger.WithError(err).Fatal("Cannot show current active virtual environment") + } + }, +} + +func init() { + rootCmd.AddCommand(statusCmd) +} diff --git a/cmd/uninstall.go b/cmd/uninstall.go new file mode 100644 index 0000000..8d83d30 --- /dev/null +++ b/cmd/uninstall.go @@ -0,0 +1,60 @@ +package cmd + +import ( + "github.com/se7entyse7en/pydockenv/internal/dependency" + "github.com/se7entyse7en/pydockenv/log" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var uninstallCmd = &cobra.Command{ + Use: "uninstall [package_1] [package_2] ... [package_n]", + Short: "unInstall packages from args", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + logger := log.Logger + err := log.SetupLogger(cmd) + if err != nil { + logger.WithError(err).Fatal("Cannot setup logger") + } + + cmdArgs, err := parseUninstallArgs(cmd, args) + if err != nil { + logger.WithError(err).Fatal("Cannot parse arguments") + } + + ctxLogger := logger.WithFields(logrus.Fields{ + "packages": cmdArgs.Packages, + }) + ctxLogger.Info("Uninstalling packages...") + + if err := dependency.Uninstall(&dependency.Packages{ + RawDependencies: cmdArgs.Packages}, + cmdArgs.Yes, + ); err != nil { + logger.WithError(err).Fatal("Cannot uninstall packages") + } + + ctxLogger.Info("Packages uninstalled!") + }, +} + +func parseUninstallArgs(cmd *cobra.Command, args []string) (*uninstallArgs, error) { + yes, err := getBoolArg(cmd, "yes") + if err != nil { + return nil, err + } + + return &uninstallArgs{Packages: args, Yes: yes}, nil +} + +type uninstallArgs struct { + Packages []string + Yes bool +} + +func init() { + rootCmd.AddCommand(uninstallCmd) + + uninstallCmd.Flags().BoolP("yes", "y", false, "Don't ask for confirmation") +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..b6af174 --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,19 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print version of pydockenv", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println(VERSION) + }, +} + +func init() { + rootCmd.AddCommand(versionCmd) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..308ac44 --- /dev/null +++ b/go.mod @@ -0,0 +1,25 @@ +module github.com/se7entyse7en/pydockenv + +go 1.14 + +require ( + github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect + github.com/Microsoft/go-winio v0.4.14 // indirect + github.com/containerd/containerd v1.3.3 // indirect + github.com/docker/distribution v2.7.1+incompatible // indirect + github.com/docker/docker v1.4.2-0.20200309214505-aa6a9891b09c + github.com/docker/go-connections v0.4.0 + github.com/docker/go-units v0.4.0 // indirect + github.com/gorilla/mux v1.7.4 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0-rc1 // indirect + github.com/opencontainers/image-spec v1.0.1 // indirect + github.com/pelletier/go-toml v1.2.0 + github.com/sirupsen/logrus v1.4.1 + github.com/spf13/cobra v0.0.6 + github.com/stretchr/testify v1.4.0 // indirect + golang.org/x/net v0.0.0-20200226121028-0de0cce0169b // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + gopkg.in/yaml.v2 v2.2.4 // indirect + gotest.tools v2.2.0+incompatible // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4535796 --- /dev/null +++ b/go.sum @@ -0,0 +1,188 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/containerd/containerd v1.3.3 h1:LoIzb5y9x5l8VKAlyrbusNPXqBY0+kviRloxFUMFwKc= +github.com/containerd/containerd v1.3.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v1.4.2-0.20200309214505-aa6a9891b09c h1:zviRyz1SWO8+WVJbi9/jlJCkrsZ54r/lTRbgtcaQhLs= +github.com/docker/docker v1.4.2-0.20200309214505-aa6a9891b09c/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs= +github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd h1:/e+gpKk9r3dJobndpTytxS2gOy6m5uvpg+ISQoEcusQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/dependency/dependency.go b/internal/dependency/dependency.go new file mode 100644 index 0000000..5eca169 --- /dev/null +++ b/internal/dependency/dependency.go @@ -0,0 +1,87 @@ +package dependency + +import ( + "fmt" + + "github.com/docker/docker/api/types" + "github.com/se7entyse7en/pydockenv/internal/executor" +) + +type Requirements struct { + FileName string + Packages +} + +type Packages struct { + Dependencies map[string]string + RawDependencies []string +} + +func Install(requirements *Requirements) error { + cmd := buildInstallCmd(requirements) + err := executor.Execute(cmd, &executor.ExecOptions{}) + if err != nil { + return fmt.Errorf("cannot install requirements in container: %w", err) + } + + return nil +} + +func InstallForContainer(container types.ContainerJSON, requirements *Requirements) error { + cmd := buildInstallCmd(requirements) + err := executor.ExecuteForContainer(container, cmd, + &executor.ExecOptions{ByPassCheck: true}) + if err != nil { + return fmt.Errorf("cannot install requirements in container: %w", err) + } + + return nil +} + +func Uninstall(packages *Packages, yes bool) error { + cmd := []string{"pip", "uninstall"} + cmd = append(cmd, parsePackages(packages)...) + if yes { + cmd = append(cmd, "-y") + } + + err := executor.Execute(cmd, &executor.ExecOptions{}) + if err != nil { + return fmt.Errorf("cannot uninstall requirements in container: %w", err) + } + + return nil +} + +func ListPackage() error { + err := executor.Execute([]string{"pip", "freeze"}, &executor.ExecOptions{}) + if err != nil { + return fmt.Errorf("cannot list packages in container: %w", err) + } + + return nil +} + +func buildInstallCmd(requirements *Requirements) []string { + cmd := []string{"pip", "install"} + if requirements.FileName != "" { + cmd = append(cmd, "-r", requirements.FileName) + } else { + cmd = append(cmd, parsePackages(&requirements.Packages)...) + } + + return cmd +} + +func parsePackages(packages *Packages) []string { + if len(packages.RawDependencies) > 0 { + return packages.RawDependencies + } + + var parsedPackages []string + for p, v := range packages.Dependencies { + parsedPackages = append(parsedPackages, fmt.Sprintf("%s%s", p, v)) + } + + return parsedPackages +} diff --git a/internal/docker/client.go b/internal/docker/client.go new file mode 100644 index 0000000..9701c42 --- /dev/null +++ b/internal/docker/client.go @@ -0,0 +1,7 @@ +package docker + +import "github.com/docker/docker/client" + +func getClient() (*client.Client, error) { + return client.NewClientWithOpts(client.FromEnv) +} diff --git a/internal/environment/environment.go b/internal/environment/environment.go new file mode 100644 index 0000000..39a63d2 --- /dev/null +++ b/internal/environment/environment.go @@ -0,0 +1,340 @@ +package environment + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/pkg/term" + "github.com/se7entyse7en/pydockenv/internal/dependency" + "github.com/se7entyse7en/pydockenv/internal/utils" + "github.com/se7entyse7en/pydockenv/log" + "github.com/sirupsen/logrus" +) + +type Config struct { + Name string + Python string + ProjectDir string + Dependencies map[string]string + ContainerArgs map[string]string + Aliases map[string]struct { + Cmd string + Ports []int + } +} + +type RawConfig struct { + Tool struct { + Pydockenv *Config + } +} + +func Create(conf *Config) error { + logger := log.Logger + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return err + } + + contImg := fmt.Sprintf("python:%s", conf.Python) + images, err := listDockerImages(cli) + if err != nil { + + return fmt.Errorf("cannot list docker images: %w", err) + } + + ctxLogger := logger.WithFields(logrus.Fields{ + "image": contImg, + }) + + if _, ok := images[contImg]; !ok { + ctxLogger.Debug("Image not found, pulling...") + out, err := cli.ImagePull(context.Background(), contImg, + types.ImagePullOptions{}) + if err != nil { + return fmt.Errorf("cannot pull image: %w", err) + } + + defer out.Close() + + termFd, isTerm := term.GetFdInfo(os.Stderr) + jsonmessage.DisplayJSONMessagesStream(out, os.Stderr, termFd, isTerm, nil) + + ctxLogger.Debug("Image pulled!") + } + + netName := fmt.Sprintf("%s_%s_network", utils.RESOURCES_PREFIX, conf.Name) + ctxLogger = logger.WithFields(logrus.Fields{ + "network-name": netName, + }) + + ctxLogger.Debug("Creating network...") + _, err = cli.NetworkCreate( + context.Background(), + netName, + types.NetworkCreate{CheckDuplicate: true}, + ) + if err != nil { + return fmt.Errorf("cannot create network: %w", err) + } + + ctxLogger.Debug("Network created!") + + contName := fmt.Sprintf("%s_%s", utils.RESOURCES_PREFIX, conf.Name) + workdir, err := filepath.Abs(conf.ProjectDir) + if err != nil { + return err + } + + ctxLogger = logger.WithFields(logrus.Fields{ + "container-image": contImg, + "container-name": contName, + "workdir": workdir, + }) + ctxLogger.Debug("Creating container...") + + jsonAliases, err := json.Marshal(conf.Aliases) + if err != nil { + return err + } + + c, err := cli.ContainerCreate( + context.Background(), + &container.Config{ + Image: contImg, + Cmd: []string{"/bin/sh"}, + OpenStdin: true, + Labels: map[string]string{ + "workdir": workdir, + "env_name": conf.Name, + "aliases": string(jsonAliases), + }, + }, + &container.HostConfig{ + NetworkMode: container.NetworkMode(netName), + Mounts: []mount.Mount{ + { + Source: workdir, + Target: "/usr/src", + Type: mount.TypeBind, + }, + }, + }, + &network.NetworkingConfig{}, + contName, + ) + if err != nil { + return fmt.Errorf("cannot create container: %w", err) + } + + ctxLogger.Debug("Container created!") + + contInfo, err := cli.ContainerInspect(context.Background(), c.ID) + if err != nil { + return fmt.Errorf("cannot inspect container: %w", err) + } + + if len(conf.Dependencies) == 0 { + return nil + } + + ctxLogger.Debugf("Installing %d dependencies...", len(conf.Dependencies)) + err = cli.ContainerStart(context.Background(), contName, + types.ContainerStartOptions{}) + if err != nil { + return fmt.Errorf("cannot start container: %w", err) + } + + err = dependency.InstallForContainer(contInfo, &dependency.Requirements{ + Packages: dependency.Packages{Dependencies: conf.Dependencies}, + }) + if err != nil { + return fmt.Errorf("cannot execute command in container: %w", err) + } + + err = cli.ContainerStop(context.Background(), contName, nil) + if err != nil { + return fmt.Errorf("cannot stop container: %w", err) + } + + ctxLogger.Debug("Dependencies installed!") + + return nil +} + +func Status() error { + logger := log.Logger + envName := utils.GetCurrentEnv() + if envName == "" { + logger.Info("No active environment") + return nil + } + + logger.Infof("Active environment: %s", envName) + return nil +} + +func Activate(envName string) error { + logger := log.Logger + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return err + } + + contName := fmt.Sprintf("%s_%s", utils.RESOURCES_PREFIX, envName) + + ctxLogger := logger.WithFields(logrus.Fields{ + "container-name": contName, + }) + ctxLogger.Debug("Starting container...") + + err = cli.ContainerStart(context.Background(), contName, + types.ContainerStartOptions{}) + if err != nil { + return fmt.Errorf("cannot start container: %w", err) + } + + ctxLogger.Debug("Container started!") + + return nil +} + +func Deactivate() error { + logger := log.Logger + + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return err + } + + envName := utils.GetCurrentEnv() + contName := fmt.Sprintf("%s_%s", utils.RESOURCES_PREFIX, envName) + + ctxLogger := logger.WithFields(logrus.Fields{ + "container-name": contName, + }) + ctxLogger.Debug("Stopping container...") + + err = cli.ContainerStop(context.Background(), contName, nil) + if err != nil { + return fmt.Errorf("cannot stop container: %w", err) + } + + ctxLogger.Debug("Container stopped!") + + return nil +} + +func ListEnvironments() error { + logger := log.Logger + + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return err + } + + currentEnvName := utils.GetCurrentEnv() + containers, err := cli.ContainerList( + context.Background(), types.ContainerListOptions{All: true}) + if err != nil { + return fmt.Errorf("cannot list containers: %w", err) + } + + var msgBuilder strings.Builder + for _, c := range containers { + contName := c.Names[0] + if !strings.HasPrefix(contName, utils.RESOURCES_PREFIX) { + continue + } + + envName := contName[len(utils.RESOURCES_PREFIX):] + m := fmt.Sprintf("%s\n", envName) + if envName == currentEnvName { + m = fmt.Sprintf("* %s", m) + } + + msgBuilder.WriteString(m) + } + + msg := msgBuilder.String() + if msg == "" { + logger.Info("No environments available") + } else { + logger.Info(msg) + } + + return nil +} + +func Remove(envName string) error { + logger := log.Logger + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return err + } + + contName := fmt.Sprintf("%s_%s", utils.RESOURCES_PREFIX, envName) + + ctxLogger := logger.WithFields(logrus.Fields{ + "container-name": contName, + }) + ctxLogger.Debug("Removing container...") + + err = cli.ContainerRemove(context.Background(), contName, + types.ContainerRemoveOptions{ + RemoveVolumes: true, + Force: true, + }) + if err != nil { + return fmt.Errorf("cannot remove container: %w", err) + } + + ctxLogger.Debug("Container removed!") + + netName := fmt.Sprintf("%s_%s_network", utils.RESOURCES_PREFIX, envName) + ctxLogger = logger.WithFields(logrus.Fields{ + "network-name": netName, + }) + + ctxLogger.Debug("Removing network...") + err = cli.NetworkRemove(context.Background(), netName) + if err != nil { + return fmt.Errorf("cannot remove network: %w", err) + } + + ctxLogger.Debug("Network Removed!") + return nil +} + +func listDockerImages(cli *client.Client) (map[string]struct{}, error) { + images, err := cli.ImageList(context.Background(), + types.ImageListOptions{All: true}) + + var imagesNames map[string]struct{} + if err != nil { + return imagesNames, err + } + + imagesNames = make(map[string]struct{}) + for _, im := range images { + for _, tag := range im.RepoTags { + if tag == ":" { + continue + } + + imagesNames[tag] = struct{}{} + } + } + + return imagesNames, nil +} diff --git a/internal/executor/executor.go b/internal/executor/executor.go new file mode 100644 index 0000000..2fe8fc6 --- /dev/null +++ b/internal/executor/executor.go @@ -0,0 +1,332 @@ +package executor + +import ( + "bufio" + "context" + "encoding/binary" + "encoding/json" + "fmt" + "io" + "os" + "strings" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" + "github.com/se7entyse7en/pydockenv/internal/utils" + "github.com/se7entyse7en/pydockenv/log" + "github.com/sirupsen/logrus" +) + +type CommandAlias struct { + Cmd string + Ports []int +} + +type CommandAliases map[string]CommandAlias + +func Execute(cmd []string, options *ExecOptions) error { + logger := log.Logger + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return err + } + + envName := utils.GetCurrentEnv() + contName := fmt.Sprintf("%s_%s", utils.RESOURCES_PREFIX, envName) + ctxLogger := logger.WithFields(logrus.Fields{ + "container-name": contName, + }) + + contInfo, err := cli.ContainerInspect(context.Background(), contName) + if err != nil { + return fmt.Errorf("cannot inspect container: %w", err) + } + + if len(cmd) == 1 { + // TODO: consider also handling len(cmd) > 1, cmd[1:] could be passed + // as arguments to the alias + alias, aliasExecOptions, err := lookupAlias(cmd[0], contInfo.Config.Labels["aliases"], options) + if err == nil { + return ExecuteForContainer(contInfo, alias, aliasExecOptions) + } + + ctxLogger.Debugf("Failed looking up for alias: %w", err) + } + + return ExecuteForContainer(contInfo, cmd, options) +} + +func ExecuteForContainer(container types.ContainerJSON, cmd []string, options *ExecOptions) error { + logger := log.Logger + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return err + } + + hostBaseWd := container.Config.Labels["workdir"] + wd, err := os.Getwd() + if err != nil { + return fmt.Errorf("cannot get working directory: %w", err) + } + + if !strings.HasPrefix(wd, hostBaseWd) && !options.ByPassCheck { + return fmt.Errorf("cannot run commands outside of %s", hostBaseWd) + } + + relativeWd := wd[len(hostBaseWd):] + guestWd := fmt.Sprintf("/usr/src%s", relativeWd) + + ctxLogger := logger.WithFields(logrus.Fields{ + "command": strings.Join(cmd, " "), + }) + + ctxLogger.Debug("Running command...") + err = withPortMapper(container, options.Ports, options.Detach, func() error { + idResp, err := cli.ContainerExecCreate(context.Background(), container.ID, + types.ExecConfig{ + Tty: true, + AttachStdin: !options.Detach, + AttachStderr: true, + AttachStdout: true, + Detach: options.Detach, + Env: buildEnv(options.EnvVars), + WorkingDir: guestWd, + Cmd: cmd, + }) + if err != nil { + return fmt.Errorf("cannot create command '%s' in container '%s': %w", + cmd, container.ID, err) + } + + resp, err := cli.ContainerExecAttach(context.Background(), idResp.ID, types.ExecStartCheck{}) + if err != nil { + return fmt.Errorf("cannot execute command '%s' in container '%s': %w", + cmd, container.ID, err) + } + + defer resp.Close() + + inout := make(chan []byte) + go func() { + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + inout <- []byte(scanner.Text()) + } + + close(inout) + }() + + go func(w io.WriteCloser) { + for { + data, ok := <-inout + if !ok { + w.Close() + return + } + + w.Write(append(data, '\n')) + } + }(resp.Conn) + + for { + header := make([]byte, 8) + _, err = io.ReadFull(resp.Reader, header) + if err != nil { + if err == io.EOF { + break + } + + _, ok := <-inout + if !ok { + // For some reason when `inout` is closed the + // error is not `io.EOF`, so we check if the + // channel has been closed + return nil + } + + return err + } + + size := binary.BigEndian.Uint32(header[4:]) + body := make([]byte, size) + _, err = io.ReadFull(resp.Reader, body) + if err != nil { + return err + } + + if header[0] == 1 { + os.Stdout.Write(body) + } else { + os.Stderr.Write(body) + } + } + + return nil + }) + + if err != nil { + return err + } + + ctxLogger.Debug("Command ran") + return nil +} + +func buildEnv(envMapping map[string]string) []string { + var env []string + for k, v := range envMapping { + env = append(env, fmt.Sprintf("%s=%s", k, v)) + } + + return env +} + +func lookupAlias(cmd string, rawAliases string, options *ExecOptions) ([]string, *ExecOptions, error) { + logger := log.Logger + ctxLogger := logger.WithFields(logrus.Fields{"cmd": cmd}) + ctxLogger.Debug("Looking for aliases for command...") + if rawAliases != "" { + var parsedAliases CommandAliases + json.Unmarshal([]byte(rawAliases), &parsedAliases) + ctxLogger.Debugf("Found %d aliases", len(parsedAliases)) + if alias, ok := parsedAliases[cmd]; ok { + options.Ports = []int{} + keys := make(map[int]bool) + for _, p := range append(options.Ports, alias.Ports...) { + if _, value := keys[p]; !value { + keys[p] = true + options.Ports = append(options.Ports, p) + } + } + + return strings.Split(alias.Cmd, " "), options, nil + } + + return []string{}, nil, fmt.Errorf("No alias found for command") + } + + return []string{}, nil, fmt.Errorf("No aliases defined") +} + +func withPortMapper(cj types.ContainerJSON, ports []int, detach bool, f func() error) error { + logger := log.Logger + ctxLogger := logger.WithFields(logrus.Fields{"ports": ports}) + ctxLogger.Debug("Running port mappers...") + + containerNames, err := runPortMapper(cj, ports) + if err != nil { + return fmt.Errorf("cannot run port mapper: %w", err) + } + + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return err + } + + err = f() + if err != nil { + return err + } + + if detach { + return nil + } + + ctxLogger.Debug("Stopping port mappers...") + for _, cName := range containerNames { + err = cli.ContainerStop(context.Background(), cName, nil) + if err != nil { + return fmt.Errorf("cannot stop container: %w", err) + } + } + + return nil +} + +func runPortMapper(cj types.ContainerJSON, ports []int) ([]string, error) { + logger := log.Logger + + if len(ports) == 0 { + return []string{}, nil + } + + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return []string{}, err + } + + netName := cj.HostConfig.NetworkMode.NetworkName() + guestIp := cj.NetworkSettings.Networks[netName].IPAddress + + var containersNames []string + for _, p := range ports { + // TODO: Use a single container for all port mappings instead of + // spinning a container for each port + contName := fmt.Sprintf("%s_port_mapper_%d", cj.Name, p) + ctxLogger := logger.WithFields(logrus.Fields{ + "port": p, + "container-name": contName, + }) + _, err := cli.ContainerInspect(context.Background(), contName) + if err != nil { + cmd := fmt.Sprintf("TCP-LISTEN:1234,fork TCP-CONNECT:%s:%d", + guestIp, p) + guestPort := fmt.Sprintf("%d/tcp", p) + + ctxLogger := ctxLogger.WithFields(logrus.Fields{ + "cmd": cmd, + "guest-port": guestPort, + }) + ctxLogger.Debug("Creating port mapper container...") + _, err := cli.ContainerCreate( + context.Background(), + &container.Config{ + Image: "alpine/socat", + Cmd: strings.Split(cmd, " "), + ExposedPorts: nat.PortSet{ + "1234/tcp": struct{}{}, + }, + }, + &container.HostConfig{ + AutoRemove: true, + PortBindings: map[nat.Port][]nat.PortBinding{ + "1234/tcp": {{ + HostIP: "0.0.0.0", + HostPort: guestPort, + }}, + }, + NetworkMode: container.NetworkMode(netName), + }, + &network.NetworkingConfig{}, + contName, + ) + if err != nil { + return []string{}, fmt.Errorf("cannot create container: %w", err) + } + + ctxLogger.Debug("Port mapper container created!") + } + + ctxLogger.Debug("Running port mapper...") + err = cli.ContainerStart(context.Background(), contName, + types.ContainerStartOptions{}) + if err != nil { + return []string{}, fmt.Errorf("cannot start container: %w", err) + } + + ctxLogger.Debug("Port mapper ran!") + containersNames = append(containersNames, contName) + } + + return containersNames, nil +} + +type ExecOptions struct { + ByPassCheck bool + Detach bool + EnvVars map[string]string + Ports []int +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go new file mode 100644 index 0000000..42bcd22 --- /dev/null +++ b/internal/utils/utils.go @@ -0,0 +1,9 @@ +package utils + +import "os" + +var RESOURCES_PREFIX = "pydockenv_" + +func GetCurrentEnv() string { + return os.Getenv("PYDOCKENV") +} diff --git a/log/log.go b/log/log.go new file mode 100644 index 0000000..10e3e46 --- /dev/null +++ b/log/log.go @@ -0,0 +1,41 @@ +package log + +import ( + "fmt" + "strings" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var Logger = logrus.New() + +func SetupLogger(cmd *cobra.Command) error { + level, err := cmd.Flags().GetString("log-level") + if err != nil { + return fmt.Errorf("unrecognized log-level: %w", err) + } + + var logLevel logrus.Level + switch l := strings.ToLower(level); l { + case "trace": + logLevel = logrus.TraceLevel + case "debug": + logLevel = logrus.DebugLevel + case "info": + logLevel = logrus.InfoLevel + case "warn": + logLevel = logrus.WarnLevel + case "error": + logLevel = logrus.ErrorLevel + case "fatal": + logLevel = logrus.FatalLevel + case "panic": + logLevel = logrus.PanicLevel + default: + return fmt.Errorf("unrecognized log-level: %s.", l) + } + + Logger.SetLevel(logLevel) + return nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..7cb3eae --- /dev/null +++ b/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "github.com/se7entyse7en/pydockenv/cmd" +) + +func main() { + cmd.Execute() +} diff --git a/meta.json b/meta.json new file mode 100644 index 0000000..f81c986 --- /dev/null +++ b/meta.json @@ -0,0 +1,9 @@ +{ + "title": "pydockenv", + "version": "0.5.0", + "author": "Lou Marvin Caraig", + "author_email": "loumarvincaraig@gmail.com", + "description": "Python Virtualenv Powered by Docker", + "project_url": "https://github.com/se7entyse7en/pydockenv", + "copyright": "Copyright 2020 Lou Marvin Caraig" +} diff --git a/pydockenv/__init__.py b/pydockenv/__init__.py deleted file mode 100644 index aaa9220..0000000 --- a/pydockenv/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -__title__ = 'pydockenv' -__version__ = '0.5.0' -__author__ = 'Lou Marvin Caraig' -__author_email__ = 'loumarvincaraig@gmail.com' -__description__ = 'Python Virtualenv Powered by Docker' -__project_url__ = 'https://github.com/se7entyse7en/pydockenv' -__copyright__ = 'Copyright 2019 Lou Marvin Caraig' diff --git a/pydockenv/cli.py b/pydockenv/cli.py deleted file mode 100644 index d5f31eb..0000000 --- a/pydockenv/cli.py +++ /dev/null @@ -1,119 +0,0 @@ -import click - -from pydockenv.commands import dependency -from pydockenv.commands import environment -from pydockenv.commands import io -from pydockenv.executor import Executor - - -@click.group() -def cli(): - pass - - -@cli.command() -@click.argument('project_dir') -@click.option('--file', 'file_', help='Toml file') -@click.option('--name', help='Name of the environment') -@click.option('--version', help='Python version') -def create(project_dir, file_, name, version): - environment.create(project_dir, file_, name, version) - - -@cli.command() -def status(): - environment.status() - - -@cli.command() -@click.argument('name') -def activate(name): - environment.activate(name) - - -@cli.command() -def deactivate(): - environment.deactivate() - - -@cli.command() -@click.argument('name') -def remove(name): - environment.remove(name) - - -@cli.command() -def list_environments(): - environment.list_environments() - - -@cli.command() -@click.argument('package', required=False) -@click.option('-f', '--file', 'requirements_file', - help='File to containing the requirements to install') -def install(package, requirements_file): - dependency.install(package, requirements_file) - - -@cli.command() -@click.argument('package') -@click.option('-y', '--yes', is_flag=True) -def uninstall(package, yes): - dependency.uninstall(package, yes) - - -@cli.command() -def list_packages(): - dependency.list_packages() - - -@cli.command() -@click.argument('name') -@click.argument('project_dir') -@click.argument('input_file') -def load(name, project_dir, input_file): - io.load(name, project_dir, input_file) - - -@cli.command() -@click.option('--output', help='Name of the output file') -def save(name, output): - io.save(name, output) - - -@cli.command() -@click.option('--output', help='Name of the output file') -def export(output): - io.export(output) - - -@cli.command() -@click.argument('args', nargs=-1) -def shell(args): - click.echo('Running...') - try: - Executor.execute('python', *args) - finally: - click.echo('Exited!') - - -@cli.command() -@click.argument('cmd') -@click.argument('args', nargs=-1) -@click.option('-d', '--detach', is_flag=True) -@click.option('-e', '--env-var', multiple=True, - help='Environment variable to set') -@click.option('-p', '--port', multiple=True, - help='Port to reach') -def run(cmd, args, detach, env_var, port): - click.echo('Running...') - env_vars = dict(e.split('=') for e in env_var) - try: - Executor.execute(cmd, *args, detach=detach, - env_vars=env_vars, ports=list(port)) - finally: - click.echo('Exited!') - - -if __name__ == '__main__': - cli() diff --git a/pydockenv/client.py b/pydockenv/client.py deleted file mode 100644 index b32e7ff..0000000 --- a/pydockenv/client.py +++ /dev/null @@ -1,13 +0,0 @@ -import docker - - -class Client: - - _instance = None - - @classmethod - def get_instance(cls): - if cls._instance is None: - cls._instance = docker.from_env() - - return cls._instance diff --git a/pydockenv/commands/__init__.py b/pydockenv/commands/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pydockenv/commands/dependency.py b/pydockenv/commands/dependency.py deleted file mode 100644 index 522610c..0000000 --- a/pydockenv/commands/dependency.py +++ /dev/null @@ -1,48 +0,0 @@ -import click - -from pydockenv.executor import Executor - - -def _build_install_args(packages, requirements_file): - if not isinstance(packages, list): - packages = [packages] - - args = ['pip', 'install'] - if requirements_file: - args.extend(['-r', requirements_file]) - else: - args.extend(packages) - - return args - - -def install(packages, requirements_file): - return Executor.execute( - *_build_install_args(packages, requirements_file)) - - -def install_for_container(container, packages, requirements_file): - return Executor.execute_for_container( - container, *_build_install_args(packages, requirements_file), - bypass_check=True) - - -def uninstall(package, yes): - click.echo('Running...') - args = ['pip', 'uninstall'] - if yes: - args.append('-y') - - args.append(package) - try: - Executor.execute(*args) - finally: - click.echo('Exited!') - - -def list_packages(): - click.echo('Running...') - try: - Executor.execute('pip', 'freeze') - finally: - click.echo('Exited!') diff --git a/pydockenv/commands/environment.py b/pydockenv/commands/environment.py deleted file mode 100644 index d157128..0000000 --- a/pydockenv/commands/environment.py +++ /dev/null @@ -1,188 +0,0 @@ -import json -import os -from dataclasses import dataclass -from dataclasses import field -from typing import Dict - -import click -import toml - -import docker -from docker.types import Mount - -from pydockenv import definitions -from pydockenv.client import Client - - -def get_current_env(): - return os.environ.get('PYDOCKENV') - - -@dataclass(frozen=True) -class EnvironmentConfig: - name: str - python: str = 'latest' - dependencies: Dict[str, str] = field(default_factory=dict) - container_args: Dict[str, str] = field(default_factory=dict) - aliases: Dict[str, Dict[str, str]] = field(default_factory=dict) - - @classmethod - def from_file(cls, file_: str) -> 'EnvironmentConfig': - config = toml.load(file_)['tool']['pydockenv'] - return EnvironmentConfig(**config) - - -def create(project_dir, file_, name, version): - if file_: - config = EnvironmentConfig.from_file(file_) - else: - config = EnvironmentConfig(name, python=version or 'latest') - - create_from_config(project_dir, config) - - -def create_from_config(project_dir: str, config: EnvironmentConfig): - click.echo(f'Creating environment {config.name} with python version ' - f'{config.python}...') - image_name = f'python:{config.python}' - - client = Client.get_instance() - try: - image = client.images.get(image_name) - except docker.errors.ImageNotFound: - click.echo(f'Image {image_name} not found, pulling...') - image = client.images.pull('python', tag=config.python) - - create_network(config.name) - create_env(image, project_dir, config) - - click.echo(f'Environment {config.name} with python version ' - f'{config.python} created!') - - -def status(): - current_env = get_current_env() - if not current_env: - click.echo('No active environment') - else: - click.echo(f'Active environment: {current_env}') - - -def activate(name): - click.echo('Activating environment...') - try: - container = Client.get_instance().containers.get( - definitions.CONTAINERS_PREFIX + name) - except docker.errors.NotFound: - click.echo(f'Environment {name} not found, exiting...') - else: - container.start() - click.echo('Environment activated!') - - -def deactivate(): - click.echo('Deactivating current environment...') - current_env = get_current_env() - try: - container = Client.get_instance().containers.get( - definitions.CONTAINERS_PREFIX + current_env) - except docker.errors.ImageNotFound: - click.echo(f'Environment {current_env} not found, exiting...') - else: - container.stop() - click.echo('Environment deactivated!') - - -def remove(name): - click.echo(f'Removing environment {name}...') - try: - container = Client.get_instance().containers.get( - definitions.CONTAINERS_PREFIX + name) - except docker.errors.NotFound: - click.echo(f'Environment {name} not found, exiting...') - raise - - kwargs = { - 'force': True, - } - container.remove(**kwargs) - delete_network(name) - click.echo(f'Environment {name} removed!') - - -def list_environments(): - click.echo(f'Listing environments...') - kwargs = { - 'all': True, - } - containers = Client.get_instance().containers.list(kwargs) - - current_env = get_current_env() - envs = [] - for c in containers: - if not c.name.startswith(definitions.CONTAINERS_PREFIX): - continue - - env_name = c.name[len(definitions.CONTAINERS_PREFIX):] - prefix = '* ' if env_name == current_env else ' ' - envs.append(f'{prefix}{env_name}') - - click.echo('\n'.join(envs)) - click.echo(f'Environments listed!') - - -def create_network(env_name): - network_name = definitions.CONTAINERS_PREFIX + env_name + '_network' - Client.get_instance().networks.create(network_name, check_duplicate=True) - - -def delete_network(env_name): - network_name = definitions.CONTAINERS_PREFIX + env_name + '_network' - try: - network = Client.get_instance().networks.get(network_name) - except docker.errors.ImageNotFound: - click.echo(f'Network {network_name} not found, exiting...') - raise - - for c in network.containers: - network.disconnect(c) - - network.remove() - - -def create_env(image, project_dir, config): - workdir = os.path.abspath(project_dir) - mounts = [ - Mount('/usr/src', workdir, type='bind') - ] - kwargs = { - 'command': '/bin/sh', - 'stdin_open': True, - 'labels': { - 'workdir': workdir, - 'env_name': config.name, - 'aliases': json.dumps(config.aliases), - }, - 'name': definitions.CONTAINERS_PREFIX + config.name, - 'mounts': mounts, - 'network': definitions.CONTAINERS_PREFIX + config.name + '_network', - } - - filtered_container_args = {k: v for k, v in config.container_args.items() - if k not in kwargs} - kwargs.update(filtered_container_args) - - container = Client.get_instance().containers.create(image, **kwargs) - - if config.dependencies: - # TODO: Remove this from here just to avoid circular imports - from pydockenv.commands import dependency - - container.start() - - click.echo(f'Installing {len(config.dependencies)} dependencies...') - packages = [f'{dep}{v}' for dep, v in config.dependencies.items()] - click.echo(f'Installing {packages}...') - dependency.install_for_container(container, packages, None) - - container.stop() diff --git a/pydockenv/commands/io.py b/pydockenv/commands/io.py deleted file mode 100644 index f9e3db6..0000000 --- a/pydockenv/commands/io.py +++ /dev/null @@ -1,130 +0,0 @@ -import subprocess - -import click -import toml - -import docker - -from pydockenv import definitions -from pydockenv.client import Client -from pydockenv.commands.environment import EnvironmentConfig -from pydockenv.commands.environment import create_env -from pydockenv.commands.environment import create_network -from pydockenv.commands.environment import get_current_env -from pydockenv.executor import Executor - - -def load(name, project_dir, input_file): - click.echo(f'Loading environment {name} from {input_file}...') - with open(input_file, 'rb') as fin: - image = Client.get_instance().images.load(fin)[0] - - create_network(name) - config = EnvironmentConfig(name) - create_env(image, project_dir, config) - - click.echo(f'Environment {name} loaded from {input_file}!') - - -def save(name, output): - current_env = get_current_env() - - click.echo(f'Saving environment {current_env}...') - - image_name = _commit(name, current_env) - _export(image_name, output) - - click.echo(f'Removing image {image_name}...') - Client.get_instance().images.remove(image_name) - click.echo(f'Image {image_name} removed') - - -def export(output): - client = Client.get_instance() - current_env = get_current_env() - try: - container = client.containers.get( - definitions.CONTAINERS_PREFIX + current_env) - except docker.errors.NotFound: - click.echo(f'Container {current_env} not found, exiting...') - raise - - click.echo(f'Exporting environment {current_env}...') - - out = Executor.execute_for_container( - container, 'pip', 'freeze', subprocess_kwargs={ - 'stdout': subprocess.PIPE, 'stderr': subprocess.PIPE - }) - - deps = { - k: f'=={v}' for k, v in ( - r.split('==') for r in out.stdout.decode('utf8').splitlines() - ) - } - - # TODO: this is a hacky way to get the python version. One way to achieve - # this could be to add a label to the initial image. But this requires - # rebuilding the image with the new label as it's not possible to add a - # label to an already built image. - out = Executor.execute_for_container( - container, 'python', '--version', subprocess_kwargs={ - 'stdout': subprocess.PIPE, 'stderr': subprocess.PIPE - }) - - python_version = out.stdout.strip().decode('utf8').split(' ')[1] - - toml_doc = { - 'tool': { - 'pydockenv': { - 'name': current_env, - 'python': python_version, - 'dependencies': deps - } - } - } - - if not output: - click.echo(toml.dumps(toml_doc)) - else: - with open(output, 'w') as fout: - toml.dump(toml_doc, fout) - - click.echo(f'Environment {current_env} exported!') - - -def _commit(name, current_env): - click.echo(f'Saving environment {current_env} as image...') - try: - container = Client.get_instance().containers.get( - definitions.CONTAINERS_PREFIX + current_env) - except docker.errors.ImageNotFound: - click.echo(f'Container {current_env} not found, exiting...') - raise - - if not name: - repository = f'{definitions.CONTAINERS_PREFIX + current_env}' - tag = 'latest' - else: - repository, tag = name.split(':') - - container.commit(repository=repository, tag=tag) - - image_name = f'{repository}:{tag}' - click.echo(f'Environment {current_env} saved as image {image_name}!') - return image_name - - -def _export(image_name, output): - click.echo(f'Saving image {image_name} to {output}...') - - try: - image = Client.get_instance().images.get(image_name) - except docker.errors.ImageNotFound: - raise - - output = output or f'{image_name}.tar.gz' - with open(output, 'wb') as fout: - for chunk in image.save(named=True): - fout.write(chunk) - - click.echo(f'Image {image_name} saved to {output}!') diff --git a/pydockenv/definitions.py b/pydockenv/definitions.py deleted file mode 100644 index 5a71c1b..0000000 --- a/pydockenv/definitions.py +++ /dev/null @@ -1,7 +0,0 @@ -import os - -import pydockenv - - -ROOT_DIR = os.path.dirname(os.path.dirname(pydockenv.__file__)) -CONTAINERS_PREFIX = 'pydockenv_' diff --git a/pydockenv/executor.py b/pydockenv/executor.py deleted file mode 100644 index b81fe35..0000000 --- a/pydockenv/executor.py +++ /dev/null @@ -1,130 +0,0 @@ -import json -import os -import subprocess -from contextlib import contextmanager -from itertools import chain - -import click - -import docker - -from pydockenv import definitions -from pydockenv.client import Client -from pydockenv.commands.environment import get_current_env - - -class Executor: - - @classmethod - def execute_for_container(cls, container, *args, **kwargs): - env_name = container.labels['env_name'] - host_base_wd = container.labels['workdir'] - current_wd = os.getcwd() - if ( - not current_wd.startswith(host_base_wd) and - not kwargs.get('bypass_check') - ): - raise RuntimeError( - f'Cannot run commands outside of {host_base_wd}') - - relative_wd = current_wd[len(host_base_wd):] - guest_wd = f'/usr/src{relative_wd}' - - detach = kwargs.get('detach') - env_vars = cls._build_env_vars(kwargs.get('env_vars')) - with cls._with_mapped_ports(container, kwargs.get('ports'), detach): - # This cannot be done with docker python sdk - cmd = ['docker', 'exec', '-w', guest_wd] - if detach: - cmd.append('-d') - else: - cmd.extend(['-i', '-t']) - - cmd = ( - cmd + env_vars + - [(definitions.CONTAINERS_PREFIX + env_name)] + - list(args) - ) - - return subprocess.run(cmd, **kwargs.get('subprocess_kwargs', {})) - - @classmethod - def execute(cls, *args, **kwargs): - client = Client.get_instance() - current_env = get_current_env() - try: - container = client.containers.get( - definitions.CONTAINERS_PREFIX + current_env) - except docker.errors.NotFound: - click.echo(f'Container {current_env} not found, exiting...') - raise - - if len(args) == 1: - aliases = container.labels.get('aliases') - if aliases: - alias = json.loads(aliases).get(args[0]) - if alias is not None: - kwargs['ports'] = alias.get('ports', []) - args = alias['cmd'].split(' ') - - return cls.execute_for_container(container, *args, **kwargs) - - @classmethod - def _build_env_vars(cls, env_vars): - if env_vars: - return list(chain.from_iterable([ - ['-e', f'{k}={v}']for k, v in env_vars.items() - ])) - - return [] - - @classmethod - @contextmanager - def _with_mapped_ports(cls, container, ports, detach): - if ports: - port_mappers_containers_names = cls._run_port_mapper( - container, ports) - else: - port_mappers_containers_names = [] - - yield - - if detach: - return - - for container_name in port_mappers_containers_names: - container = Client.get_instance().containers.get(container_name) - container.stop() - - @classmethod - def _run_port_mapper(cls, container, ports): - network_name = f'{container.name}_network' - guest_ip = container.attrs['NetworkSettings']['Networks'][ - network_name]['IPAddress'] - containers_names = [] - for port in ports: - # TODO: Use a single container for all port mappings instead of - # spinning a container for each port - name = f'{container.name}_port_mapper_{port}' - client = Client.get_instance() - - try: - container = client.containers.get(name) - except docker.errors.NotFound: - cmd = f'TCP-LISTEN:1234,fork TCP-CONNECT:{guest_ip}:{port}' - kwargs = { - 'command': cmd, - 'ports': {'1234': f'{port}/tcp'}, - 'name': name, - 'detach': True, - 'auto_remove': True, - 'network': network_name, - } - - client.containers.run('alpine/socat', **kwargs) - else: - container.start() - - containers_names.append(name) - - return containers_names diff --git a/requirements-dev.txt b/requirements-dev.txt index 338fa50..7312493 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,5 +2,3 @@ bumpversion==0.5.3 isort==4.3.16 flake8==3.7.7 twine==1.13.0 -tox==3.9.0 -requests==2.21.0 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 33ffdf5..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -Click==7.0 -docker==3.7.0 -toml==0.10.0 diff --git a/setup.py b/setup.py index 8ecc8c6..6a17ce8 100644 --- a/setup.py +++ b/setup.py @@ -1,18 +1,7 @@ -import re -import sys -from functools import partial +import json import setuptools - - -requires = [ - 'Click>=7.0,<8.0', - 'docker>=3.7.0,<3.8.0', -] - - -if sys.version_info.minor < 7: - requires.append('dataclasses==0.6') +from wheel.bdist_wheel import bdist_wheel as _bdist_wheel scripts = [ @@ -26,35 +15,31 @@ 'License :: OSI Approved :: Apache Software License', 'Operating System :: Unix', 'Programming Language :: Unix Shell', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 3', 'Topic :: Software Development', - 'Topic :: Utilities' + 'Topic :: Utilities', ] -with open('README.md', 'r') as fh: - long_description = fh.read() +with open('README.md', 'r') as fin: + long_description = fin.read() + +with open('meta.json') as fin: + about = json.load(fin) -def get_about(): - regexes = { - 'title': r"^__title__\s=\s(?P['])(?P\w*)(?P=quote)$", - 'version': r"^__version__\s=\s(?P<quote>['])(?P<version>[\d\.]*)(?P=quote)$", - 'author': r"^__author__\s=\s(?P<quote>['])(?P<author>[\w\s]*)(?P=quote)$", - 'author_email': r"^__author_email__\s=\s(?P<quote>['])(?P<author_email>.*)(?P=quote)$", - 'description': r"^__description__\s=\s(?P<quote>['])(?P<description>.*)(?P=quote)$", - 'project_url': r"^__project_url__\s=\s(?P<quote>['])(?P<project_url>.*)(?P=quote)$", - } - with open('./pydockenv/__init__.py') as f: - raw_about = f.read() +class bdist_wheel(_bdist_wheel): - extract = partial(re.search, string=raw_about, flags=re.MULTILINE) - return {k: extract(v).group(k) for k, v in regexes.items()} + def finalize_options(self): + _bdist_wheel.finalize_options(self) + self.root_is_pure = False + def get_tag(self): + if not self.plat_name_supplied: + raise ValueError('plat_name is required') -about = get_about() + return 'py2.py3', 'none', self.plat_name setuptools.setup( @@ -66,10 +51,15 @@ def get_about(): long_description=long_description, long_description_content_type='text/markdown', url=about['project_url'], - packages=setuptools.find_packages(exclude=['tests']), + packages=setuptools.find_packages(), scripts=scripts, - package_data={'': ['LICENSE']}, + package_data={ + '': ['LICENSE'], + }, + data_files=[('bin', ['bin/pydockenv_exec'])], include_package_data=True, - install_requires=requires, classifiers=classifiers, + cmdclass={ + 'bdist_wheel': bdist_wheel, + }, ) diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/base.py b/tests/base.py deleted file mode 100644 index de24243..0000000 --- a/tests/base.py +++ /dev/null @@ -1,77 +0,0 @@ -import os -import shutil -import unittest -from pathlib import Path - -import docker - -from pydockenv import definitions -from pydockenv.client import Client -from pydockenv.commands.environment import delete_network -from tests.commander import Commander - - -class BaseIntegrationTest(unittest.TestCase): - - ENV_SUFFIX = '__test-{index}' - - @classmethod - def setUpClass(cls): - cls._client = docker.from_env() - cls._low_level_client = docker.APIClient() - - @classmethod - def tearDownClass(cls): - cls._client.close() - cls._low_level_client.close() - - def setUp(self): - self._cwd = os.getcwd() - self._test_dir = Path(definitions.ROOT_DIR, '.test-dir') - self._projs_dir = Path(str(self._test_dir), 'projs') - - self._commander = Commander() - - self._env_index = 1 - os.makedirs(str(self._projs_dir)) - - def tearDown(self): - os.chdir(self._cwd) - try: - for i in range(1, self._env_index): - env_name = self._create_env_name(i) - try: - Client.get_instance().containers.get( - definitions.CONTAINERS_PREFIX + env_name).remove( - force=True) - delete_network(env_name) - except docker.errors.NotFound: - pass - - self._remove_port_mappers(env_name) - finally: - shutil.rmtree(self._test_dir.name) - - def assertCommandOk(self, command_out): - self.assertEqual(command_out.returncode, 0, - msg=command_out.stderr.decode('utf8')) - - def _remove_port_mappers(self, env_name): - prefix = definitions.CONTAINERS_PREFIX + env_name + '_port_mapper_' - for c in Client.get_instance().containers.list(all=True): - if c.name.startswith(prefix): - c.remove(force=True) - - def _env_name(self): - env_name = self._create_env_name(self._env_index) - self._env_index += 1 - return env_name - - def _create_env_name(self, index): - suffix = self.ENV_SUFFIX.format(index=index) - return f'env{suffix}' - - def _create_project_dir(self, proj_name): - proj_dir = Path(str(self._projs_dir), proj_name) - os.makedirs(str(proj_dir)) - return proj_dir diff --git a/tests/commander.py b/tests/commander.py deleted file mode 100644 index 5d8f478..0000000 --- a/tests/commander.py +++ /dev/null @@ -1,97 +0,0 @@ -import os -import subprocess -from contextlib import contextmanager -from pathlib import Path - -from pydockenv import definitions - - -BIN_PATH = str(Path(definitions.ROOT_DIR, 'bin', 'pydockenv')) - - -class Commander: - - _instance = None - - def __init__(self, env=None): - self._bin_path = BIN_PATH - self._env = env or {} - - @classmethod - def get_instance(cls): - if cls._instance is None: - cls._instance = Commander() - - return cls._instance - - def add_env_var(self, k, v): - self._env[k] = v - - def run(self, cmd, env=None): - args = cmd.split(' ') - - env = self._prepare_env(env) - - return subprocess.run( - [self._bin_path, *args], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - env=env - ) - - @contextmanager - def active_env(self, env_name): - env_diff = self.activate_env(env_name) - env = os.environ.copy() - env.update({k: v[1] for k, v in env_diff.items()}) - - try: - yield env - finally: - self.deactivate_env(env=env) - - def activate_env(self, env_name, env=None): - return self.source(f'activate {env_name}', env=env) - - def deactivate_env(self, env=None): - return self.source('deactivate', env=env) - - def source(self, cmd, env=None): - env = self._prepare_env(env) - - proc = subprocess.Popen('env', stdout=subprocess.PIPE, shell=True, - env=env) - initial_env = self._get_env(proc.stdout) - proc.communicate() - - command = f"bash -c 'PYDOCKENV_DEBUG=1 source {self._bin_path} {cmd}'" - proc = subprocess.Popen(command, stdout=subprocess.DEVNULL, - stderr=subprocess.PIPE, shell=True, env=env) - post_env = self._get_env(proc.stderr) - proc.communicate() - - env_diff = {} - for k in set().union(initial_env.keys(), post_env.keys()): - initial_value, post_value = initial_env.get(k), post_env.get(k) - if initial_value != post_value: - env_diff[k] = (initial_value, post_value) - - return env_diff - - def _get_env(self, stdout): - env = {} - for line in stdout: - (key, _, value) = line.decode('utf8').strip().partition("=") - env[key] = value - - return env - - def _prepare_env(self, env): - env = {**self._env, **(env or {})} - env['PYTHONPATH'] = definitions.ROOT_DIR - if env: - env = {k: v for k, v in {**os.environ, **env}.items() - if v is not None} - else: - env = None - - return env diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/test_dependency.py b/tests/integration/test_dependency.py deleted file mode 100644 index 05b7738..0000000 --- a/tests/integration/test_dependency.py +++ /dev/null @@ -1,36 +0,0 @@ -import os - -from tests.base import BaseIntegrationTest - - -class TestIntegrationDependencyCommands(BaseIntegrationTest): - - def test_deps_handling(self): - env_name = self._env_name() - proj_name, py_version = 'test-proj', '3.7' - - proj_dir = self._create_project_dir(proj_name) - out = self._commander.run( - f'create --name={env_name} --version={py_version} {str(proj_dir)}') - self.assertCommandOk(out) - - with self._commander.active_env(env_name) as env: - os.chdir(proj_dir) - - out = self._commander.run('list-packages', env=env) - self.assertCommandOk(out) - self.assertNotIn(f'pydockenv', out.stdout.decode('utf8')) - - out = self._commander.run('install pydockenv', env=env) - self.assertCommandOk(out) - - out = self._commander.run('list-packages', env=env) - self.assertCommandOk(out) - self.assertIn(f'pydockenv', out.stdout.decode('utf8')) - - out = self._commander.run('uninstall -y pydockenv ', env=env) - self.assertCommandOk(out) - - out = self._commander.run('list-packages', env=env) - self.assertCommandOk(out) - self.assertNotIn(f'pydockenv', out.stdout.decode('utf8')) diff --git a/tests/integration/test_environment.py b/tests/integration/test_environment.py deleted file mode 100644 index 8f47e40..0000000 --- a/tests/integration/test_environment.py +++ /dev/null @@ -1,184 +0,0 @@ -from pathlib import Path - -import docker - -from pydockenv import definitions -from tests.base import BaseIntegrationTest - - -class TestIntegrationEnvironmentCommands(BaseIntegrationTest): - - def test_create(self): - env_name = self._env_name() - proj_name, py_version = 'test-proj', '3.7' - - cont_name = definitions.CONTAINERS_PREFIX + env_name - - with self.assertRaises(docker.errors.NotFound): - self._client.containers.get(cont_name) - - proj_dir = self._create_project_dir(proj_name) - out = self._commander.run( - f'create --name={env_name} --version={py_version} {str(proj_dir)}') - self.assertCommandOk(out) - - expected = (f'Environment {env_name} with python version ' - f'{py_version} created!') - self.assertIn(expected, out.stdout.decode('utf8')) - - r = self._client.containers.get(cont_name) - self.assertEqual(r.status, 'created') - - r = self._low_level_client.inspect_container(cont_name) - self.assertEqual(len(r['Mounts']), 1) - - expected = { - 'Destination': '/usr/src', - 'Mode': '', - 'Propagation': 'rprivate', - 'RW': True, - 'Source': str(proj_dir.absolute()), - 'Type': 'bind' - } - actual = r['Mounts'][0] - self.assertEqual(expected, actual) - - expected = {f'{cont_name}_network'} - actual = set(r['NetworkSettings']['Networks'].keys()) - self.assertEqual(expected, actual) - - expected = { - 'env_name': env_name, - 'workdir': str(Path(self._projs_dir, proj_name)), - 'aliases': '{}', - } - actual = r['Config']['Labels'] - self.assertEqual(expected, actual) - - def test_remove(self): - env_name = self._env_name() - proj_name, py_version = 'test-proj', '3.7' - - cont_name = definitions.CONTAINERS_PREFIX + env_name - - with self.assertRaises(docker.errors.NotFound): - self._client.containers.get(cont_name) - - proj_dir = self._create_project_dir(proj_name) - out = self._commander.run( - f'create --name={env_name} --version={py_version} {str(proj_dir)}') - self.assertCommandOk(out) - - r = self._client.containers.get(cont_name) - self.assertEqual(r.status, 'created') - - out = self._commander.run(f'remove {env_name}') - self.assertCommandOk(out) - - with self.assertRaises(docker.errors.NotFound): - self._client.containers.get(cont_name) - - with self.assertRaises(docker.errors.NotFound): - self._client.networks.get(f'{cont_name}_network') - - def test_activate(self): - env_name = self._env_name() - proj_name, py_version = 'test-proj', '3.7' - proj_dir = self._create_project_dir(proj_name) - - out = self._commander.run( - f'create --name={env_name} --version={py_version} {str(proj_dir)}') - self.assertCommandOk(out) - env_diff = self._commander.activate_env(f'{env_name}') - - self.assertTrue({'PYDOCKENV', 'PYDOCKENV_DEBUG', 'PS1', 'SHLVL'} <= - set(env_diff.keys())) - self.assertEqual(env_diff['PYDOCKENV'][1], env_name) - self.assertEqual(env_diff['PYDOCKENV_DEBUG'][1], '1') - self.assertEqual(env_diff['PS1'][1], f'({env_name})') - self.assertEqual(int(env_diff['SHLVL'][1]), - int(env_diff['SHLVL'][0] or 0) + 1) - - def test_deactivate(self): - env_name = self._env_name() - proj_name, py_version = 'test-proj', '3.7' - proj_dir = self._create_project_dir(proj_name) - - out = self._commander.run( - f'create --name={env_name} --version={py_version} {str(proj_dir)}') - self.assertCommandOk(out) - - with self._commander.active_env(env_name) as env: - env_diff_post_deactivate = self._commander.deactivate_env(env=env) - - self.assertEqual({'PYDOCKENV', 'PS1', 'SHLVL'}, - env_diff_post_deactivate.keys()) - self.assertEqual(env_diff_post_deactivate['PYDOCKENV'][1], '') - self.assertEqual(env_diff_post_deactivate['PS1'][1], '') - self.assertEqual(int(env_diff_post_deactivate['SHLVL'][1]), - int(env_diff_post_deactivate['SHLVL'][0]) + 1) - - def test_list_environments(self): - out = self._commander.run('list-environments') - self.assertCommandOk(out) - - stdout_lines = out.stdout.decode('utf8').split('\n') - initial_envs = set([s.strip() for s in stdout_lines if s][1:-1]) - - data = [ - { - 'env_name': self._env_name(), - 'proj_name': 'test-proj-1', - 'v': '3.7', - }, - { - 'env_name': self._env_name(), - 'proj_name': 'test-proj-2', - 'v': '3.6', - }, - { - 'env_name': self._env_name(), - 'proj_name': 'test-proj-3', - 'v': '2.7', - }, - ] - - for d in data: - with self.assertRaises(docker.errors.NotFound): - self._client.containers.get( - definitions.CONTAINERS_PREFIX + d['env_name']) - - proj_dir = self._create_project_dir(d['proj_name']) - out = self._commander.run( - f"create --name={d['env_name']} --version={d['v']} " - f"{str(proj_dir)}" - ) - self.assertCommandOk(out) - - out = self._commander.run('list-environments') - self.assertCommandOk(out) - - stdout_lines = out.stdout.decode('utf8').split('\n') - envs = set([s.strip() for s in stdout_lines if s][1:-1]) - - self.assertEqual(envs - initial_envs, {d['env_name'] for d in data}) - - def test_status(self): - env_name = self._env_name() - proj_name, py_version = 'test-proj', '3.7' - proj_dir = self._create_project_dir(proj_name) - - out = self._commander.run( - f'create --name={env_name} --version={py_version} {str(proj_dir)}') - self.assertCommandOk(out) - - out = self._commander.run('status') - self.assertCommandOk(out) - self.assertEqual(out.stdout.decode('utf8').strip(), - 'No active environment') - - with self._commander.active_env(env_name) as env: - out = self._commander.run('status', env=env) - self.assertCommandOk(out) - self.assertEqual(out.stdout.decode('utf8').strip(), - f'Active environment: {env_name}') diff --git a/tests/integration/test_others.py b/tests/integration/test_others.py deleted file mode 100644 index b40709a..0000000 --- a/tests/integration/test_others.py +++ /dev/null @@ -1,41 +0,0 @@ -import os - -from pydockenv import definitions -from tests.base import BaseIntegrationTest - - -class TestIntegrationOtherCommands(BaseIntegrationTest): - - def test_run(self): - data = [ - { - 'env_name': self._env_name(), - 'proj_name': 'test-proj-1', - 'v': '3.7', - }, - { - 'env_name': self._env_name(), - 'proj_name': 'test-proj-2', - 'v': '3.6', - }, - { - 'env_name': self._env_name(), - 'proj_name': 'test-proj-3', - 'v': '2.7', - }, - ] - - for d in data: - proj_dir = self._create_project_dir(d['proj_name']) - out = self._commander.run( - f"create --name={d['env_name']} --version={d['v']} " - f"{str(proj_dir)}" - ) - self.assertCommandOk(out) - with self._commander.active_env(d['env_name']) as env: - os.chdir(proj_dir) - out = self._commander.run('run -- python --version', env=env) - self.assertCommandOk(out) - self.assertIn(f"Python {d['v']}", out.stdout.decode('utf8')) - - os.chdir(definitions.ROOT_DIR) diff --git a/tests/integration/test_port_mapper.py b/tests/integration/test_port_mapper.py deleted file mode 100644 index 8de65f4..0000000 --- a/tests/integration/test_port_mapper.py +++ /dev/null @@ -1,76 +0,0 @@ -import os - -import requests -from requests.adapters import HTTPAdapter -from requests.packages.urllib3.util.retry import Retry - -import docker - -from pydockenv import definitions -from pydockenv.client import Client -from tests.base import BaseIntegrationTest - - -class TestIntegrationPortMapperCommands(BaseIntegrationTest): - - def assertPortMapperExists(self, env_name, port): - port_mapper_container_name = ( - definitions.CONTAINERS_PREFIX + env_name + f'_port_mapper_{port}' - ) - - try: - Client.get_instance().containers.get( - port_mapper_container_name) - except docker.errors.NotFound: - self.fail( - f'Cannot find port mapper for environment {env_name} with' - f'port {port}' - ) - - def test_port_mapping_single_port(self): - env_name = self._env_name() - proj_name, py_version = 'test-proj', '3.7' - - proj_dir = self._create_project_dir(proj_name) - self._commander.run( - f'create --name={env_name} --version={py_version} {str(proj_dir)}') - with self._commander.active_env(env_name) as env: - os.chdir(proj_dir) - - port = 8000 - out = self._commander.run( - f'run -d -p {port} -- python -m http.server {port}', env=env) - self.assertCommandOk(out) - self.assertPortMapperExists(env_name, port) - - s = requests.Session() - s.mount('http://', HTTPAdapter( - max_retries=Retry(connect=3, backoff_factor=1))) - r = s.get(f'http://localhost:{port}') - - self.assertEqual(r.status_code, 200, msg=r.content) - - def test_port_mapping_multi_ports(self): - env_name = self._env_name() - proj_name, py_version = 'test-proj', '3.7' - - proj_dir = self._create_project_dir(proj_name) - self._commander.run( - f'create --name={env_name} --version={py_version} {str(proj_dir)}') - with self._commander.active_env(env_name) as env: - os.chdir(proj_dir) - - for port in range(8000, 8003): - out = self._commander.run( - f'run -d -p {port} -- python -m http.server {port}', - env=env - ) - self.assertCommandOk(out) - self.assertPortMapperExists(env_name, port) - - s = requests.Session() - s.mount('http://', HTTPAdapter( - max_retries=Retry(connect=3, backoff_factor=1))) - r = s.get(f'http://localhost:{port}') - - self.assertEqual(r.status_code, 200, msg=r.content) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 778f9d7..0000000 --- a/tox.ini +++ /dev/null @@ -1,10 +0,0 @@ -[tox] -envlist = py36,py37 -skipsdist = True -skip_missing_interpreters=true - -[testenv] -deps = - -rrequirements.txt - -rrequirements-dev.txt -commands = python -m unittest discover \ No newline at end of file