|
11 | 11 | from dreadnode.cli.api import create_api_client |
12 | 12 | from dreadnode.cli.platform.constants import DEFAULT_DOCKER_PROJECT_NAME, PlatformService |
13 | 13 | from dreadnode.cli.platform.schemas import LocalVersionSchema |
| 14 | +from dreadnode.cli.platform.utils.env_mgmt import read_env_file |
14 | 15 | from dreadnode.cli.platform.utils.printing import print_error, print_info, print_success |
15 | 16 |
|
16 | 17 | DockerContainerState = t.Literal[ |
17 | 18 | "running", "exited", "paused", "restarting", "removing", "created", "dead" |
18 | 19 | ] |
19 | 20 |
|
20 | 21 |
|
| 22 | +# create a DockerError exception that I can catch |
| 23 | +class DockerError(Exception): |
| 24 | + pass |
| 25 | + |
| 26 | + |
21 | 27 | class CaptureOutput(str, Enum): |
22 | 28 | TRUE = "true" |
23 | 29 | FALSE = "false" |
@@ -127,6 +133,9 @@ def _build_docker_compose_base_command( |
127 | 133 | for compose_file in compose_files: |
128 | 134 | cmds.extend(["-f", compose_file.as_posix()]) |
129 | 135 |
|
| 136 | + for profile in _get_profiles_to_enable(selected_version): |
| 137 | + cmds.extend(["--profile", profile]) |
| 138 | + |
130 | 139 | if selected_version.arg_overrides_env_file.exists(): |
131 | 140 | env_files.append(selected_version.arg_overrides_env_file) |
132 | 141 |
|
@@ -176,6 +185,61 @@ def get_required_service_names(selected_version: LocalVersionSchema) -> list[str |
176 | 185 | return [name for name, cfg in services.items() if isinstance(cfg, dict) and "x-required" in cfg] |
177 | 186 |
|
178 | 187 |
|
| 188 | +def _get_profiles_to_enable(selected_version: LocalVersionSchema) -> list[str]: |
| 189 | + """Get the list of profiles to enable based on environment variables. |
| 190 | +
|
| 191 | + If any of the `x-profile-disabled-vars` are set in the environment, |
| 192 | + the profile will be disabled. |
| 193 | +
|
| 194 | + E.g. |
| 195 | +
|
| 196 | + services: |
| 197 | + myservice: |
| 198 | + image: myimage:latest |
| 199 | + profiles: |
| 200 | + - myprofile |
| 201 | + x-profile-override-vars: |
| 202 | + - MY_SERVICE_HOST |
| 203 | +
|
| 204 | + If MY_SERVICE_HOST is set in the environment, the `myprofile` profile |
| 205 | + will NOT be excluded from the docker compose --profile <profile> cmd. |
| 206 | +
|
| 207 | + Args: |
| 208 | + selected_version: The selected version of the platform. |
| 209 | +
|
| 210 | + Returns: |
| 211 | + List of profile names to enable. |
| 212 | + """ |
| 213 | + |
| 214 | + contents: dict[str, t.Any] = yaml.safe_load(selected_version.compose_file.read_text()) |
| 215 | + services = contents.get("services", {}) or {} |
| 216 | + profiles_to_enable: set[str] = set() |
| 217 | + for service in services.values(): |
| 218 | + if not isinstance(service, dict): |
| 219 | + continue |
| 220 | + profiles = service.get("profiles", []) |
| 221 | + if not profiles or not isinstance(profiles, list): |
| 222 | + continue |
| 223 | + x_override_vars = service.get("x-profile-override-vars", []) |
| 224 | + if not x_override_vars or not isinstance(x_override_vars, list): |
| 225 | + profiles_to_enable.update(profiles) |
| 226 | + continue |
| 227 | + |
| 228 | + configuration_file = selected_version.configure_overrides_env_file |
| 229 | + overrides_file = selected_version.arg_overrides_env_file |
| 230 | + env_vars = {} |
| 231 | + if configuration_file.exists(): |
| 232 | + env_vars.update(read_env_file(configuration_file)) |
| 233 | + if overrides_file.exists(): |
| 234 | + env_vars.update(read_env_file(overrides_file)) |
| 235 | + # check if any of the override vars are set in the env |
| 236 | + if any(var in env_vars for var in x_override_vars): |
| 237 | + continue # skip enabling this profile |
| 238 | + profiles_to_enable.update(profiles) |
| 239 | + |
| 240 | + return list(profiles_to_enable) |
| 241 | + |
| 242 | + |
179 | 243 | def _run_docker_compose_command( |
180 | 244 | args: list[str], |
181 | 245 | timeout: int = 300, |
@@ -224,15 +288,15 @@ def _run_docker_compose_command( |
224 | 288 |
|
225 | 289 | except subprocess.CalledProcessError as e: |
226 | 290 | print_error(f"{cmd_str} failed with exit code {e.returncode}") |
227 | | - raise |
| 291 | + raise DockerError(f"Docker command failed: {e}") from e |
228 | 292 |
|
229 | | - except subprocess.TimeoutExpired: |
| 293 | + except subprocess.TimeoutExpired as e: |
230 | 294 | print_error(f"{cmd_str} timed out after {timeout} seconds") |
231 | | - raise |
| 295 | + raise DockerError(f"Docker command timed out after {timeout} seconds") from e |
232 | 296 |
|
233 | | - except FileNotFoundError: |
| 297 | + except FileNotFoundError as e: |
234 | 298 | print_error("Docker or docker compose not found. Please ensure Docker is installed.") |
235 | | - raise |
| 299 | + raise DockerError(f"Docker compose file not found: {e}") from e |
236 | 300 |
|
237 | 301 | return result |
238 | 302 |
|
@@ -269,7 +333,7 @@ def get_available_local_images() -> list[DockerImage]: |
269 | 333 | capture_output=True, |
270 | 334 | ) |
271 | 335 | images: list[DockerImage] = [] |
272 | | - for line in cp.stdout.splitlines()[1:]: # Skip header line |
| 336 | + for line in cp.stdout.splitlines(): |
273 | 337 | if line.strip(): |
274 | 338 | img = DockerImage.from_string(line.strip()) |
275 | 339 | images.append(img) |
|
0 commit comments