NAME

Developer::Dashboard::DockerCompose - compose resolver and launcher

SYNOPSIS

my $docker = Developer::Dashboard::DockerCompose->new(
    config  => $config,
    paths   => $paths,
    plugins => $plugins,
);

DESCRIPTION

This module resolves layered docker compose inputs into a final transparent docker compose command line and can optionally execute it.

When standard Compose files exist in the invocation directory, they are used as the local base and the command runs from that directory. At execution time, the resolver first runs docker compose config using the base and configured non-service layers, then reads the resulting services: map. That resolved service list is authoritative: CLI service names and service folders not present in the base config cannot cause overlays to be loaded. Matching service folders are then searched through home, project, and nested skill runtime layers; disabled.yml excludes a service overlay, while development.compose.yml is added only when a matching develop.yml marker exists. The resolver materializes the selected service overlays and only then runs the requested Compose operation. Dry-run output remains a non-executing preview. When no local base file exists, Compose resolves its normal working-directory config before the same service-selection step. When no local base or explicit Compose file exists but runtime service files do, those enabled files seed the first config pass to preserve ecosystem-wide auto-discovery; the emitted services map still determines the final stack. Native Compose help requests and help arguments belonging to commands nested under exec bypass materialization and pass through unchanged, because their output is not YAML config data.

Every Docker Compose operation is materialized before execution, even when resolution selected no explicit layered files: Compose first discovers and emits its effective base config from the Compose working directory, then the requested verb consumes that generated temporary file. When Compose layers are selected, the effective project directory is explicitly passed to both the merge and final command. This keeps lifecycle operations such as build, up, and down anchored to the invocation project rather than the temporary merged file. A user's explicit --project-directory takes precedence. Missing or empty values for that option, and undefined argument values, are rejected before invoking Compose. The public Docker helper executes operational requests through run_streaming, which uses the materialized merge, preserves the resolved project directory, inherits stdout and stderr, and keeps the temporary file available until the Compose child exits. This preserves live output for long operations such as build, up, and log-following commands.

Captured merge output is normalized before it becomes a temporary file: already-valid UTF-8 is preserved, isolated Windows-1252 bytes are converted to UTF-8, and undefined Windows-1252 octets become the Unicode replacement character. The temporary YAML file is written in raw mode, so every action using the materializing runner receives valid UTF-8 rather than invalid bytes inherited from one of its Compose layers.

METHODS

new, resolve, list_services, run, run_streaming

Construct, resolve, list, and optionally execute compose operations. resolve returns both the project discovery root and the effective Compose working root so nested invocation directories retain their local project file. run captures output for callers that need a result payload. run_streaming executes the operational command with inherited stdout and stderr and returns its exit code; it accepts the usual resolution arguments or a previously resolved hash under resolved. The public CLI uses this method so the materialized file remains present until Compose completes.

enable_service_development, disable_service_development

Create or remove the selected-home-runtime develop.yml marker for one isolated service. A marker in any matching service folder across active runtime layers enables development.compose.yml overlays for the service. Removing the marker removes every develop.yml file for that service across those layers. New markers use ~/.developer-dashboard when it exists (or when neither runtime name exists), and ~/.d2 only when that is the existing home runtime name. An existing compose.yml remains the base and is loaded first. If development is enabled but its file is absent, resolution continues with the base file and does not report an error.

$docker->enable_service_development( service => 'web' );
$docker->disable_service_development( service => 'web' );

disable_service, enable_service

Write and remove the disabled.yml marker for one isolated service, below the selected home runtime config/docker root. Any matching service layer containing disabled.yml disables the service; enabling removes every such marker before reporting success. New markers use the same home runtime name selection as development markers.

The service name reaches these methods straight from the command line and is therefore untrusted. It is resolved below the toggle root and any name that escapes that root is refused - both methods die rather than fall back to the unchecked path. Resolution is lexical and never consults the filesystem, so the containment decision cannot change between the check and the write or unlink that follows it. Marker removal walks only existing service folders discovered by the layered service resolver, and marker names are restricted to the two internal toggle files.

The refusal protects two distinct sinks, and the second is the one usually underestimated: disable_service creates directories and writes a file, while enable_service removes one, so an uncontained name gave arbitrary deletion of any file with that fixed leaf name, not merely arbitrary creation.

One case is deliberately outside this containment: a parent directory that is itself a symlink pointing outside the root is lexically innocent and is not rejected here. Resolving it would require consulting the filesystem and would reopen the time-of-check-to-time-of-use window this approach closes.

PURPOSE

This module resolves and runs dashboard-managed Docker Compose stacks. For real operations it resolves the base config first and selects layered config/docker service files only from Compose's resulting services map; dry-run previews can infer candidate services without starting Compose. It also exports the effective docker config root and constructs the final command. Operational CLI requests use run_streaming so layered configuration is materialized before execution and normal terminal output remains live.

WHY IT EXISTS

It exists because dashboard-specific Compose resolution has more rules than a plain passthrough wrapper: isolated service folders, disabled markers, addon/mode selection, and layered runtime lookup all need one tested owner.

WHEN TO USE

Use this file when changing compose file discovery, wrapper-only flags, service inference, environment exports such as DDDC, or the dry-run versus exec behavior of the docker helper.

HOW TO USE

Feed the parsed wrapper arguments into this module and let it return or execute the effective docker compose command. Avoid rebuilding compose discovery logic in the CLI wrapper or in project-local scripts.

WHAT USES IT

It is used by the dashboard docker compose helper, by docker-focused tests, and by developers who keep Compose stacks under .developer-dashboard/config/docker/ instead of shell aliases.

EXAMPLES

Example 1:

perl -Ilib -MDeveloper::Dashboard::DockerCompose -e 1

Do a direct compile-and-load check against the module from a source checkout.

Example 2:

prove -lv t/10-extension-action-docker.t

Run the focused regression tests that most directly exercise this module's behavior.

Example 3:

dashboard docker list --disabled

Inspect the effective disabled-service view through the same resolver rules as compose discovery.

Example 4:

HARNESS_PERL_SWITCHES=-MDevel::Cover prove -lr t

Recheck the module under the repository coverage gate rather than relying on a load-only probe.

Example 4:

prove -lr t

Put any module-level change back through the entire repository suite before release.