Developer-Dashboard
view release on metacpan or search on metacpan
lib/Developer/Dashboard/DockerCompose.pm view on Meta::CPAN
package Developer::Dashboard::DockerCompose;
use strict;
use warnings;
our $VERSION = '4.16';
use Capture::Tiny qw(capture);
use Cwd qw(cwd);
use File::Basename qw(dirname);
use File::Path qw(make_path);
use File::Spec;
use Developer::Dashboard::EnvLoader;
use Developer::Dashboard::JSON qw(json_encode);
# new(%args)
# Constructs the docker compose resolver and launcher.
# Input: config and paths objects.
# Output: Developer::Dashboard::DockerCompose object.
sub new {
my ( $class, %args ) = @_;
my $config = $args{config} || die 'Missing config';
my $paths = $args{paths} || die 'Missing path registry';
return bless {
config => $config,
paths => $paths,
}, $class;
}
# resolve(%args)
# Resolves the effective docker compose context and overlay stack.
# Input: optional project_root, addons, modes, services, and compose args.
# Output: hash reference describing files, env, layers, precedence, and final command.
sub resolve {
my ( $self, %args ) = @_;
my $project_root = $args{project_root} || $self->{paths}->current_project_root || cwd();
my $docker_cfg = $self->{config}->docker_config;
my $docker_root = $self->_docker_config_root;
my @passthrough = @{ $args{args} || [] };
my @compose_files = ();
my @layers;
my @base = $self->_discover_base_files($project_root);
push @compose_files, @base;
push @layers, { name => 'base', files => [@base] };
my @project_overlays = ( @{ $docker_cfg->{files} || [] }, @{ $docker_cfg->{project_overlays} || [] } );
push @compose_files, @project_overlays;
push @layers, { name => 'project', files => [@project_overlays] } if @project_overlays;
my @addons = @{ $args{addons} || [] };
my @modes = @{ $args{modes} || [] };
my @services = @{ $args{services} || [] };
my %addon_map = (
%{ $docker_cfg->{addons} || {} },
);
my %mode_map = (
%{ $docker_cfg->{modes} || {} },
);
my %service_map = (
%{ $docker_cfg->{services} || {} },
);
my @inferred_services = $self->_infer_services_from_args(
args => \@passthrough,
project_root => $project_root,
service_map => \%service_map,
);
my %service_seen;
@services = grep { !$service_seen{$_}++ } ( @services, @inferred_services );
if ( !@services ) {
my @auto_services = $self->_discover_enabled_services(
project_root => $project_root,
service_map => \%service_map,
);
@services = grep { !$service_seen{$_}++ } @auto_services;
}
my @service_files;
for my $service (@services) {
my $def = $service_map{$service};
next if ref($def) ne 'HASH';
push @service_files, @{ $def->{files} || [] } if ref( $def->{files} ) eq 'ARRAY';
}
for my $service (@services) {
push @service_files, $self->_discover_service_files(
service => $service,
project_root => $project_root,
modes => \@modes,
);
}
push @compose_files, @service_files;
push @layers, { name => 'service', files => [@service_files] } if @service_files;
my @addon_files;
for my $addon (@addons) {
my $def = $addon_map{$addon};
next if ref($def) ne 'HASH';
push @addon_files, @{ $def->{files} || [] } if ref( $def->{files} ) eq 'ARRAY';
push @modes, @{ $def->{modes} || [] } if ref( $def->{modes} ) eq 'ARRAY';
}
push @compose_files, @addon_files;
push @layers, { name => 'addon', files => [@addon_files] } if @addon_files;
my @mode_files;
for my $mode (@modes) {
my $def = $mode_map{$mode};
next if ref($def) ne 'HASH';
push @mode_files, @{ $def->{files} || [] } if ref( $def->{files} ) eq 'ARRAY';
lib/Developer/Dashboard/DockerCompose.pm view on Meta::CPAN
return File::Spec->catdir( $self->{paths}->home_runtime_root, 'config', 'docker' );
}
# _discover_service_files(%args)
# Discovers the preferred old-style isolated compose file for a named service from repo-local and global docker config roots.
# Input: service name and optional project_root.
# Output: ordered list of discovered compose file paths, preferring development.compose.yml over compose.yml per folder.
sub _discover_service_files {
my ( $self, %args ) = @_;
my $service = $args{service} || return;
my $project_root = $args{project_root} || cwd();
return if $self->_service_folder_is_disabled(
project_root => $project_root,
service => $service,
);
my @roots = $self->_service_lookup_roots(
project_root => $project_root,
service => $service,
);
my @files;
my %seen;
for my $root (@roots) {
next if !defined $root || $root eq '';
my $service_root = File::Spec->catdir( $root, $service );
next if !-d $service_root;
my $development = File::Spec->catfile( $service_root, 'development.compose.yml' );
if ( -f $development ) {
push @files, $development if !$seen{$development}++;
next;
}
my $compose = File::Spec->catfile( $service_root, 'compose.yml' );
push @files, $compose if -f $compose && !$seen{$compose}++;
}
return @files;
}
# _discover_enabled_services(%args)
# Lists isolated services that should be auto-loaded when no service is selected in the command.
# Input: project_root and optional service_map hash reference.
# Output: ordered list of auto-loaded service name strings.
sub _discover_enabled_services {
my ( $self, %args ) = @_;
my @services = $self->_discover_service_names(%args);
return grep {
!$self->_service_folder_is_disabled(
project_root => $args{project_root},
service => $_,
)
} @services;
}
# _resolve_skill_service_env(%args)
# Loads .env files from installed skill roots whose config/docker/<service>
# folder contributes one compose file to the effective service stack.
# Input: project_root and ordered service list array reference.
# Output: hash reference with loaded env file list and env overlay hash.
sub _resolve_skill_service_env {
my ( $self, %args ) = @_;
my $project_root = $args{project_root} || cwd();
my @services = @{ $args{services} || [] };
return { files => [], env => {} } if !@services;
my @skill_layers;
my %env;
my %seen;
for my $service (@services) {
for my $skill_root ( $self->_discover_service_skill_roots(
project_root => $project_root,
service => $service,
) )
{
for my $docker_var ( $self->_skill_docker_env_keys($skill_root) ) {
$env{$docker_var} = File::Spec->catdir( $skill_root, 'config', 'docker' )
if defined $docker_var && $docker_var ne '';
}
next if $seen{$skill_root}++;
push @skill_layers, $skill_root;
}
}
my $loaded = @skill_layers
? Developer::Dashboard::EnvLoader->load_skill_layers_into_hash(
base_env => { %ENV },
skill_layers => \@skill_layers,
)
: {
files => [],
env => {},
};
my %merged_env = (
%env,
%{ $loaded->{env} || {} },
);
return {
files => $loaded->{files} || [],
env => \%merged_env,
};
}
# _discover_service_skill_roots(%args)
# Resolves the installed skill roots whose config/docker/<service> folders
# contribute compose files for one effective service.
# Input: service name and optional project_root.
# Output: ordered list of participating skill root directory paths.
sub _discover_service_skill_roots {
my ( $self, %args ) = @_;
my $service = $args{service} || return;
my $project_root = $args{project_root} || cwd();
return if $self->_service_folder_is_disabled(
project_root => $project_root,
service => $service,
);
my @roots = $self->_service_lookup_roots(
project_root => $project_root,
service => $service,
( run in 1.034 second using v1.01-cache-2.11-cpan-df04353d9ac )