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 )