Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding support for docker stack config #510

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,6 @@ dmypy.json
.pyre/
docs/generated_sources
docs/site

# pycharm or other jetbrains ide
.idea/
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,28 @@ docker.run(
)
```

----------


```bash
export MYVAR=somevar
source ./.env
docker stack config --compose-file docker-compose.yml,docker-compose.overrides.yml
```

becomes
```python
from python_on_whales import docker

config = docker.stack.config(
["docker-compose.yml", "docker-compose.overrides.yml"],
env_files=[".env"],
variables={"MYVAR": "somevar"}
)

for service in config.services.values():
docker.image.pull(service.image)
```

Any Docker object can be used as a context manager to ensure it's removed even if an exception occurs:

Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ dependencies = { file = ["requirements.txt"] }
test = [
"pytest",
]
yaml = [
"pyyaml>=6.0.1",
]

[project.urls]
"Source" = "https://github.com/gabrieldemarmiesse/python-on-whales"
Expand Down
60 changes: 60 additions & 0 deletions python_on_whales/components/stack/cli_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,17 @@
import python_on_whales.components.service.cli_wrapper
import python_on_whales.components.task.cli_wrapper
from python_on_whales.client_config import DockerCLICaller
from python_on_whales.components.compose.models import ComposeConfig
from python_on_whales.utils import ValidPath, read_env_files, run, to_list

try:
import yaml

_HAS_YAML = True
except ImportError:
yaml = None
_HAS_YAML = False


class Stack:
def __init__(self, client_config, name):
Expand Down Expand Up @@ -92,6 +101,57 @@ def deploy(
run(full_cmd, capture_stdout=False, env=env)
return Stack(self.client_config, name)

def config(
self,
compose_files: Union[ValidPath, List[ValidPath]],
env_files: Optional[List[ValidPath]] = None,
variables: Optional[Dict[str, str]] = None,
thosil marked this conversation as resolved.
Show resolved Hide resolved
return_json: bool = False,
) -> Union[ComposeConfig, Dict[str, Any]]:
"""Returns the final config file, after doing merges and interpolations.

Parameters:
compose_files: One or more docker-compose files.
If there is more than one, they will be merged.
env_files: Similar to `.env` files in docker-compose, load `variables` from `.env` files.
If both `env_files` and `variables` are used, `variables` have priority.
variables: A dict dictating by what to replace the variables declared in the
docker-compose files.
In the docker CLI, you would use environment variables for this.
return_json: If `False`, a `ComposeConfig` object will be returned, and you'll be able
to take advantage of your IDE autocompletion. If you want the full json output, you
may use `return_json`. In this case, you'll get lists and dicts corresponding to the
json response, unmodified. It may be useful if you just want to print the config or
want to access a field that was not in the `ComposeConfig` class.

# Returns
A `ComposeConfig` object if `return_json` is `False`, and a `dict` otherwise.

# Raises
DockerException: if there's an error in one of the compose_files.
ImportError: if module `pyyaml` is not installed.
"""
if not _HAS_YAML:
raise ImportError(
"Install yaml dependencies for this function (ex: pip install python_on_whales[yaml])"
)
env_files = [] if env_files is None else env_files
variables = {} if variables is None else variables

full_cmd = self.docker_cmd + ["stack", "config"]
thosil marked this conversation as resolved.
Show resolved Hide resolved
full_cmd.add_args_list("--compose-file", compose_files)

env = read_env_files([Path(x) for x in env_files])
env.update(variables)

result = yaml.safe_load(
run(full_cmd, capture_stdout=True, return_stderr=False, env=env)
)

if return_json:
return result
return ComposeConfig(**result)

def list(self) -> List[Stack]:
"""Returns a list of `python_on_whales.Stack`

Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ pydantic>=1.9,<3,!=2.0.*
requests
tqdm
typer>=0.4.1
typing_extensions
typing_extensions
78 changes: 77 additions & 1 deletion tests/python_on_whales/components/test_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pytest

from python_on_whales import docker
from python_on_whales.exceptions import NotASwarmManager
from python_on_whales.exceptions import DockerException, NotASwarmManager
from python_on_whales.utils import PROJECT_ROOT


Expand Down Expand Up @@ -122,3 +122,79 @@ def test_services_not_swarm_manager():
docker.stack.services("dodo")

assert "not a swarm manager" in str(e.value).lower()


def test_stack_config_missing_compose_file():
with pytest.raises(DockerException) as e:
docker.stack.config(compose_files=[])

assert "Please specify a Compose file (with --compose-file)" in str(e.value)


def test_stack_config():
config = docker.stack.config(
[PROJECT_ROOT / "tests/python_on_whales/components/test-stack-file.yml"],
)

assert "app" in config.services
assert config.services["app"].image == "swarmpit/swarmpit:latest"


def test_stack_config_return_json():
config = docker.stack.config(
[PROJECT_ROOT / "tests/python_on_whales/components/test-stack-file.yml"],
return_json=True,
)

assert "app" in config["services"]
assert config["services"]["app"]["image"] == "swarmpit/swarmpit:latest"


def test_stack_config_variables():
config = docker.stack.config(
[PROJECT_ROOT / "tests/python_on_whales/components/test-stack-file.yml"],
variables={"SOME_VARIABLE": "hello-world"},
)

agent_service = config.services["agent"]
expected = "hello-world"
assert agent_service.environment["SOME_OTHER_VARIABLE"] == expected


def test_stack_config_variables_return_json():
config = docker.stack.config(
[PROJECT_ROOT / "tests/python_on_whales/components/test-stack-file.yml"],
variables={"SOME_VARIABLE": "hello-world"},
return_json=True,
)

agent_service = config["services"]["agent"]
expected = "hello-world"
assert agent_service["environment"]["SOME_OTHER_VARIABLE"] == expected


def test_stack_config_envfiles(tmp_path: Path):
env_file = tmp_path / "some.env"
env_file.write_text('SOME_VARIABLE="--tls=true" # some var \n # some comment')
config = docker.stack.config(
[PROJECT_ROOT / "tests/python_on_whales/components/test-stack-file.yml"],
env_files=[env_file],
)

agent_service = config.services["agent"]
expected = '"--tls=true"'
assert agent_service.environment["SOME_OTHER_VARIABLE"] == expected


def test_stack_config_envfiles_return_json(tmp_path: Path):
env_file = tmp_path / "some.env"
env_file.write_text('SOME_VARIABLE="--tls=true" # some var \n # some comment')
config = docker.stack.config(
[PROJECT_ROOT / "tests/python_on_whales/components/test-stack-file.yml"],
env_files=[env_file],
return_json=True,
)

agent_service = config["services"]["agent"]
expected = '"--tls=true"'
assert agent_service["environment"]["SOME_OTHER_VARIABLE"] == expected
1 change: 1 addition & 0 deletions tests/test-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pytest==7.*
pytest-mock
pytz
pyyaml>=6.0.1
Loading