feat: Add support for COMPOSE_PROFILES environment variable

This change implements support for the COMPOSE_PROFILES environment
variable, allowing users to specify active Compose profiles through their
environment.

The behavior is as follows:
- Profiles from COMPOSE_PROFILES (comma-separated) are activated.
- Both the environment variable and the --profile CLI flag can be used
  together, with the resulting set of active profiles being the union of
  both.

Signed-off-by: Watson Dinh <ping@w47s0n.com>
This commit is contained in:
Watson Dinh
2025-07-23 15:46:32 -05:00
parent 036c0dcd18
commit 28ec08c753
3 changed files with 76 additions and 2 deletions

View File

@ -0,0 +1 @@
Support COMPOSE_PROFILES environment variable to enable profiles.

View File

@ -2231,6 +2231,11 @@ class PodmanCompose:
env_vars = norm_as_dict(args.env)
self.environ.update(env_vars) # type: ignore[arg-type]
profiles_from_env = {
p.strip() for p in self.environ.get("COMPOSE_PROFILES", "").split(",") if p.strip()
}
requested_profiles = set(args.profile).union(profiles_from_env)
compose: dict[str, Any] = {}
# Iterate over files primitively to allow appending to files in-loop
files_iter = iter(files)
@ -2296,7 +2301,7 @@ class PodmanCompose:
# Solution is to remove 'include' key from compose obj. This doesn't break
# having `include` present and correctly processed in included files
del compose["include"]
resolved_services = self._resolve_profiles(compose.get("services", {}), set(args.profile))
resolved_services = self._resolve_profiles(compose.get("services", {}), requested_profiles)
compose["services"] = resolved_services
if not getattr(args, "no_normalize", None):
compose = normalize_final(compose, self.dirname)
@ -2318,7 +2323,7 @@ class PodmanCompose:
services = {}
log.warning("WARNING: No services defined")
# include services with no profile defined or the selected profiles
services = self._resolve_profiles(services, set(args.profile))
services = self._resolve_profiles(services, requested_profiles)
# NOTE: maybe add "extends.service" to _deps at this stage
flat_deps(services, with_extends=True)

View File

@ -90,3 +90,71 @@ class TestUpDown(unittest.TestCase, RunSubprocessMixin):
actual_services[service] = service in actual_output
self.assertEqual(expected_services, actual_services)
@parameterized.expand(
[
(
["--profile", "profile-1"],
"profile-2",
{"default-service": True, "service-1": True, "service-2": True},
),
(
[],
"profile-1,profile-2",
{"default-service": True, "service-1": True, "service-2": True},
),
(
[],
"profile-1, profile-2",
{"default-service": True, "service-1": True, "service-2": True},
),
(
[],
"",
{"default-service": True, "service-1": False, "service-2": False},
),
(
[],
",",
{"default-service": True, "service-1": False, "service-2": False},
),
],
)
def test_up_with_compose_profiles_env(
self, profiles: List[str], compose_profiles: str, expected_services: dict
) -> None:
"""
Tests the `up` command when the `COMPOSE_PROFILES` environment variable is set.
"""
up_cmd = [
"coverage",
"run",
podman_compose_path(),
"-f",
profile_compose_file(),
]
up_cmd.extend(profiles)
up_cmd.extend(["up", "-d"])
env = os.environ.copy()
env["COMPOSE_PROFILES"] = compose_profiles
self.run_subprocess_assert_returncode(up_cmd, env=env)
check_cmd = [
"podman",
"container",
"ps",
"--format",
'"{{.Names}}"',
]
out, _ = self.run_subprocess_assert_returncode(check_cmd)
self.assertEqual(len(expected_services), 3)
actual_output = out.decode("utf-8")
actual_services = {}
for service, _ in expected_services.items():
actual_services[service] = service in actual_output
self.assertEqual(expected_services, actual_services)