├── .gitignore ├── README.md ├── data.py ├── jeeves.py ├── pyproject.toml └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/python 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 4 | 5 | ### Python ### 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | cover/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | .pybuilder/ 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | # For a library or package, you might want to ignore these files since the code is 92 | # intended to run in multiple environments; otherwise, check them in: 93 | # .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 103 | __pypackages__/ 104 | 105 | # Celery stuff 106 | celerybeat-schedule 107 | celerybeat.pid 108 | 109 | # SageMath parsed files 110 | *.sage.py 111 | 112 | # Environments 113 | .env 114 | .venv 115 | env/ 116 | venv/ 117 | ENV/ 118 | env.bak/ 119 | venv.bak/ 120 | 121 | # Spyder project settings 122 | .spyderproject 123 | .spyproject 124 | 125 | # Rope project settings 126 | .ropeproject 127 | 128 | # mkdocs documentation 129 | /site 130 | 131 | # mypy 132 | .mypy_cache/ 133 | .dmypy.json 134 | dmypy.json 135 | 136 | # Pyre type checker 137 | .pyre/ 138 | 139 | # pytype static type analyzer 140 | .pytype/ 141 | 142 | # Cython debug symbols 143 | cython_debug/ 144 | 145 | # End of https://www.toptal.com/developers/gitignore/api/python -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jeeves 2 | 3 | Jeeves is a CLI tool for running development dependencies such as MySQL, Mongo, Redis etc inside pre-configured containers using simple one-liners. 4 | 5 | Running containers can be accessed via their exposed ports and can be paired with any other application on your system. 6 | 7 | Starting a service such as `mysql` is as simple as executing `jeeves start mysql` and you'll never have to look back at it. 8 | 9 | But `mysql` is not the only available service. A list of all the available services can be found in the [data.py](https://github.com/fhsinchy/jeeves/blob/master/data.py) file. 10 | 11 | Jeeves is heavily inspired from [tighten/takeout](https://github.com/tighten/takeout) and [fhsinchy/tent](https://github.com/fhsinchy/tent) projects. It is an experimental project. Hence, care should be taken if you're using it in a critical environment. 12 | 13 | ## Requirements 14 | 15 | - Python 3 16 | - Docker 17 | 18 | ## Installation 19 | 20 | ```shell 21 | pip install git+https://github.com/fhsinchy/jeeves.git#egg=jeeves 22 | pip freeze 23 | ``` 24 | 25 | Output – 26 | 27 | ```shell 28 | jeeves== 29 | ``` 30 | 31 | ## Usage 32 | 33 | The `jeeves` program has following commands: 34 | 35 | * `jeeves start ` - starts a container for the given service 36 | * `jeeves stop ` - stops and removes a container for the given service 37 | * `jeeves list` - lists all running containers 38 | 39 | All the services in `jeeves` utilizes volumes for persisting data, so even if you stop a service, it's data will be persisted in a volume for later usage. These volumes can listed by executing `docker volume ls` and can be managed like any other Docker volume. 40 | 41 | ### Start a Service 42 | 43 | The generic syntax for the `start` command is as follows: 44 | 45 | ```bash 46 | jeeves start 47 | 48 | ## starts mysql and prompts you where necessary 49 | jeeves start mysql 50 | ``` 51 | 52 | ### Start Service with Default Configuration 53 | 54 | The `--default` flag for the `start` command can be used to skip all the prompts and start a service with default configuration 55 | 56 | ```bash 57 | jeeves start --default 58 | 59 | ## starts mysql with the default configuration 60 | jeeves start mysql --default 61 | ``` 62 | 63 | ### Stop a Service 64 | 65 | The generic syntax for the `stop` command is as follows: 66 | 67 | ```bash 68 | jeeves stop 69 | 70 | ## stops mysql and removes the container 71 | ## prompts you if multiple containers are found 72 | jeeves stop mysql 73 | 74 | ## stops all mysql containers and removes them 75 | jeeves stop mysql --all 76 | ``` 77 | 78 | ## Running Multiple Versions 79 | 80 | Given all the services are running inside containers, you can spin up multiple versions of the same service as long as you're keeping the port different. 81 | 82 | Run `jeeves start mysql` twice; the first time, use the `--default` flag, and the second time, put `5.7` as tag and `3307` as host port. 83 | 84 | Now, if you run `jeeves list`, you'll see both services running at the same time. 85 | 86 | ```bash 87 | CONTAINER ID CONTAINER NAME CONTAINER LABEL 88 | e26c7f47e6 priceless_euler mysql--5.7--3308 89 | 6cc3f50081 interesting_ptolemy mysql--latest--3306 90 | ``` 91 | 92 | ## Container Management 93 | 94 | Containers started by `jeeves` are regular containers with some pre-set configurations. So you can use regular `docker` commands such as `ls`, `inspect`, `logs` etc on them. Although `jeeves` comes with a `list` command, using the `docker` commands will result in more informative results. The target of `jeeves` is to provide plug and play containers, not to become a full-fledged `docker` cli. 95 | -------------------------------------------------------------------------------- /data.py: -------------------------------------------------------------------------------- 1 | services = { 2 | "mariadb": { 3 | "name": "mariadb", 4 | "image": "docker.io/mariadb", 5 | "tag": "latest", 6 | "env": { 7 | "MYSQL_ROOT_PASSWORD": "root" 8 | }, 9 | "ports": { 10 | "source": "3306/tcp", 11 | "destination": 3306 12 | }, 13 | "volumes": { 14 | "name": "mariadb-data", 15 | "destination": "/var/lib/mysql" 16 | }, 17 | "command": "" 18 | }, 19 | "mongo": { 20 | "name": "mongo", 21 | "image": "docker.io/mongo", 22 | "tag": "latest", 23 | "env": { 24 | "MONGO_INITDB_ROOT_USERNAME": "admin", 25 | "MONGO_INITDB_ROOT_PASSWORD": "admin" 26 | }, 27 | "ports": { 28 | "source": "27017/tcp", 29 | "destination": 27017 30 | }, 31 | "volumes": { 32 | "name": "mongo-data", 33 | "destination": "/data/db" 34 | }, 35 | "command": "" 36 | }, 37 | "mysql": { 38 | "name": "mysql", 39 | "image": "docker.io/mysql", 40 | "tag": "latest", 41 | "env": { 42 | "MYSQL_ROOT_PASSWORD": "root" 43 | }, 44 | "ports": { 45 | "source": "3306/tcp", 46 | "destination": 3306 47 | }, 48 | "volumes": { 49 | "name": "mysql-data", 50 | "destination": "/var/lib/mysql" 51 | }, 52 | "command": "" 53 | }, 54 | "postgres": { 55 | "name": "postgres", 56 | "image": "docker.io/postgres", 57 | "tag": "latest", 58 | "env": { 59 | "POSTGRES_PASSWORD": "postgres" 60 | }, 61 | "ports": { 62 | "source": "5432/tcp", 63 | "destination": 5432 64 | }, 65 | "volumes": { 66 | "name": "postgres-data", 67 | "destination": "/var/lib/postgresql/data" 68 | }, 69 | "command": "" 70 | }, 71 | "redis": { 72 | "name": "redis", 73 | "image": "docker.io/redis", 74 | "tag": "latest", 75 | "env": {}, 76 | "ports": { 77 | "source": "6379/tcp", 78 | "destination": 6379 79 | }, 80 | "volumes": { 81 | "name": "redis-data", 82 | "destination": "/data" 83 | }, 84 | "command": "" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /jeeves.py: -------------------------------------------------------------------------------- 1 | from docker import from_env 2 | 3 | from click import ( 4 | echo, 5 | group, 6 | prompt, 7 | option, 8 | command, 9 | argument, 10 | version_option 11 | ) 12 | 13 | from data import services 14 | 15 | 16 | def stop_container(container): 17 | container.stop() 18 | echo(f"{container.labels['jeeves']} container stopped succesfully.") 19 | 20 | 21 | def remove_container(container): 22 | container.remove() 23 | echo(f"{container.labels['jeeves']} container removed succesfully.") 24 | 25 | 26 | def get_all_containers(client): 27 | return client.containers.list(filters={ 28 | 'label': 'jeeves', 29 | }) 30 | 31 | 32 | @group() 33 | @version_option() 34 | def jeeves(): 35 | pass 36 | 37 | 38 | @command(help='Starts a new jeeves container.') 39 | @option('-d', '--default', default=False, is_flag=True) 40 | @argument('name') 41 | def start(default, name): 42 | if name in services: 43 | service = services[name] 44 | 45 | if not default: 46 | tag = prompt( 47 | "Which tag do you want to use?", type=str, default=service['tag']) 48 | if tag != '': 49 | service['tag'] = tag 50 | 51 | for key, value in service['env'].items(): 52 | user_input = prompt( 53 | f"{key}?", type=str, default=value) 54 | if user_input != '': 55 | service['env'][key] = user_input 56 | 57 | port = prompt( 58 | "Which port do you want to use?", type=int, default=service['ports']['destination']) 59 | if port != '': 60 | service['ports']['destination'] = port 61 | 62 | volume = prompt( 63 | "What would you like to call your volume?", type=str, default=service['volumes']['name']) 64 | if volume != '': 65 | service['volumes']['name'] = volume 66 | 67 | client = from_env() 68 | label = f"{service['name']}--{service['tag']}--{service['ports']['destination']}" 69 | 70 | if len(client.containers.list(filters={'label': f"jeeves={label}"})): 71 | echo('container with same attribute is already running.') 72 | elif len(client.containers.list(filters={'label': f"jeeves={label}", 'status': 'exited'})) > 0: 73 | echo( 74 | f"starting previously created {service['name']} container.") 75 | client.containers.list( 76 | filters={'label': f"jeeves={label}", 'status': 'exited'}).pop().start() 77 | else: 78 | echo( 79 | f"creating and starting a new {service['name']} container.") 80 | client.containers.run( 81 | image=f"{service['image']}:{service['tag']}", environment=service['env'], ports={ 82 | service['ports']['source']: service['ports']['destination'] 83 | }, volumes={ 84 | service['volumes']['name']: { 85 | 'bind': service['volumes']['destination'], 86 | 'mode': 'rw' 87 | } 88 | }, labels={ 89 | 'jeeves': label 90 | }, command=service['command'], detach=True) 91 | echo(f"{service['name']} started succesfully!") 92 | else: 93 | echo(f"{name} is not a valid service name.") 94 | 95 | 96 | @command(help='Stops a running jeeves container.') 97 | @option('-a', '--all', default=False, is_flag=True) 98 | @argument('name') 99 | def stop(all, name): 100 | containers = get_all_containers(from_env()) 101 | 102 | if len(containers) > 0: 103 | filtered_containers = tuple(filter( 104 | lambda container: container.labels['jeeves'].split('--')[0] == name, containers)) 105 | 106 | if len(filtered_containers) > 0: 107 | if all or len(filtered_containers) == 1: 108 | for container in filtered_containers: 109 | stop_container(container) 110 | remove_container(container) 111 | else: 112 | for index, container in enumerate(filtered_containers): 113 | echo(f"{index} --> {container.labels['jeeves']}") 114 | selected_container = prompt( 115 | f"pick the container you want to stop (0 - {len(filtered_containers) - 1})", type=int) 116 | 117 | stop_container(containers[selected_container]) 118 | remove_container(containers[selected_container]) 119 | else: 120 | echo(f"there are no {name} containers running.") 121 | else: 122 | echo('there are no containers running.') 123 | 124 | 125 | @command(help='Lists all running jeeves containers.') 126 | def list(): 127 | containers = get_all_containers(from_env()) 128 | 129 | echo("{:<15} {:<20} {:<20}".format( 130 | 'CONTAINER ID', 'CONTAINER NAME', 'CONTAINER LABEL')) 131 | for container in containers: 132 | echo("{:<15} {:<20} {:<20}".format(container.short_id, 133 | container.name, container.labels['jeeves'])) 134 | 135 | 136 | jeeves.add_command(start) 137 | jeeves.add_command(stop) 138 | jeeves.add_command(list) 139 | 140 | 141 | if __name__ == "__main__": 142 | jeeves() 143 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # pyproject.toml 2 | 3 | [build-system] 4 | requires = ["setuptools>=61.0.0", "wheel"] 5 | build-backend = "setuptools.build_meta" 6 | 7 | [project] 8 | name = "jeeves" 9 | version = "1.0.0" 10 | description = "Docker based development-only dependency manager for Windows, Linux, and macOS" 11 | readme = "README.md" 12 | authors = [{ name = "Farhan Hasin Chowdhury", email = "shovik.is.here@gmail.com" }] 13 | license = { file = "LICENSE" } 14 | classifiers = [ 15 | "License :: OSI Approved :: GPL-3.0 License", 16 | "Programming Language :: Python :: 3", 17 | ] 18 | keywords = ["docker"] 19 | dependencies = [ 20 | "click==8.0.3", 21 | "docker==4.2.2" 22 | ] 23 | requires-python = ">=3.8" 24 | 25 | [project.optional-dependencies] 26 | dev = ["pip-tools"] 27 | 28 | [project.urls] 29 | Homepage = "https://github.com/fhsinchy/jeeves" 30 | 31 | [project.scripts] 32 | jeeves = "jeeves:main" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click==8.0.3 2 | docker==4.2.2 --------------------------------------------------------------------------------