From e7c78c33909b430f4151cf6d71219eb4ab350d4c Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Tue, 14 Apr 2026 18:04:57 +0530 Subject: [PATCH 1/6] Use docker cli --- .github/workflows/build.yml | 5 ++++- requirements.in | 1 - requirements.txt | 1 - setup.cfg | 2 +- stackconfig/cli.py | 5 ++--- stackconfig/stackconfig.py | 43 ++++++++++++++++++++----------------- tests/test_stackconfig.py | 15 ++++++------- 7 files changed, 36 insertions(+), 36 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0f68ed9..26da174 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,13 +9,16 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.9] + python-version: [3.9, 3.11] os: [ubuntu-latest] steps: - name: Checkout uses: actions/checkout@v2 + - name: Set up Docker + uses: docker/setup-buildx-action@v3 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: diff --git a/requirements.in b/requirements.in index 3f644b8..c1a5818 100644 --- a/requirements.in +++ b/requirements.in @@ -1,5 +1,4 @@ Click -docker-compose==1.29.2 pyyaml>=5.4.1 MarkupSafe jinja2 diff --git a/requirements.txt b/requirements.txt index 1c0c2ba..c1a5818 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ Click -docker-compose>=1.26.2, <=1.29.2 pyyaml>=5.4.1 MarkupSafe jinja2 diff --git a/setup.cfg b/setup.cfg index 592a9fd..a0762c2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,5 +3,5 @@ universal = 1 [metadata] description_file = README.md -version = 1.0.2 +version = 1.1.0 diff --git a/stackconfig/cli.py b/stackconfig/cli.py index 0aa49fd..0768b64 100644 --- a/stackconfig/cli.py +++ b/stackconfig/cli.py @@ -40,8 +40,7 @@ help="Jinja2 template that needs to be a valid docker-compose file after being rendered.", default=[], ) -@click.option("--version", help="Set valid version for the final docker-compose file", default=None) -def cli(file, output, j2template=None, j2data=None, version=None): +def cli(file, output, j2template=None, j2data=None): try: jinja_files = [] file = list(set(file)) @@ -52,7 +51,7 @@ def cli(file, output, j2template=None, j2data=None, version=None): ) jinja_files = render_jijnja2_compose(list(set(j2template)), j2data) file = file + jinja_files - stack_config = StackConfigCompose(file, output, version) + stack_config = StackConfigCompose(file, output) stack_config.merge_stack_compose() print(f"INFO: The docker-compose file was saved in: {output}") except Exception as exc: diff --git a/stackconfig/stackconfig.py b/stackconfig/stackconfig.py index 8f32a32..7495d1f 100644 --- a/stackconfig/stackconfig.py +++ b/stackconfig/stackconfig.py @@ -1,30 +1,36 @@ import tempfile - -from compose.config.serialize import serialize_config -from compose.cli.command import get_config_from_options - +import subprocess from stackconfig.utils.yaml_utils import save_compose, load_compose, remove_files from stackconfig.utils.validate_compose import validate_docker_stack_compose class StackConfigCompose: - def __init__(self, files, output, version=None): + def __init__(self, files, output, **kwargs): self.files = files self.output = output - self.version = version self.compose_dict = dict() def merge_stack_compose(self): """ - Merges docker-compose files using docker-compose library + Merges docker-compose files using docker-compose CLI """ - # using docker-compose library merge process - compose_config = get_config_from_options( - ".", {"--file": self.files}, {"--no-interpolate": False} - ) - compose_config_str = serialize_config(compose_config, None, escape_dollar=True) - + # Build command: docker compose -f file1 -f file2 config + cmd = ["docker", "compose"] + for f in self.files: + cmd.extend(["-f", f]) + cmd.append("config") + cmd.append("--no-interpolate") + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + except subprocess.CalledProcessError as e: + raise Exception(e.stderr or str(e)) + compose_config_str = result.stdout # tmp file name_tmp_file = tempfile.NamedTemporaryFile().name save_compose(compose_config_str, name_tmp_file, as_text=True) @@ -32,9 +38,6 @@ def merge_stack_compose(self): remove_files(name_tmp_file) - if self.version and isinstance(self.version, str): - self.compose_dict["version"] = self.version - # validate self.remove_invalid_options() validate_docker_stack_compose(self.compose_dict) @@ -48,8 +51,8 @@ def remove_invalid_options(self): that are going to be ignored when deploying a docker stack """ - if "version" in self.compose_dict and float(self.compose_dict["version"]) >= 3: - for s, sd in self.compose_dict["services"].items(): - sd.pop("depends_on", None) - self.compose_dict["services"][s] = sd + # docker stack deploy ignores depends_on, so we need to remove it from the compose file. + for s, sd in self.compose_dict["services"].items(): + sd.pop("depends_on", None) + self.compose_dict["services"][s] = sd diff --git a/tests/test_stackconfig.py b/tests/test_stackconfig.py index ff6a8ec..175a4f6 100644 --- a/tests/test_stackconfig.py +++ b/tests/test_stackconfig.py @@ -23,7 +23,6 @@ def test_merge_compose_files(mock_success_subprocess): ["tests/example_compose.yml"] + templates, "/tmp/temp_result.yml" ) c.merge_stack_compose() - assert c.compose_dict["version"] == "3.8" assert "deploy" in c.compose_dict["services"]["ui"] assert "placement" in c.compose_dict["services"]["ui"]["deploy"] assert "max_replicas_per_node" in c.compose_dict["services"]["ui"]["deploy"]["placement"] @@ -39,28 +38,26 @@ def test_merge_compose_files_invalid_syntax(mock_success_subprocess): assert f"Please be sure the template {override_file} is valid" in str(err) -@pytest.mark.parametrize("version", [(None), ("3.9")]) -def test_merge_compose_files_invalid(version, mock_success_subprocess): +def test_merge_compose_files_invalid(): c = StackConfigCompose( - ["tests/example_compose.yml"], "/tmp/temp_result_invalid.yml", version + ["tests/example_compose.yml"], "/tmp/temp_result_invalid.yml", ) c.merge_stack_compose() - if not version: - version = "3.8" - assert c.compose_dict["version"] == version assert "depends_on" not in c.compose_dict["services"]["api"] def test_merge_compose_files_syntax_error(mock_success_subprocess): with open("/tmp/invalid-compose.yml", "+w") as file: file.writelines("{}\ntests: test_value".format("test")) + with open("/tmp/override_invalid.yml", "+w") as file: + file.writelines("{}\ntests: test_value".format("test")) with pytest.raises(Exception) as exc: c = StackConfigCompose( ["/tmp/invalid-compose.yml", "/tmp/override_invalid.yml"], "/tmp/temp_result_invalid.yml", ) c.merge_stack_compose() - assert "mapping values are not allowed here" in str(exc) + assert "mapping values are not allowed in this context" in str(exc) def test_merge_compose_files_invalid_syntax_compose_validation(mock_error_subprocess): @@ -73,4 +70,4 @@ def test_merge_compose_files_invalid_syntax_compose_validation(mock_error_subpro ["tests/example_compose.yml"] + templates, "/tmp/temp_result.yml" ) c.merge_stack_compose() - assert f"services.service_custom.deploy.replicas contains an invalid type" in str(err) + assert f"services.service_custom.deploy.replicas must be a integer" in str(err) From 11d423a7dedddebda5cece2e9ddd71b84c2a3aab Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Fri, 17 Apr 2026 16:44:18 +0530 Subject: [PATCH 2/6] Use stack config --- stackconfig/stackconfig.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/stackconfig/stackconfig.py b/stackconfig/stackconfig.py index 7495d1f..c64dcfb 100644 --- a/stackconfig/stackconfig.py +++ b/stackconfig/stackconfig.py @@ -16,11 +16,10 @@ def merge_stack_compose(self): Merges docker-compose files using docker-compose CLI """ # Build command: docker compose -f file1 -f file2 config - cmd = ["docker", "compose"] + cmd = ["docker", "stack", "config"] for f in self.files: - cmd.extend(["-f", f]) - cmd.append("config") - cmd.append("--no-interpolate") + cmd.extend(["-c", f]) + cmd.append("--skip-interpolation") try: result = subprocess.run( cmd, From 43e002e7188031895deb060282bebb8e46f9682f Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Mon, 20 Apr 2026 17:50:18 +0530 Subject: [PATCH 3/6] Add version for backward compatibility --- stackconfig/cli.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/stackconfig/cli.py b/stackconfig/cli.py index 0768b64..416c87b 100644 --- a/stackconfig/cli.py +++ b/stackconfig/cli.py @@ -40,7 +40,8 @@ help="Jinja2 template that needs to be a valid docker-compose file after being rendered.", default=[], ) -def cli(file, output, j2template=None, j2data=None): +@click.option("--version", help="Set valid version for the final docker-compose file", default=None) +def cli(file, output, j2template=None, j2data=None, version=None): try: jinja_files = [] file = list(set(file)) @@ -51,7 +52,9 @@ def cli(file, output, j2template=None, j2data=None): ) jinja_files = render_jijnja2_compose(list(set(j2template)), j2data) file = file + jinja_files - stack_config = StackConfigCompose(file, output) + # version is no longer supported by docker stack config. + # But we want to keep it for backward compatibility with the users of stackconfig but ignore it when provided. + stack_config = StackConfigCompose(file, output, version) stack_config.merge_stack_compose() print(f"INFO: The docker-compose file was saved in: {output}") except Exception as exc: From c38c0690f9f174253086f48cffae7070fc19a3dd Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Wed, 22 Apr 2026 14:34:24 +0530 Subject: [PATCH 4/6] Keep version --- stackconfig/cli.py | 2 -- stackconfig/stackconfig.py | 17 ++++++++++------- tests/test_stackconfig.py | 10 +++++++--- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/stackconfig/cli.py b/stackconfig/cli.py index 416c87b..0aa49fd 100644 --- a/stackconfig/cli.py +++ b/stackconfig/cli.py @@ -52,8 +52,6 @@ def cli(file, output, j2template=None, j2data=None, version=None): ) jinja_files = render_jijnja2_compose(list(set(j2template)), j2data) file = file + jinja_files - # version is no longer supported by docker stack config. - # But we want to keep it for backward compatibility with the users of stackconfig but ignore it when provided. stack_config = StackConfigCompose(file, output, version) stack_config.merge_stack_compose() print(f"INFO: The docker-compose file was saved in: {output}") diff --git a/stackconfig/stackconfig.py b/stackconfig/stackconfig.py index c64dcfb..dac5554 100644 --- a/stackconfig/stackconfig.py +++ b/stackconfig/stackconfig.py @@ -6,16 +6,17 @@ class StackConfigCompose: - def __init__(self, files, output, **kwargs): + def __init__(self, files, output, version=None): self.files = files self.output = output + self.version = version self.compose_dict = dict() def merge_stack_compose(self): """ Merges docker-compose files using docker-compose CLI """ - # Build command: docker compose -f file1 -f file2 config + # Build command: docker stack config -c file1 -c file2 cmd = ["docker", "stack", "config"] for f in self.files: cmd.extend(["-c", f]) @@ -37,6 +38,9 @@ def merge_stack_compose(self): remove_files(name_tmp_file) + if self.version and isinstance(self.version, str): + self.compose_dict["version"] = self.version + # validate self.remove_invalid_options() validate_docker_stack_compose(self.compose_dict) @@ -50,8 +54,7 @@ def remove_invalid_options(self): that are going to be ignored when deploying a docker stack """ - # docker stack deploy ignores depends_on, so we need to remove it from the compose file. - for s, sd in self.compose_dict["services"].items(): - sd.pop("depends_on", None) - self.compose_dict["services"][s] = sd - + if "version" in self.compose_dict and float(self.compose_dict["version"]) >= 3: + for s, sd in self.compose_dict["services"].items(): + sd.pop("depends_on", None) + self.compose_dict["services"][s] = sd diff --git a/tests/test_stackconfig.py b/tests/test_stackconfig.py index 175a4f6..a859b1f 100644 --- a/tests/test_stackconfig.py +++ b/tests/test_stackconfig.py @@ -23,6 +23,7 @@ def test_merge_compose_files(mock_success_subprocess): ["tests/example_compose.yml"] + templates, "/tmp/temp_result.yml" ) c.merge_stack_compose() + assert c.compose_dict["version"] == "3.8" assert "deploy" in c.compose_dict["services"]["ui"] assert "placement" in c.compose_dict["services"]["ui"]["deploy"] assert "max_replicas_per_node" in c.compose_dict["services"]["ui"]["deploy"]["placement"] @@ -37,12 +38,15 @@ def test_merge_compose_files_invalid_syntax(mock_success_subprocess): ) assert f"Please be sure the template {override_file} is valid" in str(err) - -def test_merge_compose_files_invalid(): +@pytest.mark.parametrize("version", [(None), ("3.9")]) +def test_merge_compose_files_invalid(version, mock_success_subprocess): c = StackConfigCompose( - ["tests/example_compose.yml"], "/tmp/temp_result_invalid.yml", + ["tests/example_compose.yml"], "/tmp/temp_result_invalid.yml", version ) c.merge_stack_compose() + if not version: + version = "3.8" + assert c.compose_dict["version"] == version assert "depends_on" not in c.compose_dict["services"]["api"] From 5dd939ddd0d7d31ee036d973478ee4f0daeeab14 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Wed, 22 Apr 2026 14:37:40 +0530 Subject: [PATCH 5/6] Undo changes --- stackconfig/stackconfig.py | 3 ++- tests/test_stackconfig.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/stackconfig/stackconfig.py b/stackconfig/stackconfig.py index dac5554..508a7cc 100644 --- a/stackconfig/stackconfig.py +++ b/stackconfig/stackconfig.py @@ -14,7 +14,7 @@ def __init__(self, files, output, version=None): def merge_stack_compose(self): """ - Merges docker-compose files using docker-compose CLI + Merges docker-compose files using docker stack config CLI """ # Build command: docker stack config -c file1 -c file2 cmd = ["docker", "stack", "config"] @@ -58,3 +58,4 @@ def remove_invalid_options(self): for s, sd in self.compose_dict["services"].items(): sd.pop("depends_on", None) self.compose_dict["services"][s] = sd + diff --git a/tests/test_stackconfig.py b/tests/test_stackconfig.py index a859b1f..d493048 100644 --- a/tests/test_stackconfig.py +++ b/tests/test_stackconfig.py @@ -38,6 +38,7 @@ def test_merge_compose_files_invalid_syntax(mock_success_subprocess): ) assert f"Please be sure the template {override_file} is valid" in str(err) + @pytest.mark.parametrize("version", [(None), ("3.9")]) def test_merge_compose_files_invalid(version, mock_success_subprocess): c = StackConfigCompose( From bd3493146c8fdda4974965ec583c08703573d577 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Wed, 22 Apr 2026 18:05:52 +0530 Subject: [PATCH 6/6] Enable interpolation --- stackconfig/stackconfig.py | 1 - 1 file changed, 1 deletion(-) diff --git a/stackconfig/stackconfig.py b/stackconfig/stackconfig.py index 508a7cc..7a4e6f0 100644 --- a/stackconfig/stackconfig.py +++ b/stackconfig/stackconfig.py @@ -20,7 +20,6 @@ def merge_stack_compose(self): cmd = ["docker", "stack", "config"] for f in self.files: cmd.extend(["-c", f]) - cmd.append("--skip-interpolation") try: result = subprocess.run( cmd,